From 54854ad9568fc440a5d1adb4a985381c8b07facb Mon Sep 17 00:00:00 2001 From: dvladir Date: Mon, 25 Mar 2024 11:14:48 +0300 Subject: [PATCH 1/4] SED-2978 POC: user avatars --- .../avatar-circle.component.html | 1 + .../avatar-circle.component.scss | 14 +++ .../avatar-circle/avatar-circle.component.ts | 59 +++++++++++ .../avatar-color-preference-key.token.ts | 5 + .../lib/modules/basics/step-basics.module.ts | 5 + .../color-chooser.component.html | 14 +++ .../color-chooser.component.scss | 29 +++++ .../color-chooser/color-chooser.component.ts | 59 +++++++++++ .../color-field/color-field.base.ts | 29 +++++ .../color-field/color-field.component.html | 3 + .../color-field/color-field.component.scss | 4 + .../color-field/color-field.component.ts | 65 ++++++++++++ .../color-picker-content.component.html | 7 ++ .../color-picker-content.component.scss | 3 + .../color-picker-content.component.ts | 27 +++++ .../color-picker/color-picker.component.ts | 38 +++++++ .../src/lib/modules/color-picker/index.ts | 14 +++ .../color-field-container.service.ts | 31 ++++++ .../color-picker/injectables/colors.token.ts | 67 ++++++++++++ .../injectables/random-color.token.ts | 13 +++ .../modules/color-picker/types/color-field.ts | 8 ++ .../step-core/src/lib/step-core.module.ts | 4 + .../step-core/styles/_core-variables.scss | 1 + .../styles/components/_step-header.scss | 1 + .../main-view/main-view.component.html | 4 +- .../main-view/main-view.component.scss | 10 ++ .../main-view/main-view.component.ts | 2 + .../src/lib/modules/admin/admin.module.ts | 8 ++ .../avatar-editor.component.html | 1 + .../avatar-editor.component.scss | 3 + .../avatar-editor/avatar-editor.component.ts | 44 ++++++++ .../my-account/my-account.component.html | 48 +++++---- .../my-account/my-account.component.scss | 14 ++- .../my-account/my-account.component.ts | 99 ++--------------- .../user-avatar/user-avatar.component.html | 1 + .../user-avatar/user-avatar.component.scss} | 0 .../user-avatar/user-avatar.component.ts | 29 +++++ .../injectables/screen-dialogs.service.ts | 0 .../admin/injectables/user-state.service.ts | 100 ++++++++++++++++++ 39 files changed, 743 insertions(+), 121 deletions(-) create mode 100644 projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html create mode 100644 projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.scss create mode 100644 projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts create mode 100644 projects/step-core/src/lib/modules/basics/injectables/avatar-color-preference-key.token.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.scss create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.html create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.scss create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.scss create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/index.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/injectables/colors.token.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/injectables/random-color.token.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/types/color-field.ts create mode 100644 projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html create mode 100644 projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.scss create mode 100644 projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts create mode 100644 projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.html rename projects/step-frontend/src/lib/modules/admin/{services/screen-dialogs.service.ts => components/user-avatar/user-avatar.component.scss} (100%) create mode 100644 projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts create mode 100644 projects/step-frontend/src/lib/modules/admin/injectables/screen-dialogs.service.ts create mode 100644 projects/step-frontend/src/lib/modules/admin/injectables/user-state.service.ts diff --git a/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html new file mode 100644 index 0000000000..c1fd486a2d --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html @@ -0,0 +1 @@ +
{{ initials() }}
diff --git a/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.scss b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.scss new file mode 100644 index 0000000000..45f9f8d111 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.scss @@ -0,0 +1,14 @@ +@use 'projects/step-core/styles/core-variables' as var; + +:host { + display: flex; + width: 2em; + height: 2em; + border-radius: 2em; + align-items: center; + justify-content: center; + & > div { + user-select: none; + color: var.$white; + } +} diff --git a/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts new file mode 100644 index 0000000000..d6a1848dcc --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts @@ -0,0 +1,59 @@ +import { ChangeDetectionStrategy, Component, computed, HostBinding, input, Input } from '@angular/core'; + +const DELIMITERS = new Set([' ', '_', '-', '.', '+']); +const DEFAULT_BACKGROUND = '#4b5565'; + +@Component({ + selector: 'step-avatar-circle', + templateUrl: './avatar-circle.component.html', + styleUrl: './avatar-circle.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AvatarCircleComponent { + @HostBinding('style.background-color') + @Input({ transform: (value: string | undefined | null) => value || DEFAULT_BACKGROUND }) + background = DEFAULT_BACKGROUND; + + name = input('', { + transform: (value: string | undefined | null) => value ?? '', + }); + + protected initials = computed(() => { + const name = this.name().trim(); + if (!name) { + return ''; + } + + const words = this.splitStringWithMultipleDelimiters(name); + + if (!words.length) { + return ''; + } + + if (words.length === 1) { + return words[0][0].toUpperCase(); + } + + return words[0][0].toUpperCase() + words[words.length - 1][0].toUpperCase(); + }); + + private splitStringWithMultipleDelimiters(str: string): string[] { + const parts = []; + let buf = ''; + + for (let i = 0; i < str.length; i++) { + if (DELIMITERS.has(str[i])) { + if (buf) { + parts.push(buf); + buf = ''; + } + continue; + } + buf += str[i]; + } + if (buf) { + parts.push(buf); + } + return parts; + } +} diff --git a/projects/step-core/src/lib/modules/basics/injectables/avatar-color-preference-key.token.ts b/projects/step-core/src/lib/modules/basics/injectables/avatar-color-preference-key.token.ts new file mode 100644 index 0000000000..5159e5543f --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/injectables/avatar-color-preference-key.token.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; + +export const AVATAR_COLOR_PREFERENCE_KEY = new InjectionToken('Avatar color preference key', { + factory: () => 'avatar_color', +}); diff --git a/projects/step-core/src/lib/modules/basics/step-basics.module.ts b/projects/step-core/src/lib/modules/basics/step-basics.module.ts index f6fb45e80b..3fe7928d0d 100644 --- a/projects/step-core/src/lib/modules/basics/step-basics.module.ts +++ b/projects/step-core/src/lib/modules/basics/step-basics.module.ts @@ -53,6 +53,7 @@ import { ConfirmationDialogComponent } from './components/confirmation-dialog/co import { MessagesListDialogComponent } from './components/messages-list-dialog/messages-list-dialog.component'; import { MessageDialogComponent } from './components/message-dialog/message-dialog.component'; import { ProjectNamePipe } from './pipes/project-name.pipe'; +import { AvatarCircleComponent } from './components/avatar-circle/avatar-circle.component'; @NgModule({ imports: [CommonModule, FormsModule, ReactiveFormsModule, StepMaterialModule, RouterModule], @@ -107,6 +108,7 @@ import { ProjectNamePipe } from './pipes/project-name.pipe'; MarkerComponent, AlertsContainerComponent, ProjectNamePipe, + AvatarCircleComponent, ], exports: [ CommonModule, @@ -164,6 +166,7 @@ import { ProjectNamePipe } from './pipes/project-name.pipe'; MarkerComponent, AlertsContainerComponent, ProjectNamePipe, + AvatarCircleComponent, ], }) export class StepBasicsModule {} @@ -242,6 +245,7 @@ export * from './injectables/item-hover-receiver.service'; export * from './injectables/item-hold-receiver.service'; export * from './injectables/object-utils.service'; export * from './injectables/dialogs.service'; +export * from './injectables/avatar-color-preference-key.token'; export * from './types/bulk-operation-type.enum'; export * from './types/string-array-regex'; export * from './injectables/dialog-parent.service'; @@ -261,5 +265,6 @@ export * from './components/confirmation-dialog/confirmation-dialog.component'; export * from './components/messages-list-dialog/messages-list-dialog.component'; export * from './components/message-dialog/message-dialog.component'; export * from './components/dialog-route/dialog-route.component'; +export * from './components/avatar-circle/avatar-circle.component'; export * from './types/mutable'; export * from './types/date-format.enum'; diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html new file mode 100644 index 0000000000..edc7df811b --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html @@ -0,0 +1,14 @@ +@for (color of _colors; track color) { + @if (!color) { +
+ +
+ } @else { +
+ } +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.scss b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.scss new file mode 100644 index 0000000000..e3a361f37b --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.scss @@ -0,0 +1,29 @@ +@use 'projects/step-core/styles/core-variables' as var; +@use 'projects/step-core/styles/core-mixins' as core; + +:host { + display: grid; + grid-template-columns: repeat(10, 4rem); + grid-auto-rows: 4rem; + gap: 0.1rem; + + &:not(.disabled) .color-item { + cursor: pointer; + } +} + +.color-item.selected { + border: dotted 0.2rem var.$white; +} + +.clear { + border: solid 0.2rem var.$muted; + display: flex; + align-items: center; + justify-content: center; + step-icon { + @include core.step-icon-size(6.8rem); + stroke-width: 0.5px; + color: var.$muted; + } +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts new file mode 100644 index 0000000000..8d44c4f721 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts @@ -0,0 +1,59 @@ +import { ChangeDetectionStrategy, Component, forwardRef, HostBinding, inject } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { StepBasicsModule } from '../../../basics/step-basics.module'; +import { COLORS } from '../../injectables/colors.token'; + +type OnChange = (value: string) => void; +type OnTouch = () => void; + +@Component({ + selector: 'step-color-chooser', + standalone: true, + imports: [StepBasicsModule], + templateUrl: './color-chooser.component.html', + styleUrl: './color-chooser.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorChooserComponent), + multi: true, + }, + ], +}) +export class ColorChooserComponent implements ControlValueAccessor { + protected readonly _colors = [...inject(COLORS), '']; + + protected selectedColor?: string; + + @HostBinding('class.disabled') + protected isDisabled?: boolean; + + private onChange?: OnChange; + private onTouch?: OnTouch; + + writeValue(value: string): void { + this.selectedColor = value; + } + + registerOnChange(fn: OnChange): void { + this.onChange = fn; + } + + registerOnTouched(fn: OnTouch): void { + this.onTouch = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + protected selectColor(color: string): void { + if (this.isDisabled) { + return; + } + this.selectedColor = color; + this.onChange?.(color); + this.onTouch?.(); + } +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts new file mode 100644 index 0000000000..aa4f7f6616 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts @@ -0,0 +1,29 @@ +import { ColorField } from '../../types/color-field'; +import { Directive, effect, ElementRef, inject, input, Signal } from '@angular/core'; +import { ColorPickerComponent } from '../color-picker/color-picker.component'; + +@Directive({}) +export abstract class ColorFieldBase implements ColorField { + protected _elRef = inject>(ElementRef); + + colorPicker = input(); + + private effectPickerChanged = effect(() => { + this.colorPicker()?.registerInput(this); + }); + + getConnectedOverlayOrigin(): ElementRef | undefined { + return this._elRef; + } + + abstract getModel(): string | undefined; + abstract setModel(value?: string): void; + abstract isDisabled(): boolean; + + chooseColor(): void { + if (this.isDisabled()) { + return; + } + this.colorPicker()?.open(); + } +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.html b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.html new file mode 100644 index 0000000000..5a82594900 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.html @@ -0,0 +1,3 @@ + diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.scss b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.scss new file mode 100644 index 0000000000..c866368cce --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.scss @@ -0,0 +1,4 @@ +.color-container { + width: 3.5rem; + height: 2rem; +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.ts b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.ts new file mode 100644 index 0000000000..a6828e47d1 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.component.ts @@ -0,0 +1,65 @@ +import { ColorFieldBase } from './color-field.base'; +import { ChangeDetectionStrategy, Component, forwardRef, HostListener } from '@angular/core'; +import { StepBasicsModule } from '../../../basics/step-basics.module'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +type OnChange = (value?: string) => void; +type OnTouch = () => void; + +@Component({ + selector: 'step-color-field', + templateUrl: './color-field.component.html', + styleUrl: './color-field.component.scss', + standalone: true, + imports: [StepBasicsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ColorFieldComponent), + multi: true, + }, + ], +}) +export class ColorFieldComponent extends ColorFieldBase implements ControlValueAccessor { + private onChange?: OnChange; + private onTouch?: OnTouch; + + protected model?: string; + + protected disabled = false; + + registerOnChange(fn: OnChange): void { + this.onChange = fn; + } + + registerOnTouched(fn: OnTouch): void { + this.onTouch = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(model?: string): void { + this.model = model; + } + + override getModel(): string | undefined { + return this.model; + } + + override isDisabled(): boolean { + return this.disabled; + } + + override setModel(value?: string): void { + this.model = value; + this.onChange?.(value); + } + + @HostListener('blur') + private handleBlur(): void { + this.onTouch?.(); + } +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html new file mode 100644 index 0000000000..0c66f5ee9b --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html @@ -0,0 +1,7 @@ +
+ + + + + +
diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.scss b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.scss new file mode 100644 index 0000000000..59894bab51 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.scss @@ -0,0 +1,3 @@ +step-color-picker-content mat-card { + padding: 0 !important; +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts new file mode 100644 index 0000000000..65c5d01436 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component, effect, inject, model, ViewEncapsulation } from '@angular/core'; +import { StepBasicsModule } from '../../../basics/step-basics.module'; +import { ColorChooserComponent } from '../color-chooser/color-chooser.component'; +import { ColorFieldContainerService } from '../../injectables/color-field-container.service'; +import { CdkTrapFocus } from '@angular/cdk/a11y'; + +@Component({ + selector: 'step-color-picker-content', + standalone: true, + imports: [StepBasicsModule, ColorChooserComponent, CdkTrapFocus], + templateUrl: './color-picker-content.component.html', + styleUrl: './color-picker-content.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class ColorPickerContentComponent { + private _field = inject(ColorFieldContainerService); + + protected color = model(this._field.getModel()); + + private effectColorChange = effect(() => { + const color = this.color(); + if (color !== this._field.getModel()) { + this._field.setModel(color); + } + }); +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts b/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts new file mode 100644 index 0000000000..49ddaeddec --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts @@ -0,0 +1,38 @@ +import { Component, inject, Input, ViewEncapsulation } from '@angular/core'; +import { ColorFieldContainerService } from '../../injectables/color-field-container.service'; +import { PopoverOverlayService } from '../../../basics/step-basics.module'; +import { ColorField } from '../../types/color-field'; +import { ColorPickerContentComponent } from '../color-picker-content/color-picker-content.component'; + +@Component({ + selector: 'step-color-picker', + standalone: true, + imports: [], + template: '', + encapsulation: ViewEncapsulation.None, + exportAs: 'ColorPicker', + providers: [ColorFieldContainerService, PopoverOverlayService], +}) +export class ColorPickerComponent { + private _popoverOverlay = inject(PopoverOverlayService); + private _fieldContainer = inject(ColorFieldContainerService); + + @Input() xPosition: 'start' | 'end' = 'start'; + @Input() yPosition: 'above' | 'below' = 'below'; + + registerInput(field: ColorField): void { + this._fieldContainer.registerField(field); + } + + open(): void { + const xPosition = this.xPosition; + const yPosition = this.yPosition; + this._popoverOverlay + .setPositions({ xPosition, yPosition }) + .open(ColorPickerContentComponent, this._fieldContainer.getConnectedOverlayOrigin()?.nativeElement); + } + + close(): void { + this._popoverOverlay.close(); + } +} diff --git a/projects/step-core/src/lib/modules/color-picker/index.ts b/projects/step-core/src/lib/modules/color-picker/index.ts new file mode 100644 index 0000000000..053132fa10 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/index.ts @@ -0,0 +1,14 @@ +import { ColorChooserComponent } from './components/color-chooser/color-chooser.component'; +import { ColorPickerComponent } from './components/color-picker/color-picker.component'; +import { ColorFieldComponent } from './components/color-field/color-field.component'; + +export * from './components/color-chooser/color-chooser.component'; +export * from './components/color-field/color-field.base'; +export * from './components/color-field/color-field.component'; +export * from './components/color-picker/color-picker.component'; +export * from './components/color-picker-content/color-picker-content.component'; +export * from './injectables/colors.token'; +export * from './injectables/random-color.token'; +export * from './types/color-field'; + +export const COLOR_PICKER_EXPORTS = [ColorChooserComponent, ColorPickerComponent, ColorFieldComponent]; diff --git a/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts b/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts new file mode 100644 index 0000000000..f5597be134 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts @@ -0,0 +1,31 @@ +import { ElementRef, Injectable, OnDestroy } from '@angular/core'; +import { ColorField } from '../types/color-field'; + +@Injectable() +export class ColorFieldContainerService implements ColorField, OnDestroy { + private field?: ColorField; + + registerField(field?: ColorField): void { + this.field = field; + } + + getConnectedOverlayOrigin(): ElementRef | undefined { + return this.field?.getConnectedOverlayOrigin(); + } + + getModel(): string | undefined { + return this.field?.getModel(); + } + + isDisabled(): boolean { + return this.field?.isDisabled() ?? false; + } + + setModel(value?: string): void { + this.field?.setModel(value); + } + + ngOnDestroy(): void { + this.field = undefined; + } +} diff --git a/projects/step-core/src/lib/modules/color-picker/injectables/colors.token.ts b/projects/step-core/src/lib/modules/color-picker/injectables/colors.token.ts new file mode 100644 index 0000000000..167b2b5f8b --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/injectables/colors.token.ts @@ -0,0 +1,67 @@ +import { InjectionToken } from '@angular/core'; + +const makeHSLColor = (colorI: number, totalColors: number) => { + if (totalColors < 1) { + totalColors = 1; + } + const h = (colorI * (360 / totalColors)) % 360; + const s = 0.6; + const l = 0.3; + return { h, s, l }; +}; + +const hslToRgb = (h: number, s: number, l: number) => { + let c = (1 - Math.abs(2 * l - 1)) * s, + x = c * (1 - Math.abs(((h / 60) % 2) - 1)), + m = l - c / 2, + r = 0, + g = 0, + b = 0; + + if (0 <= h && h < 60) { + r = c; + g = x; + b = 0; + } else if (60 <= h && h < 120) { + r = x; + g = c; + b = 0; + } else if (120 <= h && h < 180) { + r = 0; + g = c; + b = x; + } else if (180 <= h && h < 240) { + r = 0; + g = x; + b = c; + } else if (240 <= h && h < 300) { + r = x; + g = 0; + b = c; + } else if (300 <= h && h < 360) { + r = c; + g = 0; + b = x; + } + // Having obtained RGB, convert channels to hex + let rStr = Math.round((r + m) * 255).toString(16); + let gStr = Math.round((g + m) * 255).toString(16); + let bStr = Math.round((b + m) * 255).toString(16); + + // Prepend 0s, if necessary + if (rStr.length == 1) rStr = `0${rStr}`; + if (gStr.length == 1) gStr = `0${gStr}`; + if (bStr.length == 1) bStr = `0${bStr}`; + + return `#${rStr}${gStr}${bStr}`; +}; + +const makeColor = (colorI: number, totalColors: number) => { + const { h, s, l } = makeHSLColor(colorI, totalColors); + return hslToRgb(h, s, l); +}; + +export const COLORS = new InjectionToken('List of various colors', { + providedIn: 'root', + factory: () => new Array(120).fill(0).map((_, i, self) => makeColor(i, self.length)), +}); diff --git a/projects/step-core/src/lib/modules/color-picker/injectables/random-color.token.ts b/projects/step-core/src/lib/modules/color-picker/injectables/random-color.token.ts new file mode 100644 index 0000000000..04bad0813a --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/injectables/random-color.token.ts @@ -0,0 +1,13 @@ +import { inject, InjectionToken } from '@angular/core'; +import { COLORS } from './colors.token'; + +export const RANDOM_COLOR = new InjectionToken('Random color function', { + providedIn: 'root', + factory: () => { + const colors = inject(COLORS); + return () => { + const index = Math.floor(Math.random() * colors.length); + return colors[index]; + }; + }, +}); diff --git a/projects/step-core/src/lib/modules/color-picker/types/color-field.ts b/projects/step-core/src/lib/modules/color-picker/types/color-field.ts new file mode 100644 index 0000000000..a82d759cb6 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/types/color-field.ts @@ -0,0 +1,8 @@ +import { ElementRef } from '@angular/core'; + +export interface ColorField { + getModel(): string | undefined; + setModel(value?: string): void; + isDisabled(): boolean; + getConnectedOverlayOrigin(): ElementRef | undefined; +} diff --git a/projects/step-core/src/lib/step-core.module.ts b/projects/step-core/src/lib/step-core.module.ts index 2b8fc17ef5..352f7ab59f 100644 --- a/projects/step-core/src/lib/step-core.module.ts +++ b/projects/step-core/src/lib/step-core.module.ts @@ -69,6 +69,7 @@ import { PLAN_COMMON_EXPORTS } from './modules/plan-common'; import { IMPORT_EXPORT_EXPORTS } from './modules/import-export'; import { AUTH_EXPORTS } from './modules/auth'; import { DRAG_DROP_EXPORTS } from './modules/drag-drop'; +import { COLOR_PICKER_EXPORTS } from './modules/color-picker'; @NgModule({ declarations: [ @@ -138,6 +139,7 @@ import { DRAG_DROP_EXPORTS } from './modules/drag-drop'; PLAN_COMMON_EXPORTS, IMPORT_EXPORT_EXPORTS, DRAG_DROP_EXPORTS, + COLOR_PICKER_EXPORTS, ], exports: [ CommonModule, @@ -202,6 +204,7 @@ import { DRAG_DROP_EXPORTS } from './modules/drag-drop'; PLAN_COMMON_EXPORTS, IMPORT_EXPORT_EXPORTS, DRAG_DROP_EXPORTS, + COLOR_PICKER_EXPORTS, ], providers: [ CORE_INITIALIZER, @@ -327,3 +330,4 @@ export * from './services/artefacts-factory.service'; export * from './services/keyword-executor.service'; export * from './components/report-node-icon/report-node-icon.component'; export * from './modules/drag-drop'; +export * from './modules/color-picker'; diff --git a/projects/step-core/styles/_core-variables.scss b/projects/step-core/styles/_core-variables.scss index cf5b03691e..9b7ba3b846 100644 --- a/projects/step-core/styles/_core-variables.scss +++ b/projects/step-core/styles/_core-variables.scss @@ -1,4 +1,5 @@ $white: #fff; +$black: #000; $dark-gray: #555; $muted: #777; $muted-light: #7c7c7c; diff --git a/projects/step-core/styles/components/_step-header.scss b/projects/step-core/styles/components/_step-header.scss index 4db9fec0e6..56a3f1dda4 100644 --- a/projects/step-core/styles/components/_step-header.scss +++ b/projects/step-core/styles/components/_step-header.scss @@ -37,6 +37,7 @@ $default-color: dc.default-colors(); margin: 0.6rem 2rem; list-style-type: none; display: flex; + align-items: center; gap: 1rem; a { diff --git a/projects/step-frontend/src/lib/components/main-view/main-view.component.html b/projects/step-frontend/src/lib/components/main-view/main-view.component.html index 8ce54e44d1..affb64a18a 100644 --- a/projects/step-frontend/src/lib/components/main-view/main-view.component.html +++ b/projects/step-frontend/src/lib/components/main-view/main-view.component.html @@ -9,8 +9,8 @@ @for (item of navBarRightMenuItems; track item.id) { } - - + + @if (_authService.context$ | async; as ctx) { {{ ctx.userID }} [{{ ctx.role }}] } diff --git a/projects/step-frontend/src/lib/components/main-view/main-view.component.scss b/projects/step-frontend/src/lib/components/main-view/main-view.component.scss index 8b7e6149e0..a7f8439b29 100644 --- a/projects/step-frontend/src/lib/components/main-view/main-view.component.scss +++ b/projects/step-frontend/src/lib/components/main-view/main-view.component.scss @@ -1,3 +1,13 @@ #mainView { height: 100%; } + +.user { + display: flex; + align-items: center; + gap: 1rem; + + step-user-avatar { + font-size: 1.2rem; + } +} diff --git a/projects/step-frontend/src/lib/components/main-view/main-view.component.ts b/projects/step-frontend/src/lib/components/main-view/main-view.component.ts index d82037dc1f..2c39604141 100644 --- a/projects/step-frontend/src/lib/components/main-view/main-view.component.ts +++ b/projects/step-frontend/src/lib/components/main-view/main-view.component.ts @@ -1,10 +1,12 @@ import { Component, inject } from '@angular/core'; import { AppConfigContainerService, AuthService, ViewRegistryService } from '@exense/step-core'; +import { UserStateService } from '../../modules/admin/admin.module'; @Component({ selector: 'step-main-view', templateUrl: './main-view.component.html', styleUrls: ['./main-view.component.scss'], + providers: [UserStateService], }) export class MainViewComponent { private _viewRegistry = inject(ViewRegistryService); diff --git a/projects/step-frontend/src/lib/modules/admin/admin.module.ts b/projects/step-frontend/src/lib/modules/admin/admin.module.ts index 577e76c1d3..2db6851954 100644 --- a/projects/step-frontend/src/lib/modules/admin/admin.module.ts +++ b/projects/step-frontend/src/lib/modules/admin/admin.module.ts @@ -18,6 +18,8 @@ import { RenderOptionsPipe } from './pipes/render-options.pipe'; import { UserSelectionComponent } from './components/user-selection/user-selection.component'; import { ActivatedRouteSnapshot } from '@angular/router'; import { CURRENT_SCREEN_CHOICE_DEFAULT } from './types/constants'; +import { UserAvatarComponent } from './components/user-avatar/user-avatar.component'; +import { AvatarEditorComponent } from './components/avatar-editor/avatar-editor.component'; @NgModule({ declarations: [ @@ -27,12 +29,16 @@ import { CURRENT_SCREEN_CHOICE_DEFAULT } from './types/constants'; ScreenInputDropdownOptionsComponent, RenderOptionsPipe, UserSelectionComponent, + UserAvatarComponent, + AvatarEditorComponent, ], exports: [ MyAccountComponent, ScreenConfigurationListComponent, ScreenInputEditDialogComponent, UserSelectionComponent, + AvatarEditorComponent, + UserAvatarComponent, ], imports: [StepCoreModule, StepCommonModule], providers: [RenderOptionsPipe], @@ -123,3 +129,5 @@ export class AdminModule { ); } } + +export * from './injectables/user-state.service'; diff --git a/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html new file mode 100644 index 0000000000..fb2e6de7f3 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html @@ -0,0 +1 @@ + diff --git a/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.scss b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.scss new file mode 100644 index 0000000000..9ea0b7d2ac --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.scss @@ -0,0 +1,3 @@ +step-avatar-editor:not(.is-disabled) { + cursor: pointer; +} diff --git a/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts new file mode 100644 index 0000000000..fe0c039d59 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts @@ -0,0 +1,44 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + HostListener, + inject, + Input, + ViewEncapsulation, +} from '@angular/core'; +import { AVATAR_COLOR_PREFERENCE_KEY, ColorFieldBase } from '@exense/step-core'; +import { UserStateService } from '../../injectables/user-state.service'; + +@Component({ + selector: 'step-avatar-editor', + templateUrl: './avatar-editor.component.html', + styleUrl: './avatar-editor.component.scss', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AvatarEditorComponent extends ColorFieldBase { + private _userState = inject(UserStateService); + private _avatarColorKey = inject(AVATAR_COLOR_PREFERENCE_KEY); + + @HostBinding('class.is-disabled') + @Input() + disabled = false; + + getModel(): string | undefined { + return this._userState.getPreference(this._avatarColorKey); + } + + setModel(value?: string): void { + this._userState.changePreference(this._avatarColorKey, value); + } + + isDisabled(): boolean { + return this.disabled; + } + + @HostListener('click') + private handleClick(): void { + this.chooseColor(); + } +} diff --git a/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.html b/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.html index 5ace4ce580..3bf09f695b 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.html +++ b/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.html @@ -5,19 +5,23 @@ Change password - +
- - Username - {{ user.username }} - -
-
- - Role - {{ user.role }} - + +
+ @if (_userState.user$ | async; as user) { +
+ + Username + {{ user.username }} + + + Role + {{ user.role }} + +
+ }
@@ -26,15 +30,14 @@
Api key
- + /> @@ -78,22 +81,21 @@
Preferences
- + /> - +
-
Key - + @@ -101,15 +103,15 @@ Value - + - diff --git a/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.scss b/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.scss index c382b1881a..de400d64d1 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.scss +++ b/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.scss @@ -15,15 +15,21 @@ step-my-account { justify-content: space-between; } - .credentials { + .user { display: flex; - flex-direction: column; + gap: 2rem; section { display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; padding: 1rem 0; + gap: 0.5rem; + + step-avatar-editor { + font-size: 2.4rem; + } } } diff --git a/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.ts b/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.ts index ee86b48f0f..e3c8b02e21 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.ts +++ b/projects/step-frontend/src/lib/modules/admin/components/my-account/my-account.component.ts @@ -1,50 +1,13 @@ -import { KeyValue } from '@angular/common'; -import { - Component, - EventEmitter, - inject, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - SimpleChanges, - ViewEncapsulation, -} from '@angular/core'; +import { Component, inject, ViewEncapsulation } from '@angular/core'; import { AuthService, CredentialsService, DialogsService, GenerateApiKeyService, - Preferences, TableFetchLocalDataSource, - User, - UserService, } from '@exense/step-core'; -import { BehaviorSubject, filter, of, pipe, switchMap, tap } from 'rxjs'; - -const preferencesToKVPairArray = (preferences?: Preferences): KeyValue[] => { - const prefsObject = preferences?.preferences || {}; - const result = Object.keys(prefsObject).reduce( - (result, key) => { - const value = prefsObject[key] || ''; - return [...result, { key, value }]; - }, - [] as KeyValue[], - ); - return result; -}; - -const kvPairArrayToPreferences = (values?: KeyValue[]): Preferences => { - const preferences = (values || []).reduce( - (res, { key, value }) => { - res[key] = value; - return res; - }, - {} as { [key: string]: string }, - ); - return { preferences }; -}; +import { filter, of, pipe, switchMap, tap } from 'rxjs'; +import { UserStateService } from '../../injectables/user-state.service'; @Component({ selector: 'step-my-account', @@ -52,25 +15,18 @@ const kvPairArrayToPreferences = (values?: KeyValue[]): Preferen styleUrls: ['./my-account.component.scss'], encapsulation: ViewEncapsulation.None, }) -export class MyAccountComponent implements OnInit, OnChanges, OnDestroy { - private _userApi = inject(UserService); +export class MyAccountComponent { private _authService = inject(AuthService); private _credentialsService = inject(CredentialsService); private _generateApiKey = inject(GenerateApiKeyService); private _dialogs = inject(DialogsService); + readonly _userState = inject(UserStateService); readonly canChangePassword = !!this._authService.getConf()?.passwordManagement; - readonly canGenerateApiKey = !!this._authService.getConf()?.authentication; - readonly preferences$ = new BehaviorSubject[]>([]); - - @Input() error?: string; - @Output() errorChange: EventEmitter = new EventEmitter(); - - user: Partial = {}; - preferences: KeyValue[] = []; + readonly canEdit = !!this._authService.getConf()?.authentication; readonly tokensSource = new TableFetchLocalDataSource(() => { - if (!this.canGenerateApiKey) { + if (!this.canEdit) { return of([]); } return this._generateApiKey.getServiceAccountTokens(); @@ -78,24 +34,6 @@ export class MyAccountComponent implements OnInit, OnChanges, OnDestroy { private reloadTokens = pipe(tap((result) => this.tokensSource.reload())); - ngOnInit(): void { - this._userApi.getMyUser().subscribe((user) => { - this.user = user || {}; - this.preferences$.next(preferencesToKVPairArray(this.user?.preferences)); - }); - } - - ngOnChanges(changes: SimpleChanges): void { - const errorChange = changes['error']; - if (errorChange?.currentValue !== errorChange?.previousValue) { - this.errorChange.emit(errorChange?.currentValue); - } - } - - ngOnDestroy(): void { - this.preferences$.complete(); - } - changePwd(): void { this._credentialsService.changePassword(false); } @@ -114,27 +52,4 @@ export class MyAccountComponent implements OnInit, OnChanges, OnDestroy { ) .subscribe(); } - - addPreference(): void { - const key = ''; - const value = ''; - const preferences = [...this.preferences$.value, { key, value }]; - this.preferences$.next(preferences); - } - - removePreference(itemToRemove: KeyValue): void { - const preferences = this.preferences$.value.filter((item) => item !== itemToRemove); - this.preferences$.next(preferences); - this.savePreferences(); - } - - savePreferences(): void { - const preferences = kvPairArrayToPreferences(this.preferences$.value); - this._userApi.putPreferences(preferences).subscribe({ - error: (err) => { - this.error = 'Unable to save preferences. Please contact your administrator.'; - this.errorChange.emit(this.error); - }, - }); - } } diff --git a/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.html b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.html new file mode 100644 index 0000000000..2d8e6f78d8 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.html @@ -0,0 +1 @@ + diff --git a/projects/step-frontend/src/lib/modules/admin/services/screen-dialogs.service.ts b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.scss similarity index 100% rename from projects/step-frontend/src/lib/modules/admin/services/screen-dialogs.service.ts rename to projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.scss diff --git a/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts new file mode 100644 index 0000000000..df07a351e0 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { map, of, switchMap } from 'rxjs'; +import { AuthService, AVATAR_COLOR_PREFERENCE_KEY } from '@exense/step-core'; +import { UserStateService } from '../../injectables/user-state.service'; + +const ANONYMOUS_COLOR = '#0082cb'; + +@Component({ + selector: 'step-user-avatar', + templateUrl: './user-avatar.component.html', + styleUrl: './user-avatar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserAvatarComponent { + private _auth = inject(AuthService); + private _userState = inject(UserStateService); + private _avatarColorPreferenceKey = inject(AVATAR_COLOR_PREFERENCE_KEY); + + readonly userName$ = this._userState.user$.pipe(map((user) => user.username)); + + readonly color$ = this._auth.initialize$.pipe( + switchMap((user) => { + if (!this._auth.getConf()?.authentication) { + return of(ANONYMOUS_COLOR); + } + return this._userState.getPreference$(this._avatarColorPreferenceKey); + }), + ); +} diff --git a/projects/step-frontend/src/lib/modules/admin/injectables/screen-dialogs.service.ts b/projects/step-frontend/src/lib/modules/admin/injectables/screen-dialogs.service.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/projects/step-frontend/src/lib/modules/admin/injectables/user-state.service.ts b/projects/step-frontend/src/lib/modules/admin/injectables/user-state.service.ts new file mode 100644 index 0000000000..e84e88a0c8 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/injectables/user-state.service.ts @@ -0,0 +1,100 @@ +import { inject, Injectable, OnDestroy } from '@angular/core'; +import { BehaviorSubject, map, Observable, shareReplay, switchMap, tap, combineLatest } from 'rxjs'; +import { KeyValue } from '@angular/common'; +import { AuthService, Preferences, User, UserService } from '@exense/step-core'; + +type PreferenceItem = KeyValue; + +const preferencesToKVPairArray = (preferences?: Preferences) => + Object.entries(preferences?.preferences || {}).reduce( + (result, [key, value]) => [...result, { key, value: value ?? '' }], + [] as PreferenceItem[], + ); + +const kvPairArrayToPreferences = (values?: PreferenceItem[]): Preferences => { + const preferences = (values || []).reduce( + (res, { key, value }) => { + res[key] = value; + return res; + }, + {} as Record, + ); + return { preferences }; +}; + +@Injectable() +export class UserStateService implements OnDestroy { + private _auth = inject(AuthService); + private _userApi = inject(UserService); + + private preferencesInternal$ = new BehaviorSubject([]); + + readonly user$ = this._auth.initialize$.pipe( + switchMap(() => this._userApi.getMyUser()), + map((user) => (user ?? {}) as Partial), + shareReplay(1), + ); + + private preferencesRemote$ = this.user$.pipe( + map((user) => preferencesToKVPairArray(user?.preferences)), + tap((preferences) => this.preferencesInternal$.next(preferences)), + shareReplay(1), + ); + + readonly preferences$ = combineLatest([this.preferencesRemote$, this.preferencesInternal$]).pipe( + map(([_, prefs]) => prefs), + ); + + ngOnDestroy(): void { + this.preferencesInternal$.complete(); + } + + addPreference(): void { + const preferences = this.preferencesInternal$.value; + this.preferencesInternal$.next([...preferences, { key: '', value: '' }]); + } + + removePreference(item: PreferenceItem): void { + const preferences = this.preferencesInternal$.value.filter((pref) => + !pref.key ? pref !== item : pref.key !== item.key, + ); + this.savePreferences(preferences); + } + + changePreference(key: string, value?: string): void { + if (!value) { + this.removePreference({ key, value: '' }); + return; + } + const preferences = this.preferencesInternal$.value; + const prefItem = preferences.find((item) => item.key === key); + if (prefItem) { + prefItem.value = value; + } else { + preferences.push({ key, value }); + } + this.savePreferences(preferences); + } + + getPreference$(key: string): Observable { + return this.preferences$.pipe( + map((preferences) => { + const prefItem = preferences.find((item) => item.key === key); + return prefItem?.value; + }), + ); + } + + getPreference(key: string): string | undefined { + const preferences = this.preferencesInternal$.value; + const prefItem = preferences?.find((item) => item.key === key); + return prefItem?.value; + } + + savePreferences(preferences?: PreferenceItem[]): void { + preferences = preferences ?? this.preferencesInternal$.value; + this._userApi + .putPreferences(kvPairArrayToPreferences(preferences)) + .subscribe(() => this.preferencesInternal$.next(preferences!)); + } +} From cd5a81bccca4842314c3f175054641684051a142 Mon Sep 17 00:00:00 2001 From: dvladir Date: Mon, 1 Apr 2024 15:22:29 +0300 Subject: [PATCH 2/4] SED-2978 POC: user avatars --- .../step-core/src/lib/modules/auth/index.ts | 1 + .../modules/auth/injectables/users.service.ts | 28 +++++++++++++++++ .../color-chooser.component.html | 2 +- .../color-chooser/color-chooser.component.ts | 8 +++-- .../color-field/color-field.base.ts | 5 ++-- .../color-picker-content.component.html | 7 ++++- .../color-picker-content.component.ts | 10 ++++--- .../color-picker/color-picker.component.ts | 8 ++++- .../color-field-container.service.ts | 13 ++++++++ .../types/color-picker-settings.ts | 4 +++ .../main-view/main-view.component.html | 2 +- .../main-view/main-view.component.scss | 2 +- .../src/lib/modules/admin/admin.module.ts | 5 +++- .../avatar-editor.component.html | 2 +- .../avatar-editor/avatar-editor.component.ts | 5 +++- .../current-user-avatar.component.html | 1 + .../current-user-avatar.component.scss | 0 .../current-user-avatar.component.ts | 28 +++++++++++++++++ .../user-avatar/user-avatar.component.ts | 30 +++++++++++-------- .../src/lib/modules/admin/types/constants.ts | 2 ++ .../execution-list.component.html | 6 +++- .../execution-list.component.scss | 7 +++++ .../lib/modules/execution/execution.module.ts | 3 +- 23 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 projects/step-core/src/lib/modules/auth/injectables/users.service.ts create mode 100644 projects/step-core/src/lib/modules/color-picker/types/color-picker-settings.ts create mode 100644 projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html create mode 100644 projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.scss create mode 100644 projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts diff --git a/projects/step-core/src/lib/modules/auth/index.ts b/projects/step-core/src/lib/modules/auth/index.ts index 44fbf0eb54..22e363c8f8 100644 --- a/projects/step-core/src/lib/modules/auth/index.ts +++ b/projects/step-core/src/lib/modules/auth/index.ts @@ -7,6 +7,7 @@ export * from './injectables/auth.service'; export * from './injectables/additional-right-rule.service'; export * from './injectables/credentials.service'; export * from './injectables/logout-cleanup.token'; +export * from './injectables/users.service'; export * from './pipes/has-right.pipe'; diff --git a/projects/step-core/src/lib/modules/auth/injectables/users.service.ts b/projects/step-core/src/lib/modules/auth/injectables/users.service.ts new file mode 100644 index 0000000000..ae48b11c45 --- /dev/null +++ b/projects/step-core/src/lib/modules/auth/injectables/users.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { User } from '../../../client/step-client-module'; + +export interface UsersStrategy { + getUserById(userId: string): Observable; +} + +const DEFAULT_USER_STRATEGY: UsersStrategy = { + getUserById(userId: string): Observable { + return of(undefined); + }, +}; + +@Injectable({ + providedIn: 'root', +}) +export class UsersService implements UsersStrategy { + private strategy: UsersStrategy = DEFAULT_USER_STRATEGY; + + getUserById(userId: string): Observable { + return this.strategy.getUserById(userId); + } + + useStrategy(strategy: UsersStrategy): void { + this.strategy = strategy; + } +} diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html index edc7df811b..369f216295 100644 --- a/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html +++ b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.html @@ -1,4 +1,4 @@ -@for (color of _colors; track color) { +@for (color of colors(); track color) { @if (!color) {
diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts index 8d44c4f721..82c5025c37 100644 --- a/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts +++ b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, forwardRef, HostBinding, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, forwardRef, HostBinding, inject, input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { StepBasicsModule } from '../../../basics/step-basics.module'; import { COLORS } from '../../injectables/colors.token'; @@ -22,7 +22,11 @@ type OnTouch = () => void; ], }) export class ColorChooserComponent implements ControlValueAccessor { - protected readonly _colors = [...inject(COLORS), '']; + private _colors = inject(COLORS); + + showClearColor = input.required(); + + protected colors = computed(() => (this.showClearColor() ? [...this._colors, ''] : this._colors)); protected selectedColor?: string; diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts index aa4f7f6616..341ae469dc 100644 --- a/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts +++ b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts @@ -1,6 +1,7 @@ import { ColorField } from '../../types/color-field'; import { Directive, effect, ElementRef, inject, input, Signal } from '@angular/core'; import { ColorPickerComponent } from '../color-picker/color-picker.component'; +import { ColorPickerSettings } from '../../types/color-picker-settings'; @Directive({}) export abstract class ColorFieldBase implements ColorField { @@ -20,10 +21,10 @@ export abstract class ColorFieldBase implements ColorField { abstract setModel(value?: string): void; abstract isDisabled(): boolean; - chooseColor(): void { + chooseColor(settings?: ColorPickerSettings): void { if (this.isDisabled()) { return; } - this.colorPicker()?.open(); + this.colorPicker()?.open(settings); } } diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html index 0c66f5ee9b..4083cdef34 100644 --- a/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html @@ -1,7 +1,12 @@
+ @if (settings.title) { + + {{ settings.title }} + + } - +
diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts index 65c5d01436..f6ca6be563 100644 --- a/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts @@ -14,14 +14,16 @@ import { CdkTrapFocus } from '@angular/cdk/a11y'; encapsulation: ViewEncapsulation.None, }) export class ColorPickerContentComponent { - private _field = inject(ColorFieldContainerService); + private _fieldContainer = inject(ColorFieldContainerService); - protected color = model(this._field.getModel()); + protected settings = this._fieldContainer.getSettings(); + + protected color = model(this._fieldContainer.getModel()); private effectColorChange = effect(() => { const color = this.color(); - if (color !== this._field.getModel()) { - this._field.setModel(color); + if (color !== this._fieldContainer.getModel()) { + this._fieldContainer.setModel(color); } }); } diff --git a/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts b/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts index 49ddaeddec..df447f15d5 100644 --- a/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts @@ -3,6 +3,7 @@ import { ColorFieldContainerService } from '../../injectables/color-field-contai import { PopoverOverlayService } from '../../../basics/step-basics.module'; import { ColorField } from '../../types/color-field'; import { ColorPickerContentComponent } from '../color-picker-content/color-picker-content.component'; +import { ColorPickerSettings } from '../../types/color-picker-settings'; @Component({ selector: 'step-color-picker', @@ -24,9 +25,14 @@ export class ColorPickerComponent { this._fieldContainer.registerField(field); } - open(): void { + open(settings?: ColorPickerSettings): void { const xPosition = this.xPosition; const yPosition = this.yPosition; + + if (settings) { + this._fieldContainer.setup(settings); + } + this._popoverOverlay .setPositions({ xPosition, yPosition }) .open(ColorPickerContentComponent, this._fieldContainer.getConnectedOverlayOrigin()?.nativeElement); diff --git a/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts b/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts index f5597be134..29119939cb 100644 --- a/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts +++ b/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts @@ -1,9 +1,18 @@ import { ElementRef, Injectable, OnDestroy } from '@angular/core'; import { ColorField } from '../types/color-field'; +import { ColorPickerSettings } from '../types/color-picker-settings'; @Injectable() export class ColorFieldContainerService implements ColorField, OnDestroy { private field?: ColorField; + private settings: ColorPickerSettings = { + showClearColor: true, + }; + + setup(settings: ColorPickerSettings): this { + this.settings = settings; + return this; + } registerField(field?: ColorField): void { this.field = field; @@ -28,4 +37,8 @@ export class ColorFieldContainerService implements ColorField, OnDestroy { ngOnDestroy(): void { this.field = undefined; } + + getSettings(): ColorPickerSettings { + return this.settings; + } } diff --git a/projects/step-core/src/lib/modules/color-picker/types/color-picker-settings.ts b/projects/step-core/src/lib/modules/color-picker/types/color-picker-settings.ts new file mode 100644 index 0000000000..3816216f64 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/types/color-picker-settings.ts @@ -0,0 +1,4 @@ +export interface ColorPickerSettings { + title?: string; + showClearColor?: boolean; +} diff --git a/projects/step-frontend/src/lib/components/main-view/main-view.component.html b/projects/step-frontend/src/lib/components/main-view/main-view.component.html index affb64a18a..6be5bdf551 100644 --- a/projects/step-frontend/src/lib/components/main-view/main-view.component.html +++ b/projects/step-frontend/src/lib/components/main-view/main-view.component.html @@ -10,7 +10,7 @@ }
- + @if (_authService.context$ | async; as ctx) { {{ ctx.userID }} [{{ ctx.role }}] } diff --git a/projects/step-frontend/src/lib/components/main-view/main-view.component.scss b/projects/step-frontend/src/lib/components/main-view/main-view.component.scss index a7f8439b29..d09d0f222e 100644 --- a/projects/step-frontend/src/lib/components/main-view/main-view.component.scss +++ b/projects/step-frontend/src/lib/components/main-view/main-view.component.scss @@ -7,7 +7,7 @@ align-items: center; gap: 1rem; - step-user-avatar { + step-current-user-avatar { font-size: 1.2rem; } } diff --git a/projects/step-frontend/src/lib/modules/admin/admin.module.ts b/projects/step-frontend/src/lib/modules/admin/admin.module.ts index 2db6851954..a25edd03d7 100644 --- a/projects/step-frontend/src/lib/modules/admin/admin.module.ts +++ b/projects/step-frontend/src/lib/modules/admin/admin.module.ts @@ -18,8 +18,9 @@ import { RenderOptionsPipe } from './pipes/render-options.pipe'; import { UserSelectionComponent } from './components/user-selection/user-selection.component'; import { ActivatedRouteSnapshot } from '@angular/router'; import { CURRENT_SCREEN_CHOICE_DEFAULT } from './types/constants'; -import { UserAvatarComponent } from './components/user-avatar/user-avatar.component'; +import { CurrentUserAvatarComponent } from './components/current-user-avatar/current-user-avatar.component'; import { AvatarEditorComponent } from './components/avatar-editor/avatar-editor.component'; +import { UserAvatarComponent } from './components/user-avatar/user-avatar.component'; @NgModule({ declarations: [ @@ -29,6 +30,7 @@ import { AvatarEditorComponent } from './components/avatar-editor/avatar-editor. ScreenInputDropdownOptionsComponent, RenderOptionsPipe, UserSelectionComponent, + CurrentUserAvatarComponent, UserAvatarComponent, AvatarEditorComponent, ], @@ -38,6 +40,7 @@ import { AvatarEditorComponent } from './components/avatar-editor/avatar-editor. ScreenInputEditDialogComponent, UserSelectionComponent, AvatarEditorComponent, + CurrentUserAvatarComponent, UserAvatarComponent, ], imports: [StepCoreModule, StepCommonModule], diff --git a/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html index fb2e6de7f3..bd8d2f4caf 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html +++ b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.html @@ -1 +1 @@ - + diff --git a/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts index fe0c039d59..494acf3028 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts +++ b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts @@ -39,6 +39,9 @@ export class AvatarEditorComponent extends ColorFieldBase { @HostListener('click') private handleClick(): void { - this.chooseColor(); + this.chooseColor({ + title: 'Select Avatar Color', + showClearColor: false, + }); } } diff --git a/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html new file mode 100644 index 0000000000..2d8e6f78d8 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html @@ -0,0 +1 @@ + diff --git a/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.scss b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts new file mode 100644 index 0000000000..accff6d5fb --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts @@ -0,0 +1,28 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { map, of, switchMap } from 'rxjs'; +import { AuthService, AVATAR_COLOR_PREFERENCE_KEY } from '@exense/step-core'; +import { UserStateService } from '../../injectables/user-state.service'; +import { ANONYMOUS_COLOR } from '../../types/constants'; + +@Component({ + selector: 'step-current-user-avatar', + templateUrl: './current-user-avatar.component.html', + styleUrl: './current-user-avatar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CurrentUserAvatarComponent { + private _auth = inject(AuthService); + private _userState = inject(UserStateService); + private _avatarColorPreferenceKey = inject(AVATAR_COLOR_PREFERENCE_KEY); + + readonly userName$ = this._userState.user$.pipe(map((user) => user.username)); + + readonly color$ = this._auth.initialize$.pipe( + switchMap((user) => { + if (!this._auth.getConf()?.authentication) { + return of(ANONYMOUS_COLOR); + } + return this._userState.getPreference$(this._avatarColorPreferenceKey); + }), + ); +} diff --git a/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts index df07a351e0..4719a8cd8e 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts +++ b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts @@ -1,9 +1,8 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { map, of, switchMap } from 'rxjs'; -import { AuthService, AVATAR_COLOR_PREFERENCE_KEY } from '@exense/step-core'; -import { UserStateService } from '../../injectables/user-state.service'; - -const ANONYMOUS_COLOR = '#0082cb'; +import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; +import { AuthService, AVATAR_COLOR_PREFERENCE_KEY, UsersService } from '@exense/step-core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { map, switchMap } from 'rxjs'; +import { ANONYMOUS_COLOR } from '../../types/constants'; @Component({ selector: 'step-user-avatar', @@ -13,17 +12,24 @@ const ANONYMOUS_COLOR = '#0082cb'; }) export class UserAvatarComponent { private _auth = inject(AuthService); - private _userState = inject(UserStateService); + private _users = inject(UsersService); private _avatarColorPreferenceKey = inject(AVATAR_COLOR_PREFERENCE_KEY); - readonly userName$ = this._userState.user$.pipe(map((user) => user.username)); + userId = input.required(); + + private user$ = toObservable(this.userId).pipe( + switchMap((userId) => this._users.getUserById(userId)), + takeUntilDestroyed(), + ); + + protected userName$ = this.user$.pipe(map((user) => user?.username ?? this.userId())); - readonly color$ = this._auth.initialize$.pipe( - switchMap((user) => { + protected color$ = this.user$.pipe( + map((user) => { if (!this._auth.getConf()?.authentication) { - return of(ANONYMOUS_COLOR); + return ANONYMOUS_COLOR; } - return this._userState.getPreference$(this._avatarColorPreferenceKey); + return user?.preferences?.preferences?.[this._avatarColorPreferenceKey]; }), ); } diff --git a/projects/step-frontend/src/lib/modules/admin/types/constants.ts b/projects/step-frontend/src/lib/modules/admin/types/constants.ts index 987b540629..3a503769cb 100644 --- a/projects/step-frontend/src/lib/modules/admin/types/constants.ts +++ b/projects/step-frontend/src/lib/modules/admin/types/constants.ts @@ -1 +1,3 @@ export const CURRENT_SCREEN_CHOICE_DEFAULT = 'executionParameters'; + +export const ANONYMOUS_COLOR = '#0082cb'; diff --git a/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html b/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html index c6614dc699..f411db8ea7 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html @@ -66,7 +66,11 @@ User - {{ element?.executionParameters?.userID }} + +
+ +
+
diff --git a/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.scss b/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.scss index 68434b84c4..af85f8c96a 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.scss +++ b/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.scss @@ -57,6 +57,13 @@ step-execution-list { justify-content: center; white-space: nowrap; } + + .user-cell { + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + } } step-popover-content > .execution-duration-description { diff --git a/projects/step-frontend/src/lib/modules/execution/execution.module.ts b/projects/step-frontend/src/lib/modules/execution/execution.module.ts index d4dbeff310..f08011633a 100644 --- a/projects/step-frontend/src/lib/modules/execution/execution.module.ts +++ b/projects/step-frontend/src/lib/modules/execution/execution.module.ts @@ -36,6 +36,7 @@ import { IsExecutionProgressPipe } from './pipes/is-execution-progress.pipe'; import { ExecutionsComponent } from './components/executions/executions.component'; import { ExecutionOpenerComponent } from './components/execution-opener/execution-opener.component'; import { ExecutionRunningStatusHeaderComponent } from './components/execution-running-status-header/execution-running-status-header.component'; +import { AdminModule } from '../admin/admin.module'; @NgModule({ declarations: [ @@ -67,7 +68,7 @@ import { ExecutionRunningStatusHeaderComponent } from './components/execution-ru ExecutionOpenerComponent, ExecutionRunningStatusHeaderComponent, ], - imports: [StepCommonModule, OperationsModule, ReportNodesModule, TimeSeriesModule], + imports: [StepCommonModule, OperationsModule, ReportNodesModule, TimeSeriesModule, AdminModule], exports: [ ExecutionListComponent, ExecutionStepComponent, From a4cb324c51f9aee2f7f6caae1f7a2c3aca2f5d42 Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 2 Apr 2024 13:09:39 +0300 Subject: [PATCH 3/4] SED-2978 POC: user avatars (fix for not found users) --- .../components/execution-list/execution-list.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html b/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html index f411db8ea7..9e1ed30fb3 100644 --- a/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html +++ b/projects/step-frontend/src/lib/modules/execution/components/execution-list/execution-list.component.html @@ -68,7 +68,9 @@ User
- + @if (element?.executionParameters?.userID) { + + }
From d93d0b25e4b96ec272e49e54522e7b934a139532 Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 2 Apr 2024 17:49:53 +0300 Subject: [PATCH 4/4] SED-2978 POC: user avatars. Fix tooltip --- .../avatar-circle/avatar-circle.component.html | 2 +- .../components/avatar-circle/avatar-circle.component.ts | 4 ++++ .../lib/components/main-view/main-view.component.html | 3 --- .../current-user-avatar.component.html | 2 +- .../current-user-avatar/current-user-avatar.component.ts | 9 +++++++++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html index c1fd486a2d..a80401236e 100644 --- a/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html +++ b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.html @@ -1 +1 @@ -
{{ initials() }}
+
{{ initials() }}
diff --git a/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts index d6a1848dcc..10f1623d4e 100644 --- a/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts +++ b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts @@ -18,6 +18,10 @@ export class AvatarCircleComponent { transform: (value: string | undefined | null) => value ?? '', }); + tooltip = input(undefined); + + protected displayTooltip = computed(() => this.tooltip() ?? this.name()); + protected initials = computed(() => { const name = this.name().trim(); if (!name) { diff --git a/projects/step-frontend/src/lib/components/main-view/main-view.component.html b/projects/step-frontend/src/lib/components/main-view/main-view.component.html index 6be5bdf551..a7456b3039 100644 --- a/projects/step-frontend/src/lib/components/main-view/main-view.component.html +++ b/projects/step-frontend/src/lib/components/main-view/main-view.component.html @@ -11,9 +11,6 @@ }
- @if (_authService.context$ | async; as ctx) { - {{ ctx.userID }} [{{ ctx.role }}] - }
diff --git a/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html index 2d8e6f78d8..3dd40b0fc7 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html +++ b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.html @@ -1 +1 @@ - + diff --git a/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts index accff6d5fb..b20ee05faa 100644 --- a/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts +++ b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts @@ -17,6 +17,15 @@ export class CurrentUserAvatarComponent { readonly userName$ = this._userState.user$.pipe(map((user) => user.username)); + readonly tooltip$ = this._auth.context$.pipe( + map((ctx) => { + if (!ctx) { + return undefined; + } + return `${ctx.userID} [${ctx.role}]`; + }), + ); + readonly color$ = this._auth.initialize$.pipe( switchMap((user) => { if (!this._auth.getConf()?.authentication) {