diff --git a/projects/step-core/src/lib/modules/auth/index.ts b/projects/step-core/src/lib/modules/auth/index.ts index 44fbf0eb5..22e363c8f 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 000000000..ae48b11c4 --- /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/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 000000000..a80401236 --- /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 000000000..45f9f8d11 --- /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 000000000..10f1623d4 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/avatar-circle/avatar-circle.component.ts @@ -0,0 +1,63 @@ +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 ?? '', + }); + + tooltip = input(undefined); + + protected displayTooltip = computed(() => this.tooltip() ?? this.name()); + + 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 000000000..5159e5543 --- /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 17396eac4..852e66a94 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 {} @@ -244,6 +247,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'; @@ -265,5 +269,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 000000000..369f21629 --- /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 000000000..e3a361f37 --- /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 000000000..82c5025c3 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-chooser/color-chooser.component.ts @@ -0,0 +1,63 @@ +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'; + +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 { + private _colors = inject(COLORS); + + showClearColor = input.required(); + + protected colors = computed(() => (this.showClearColor() ? [...this._colors, ''] : this._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 000000000..341ae469d --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-field/color-field.base.ts @@ -0,0 +1,30 @@ +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 { + 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(settings?: ColorPickerSettings): void { + if (this.isDisabled()) { + return; + } + this.colorPicker()?.open(settings); + } +} 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 000000000..5a8259490 --- /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 000000000..c866368cc --- /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 000000000..a6828e47d --- /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 000000000..4083cdef3 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.html @@ -0,0 +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.scss b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.scss new file mode 100644 index 000000000..59894bab5 --- /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 000000000..f6ca6be56 --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker-content/color-picker-content.component.ts @@ -0,0 +1,29 @@ +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 _fieldContainer = inject(ColorFieldContainerService); + + protected settings = this._fieldContainer.getSettings(); + + protected color = model(this._fieldContainer.getModel()); + + private effectColorChange = effect(() => { + const color = this.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 new file mode 100644 index 000000000..df447f15d --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/components/color-picker/color-picker.component.ts @@ -0,0 +1,44 @@ +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'; +import { ColorPickerSettings } from '../../types/color-picker-settings'; + +@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(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); + } + + 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 000000000..053132fa1 --- /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 000000000..29119939c --- /dev/null +++ b/projects/step-core/src/lib/modules/color-picker/injectables/color-field-container.service.ts @@ -0,0 +1,44 @@ +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; + } + + 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; + } + + getSettings(): ColorPickerSettings { + return this.settings; + } +} 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 000000000..167b2b5f8 --- /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 000000000..04bad0813 --- /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 000000000..a82d759cb --- /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/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 000000000..3816216f6 --- /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-core/src/lib/step-core.module.ts b/projects/step-core/src/lib/step-core.module.ts index c049ceecc..2a5491eca 100644 --- a/projects/step-core/src/lib/step-core.module.ts +++ b/projects/step-core/src/lib/step-core.module.ts @@ -66,6 +66,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'; import { BOOKMARKS_EXPORTS } from './modules/bookmarks'; import { DashboardNavigationParamsPipe } from './pipes/dashboard-navigation-params.pipe'; import { EXECUTION_COMMON_EXPORTS } from './modules/execution-common'; @@ -138,6 +139,7 @@ import { RICH_EDITOR_EXPORTS } from './modules/rich-editor'; PLAN_COMMON_EXPORTS, IMPORT_EXPORT_EXPORTS, DRAG_DROP_EXPORTS, + COLOR_PICKER_EXPORTS, BOOKMARKS_EXPORTS, EXECUTION_COMMON_EXPORTS, RICH_EDITOR_EXPORTS, @@ -203,6 +205,7 @@ import { RICH_EDITOR_EXPORTS } from './modules/rich-editor'; PLAN_COMMON_EXPORTS, IMPORT_EXPORT_EXPORTS, DRAG_DROP_EXPORTS, + COLOR_PICKER_EXPORTS, BOOKMARKS_EXPORTS, EXECUTION_COMMON_EXPORTS, DashboardNavigationParamsPipe, @@ -332,5 +335,6 @@ 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'; export * from './pipes/dashboard-navigation-params.pipe'; export * from './modules/rich-editor'; diff --git a/projects/step-core/styles/_core-variables.scss b/projects/step-core/styles/_core-variables.scss index cf5b03691..9b7ba3b84 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 4db9fec0e..56a3f1dda 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 44119a2c7..a34f3c205 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 @@ -12,11 +12,8 @@ - - - @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 910a0d048..367023b71 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 @@ -2,6 +2,16 @@ height: 100%; } +.user { + display: flex; + align-items: center; + gap: 1rem; + + step-current-user-avatar { + font-size: 1.2rem; + } +} + .icon-bookmark { cursor: pointer; } 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 b3f8e1421..5c73114cc 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 @@ -6,11 +6,13 @@ import { ViewRegistryService, } from '@exense/step-core'; import { MatDialog } from '@angular/material/dialog'; +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 63e8f1172..92ac327ef 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,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 { 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: [ @@ -27,12 +30,18 @@ import { CURRENT_SCREEN_CHOICE_DEFAULT } from './types/constants'; ScreenInputDropdownOptionsComponent, RenderOptionsPipe, UserSelectionComponent, + CurrentUserAvatarComponent, + UserAvatarComponent, + AvatarEditorComponent, ], exports: [ MyAccountComponent, ScreenConfigurationListComponent, ScreenInputEditDialogComponent, UserSelectionComponent, + AvatarEditorComponent, + CurrentUserAvatarComponent, + UserAvatarComponent, ], imports: [StepCoreModule, StepCommonModule], providers: [RenderOptionsPipe], @@ -123,3 +132,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 000000000..bd8d2f4ca --- /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 000000000..9ea0b7d2a --- /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 000000000..494acf302 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/avatar-editor/avatar-editor.component.ts @@ -0,0 +1,47 @@ +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({ + 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 000000000..3dd40b0fc --- /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/services/screen-dialogs.service.ts b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-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/current-user-avatar/current-user-avatar.component.scss 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 000000000..b20ee05fa --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/current-user-avatar/current-user-avatar.component.ts @@ -0,0 +1,37 @@ +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 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) { + return of(ANONYMOUS_COLOR); + } + return this._userState.getPreference$(this._avatarColorPreferenceKey); + }), + ); +} 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 5ace4ce58..3bf09f695 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 c382b1881..de400d64d 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 ee86b48f0..e3c8b02e2 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 000000000..2d8e6f78d --- /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/components/user-avatar/user-avatar.component.scss b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.scss new file mode 100644 index 000000000..e69de29bb 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 000000000..4719a8cd8 --- /dev/null +++ b/projects/step-frontend/src/lib/modules/admin/components/user-avatar/user-avatar.component.ts @@ -0,0 +1,35 @@ +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', + templateUrl: './user-avatar.component.html', + styleUrl: './user-avatar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UserAvatarComponent { + private _auth = inject(AuthService); + private _users = inject(UsersService); + private _avatarColorPreferenceKey = inject(AVATAR_COLOR_PREFERENCE_KEY); + + 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())); + + protected color$ = this.user$.pipe( + map((user) => { + if (!this._auth.getConf()?.authentication) { + return ANONYMOUS_COLOR; + } + return user?.preferences?.preferences?.[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 000000000..e69de29bb 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 000000000..e84e88a0c --- /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!)); + } +} 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 987b54062..3a503769c 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 c6614dc69..9e1ed30fb 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,13 @@ User - {{ element?.executionParameters?.userID }} + +
+ @if (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 5c15a2934..983fa6d18 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 d4dbeff31..f08011633 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,