Compare commits

..

7 Commits

Author SHA1 Message Date
Nicoleta Panaghiu
5e26e217a1 RED-10509: added disableStopPropagation option for action config. 2024-11-25 10:40:42 +02:00
Timo Bejan
24b67f3fb4 removed logging 2024-11-06 15:55:44 +02:00
Timo Bejan
2a486236aa Removed offset and fixed notification polling 2024-11-06 15:55:40 +02:00
Nicoleta Panaghiu
97b614a899 RED-9985: prevent filter categories from collapsing upon selection. 2024-09-26 11:54:09 +03:00
Nicoleta Panaghiu
9ca054613c RED-9372: always include scroll-bar mixin. 2024-09-10 11:38:59 +03:00
Nicoleta Panaghiu
5a751932a6 RED-9372: fixed table-items moving on hover. 2024-09-05 17:20:08 +03:00
Nicoleta Panaghiu
cdd6ed5607 RED-9777: increase dialog help button z-index. 2024-08-14 11:44:53 +03:00
101 changed files with 184 additions and 484 deletions

View File

@ -206,7 +206,6 @@ module.exports = {
], ],
rules: { rules: {
'rxjs/no-ignored-subscription': 'error', 'rxjs/no-ignored-subscription': 'error',
'@angular-eslint/prefer-standalone': 'off',
'@angular-eslint/directive-selector': [ '@angular-eslint/directive-selector': [
'error', 'error',
{ {

View File

@ -1,19 +0,0 @@
sonarqube:
stage: test
image:
name: sonarsource/sonar-scanner-cli:11.1
entrypoint:
- ''
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: '0'
cache:
key: "${CI_JOB_NAME}"
paths:
- ".sonar/cache"
script:
- sonar-scanner
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
- if: "$CI_COMMIT_BRANCH =~ /^release/"

View File

@ -1,2 +0,0 @@
sonar.projectKey=common-ui
sonar.qualitygate.wait=false

View File

@ -22,14 +22,6 @@
font-size: var(--iqser-font-size); font-size: var(--iqser-font-size);
} }
&.extra-small {
height: 16px;
width: 16px;
min-width: 16px;
font-size: 10px;
font-weight: lighter;
}
&.gray-dark { &.gray-dark {
background-color: var(--iqser-user-avatar-1); background-color: var(--iqser-user-avatar-1);
color: var(--iqser-text); color: var(--iqser-text);

View File

@ -47,10 +47,7 @@
font-weight: bold; font-weight: bold;
padding-bottom: 8px; padding-bottom: 8px;
} }
}
&.redaction,
&.force-annotation {
iqser-details-radio { iqser-details-radio {
padding-top: 20px; padding-top: 20px;
} }

View File

@ -156,11 +156,9 @@ section.settings {
box-sizing: border-box; box-sizing: border-box;
background: var(--iqser-background); background: var(--iqser-background);
overflow: hidden; overflow: hidden;
&.with-transition { transition:
transition: width ease-in-out 0.2s,
width ease-in-out 0.2s, min-width ease-in-out 0.2s;
min-width ease-in-out 0.2s;
}
@include common-mixins.scroll-bar; @include common-mixins.scroll-bar;
&:hover { &:hover {
@ -256,10 +254,6 @@ section.settings {
cursor: pointer; cursor: pointer;
} }
.cursor-default {
cursor: default;
}
.fit-content { .fit-content {
width: fit-content; width: fit-content;
} }

View File

@ -1,5 +1,3 @@
@use 'sass:string';
@use 'sass:list';
/* Margins, paddings */ /* Margins, paddings */
$start: 0; $start: 0;
@ -9,19 +7,19 @@ $values: '';
$sides: (top, bottom, left, right); $sides: (top, bottom, left, right);
@for $i from $start + 1 through $end { @for $i from $start + 1 through $end {
$values: list.append($values, $i, comma); $values: append($values, $i, comma);
$values: list.set-nth($values, 1, $start); $values: set-nth($values, 1, $start);
} }
// TODO: Check if !important can be avoided // TODO: Check if !important can be avoided
@each $space in $values { @each $space in $values {
@each $side in $sides { @each $side in $sides {
.m#{string.slice($side, 0, 1)}-#{$space} { .m#{str-slice($side, 0, 1)}-#{$space} {
margin-#{$side}: #{$space}px !important; margin-#{$side}: #{$space}px !important;
} }
.p#{string.slice($side, 0, 1)}-#{$space} { .p#{str-slice($side, 0, 1)}-#{$space} {
padding-#{$side}: #{$space}px !important; padding-#{$side}: #{$space}px !important;
} }
} }

View File

@ -8,6 +8,7 @@ import { randomString } from '../../utils';
templateUrl: './chevron-button.component.html', templateUrl: './chevron-button.component.html',
styleUrls: ['./chevron-button.component.scss'], styleUrls: ['./chevron-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIconModule, MatButtonModule], imports: [MatIconModule, MatButtonModule],
}) })
export class ChevronButtonComponent { export class ChevronButtonComponent {

View File

@ -24,6 +24,7 @@ import { CircleButtonType, CircleButtonTypes } from '../types/circle-button.type
templateUrl: './circle-button.component.html', templateUrl: './circle-button.component.html',
styleUrls: ['./circle-button.component.scss'], styleUrls: ['./circle-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatTooltipModule, MatIconModule, MatButtonModule, StopPropagationDirective], imports: [MatTooltipModule, MatIconModule, MatButtonModule, StopPropagationDirective],
}) })
export class CircleButtonComponent { export class CircleButtonComponent {

View File

@ -11,6 +11,7 @@ import { IconButtonType, IconButtonTypes } from '../types/icon-button.type';
selector: 'iqser-icon-button', selector: 'iqser-icon-button',
templateUrl: './icon-button.component.html', templateUrl: './icon-button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgClass, MatButtonModule, MatIconModule, StopPropagationDirective], imports: [NgClass, MatButtonModule, MatIconModule, StopPropagationDirective],
}) })
export class IconButtonComponent { export class IconButtonComponent {

View File

@ -11,10 +11,6 @@
} }
<div class="dialog-content"> <div class="dialog-content">
@if (config.component) {
<ng-container #detailsComponent></ng-container>
}
<p [class.heading]="isDeleteAction" [innerHTML]="config.question" class="mt-0 mb-8"></p> <p [class.heading]="isDeleteAction" [innerHTML]="config.question" class="mt-0 mb-8"></p>
@if (config.details) { @if (config.details) {
<p [innerHTML]="config.details" class="mt-0"></p> <p [innerHTML]="config.details" class="mt-0"></p>
@ -39,20 +35,14 @@
} }
</div> </div>
<div class="dialog-actions" [class.reverse]="config.cancelButtonPrimary"> <div class="dialog-actions">
@if (!config.cancelButtonPrimary) { <iqser-icon-button
<iqser-icon-button (action)="confirm(confirmOption)"
(action)="confirm(confirmOption)" [disabled]="(config.requireInput && confirmationDoesNotMatch()) || config.disableConfirm"
[disabled]="(config.requireInput && confirmationDoesNotMatch()) || config.disableConfirm" [label]="config.confirmationText"
[label]="config.confirmationText" [type]="iconButtonTypes.primary"
[type]="iconButtonTypes.primary" buttonId="confirm"
buttonId="confirm" ></iqser-icon-button>
></iqser-icon-button>
} @else {
<div (click)="confirm(confirmOption)" class="all-caps-label cancel no-uppercase" id="confirm">
{{ config.confirmationText }}
</div>
}
@if (config.alternativeConfirmationText) { @if (config.alternativeConfirmationText) {
<iqser-icon-button <iqser-icon-button
@ -70,13 +60,9 @@
} }
@if (!config.discardChangesText) { @if (!config.discardChangesText) {
@if (config.cancelButtonPrimary) { <div (click)="deny()" class="all-caps-label cancel">
<iqser-icon-button (click)="deny()" [label]="config.denyText" [type]="iconButtonTypes.primary"></iqser-icon-button> {{ config.denyText }}
} @else { </div>
<div (click)="deny()" class="all-caps-label cancel">
{{ config.denyText }}
</div>
}
} }
</div> </div>

View File

@ -6,12 +6,3 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.reverse {
flex-direction: row-reverse;
justify-content: flex-end;
}
.no-uppercase {
text-transform: unset;
}

View File

