Compare commits

...

56 Commits

Author SHA1 Message Date
Valentin-Gabriel Mihai
7f13fa62d3 Merge branch 'unprotected' into 'master'
update master

See merge request fforesight/shared-ui-libraries/common-ui!15
2024-12-13 13:15:01 +01:00
Dan Percic
4ed7215292 skip quality gate 2024-12-13 13:16:06 +02:00
Dan Percic
1ea220b489 Merge remote-tracking branch 'origin/master' into unprotected 2024-12-13 13:15:39 +02:00
Valentin Mihai
fc8be33dc6 RED-9580 - added getOne method for stats service to call new users stats endpoint and updated data from "delete user dialog" with new backend data 2024-12-13 12:02:51 +02:00
Valentin Mihai
58382ddfee RED-10301 - set app type config vars based existing tenants from local storage or used default config if no tenant was set before 2024-12-11 18:22:28 +02:00
Nicoleta Panaghiu
0d20afcf27 RED-10647: update manual base link depending on application type. 2024-12-11 18:08:21 +02:00
Nicoleta Panaghiu
aa5fc54576 RED-9856: support a smaller avatar size. 2024-12-10 17:02:25 +02:00
Christoph Schabert
e5c4fd7d3c add sonarqube config 2024-12-09 15:27:35 +01:00
Nicoleta Panaghiu
a4e3ed8854 RED-3800: removed debug statements. 2024-12-07 14:07:55 +02:00
Nicoleta Panaghiu
acabdba657 RED-3800: fixed sass update breaking changes. 2024-12-07 14:03:56 +02:00
Nicoleta Panaghiu
0e6e4f7b09 RED-10614: fixed missing injection context error, 2024-12-06 19:22:12 +02:00
Dan Percic
7bb24c4f91 lint 2024-12-05 12:50:45 +02:00
Dan Percic
e21c225ddd updates 2024-12-05 12:49:51 +02:00
Dan Percic
8582f2e6be ng 19 2024-12-05 11:46:15 +02:00
Dan Percic
b929f1d136 Merge branch 'VM/RED-10301' into 'unprotected'
RED-10301 - Use RM/DM UI depending on application type of tenant

