Browse Source

Feature/improve profile page (#828)

* * Better design
* Open profile in normal link.
* Fix github email scope.
* No Email merging.

* Remove unused directly and fix a second link to the profile.
pull/831/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
807c6a93fc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      backend/i18n/source/backend_en.json
  2. 3
      backend/src/Squidex.Shared/Texts.it.resx
  3. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  4. 5
      backend/src/Squidex.Shared/Texts.resx
  5. 3
      backend/src/Squidex.Shared/Texts.zh.resx
  6. 69
      backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  7. 10
      backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs
  8. 48
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  9. 18
      backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  10. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml
  11. 1
      backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs
  12. 2
      backend/src/Squidex/wwwroot/scripts/editor-references.html
  13. 2
      frontend/src/app/framework/angular/external-link.directive.ts
  14. 23
      frontend/src/app/framework/angular/popup-link.directive.ts
  15. 1
      frontend/src/app/framework/declarations.ts
  16. 4
      frontend/src/app/framework/module.ts
  17. 4
      frontend/src/app/shell/pages/internal/profile-menu.component.html
  18. 37
      frontend/src/app/theme/_static.scss

3
backend/i18n/source/backend_en.json

@ -190,7 +190,7 @@
"contents.workflowErrorUpdate": "The workflow does not allow updates at status {status}",
"dotnet_identity_DefaultEror": "An unknown failure has occurred.",
"dotnet_identity_DuplicateEmail": "Email is already taken.",
"dotnet_identity_DuplicateUserName": "User name is already taken.",
"dotnet_identity_DuplicateUserName": "Email is already taken by another user. You can add this external login to your account if you go to the profile page.",
"dotnet_identity_InvalidEmail": "Email is invalid.",
"dotnet_identity_InvalidUserName": "User name '{0}' is invalid, can only contain letters or digits.",
"dotnet_identity_LoginAlreadyAssociated": "A user with this login already exists.",
@ -250,6 +250,7 @@
"history.schemas.updated": "updated schema {[Name]}.",
"history.statusChanged": "changed status of {[Schema]} content to {[Status]}.",
"login.githubPrivateEmail": "Your email address is set to private in Github. Please set it to public to use Github login.",
"login.noEmailAddress": "We cannot get the email address from authentication provider.",
"rules.ruleAlreadyRunning": "Another rule is already running.",
"schemas.dateTimeCalculatedDefaultAndDefaultError": "Calculated default value and default value cannot be used together.",
"schemas.duplicateFieldName": "Field '{field}' has been added twice.",

3
backend/src/Squidex.Shared/Texts.it.resx

@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Il tuo indirizzo email è impostato su privato in Github. Impostalo come pubblico per poter utilizzare il login Github.</value>
</data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>È in esecuzione un'altra regola.</value>
</data>

3
backend/src/Squidex.Shared/Texts.nl.resx

@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Jouw e-mailadres is ingesteld op privé in Github. Stel het in op openbaar om Github-login te gebruiken.</value>
</data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>Er wordt al een andere regel uitgevoerd.</value>
</data>

5
backend/src/Squidex.Shared/Texts.resx

@ -656,7 +656,7 @@
<value>Email is already taken.</value>
</data>
<data name="dotnet_identity_DuplicateUserName" xml:space="preserve">
<value>User name is already taken.</value>
<value>Email is already taken by another user. You can add this external login to your account if you go to the profile page.</value>
</data>
<data name="dotnet_identity_InvalidEmail" xml:space="preserve">
<value>Email is invalid.</value>
@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>Your email address is set to private in Github. Please set it to public to use Github login.</value>
</data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>Another rule is already running.</value>
</data>

3
backend/src/Squidex.Shared/Texts.zh.resx

@ -835,6 +835,9 @@
<data name="login.githubPrivateEmail" xml:space="preserve">
<value>您的邮箱在 Github 中设置为私有。请设置为公开以使用 Github 登录。</value>
</data>
<data name="login.noEmailAddress" xml:space="preserve">
<value>We cannot get the email address from authentication provider.</value>
</data>
<data name="rules.ruleAlreadyRunning" xml:space="preserve">
<value>另一个规则已经在运行。</value>
</data>

69
backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -181,11 +181,10 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{
var provider = externalProviders[0].AuthenticationScheme;
var properties =
SignInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
var challengeRedirectUrl = Url.Action(nameof(ExternalCallback));
var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl);
return Challenge(properties, provider);
return Challenge(challengeProperties, provider);
}
var vm = new LoginVM
@ -204,25 +203,24 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
[Route("account/external/")]
public IActionResult External(string provider, string? returnUrl = null)
{
var properties =
SignInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(ExternalCallback), new { ReturnUrl = returnUrl }));
var challengeRedirectUrl = Url.Action(nameof(ExternalCallback), new { returnUrl });
var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, challengeRedirectUrl);
return Challenge(properties, provider);
return Challenge(challengeProperties, provider);
}
[HttpGet]
[Route("account/external-callback/")]
public async Task<IActionResult> ExternalCallback(string? returnUrl = null)
{
var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync();
var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync();
if (externalLogin == null)
if (login == null)
{
return RedirectToAction(nameof(Login));
}
var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true);
var result = await SignInManager.ExternalLoginSignInAsync(login.LoginProvider, login.ProviderKey, true);
if (!result.Succeeded && result.IsLockedOut)
{
@ -235,42 +233,33 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
if (isLoggedIn)
{
user = await userService.FindByLoginAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, HttpContext.RequestAborted);
user = await userService.FindByLoginAsync(login.LoginProvider, login.ProviderKey, HttpContext.RequestAborted);
}
else
{
var email = externalLogin.Principal.GetEmail();
var email = login.Principal.GetEmail();
if (string.IsNullOrWhiteSpace(email))
{
throw new DomainException("User has no exposed email address.");
throw new DomainException(T.Get("login.noEmailAddress"));
}
user = await userService.FindByEmailAsync(email, HttpContext.RequestAborted);
if (user != null)
{
var update = CreateUserValues(externalLogin, email, user);
await userService.UpdateAsync(user.Id, update, ct: HttpContext.RequestAborted);
}
else
var values = new UserValues
{
var update = CreateUserValues(externalLogin, email);
CustomClaims = login.Principal.Claims.GetSquidexClaims().ToList()
};
user = await userService.CreateAsync(email, update, identityOptions.LockAutomatically, HttpContext.RequestAborted);
}
user = await userService.CreateAsync(email, values, identityOptions.LockAutomatically, HttpContext.RequestAborted);
await userService.AddLoginAsync(user.Id, externalLogin, HttpContext.RequestAborted);
await userService.AddLoginAsync(user.Id, login,
HttpContext.RequestAborted);
var (success, locked) = await LoginAsync(externalLogin);
(isLoggedIn, var locked) = await LoginAsync(login);
if (locked)
{
return View(nameof(LockedOut));
}
isLoggedIn = success;
}
if (!isLoggedIn)
@ -287,26 +276,6 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
}
}
private static UserValues CreateUserValues(ExternalLoginInfo externalLogin, string email, IUser? user = null)
{
var values = new UserValues
{
CustomClaims = externalLogin.Principal.Claims.GetSquidexClaims().ToList()
};
if (user != null && !user.Claims.HasPictureUrl())
{
values.PictureUrl = GravatarHelper.CreatePictureUrl(email);
}
if (user != null && !user.Claims.HasDisplayName())
{
values.DisplayName = email;
}
return values;
}
private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin)
{
var result = await SignInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true);