@ -1,24 +1,15 @@
import { NgTemplateOutlet } from '@angular/common'; import { NgTemplateOutlet } from '@angular/common';
import { import { ChangeDetectionStrategy, Component, HostListener, inject, TemplateRef } from '@angular/core';
AfterViewInit,
ChangeDetectionStrategy,
Component,
HostListener,
inject,
TemplateRef,
Type,
viewChild,
ViewContainerRef,
} from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CircleButtonComponent, IconButtonTypes } from '../../buttons'; import { CircleButtonComponent } from '../../buttons/circle-button/circle-button.component';
import { IconButtonComponent } from '../../buttons'; import { IconButtonComponent } from '../../buttons/icon-button/icon-button.component';
import { ValuesOf } from '../../utils'; import { IconButtonTypes } from '../../buttons/types/icon-button.type';
import { ValuesOf } from '../../utils/types/utility-types';
export const TitleColors = { export const TitleColors = {
DEFAULT: 'default', DEFAULT: 'default',
@ -57,9 +48,6 @@ interface InternalConfirmationDialogData {
readonly checkboxes: CheckBox[]; readonly checkboxes: CheckBox[];
readonly checkboxesValidation: boolean; readonly checkboxesValidation: boolean;
readonly toastMessage?: string; readonly toastMessage?: string;
readonly component?: Type<unknown>;
readonly componentInputs?: { [key: string]: unknown };
readonly cancelButtonPrimary?: boolean;
} }
export type IConfirmationDialogData = Partial<InternalConfirmationDialogData>; export type IConfirmationDialogData = Partial<InternalConfirmationDialogData>;
@ -77,9 +65,6 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
denyText: options?.denyText ?? _('common.confirmation-dialog.deny'), denyText: options?.denyText ?? _('common.confirmation-dialog.deny'),
checkboxes: options?.checkboxes ?? [], checkboxes: options?.checkboxes ?? [],
checkboxesValidation: typeof options?.checkboxesValidation === 'boolean' ? options.checkboxesValidation : true, checkboxesValidation: typeof options?.checkboxesValidation === 'boolean' ? options.checkboxesValidation : true,
component: options?.component,
componentInputs: options?.componentInputs,
cancelButtonPrimary: options?.cancelButtonPrimary ?? false,
}; };
} }
@ -87,6 +72,7 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
templateUrl: './confirmation-dialog.component.html', templateUrl: './confirmation-dialog.component.html',
styleUrls: ['./confirmation-dialog.component.scss'], styleUrls: ['./confirmation-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ imports: [
MatIconModule, MatIconModule,
FormsModule, FormsModule,
@ -98,14 +84,13 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
MatDialogModule, MatDialogModule,
], ],
}) })
export class ConfirmationDialogComponent implements AfterViewInit { export class ConfirmationDialogComponent {
readonly config = getConfig(inject(MAT_DIALOG_DATA)); readonly config = getConfig(inject(MAT_DIALOG_DATA));
inputValue = ''; inputValue = '';
showToast = false; showToast = false;
readonly inputLabel: string; readonly inputLabel: string;
readonly confirmOptions = ConfirmOptions; readonly confirmOptions = ConfirmOptions;
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly detailsComponentRef = viewChild.required('detailsComponent', { read: ViewContainerRef });
constructor( constructor(
private readonly _dialogRef: MatDialogRef<ConfirmationDialogComponent, ConfirmOption>, private readonly _dialogRef: MatDialogRef<ConfirmationDialogComponent, ConfirmOption>,
@ -130,19 +115,13 @@ export class ConfirmationDialogComponent implements AfterViewInit {
return ConfirmOptions.CONFIRM; return ConfirmOptions.CONFIRM;
} }
@HostListener('window:keyup.enter', ['$event']) @HostListener('window:keyup.enter')
onKeyupEnter(event: KeyboardEvent): void { onKeyupEnter(): void {
event?.stopImmediatePropagation(); if (this.config.requireInput && !this.confirmationDoesNotMatch()) {
if (!this.config.requireInput || !this.confirmationDoesNotMatch()) { this.confirm(ConfirmOptions.CONFIRM);
if (!this.config.cancelButtonPrimary) this.confirm(ConfirmOptions.CONFIRM);
else this.deny();
} }
} }
ngAfterViewInit() {
this.#initializeDetailsComponent();
}
confirmationDoesNotMatch(): boolean { confirmationDoesNotMatch(): boolean {
return this.inputValue.toLowerCase() !== this.config.confirmationText.toLowerCase(); return this.inputValue.toLowerCase() !== this.config.confirmationText.toLowerCase();
} }
@ -177,13 +156,9 @@ export class ConfirmationDialogComponent implements AfterViewInit {
}); });
} }
#initializeDetailsComponent() { @HostListener('window:keydown.Enter', ['$event'])
if (!this.config.component) return; onEnter(event: KeyboardEvent): void {
const component = this.detailsComponentRef().createComponent(this.config.component); event?.stopImmediatePropagation();
if (this.config.componentInputs) { this.confirm(ConfirmOptions.CONFIRM);
for (const [key, value] of Object.entries(this.config.componentInputs)) {
(component.instance as any)[key] = value;
}
}
} }
} }

View File