See merge request fforesight/shared-ui-libraries/common-ui!14
2024-12-03 13:16:28 +01:00
Valentin Mihai
55b21c4989 lint 2024-12-03 12:37:45 +02:00
Valentin Mihai
911516add2 RED-10301 - Use RM/DM UI depending on application type of tenant 2024-12-03 12:31:24 +02:00
Nicoleta Panaghiu
cbdfcf4d8f RED-10517: fixed selected sorting. 2024-11-27 16:52:17 +02:00
Nicoleta Panaghiu
72e760fff8 RED-9885: scoped right-container transition. 2024-11-27 16:24:51 +02:00
Nicoleta Panaghiu
ae0eebcc6f RED-10509: added disableStopPropagation option for action config. 2024-11-24 13:30:43 +02:00
Timo Bejan
7c6f9fc25e removed logging 2024-11-06 15:49:04 +02:00
Timo Bejan
310cc4bb51 Removed offset and fixed notification polling 2024-11-06 15:39:31 +02:00
Valentin Mihai
99facc0434 RED-10373 - updated details radio option description to can receive number params 2024-11-04 16:55:40 +02:00
Dan Percic
ebaf1709b1 add some overrides & keycloak token$ 2024-11-01 19:20:42 +02:00
Nicoleta Panaghiu
17d2e8c530 RED-10206: added disabled and tooltip directives for button config. 2024-11-01 14:46:19 +02:00
Dan Percic
b9c01a287c lint fixes 2024-10-31 11:28:14 +02:00
Dan Percic
6c7865a8ec fix filters track expression 2024-10-31 11:26:49 +02:00
Nicoleta Panaghiu
9e457a13b4 RED-8277: only show pointer cursor if entity has routerLink. 2024-10-23 16:09:16 +03:00
Nicoleta Panaghiu
fba9b330dd RED-8277: use pointer cursor for table items. 2024-10-22 17:55:52 +03:00
Valentin Mihai
d7ad450ca1 RED-7340 - red border around the page input field should only appear after the user has finished entering the page range 2024-10-22 17:19:48 +03:00
Valentin Mihai
a5d10ad148 RED-7340 - When a user has entered an invalid page range string, display a read border around the input field 2024-10-21 17:52:48 +03:00
Nicoleta Panaghiu
e92bd55cfc RED-3800: some fixes after switching from div to a. 2024-10-17 11:59:38 +03:00
Nicoleta Panaghiu
f36b1fa8e2 RED-8277: use a instead of div. 2024-10-16 15:26:29 +03:00
Nicoleta Panaghiu
3f214d9726 RED-10139: moved formToSignal functions to common.
Future refactoring purposes.
2024-10-16 13:44:53 +03:00
Valentin Mihai
ba85260cc4 RED-7340 - set fix width for extra option input 2024-10-16 11:55:33 +03:00
Nicoleta Panaghiu
e88929f0d4 RED-10180: increased extraOption container width. 2024-10-14 17:25:31 +03:00
Nicoleta Panaghiu
32de775859 RED-10183: set default language in user preference. 2024-10-14 12:48:58 +03:00
Valentin Mihai
3c89b8f7e7 RED-7340 - remove not used imports 2024-10-05 20:48:00 +03:00
Valentin Mihai
34387d49d2 RED-7340 - updated details radio component to have an option input when is needed 2024-09-27 16:41:59 +03:00
Nicoleta Panaghiu
304657d259 RED-9985: prevent filter categories from collapsing upon selection. 2024-09-26 11:51:52 +03:00
Nicoleta Panaghiu
835cb7820e RED-9578: move custom component above the question. 2024-09-25 13:41:37 +03:00
Adina Țeudan
3724a6c0b6 Ignored keys for form changed in dialogs 2024-09-23 22:23:02 +03:00
Nicoleta Panaghiu
c71a4995a6 RED-9578: added support for custom component and reversing buttons. 2024-09-19 14:52:04 +03:00
Valentin Mihai
c644eaeba2 RED-7345 - added force-annotation class 2024-09-16 21:15:52 +03:00
Nicoleta Panaghiu
2faecb44a9 RED-9372: always include scroll-bar mixin. 2024-09-10 11:37:55 +03:00
Nicoleta Panaghiu
81513d34dc RED-9372: fixed table-items moving on hover. 2024-09-05 17:14:22 +03:00
Nicoleta Panaghiu
9bc05f1165 RED-9987: added sendSetPasswordMail flag. 2024-09-03 13:45:40 +03:00
Nicoleta Panaghiu
007e761bd5 RED-9916: fixed button ids. 2024-08-20 13:22:07 +03:00
Nicoleta Panaghiu
0ca5e7e2ad RED-9777: increase dialog help button z-index. 2024-08-14 11:42:31 +03:00
Nicoleta Panaghiu
6547eb2ad5 RED-9777: fixed viewer header alignment + added components view parent. 2024-08-12 11:15:53 +03:00
Adina Țeudan
17943f2e8d HasScrollbar directive with signals 2024-08-09 23:51:51 +03:00
Adina Țeudan
c8b3e3eb3c Revert "RED-9372: fixed table-item elements moving on hover when scrollable."
This reverts commit 845c819392bc5f73a5feb584dc879e965107a9c0.
2024-08-09 23:51:21 +03:00
Nicoleta Panaghiu
c331a61309 RED-9817: removed HTML code for space. 2024-08-09 12:15:50 +03:00
Valentin Mihai
5b51eb85a1 RED-9788 - remove active listing entity service 2024-08-09 08:56:48 +03:00
Valentin Mihai
f079b6c157 RED-9201 - update upload component height 2024-08-08 11:05:01 +03:00
Adina Țeudan
a8b5cfce14 Unordered async guards 2024-08-07 21:30:41 +03:00
111 changed files with 545 additions and 243 deletions

