+
-
+ @if (showDot()) {
+
+ }
+
+ @if (dropdownButton()) {
+
+ }
diff --git a/src/lib/buttons/circle-button/circle-button.component.scss b/src/lib/buttons/circle-button/circle-button.component.scss
index 0320fef..66ab031 100644
--- a/src/lib/buttons/circle-button/circle-button.component.scss
+++ b/src/lib/buttons/circle-button/circle-button.component.scss
@@ -1,4 +1,16 @@
:host > div {
width: var(--circle-button-size);
height: var(--circle-button-size);
+
+ .arrow-down {
+ border: 5px solid transparent;
+ border-top-color: black;
+ position: fixed;
+ margin-left: 12px;
+ margin-top: -8px;
+
+ &.disabled {
+ border-top-color: var(--iqser-grey-3);
+ }
+ }
}
diff --git a/src/lib/buttons/circle-button/circle-button.component.ts b/src/lib/buttons/circle-button/circle-button.component.ts
index 840379a..a7c3532 100644
--- a/src/lib/buttons/circle-button/circle-button.component.ts
+++ b/src/lib/buttons/circle-button/circle-button.component.ts
@@ -1,5 +1,16 @@
-import { NgIf } from '@angular/common';
-import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, inject, Input, OnInit, Output, ViewChild } from '@angular/core';
+import {
+ booleanAttribute,
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ ElementRef,
+ EventEmitter,
+ inject,
+ input,
+ numberAttribute,
+ Output,
+ viewChild,
+} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
@@ -13,38 +24,39 @@ import { CircleButtonType, CircleButtonTypes } from '../types/circle-button.type
templateUrl: './circle-button.component.html',
styleUrls: ['./circle-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
- standalone: true,
- imports: [MatTooltipModule, MatIconModule, NgIf, MatButtonModule, StopPropagationDirective],
+ imports: [MatTooltipModule, MatIconModule, MatButtonModule, StopPropagationDirective],
})
-export class CircleButtonComponent implements OnInit {
+export class CircleButtonComponent {
readonly #elementRef = inject(ElementRef
);
- @ViewChild(MatTooltip) private readonly _matTooltip!: MatTooltip;
+ protected readonly _matTooltip = viewChild.required(MatTooltip);
protected readonly _circleButtonTypes = CircleButtonTypes;
protected readonly _hasRouterLink = !!inject(RouterLink, { optional: true, host: true });
- @Input() buttonId = `${randomString()}-circle-button`;
- @Input({ required: true }) icon!: string;
- @Input() tooltip?: string;
- @Input() tooltipClass?: string;
- @Input() showDot = false;
- @Input() tooltipPosition: IqserTooltipPosition = IqserTooltipPositions.above;
- @Input() disabled = false;
- @Input() type: CircleButtonType = CircleButtonTypes.default;
- @Input() greySelected = false;
- @Input() helpModeButton = false;
- @Input() removeTooltip = false;
- @Input() isSubmit = false;
- @Input() size = 34;
- @Input() iconSize = 14;
+ readonly buttonId = input(`${randomString()}-circle-button`);
+ readonly icon = input.required();
+ readonly tooltip = input('');
+ readonly tooltipClass = input('');
+ readonly showDot = input(false, { transform: booleanAttribute });
+ readonly tooltipPosition = input(IqserTooltipPositions.above);
+ readonly disabled = input(false, { transform: booleanAttribute });
+ readonly type = input(CircleButtonTypes.default);
+ readonly greySelected = input(false, { transform: booleanAttribute });
+ readonly removeTooltip = input(false, { transform: booleanAttribute });
+ readonly isSubmit = input(false, { transform: booleanAttribute });
+ readonly dropdownButton = input(false, { transform: booleanAttribute });
+ readonly size = input(34, { transform: numberAttribute });
+ readonly iconSize = input(14, { transform: numberAttribute });
@Output() readonly action = new EventEmitter();
- ngOnInit() {
- this.#elementRef.nativeElement.style.setProperty('--circle-button-size', `${this.size}px`);
- this.#elementRef.nativeElement.style.setProperty('--circle-button-icon-size', `${this.iconSize}px`);
+ constructor() {
+ effect(() => {
+ this.#elementRef.nativeElement.style.setProperty('--circle-button-size', `${this.size()}px`);
+ this.#elementRef.nativeElement.style.setProperty('--circle-button-icon-size', `${this.iconSize()}px`);
+ });
}
performAction($event: MouseEvent) {
- if (this.removeTooltip) {
- this._matTooltip.hide();
+ if (this.removeTooltip()) {
+ this._matTooltip().hide();
// Timeout to allow tooltip to disappear first,
// useful when removing an item from the list without a confirmation dialog
setTimeout(() => this.action.emit($event));
diff --git a/src/lib/buttons/icon-button/icon-button.component.html b/src/lib/buttons/icon-button/icon-button.component.html
index d9d4967..d086836 100644
--- a/src/lib/buttons/icon-button/icon-button.component.html
+++ b/src/lib/buttons/icon-button/icon-button.component.html
@@ -1,14 +1,19 @@
-
+@if (showDot()) {
+
+}
diff --git a/src/lib/buttons/icon-button/icon-button.component.ts b/src/lib/buttons/icon-button/icon-button.component.ts
index 6f2c018..159a512 100644
--- a/src/lib/buttons/icon-button/icon-button.component.ts
+++ b/src/lib/buttons/icon-button/icon-button.component.ts
@@ -1,37 +1,42 @@
-import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, Output } from '@angular/core';
-import { IconButtonType, IconButtonTypes } from '../types/icon-button.type';
-import { randomString } from '../../utils';
-import { NgClass, NgIf } from '@angular/common';
+import { NgClass } from '@angular/common';
+import { booleanAttribute, ChangeDetectionStrategy, Component, computed, EventEmitter, inject, input, Output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
-import { StopPropagationDirective } from '../../directives';
import { RouterLink } from '@angular/router';
+import { StopPropagationDirective } from '../../directives';
+import { randomString } from '../../utils';
+import { IconButtonType, IconButtonTypes } from '../types/icon-button.type';
@Component({
selector: 'iqser-icon-button',
templateUrl: './icon-button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
- standalone: true,
- imports: [NgClass, MatButtonModule, NgIf, MatIconModule, StopPropagationDirective],
+ imports: [NgClass, MatButtonModule, MatIconModule, StopPropagationDirective],
})
export class IconButtonComponent {
- @Input({ required: true }) label!: string;
- @Input() buttonId = `${randomString()}-icon-button`;
- @Input() icon?: string;
- @Input() showDot = false;
- @Input() active = false;
- @Input() disabled = false;
- @Input() submit = false;
- @Input() type: IconButtonType = IconButtonTypes.default;
- @Output() readonly action = new EventEmitter();
protected readonly _hasRouterLink = !!inject(RouterLink, { optional: true, host: true });
-
- get classes(): Record {
+ readonly label = input.required();
+ readonly buttonId = input(`${randomString()}-icon-button`);
+ readonly icon = input();
+ readonly showDot = input(false, { transform: booleanAttribute });
+ readonly active = input(false, { transform: booleanAttribute });
+ readonly disabled = input(false, { transform: booleanAttribute });
+ readonly submit = input(false, { transform: booleanAttribute });
+ readonly type = input(IconButtonTypes.default);
+ protected readonly _classes = computed(() => {
return {
- overlay: this.showDot,
- [this.type]: true,
- 'has-icon': !!this.icon,
- active: this.active,
+ overlay: this.showDot(),
+ [this.type()]: true,
+ 'has-icon': !!this.icon(),
+ active: this.active(),
};
+ });
+ @Output() readonly action = new EventEmitter();
+
+ emitAction($event: MouseEvent) {
+ const activeElement = document.activeElement as HTMLElement;
+ if (activeElement.tagName?.toLowerCase() === 'button') {
+ this.action.emit($event);
+ }
}
}
diff --git a/src/lib/buttons/index.ts b/src/lib/buttons/index.ts
index 30ba65f..8476a99 100644
--- a/src/lib/buttons/index.ts
+++ b/src/lib/buttons/index.ts
@@ -3,4 +3,3 @@ export * from './types/circle-button.type';
export * from './icon-button/icon-button.component';
export * from './circle-button/circle-button.component';
-export * from './chevron-button/chevron-button.component';
diff --git a/src/lib/buttons/types/circle-button.type.ts b/src/lib/buttons/types/circle-button.type.ts
index dd4494c..349c720 100644
--- a/src/lib/buttons/types/circle-button.type.ts
+++ b/src/lib/buttons/types/circle-button.type.ts
@@ -3,7 +3,6 @@ export const CircleButtonTypes = {
primary: 'primary',
warn: 'warn',
dark: 'dark',
- help: 'help',
} as const;
export type CircleButtonType = keyof typeof CircleButtonTypes;
diff --git a/src/lib/caching/dynamic-cache.ts b/src/lib/caching/dynamic-cache.ts
index d9cc4c4..a3d61e2 100644
--- a/src/lib/caching/dynamic-cache.ts
+++ b/src/lib/caching/dynamic-cache.ts
@@ -1,4 +1,4 @@
-import { List } from '../utils';
+import { List } from '../utils/types/iqser-types';
export interface DynamicCache {
readonly urls: List;
diff --git a/src/lib/dialog/base-dialog.component.ts b/src/lib/dialog/base-dialog.component.ts
index b17e202..6849431 100644
--- a/src/lib/dialog/base-dialog.component.ts
+++ b/src/lib/dialog/base-dialog.component.ts
@@ -1,34 +1,37 @@
-import { AfterViewInit, Directive, HostListener, inject, OnDestroy } from '@angular/core';
-import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { AfterViewInit, Directive, HostListener, inject, OnDestroy, signal } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
-import { hasFormChanged, IqserEventTarget } from '../utils';
-import { ConfirmOptions } from '.';
-import { ConfirmationDialogService } from './confirmation-dialog.service';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { debounceTime, firstValueFrom, fromEvent, merge, of, Subscription } from 'rxjs';
-import { LoadingService } from '../loading';
-import { Toaster } from '../services';
-import { IconButtonTypes } from '../buttons';
import { tap } from 'rxjs/operators';
+import { IconButtonTypes } from '../buttons/types/icon-button.type';
+import { LoadingService } from '../loading/loading.service';
+import { Toaster } from '../services/toaster.service';
+import { hasFormChanged } from '../utils/functions';
+import { IqserEventTarget } from '../utils/types/events.type';
+import { ConfirmationDialogService } from './confirmation-dialog.service';
+import { ConfirmOptions } from './confirmation-dialog/confirmation-dialog.component';
-const TARGET_NODE = 'mat-dialog-container';
+const DIALOG_CONTAINER = 'mat-dialog-container';
+const TEXT_INPUT = 'text';
export interface SaveOptions {
closeAfterSave?: boolean;
+ nextAction?: boolean;
addMembers?: boolean;
}
@Directive()
export abstract class BaseDialogComponent implements AfterViewInit, OnDestroy {
- readonly iconButtonTypes = IconButtonTypes;
- form?: UntypedFormGroup;
- initialFormValue!: Record;
+ readonly #confirmationDialogService = inject(ConfirmationDialogService);
+ readonly #dialog = inject(MatDialog);
+ protected readonly _hasErrors = signal(true);
protected readonly _formBuilder = inject(UntypedFormBuilder);
protected readonly _loadingService = inject(LoadingService);
protected readonly _toaster = inject(Toaster);
- protected readonly _subscriptions: Subscription = new Subscription();
- readonly #confirmationDialogService = inject(ConfirmationDialogService);
- readonly #dialog = inject(MatDialog);
- #hasErrors = false;
+ protected readonly _subscriptions = new Subscription();
+ readonly iconButtonTypes = IconButtonTypes;
+ form?: UntypedFormGroup;
+ initialFormValue!: Record;
protected constructor(
protected readonly _dialogRef: MatDialogRef,
@@ -44,16 +47,19 @@ export abstract class BaseDialogComponent implements AfterViewInit, OnDestroy {
}
get disabled(): boolean {
- return !this.valid || !this.changed || this.#hasErrors;
+ return !this.valid || !this.changed || this._hasErrors();
}
ngAfterViewInit() {
this._subscriptions.add(this._dialogRef.backdropClick().subscribe(() => this.close()));
const valueChanges = this.form?.valueChanges ?? of(null);
const events = [fromEvent(window, 'keyup'), fromEvent(window, 'input'), valueChanges];
+ this._hasErrors.set(!!document.getElementsByClassName('ng-invalid')[0]);
const events$ = merge(...events).pipe(
debounceTime(10),
- tap(() => (this.#hasErrors = !!document.getElementsByClassName('ng-invalid')[0])),
+ tap(() => {
+ this._hasErrors.set(!!document.getElementsByClassName('ng-invalid')[0]);
+ }),
);
this._subscriptions.add(events$.subscribe());
}
@@ -82,9 +88,18 @@ export abstract class BaseDialogComponent implements AfterViewInit, OnDestroy {
@HostListener('window:keydown.Enter', ['$event'])
onEnter(event: KeyboardEvent): void {
- event?.stopImmediatePropagation();
- const node = (event.target as IqserEventTarget).localName?.trim()?.toLowerCase();
- if (this.valid && !this.disabled && this.changed && node === TARGET_NODE) {
+ const target = event.target as IqserEventTarget;
+ const isDialogSelected = target.localName?.trim()?.toLowerCase() === DIALOG_CONTAINER;
+ const isTextInputSelected = target.type?.trim()?.toLowerCase() === TEXT_INPUT;
+
+ if (
+ this.valid &&
+ !this.disabled &&
+ (this.changed || !this._isInEditMode) &&
+ this.#dialog.openDialogs.length === 1 &&
+ (isDialogSelected || isTextInputSelected)
+ ) {
+ event?.stopImmediatePropagation();
this.save();
}
}
diff --git a/src/lib/dialog/confirmation-dialog.service.ts b/src/lib/dialog/confirmation-dialog.service.ts
index dfba015..e7b3b2f 100644
--- a/src/lib/dialog/confirmation-dialog.service.ts
+++ b/src/lib/dialog/confirmation-dialog.service.ts
@@ -1,7 +1,13 @@
import { inject, Injectable } from '@angular/core';
-import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { ConfirmationDialogComponent, ConfirmOption, defaultDialogConfig, IConfirmationDialogData, TitleColors } from '.';
import { MatDialog } from '@angular/material/dialog';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import {
+ ConfirmationDialogComponent,
+ ConfirmOption,
+ IConfirmationDialogData,
+ TitleColors,
+} from './confirmation-dialog/confirmation-dialog.component';
+import { defaultDialogConfig } from './dialog.service';
@Injectable({
providedIn: 'root',
diff --git a/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.html b/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.html
index 015944e..c15018f 100644
--- a/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.html
+++ b/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.html
@@ -1,56 +1,83 @@
-
+ @if (showToast && config.toastMessage) {
+
+ }
+ @if (config.component) {
+
+ }
+
-
+ @if (config.details) {
+
+ }
-
-
-
-
+ @if (config.requireInput) {
+
+
+
+
+ }
-
0" class="mt-24 checkboxes-wrapper">
-
-
- {{ checkbox.label | translate: config.translateParams }}
-
-
-
-
+ @if (config.checkboxes.length > 0) {
+
+ @for (checkbox of config.checkboxes; track checkbox) {
+
+ {{ checkbox.label | translate: config.translateParams }}
+
+
+ }
+
+ }
-
-
+
+ @if (!config.cancelButtonPrimary) {
+
+ } @else {
+
+ {{ config.confirmationText }}
+
+ }
-
+ @if (config.alternativeConfirmationText) {
+
+ }
-
- {{ config.discardChangesText }}
-
+ @if (config.discardChangesText) {
+
+ {{ config.discardChangesText }}
+
+ }
-
- {{ config.denyText }}
-
+ @if (!config.discardChangesText) {
+ @if (config.cancelButtonPrimary) {
+
+ } @else {
+
+ {{ config.denyText }}
+
+ }
+ }
diff --git a/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.scss b/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.scss
index 21c8eab..3269ccc 100644
--- a/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.scss
+++ b/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.scss
@@ -6,3 +6,12 @@
display: flex;
flex-direction: column;
}
+
+.reverse {
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+}
+
+.no-uppercase {
+ text-transform: unset;
+}
diff --git a/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.ts b/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.ts
index 0f1c635..73eb65e 100644
--- a/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.ts
+++ b/src/lib/dialog/confirmation-dialog/confirmation-dialog.component.ts
@@ -1,12 +1,23 @@
-import { ChangeDetectionStrategy, Component, HostListener, inject, TemplateRef } from '@angular/core';
-import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
-import { TranslateModule, TranslateService } from '@ngx-translate/core';
-import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { CircleButtonComponent, IconButtonComponent, IconButtonTypes } from '../../buttons';
-import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
-import { MatIconModule } from '@angular/material/icon';
+import { NgTemplateOutlet } from '@angular/common';
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ HostListener,
+ inject,
+ TemplateRef,
+ Type,
+ viewChild,
+ ViewContainerRef,
+} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
+import { MatIconModule } from '@angular/material/icon';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { TranslateModule, TranslateService } from '@ngx-translate/core';
+import { CircleButtonComponent, IconButtonTypes } from '../../buttons';
+import { IconButtonComponent } from '../../buttons';
import { ValuesOf } from '../../utils';
export const TitleColors = {
@@ -18,7 +29,7 @@ export type TitleColor = ValuesOf
;
export const ConfirmOptions = {
CONFIRM: 1,
- SECOND_CONFIRM: 2,
+ CONFIRM_WITH_ACTION: 2,
DISCARD_CHANGES: 3,
} as const;
@@ -46,6 +57,9 @@ interface InternalConfirmationDialogData {
readonly checkboxes: CheckBox[];
readonly checkboxesValidation: boolean;
readonly toastMessage?: string;
+ readonly component?: Type;
+ readonly componentInputs?: { [key: string]: unknown };
+ readonly cancelButtonPrimary?: boolean;
}
export type IConfirmationDialogData = Partial;
@@ -63,6 +77,9 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
denyText: options?.denyText ?? _('common.confirmation-dialog.deny'),
checkboxes: options?.checkboxes ?? [],
checkboxesValidation: typeof options?.checkboxesValidation === 'boolean' ? options.checkboxesValidation : true,
+ component: options?.component,
+ componentInputs: options?.componentInputs,
+ cancelButtonPrimary: options?.cancelButtonPrimary ?? false,
};
}
@@ -70,12 +87,9 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
templateUrl: './confirmation-dialog.component.html',
styleUrls: ['./confirmation-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
- standalone: true,
imports: [
- NgIf,
MatIconModule,
FormsModule,
- NgForOf,
MatCheckboxModule,
TranslateModule,
NgTemplateOutlet,
@@ -84,13 +98,14 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
MatDialogModule,
],
})
-export class ConfirmationDialogComponent {
+export class ConfirmationDialogComponent implements AfterViewInit {
readonly config = getConfig(inject(MAT_DIALOG_DATA));
inputValue = '';
showToast = false;
readonly inputLabel: string;
readonly confirmOptions = ConfirmOptions;
readonly iconButtonTypes = IconButtonTypes;
+ readonly detailsComponentRef = viewChild.required('detailsComponent', { read: ViewContainerRef });
constructor(
private readonly _dialogRef: MatDialogRef,
@@ -110,18 +125,24 @@ export class ConfirmationDialogComponent {
get confirmOption(): ConfirmOption {
if (!this.config.checkboxesValidation && this.config.checkboxes[0]?.value) {
- return ConfirmOptions.SECOND_CONFIRM;
+ return ConfirmOptions.CONFIRM_WITH_ACTION;
}
return ConfirmOptions.CONFIRM;
}
- @HostListener('window:keyup.enter')
- onKeyupEnter(): void {
- if (this.config.requireInput && !this.confirmationDoesNotMatch()) {
- this.confirm(ConfirmOptions.CONFIRM);
+ @HostListener('window:keyup.enter', ['$event'])
+ onKeyupEnter(event: KeyboardEvent): void {
+ event?.stopImmediatePropagation();
+ if (!this.config.requireInput || !this.confirmationDoesNotMatch()) {
+ if (!this.config.cancelButtonPrimary) this.confirm(ConfirmOptions.CONFIRM);
+ else this.deny();
}
}
+ ngAfterViewInit() {
+ this.#initializeDetailsComponent();
+ }
+
confirmationDoesNotMatch(): boolean {
return this.inputValue.toLowerCase() !== this.config.confirmationText.toLowerCase();
}
@@ -155,4 +176,14 @@ export class ConfirmationDialogComponent {
Object.assign(obj, { [key]: value });
});
}
+
+ #initializeDetailsComponent() {
+ if (!this.config.component) return;
+ const component = this.detailsComponentRef().createComponent(this.config.component);
+ if (this.config.componentInputs) {
+ for (const [key, value] of Object.entries(this.config.componentInputs)) {
+ (component.instance as any)[key] = value;
+ }
+ }
+ }
}
diff --git a/src/lib/dialog/dialog.service.ts b/src/lib/dialog/dialog.service.ts
index 7654395..8cb89e8 100644
--- a/src/lib/dialog/dialog.service.ts
+++ b/src/lib/dialog/dialog.service.ts
@@ -1,8 +1,8 @@
-import { Injectable } from '@angular/core';
-import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { ComponentType } from '@angular/cdk/portal';
-import { mergeMap } from 'rxjs/operators';
+import { Injectable, Type } from '@angular/core';
+import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { from } from 'rxjs';
+import { mergeMap } from 'rxjs/operators';
export const largeDialogConfig: MatDialogConfig = {
width: '90vw',
@@ -64,4 +64,34 @@ export abstract class DialogService {
return ref;
}
+
+ open(
+ type: Type,
+ data?: unknown,
+ config?: object,
+ cb?: (...params: unknown[]) => Promise | void,
+ finallyCb?: (...params: unknown[]) => void | Promise,
+ ): MatDialogRef {
+ const ref = this._dialog.open(type, {
+ ...defaultDialogConfig,
+ ...(config || {}),
+ data,
+ });
+
+ const fn = async (result: unknown) => {
+ if (result && cb) {
+ await cb(result);
+ }
+
+ if (finallyCb) {
+ await finallyCb(result);
+ }
+ };
+
+ ref.afterClosed()
+ .pipe(mergeMap(result => from(fn(result))))
+ .subscribe();
+
+ return ref;
+ }
}
diff --git a/src/lib/dialog/iqser-dialog-component.directive.ts b/src/lib/dialog/iqser-dialog-component.directive.ts
index 98c866f..b4ac0d7 100644
--- a/src/lib/dialog/iqser-dialog-component.directive.ts
+++ b/src/lib/dialog/iqser-dialog-component.directive.ts
@@ -1,29 +1,32 @@
import { Directive, HostListener, inject } from '@angular/core';
-import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { hasFormChanged, IqserEventTarget } from '../utils';
import { FormGroup } from '@angular/forms';
+import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { IconButtonTypes } from '../buttons';
+import { hasFormChanged, IqserEventTarget } from '../utils';
+const DIALOG_CONTAINER = 'mat-dialog-container';
const DATA_TYPE_SYMBOL = Symbol.for('DATA_TYPE');
const RETURN_TYPE_SYMBOL = Symbol.for('RETURN_TYPE');
export type DATA_TYPE = typeof DATA_TYPE_SYMBOL;
export type RETURN_TYPE = typeof RETURN_TYPE_SYMBOL;
-const TARGET_NODE = 'mat-dialog-container';
-
@Directive()
-export abstract class IqserDialogComponent {
+export abstract class IqserDialogComponent {
readonly [DATA_TYPE_SYMBOL]!: DataType;
readonly [RETURN_TYPE_SYMBOL]!: ReturnType;
+ readonly iconButtonTypes = IconButtonTypes;
readonly dialogRef = inject(MatDialogRef);
readonly data = inject(MAT_DIALOG_DATA);
readonly dialog = inject(MatDialog);
readonly form?: FormGroup;
+ readonly ignoredKeys: string[] = [];
+
initialFormValue: Record = {};
- constructor() {
+ constructor(private readonly _editMode = false) {
this.dialogRef
.backdropClick()
.pipe(takeUntilDestroyed())
@@ -36,7 +39,7 @@ export abstract class IqserDialogComponent
}
get changed(): boolean {
- return !this.form || hasFormChanged(this.form, this.initialFormValue);
+ return !this.form || hasFormChanged(this.form, this.initialFormValue, this.ignoredKeys);
}
get disabled(): boolean {
@@ -53,14 +56,18 @@ export abstract class IqserDialogComponent
@HostListener('window:keydown.Enter', ['$event'])
onEnter(event: KeyboardEvent): void {
event?.stopImmediatePropagation();
- const node = (event.target as IqserEventTarget).localName?.trim()?.toLowerCase();
- if (this.onEnterValidator(event) && node === TARGET_NODE) {
+ if (this.onEnterValidator(event)) {
this.close();
}
}
onEnterValidator(event: KeyboardEvent) {
- return this.valid && !this.disabled && this.changed;
+ const targetElement = (event.target as IqserEventTarget).localName?.trim()?.toLowerCase();
+ const canClose = targetElement === DIALOG_CONTAINER && this.valid;
+ if (this._editMode) {
+ return canClose && this.changed;
+ }
+ return canClose;
}
close(dialogResult?: ReturnType) {
diff --git a/src/lib/dialog/iqser-dialog.service.ts b/src/lib/dialog/iqser-dialog.service.ts
index 599d65b..806c5b8 100644
--- a/src/lib/dialog/iqser-dialog.service.ts
+++ b/src/lib/dialog/iqser-dialog.service.ts
@@ -13,10 +13,11 @@ export class IqserDialog {
open<
Component extends IqserDialogComponent,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
Data extends Component[DATA_TYPE] = Component[DATA_TYPE],
Return extends Component[RETURN_TYPE] = Component[RETURN_TYPE],
- >(dialog: ComponentType, config?: MatDialogConfig) {
- const ref = this._dialog.open(dialog, config);
+ >(dialog: ComponentType, config?: MatDialogConfig) {
+ const ref = this._dialog.open(dialog, config);
return {
...ref,
result() {
@@ -29,7 +30,7 @@ export class IqserDialog {
Component extends IqserDialogComponent,
Data extends Component[DATA_TYPE] = Component[DATA_TYPE],
Return extends Component[RETURN_TYPE] = Component[RETURN_TYPE],
- >(dialog: ComponentType, config?: MatDialogConfig) {
+ >(dialog: ComponentType, config?: MatDialogConfig) {
return this.open(dialog, { ...largeDialogConfig, ...config });
}
@@ -37,7 +38,7 @@ export class IqserDialog {
Component extends IqserDialogComponent,
Data extends Component[DATA_TYPE] = Component[DATA_TYPE],
Return extends Component[RETURN_TYPE] = Component[RETURN_TYPE],
- >(dialog: ComponentType, config?: MatDialogConfig) {
+ >(dialog: ComponentType, config?: MatDialogConfig) {
return this.open(dialog, { ...defaultDialogConfig, ...config });
}
}
diff --git a/src/lib/directives/disable-stop-propagation.directive.ts b/src/lib/directives/disable-stop-propagation.directive.ts
index 64d9de0..a1edccb 100644
--- a/src/lib/directives/disable-stop-propagation.directive.ts
+++ b/src/lib/directives/disable-stop-propagation.directive.ts
@@ -1,9 +1,8 @@
-import { booleanAttribute, Directive, Input } from '@angular/core';
+import { booleanAttribute, Directive, input } from '@angular/core';
@Directive({
selector: '[iqserDisableStopPropagation]',
- standalone: true,
})
export class DisableStopPropagationDirective {
- @Input({ transform: booleanAttribute }) iqserDisableStopPropagation = true;
+ readonly iqserDisableStopPropagation = input(true, { transform: booleanAttribute });
}
diff --git a/src/lib/directives/has-scrollbar.directive.ts b/src/lib/directives/has-scrollbar.directive.ts
index 3b94ed5..561f7ff 100644
--- a/src/lib/directives/has-scrollbar.directive.ts
+++ b/src/lib/directives/has-scrollbar.directive.ts
@@ -1,18 +1,24 @@
-import { ChangeDetectorRef, Directive, ElementRef, HostBinding, HostListener, OnChanges, OnInit } from '@angular/core';
+import { Directive, ElementRef, OnDestroy, OnInit, signal } from '@angular/core';
@Directive({
selector: '[iqserHasScrollbar]',
- standalone: true,
+ host: {
+ '[class]': '_class()',
+ },
})
-export class HasScrollbarDirective implements OnInit, OnChanges {
- @HostBinding('class') class = '';
+export class HasScrollbarDirective implements OnInit, OnDestroy {
+ private readonly _resizeObserver: ResizeObserver;
+ protected readonly _class = signal('');
- constructor(
- protected readonly _elementRef: ElementRef,
- protected readonly _changeDetector: ChangeDetectorRef,
- ) {}
+ constructor(protected readonly _elementRef: ElementRef) {
+ this._resizeObserver = new ResizeObserver(() => {
+ this.process();
+ });
- get hasScrollbar() {
+ this._resizeObserver.observe(this._elementRef.nativeElement);
+ }
+
+ private get _hasScrollbar() {
const element = this._elementRef?.nativeElement as HTMLElement;
return element.clientHeight < element.scrollHeight;
}
@@ -21,16 +27,12 @@ export class HasScrollbarDirective implements OnInit, OnChanges {
setTimeout(() => this.process(), 0);
}
- @HostListener('window:resize')
process() {
- const newClass = this.hasScrollbar ? 'has-scrollbar' : '';
- if (this.class !== newClass) {
- this.class = newClass;
- this._changeDetector.markForCheck();
- }
+ const newClass = this._hasScrollbar ? 'has-scrollbar' : '';
+ this._class.set(newClass);
}
- ngOnChanges() {
- this.process();
+ ngOnDestroy() {
+ this._resizeObserver.unobserve(this._elementRef.nativeElement);
}
}
diff --git a/src/lib/directives/hidden-action.directive.ts b/src/lib/directives/hidden-action.directive.ts
index 23940c6..d72391f 100644
--- a/src/lib/directives/hidden-action.directive.ts
+++ b/src/lib/directives/hidden-action.directive.ts
@@ -2,7 +2,6 @@ import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/c
@Directive({
selector: '[iqserHiddenAction]',
- standalone: true,
})
export class HiddenActionDirective {
@Input() requiredClicks = 4;
diff --git a/src/lib/directives/prevent-default.directive.ts b/src/lib/directives/prevent-default.directive.ts
index a63e9d2..b083e5e 100644
--- a/src/lib/directives/prevent-default.directive.ts
+++ b/src/lib/directives/prevent-default.directive.ts
@@ -3,7 +3,6 @@ import { NGXLogger } from 'ngx-logger';
@Directive({
selector: '[iqserPreventDefault]',
- standalone: true,
})
export class PreventDefaultDirective {
readonly #logger = inject(NGXLogger);
diff --git a/src/lib/directives/stop-propagation.directive.ts b/src/lib/directives/stop-propagation.directive.ts
index 211bfb3..481bd3c 100644
--- a/src/lib/directives/stop-propagation.directive.ts
+++ b/src/lib/directives/stop-propagation.directive.ts
@@ -1,10 +1,9 @@
import { booleanAttribute, Directive, HostListener, inject, Input } from '@angular/core';
-import { DisableStopPropagationDirective } from './disable-stop-propagation.directive';
import { NGXLogger } from 'ngx-logger';
+import { DisableStopPropagationDirective } from './disable-stop-propagation.directive';
@Directive({
selector: '[iqserStopPropagation]',
- standalone: true,
})
export class StopPropagationDirective {
readonly #disableStopPropagation = inject(DisableStopPropagationDirective, { optional: true });
@@ -13,13 +12,14 @@ export class StopPropagationDirective {
@HostListener('click', ['$event'])
onClick($event: Event) {
- if (this.#disableStopPropagation?.iqserDisableStopPropagation) {
+ if (this.#disableStopPropagation?.iqserDisableStopPropagation()) {
this.#logger.info('[CLICK] iqserStopPropagation is disabled by iqserDisableStopPropagation');
return;
}
if (this.iqserStopPropagation) {
this.#logger.info('[CLICK] iqserStopPropagation');
+ $event.preventDefault();
$event.stopPropagation();
}
}
diff --git a/src/lib/directives/sync-width.directive.ts b/src/lib/directives/sync-width.directive.ts
index 11d0eb9..b99cccd 100644
--- a/src/lib/directives/sync-width.directive.ts
+++ b/src/lib/directives/sync-width.directive.ts
@@ -2,7 +2,6 @@ import { Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/
@Directive({
selector: '[iqserSyncWidth]',
- standalone: true,
})
export class SyncWidthDirective implements OnDestroy {
@Input() iqserSyncWidth!: string;
diff --git a/src/lib/empty-state/empty-state.component.html b/src/lib/empty-state/empty-state.component.html
index 49e188f..a856539 100644
--- a/src/lib/empty-state/empty-state.component.html
+++ b/src/lib/empty-state/empty-state.component.html
@@ -1,26 +1,22 @@
-
-
+
+ @if (icon(); as icon) {
+
+ }
-
+
-
+ @if (showButton() && this.action.observed) {
+
+ }
diff --git a/src/lib/empty-state/empty-state.component.ts b/src/lib/empty-state/empty-state.component.ts
index 622c31b..9e3cd93 100644
--- a/src/lib/empty-state/empty-state.component.ts
+++ b/src/lib/empty-state/empty-state.component.ts
@@ -1,33 +1,42 @@
-import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
-import { IconButtonComponent, IconButtonTypes } from '../buttons';
-import { randomString } from '../utils';
-import { NgIf, NgStyle } from '@angular/common';
+import { NgStyle } from '@angular/common';
+import {
+ booleanAttribute,
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ EventEmitter,
+ input,
+ numberAttribute,
+ Output,
+} from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
-import { IqserHelpModeModule } from '../help-mode';
+import { IconButtonComponent } from '../buttons/icon-button/icon-button.component';
+import { IconButtonTypes } from '../buttons/types/icon-button.type';
+import { randomString } from '../utils/functions';
@Component({
- selector: 'iqser-empty-state [text]',
+ selector: 'iqser-empty-state',
templateUrl: './empty-state.component.html',
styleUrls: ['./empty-state.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
- standalone: true,
- imports: [NgStyle, MatIconModule, NgIf, IconButtonComponent, IqserHelpModeModule],
+ imports: [NgStyle, MatIconModule, IconButtonComponent],
})
-export class EmptyStateComponent implements OnInit {
- readonly iconButtonTypes = IconButtonTypes;
+export class EmptyStateComponent {
+ protected readonly iconButtonTypes = IconButtonTypes;
- @Input() text!: string;
- @Input() icon?: string;
- @Input() showButton = true;
- @Input() buttonIcon = 'iqser:plus';
- @Input() buttonLabel?: string;
- @Input() buttonId = `${randomString()}-icon-button`;
- @Input() horizontalPadding = 100;
- @Input() verticalPadding = 120;
- @Input() helpModeKey?: string;
+ readonly text = input.required
();
+ readonly icon = input();
+ readonly showButton = input(true, { transform: booleanAttribute });
+ readonly buttonIcon = input('iqser:plus');
+ readonly buttonLabel = input();
+ readonly buttonId = input(`${randomString()}-icon-button`);
+ readonly horizontalPadding = input(100, { transform: numberAttribute });
+ readonly verticalPadding = input(120, { transform: numberAttribute });
+ protected readonly styles = computed(() => ({
+ 'padding-top': this.verticalPadding() + 'px',
+ 'padding-left': this.horizontalPadding() + 'px',
+ 'padding-right': this.horizontalPadding() + 'px',
+ }));
+ readonly helpModeKey = input();
@Output() readonly action = new EventEmitter();
-
- ngOnInit(): void {
- this.showButton = this.showButton && this.action.observed;
- }
}
diff --git a/src/lib/error/connection-status/connection-status.component.html b/src/lib/error/connection-status/connection-status.component.html
index 45b1490..4c7cc8e 100644
--- a/src/lib/error/connection-status/connection-status.component.html
+++ b/src/lib/error/connection-status/connection-status.component.html
@@ -1,8 +1,5 @@
-
-
-
+@if (connectionStatus(); as status) {
+
+
+
+}
diff --git a/src/lib/error/connection-status/connection-status.component.ts b/src/lib/error/connection-status/connection-status.component.ts
index 04f3e36..0f8dfc2 100644
--- a/src/lib/error/connection-status/connection-status.component.ts
+++ b/src/lib/error/connection-status/connection-status.component.ts
@@ -1,6 +1,7 @@
-import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
-import { connectionStatusTranslations } from '../../translations';
import { animate, state, style, transition, trigger } from '@angular/animations';
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { connectionStatusTranslations } from '../../translations';
import { ErrorService } from '../error.service';
@Component({
@@ -16,8 +17,9 @@ import { ErrorService } from '../error.service';
]),
],
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: false,
})
export class ConnectionStatusComponent {
- connectionStatusTranslations = connectionStatusTranslations;
- protected readonly errorService = inject(ErrorService);
+ protected readonly connectionStatusTranslations = connectionStatusTranslations;
+ protected readonly connectionStatus = toSignal(inject(ErrorService).connectionStatus$);
}
diff --git a/src/lib/error/full-page-error/full-page-error.component.html b/src/lib/error/full-page-error/full-page-error.component.html
index eb9b099..cf35e6b 100644
--- a/src/lib/error/full-page-error/full-page-error.component.html
+++ b/src/lib/error/full-page-error/full-page-error.component.html
@@ -1,13 +1,11 @@
-
+@if (errorService.error$ | async; as error) {
-
-
-
- {{ error.message }}
-
+ @if (error.message) {
+ {{ error.message }}
+ }
-
+}
diff --git a/src/lib/error/full-page-error/full-page-error.component.ts b/src/lib/error/full-page-error/full-page-error.component.ts
index 0a2f7a6..f2bbf75 100644
--- a/src/lib/error/full-page-error/full-page-error.component.ts
+++ b/src/lib/error/full-page-error/full-page-error.component.ts
@@ -1,18 +1,18 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { IconButtonTypes } from '../../buttons';
import { CustomError, ErrorService, ErrorType } from '../error.service';
-import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'iqser-full-page-error',
templateUrl: './full-page-error.component.html',
styleUrls: ['./full-page-error.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: false,
})
export class FullPageErrorComponent {
- readonly iconButtonTypes = IconButtonTypes;
-
- constructor(readonly errorService: ErrorService) {}
+ protected readonly iconButtonTypes = IconButtonTypes;
+ protected readonly errorService = inject(ErrorService);
errorTitle(error: ErrorType): string {
return error instanceof CustomError ? error.label : _('error.title');
diff --git a/src/lib/error/server-error-interceptor.ts b/src/lib/error/server-error-interceptor.ts
index 98f4b46..565d5cf 100644
--- a/src/lib/error/server-error-interceptor.ts
+++ b/src/lib/error/server-error-interceptor.ts
@@ -8,13 +8,12 @@ import {
HttpStatusCode,
} from '@angular/common/http';
import { Inject, Injectable, Optional } from '@angular/core';
-import { MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs';
+import { finalize, MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { MAX_RETRIES_ON_SERVER_ERROR, SERVER_ERROR_SKIP_PATHS } from './tokens';
import { ErrorService } from './error.service';
-import { KeycloakService } from 'keycloak-angular';
-import { IqserConfigService } from '../services';
import { KeycloakStatusService } from '../tenants';
+import { LoadingService } from '../loading';
function updateSeconds(seconds: number) {
if (seconds === 0 || seconds === 1) {
@@ -56,8 +55,7 @@ export class ServerErrorInterceptor implements HttpInterceptor {
constructor(
private readonly _errorService: ErrorService,
- private readonly _keycloakService: KeycloakService,
- private readonly _configService: IqserConfigService,
+ private readonly _loadingService: LoadingService,
private readonly _keycloakStatusService: KeycloakStatusService,
@Optional() @Inject(MAX_RETRIES_ON_SERVER_ERROR) private readonly _maxRetries: number,
@Optional() @Inject(SERVER_ERROR_SKIP_PATHS) private readonly _skippedPaths: string[],
@@ -84,7 +82,11 @@ export class ServerErrorInterceptor implements HttpInterceptor {
this._urlsWithError.add(req.url);
}
- return throwError(() => error);
+ return throwError(() => error).pipe(
+ finalize(() => {
+ this._loadingService.stop();
+ }),
+ );
}),
backoffOnServerError(this._maxRetries, this._skippedPaths),
);
diff --git a/src/lib/filtering/filter-card/filter-card.component.html b/src/lib/filtering/filter-card/filter-card.component.html
index fae3b07..9238f21 100644
--- a/src/lib/filtering/filter-card/filter-card.component.html
+++ b/src/lib/filtering/filter-card/filter-card.component.html
@@ -1,42 +1,51 @@
-
-
+@if (primaryFilterGroup$ | async; as primaryGroup) {
+ @if (primaryGroup.filterceptionPlaceholder) {
+
+
+
+ }
-
-
-
-
-
-
-
+ @if (secondaryFilterGroup$ | async; as secondaryGroup) {
+
+
+
+ @for (filter of secondaryGroup.filters; track filter.id) {
+
+ }
+
+ }
+}
{{ filter?.label }}
@@ -44,30 +53,51 @@
-
-
+
diff --git a/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts
index 8d25698..76fb615 100644
--- a/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts
+++ b/src/lib/help-mode/help-mode-dialog/help-mode-dialog.component.ts
@@ -1,4 +1,9 @@
-import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
+import { MatDialogRef } from '@angular/material/dialog';
+import { Subscription } from 'rxjs';
+import { MatCheckbox } from '@angular/material/checkbox';
+import { CircleButtonComponent } from '../../buttons';
+import { TranslateModule } from '@ngx-translate/core';
const HIGHER_CDK_OVERLAY_CONTAINER_ZINDEX = '1200';
const DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX = '800';
@@ -7,17 +12,36 @@ const DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX = '800';
templateUrl: './help-mode-dialog.component.html',
styleUrls: ['./help-mode-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [MatCheckbox, CircleButtonComponent, TranslateModule],
})
export class HelpModeDialogComponent implements OnInit, OnDestroy {
+ #backdropClickSubscription: Subscription;
+ protected doNotShowAgainOption = false;
+
+ constructor(protected readonly _dialogRef: MatDialogRef) {
+ this.#backdropClickSubscription = this._dialogRef.backdropClick().subscribe(() => this.close());
+ }
+
+ @HostListener('window:keydown.Enter', ['$event'])
+ @HostListener('window:keydown.Escape', ['$event'])
+ close() {
+ return this._dialogRef.close(this.doNotShowAgainOption);
+ }
+
ngOnInit(): void {
- this._setCdkOverlayContainerZindex(HIGHER_CDK_OVERLAY_CONTAINER_ZINDEX);
+ this._setCdkOverlayContainerZIndex(HIGHER_CDK_OVERLAY_CONTAINER_ZINDEX);
}
ngOnDestroy(): void {
- this._setCdkOverlayContainerZindex(DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX);
+ this._setCdkOverlayContainerZIndex(DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX);
+ this.#backdropClickSubscription.unsubscribe();
}
- private _setCdkOverlayContainerZindex(zIndex: string): void {
+ setDoNotShowAgainOption(checked: boolean): void {
+ this.doNotShowAgainOption = checked;
+ }
+
+ private _setCdkOverlayContainerZIndex(zIndex: string): void {
const cdkOverlayContainer = document.querySelector('.cdk-overlay-container');
if (cdkOverlayContainer) {
cdkOverlayContainer.style.zIndex = zIndex;
diff --git a/src/lib/help-mode/help-mode.module.ts b/src/lib/help-mode/help-mode.module.ts
deleted file mode 100644
index f0250c7..0000000
--- a/src/lib/help-mode/help-mode.module.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { ModuleWithProviders, NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { TranslateModule } from '@ngx-translate/core';
-import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
-import { HelpModeComponent } from './help-mode/help-mode.component';
-import { HelpButtonComponent } from './help-button/help-button.component';
-import { HelpModeKey, HelpModeService } from './help-mode.service';
-import { MatDialogModule } from '@angular/material/dialog';
-import { CircleButtonComponent } from '../buttons';
-import { HELP_MODE_KEYS } from './tokens';
-
-const components = [HelpModeComponent, HelpModeDialogComponent, HelpButtonComponent];
-
-@NgModule({
- declarations: [...components],
- imports: [CommonModule, MatDialogModule, TranslateModule, CircleButtonComponent],
- exports: [...components],
-})
-export class IqserHelpModeModule {
- static forRoot(helpModeKeys: HelpModeKey[]): ModuleWithProviders {
- return {
- ngModule: IqserHelpModeModule,
- providers: [{ provide: HELP_MODE_KEYS, useValue: helpModeKeys }, HelpModeService],
- };
- }
-}
diff --git a/src/lib/help-mode/help-mode.service.ts b/src/lib/help-mode/help-mode.service.ts
index 287a047..38cc90c 100644
--- a/src/lib/help-mode/help-mode.service.ts
+++ b/src/lib/help-mode/help-mode.service.ts
@@ -1,22 +1,26 @@
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
-import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
+import { getConfig } from '../services';
+import { IqserUserPreferenceService } from '../services';
import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
import { HELP_MODE_KEYS, MANUAL_BASE_URL } from './tokens';
+import { HelpModeKey } from './types';
import {
- ANNOTATIONS_LIST_ID,
+ DOCUMINE_THEME_CLASS,
HELP_HIGHLIGHT_CLASS,
HELP_MODE_CLASS,
OVERLAPPING_DROPDOWNS_IDS,
OverlappingElement,
PDF_TRON_IFRAME_ID,
SCROLL_BUTTONS_IDS,
+ SCROLLABLE_PARENT_VIEWS_IDS,
ScrollableParentView,
ScrollableParentViews,
- VIRTUAL_SCROLL_ID,
WEB_VIEWER_ELEMENTS,
} from './utils/constants';
+import { toSignal } from '@angular/core/rxjs-interop';
export interface Helper {
readonly element: HTMLElement;
@@ -27,24 +31,17 @@ export interface Helper {
readonly iframeElement?: boolean;
}
-export interface HelpModeKey {
- readonly elementKey: string;
- readonly documentKey: string;
- readonly scrollableParentView?: ScrollableParentView;
- readonly overlappingElements?: OverlappingElement[];
- readonly dialogElement?: boolean;
-}
-
@Injectable()
export class HelpModeService {
- helpButtonKey: string | undefined;
readonly #isHelpModeActive$ = new BehaviorSubject(false);
- readonly isHelpModeActive$ = this.#isHelpModeActive$.asObservable();
readonly #helpModeDialogIsOpened$ = new BehaviorSubject(false);
- readonly helpModeDialogIsOpened$ = this.#helpModeDialogIsOpened$.asObservable();
readonly #renderer: Renderer2;
+ readonly #isDocumine = getConfig().IS_DOCUMINE;
#helpers: Record = {};
#dialogMode = false;
+ readonly isHelpModeActive$ = this.#isHelpModeActive$.asObservable();
+ readonly isHelpModeActive = toSignal(this.isHelpModeActive$, { initialValue: false });
+ readonly helpModeDialogIsOpened$ = this.#helpModeDialogIsOpened$.asObservable();
constructor(
@Inject(HELP_MODE_KEYS) private readonly _keys: HelpModeKey[],
@@ -52,33 +49,34 @@ export class HelpModeService {
private readonly _dialog: MatDialog,
private readonly _rendererFactory: RendererFactory2,
private readonly _translateService: TranslateService,
+ private readonly _iqserUserPreferenceService: IqserUserPreferenceService,
) {
this.#renderer = this._rendererFactory.createRenderer(null, null);
}
- get isHelpModeActive(): boolean {
- return this.#isHelpModeActive$.getValue();
- }
-
get helpModeDialogIsOpened(): boolean {
return this.#helpModeDialogIsOpened$.getValue();
}
- openHelpModeDialog(): MatDialogRef {
- this.#helpModeDialogIsOpened$.next(true);
+ async openHelpModeDialog() {
+ if (!this._iqserUserPreferenceService.getHelpModeDialog()) {
+ this.#helpModeDialogIsOpened$.next(true);
- const ref = this._dialog.open(HelpModeDialogComponent, {
- width: '600px',
- });
+ const ref = this._dialog.open(HelpModeDialogComponent, {
+ width: '600px',
+ });
- firstValueFrom(ref.afterClosed()).then(() => {
- this.#helpModeDialogIsOpened$.next(false);
- });
- return ref;
+ firstValueFrom(ref.afterClosed()).then(result => {
+ this.#helpModeDialogIsOpened$.next(false);
+ if (result) {
+ this._iqserUserPreferenceService.toggleHelpModeDialog();
+ }
+ });
+ }
}
activateHelpMode(dialogMode: boolean = false): void {
- if (!this.isHelpModeActive) {
+ if (!this.isHelpModeActive()) {
document.body.style.setProperty('overflow', 'unset');
this.#isHelpModeActive$.next(true);
this.openHelpModeDialog();
@@ -92,7 +90,7 @@ export class HelpModeService {
}
deactivateHelpMode(): void {
- if (this.isHelpModeActive) {
+ if (this.isHelpModeActive()) {
document.body.style.removeProperty('overflow');
this.#isHelpModeActive$.next(false);
this.#disableHelperElements();
@@ -100,6 +98,19 @@ export class HelpModeService {
}
}
+ highlightHelperElements(): void {
+ Object.values(this.#helpers).forEach(helper => {
+ this.#renderer.addClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
+ setTimeout(() => {
+ this.#renderer.removeClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
+ }, 500);
+ });
+ }
+
+ updateHelperElements() {
+ Object.values(this.#helpers).forEach(helper => this.#updateHelperElement(helper));
+ }
+
#createHelpers() {
for (const key of Object.values(this._keys)) {
const elements = document.querySelectorAll(`[help-mode-key='${key.elementKey}']`);
@@ -135,9 +146,12 @@ export class HelpModeService {
#getHelperElement(element: HTMLElement, key: string): HTMLElement {
const helperElement = this.#renderer.createElement('a') as HTMLElement;
- this.#renderer.setAttribute(helperElement, 'href', this.generateDocsLink(key));
+ this.#renderer.setAttribute(helperElement, 'href', this.#generateDocsLink(key));
this.#renderer.setAttribute(helperElement, 'target', '_blank');
this.#renderer.addClass(helperElement, HELP_MODE_CLASS);
+ if (this.#isDocumine) {
+ this.#renderer.addClass(helperElement, DOCUMINE_THEME_CLASS);
+ }
return helperElement;
}
@@ -145,24 +159,11 @@ export class HelpModeService {
return Math.random().toString(36).substring(2, 9);
}
- generateDocsLink(key: string) {
+ #generateDocsLink(key: string) {
const currentLang = this._translateService.currentLang;
return `${this._manualBaseURL}/${currentLang}/index-${currentLang}.html?contextId=${key}`;
}
- highlightHelperElements(): void {
- Object.values(this.#helpers).forEach(helper => {
- this.#renderer.addClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
- setTimeout(() => {
- this.#renderer.removeClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
- }, 500);
- });
- }
-
- updateHelperElements() {
- Object.values(this.#helpers).forEach(helper => this.#updateHelperElement(helper));
- }
-
#isElementVisible(helper: Helper): boolean {
if (helper.iframeElement && !this.#isFilePreviewPage()) {
return false;
@@ -174,8 +175,7 @@ export class HelpModeService {
}
if (helper.scrollableParentView) {
- const scrollableElementId =
- helper.scrollableParentView === ScrollableParentViews.VIRTUAL_SCROLL ? VIRTUAL_SCROLL_ID : ANNOTATIONS_LIST_ID;
+ const scrollableElementId = SCROLLABLE_PARENT_VIEWS_IDS[helper.scrollableParentView];
const scrollableElement: HTMLElement = document.getElementById(scrollableElementId);
if (!scrollableElement) {
@@ -242,6 +242,7 @@ export class HelpModeService {
const iframe: HTMLIFrameElement = document.getElementById(PDF_TRON_IFRAME_ID) as HTMLIFrameElement;
const iframeRect = iframe.getBoundingClientRect();
dimensions.y += iframeRect.top;
+ dimensions.x += iframeRect.left;
}
helper.helperElement.style.cssText = `
diff --git a/src/lib/help-mode/help-mode/help-mode.component.html b/src/lib/help-mode/help-mode/help-mode.component.html
index 0f59d6e..f9d996c 100644
--- a/src/lib/help-mode/help-mode/help-mode.component.html
+++ b/src/lib/help-mode/help-mode/help-mode.component.html
@@ -1,20 +1,23 @@
-
-
-
-
{{ 'help-mode.bottom-text' | translate }}
-
- {{ 'help-mode.instructions' | translate }}
-
-
- (esc)
-
+@if (helpModeService.isHelpModeActive$ | async) {
+
-
+}
diff --git a/src/lib/help-mode/help-mode/help-mode.component.ts b/src/lib/help-mode/help-mode/help-mode.component.ts
index 6ff53e3..7a8d6d2 100644
--- a/src/lib/help-mode/help-mode/help-mode.component.ts
+++ b/src/lib/help-mode/help-mode/help-mode.component.ts
@@ -2,13 +2,16 @@ import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core'
import { HelpModeService } from '../help-mode.service';
import { IqserEventTarget } from '../../utils';
import { MatDialog } from '@angular/material/dialog';
-import { CircleButtonTypes } from '../../buttons';
+import { CircleButtonComponent, CircleButtonTypes } from '../../buttons';
+import { AsyncPipe } from '@angular/common';
+import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'iqser-help-mode',
templateUrl: './help-mode.component.html',
styleUrls: ['./help-mode.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [AsyncPipe, TranslateModule, CircleButtonComponent],
})
export class HelpModeComponent {
readonly circleButtonTypes = CircleButtonTypes;
@@ -20,7 +23,7 @@ export class HelpModeComponent {
@HostListener('document:keydown.escape', ['$event'])
onEscKeydownHandler(event: KeyboardEvent): void {
- if (!this.helpModeService.helpModeDialogIsOpened && this.helpModeService.isHelpModeActive) {
+ if (!this.helpModeService.helpModeDialogIsOpened && this.helpModeService.isHelpModeActive()) {
event?.stopPropagation();
this.helpModeService.deactivateHelpMode();
}
@@ -29,12 +32,7 @@ export class HelpModeComponent {
@HostListener('document:keydown.h', ['$event'])
onHKeydownHandler(event: KeyboardEvent): void {
const node = (event.target as IqserEventTarget).localName;
- if (!this.helpModeService.isHelpModeActive && node !== 'input' && node !== 'textarea') {
- if (this.helpModeService.helpButtonKey) {
- const url = this.helpModeService.generateDocsLink(this.helpModeService.helpButtonKey);
- window.open(url, '_blank');
- return;
- }
+ if (!this.helpModeService.isHelpModeActive() && node !== 'input' && node !== 'textarea') {
const dialogMode = !!this._dialog.openDialogs.length;
this.helpModeService.activateHelpMode(dialogMode);
}
@@ -42,14 +40,14 @@ export class HelpModeComponent {
@HostListener('click')
onClick(): void {
- if (this.helpModeService.isHelpModeActive) {
+ if (this.helpModeService.isHelpModeActive()) {
this.helpModeService.highlightHelperElements();
}
}
@HostListener('window:resize')
onResize() {
- if (this.helpModeService.isHelpModeActive) {
+ if (this.helpModeService.isHelpModeActive()) {
this.helpModeService.updateHelperElements();
}
}
diff --git a/src/lib/help-mode/index.ts b/src/lib/help-mode/index.ts
index ab6c4be..f8a16c0 100644
--- a/src/lib/help-mode/index.ts
+++ b/src/lib/help-mode/index.ts
@@ -1,7 +1,7 @@
export * from './tokens';
-export * from './help-mode.module';
export * from './help-mode.service';
export * from './help-mode/help-mode.component';
export * from './help-button/help-button.component';
export * from './help-mode-dialog/help-mode-dialog.component';
export * from './utils/constants';
+export * from './utils/help-mode.provider';
diff --git a/src/lib/help-mode/tokens.ts b/src/lib/help-mode/tokens.ts
index c842b24..46546ea 100644
--- a/src/lib/help-mode/tokens.ts
+++ b/src/lib/help-mode/tokens.ts
@@ -1,6 +1,6 @@
import { inject, InjectionToken } from '@angular/core';
-import { IqserConfigService } from '../services';
-import { HelpModeKey } from './help-mode.service';
+import { IqserConfigService } from '../services/iqser-config.service';
+import { HelpModeKey } from './types';
export const HELP_MODE_KEYS = new InjectionToken
('Help mode keys');
export const MANUAL_BASE_URL = new InjectionToken('Base manual URL', {
diff --git a/src/lib/help-mode/types.ts b/src/lib/help-mode/types.ts
new file mode 100644
index 0000000..6be4340
--- /dev/null
+++ b/src/lib/help-mode/types.ts
@@ -0,0 +1,9 @@
+import { OverlappingElement, ScrollableParentView } from './utils/constants';
+
+export interface HelpModeKey {
+ readonly elementKey: string;
+ readonly documentKey: string;
+ readonly scrollableParentView?: ScrollableParentView;
+ readonly overlappingElements?: OverlappingElement[];
+ readonly dialogElement?: boolean;
+}
diff --git a/src/lib/help-mode/utils/constants.ts b/src/lib/help-mode/utils/constants.ts
index d97a3cf..b0cbf3e 100644
--- a/src/lib/help-mode/utils/constants.ts
+++ b/src/lib/help-mode/utils/constants.ts
@@ -1,22 +1,32 @@
-export const VIRTUAL_SCROLL_ID = 'virtual-scroll';
-export const ANNOTATIONS_LIST_ID = 'annotations-list';
export const OVERLAPPING_DROPDOWNS_IDS = {
USER_MENU: 'user-menu-items',
WORKLOAD_FILTER: 'workload-filters',
DOCUMENT_INFO: 'document-info',
+ BREADCRUMBS_MENU: 'breadcrumbs-menu-items',
};
export const SCROLL_BUTTONS_IDS = ['scroll-up', 'scroll-down'];
export const PDF_TRON_IFRAME_ID = 'webviewer-1';
export const WEB_VIEWER_ELEMENTS = [
{
querySelector: '.HeaderItems',
- documentKey: 'pdf_features',
+ documentKey: 'document_viewer_features',
},
];
export const ScrollableParentViews = {
VIRTUAL_SCROLL: 'VIRTUAL_SCROLL',
ANNOTATIONS_LIST: 'ANNOTATIONS_LIST',
+ SCM_EDIT_DIALOG: 'SCM_EDIT_DIALOG',
+ WORKFLOW_VIEW: 'WORKFLOW_VIEW',
+ COMPONENTS_VIEW: 'COMPONENTS_VIEW',
+} as const;
+
+export const SCROLLABLE_PARENT_VIEWS_IDS = {
+ VIRTUAL_SCROLL: 'virtual-scroll',
+ ANNOTATIONS_LIST: 'annotations-list',
+ SCM_EDIT_DIALOG: 'scm-edit',
+ WORKFLOW_VIEW: 'workflow-view',
+ COMPONENTS_VIEW: 'components-view',
} as const;
export type ScrollableParentView = keyof typeof ScrollableParentViews;
@@ -25,9 +35,11 @@ export const OverlappingElements = {
USER_MENU: 'USER_MENU',
WORKLOAD_FILTER: 'WORKLOAD_FILTER',
DOCUMENT_INFO: 'DOCUMENT_INFO',
+ BREADCRUMBS_MENU: 'BREADCRUMBS_MENU',
} as const;
export type OverlappingElement = keyof typeof OverlappingElements;
export const HELP_MODE_CLASS = 'help-mode';
+export const DOCUMINE_THEME_CLASS = 'documine-theme';
export const HELP_HIGHLIGHT_CLASS = 'help-highlight';
diff --git a/src/lib/help-mode/utils/help-mode.provider.ts b/src/lib/help-mode/utils/help-mode.provider.ts
new file mode 100644
index 0000000..cd1804b
--- /dev/null
+++ b/src/lib/help-mode/utils/help-mode.provider.ts
@@ -0,0 +1,7 @@
+import { HelpModeService } from '../help-mode.service';
+import { HELP_MODE_KEYS } from '../tokens';
+import { HelpModeKey } from '../types';
+
+export function provideHelpMode(helpModeKeys: HelpModeKey[]) {
+ return [{ provide: HELP_MODE_KEYS, useValue: helpModeKeys }, HelpModeService];
+}
diff --git a/src/lib/inputs/details-radio/details-radio-option.ts b/src/lib/inputs/details-radio/details-radio-option.ts
index d123db7..4712dd7 100644
--- a/src/lib/inputs/details-radio/details-radio-option.ts
+++ b/src/lib/inputs/details-radio/details-radio-option.ts
@@ -1,18 +1,29 @@
-interface ExtraOption {
+interface AdditionalField {
label: string;
- checked: boolean;
+ description?: string;
+}
+
+interface AdditionalCheck extends AdditionalField {
+ checked?: boolean;
hidden?: boolean;
disabled?: boolean;
}
+interface AdditionalInput extends AdditionalField {
+ value: string;
+ placeholder?: string;
+ errorCode?: string;
+}
+
export interface DetailsRadioOption {
id?: string;
label: string;
description: string;
- descriptionParams?: Record;
+ descriptionParams?: Record;
icon?: string;
value: I;
disabled?: boolean;
tooltip?: string;
- extraOption?: ExtraOption;
+ additionalCheck?: AdditionalCheck;
+ additionalInput?: AdditionalInput;
}
diff --git a/src/lib/inputs/details-radio/details-radio.component.html b/src/lib/inputs/details-radio/details-radio.component.html
index aae9694..d0dbd64 100644
--- a/src/lib/inputs/details-radio/details-radio.component.html
+++ b/src/lib/inputs/details-radio/details-radio.component.html
@@ -1,45 +1,83 @@
-