10
backend/src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs

@ -16,23 +16,23 @@ namespace Squidex.Areas.IdentityServer.Controllers
{
public static async Task<ExternalLoginInfo> GetExternalLoginInfoWithDisplayNameAsync(this SignInManager<IdentityUser> signInManager, string? expectedXsrf = null)
{
var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf);
var login = await signInManager.GetExternalLoginInfoAsync(expectedXsrf);
if (externalLogin == null)
if (login == null)
{
throw new InvalidOperationException("Request from external provider cannot be handled.");
}
var email = externalLogin.Principal.GetEmail();
var email = login.Principal.GetEmail();
if (string.IsNullOrWhiteSpace(email))
{
throw new InvalidOperationException("External provider does not provide email claim.");
}
externalLogin.ProviderDisplayName = email;
login.ProviderDisplayName = email;
return externalLogin;
return login;
}
public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IdentityUser> signInManager)

48
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -59,18 +59,19 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
var properties =
SignInManager.ConfigureExternalAuthenticationProperties(provider,
Url.Action(nameof(AddLoginCallback)), userService.GetUserId(User, HttpContext.RequestAborted));
var userId = userService.GetUserId(User, HttpContext.RequestAborted);
return Challenge(properties, provider);
var challengeRedirectUrl = Url.Action(nameof(AddLoginCallback));
var challengeProperties = SignInManager.ConfigureExternalAuthenticationProperties(provider, userId);
return Challenge(challengeProperties, provider);
}
[HttpGet]
[Route("/account/profile/login-add-callback/")]
public Task<IActionResult> AddLoginCallback()
{
return MakeChangeAsync(u => AddLoginAsync(u),
return MakeChangeAsync((id, ct) => AddLoginAsync(id, ct),
T.Get("users.profile.addLoginDone"), None.Value);
}
@ -78,7 +79,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/update/")]
public Task<IActionResult> UpdateProfile(ChangeProfileModel model)
{
return MakeChangeAsync(id => userService.UpdateAsync(id, model.ToValues(), ct: HttpContext.RequestAborted),
return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct),
T.Get("users.profile.updateProfileDone"), model);
}
@ -86,7 +87,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/properties/")]
public Task<IActionResult> UpdateProperties(ChangePropertiesModel model)
{
return MakeChangeAsync(id => userService.UpdateAsync(id, model.ToValues(), ct: HttpContext.RequestAborted),
return MakeChangeAsync((id, ct) => userService.UpdateAsync(id, model.ToValues(), ct: ct),
T.Get("users.profile.updatePropertiesDone"), model);
}
@ -94,7 +95,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/login-remove/")]
public Task<IActionResult> RemoveLogin(RemoveLoginModel model)
{
return MakeChangeAsync(id => userService.RemoveLoginAsync(id, model.LoginProvider, model.ProviderKey, HttpContext.RequestAborted),
return MakeChangeAsync((id, ct) => userService.RemoveLoginAsync(id, model.LoginProvider, model.ProviderKey, ct),
T.Get("users.profile.removeLoginDone"), model);
}
@ -102,7 +103,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-set/")]
public Task<IActionResult> SetPassword(SetPasswordModel model)
{
return MakeChangeAsync(id => userService.SetPasswordAsync(id, model.Password, ct: HttpContext.RequestAborted),
return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, ct: ct),
T.Get("users.profile.setPasswordDone"), model);
}
@ -110,7 +111,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/password-change/")]
public Task<IActionResult> ChangePassword(ChangePasswordModel model)
{
return MakeChangeAsync(id => userService.SetPasswordAsync(id, model.Password, model.OldPassword, HttpContext.RequestAborted),
return MakeChangeAsync((id, ct) => userService.SetPasswordAsync(id, model.Password, model.OldPassword, ct),
T.Get("users.profile.changePasswordDone"), model);
}
@ -118,7 +119,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/generate-client-secret/")]
public Task<IActionResult> GenerateClientSecret()
{
return MakeChangeAsync(id => GenerateClientSecretAsync(id),
return MakeChangeAsync((id, ct) => GenerateClientSecretAsync(id, ct),
T.Get("users.profile.generateClientDone"), None.Value);
}
@ -126,39 +127,42 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/upload-picture/")]
public Task<IActionResult> UploadPicture(List<IFormFile> file)
{
return MakeChangeAsync(user => UpdatePictureAsync(file, user),
return MakeChangeAsync((id, ct) => UpdatePictureAsync(file, id, ct),
T.Get("users.profile.uploadPictureDone"), None.Value);
}
private async Task GenerateClientSecretAsync(string id)
private async Task GenerateClientSecretAsync(string id,
CancellationToken ct)
{
var update = new UserValues { ClientSecret = RandomHash.New() };
await userService.UpdateAsync(id, update, ct: HttpContext.RequestAborted);
await userService.UpdateAsync(id, update, ct: ct);
}
private async Task AddLoginAsync(string id)
private async Task AddLoginAsync(string id,
CancellationToken ct)
{
var externalLogin = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id);
var login = await SignInManager.GetExternalLoginInfoWithDisplayNameAsync(id);
await userService.AddLoginAsync(id, externalLogin, HttpContext.RequestAborted);
await userService.AddLoginAsync(id, login, ct);
}
private async Task UpdatePictureAsync(List<IFormFile> files, string id)
private async Task UpdatePictureAsync(List<IFormFile> files, string id,
CancellationToken ct)
{
if (files.Count != 1)
{
throw new ValidationException(T.Get("validation.onlyOneFile"));
}
await UploadResizedAsync(files[0], id, HttpContext.RequestAborted);
await UploadResizedAsync(files[0], id, ct);
var update = new UserValues
{
PictureUrl = SquidexClaimTypes.PictureUrlStore
};
await userService.UpdateAsync(id, update, ct: HttpContext.RequestAborted);
await userService.UpdateAsync(id, update, ct: ct);
}
private async Task UploadResizedAsync(IFormFile file, string id,
@ -193,7 +197,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
}
}
private async Task<IActionResult> MakeChangeAsync<TModel>(Func<string, Task> action, string successMessage, TModel? model = null) where TModel : class
private async Task<IActionResult> MakeChangeAsync<TModel>(Func<string, CancellationToken, Task> action, string successMessage, TModel? model = null) where TModel : class
{
var user = await userService.GetAsync(User, HttpContext.RequestAborted);
@ -210,7 +214,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
string errorMessage;
try
{
await action(user.Id);
await action(user.Id, HttpContext.RequestAborted);
await SignInManager.SignInAsync((IdentityUser)user.Identity, true);

18
backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -8,7 +8,7 @@
@if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span class="errors">Html.ValidationMessage(field)</span>
<span class="errors">@Html.ValidationMessage(field)</span>
</div>
}
}
@ -77,6 +77,8 @@
@if (Model!.ExternalProviders.Any())
{
<hr />
<div class="profile-section">
<h2>@T.Get("users.profile.loginsTitle")</h2>
@ -127,6 +129,8 @@
@if (Model!.HasPasswordAuth)
{
<hr />
<div class="profile-section">
<h2>@T.Get("users.profile.passwordTitle")</h2>
@ -189,19 +193,21 @@
</div>
}
<hr />
<div class="profile-section">
<h2>@T.Get("users.profile.clientTitle")</h2>
<small class="form-text text-muted mt-2 mb-2">@T.Get("users.profile.clientHint")</small>
<div class="row no-gutters form-group">
<div class="row g-2 form-group">
<div class="col-8">
<label for="clientId">@T.Get("common.clientId")</label>
<input class="form-control" name="clientId" id="clientId" readonly value="@Model!.Id" />
</div>
</div>
<div class="row no-gutters form-group">
<div class="row g-2 form-group">
<div class="col-8">
<label for="clientSecret">@T.Get("common.clientSecret")</label>
@ -217,6 +223,8 @@
</div>
</div>
<hr />
<div class="profile-section">
<h2>@T.Get("users.profile.propertiesTitle")</h2>
@ -226,7 +234,7 @@
<div class="mb-2" id="properties">
@for (var i = 0; i < Model!.Properties.Count; i++)
{
<div class="row no-gutters form-group">
<div class="row g-2 form-group">
<div class="col-5 pr-2">
@{ RenderValidation($"Properties[{i}].Name"); }
@ -299,7 +307,7 @@
var template = document.createElement('template');
template.innerHTML =
`<div class="row no-gutters form-group">
`<div class="row g-2 form-group">
<div class="col-5 pr-2">
<input class="form-control" />
</div>

2
backend/src/Squidex/Areas/IdentityServer/Views/Setup/Setup.cshtml

@ -8,7 +8,7 @@
@if (ViewContext.ViewData.ModelState[field]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
<div class="errors-container">
<span class="errors">Html.ValidationMessage(field)</span>
<span class="errors">@Html.ValidationMessage(field)</span>
</div>
}
}

1
backend/src/Squidex/Config/Authentication/GithubAuthenticationServices.cs

@ -20,6 +20,7 @@ namespace Squidex.Config.Authentication
options.ClientId = identityOptions.GithubClient;
options.ClientSecret = identityOptions.GithubSecret;
options.Events = new GithubHandler();
options.Scope.Add("user:email");
});
}