View File

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

19
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,19 @@
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/"

2
sonar-project.properties Normal file
View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
width: 100%;
box-sizing: border-box;
background: var(--iqser-alt-background);
height: 68px;
&.drag-over {
background-color: var(--iqser-file-drop-drag-over);
@ -15,7 +16,6 @@
.upload-area {
gap: 16px;
height: 88px;
cursor: pointer;
padding: 0 32px;
@ -33,7 +33,6 @@
.file-area {
gap: 10px;
height: 48px;
mat-icon:first-child {
opacity: 0.5;

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ 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, MatButtonModule, StopPropagationDirective],
})
export class CircleButtonComponent {

View File

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

View File

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

View File

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

View File

@ -1,15 +1,24 @@
import { NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, HostListener, inject, TemplateRef } from '@angular/core';
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 } from '../../buttons/circle-button/circle-button.component';
import { IconButtonComponent } from '../../buttons/icon-button/icon-button.component';
import { IconButtonTypes } from '../../buttons/types/icon-button.type';
import { ValuesOf } from '../../utils/types/utility-types';
import { CircleButtonComponent, IconButtonTypes } from '../../buttons';
import { IconButtonComponent } from '../../buttons';
import { ValuesOf } from '../../utils';
export const TitleColors = {
DEFAULT: 'default',
@ -48,6 +57,9 @@ interface InternalConfirmationDialogData {
readonly checkboxes: CheckBox[];
readonly checkboxesValidation: boolean;
readonly toastMessage?: string;
readonly component?: Type<unknown>;
readonly componentInputs?: { [key: string]: unknown };
readonly cancelButtonPrimary?: boolean;
}
export type IConfirmationDialogData = Partial<InternalConfirmationDialogData>;
@ -65,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,
};
}
@ -72,7 +87,6 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
templateUrl: './confirmation-dialog.component.html',
styleUrls: ['./confirmation-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
MatIconModule,
FormsModule,
@ -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<ConfirmationDialogComponent, ConfirmOption>,
@ -115,13 +130,19 @@ export class ConfirmationDialogComponent {
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();
}
@ -156,9 +177,13 @@ export class ConfirmationDialogComponent {
});
}
@HostListener('window:keydown.Enter', ['$event'])
onEnter(event: KeyboardEvent): void {
event?.stopImmediatePropagation();
this.confirm(ConfirmOptions.CONFIRM);
#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;
}
}
}
}

View File

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

View File

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

View File

