src/app/core/user/user-details/user-details.component.ts
Reusable user account details form component.
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-user-details |
| standalone | true |
| imports |
MatFormFieldModule
MatInputModule
MatSelectModule
MatTooltipModule
MatButtonModule
MatMenuModule
FontAwesomeModule
MatDialogModule
Angulartics2Module
|
| styleUrls | ./user-details.component.scss |
| templateUrl | ./user-details.component.html |
ReactiveFormsModule
MatFormFieldModule
MatInputModule
MatSelectModule
MatTooltipModule
MatButtonModule
MatMenuModule
FontAwesomeModule
MatDialogModule
DialogCloseComponent
Angulartics2Module
EditEntityComponent
Properties |
Methods |
|
Inputs |
Outputs |
constructor()
|
| isInDialog | |
Type : boolean
|
|
Default value : !!this._dialogData || false
|
|
| isProfileMode | |
Type : boolean
|
|
Default value : false
|
|
| userAccount | |
Type : UserAccount | null
|
|
Default value : this._dialogData?.userAccount
|
|
| userAccount | |
Type : UserAccount | null
|
|
| cancel |
cancel()
|
|
Returns :
void
|
| changePassword |
changePassword()
|
|
Returns :
void
|
| Async deleteAccount |
deleteAccount()
|
|
Returns :
any
|
| editMode |
editMode()
|
|
Returns :
void
|
| Async enableAccount | ||||||
enableAccount(enabled: boolean)
|
||||||
|
Parameters :
Returns :
any
|
| getFormError |
getFormError(field: string, errorType: string)
|
|
Returns :
boolean
|
| getGlobalError |
getGlobalError()
|
|
Returns :
string | null
|
| Async resendInvitation |
resendInvitation()
|
|
Returns :
any
|
| Async save |
save()
|
|
Returns :
any
|
| creatingNewAccount |
Type : unknown
|
Default value : computed(() => {
return !this.userAccount()?.id && !this.isProfileMode();
})
|
| Protected currentUser |
Type : unknown
|
Default value : inject(CurrentUserSubject, { optional: true })
|
| form |
Type : FormGroup
|
| formDisabled |
Type : unknown
|
Default value : signal(true)
|
|
Signal tracking whether the form is disabled (view mode) or enabled (edit mode).
This is automatically updating the |
| resendingInvitation |
Type : unknown
|
Default value : signal(false)
|
|
Whether a resend invitation request is currently in progress (disables the button to prevent duplicate emails). |
| showPasswordChange |
Type : unknown
|
Default value : computed(() => this.isProfileMode())
|
| userAccountEntityTypes |
Type : unknown
|
Default value : computed(() =>
this.entityRegistry
.getEntityTypes()
.filter(({ value }) => value.enableUserAccounts)
.map(({ key }) => key),
)
|
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
input,
model,
resource,
signal,
} from "@angular/core";
import { HttpClient } from "@angular/common/http";
import {
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatSelectModule } from "@angular/material/select";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatButtonModule } from "@angular/material/button";
import { MatMenuModule } from "@angular/material/menu";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
import { Role, UserAccount } from "../user-admin-service/user-account";
import { UserAdminService } from "../user-admin-service/user-admin.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { DialogCloseComponent } from "../../common-components/dialog-close/dialog-close.component";
import { AlertService } from "../../alerts/alert.service";
import { KeycloakAuthService } from "../../session/auth/keycloak/keycloak-auth.service";
import { CurrentUserSubject } from "../../session/current-user-subject";
import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service";
import { Angulartics2Module } from "angulartics2";
import { environment } from "../../../../environments/environment";
import { hasRemoteSession } from "../../session/session-type";
import { EditEntityComponent } from "../../basic-datatypes/entity/edit-entity/edit-entity.component";
import { lastValueFrom, of, firstValueFrom } from "rxjs";
import { Entity } from "../../entity/model/entity";
import { EntityRegistry } from "../../entity/database-entity.decorator";
import { Logging } from "#src/app/core/logging/logging.service";
import { catchError, map } from "rxjs/operators";
import {
entityIdsMatch,
UserAccountActionGuardService,
} from "../user-admin-service/user-account-action-guard.service";
/**
* Options as input to the UserDetailsComponent when it is opened in a dialog.
*/
export interface UserDetailsDialogData {
userAccount: UserAccount | null;
}
/**
* Return value of the UserDetailsComponent after a user interacts with it.
*/
export interface UserDetailsAction {
type: "formCancel" | "editRequested" | "accountCreated" | "accountUpdated";
data?: any;
}
/**
* Reusable user account details form component.
*/
@UntilDestroy()
@Component({
selector: "app-user-details",
templateUrl: "./user-details.component.html",
styleUrls: ["./user-details.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatTooltipModule,
MatButtonModule,
MatMenuModule,
FontAwesomeModule,
MatDialogModule,
DialogCloseComponent,
Angulartics2Module,
EditEntityComponent,
],
})
export class UserDetailsComponent {
private fb = inject(FormBuilder);
private readonly userAdminService = inject(UserAdminService);
private readonly alertService = inject(AlertService);
private readonly http = inject(HttpClient);
private readonly _dialogData: UserDetailsDialogData = inject(
MAT_DIALOG_DATA,
{ optional: true },
);
private readonly dialogRef = inject(
MatDialogRef<UserDetailsComponent, UserDetailsAction>,
{ optional: true },
);
private readonly authService = inject(KeycloakAuthService, {
optional: true,
});
protected currentUser = inject(CurrentUserSubject, { optional: true });
private readonly accountActionGuard = inject(UserAccountActionGuardService);
private readonly confirmationDialog = inject(ConfirmationDialogService);
private readonly entityRegistry = inject(EntityRegistry);
userAccount = model<UserAccount | null>(this._dialogData?.userAccount);
isInDialog = input<boolean>(!!this._dialogData || false);
isProfileMode = input<boolean>(false);
userAccountEntityTypes = computed(() =>
this.entityRegistry
.getEntityTypes()
.filter(({ value }) => value.enableUserAccounts)
.map(({ key }) => key),
);
/**
* Signal tracking whether the form is disabled (view mode) or enabled (edit mode).
* This is automatically updating the `form.disabled` also.
*/
formDisabled = signal(true);
private readonly formDisabledEffect = effect(() => {
if (!this.form) return;
// special form rules:
if (this.isProfileMode() && !this.formDisabled()) {
this.formDisabled.set(true);
}
if (this.formDisabled()) {
this.form.disable();
} else {
this.form.enable();
}
});
showPasswordChange = computed(() => this.isProfileMode());
passwordChangeDisabled = computed(() => {
if (!this.isProfileMode()) return false;
if (!hasRemoteSession(environment.session_type)) {
return true; // Disabled in demo mode
}
if (typeof navigator !== "undefined" && !navigator.onLine) {
return true; // Disabled when offline
}
return false;
});
creatingNewAccount = computed(() => {
return !this.userAccount()?.id && !this.isProfileMode();
});
/**
* Whether the account was created but the user has not yet completed
* the invitation email (verified email + set initial password).
*/
invitationPending = computed(() => {
return (
!this.isProfileMode() &&
!this.creatingNewAccount() &&
this.userAccount()?.emailVerified === false
);
});
availableRoles = resource<Role[], unknown>({
loader: async () => {
if (this.isProfileMode()) {
return this.userAccount()?.roles ?? [];
}
try {
return await lastValueFrom(this.userAdminService.getAllRoles());
} catch (err) {
// in profile view, this may be expected
Logging.debug("Failed to load available roles:", err);
return [];
}
},
defaultValue: [],
});
form: FormGroup;
constructor() {
this.initForm();
// Add roles validation only when not in profile mode
effect(() => {
const isProfileMode = this.isProfileMode();
const rolesControl = this.form.get("roles");
if (rolesControl) {
if (isProfileMode) {
rolesControl.clearValidators();
} else {
rolesControl.setValidators([Validators.required]);
}
rolesControl.updateValueAndValidity();
}
});
// Once an account already has a linked profile, the field can no longer be cleared here
// (the control used to be readonly for this exact reason) - only re-linked to a different
// profile via `updateAccount()`. An account with no profile at all stays optional to edit,
// since #3087 (system-init assistant) relies on that being a valid, silent state.
effect(() => {
const currentUserEntityId = this.userAccount()?.userEntityId;
const profileControl = this.form.get("userEntityId");
if (profileControl) {
if (!this.creatingNewAccount() && currentUserEntityId) {
profileControl.setValidators([Validators.required]);
} else {
profileControl.clearValidators();
}
profileControl.updateValueAndValidity();
}
});
// Auto-trim whitespace from email
this.form.valueChanges.pipe(untilDestroyed(this)).subscribe((next) => {
if (next.email?.startsWith(" ") || next.email?.endsWith(" ")) {
this.form
.get("email")
?.setValue(next.email.trim(), { emitEvent: false });
}
});
effect(() => {
const user = this.userAccount();
const roleOptions = this.availableRoles.value(); // make effect dependent on roles also (which are used in updateFormFromUser)
if (user) {
this.updateFormFromUser(user);
}
});
}
private initForm() {
this.form = this.fb.group({
email: ["", [Validators.required, Validators.email]],
roles: new FormControl<Role[]>([]),
userEntityId: new FormControl<string | null>(null),
});
// Initialize form as disabled
if (!this.creatingNewAccount()) {
this.formDisabled.set(true);
} else {
this.formDisabled.set(false);
}
this.form.valueChanges.pipe(untilDestroyed(this)).subscribe(() => {
this.form.markAllAsTouched();
this.form.markAsDirty();
});
}
private updateFormFromUser(user: UserAccount) {
this.form.patchValue(
{
email: user.email,
roles: (user.roles ?? [])
.map((role) =>
this.availableRoles.value()?.find((r) => r.id === role.id),
)
.filter((role): role is Role => role !== undefined), // Filter out undefined roles
userEntityId: !user.userEntityId
? null
: user.userEntityId.includes(":")
? user.userEntityId
: "User:" + user.userEntityId,
},
{ emitEvent: false },
);
this.form.markAsPristine();
}
async save() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const formValue = this.form.getRawValue();
if (this.creatingNewAccount()) {
await this.createAccount(formValue);
} else {
await this.updateAccount(formValue);
}
}
private async createAccount(formData: Partial<UserAccount>) {
const userEntityId = formData.userEntityId;
if (!userEntityId) {
this.alertService.addDanger(
$localize`:Error message:No profile ID available for user creation`,
);
return;
}
if (!formData.email || !formData.roles) {
return;
}
const existingAccount = await firstValueFrom(
this.userAdminService.getUser(userEntityId),
).catch(() => null);
if (existingAccount) {
this.alertService.addDanger(
$localize`:Error message:A user account already exists for this profile. Each profile can only be linked to one account.`,
);
return;
}
this.userAdminService
.createUser(userEntityId, formData.email, formData.roles)
.subscribe({
next: (createdUser) => {
this.alertService.addInfo(
$localize`:Snackbar message:Account created. An email has been sent to ${formData.email}`,
);
this.formDisabled.set(true);
this.closeDialog({
type: "accountCreated",
data: {
...formData,
userEntityId: userEntityId,
enabled: true,
id: createdUser.id,
} as UserAccount,
});
},
error: (err) => {
this.alertService.addDanger(
err?.error?.message ||
err?.message ||
$localize`:Error message:Failed to create account`,
);
},
});
}
private async updateAccount(formData: Partial<UserAccount>) {
const currentUser = this.userAccount();
if (!currentUser) {
return;
}
const update: Partial<UserAccount> = {};
if (formData.email !== currentUser.email) {
update.email = formData.email;
}
if (JSON.stringify(formData.roles) !== JSON.stringify(currentUser.roles)) {
update.roles = formData.roles;
}
const profileChanged =
!!formData.userEntityId &&
!entityIdsMatch(formData.userEntityId, currentUser.userEntityId);
if (profileChanged) {
update.userEntityId = formData.userEntityId;
}
if (Object.keys(update).length === 0) {
this.closeDialog({ type: "formCancel" });
return;
}
if (
profileChanged &&
!(await this.confirmProfileChange(currentUser, formData.userEntityId))
) {
return;
}
// capture before the update overwrites `userAccount()` with the new profile
const isOwnAccountRelink =
profileChanged &&
this.accountActionGuard.isOwnAccount({
userAccountId: currentUser.id,
userEntityId: currentUser.userEntityId,
});
const result = await this.updateUserAccount(
update,
$localize`:Snackbar message:Successfully updated user`,
);
if (result && isOwnAccountRelink) {
this.alertService.addWarning(
$localize`:own account relink notice:Your account is now linked to a different profile. You need to log out and log back in for this to take effect in your current session.`,
);
}
this.formDisabled.set(true);
this.closeDialog({
type: "accountUpdated",
data: {
user: result,
},
});
}
/**
* Checks the one-account-per-profile rule and asks the admin to confirm re-linking the
* account, since the previous profile record stays in the database but stops being anyone's
* login profile.
*/
private async confirmProfileChange(
currentUser: UserAccount,
newEntityId: string,
): Promise<boolean> {
let conflictingAccount: UserAccount | null;
try {
// getUser resolves to null when no account is linked to that profile,
// and only throws when the lookup itself failed
conflictingAccount = await firstValueFrom(
this.userAdminService.getUser(newEntityId),
);
} catch (error) {
// a failed lookup is not evidence that the profile is free - refuse the change rather
// than risk linking a second account to a profile that already has one
Logging.error("Failed to check for an existing user account", error);
this.alertService.addDanger(
$localize`:Error message:Could not check whether this profile already has a user account. Please try again.`,
);
return false;
}
// getUser looks up by the new profile's exact_username, so on an unchanged save it resolves
// back to this very account - only a *different* account's id is an actual conflict.
if (conflictingAccount && conflictingAccount.id !== currentUser.id) {
this.alertService.addDanger(
$localize`:Error message:A user account already exists for this profile. Each profile can only be linked to one account.`,
);
return false;
}
const confirmed = await this.confirmationDialog.getConfirmation(
$localize`:confirm changing linked profile title:Change linked profile?`,
$localize`:confirm changing linked profile message:This account will be linked to the newly selected profile record instead. The previous profile record stays in the database, but will no longer be linked to a login account. Permission rules based on this user's identity, as well as records created, updated or assigned to this user going forward, will use the new profile record instead.`,
);
return confirmed === true;
}
private async updateUserAccount(
update: Partial<UserAccount>,
message: string,
): Promise<UserAccount | null> {
const currentUser = this.userAccount();
if (!currentUser) {
return null;
}
return lastValueFrom(
this.userAdminService.updateUser(currentUser.id, update).pipe(
map((result) => {
if (!result.userUpdated) {
Logging.warn("User account not updated");
return null;
}
this.alertService.addInfo(message);
const updatedUser = { ...currentUser, ...update };
this.userAccount.set(updatedUser);
if (update.roles?.length > 0 || update.userEntityId) {
// roles and the linked profile both affect which documents the permission backend
// replicates to this user (rules keyed on `${user.roles}` / `${user.entityId}`)
this.triggerSyncReset();
}
return updatedUser;
}),
catchError((error) => {
this.alertService.addDanger(
error?.error?.message ||
error?.message ||
$localize`:Error message:Failed to update user account`,
);
Logging.error("Failed to update user account", error);
return of(null);
}),
),
);
}
async deleteAccount() {
const currentAccount = this.userAccount();
if (!currentAccount?.userEntityId) {
return;
}
if (
this.accountActionGuard.isOwnAccount({
userAccountId: currentAccount.id,
userEntityId: currentAccount.userEntityId,
})
) {
await this.accountActionGuard.showSelfAccountActionBlockedWarning(
"delete",
);
return;
}
const confirmed = await this.confirmationDialog.getConfirmation(
$localize`:delete account confirmation title:Delete user account?`,
$localize`:delete account confirmation dialog:Are you sure you want to permanently delete this user account? This cannot be undone.`,
);
if (!confirmed) return;
try {
await firstValueFrom(
this.userAdminService.deleteUser(currentAccount.userEntityId),
);
this.alertService.addInfo(
$localize`:delete account success message:User account has been deleted.`,
);
this.userAccount.set(null);
} catch (err) {
this.alertService.addDanger(
err?.["error"]?.message ||
err?.["message"] ||
$localize`:Error message:Failed to delete user account`,
);
}
}
async enableAccount(enabled: boolean) {
if (
!enabled &&
this.accountActionGuard.isOwnAccount({
userAccountId: this.userAccount()?.id,
userEntityId: this.userAccount()?.userEntityId,
})
) {
await this.accountActionGuard.showSelfAccountActionBlockedWarning(
"deactivate",
);
return;
}
const message = enabled
? $localize`:Snackbar message:Account has been activated, user can login again.`
: $localize`:Snackbar message:Account has been disabled, user will not be able to login anymore.`;
await this.updateUserAccount({ enabled }, message);
}
/**
* Whether a resend invitation request is currently in progress
* (disables the button to prevent duplicate emails).
*/
resendingInvitation = signal(false);
async resendInvitation() {
const currentUser = this.userAccount();
if (!currentUser?.id || this.resendingInvitation()) {
return;
}
this.resendingInvitation.set(true);
try {
await firstValueFrom(
this.userAdminService.resendInvitation(currentUser.id),
);
this.alertService.addInfo(
$localize`:Snackbar message:Invitation email has been sent to ${currentUser.email}`,
);
} catch (err) {
this.alertService.addDanger(
err?.["error"]?.message ||
err?.["message"] ||
$localize`:Error message:Failed to send invitation email`,
);
} finally {
this.resendingInvitation.set(false);
}
}
editMode() {
this.formDisabled.set(false);
}
cancel() {
this.form.reset();
const user = this.userAccount();
if (user) {
this.updateFormFromUser(user);
}
this.formDisabled.set(true);
this.closeDialog({ type: "formCancel" });
}
private closeDialog(result: UserDetailsAction) {
if (this.dialogRef) {
this.dialogRef.close(result);
}
}
getFormError(field: string, errorType: string): boolean {
return this.form.get(field)?.hasError(errorType) ?? false;
}
getGlobalError(): string | null {
return this.form.getError("failed");
}
changePassword() {
if (this.authService) {
this.authService.changePassword();
}
}
/**
* Reset server DB sync state to ensure previously hidden docs are re-synced
* after an account has gained more access permissions.
*
* see https://github.com/Aam-Digital/replication-backend/blob/master/src/admin/admin.controller.ts
* @private
*/
private triggerSyncReset() {
this.http
.post(
`${environment.DB_PROXY_PREFIX}/admin/clear_local/${Entity.DATABASE}`,
undefined,
)
.subscribe({
next: () => undefined,
// request fails if no permission backend is used - this is fine
error: () => undefined,
});
}
}
@if (isInDialog()) {
<div>
@if (creatingNewAccount()) {
<h2 mat-dialog-title i18n>Create User Account</h2>
} @else {
<h2 mat-dialog-title i18n>Edit User Account</h2>
}
<app-dialog-close></app-dialog-close>
</div>
}
<div class="flex-column padding-right-regular padding-left-regular">
@if (!isProfileMode()) {
<!-- Creating a new account -->
@if (creatingNewAccount()) {
<div class="align-self-start padding-bottom-small">
@if (userAccount()?.userEntityId) {
<p i18n class="field-hint field-warning">
There is no user account set up for this user yet. Enable this user
to log into the app by filling the details below.
</p>
}
@if (!formDisabled()) {
<button
mat-raised-button
type="submit"
(click)="save()"
class="invite-button"
color="accent"
[class.invite-button-animate]="true"
i18n
>
Create account & send invitation
</button>
}
</div>
} @else {
<!-- Actions editing existing account -->
<div class="user-account-actions padding-bottom-small padding-top-small">
@if (formDisabled()) {
<button
mat-raised-button
class="action-button"
(click)="editMode()"
i18n="Edit button for forms"
>
Edit
</button>
@if (invitationPending()) {
<button
mat-stroked-button
class="action-button"
(click)="resendInvitation()"
[disabled]="resendingInvitation()"
i18n="button to resend the account invitation email"
>
Resend invitation
</button>
}
} @else {
@if (userAccount()?.enabled) {
<button
mat-stroked-button
class="action-button"
(click)="enableAccount(false)"
i18n="button to disable a user account"
>
Deactivate account
</button>
} @else {
<button
mat-stroked-button
class="action-button"
(click)="enableAccount(true)"
i18n="button to enable a user account"
>
Activate account
</button>
}
<button
mat-raised-button
color="accent"
class="action-button"
[disabled]="!form.valid || !form.dirty"
(click)="save()"
i18n="Save button for forms"
>
Save
</button>
<button
mat-stroked-button
class="action-button"
(click)="cancel()"
i18n="Cancel button for forms"
>
Cancel
</button>
<!-- Three-dots overflow menu for secondary/destructive actions -->
<button
mat-icon-button
color="warn"
class="more-actions-button"
[matMenuTriggerFor]="userAccountActionsMenu"
matTooltip="More actions"
i18n-matTooltip
>
<fa-icon icon="ellipsis-v" class="standard-icon"></fa-icon>
</button>
<mat-menu #userAccountActionsMenu>
<button
mat-menu-item
(click)="deleteAccount()"
i18n="button to delete user account"
>
Delete account
</button>
</mat-menu>
}
</div>
}
}
@if (userAccount() && !userAccount()?.enabled) {
<div class="flex-row align-center gap-small margin-bottom-regular">
<fa-icon class="color-error" icon="triangle-exclamation"></fa-icon>
<span i18n="Hint in user account page" class="color-error">
User is currently disabled and will not be able to login to the app
</span>
</div>
}
@if (invitationPending()) {
<div class="flex-row align-center gap-small margin-bottom-regular">
<fa-icon class="color-error" icon="triangle-exclamation"></fa-icon>
<span i18n="Hint in user account page" class="color-error">
User has not yet accepted the invitation email and cannot log in yet.
</span>
</div>
}
<div class="user-details-form">
<form [formGroup]="form">
<mat-form-field class="full-width">
<mat-label i18n="label of email input">Email</mat-label>
<input matInput type="text" formControlName="email" />
@if (getFormError("email", "email")) {
<mat-error i18n> Please enter a valid email</mat-error>
}
@if (getFormError("email", "required")) {
<mat-error i18n> This field is required</mat-error>
}
@if (isInDialog() && userAccount()?.userEntityId) {
<mat-hint i18n="hint showing username/entity ID in dialog">
Username: {{ userAccount()?.userEntityId }}
</mat-hint>
}
@if (isProfileMode()) {
<mat-hint i18n>
Changing your email is temporarily only possible for administrators.
Please contact your project coordinator or the support team.
</mat-hint>
}
</mat-form-field>
<mat-form-field class="full-width">
<mat-label i18n="label of roles input">Roles</mat-label>
<mat-select formControlName="roles" multiple>
<mat-select-trigger>
@for (role of form.get("roles")?.value; track role.id) {
<span>{{ role.name }} </span>
}
</mat-select-trigger>
@for (role of availableRoles.value(); track role.id) {
<mat-option [value]="role" [matTooltip]="role.description">
{{ role.description }}
<em class="role-name">{{ role.name }}</em>
</mat-option>
}
</mat-select>
@if (getFormError("roles", "required")) {
<mat-error i18n> This field is required</mat-error>
}
@if (!isInDialog()) {
<mat-hint i18n>
You should select at least one user role. Otherwise this user will
not even be able to access the basic app layout like the menu.
</mat-hint>
}
</mat-form-field>
@if (showPasswordChange()) {
<div class="field-row">
<button
mat-stroked-button
type="button"
color="primary"
(click)="changePassword()"
angulartics2On="click"
angularticsCategory="User"
angularticsAction="password_update"
[disabled]="passwordChangeDisabled()"
i18n="button to change password"
>
Change Password
</button>
</div>
}
<mat-form-field class="full-width" floatLabel="always">
<mat-label i18n>Profile</mat-label>
<app-edit-entity
formControlName="userEntityId"
[entityType]="userAccountEntityTypes()"
></app-edit-entity>
@if (getFormError("userEntityId", "required")) {
<mat-error i18n>This field is required</mat-error>
}
@if (!creatingNewAccount() && !formDisabled()) {
<mat-hint
i18n="
hint explaining the consequence of changing the linked profile
"
>
Changing this links the account to a different profile record. The
previous record stays in the database but will no longer have a
login account.
</mat-hint>
}
</mat-form-field>
@if (getGlobalError()) {
<mat-error class="global-error">{{ getGlobalError() }}</mat-error>
}
</form>
</div>
</div>
./user-details.component.scss
@use "variables/colors";
.user-details-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.user-account-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
flex-wrap: wrap;
}
.more-actions-button {
align-self: center;
}
.field-row {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem 0;
}
.field-label {
font-weight: 500;
color: colors.$muted;
}
.field-value {
color: colors.$primary;
}
.role-name {
font-size: 0.75rem;
margin-left: 0.5rem;
}
.global-error {
margin-top: 1rem;
padding: 0.5rem;
background-color: rgba(244, 67, 54, 0.1);
border-radius: 4px;
}