2
backend/src/Squidex/wwwroot/scripts/editor-references.html

@ -19,7 +19,7 @@
<body>
<select class="form-select" id="editor">
<option></option>
</textarea>
</select>
<script>
const element = document.getElementById('editor');

2
frontend/src/app/framework/angular/external-link.directive.ts

@ -12,7 +12,7 @@ import { AfterViewInit, Directive, ElementRef, Input, Renderer2 } from '@angular
})
export class ExternalLinkDirective implements AfterViewInit {
@Input('sqxExternalLink')
public type?: string;
public type?: 'noicon' | 'default' | string;
constructor(
private readonly element: ElementRef<Element>,

23
frontend/src/app/framework/angular/popup-link.directive.ts

@ -1,23 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Directive, HostListener, Input } from '@angular/core';
@Directive({
selector: '[sqxPopupLink]',
})
export class PopupLinkDirective {
@Input('sqxPopupLink')
public url!: string;
@HostListener('click')
public onClick(): boolean {
window.open(this.url, '_target', 'location=no,toolbar=no,width=500,height=500,left=100,top=100;');
return false;
}
}

1
frontend/src/app/framework/declarations.ts

@ -65,7 +65,6 @@ export * from './angular/pipes/money.pipe';
export * from './angular/pipes/name.pipe';
export * from './angular/pipes/numbers.pipes';
export * from './angular/pipes/translate.pipe';
export * from './angular/popup-link.directive';
export * from './angular/resized.directive';
export * from './angular/routers/can-deactivate.guard';
export * from './angular/routers/parent-link.directive';

4
frontend/src/app/framework/module.ts

@ -11,7 +11,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { ColorPickerModule } from 'ngx-color-picker';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, PopupLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterceptor, CanDeactivateGuard, CheckboxGroupComponent, ClipboardService, CodeComponent, CodeEditorComponent, ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, ControlErrorsMessagesComponent, CopyDirective, DarkenPipe, DatePipe, DateTimeEditorComponent, DayOfWeekPipe, DayPipe, DialogRendererComponent, DialogService, DisplayNamePipe, DropdownComponent, DropdownMenuComponent, DurationPipe, EditableTitleComponent, ExternalLinkDirective, FileDropDirective, FileSizePipe, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, FromNowPipe, FullDateTimePipe, HighlightPipe, HoverBackgroundDirective, ImageSourceDirective, IndeterminateValueDirective, ISODatePipe, KeysPipe, KNumberPipe, LanguageSelectorComponent, LayoutComponent, LayoutContainerDirective, LightenPipe, ListViewComponent, LoadingInterceptor, LoadingService, LocalizedInputComponent, LocalStoreService, MarkdownDirective, MarkdownInlinePipe, MarkdownPipe, MessageBus, ModalDialogComponent, ModalDirective, ModalPlacementDirective, MoneyPipe, MonthPipe, OnboardingService, OnboardingTooltipComponent, PagerComponent, ParentLinkDirective, ProgressBarComponent, ResizedDirective, ResizeService, ResourceLoaderService, RootViewComponent, SafeHtmlPipe, SafeResourceUrlPipe, SafeUrlPipe, ScrollActiveDirective, ShortcutComponent, ShortcutDirective, ShortcutService, ShortDatePipe, ShortTimePipe, StarsComponent, StatusIconComponent, StopClickDirective, StopDragDirective, SyncScollingDirective, SyncWidthDirective, TabRouterlinkDirective, TagEditorComponent, TemplateWrapperDirective, TempService, TitleComponent, TitleService, ToggleComponent, ToolbarComponent, TooltipDirective, TransformInputDirective, TranslatePipe, VideoPlayerComponent } from './declarations';
@NgModule({
imports: [
@ -76,7 +76,6 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
OnboardingTooltipComponent,
PagerComponent,
ParentLinkDirective,
PopupLinkDirective,
ProgressBarComponent,
ResizedDirective,
RootViewComponent,
@ -161,7 +160,6 @@ import { AnalyticsService, AutocompleteComponent, AvatarComponent, CachingInterc
OnboardingTooltipComponent,
PagerComponent,
ParentLinkDirective,
PopupLinkDirective,
ProgressBarComponent,
ReactiveFormsModule,
ResizedDirective,

4
frontend/src/app/shell/pages/internal/profile-menu.component.html

@ -10,7 +10,7 @@
<ng-container *sqxModal="modalMenu;onRoot:false;closeAlways:true">
<sqx-dropdown-menu [sqxAnchoredTo]="button" [offsetY]="10">
<a class="dropdown-item dropdown-info" [sqxPopupLink]="snapshot.profileUrl">
<a class="dropdown-item dropdown-info" href="{{snapshot.profileUrl}}" sqxExternalLink="noicon">
<div>{{ 'profile.userEmail' | sqxTranslate }}</div>
<strong>{{snapshot.profileEmail}}</strong>
@ -22,7 +22,7 @@
{{ 'common.administration' | sqxTranslate }}
</a>
<a class="dropdown-item" [sqxPopupLink]="snapshot.profileUrl">
<a class="dropdown-item" href="{{snapshot.profileUrl}}" sqxExternalLink="noicon">
{{ 'profile.title' | sqxTranslate }}
</a>

37
frontend/src/app/theme/_static.scss

@ -48,8 +48,10 @@ noscript {
// Fix logo on the top right corner of the screen.
&-logo {
@include absolute(1rem, 1rem, auto, auto);
height: 1.75rem;
display: block;
margin-left: auto;
margin-bottom: 1rem;
height: 2.5rem;
}
&-password-signup {
@ -57,7 +59,7 @@ noscript {
}
&-section {
margin-top: 3rem;
margin-top: 2rem;
}
&-section-sm {
@ -94,7 +96,10 @@ noscript {
border-bottom: 1px solid $color-border;
margin-bottom: 2.5rem;
margin-top: 1.5rem;
margin-left: auto;
margin-right: auto;
text-align: center;
width: 7rem;
}
&-text {
@ -108,10 +113,36 @@ noscript {
}
}
h1 {
font-size: 1.75rem;
font-weight: 400;
}
hr {
margin-top: 2rem;
margin-left: auto;
margin-right: auto;
width: 7rem;
}
.card {
border-width: 1px;
}
.card-body {
padding: 1.5rem;
}
.table {
tbody {
border-top: 1px solid $color-border;
}
tr {
border-top: 0;
}
}
label {
.card {
font-weight: normal;

Loading…
Cancel
Save