@ -1,39 +1,35 @@
import { ChangeDetectorRef, Directive, ElementRef, HostBinding, OnDestroy, 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, OnDestroy {
@HostBinding('class') class = '';
private readonly _resizeObserver: ResizeObserver;
protected readonly _class = signal('');
get hasScrollbar() {
const element = this._elementRef?.nativeElement as HTMLElement;
return element.clientHeight < element.scrollHeight;
}
constructor(
protected readonly _elementRef: ElementRef,
protected readonly _changeDetector: ChangeDetectorRef,
) {
this._resizeObserver = new ResizeObserver(entry => {
constructor(protected readonly _elementRef: ElementRef) {
this._resizeObserver = new ResizeObserver(() => {
this.process();
});
this._resizeObserver.observe(this._elementRef.nativeElement);
}
private get _hasScrollbar() {
const element = this._elementRef?.nativeElement as HTMLElement;
return element.clientHeight < element.scrollHeight;
}
ngOnInit() {
setTimeout(() => this.process(), 0);
}
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);
}
ngOnDestroy() {

View File

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

View File

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

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
@if (primaryFilters$ | async; as filters) {
<div class="filter-content">
@for (filter of filters; track filter) {
@for (filter of filters; track filter.id) {
<ng-container
[ngTemplateOutletContext]="{
filter: filter,
@ -33,7 +33,7 @@
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
@for (filter of secondaryGroup.filters; track filter) {
@for (filter of secondaryGroup.filters; track filter.id) {
<ng-container
[ngTemplateOutletContext]="{
filter: filter,

View File

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

View File

@ -16,6 +16,7 @@ function copySettings(oldFilters: INestedFilter[], newFilters: INestedFilter[])
const newFilter = newFilters.find(f => f.id === filter.id);
if (newFilter) {
newFilter.checked = filter.checked;
newFilter.expanded = filter.expanded;
newFilter.indeterminate = filter.indeterminate;
if (filter.children && newFilter.children) {
copySettings(filter.children, newFilter.children);

View File

@ -44,11 +44,6 @@ export class FilterService {
this.showResetFilters$ = this._showResetFilters$;
}
get noAnnotationsFilterChecked() {
const filterGroup = this.filterGroups.find(g => g.slug === 'primaryFilters');
return !!filterGroup?.filters[0]?.children.find(f => f.id === 'no-annotations-filter' && f.checked);
}
get filterGroups(): IFilterGroup[] {
return Object.values(this.#filterGroups$.getValue());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
&.active,
&.dialog-toggle {
z-index: 1100;
z-index: 1200;
}
.toggle-input {

View File

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

View File

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

View File

@ -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 = `

View File

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

View File

@ -9,7 +9,7 @@ export const PDF_TRON_IFRAME_ID = 'webviewer-1';
export const WEB_VIEWER_ELEMENTS = [
{
querySelector: '.HeaderItems',
documentKey: 'pdf_features',
documentKey: 'document_viewer_features',
},
];
@ -18,6 +18,7 @@ export const ScrollableParentViews = {
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 = {
@ -25,6 +26,7 @@ export const SCROLLABLE_PARENT_VIEWS_IDS = {
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;

View File

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

View File

@ -14,29 +14,54 @@
<div class="icon-option">
<mat-icon [svgIcon]="option.icon" class="icon"></mat-icon>
<div class="text">
<label class="details-radio-label pointer">{{ option.label | translate: option.descriptionParams }}</label>
<label class="details-radio-label pointer">{{
option.label | translate: option.descriptionParams | replaceNbsp
}}</label>
<span class="hint">{{ option.description | translate: option.descriptionParams | replaceNbsp }}</span>
@if (option.extraOption && !option.extraOption.hidden && isSelected(option)) {
<div class="iqser-input-group">
<mat-checkbox
(change)="emitExtraOption()"
[(ngModel)]="option.extraOption.checked"
[checked]="option.extraOption.checked"
[disabled]="!!option.extraOption.disabled"
color="primary"
>
{{ option.extraOption.label | translate }}
</mat-checkbox>
@if (isSelected(option)) {
@if (option.additionalCheck && !option.additionalCheck.hidden) {
<div class="iqser-input-group w-450">
<mat-checkbox
(change)="emitExtraOption()"
[(ngModel)]="option.additionalCheck.checked"
[checked]="option.additionalCheck.checked"
[disabled]="!!option.additionalCheck.disabled"
color="primary"
>
{{ option.additionalCheck.label | translate | replaceNbsp }}
</mat-checkbox>
@if (option.extraOption.description) {
<span
[innerHTML]="option.extraOption.description | translate"
class="hint extra-option-description"
></span>
}
</div>
@if (option.additionalCheck.description) {
<span
[innerHTML]="option.additionalCheck.description | translate"
class="hint additional-check-description"
></span>
}
</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>
@ -48,10 +73,10 @@
<div class="flex-align-items-center mb-8">
<iqser-round-checkbox [active]="isSelected(option)" class="mr-6"></iqser-round-checkbox>
<label class="details-radio-label pointer">{{ option.label | translate }}</label>
<label class="details-radio-label pointer">{{ option.label | translate | replaceNbsp }}</label>
</div>
<span class="hint">{{ option.description | translate }}</span>
<span class="hint">{{ option.description | translate | replaceNbsp }}</span>
}
</div>
}

View File

@ -43,10 +43,44 @@ label {
}
}
.extra-option-description {
.additional-check-description {
margin-left: 23px;
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 {

View File

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

View File

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

View File

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

View File

@ -1,19 +1,47 @@
import { ChangeDetectorRef, Directive, inject } from '@angular/core';
import { ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms';
import { ChangeDetectorRef, Directive, inject, Injector, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormControl,
FormControlDirective,
FormControlName,
FormGroupDirective,
NgControl,
ValidationErrors,
Validator,
} from '@angular/forms';
@Directive()
export abstract class FormFieldComponent<I> implements ControlValueAccessor, Validator {
export abstract class FormFieldComponent<I> implements ControlValueAccessor, Validator, OnInit {
touched = false;
disabled = false;
protected readonly _changeRef = inject(ChangeDetectorRef);
protected readonly _injector = inject(Injector);
protected _formControl: FormControl | undefined;
protected _value: I | undefined;
get value(): I | undefined {
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
onChange = (value?: I) => {};

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import { BaseHeaderConfig } from './base-config.model';
import { Observable } from 'rxjs';
import { OverlappingElement } from '../../../help-mode';
export interface ActionConfig extends BaseHeaderConfig {
readonly action: ($event: MouseEvent) => void;
readonly helpModeKey?: string;
readonly disabled$?: Observable<boolean>;
readonly disabled?: boolean;
readonly disableStopPropagation?: boolean;
}

View File

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

View File

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

View File

@ -15,13 +15,14 @@ import { filterEach } from '../../utils';
import { List } from '../../utils';
import { IListable } from '../models';
import { ActionConfig, ButtonConfig, SearchPosition, SearchPositions } from './models';
import { MatTooltip } from '@angular/material/tooltip';
import { DisableStopPropagationDirective } from '../../directives';
@Component({
selector: 'iqser-page-header',
templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
AsyncPipe,
NgTemplateOutlet,
@ -31,6 +32,8 @@ import { ActionConfig, ButtonConfig, SearchPosition, SearchPositions } from './m
CircleButtonComponent,
TranslateModule,
InputWithActionComponent,
MatTooltip,
DisableStopPropagationDirective,
],
})
export class PageHeaderComponent<T extends IListable> {

View File

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

View File

@ -1,13 +0,0 @@
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ActiveListingEntityService {
readonly #activeEntity = signal<string>('');
readonly activeEntity = this.#activeEntity.asReadonly();
updateEntity(entity: string = '') {
this.#activeEntity.set(entity);
}
}

View File

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

View File

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

View File

@ -11,35 +11,33 @@
<!-- 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 -->
@if (itemMouseEnterFn || itemMouseLeaveFn) {
<div
<a
(mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)"
(mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)"
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
[class.active-entity]="activeListingEntityService.activeEntity() === entity.id"
[id]="rowIdPrefix + '-' + ((entity[namePropertyKey] | snakeCase) ?? entity.id)"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink"
>
<iqser-table-item
(click)="multiSelect(entity, $event)"
[entity]="entity"
[entity]="$any(entity)"
[selectionEnabled]="selectionEnabled"
></iqser-table-item>
</div>
</a>
} @else {
<div
<a
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
[class.active-entity]="activeListingEntityService.activeEntity() === entity.id"
[id]="rowIdPrefix + '-' + ((entity[namePropertyKey] | snakeCase) ?? entity.id)"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink"
>
<iqser-table-item
(click)="multiSelect(entity, $event)"
[entity]="entity"
[entity]="$any(entity)"
[selectionEnabled]="selectionEnabled"
></iqser-table-item>
</div>
</a>
}
</ng-container>
</cdk-virtual-scroll-viewport>

View File

@ -2,7 +2,7 @@
:host cdk-virtual-scroll-viewport {
height: calc(100vh - 50px - 31px - var(--iqser-top-bar-height) - 50px);
overflow-y: hidden !important;
overflow-y: auto !important;
background-color: var(--iqser-background);
@include mixins.scroll-bar;
@ -33,7 +33,7 @@
}
&:hover,
&.active-entity,
&:has(iqser-circle-button[aria-expanded='true']),
&.help-mode-active {
.selection-column iqser-round-checkbox .wrapper {
opacity: 1;
@ -72,3 +72,8 @@
.display-contents {
display: contents;
}
a {
display: contents;
@include mixins.clear-a;
}

View File

@ -15,13 +15,11 @@ import { IListable } from '../models/listable';
import { Id } from '../models/trackable';
import { ListingService } from '../services/listing.service';
import { TableItemComponent } from './table-item/table-item.component';
import { ActiveListingEntityService } from '../services/active-listing-entity.service';
@Component({
selector: 'iqser-table-content',
templateUrl: './table-content.component.html',
styleUrls: ['./table-content.component.scss'],
standalone: true,
imports: [
CdkVirtualScrollViewport,
AsyncPipe,
@ -54,7 +52,6 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
constructor(
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<Class>,
readonly listingService: ListingService<Class>,
readonly activeListingEntityService: ActiveListingEntityService,
@Optional() readonly helpModeService: HelpModeService,
) {
super();
@ -89,7 +86,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
getTableItemClasses(entity: Class): Record<string, boolean> {
const classes: Record<string, boolean> = {
'table-item': true,
pointer: !!entity.routerLink && entity.routerLink.length > 0,
'cursor-default': !entity.routerLink,
};
for (const key in this.tableItemClasses) {
if (Object.prototype.hasOwnProperty.call(this.tableItemClasses, key)) {

View File

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

View File

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

View File

@ -28,7 +28,6 @@ const SCROLLBAR_WIDTH = 11;
selector: 'iqser-table [tableColumnConfigs] [itemSize]',
templateUrl: './table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TableHeaderComponent, NgTemplateOutlet, AsyncPipe, EmptyStateComponent, ScrollButtonComponent, TableContentComponent],
})
export class TableComponent<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> implements OnChanges {
@ -87,18 +86,17 @@ export class TableComponent<Class extends IListable<PrimaryKey>, PrimaryKey exte
}
private _setColumnsWidth(element: HTMLElement) {
let gridTemplateColumnsHover = '';
let gridTemplateColumns = '';
if (this.selectionEnabled) {
gridTemplateColumnsHover += '30px ';
gridTemplateColumns += '30px ';
}
for (const config of this.tableColumnConfigs) {
gridTemplateColumnsHover += `${config.width || '1fr'} `;
gridTemplateColumns += `${config.width || '1fr'} `;
}
gridTemplateColumnsHover += this.emptyColumnWidth || '';
const gridTemplateColumns = `${gridTemplateColumnsHover} ${SCROLLBAR_WIDTH}px`;
gridTemplateColumns += this.emptyColumnWidth || '';
gridTemplateColumns = `${gridTemplateColumns} ${SCROLLBAR_WIDTH}px`;
element.style.setProperty('--gridTemplateColumns', gridTemplateColumns);
element.style.setProperty('--gridTemplateColumnsHover', gridTemplateColumnsHover);
}
private _setItemSize(element: HTMLElement) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,6 @@ import { assertTemplate, IqserPermissionsDirective } from './permissions.directi
@Directive({
selector: '[deny]',
standalone: true,
})
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 {
return isString(value) ? [value] : value ?? [];
return isString(value) ? [value] : (value ?? []);
}
export function isRedirectWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,3 +86,23 @@ export function orderedAsyncGuards(guards: Array<AsyncGuard>): CanActivateFn {
);
};
}
export function unorderedAsyncGuards(guards: Array<AsyncGuard>): CanActivateFn {
return async (route, state) => {
const injector = inject(Injector);
const loadingService = inject(LoadingService);
loadingService.start();
try {
const result = await Promise.all(guards.map(guard => runInInjectionContext(injector, () => guard(route, state))));
loadingService.stop();
return result.every(Boolean);
} catch (error) {
console.error(error);
loadingService.stop();
}
return false;
};
}

View File

@ -6,7 +6,6 @@ import { HeadersConfiguration } from '../utils/headers-configuration';
import { List } from '../utils/types/iqser-types';
export const ROOT_CHANGES_KEY = 'root';
export const LAST_CHECKED_OFFSET = 30000;
export interface QueryParam {
readonly key: string;
@ -19,9 +18,7 @@ export interface QueryParam {
*/
export abstract class GenericService<I> {
protected readonly _http = inject(HttpClient);
protected readonly _lastCheckedForChanges = new Map<string, string>([
[ROOT_CHANGES_KEY, new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString()],
]);
protected readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, new Date(Date.now()).toISOString()]]);
protected abstract readonly _defaultModelPath: string;
protected readonly _serviceName: string = 'redaction-gateway-v1';
@ -126,6 +123,6 @@ export abstract class GenericService<I> {
}
protected _updateLastChanged(key = ROOT_CHANGES_KEY): void {
this._lastCheckedForChanges.set(key, new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString());
this._lastCheckedForChanges.set(key, new Date().toISOString());
}
}

View File

@ -3,14 +3,18 @@ import { Title } from '@angular/platform-browser';
import { CacheApiService } from '../caching/cache-api.service';
import { wipeAllCaches } from '../caching/cache-utils';
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()
export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
protected readonly _cacheApiService = inject(CacheApiService);
protected readonly _titleService = inject(Title);
protected readonly _tenantsService = inject(TenantsService);
constructor(@Inject('Doesnt matter') protected _values: T) {
this._checkFrontendVersion();
this.#updateAppType();
this._titleService.setTitle(this._values.APP_NAME);
}
@ -23,6 +27,16 @@ export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
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 {
this._cacheApiService.getCachedValue('FRONTEND_APP_VERSION').then(async lastVersion => {
const version = this._values.FRONTEND_APP_VERSION;
@ -34,6 +48,28 @@ export class IqserConfigService<T extends IqserAppConfig = IqserAppConfig> {
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>() {

View File

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

View File

@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable, switchMap } from 'rxjs';
import { tap } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { HeadersConfiguration } from '../utils/headers-configuration';
import { mapEach } from '../utils/operators';
@ -26,6 +26,14 @@ 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 {
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';
@Component({
selector: 'iqser-logo',
template: ` <mat-icon [svgIcon]="icon"></mat-icon>`,
template: `<mat-icon [svgIcon]="icon()"></mat-icon>`,
styles: [
`
:host {
@ -17,9 +17,8 @@ import { MatIconModule } from '@angular/material/icon';
`,
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatIconModule],
})
export class LogoComponent {
@Input({ required: true }) icon!: string;
readonly icon = input.required<string>();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import { inject, Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakEventType, KeycloakService } from 'keycloak-angular';
import { NGXLogger } from 'ngx-logger';
import { filter, switchMap } from 'rxjs/operators';
import { getConfig } from '../../services/iqser-config.service';
import { log, shareLast } from '../../utils';
import { UI_ROOT } from '../../utils/tokens';
import { getKeycloakOptions } from '../keycloak-options';
import { TenantsService } from './tenants.service';
@ -13,6 +15,12 @@ export class KeycloakStatusService {
readonly #tenantsService = inject(TenantsService);
readonly #uiRoot = inject(UI_ROOT);
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) {
const keycloakInstance = this.#keycloakService?.getKeycloakInstance();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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