@ -22,8 +22,6 @@ export abstract class IqserDialogComponent<ComponentType, DataType = null, Retur
readonly data = inject<DataType>(MAT_DIALOG_DATA); readonly data = inject<DataType>(MAT_DIALOG_DATA);
readonly dialog = inject(MatDialog); readonly dialog = inject(MatDialog);
readonly form?: FormGroup; readonly form?: FormGroup;
readonly ignoredKeys: string[] = [];
initialFormValue: Record<string, unknown> = {}; initialFormValue: Record<string, unknown> = {};
constructor(private readonly _editMode = false) { constructor(private readonly _editMode = false) {
@ -39,7 +37,7 @@ export abstract class IqserDialogComponent<ComponentType, DataType = null, Retur
} }
get changed(): boolean { get changed(): boolean {
return !this.form || hasFormChanged(this.form, this.initialFormValue, this.ignoredKeys); return !this.form || hasFormChanged(this.form, this.initialFormValue);
} }
get disabled(): boolean { get disabled(): boolean {

View File

@ -2,6 +2,7 @@ import { booleanAttribute, Directive, input } from '@angular/core';
@Directive({ @Directive({
selector: '[iqserDisableStopPropagation]', selector: '[iqserDisableStopPropagation]',
standalone: true,
}) })
export class DisableStopPropagationDirective { export class DisableStopPropagationDirective {
readonly iqserDisableStopPropagation = input(true, { transform: booleanAttribute }); readonly iqserDisableStopPropagation = input(true, { transform: booleanAttribute });

View File

@ -2,6 +2,7 @@ import { Directive, ElementRef, OnDestroy, OnInit, signal } from '@angular/core'
@Directive({ @Directive({
selector: '[iqserHasScrollbar]', selector: '[iqserHasScrollbar]',
standalone: true,
host: { host: {
'[class]': '_class()', '[class]': '_class()',
}, },

View File

@ -2,6 +2,7 @@ import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/c
@Directive({ @Directive({
selector: '[iqserHiddenAction]', selector: '[iqserHiddenAction]',
standalone: true,
}) })
export class HiddenActionDirective { export class HiddenActionDirective {
@Input() requiredClicks = 4; @Input() requiredClicks = 4;

View File

@ -3,6 +3,7 @@ import { NGXLogger } from 'ngx-logger';
@Directive({ @Directive({
selector: '[iqserPreventDefault]', selector: '[iqserPreventDefault]',
standalone: true,
}) })
export class PreventDefaultDirective { export class PreventDefaultDirective {
readonly #logger = inject(NGXLogger); readonly #logger = inject(NGXLogger);

View File

@ -4,6 +4,7 @@ import { DisableStopPropagationDirective } from './disable-stop-propagation.dire
@Directive({ @Directive({
selector: '[iqserStopPropagation]', selector: '[iqserStopPropagation]',
standalone: true,
}) })
export class StopPropagationDirective { export class StopPropagationDirective {
readonly #disableStopPropagation = inject(DisableStopPropagationDirective, { optional: true }); readonly #disableStopPropagation = inject(DisableStopPropagationDirective, { optional: true });

View File

@ -2,6 +2,7 @@ import { Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/
@Directive({ @Directive({
selector: '[iqserSyncWidth]', selector: '[iqserSyncWidth]',
standalone: true,
}) })
export class SyncWidthDirective implements OnDestroy { export class SyncWidthDirective implements OnDestroy {
@Input() iqserSyncWidth!: string; @Input() iqserSyncWidth!: string;

View File

@ -19,6 +19,7 @@ import { randomString } from '../utils/functions';
templateUrl: './empty-state.component.html', templateUrl: './empty-state.component.html',
styleUrls: ['./empty-state.component.scss'], styleUrls: ['./empty-state.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgStyle, MatIconModule, IconButtonComponent], imports: [NgStyle, MatIconModule, IconButtonComponent],
}) })
export class EmptyStateComponent { export class EmptyStateComponent {

View File

@ -17,7 +17,6 @@ import { ErrorService } from '../error.service';
]), ]),
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class ConnectionStatusComponent { export class ConnectionStatusComponent {
protected readonly connectionStatusTranslations = connectionStatusTranslations; protected readonly connectionStatusTranslations = connectionStatusTranslations;

View File

@ -8,7 +8,6 @@ import { CustomError, ErrorService, ErrorType } from '../error.service';
templateUrl: './full-page-error.component.html', templateUrl: './full-page-error.component.html',
styleUrls: ['./full-page-error.component.scss'], styleUrls: ['./full-page-error.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class FullPageErrorComponent { export class FullPageErrorComponent {
protected readonly iconButtonTypes = IconButtonTypes; protected readonly iconButtonTypes = IconButtonTypes;

View File

@ -37,6 +37,7 @@ const atLeastOneIsExpandable = pipe(
}, },
}, },
], ],
standalone: true,
imports: [AsyncPipe, InputWithActionComponent, NgTemplateOutlet, TranslateModule, MatIcon, MatCheckbox, StopPropagationDirective], imports: [AsyncPipe, InputWithActionComponent, NgTemplateOutlet, TranslateModule, MatIcon, MatCheckbox, StopPropagationDirective],
}) })
export class FilterCardComponent implements OnInit { export class FilterCardComponent implements OnInit {

View File

@ -8,8 +8,8 @@ export class NestedFilter extends Filter implements INestedFilter, IListable {
disabled?: boolean; disabled?: boolean;
helpModeKey?: string; helpModeKey?: string;
readonly children: Filter[]; readonly children: Filter[];
override readonly skipTranslation?: boolean; readonly skipTranslation?: boolean;
override readonly metadata?: Record<string, any>; readonly metadata?: Record<string, any>;
constructor(nestedFilter: INestedFilter) { constructor(nestedFilter: INestedFilter) {
super(nestedFilter); super(nestedFilter);

View File

@ -29,6 +29,7 @@ import { IFilterGroup } from '../models/filter-group.model';
StopPropagationDirective, StopPropagationDirective,
MatMenuContent, MatMenuContent,
], ],
standalone: true,
}) })
export class PopupFilterComponent implements OnInit { export class PopupFilterComponent implements OnInit {
@Input() primaryFiltersSlug!: string; @Input() primaryFiltersSlug!: string;

View File

@ -1,5 +1,5 @@
@if (quickFilters$ | async; as filters) { @if (quickFilters$ | async; as filters) {
@for (filter of filters; track filter.id) { @for (filter of filters; track filter) {
<div <div
(click)="filterService.toggleFilter('quickFilters', filter.id)" (click)="filterService.toggleFilter('quickFilters', filter.id)"
[class.active]="filter.checked" [class.active]="filter.checked"

View File

@ -6,7 +6,6 @@ import { FilterService } from '../filter.service';
templateUrl: './quick-filters.component.html', templateUrl: './quick-filters.component.html',
styleUrls: ['./quick-filters.component.scss'], styleUrls: ['./quick-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class QuickFiltersComponent { export class QuickFiltersComponent {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters'); readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');

View File

@ -13,6 +13,7 @@ import { SimpleFilterOption } from '../models/simple-filter-option';
selector: 'iqser-simple-popup-filter', selector: 'iqser-simple-popup-filter',
templateUrl: './simple-popup-filter.component.html', templateUrl: './simple-popup-filter.component.html',
styleUrls: ['./simple-popup-filter.component.scss'], styleUrls: ['./simple-popup-filter.component.scss'],
standalone: true,
imports: [ imports: [
MatMenuModule, MatMenuModule,
IconButtonComponent, IconButtonComponent,

View File

@ -7,7 +7,6 @@ import { IFilter } from '../models/filter.model';
selector: 'iqser-single-filter', selector: 'iqser-single-filter',
templateUrl: './single-filter.component.html', templateUrl: './single-filter.component.html',
styleUrls: ['./single-filter.component.scss'], styleUrls: ['./single-filter.component.scss'],
standalone: false,
}) })
export class SingleFilterComponent { export class SingleFilterComponent {
@Input() filter!: IFilter; @Input() filter!: IFilter;

View File

@ -10,6 +10,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
selector: 'iqser-help-button', selector: 'iqser-help-button',
templateUrl: './help-button.component.html', templateUrl: './help-button.component.html',
styleUrls: ['./help-button.component.scss'], styleUrls: ['./help-button.component.scss'],
standalone: true,
imports: [MatIcon, MatTooltip], imports: [MatIcon, MatTooltip],
}) })
export class HelpButtonComponent implements OnInit, OnDestroy { export class HelpButtonComponent implements OnInit, OnDestroy {

View File

@ -12,6 +12,7 @@ const DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX = '800';
templateUrl: './help-mode-dialog.component.html', templateUrl: './help-mode-dialog.component.html',
styleUrls: ['./help-mode-dialog.component.scss'], styleUrls: ['./help-mode-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatCheckbox, CircleButtonComponent, TranslateModule], imports: [MatCheckbox, CircleButtonComponent, TranslateModule],
}) })
export class HelpModeDialogComponent implements OnInit, OnDestroy { export class HelpModeDialogComponent implements OnInit, OnDestroy {

View File

@ -3,7 +3,7 @@ import { HelpModeService } from '../help-mode.service';
import { IqserEventTarget } from '../../utils'; import { IqserEventTarget } from '../../utils';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { CircleButtonComponent, CircleButtonTypes } from '../../buttons'; import { CircleButtonComponent, CircleButtonTypes } from '../../buttons';
import { AsyncPipe } from '@angular/common'; import { AsyncPipe, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@Component({ @Component({
@ -11,7 +11,8 @@ import { TranslateModule } from '@ngx-translate/core';
templateUrl: './help-mode.component.html', templateUrl: './help-mode.component.html',
styleUrls: ['./help-mode.component.scss'], styleUrls: ['./help-mode.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [AsyncPipe, TranslateModule, CircleButtonComponent], standalone: true,
imports: [AsyncPipe, TranslateModule, NgIf, CircleButtonComponent],
}) })
export class HelpModeComponent { export class HelpModeComponent {
readonly circleButtonTypes = CircleButtonTypes; readonly circleButtonTypes = CircleButtonTypes;

View File

@ -1,29 +1,19 @@
interface AdditionalField { interface ExtraOption {
label: string; label: string;
description?: string; checked: boolean;
}
interface AdditionalCheck extends AdditionalField {
checked?: boolean;
hidden?: boolean; hidden?: boolean;
disabled?: boolean; disabled?: boolean;
} description?: string;
interface AdditionalInput extends AdditionalField {
value: string;
placeholder?: string;
errorCode?: string;
} }
export interface DetailsRadioOption<I> { export interface DetailsRadioOption<I> {
id?: string; id?: string;
label: string; label: string;
description: string; description: string;
descriptionParams?: Record<string, string | number>; descriptionParams?: Record<string, string>;
icon?: string; icon?: string;
value: I; value: I;
disabled?: boolean; disabled?: boolean;
tooltip?: string; tooltip?: string;
additionalCheck?: AdditionalCheck; extraOption?: ExtraOption;
additionalInput?: AdditionalInput;
} }

View File

@ -20,48 +20,25 @@
<span class="hint">{{ option.description | translate: option.descriptionParams | replaceNbsp }}</span> <span class="hint">{{ option.description | translate: option.descriptionParams | replaceNbsp }}</span>
@if (isSelected(option)) { @if (option.extraOption && !option.extraOption.hidden && isSelected(option)) {
@if (option.additionalCheck && !option.additionalCheck.hidden) { <div class="iqser-input-group">
<div class="iqser-input-group w-450"> <mat-checkbox
<mat-checkbox (change)="emitExtraOption()"
(change)="emitExtraOption()" [(ngModel)]="option.extraOption.checked"
[(ngModel)]="option.additionalCheck.checked" [checked]="option.extraOption.checked"
[checked]="option.additionalCheck.checked" [disabled]="!!option.extraOption.disabled"
[disabled]="!!option.additionalCheck.disabled" color="primary"
color="primary" >
> {{ option.extraOption.label | translate | replaceNbsp }}
{{ option.additionalCheck.label | translate | replaceNbsp }} </mat-checkbox>
</mat-checkbox>
@if (option.additionalCheck.description) { @if (option.extraOption.description) {
<span <span
[innerHTML]="option.additionalCheck.description | translate" [innerHTML]="option.extraOption.description | translate"
class="hint additional-check-description" class="hint extra-option-description"
></span> ></span>
} }
</div> </div>
}
@if (option.additionalInput) {
<div class="iqser-input-group w-full additional-input">
<span class="label"> {{ option.additionalInput.label | translate }} </span>
<div class="flex-column">
<input
[(ngModel)]="option.additionalInput.value"
[ngClass]="{ error: additionalInputTouched && hasError(option.additionalInput.errorCode) }"
[placeholder]="
option.additionalInput.placeholder ? (option.additionalInput.placeholder | translate) : ''
"
(blur)="additionalInputTouched = true"
(focus)="additionalInputTouched = false"
(keydown)="emitExtraOption()"
/>
@if (option.additionalInput.description) {
<span class="hint" [innerHTML]="option.additionalInput.description | translate"></span>
}
</div>
</div>
}
} }
</div> </div>

View File

@ -43,44 +43,10 @@ label {
} }
} }
.additional-check-description { .extra-option-description {
margin-left: 23px; margin-left: 23px;
opacity: 0.49; opacity: 0.49;
} }
.additional-input {
display: flex;
flex-direction: row;
gap: 10px;
span {
margin-top: 8px;
font-size: 12px;
}
div {
.error {
border-color: var(--iqser-red-1);
}
display: flex;
span {
font-size: 10px;
margin-top: 4px;
}
}
.flex-column {
flex: 1;
input {
width: 232px;
min-height: 30px;
height: 30px;
}
}
}
} }
.row { .row {

View File

@ -14,6 +14,7 @@ import { DetailsRadioOption } from './details-radio-option';
selector: 'iqser-details-radio', selector: 'iqser-details-radio',
templateUrl: './details-radio.component.html', templateUrl: './details-radio.component.html',
styleUrls: ['./details-radio.component.scss'], styleUrls: ['./details-radio.component.scss'],
standalone: true,
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -41,8 +42,8 @@ import { DetailsRadioOption } from './details-radio-option';
export class DetailsRadioComponent<I> extends FormFieldComponent<DetailsRadioOption<I>> { export class DetailsRadioComponent<I> extends FormFieldComponent<DetailsRadioOption<I>> {
readonly options = input.required<DetailsRadioOption<I>[]>(); readonly options = input.required<DetailsRadioOption<I>[]>();
readonly displayInRow = input(false, { transform: booleanAttribute }); readonly displayInRow = input(false, { transform: booleanAttribute });
readonly extraOptionChanged = output<DetailsRadioOption<I>>(); readonly extraOptionChanged = output<DetailsRadioOption<I>>();
additionalInputTouched = false;
toggleOption(option: DetailsRadioOption<I>) { toggleOption(option: DetailsRadioOption<I>) {
if (option.value !== this._value?.value && !option.disabled) { if (option.value !== this._value?.value && !option.disabled) {

View File

@ -23,6 +23,7 @@ type DynamicInput = number | string | Date;
templateUrl: './dynamic-input.component.html', templateUrl: './dynamic-input.component.html',
styleUrls: ['./dynamic-input.component.scss'], styleUrls: ['./dynamic-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -50,7 +51,7 @@ export class DynamicInputComponent extends FormFieldComponent<DynamicInput> {
readonly isNumber = computed(() => this.type() === InputTypes.NUMBER); readonly isNumber = computed(() => this.type() === InputTypes.NUMBER);
readonly isText = computed(() => this.type() === InputTypes.TEXT); readonly isText = computed(() => this.type() === InputTypes.TEXT);
override writeValue(input: DynamicInput): void { writeValue(input: DynamicInput): void {
this.input.set(input); this.input.set(input);
} }

View File

@ -8,6 +8,7 @@ import { CircleButtonType, CircleButtonTypes } from '../../buttons/types/circle-
templateUrl: './editable-input.component.html', templateUrl: './editable-input.component.html',
styleUrls: ['./editable-input.component.scss'], styleUrls: ['./editable-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [CircleButtonComponent, FormsModule], imports: [CircleButtonComponent, FormsModule],
host: { host: {
'[class.editing]': '_editing()', '[class.editing]': '_editing()',

View File

@ -1,47 +1,19 @@
import { ChangeDetectorRef, Directive, inject, Injector, OnInit } from '@angular/core'; import { ChangeDetectorRef, Directive, inject } from '@angular/core';
import { import { ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms';
ControlValueAccessor,
FormControl,
FormControlDirective,
FormControlName,
FormGroupDirective,
NgControl,
ValidationErrors,
Validator,
} from '@angular/forms';
@Directive() @Directive()
export abstract class FormFieldComponent<I> implements ControlValueAccessor, Validator, OnInit { export abstract class FormFieldComponent<I> implements ControlValueAccessor, Validator {
touched = false; touched = false;
disabled = false; disabled = false;
protected readonly _changeRef = inject(ChangeDetectorRef); protected readonly _changeRef = inject(ChangeDetectorRef);
protected readonly _injector = inject(Injector);
protected _formControl: FormControl | undefined;
protected _value: I | undefined; protected _value: I | undefined;
get value(): I | undefined { get value(): I | undefined {
return this._value; return this._value;
} }
ngOnInit() {
const ngControl = this._injector.get(NgControl);
if (ngControl instanceof FormControlName) {
this._formControl = this._injector.get(FormGroupDirective).getControl(ngControl);
} else {
this._formControl = (ngControl as FormControlDirective).form as FormControl;
}
}
hasError(errorCode: string | undefined): boolean {
if (errorCode && this._formControl) {
return this._formControl.hasError(errorCode);
}
return false;
}
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
onChange = (value?: I) => {}; onChange = (value?: I) => {};

View File

@ -10,6 +10,7 @@ import { randomString } from '../../utils/functions';
templateUrl: './input-with-action.component.html', templateUrl: './input-with-action.component.html',
styleUrls: ['./input-with-action.component.scss'], styleUrls: ['./input-with-action.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FormsModule, MatIconModule, CircleButtonComponent], imports: [FormsModule, MatIconModule, CircleButtonComponent],
}) })
export class InputWithActionComponent { export class InputWithActionComponent {

View File

@ -6,6 +6,7 @@ import { MatIconModule } from '@angular/material/icon';
templateUrl: './round-checkbox.component.html', templateUrl: './round-checkbox.component.html',
styleUrls: ['./round-checkbox.component.scss'], styleUrls: ['./round-checkbox.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIconModule], imports: [MatIconModule],
}) })
export class RoundCheckboxComponent { export class RoundCheckboxComponent {

View File

@ -3,5 +3,4 @@ import { ActionConfig } from './action-config.model';
export interface ButtonConfig extends ActionConfig { export interface ButtonConfig extends ActionConfig {
readonly type?: IconButtonType; readonly type?: IconButtonType;
readonly tooltip?: string;
} }

View File

@ -46,14 +46,11 @@
@if (!config.hide) { @if (!config.hide) {
<iqser-icon-button <iqser-icon-button
(action)="config.action($event)" (action)="config.action($event)"
[buttonId]="config.label.replace('.', '-')" [buttonId]="(config.label | translate).replace('.', '-')"
[icon]="config.icon" [icon]="config.icon"
[label]="config.label | translate" [label]="config.label | translate"
[type]="config.type" [type]="config.type"
[matTooltip]="(config.tooltip | translate) ?? ''"
[disabled]="config.disabled"
[attr.help-mode-key]="config.helpModeKey" [attr.help-mode-key]="config.helpModeKey"
matTooltipPosition="above"
></iqser-icon-button> ></iqser-icon-button>
} }
} }

View File

@ -15,7 +15,6 @@ import { filterEach } from '../../utils';
import { List } from '../../utils'; import { List } from '../../utils';
import { IListable } from '../models'; import { IListable } from '../models';
import { ActionConfig, ButtonConfig, SearchPosition, SearchPositions } from './models'; import { ActionConfig, ButtonConfig, SearchPosition, SearchPositions } from './models';
import { MatTooltip } from '@angular/material/tooltip';
import { DisableStopPropagationDirective } from '../../directives'; import { DisableStopPropagationDirective } from '../../directives';
@Component({ @Component({
@ -23,6 +22,7 @@ import { DisableStopPropagationDirective } from '../../directives';
templateUrl: './page-header.component.html', templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss'], styleUrls: ['./page-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ imports: [
AsyncPipe, AsyncPipe,
NgTemplateOutlet, NgTemplateOutlet,
@ -32,7 +32,6 @@ import { DisableStopPropagationDirective } from '../../directives';
CircleButtonComponent, CircleButtonComponent,
TranslateModule, TranslateModule,
InputWithActionComponent, InputWithActionComponent,
MatTooltip,
DisableStopPropagationDirective, DisableStopPropagationDirective,
], ],
}) })

View File

@ -18,6 +18,7 @@ type ButtonType = keyof typeof ButtonTypes;
templateUrl: './scroll-button.component.html', templateUrl: './scroll-button.component.html',
styleUrls: ['./scroll-button.component.scss'], styleUrls: ['./scroll-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [AsyncPipe, MatIcon], imports: [AsyncPipe, MatIcon],
}) })
export class ScrollButtonComponent implements OnInit { export class ScrollButtonComponent implements OnInit {

View File

@ -52,7 +52,7 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
get selected(): Class[] { get selected(): Class[] {
const selectedIds = this.selectedIds; const selectedIds = this.selectedIds;
return selectedIds.map(id => this._entitiesService.all.find(a => a.id === id)).filter(a => !!a); return this._entitiesService.all.filter(a => selectedIds.includes(a.id));
} }
get selectedIds(): PrimaryKey[] { get selectedIds(): PrimaryKey[] {

View File

@ -12,6 +12,7 @@ import { Id, IListable } from '../models';
templateUrl: './table-column-name.component.html', templateUrl: './table-column-name.component.html',
styleUrls: ['./table-column-name.component.scss'], styleUrls: ['./table-column-name.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIcon, MatTooltip, TranslateModule, AsyncPipe, NgClass], imports: [MatIcon, MatTooltip, TranslateModule, AsyncPipe, NgClass],
}) })
export class TableColumnNameComponent<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']> { export class TableColumnNameComponent<T extends IListable<PrimaryKey>, PrimaryKey extends Id = T['id']> {

View File

@ -11,7 +11,7 @@
<!-- mouseenter and mouseleave triggers change detection event if itemMouse functions are undefined --> <!-- mouseenter and mouseleave triggers change detection event if itemMouse functions are undefined -->
<!-- this little hack below ensures that change detection won't be triggered if functions are undefined --> <!-- this little hack below ensures that change detection won't be triggered if functions are undefined -->
@if (itemMouseEnterFn || itemMouseLeaveFn) { @if (itemMouseEnterFn || itemMouseLeaveFn) {
<a <div
(mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)" (mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)"
(mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)" (mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)"
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async" [class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
@ -21,12 +21,12 @@
> >
<iqser-table-item <iqser-table-item
(click)="multiSelect(entity, $event)" (click)="multiSelect(entity, $event)"
[entity]="$any(entity)" [entity]="entity"
[selectionEnabled]="selectionEnabled" [selectionEnabled]="selectionEnabled"
></iqser-table-item> ></iqser-table-item>
</a> </div>
} @else { } @else {
<a <div
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async" [class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
[id]="rowIdPrefix + '-' + ((entity[namePropertyKey] | snakeCase) ?? entity.id)" [id]="rowIdPrefix + '-' + ((entity[namePropertyKey] | snakeCase) ?? entity.id)"
[ngClass]="getTableItemClasses(entity)" [ngClass]="getTableItemClasses(entity)"
@ -34,10 +34,10 @@
> >
<iqser-table-item <iqser-table-item
(click)="multiSelect(entity, $event)" (click)="multiSelect(entity, $event)"
[entity]="$any(entity)" [entity]="entity"
[selectionEnabled]="selectionEnabled" [selectionEnabled]="selectionEnabled"
></iqser-table-item> ></iqser-table-item>
</a> </div>
} }
</ng-container> </ng-container>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>

View File

@ -72,8 +72,3 @@
.display-contents { .display-contents {
display: contents; display: contents;
} }
a {
display: contents;
@include mixins.clear-a;
}

View File

@ -20,6 +20,7 @@ import { TableItemComponent } from './table-item/table-item.component';
selector: 'iqser-table-content', selector: 'iqser-table-content',
templateUrl: './table-content.component.html', templateUrl: './table-content.component.html',
styleUrls: ['./table-content.component.scss'], styleUrls: ['./table-content.component.scss'],
standalone: true,
imports: [ imports: [
CdkVirtualScrollViewport, CdkVirtualScrollViewport,
AsyncPipe, AsyncPipe,
@ -86,7 +87,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
getTableItemClasses(entity: Class): Record<string, boolean> { getTableItemClasses(entity: Class): Record<string, boolean> {
const classes: Record<string, boolean> = { const classes: Record<string, boolean> = {
'table-item': true, 'table-item': true,
'cursor-default': !entity.routerLink, pointer: !!entity.routerLink && entity.routerLink.length > 0,
}; };
for (const key in this.tableItemClasses) { for (const key in this.tableItemClasses) {
if (Object.prototype.hasOwnProperty.call(this.tableItemClasses, key)) { if (Object.prototype.hasOwnProperty.call(this.tableItemClasses, key)) {

View File

@ -12,6 +12,7 @@ import { ListingService } from '../../services/listing.service';
templateUrl: './table-item.component.html', templateUrl: './table-item.component.html',
styleUrls: ['./table-item.component.scss'], styleUrls: ['./table-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [RoundCheckboxComponent, AsyncPipe, NgTemplateOutlet], imports: [RoundCheckboxComponent, AsyncPipe, NgTemplateOutlet],
}) })
export class TableItemComponent<T extends IListable> implements OnChanges { export class TableItemComponent<T extends IListable> implements OnChanges {
@ -33,7 +34,6 @@ export class TableItemComponent<T extends IListable> implements OnChanges {
toggleEntitySelected($event: MouseEvent, entity: T): void { toggleEntitySelected($event: MouseEvent, entity: T): void {
$event.stopPropagation(); $event.stopPropagation();
$event.preventDefault();
this.listingService.select(entity, $event.shiftKey); this.listingService.select(entity, $event.shiftKey);
} }
} }

View File

@ -13,6 +13,7 @@ import { TableColumnNameComponent } from '../table-column-name/table-column-name
templateUrl: './table-header.component.html', templateUrl: './table-header.component.html',
styleUrls: ['./table-header.component.scss'], styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ imports: [
RoundCheckboxComponent, RoundCheckboxComponent,
AsyncPipe, AsyncPipe,

View File

@ -28,6 +28,7 @@ const SCROLLBAR_WIDTH = 11;
selector: 'iqser-table [tableColumnConfigs] [itemSize]', selector: 'iqser-table [tableColumnConfigs] [itemSize]',
templateUrl: './table.component.html', templateUrl: './table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TableHeaderComponent, NgTemplateOutlet, AsyncPipe, EmptyStateComponent, ScrollButtonComponent, TableContentComponent], imports: [TableHeaderComponent, NgTemplateOutlet, AsyncPipe, EmptyStateComponent, ScrollButtonComponent, TableContentComponent],
}) })
export class TableComponent<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> implements OnChanges { export class TableComponent<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> implements OnChanges {

View File

@ -36,6 +36,7 @@ interface ColumnHeaderContext {
templateUrl: './column-header.component.html', templateUrl: './column-header.component.html',
styleUrls: ['./column-header.component.scss'], styleUrls: ['./column-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [AsyncPipe, TranslateModule, RoundCheckboxComponent, NgTemplateOutlet, CircleButtonComponent], imports: [AsyncPipe, TranslateModule, RoundCheckboxComponent, NgTemplateOutlet, CircleButtonComponent],
}) })
export class ColumnHeaderComponent<T extends IListable, K extends string> extends ContextComponent<ColumnHeaderContext> implements OnInit { export class ColumnHeaderComponent<T extends IListable, K extends string> extends ContextComponent<ColumnHeaderContext> implements OnInit {

View File

@ -1,5 +1,5 @@
@use '../../../assets/styles/common-variables'; @import '../../../assets/styles/common-variables';
@use '../../../assets/styles/common-mixins' as mixins; @import '../../../assets/styles/common-mixins';
:host { :host {
display: flex; display: flex;
@ -67,7 +67,7 @@
.cdk-drop-list { .cdk-drop-list {
overflow-y: auto; overflow-y: auto;
@include mixins.no-scroll-bar; @include no-scroll-bar;
min-height: calc(100% - 36px); min-height: calc(100% - 36px);
&.multi-select-active { &.multi-select-active {
@ -135,5 +135,5 @@
cdk-virtual-scroll-viewport { cdk-virtual-scroll-viewport {
height: 100%; height: 100%;
@include mixins.no-scroll-bar; @include no-scroll-bar;
} }

View File

@ -50,6 +50,7 @@ interface WorkflowContext<T> {
templateUrl: './workflow.component.html', templateUrl: './workflow.component.html',
styleUrls: ['./workflow.component.scss'], styleUrls: ['./workflow.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ imports: [
TableHeaderComponent, TableHeaderComponent,
AsyncPipe, AsyncPipe,

View File

@ -5,7 +5,6 @@ import { LoadingService } from '../loading.service';
selector: 'iqser-full-page-loading-indicator', selector: 'iqser-full-page-loading-indicator',
templateUrl: './full-page-loading-indicator.component.html', templateUrl: './full-page-loading-indicator.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class FullPageLoadingIndicatorComponent { export class FullPageLoadingIndicatorComponent {
constructor(readonly loadingService: LoadingService) {} constructor(readonly loadingService: LoadingService) {}

View File

@ -1,16 +1,15 @@
import { ChangeDetectionStrategy, Component, Input, OnInit, Optional } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, Optional, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { FilterService, INestedFilter } from '../../filtering';
import { get, shareLast } from '../../utils';
import { ProgressBarConfigModel } from './progress-bar-config.model'; import { ProgressBarConfigModel } from './progress-bar-config.model';
import { FilterService, INestedFilter } from '../../filtering';
import { Observable, of } from 'rxjs';
import { get, shareLast } from '../../utils';
import { map } from 'rxjs/operators';
@Component({ @Component({
selector: 'iqser-progress-bar [config]', selector: 'iqser-progress-bar [config]',
templateUrl: './progress-bar.component.html', templateUrl: './progress-bar.component.html',
styleUrls: ['./progress-bar.component.scss'], styleUrls: ['./progress-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class ProgressBarComponent implements OnInit { export class ProgressBarComponent implements OnInit {
@Input() config!: ProgressBarConfigModel; @Input() config!: ProgressBarConfigModel;

View File

@ -6,7 +6,6 @@ import { ILoadingConfig } from '../loading.service';
templateUrl: './progress-loading.component.html', templateUrl: './progress-loading.component.html',
styleUrls: ['./progress-loading.component.scss'], styleUrls: ['./progress-loading.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class ProgressLoadingComponent { export class ProgressLoadingComponent {
@Input() config!: ILoadingConfig; @Input() config!: ILoadingConfig;

View File

@ -7,6 +7,7 @@ import { PaginationSettings } from './pagination-settings';
selector: 'iqser-pagination', selector: 'iqser-pagination',
templateUrl: './pagination.component.html', templateUrl: './pagination.component.html',
styleUrls: ['./pagination.component.scss'], styleUrls: ['./pagination.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TranslateModule], imports: [TranslateModule],
}) })

View File

@ -6,6 +6,7 @@ import { assertTemplate, IqserPermissionsDirective } from './permissions.directi
@Directive({ @Directive({
selector: '[allow]', selector: '[allow]',
standalone: true,
}) })
export class IqserAllowDirective extends IqserPermissionsDirective implements OnDestroy, OnInit { export class IqserAllowDirective extends IqserPermissionsDirective implements OnDestroy, OnInit {
/** /**

View File

@ -6,6 +6,7 @@ import { assertTemplate, IqserPermissionsDirective } from './permissions.directi
@Directive({ @Directive({
selector: '[deny]', selector: '[deny]',
standalone: true,
}) })
export class IqserDenyDirective extends IqserPermissionsDirective implements OnDestroy, OnInit { export class IqserDenyDirective extends IqserPermissionsDirective implements OnDestroy, OnInit {
/** /**

View File

@ -29,7 +29,7 @@ export function isArray<T>(value: unknown): value is T[] {
} }
export function toArray(value?: string | List): List { export function toArray(value?: string | List): List {
return isString(value) ? [value] : (value ?? []); return isString(value) ? [value] : value ?? [];
} }
export function isRedirectWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters { export function isRedirectWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters {

View File

@ -3,6 +3,7 @@ import { capitalize } from '../utils';
@Pipe({ @Pipe({
name: 'capitalize', name: 'capitalize',
standalone: true,
}) })
export class CapitalizePipe implements PipeTransform { export class CapitalizePipe implements PipeTransform {
transform(value: string): string { transform(value: string): string {

View File

@ -3,6 +3,7 @@ import { humanizeCamelCase } from '../utils';
@Pipe({ @Pipe({
name: 'humanizeCamelCase', name: 'humanizeCamelCase',
standalone: true,
}) })
export class HumanizeCamelCasePipe implements PipeTransform { export class HumanizeCamelCasePipe implements PipeTransform {
transform(item: string): string { transform(item: string): string {

View File

@ -3,6 +3,7 @@ import { humanize } from '../utils';
@Pipe({ @Pipe({
name: 'humanize', name: 'humanize',
standalone: true,
}) })
export class HumanizePipe implements PipeTransform { export class HumanizePipe implements PipeTransform {
transform(item: string, lowercase = false): string { transform(item: string, lowercase = false): string {

View File

@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'log', name: 'log',
standalone: true,
}) })
export class LogPipe implements PipeTransform { export class LogPipe implements PipeTransform {
transform<T>(value: T, message = ''): T { transform<T>(value: T, message = ''): T {

View File

@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'replaceNbsp', name: 'replaceNbsp',
standalone: true,
}) })
export class ReplaceNbspPipe implements PipeTransform { export class ReplaceNbspPipe implements PipeTransform {
transform(value: string): string { transform(value: string): string {

View File

@ -3,6 +3,7 @@ import { size } from '../utils';
@Pipe({ @Pipe({
name: 'size', name: 'size',
standalone: true,
}) })
export class SizePipe implements PipeTransform { export class SizePipe implements PipeTransform {
transform(value: number): string { transform(value: number): string {

View File

@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ @Pipe({
name: 'snakeCase', name: 'snakeCase',
standalone: true,
}) })
export class SnakeCasePipe implements PipeTransform { export class SnakeCasePipe implements PipeTransform {
transform(value: string): string | undefined { transform(value: string): string | undefined {

View File

@ -18,7 +18,9 @@ export interface QueryParam {
*/ */
export abstract class GenericService<I> { export abstract class GenericService<I> {
protected readonly _http = inject(HttpClient); protected readonly _http = inject(HttpClient);
protected readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, new Date(Date.now()).toISOString()]]); protected readonly _lastCheckedForChanges = new Map<string, string>([
[ROOT_CHANGES_KEY, new Date(Date.now()).toISOString()],
]);
protected abstract readonly _defaultModelPath: string; protected abstract readonly _defaultModelPath: string;
protected readonly _serviceName: string = 'redaction-gateway-v1'; protected readonly _serviceName: string = 'redaction-gateway-v1';
@ -38,7 +40,7 @@ export abstract class GenericService<I> {
headers: HeadersConfiguration.getHeaders({ contentType: false }), headers: HeadersConfiguration.getHeaders({ contentType: false }),
observe: 'body', observe: 'body',
params: this._queryParams(queryParams), params: this._queryParams(queryParams),
}); })
} }
getFor<R = I[]>(entityId: string, queryParams?: List<QueryParam>): Observable<R> { getFor<R = I[]>(entityId: string, queryParams?: List<QueryParam>): Observable<R> {

View File

@ -3,18 +3,14 @@ import { Title } from '@angular/platform-browser';
import { CacheApiService } from '../caching/cache-api.service'; import { CacheApiService } from '../caching/cache-api.service';
import { wipeAllCaches } from '../caching/cache-utils'; import { wipeAllCaches } from '../caching/cache-utils';
import { IqserAppConfig } from '../utils/iqser-app-config'; import { IqserAppConfig } from '../utils/iqser-app-config';
import { LANDING_PAGE_THEMES, MANUAL_BASE_URL, THEME_DIRECTORIES } from '../utils/constants';
import { IStoredTenantId, TenantsService } from '../tenants';
@Injectable() @Injectable()
export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> { export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
protected readonly _cacheApiService = inject(CacheApiService); protected readonly _cacheApiService = inject(CacheApiService);
protected readonly _titleService = inject(Title); protected readonly _titleService = inject(Title);
protected readonly _tenantsService = inject(TenantsService);
constructor(@Inject('Doesnt matter') protected _values: T) { constructor(@Inject('Doesnt matter') protected _values: T) {
this._checkFrontendVersion(); this._checkFrontendVersion();
this.#updateAppType();
this._titleService.setTitle(this._values.APP_NAME); this._titleService.setTitle(this._values.APP_NAME);
} }
@ -27,16 +23,6 @@ export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
this._titleService.setTitle(this._values.APP_NAME); this._titleService.setTitle(this._values.APP_NAME);
} }
updateIsDocumine(tenant: IStoredTenantId): void {
const isDocumine = tenant.documine;
this._values = {
...this._values,
IS_DOCUMINE: isDocumine,
THEME: !isDocumine ? THEME_DIRECTORIES.REDACT : THEME_DIRECTORIES.SCM,
MANUAL_BASE_URL: !isDocumine ? MANUAL_BASE_URL.REDACT_MANAGER : MANUAL_BASE_URL.DOCUMINE,
};
}
protected _checkFrontendVersion(): void { protected _checkFrontendVersion(): void {
this._cacheApiService.getCachedValue('FRONTEND_APP_VERSION').then(async lastVersion => { this._cacheApiService.getCachedValue('FRONTEND_APP_VERSION').then(async lastVersion => {
const version = this._values.FRONTEND_APP_VERSION; const version = this._values.FRONTEND_APP_VERSION;
@ -48,28 +34,6 @@ export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
await this._cacheApiService.cacheValue('FRONTEND_APP_VERSION', version); await this._cacheApiService.cacheValue('FRONTEND_APP_VERSION', version);
}); });
} }
#updateAppType() {
const storedTenants = this._tenantsService.getStoredTenants();
if (storedTenants.length) {
const isRedaction = !!storedTenants.find(t => !t.documine);
const isDocumine = !!storedTenants.find(t => t.documine);
const landingPageTheme =
isRedaction && isDocumine
? LANDING_PAGE_THEMES.MIXED
: isDocumine
? LANDING_PAGE_THEMES.DOCUMINE
: LANDING_PAGE_THEMES.REDACT_MANAGER;
this._values = {
...this._values,
LANDING_PAGE_THEME: landingPageTheme,
APP_NAME: landingPageTheme === LANDING_PAGE_THEMES.MIXED ? 'Knecon Cloud' : isDocumine ? 'DocuMine' : 'RedactManager',
IS_DOCUMINE: isDocumine,
THEME: !isDocumine ? THEME_DIRECTORIES.REDACT : THEME_DIRECTORIES.SCM,
};
}
}
} }
export function getConfig<T extends IqserAppConfig = IqserAppConfig>() { export function getConfig<T extends IqserAppConfig = IqserAppConfig>() {

View File

@ -1,7 +1,7 @@
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { List } from '../utils'; import { List } from '../utils/types/iqser-types';
import { GenericService } from './generic.service'; import { GenericService } from './generic.service';
export type UserAttributes = Record<string, List>; export type UserAttributes = Record<string, List>;
@ -16,7 +16,7 @@ export const KEYS = {
export abstract class IqserUserPreferenceService extends GenericService<UserAttributes> { export abstract class IqserUserPreferenceService extends GenericService<UserAttributes> {
#userAttributes: UserAttributes = {}; #userAttributes: UserAttributes = {};
protected abstract readonly _devFeaturesEnabledKey: string; protected abstract readonly _devFeaturesEnabledKey: string;
protected override readonly _serviceName: string = 'tenant-user-management'; protected readonly _serviceName: string = 'tenant-user-management';
get userAttributes(): UserAttributes { get userAttributes(): UserAttributes {
return this.#userAttributes; return this.#userAttributes;
@ -37,7 +37,7 @@ export abstract class IqserUserPreferenceService extends GenericService<UserAttr
} }
getLanguage(): string { getLanguage(): string {
return this._getAttribute(KEYS.language, 'en'); return this._getAttribute(KEYS.language);
} }
async saveLanguage(language: string): Promise<void> { async saveLanguage(language: string): Promise<void> {

View File

@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable, switchMap } from 'rxjs'; import { BehaviorSubject, Observable, switchMap } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { HeadersConfiguration } from '../utils/headers-configuration'; import { HeadersConfiguration } from '../utils/headers-configuration';
import { mapEach } from '../utils/operators'; import { mapEach } from '../utils/operators';
@ -26,14 +26,6 @@ export abstract class StatsService<E, I = E> {
); );
} }
getOne(id: string): Observable<E> {
const request = this.#http.get<I>(`/${this._serviceName}/${encodeURI(this._defaultModelPath)}/${id}`, {
headers: HeadersConfiguration.getHeaders(),
});
return request.pipe(map(entity => new this._entityClass(entity)));
}
get(key: string): E { get(key: string): E {
return this._getBehaviourSubject(key).value; return this._getBehaviourSubject(key).value;
} }

View File

@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@Component({ @Component({
selector: 'iqser-logo', selector: 'iqser-logo',
template: `<mat-icon [svgIcon]="icon()"></mat-icon>`, template: ` <mat-icon [svgIcon]="icon"></mat-icon>`,
styles: [ styles: [
` `
:host { :host {
@ -17,8 +17,9 @@ import { MatIconModule } from '@angular/material/icon';
`, `,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIconModule], imports: [MatIconModule],
}) })
export class LogoComponent { export class LogoComponent {
readonly icon = input.required<string>(); @Input({ required: true }) icon!: string;
} }

View File

@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
selector: 'iqser-side-nav [title]', selector: 'iqser-side-nav [title]',
templateUrl: './side-nav.component.html', templateUrl: './side-nav.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
}) })
export class SideNavComponent { export class SideNavComponent {
@Input() title!: string; @Input() title!: string;

View File

@ -9,6 +9,7 @@ import { tap } from 'rxjs/operators';
templateUrl: './skeleton.component.html', templateUrl: './skeleton.component.html',
styleUrls: ['./skeleton.component.scss'], styleUrls: ['./skeleton.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgTemplateOutlet, AsyncPipe], imports: [NgTemplateOutlet, AsyncPipe],
}) })
export class SkeletonComponent { export class SkeletonComponent {

View File

@ -13,6 +13,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
`, `,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
}) })
export class SmallChipComponent { export class SmallChipComponent {
@Input() color!: string; @Input() color!: string;

View File

@ -4,6 +4,7 @@ import { Component, HostBinding, Input } from '@angular/core';
selector: 'iqser-spacer [height]', selector: 'iqser-spacer [height]',
template: ' <div></div> ', template: ' <div></div> ',
styleUrls: ['./spacer.component.scss'], styleUrls: ['./spacer.component.scss'],
standalone: true,
}) })
export class SpacerComponent { export class SpacerComponent {
@Input({ required: true }) height!: number; @Input({ required: true }) height!: number;

View File

@ -9,6 +9,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
styleUrls: ['./status-bar.component.scss'], styleUrls: ['./status-bar.component.scss'],
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgClass, NgStyle, MatTooltipModule], imports: [NgClass, NgStyle, MatTooltipModule],
}) })
export class StatusBarComponent<T extends string> { export class StatusBarComponent<T extends string> {

View File

@ -8,6 +8,7 @@ import { StopPropagationDirective } from '../../directives';
@Component({ @Component({
templateUrl: './toast.component.html', templateUrl: './toast.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIconModule, StopPropagationDirective], imports: [MatIconModule, StopPropagationDirective],
}) })
export class ToastComponent extends Toast { export class ToastComponent extends Toast {

View File

@ -3,7 +3,7 @@ import { SortingOrder } from './models/sorting-order.type';
import { KeysOf } from '../utils'; import { KeysOf } from '../utils';
import { sort } from './functions'; import { sort } from './functions';
@Pipe({ name: 'sortBy' }) @Pipe({ name: 'sortBy', standalone: true })
export class SortByPipe implements PipeTransform { export class SortByPipe implements PipeTransform {
transform<T>(values: T[], order: SortingOrder, column: KeysOf<T>): T[] { transform<T>(values: T[], order: SortingOrder, column: KeysOf<T>): T[] {
return sort(values, order, column); return sort(values, order, column);

View File

@ -1,9 +1,7 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { KeycloakEventType, KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { filter, switchMap } from 'rxjs/operators';
import { getConfig } from '../../services/iqser-config.service'; import { getConfig } from '../../services/iqser-config.service';
import { log, shareLast } from '../../utils';
import { UI_ROOT } from '../../utils/tokens'; import { UI_ROOT } from '../../utils/tokens';
import { getKeycloakOptions } from '../keycloak-options'; import { getKeycloakOptions } from '../keycloak-options';
import { TenantsService } from './tenants.service'; import { TenantsService } from './tenants.service';
@ -15,12 +13,6 @@ export class KeycloakStatusService {
readonly #tenantsService = inject(TenantsService); readonly #tenantsService = inject(TenantsService);
readonly #uiRoot = inject(UI_ROOT); readonly #uiRoot = inject(UI_ROOT);
readonly #logger = inject(NGXLogger); readonly #logger = inject(NGXLogger);
readonly token$ = this.#keycloakService.keycloakEvents$.pipe(
log('[KEYCLOAK] New event:'),
filter(event => event.type === KeycloakEventType.OnAuthSuccess || event.type === KeycloakEventType.OnAuthRefreshSuccess),
switchMap(() => this.#keycloakService.getToken()),
shareLast(),
);
createLoginUrlAndExecute(username?: string | null) { createLoginUrlAndExecute(username?: string | null) {
const keycloakInstance = this.#keycloakService?.getKeycloakInstance(); const keycloakInstance = this.#keycloakService?.getKeycloakInstance();

View File

@ -1,18 +1,14 @@
import { inject, Injectable, signal } from '@angular/core'; import { inject, Injectable, signal } from '@angular/core';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { firstValueFrom, Observable, take } from 'rxjs';
import { GenericService } from '../../services';
import { List } from '../../utils'; import { List } from '../../utils';
import { GenericService } from '../../services';
import { Tenant, TenantDetails } from '../types'; import { Tenant, TenantDetails } from '../types';
import { APPLICATION_TYPES } from '../../utils/constants'; import { Observable } from 'rxjs';
import { toObservable } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs/operators';
export interface IStoredTenantId { export interface IStoredTenantId {
readonly tenantId: string; readonly tenantId: string;
readonly created: string; readonly created: string;
readonly documine: boolean;
} }
export type StoredTenantIds = List<IStoredTenantId>; export type StoredTenantIds = List<IStoredTenantId>;
@ -31,35 +27,25 @@ export class TenantsService extends GenericService<Tenant> {
key: localStorage.key.bind(localStorage), key: localStorage.key.bind(localStorage),
}; };
readonly #activeTenantId = signal(''); readonly #activeTenantId = signal('');
readonly #tenantSet = signal(false);
readonly tenantSet$ = toObservable(this.#tenantSet);
protected readonly _defaultModelPath = 'tenants'; protected readonly _defaultModelPath = 'tenants';
protected override readonly _serviceName: string = 'tenant-user-management'; protected readonly _serviceName: string = 'tenant-user-management';
get activeTenantId() { get activeTenantId() {
return this.#activeTenantId(); return this.#activeTenantId();
} }
get activeTenant() {
return this.getStoredTenants().find(t => t.tenantId === this.activeTenantId);
}
async selectTenant(tenantId: string): Promise<boolean> { async selectTenant(tenantId: string): Promise<boolean> {
this.#mutateStorage(tenantId); this.#mutateStorage(tenantId);
this.#setActiveTenantId(tenantId); this.#setActiveTenantId(tenantId);
return true; return true;
} }
async storeTenant() { storeTenant() {
const storedTenants = this.getStoredTenants(); const storedTenants = this.getStoredTenants();
const activeTenantId = this.#activeTenantId(); const activeTenantId = this.#activeTenantId();
const existing = storedTenants.find(s => s.tenantId === activeTenantId); const existing = storedTenants.find(s => s.tenantId === activeTenantId);
const tenant = await firstValueFrom(this.getActiveTenant());
if (existing) { if (existing) {
this.#logger.info('[TENANTS] Stored tenant exists: ', storedTenants); this.#logger.info('[TENANTS] Stored tenant exists: ', storedTenants);
this.#tenantSet.set(true);
return; return;
} }
@ -68,13 +54,8 @@ export class TenantsService extends GenericService<Tenant> {
return; return;
} }
storedTenants.push({ storedTenants.push({ tenantId: activeTenantId, created: new Date().toISOString() });
tenantId: activeTenantId,
created: new Date().toISOString(),
documine: tenant.applicationType === APPLICATION_TYPES.DOCUMINE,
});
this.#storageReference.setItem(STORED_TENANTS_KEY, JSON.stringify(storedTenants)); this.#storageReference.setItem(STORED_TENANTS_KEY, JSON.stringify(storedTenants));
this.#tenantSet.set(true);
this.#logger.info('[TENANTS] Stored tenants: ', storedTenants); this.#logger.info('[TENANTS] Stored tenants: ', storedTenants);
} }
@ -123,13 +104,6 @@ export class TenantsService extends GenericService<Tenant> {
return this._getOne([this.activeTenantId]); return this._getOne([this.activeTenantId]);
} }
waitForSettingTenant(): Observable<boolean> {
return this.tenantSet$.pipe(
filter(tenantSet => tenantSet),
take(1),
);
}
#setActiveTenantId(tenantId: string) { #setActiveTenantId(tenantId: string) {
this.#logger.info('[TENANTS] Set current tenant id: ', tenantId); this.#logger.info('[TENANTS] Set current tenant id: ', tenantId);
this.#activeTenantId.set(tenantId); this.#activeTenantId.set(tenantId);

View File

@ -21,7 +21,7 @@
<div> <div>
@for (stored of storedTenants; track stored) { @for (stored of storedTenants; track stored) {
<div (click)="select(stored.tenantId)" class="d-flex pointer mat-elevation-z2 card stored-tenant-card mt-10"> <div (click)="select(stored.tenantId)" class="d-flex pointer mat-elevation-z2 card stored-tenant-card mt-10">
<iqser-logo class="card-icon" [icon]="tenantIcon(stored)" mat-card-image></iqser-logo> <iqser-logo class="card-icon" icon="iqser:logo" mat-card-image></iqser-logo>
<div class="card-content flex-column"> <div class="card-content flex-column">
<span class="heading">{{ stored.tenantId }}</span> <span class="heading">{{ stored.tenantId }}</span>
</div> </div>

View File

@ -15,7 +15,6 @@ import { KeycloakStatusService } from '../services/keycloak-status.service';
templateUrl: './tenant-select.component.html', templateUrl: './tenant-select.component.html',
styleUrls: ['./tenant-select.component.scss'], styleUrls: ['./tenant-select.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class TenantSelectComponent { export class TenantSelectComponent {
readonly #uiRoot = inject(UI_ROOT); readonly #uiRoot = inject(UI_ROOT);
@ -71,10 +70,6 @@ export class TenantSelectComponent {
this.#loadStoredTenants(); this.#loadStoredTenants();
} }
protected tenantIcon(tenant: IStoredTenantId) {
return `red:${tenant.documine ? 'documine' : 'redaction'}-logo`;
}
#loadStoredTenants() { #loadStoredTenants() {
this.storedTenants = this.tenantsService.getStoredTenants().sort((a, b) => a.tenantId.localeCompare(b.tenantId)); this.storedTenants = this.tenantsService.getStoredTenants().sort((a, b) => a.tenantId.localeCompare(b.tenantId));
} }

View File

@ -1,10 +1,7 @@
import { ApplicationType } from '../../utils/constants';
export type TenantDetails = Record<string, unknown>; export type TenantDetails = Record<string, unknown>;
export interface Tenant<TD extends TenantDetails = TenantDetails> { export interface Tenant<TD extends TenantDetails = TenantDetails> {
tenantId: string; tenantId: string;
displayName: string; displayName: string;
applicationType: ApplicationType;
details: TD; details: TD;
} }

View File

@ -2,13 +2,11 @@ import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { getConfig } from '../services'; import { getConfig } from '../services';
import { PruningTranslationLoader } from '../utils'; import { PruningTranslationLoader } from '../utils';
import { TenantsService } from '../tenants';
export function pruningTranslationLoaderFactory(pathPrefix: string): PruningTranslationLoader { export function pruningTranslationLoaderFactory(pathPrefix: string): PruningTranslationLoader {
const httpClient = inject(HttpClient); const httpClient = inject(HttpClient);
const tenantService = inject(TenantsService);
const config = getConfig(); const config = getConfig();
const version = config.FRONTEND_APP_VERSION; const version = config.FRONTEND_APP_VERSION;
return new PruningTranslationLoader(httpClient, tenantService, pathPrefix, `.json?version=${version}`); return new PruningTranslationLoader(httpClient, pathPrefix, `.json?version=${version}`);
} }

View File

@ -4,7 +4,7 @@ import { escapeHtml } from '../utils';
@Injectable() @Injectable()
export class IqserTranslateParser extends TranslateDefaultParser { export class IqserTranslateParser extends TranslateDefaultParser {
override interpolate(expr: any, params?: Record<string, unknown>) { interpolate(expr: any, params?: Record<string, unknown>) {
const entries = Object.entries(params ?? {}); const entries = Object.entries(params ?? {});
const escapedParams = entries.reduce((acc, [key, value]) => ({ ...acc, [key]: escapeHtml(value) }), {}); const escapedParams = entries.reduce((acc, [key, value]) => ({ ...acc, [key]: escapeHtml(value) }), {});
return super.interpolate(expr, escapedParams); return super.interpolate(expr, escapedParams);

View File

@ -1,42 +1,21 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { TranslateLoader } from '@ngx-translate/core'; import { TranslateLoader } from '@ngx-translate/core';
import { map, switchMap } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { TenantsService } from '../tenants';
import { APP_TYPE_PATHS } from '../utils/constants';
import { inject } from '@angular/core';
import { GET_TENANT_FROM_PATH_FN } from '../utils';
interface T { interface T {
[key: string]: string | T; [key: string]: string | T;
} }
export class PruningTranslationLoader implements TranslateLoader { export class PruningTranslationLoader implements TranslateLoader {
readonly tenant = inject(GET_TENANT_FROM_PATH_FN)();
constructor( constructor(
private readonly _http: HttpClient, private readonly _http: HttpClient,
private readonly _tenantService: TenantsService,
private readonly _prefix: string, private readonly _prefix: string,
private readonly _suffix: string, private readonly _suffix: string,
) {} ) {}
getTranslation(lang: string): Observable<T> { getTranslation(lang: string): Observable<T> {
if (this.tenant) { return this._http.get(`${this._prefix}${lang}${this._suffix}`).pipe(map(result => this._process(result as T)));
return this._tenantService.waitForSettingTenant().pipe(
switchMap(() => {
const tenant = this._tenantService.activeTenant;
const translationPath = tenant?.documine ? APP_TYPE_PATHS.SCM : APP_TYPE_PATHS.REDACT;
return this._http
.get(`${this._prefix}${translationPath}/${lang}${this._suffix}`)
.pipe(map(result => this._process(result as T)));
}),
);
}
return this._http
.get(`${this._prefix}${APP_TYPE_PATHS.REDACT}/${lang}${this._suffix}`)
.pipe(map(result => this._process(result as T)));
} }
private _process(object: T): T { private _process(object: T): T {

View File

@ -2,6 +2,7 @@ import { Directive, EventEmitter, HostBinding, HostListener, Output } from '@ang
@Directive({ @Directive({
selector: '[iqserDragDropFileUpload]', selector: '[iqserDragDropFileUpload]',
standalone: true,
}) })
export class DragDropFileUploadDirective { export class DragDropFileUploadDirective {
@Output() readonly fileDropped = new EventEmitter(); @Output() readonly fileDropped = new EventEmitter();

View File

@ -1,14 +1,17 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { DragDropFileUploadDirective } from './drag-drop-file-upload.directive'; import { DragDropFileUploadDirective } from './drag-drop-file-upload.directive';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@Component({ @Component({
selector: 'iqser-upload-file', selector: 'iqser-upload-file',
templateUrl: './upload-file.component.html', templateUrl: './upload-file.component.html',
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: { '[class.iqser-upload-file]': 'true' }, host: { '[class.iqser-upload-file]': 'true' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DragDropFileUploadDirective, MatIcon, TranslateModule], standalone: true,
imports: [DragDropFileUploadDirective, MatIcon, NgIf, TranslateModule],
}) })
export class UploadFileComponent { export class UploadFileComponent {
@ViewChild('attachFileInput', { static: true }) attachFileInput!: ElementRef; @ViewChild('attachFileInput', { static: true }) attachFileInput!: ElementRef;

View File

@ -1,19 +1,21 @@
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { IqserUser } from '../../iqser-user.model'; import { IqserUser } from '../../iqser-user.model';
import { NamePipe } from '../../name.pipe';
import { IqserUserService } from '../../services/iqser-user.service'; import { IqserUserService } from '../../services/iqser-user.service';
import { NamePipeOptions } from '../../types/name-pipe-options'; import { NamePipeOptions } from '../../types/name-pipe-options';
import { IIqserUser } from '../../types/user.response'; import { IIqserUser } from '../../types/user.response';
import { NgIf } from '@angular/common';
import { NamePipe } from '../../name.pipe';
import { MatTooltip } from '@angular/material/tooltip';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'iqser-initials-avatar', selector: 'iqser-initials-avatar',
templateUrl: './initials-avatar.component.html', templateUrl: './initials-avatar.component.html',
styleUrls: ['./initials-avatar.component.scss'], styleUrls: ['./initials-avatar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NamePipe, MatTooltip], standalone: true,
imports: [NgIf, NamePipe, MatTooltip],
}) })
export class InitialsAvatarComponent< export class InitialsAvatarComponent<
Interface extends IIqserUser = IIqserUser, Interface extends IIqserUser = IIqserUser,
@ -21,9 +23,11 @@ export class InitialsAvatarComponent<
> { > {
readonly #userService = inject<IqserUserService<Interface, Class>>(IqserUserService); readonly #userService = inject<IqserUserService<Interface, Class>>(IqserUserService);
readonly #translateService = inject(TranslateService); readonly #translateService = inject(TranslateService);
readonly #isSystemUser = computed(() => this._user()?.id?.toLowerCase() === 'system');
readonly #users = toSignal(this.#userService.all$); readonly #users = toSignal(this.#userService.all$);
readonly color = input('lightgray'); readonly color = input('lightgray');
readonly size = input<'small' | 'large' | 'extra-small'>('small'); readonly size = input<'small' | 'large'>('small');
readonly withName = input(false); readonly withName = input(false);
readonly showYou = input(false); readonly showYou = input(false);
readonly tooltipPosition = input<'below' | 'above'>('above'); readonly tooltipPosition = input<'below' | 'above'>('above');
@ -31,6 +35,7 @@ export class InitialsAvatarComponent<
readonly showTooltip = input(true); readonly showTooltip = input(true);
readonly user = input.required<Class | string>(); readonly user = input.required<Class | string>();
readonly showBorderCondition = input<<T extends Class = Class>(user: T) => boolean>(user => user.isSpecial); readonly showBorderCondition = input<<T extends Class = Class>(user: T) => boolean>(user => user.isSpecial);
readonly _user = computed(() => { readonly _user = computed(() => {
const user = this.user(); const user = this.user();
if (typeof user === 'string') { if (typeof user === 'string') {
@ -40,7 +45,6 @@ export class InitialsAvatarComponent<
} }
return user; return user;
}); });
readonly #isSystemUser = computed(() => this._user()?.id?.toLowerCase() === 'system');
readonly isCurrentUser = computed(() => this.#userService.currentUser?.id === this._user()?.id); readonly isCurrentUser = computed(() => this.#userService.currentUser?.id === this._user()?.id);
readonly hasBorder = computed(() => !!this._user() && !this.isCurrentUser() && this.showBorderCondition()(this._user()!)); readonly hasBorder = computed(() => !!this._user() && !this.isCurrentUser() && this.showBorderCondition()(this._user()!));
readonly disabled = computed(() => !!this._user() && !this.#isSystemUser() && !this._user()?.hasAnyRole); readonly disabled = computed(() => !!this._user() && !this.#isSystemUser() && !this._user()?.hasAnyRole);

View File

@ -6,7 +6,6 @@ import { IqserUserService } from '../../services/iqser-user.service';
templateUrl: './user-button.component.html', templateUrl: './user-button.component.html',
styleUrls: ['./user-button.component.scss'], styleUrls: ['./user-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
}) })
export class UserButtonComponent { export class UserButtonComponent {
@Input() showDot = false; @Input() showDot = false;

View File

@ -19,6 +19,7 @@ function getInitials(name: string) {
@Pipe({ @Pipe({
name: 'name', name: 'name',
standalone: true,
}) })
export class NamePipe implements PipeTransform { export class NamePipe implements PipeTransform {
readonly #translateService = inject(TranslateService); readonly #translateService = inject(TranslateService);

View File

@ -23,11 +23,11 @@ export abstract class IqserUserService<
Interface extends IIqserUser = IIqserUser, Interface extends IIqserUser = IIqserUser,
Class extends IqserUser & Interface = IqserUser & Interface, Class extends IqserUser & Interface = IqserUser & Interface,
> extends EntitiesService<Interface, Class> { > extends EntitiesService<Interface, Class> {
readonly #uiRoot = inject(UI_ROOT); readonly currentUser$: Observable<Class | undefined>;
protected abstract override readonly _defaultModelPath: string; protected abstract readonly _defaultModelPath: string;
protected abstract readonly _permissionsFilter: (role: string) => boolean; protected abstract readonly _permissionsFilter: (role: string) => boolean;
protected abstract readonly _rolesFilter: (role: string) => boolean; protected abstract readonly _rolesFilter: (role: string) => boolean;
protected abstract override readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class; protected abstract readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class;
protected readonly _currentUser$ = new BehaviorSubject<Class | undefined>(undefined); protected readonly _currentUser$ = new BehaviorSubject<Class | undefined>(undefined);
protected readonly _toaster = inject(Toaster); protected readonly _toaster = inject(Toaster);
protected readonly _keycloakService = inject(KeycloakService); protected readonly _keycloakService = inject(KeycloakService);
@ -35,8 +35,8 @@ export abstract class IqserUserService<
protected readonly _keycloakStatusService = inject(KeycloakStatusService); protected readonly _keycloakStatusService = inject(KeycloakStatusService);
protected readonly _permissionsService = inject(IqserPermissionsService, { optional: true }); protected readonly _permissionsService = inject(IqserPermissionsService, { optional: true });
protected readonly _rolesService = inject(IqserRolesService, { optional: true }); protected readonly _rolesService = inject(IqserRolesService, { optional: true });
protected override readonly _serviceName: string = 'tenant-user-management'; protected readonly _serviceName: string = 'tenant-user-management';
readonly currentUser$: Observable<Class | undefined>; readonly #uiRoot = inject(UI_ROOT);
constructor() { constructor() {
super(); super();
@ -80,7 +80,7 @@ export abstract class IqserUserService<
await this._keycloakService.login({ action: 'UPDATE_PASSWORD' }); await this._keycloakService.login({ action: 'UPDATE_PASSWORD' });
} }
override loadAll() { loadAll() {
return this.getAll().pipe( return this.getAll().pipe(
mapEach(user => new this._entityClass(user, user.roles, user.userId)), mapEach(user => new this._entityClass(user, user.roles, user.userId)),
tap(users => this.setEntities(users)), tap(users => this.setEntities(users)),
@ -121,7 +121,7 @@ export abstract class IqserUserService<
return this.find(userId)?.name; return this.find(userId)?.name;
} }
override getAll(url = this._defaultModelPath): Observable<Interface[]> { getAll(url = this._defaultModelPath): Observable<Interface[]> {
return super.getAll(url, [{ key: 'refreshCache', value: true }]); return super.getAll(url, [{ key: 'refreshCache', value: true }]);
} }
@ -157,12 +157,12 @@ export abstract class IqserUserService<
return this._post(body); return this._post(body);
} }
override delete(userIds: List) { delete(userIds: List) {
const queryParams = userIds.map<QueryParam>(userId => ({ key: 'userId', value: userId })); const queryParams = userIds.map<QueryParam>(userId => ({ key: 'userId', value: userId }));
return super.delete(userIds, this._defaultModelPath, queryParams); return super.delete(userIds, this._defaultModelPath, queryParams);
} }
override find(id: string): Class | undefined { find(id: string): Class | undefined {
if (id?.toLowerCase() === 'system') { if (id?.toLowerCase() === 'system') {
return this.newSystemUser(); return this.newSystemUser();
} }

View File

@ -5,5 +5,4 @@ export interface ICreateUserRequest {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
roles?: List; roles?: List;
sendSetPasswordMail?: boolean;
} }

View File

@ -51,31 +51,3 @@ export const ICONS = new Set([
'visibility', 'visibility',
'visibility-off', 'visibility-off',
]); ]);
export const LANDING_PAGE_THEMES = {
REDACT_MANAGER: 'redactmanager',
DOCUMINE: 'documine',
MIXED: 'mixed',
} as const;
export const THEME_DIRECTORIES = {
REDACT: 'redact',
SCM: 'scm',
} as const;
export const APP_TYPE_PATHS = {
REDACT: 'redact',
SCM: 'scm',
} as const;
export const APPLICATION_TYPES = {
REDACT_MANAGER: 'RedactManager',
DOCUMINE: 'DocuMine',
} as const;
export type ApplicationType = (typeof APPLICATION_TYPES)[keyof typeof APPLICATION_TYPES];
export const MANUAL_BASE_URL = {
REDACT_MANAGER: 'https://docs.redactmanager.com/preview',
DOCUMINE: 'https://docs.documine.ai/1.2',
};

View File

@ -1,10 +1,9 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import dayjs, { type Dayjs } from 'dayjs'; import dayjs, { type Dayjs } from 'dayjs';
import { forOwn, has, isEqual, isPlainObject, transform } from 'lodash-es'; import { forOwn, has, isEqual, isPlainObject, transform } from 'lodash-es';
import { Id, ITrackable } from '../listing/models/trackable'; import { Id, ITrackable } from '../listing/models/trackable';
import { toSignal } from '@angular/core/rxjs-interop';
export function capitalize(value: string | string): string { export function capitalize(value: string | string): string {
if (!value) { if (!value) {
@ -142,16 +141,12 @@ export function trackByFactory<T extends ITrackable<PrimaryKey>, PrimaryKey exte
return (_index: number, item: T): Id => item.id; return (_index: number, item: T): Id => item.id;
} }
export function hasFormChanged(form: UntypedFormGroup, initialFormValue: Record<string, unknown>, ignoredKeys: string[] = []): boolean { export function hasFormChanged(form: UntypedFormGroup, initialFormValue: Record<string, unknown>): boolean {
if (!form || !initialFormValue) { if (!form || !initialFormValue) {
return false; return false;
} }
for (const key of Object.keys(form.getRawValue())) { for (const key of Object.keys(form.getRawValue())) {
if (ignoredKeys.includes(key)) {
continue;
}
const initialValue = initialFormValue[key]; const initialValue = initialFormValue[key];
const updatedValue = form.get(key)?.value; const updatedValue = form.get(key)?.value;
@ -331,11 +326,3 @@ export function getParamFromDialog(param: string, activatedRoute = inject(Activa
return getParam(param, getLastChild(activatedRoute.root)); return getParam(param, getLastChild(activatedRoute.root));
} }
export function formValueToSignal<T>(control: AbstractControl<T>) {
return toSignal(control.valueChanges, { initialValue: control.value });
}
export function formStatusToSignal<T>(control: AbstractControl<T>) {
return toSignal(control.statusChanges, { initialValue: control.status });
}

Some files were not shown because too many files have changed in this diff Show More