Compare commits
29 Commits
release-4.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f13fa62d3 | ||
|
|
4ed7215292 | ||
|
|
1ea220b489 | ||
|
|
fc8be33dc6 | ||
|
|
58382ddfee | ||
|
|
0d20afcf27 | ||
|
|
aa5fc54576 | ||
|
|
e5c4fd7d3c | ||
|
|
a4e3ed8854 | ||
|
|
acabdba657 | ||
|
|
0e6e4f7b09 | ||
|
|
7bb24c4f91 | ||
|
|
e21c225ddd | ||
|
|
8582f2e6be | ||
|
|
b929f1d136 | ||
|
|
55b21c4989 | ||
|
|
911516add2 | ||
|
|
cbdfcf4d8f | ||
|
|
72e760fff8 | ||
|
|
ae0eebcc6f | ||
|
|
7c6f9fc25e | ||
|
|
310cc4bb51 | ||
|
|
99facc0434 | ||
|
|
ebaf1709b1 | ||
|
|
17d2e8c530 | ||
|
|
b9c01a287c | ||
|
|
6c7865a8ec | ||
|
|
9e457a13b4 | ||
|
|
fba9b330dd |
@ -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
19
.gitlab-ci.yml
Normal 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
2
sonar-project.properties
Normal file
@ -0,0 +1,2 @@
|
||||
sonar.projectKey=common-ui
|
||||
sonar.qualitygate.wait=false
|
||||
@ -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);
|
||||
|
||||
@ -156,9 +156,11 @@ 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 {
|
||||
@ -254,6 +256,10 @@ section.settings {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.fit-content {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NgClass, NgTemplateOutlet } from '@angular/common';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@ -87,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,
|
||||
@ -97,7 +96,6 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
|
||||
IconButtonComponent,
|
||||
CircleButtonComponent,
|
||||
MatDialogModule,
|
||||
NgClass,
|
||||
],
|
||||
})
|
||||
export class ConfirmationDialogComponent implements AfterViewInit {
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -2,7 +2,6 @@ import { Directive, ElementRef, OnDestroy, OnInit, signal } from '@angular/core'
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserHasScrollbar]',
|
||||
standalone: true,
|
||||
host: {
|
||||
'[class]': '_class()',
|
||||
},
|
||||
|
||||
@ -2,7 +2,6 @@ import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/c
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserHiddenAction]',
|
||||
standalone: true,
|
||||
})
|
||||
export class HiddenActionDirective {
|
||||
@Input() requiredClicks = 4;
|
||||
|
||||
@ -3,7 +3,6 @@ import { NGXLogger } from 'ngx-logger';
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserPreventDefault]',
|
||||
standalone: true,
|
||||
})
|
||||
export class PreventDefaultDirective {
|
||||
readonly #logger = inject(NGXLogger);
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -17,6 +17,7 @@ import { ErrorService } from '../error.service';
|
||||
]),
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ConnectionStatusComponent {
|
||||
protected readonly connectionStatusTranslations = connectionStatusTranslations;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -37,7 +37,6 @@ const atLeastOneIsExpandable = pipe(
|
||||
},
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, InputWithActionComponent, NgTemplateOutlet, TranslateModule, MatIcon, MatCheckbox, StopPropagationDirective],
|
||||
})
|
||||
export class FilterCardComponent implements OnInit {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -29,7 +29,6 @@ import { IFilterGroup } from '../models/filter-group.model';
|
||||
StopPropagationDirective,
|
||||
MatMenuContent,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class PopupFilterComponent implements OnInit {
|
||||
@Input() primaryFiltersSlug!: string;
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -19,7 +19,7 @@ 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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -3,4 +3,5 @@ import { ActionConfig } from './action-config.model';
|
||||
|
||||
export interface ButtonConfig extends ActionConfig {
|
||||
readonly type?: IconButtonType;
|
||||
readonly tooltip?: string;
|
||||
}
|
||||
|
||||
@ -50,7 +50,10 @@
|
||||
[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>
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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[] {
|
||||
|
||||
@ -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']> {
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
>
|
||||
<iqser-table-item
|
||||
(click)="multiSelect(entity, $event)"
|
||||
[entity]="entity"
|
||||
[entity]="$any(entity)"
|
||||
[selectionEnabled]="selectionEnabled"
|
||||
></iqser-table-item>
|
||||
</a>
|
||||
@ -34,7 +34,7 @@
|
||||
>
|
||||
<iqser-table-item
|
||||
(click)="multiSelect(entity, $event)"
|
||||
[entity]="entity"
|
||||
[entity]="$any(entity)"
|
||||
[selectionEnabled]="selectionEnabled"
|
||||
></iqser-table-item>
|
||||
</a>
|
||||
|
||||
@ -75,6 +75,5 @@
|
||||
|
||||
a {
|
||||
display: contents;
|
||||
cursor: default;
|
||||
@include mixins.clear-a;
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ import { TableItemComponent } from './table-item/table-item.component';
|
||||
selector: 'iqser-table-content',
|
||||
templateUrl: './table-content.component.html',
|
||||
styleUrls: ['./table-content.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CdkVirtualScrollViewport,
|
||||
AsyncPipe,
|
||||
@ -87,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)) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -50,7 +50,6 @@ interface WorkflowContext<T> {
|
||||
templateUrl: './workflow.component.html',
|
||||
styleUrls: ['./workflow.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
TableHeaderComponent,
|
||||
AsyncPipe,
|
||||
|
||||
@ -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) {}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -6,7 +6,6 @@ import { assertTemplate, IqserPermissionsDirective } from './permissions.directi
|
||||
|
||||
@Directive({
|
||||
selector: '[allow]',
|
||||
standalone: true,
|
||||
})
|
||||
export class IqserAllowDirective extends IqserPermissionsDirective implements OnDestroy, OnInit {
|
||||
/**
|
||||
|
||||
@ -6,7 +6,6 @@ import { assertTemplate, IqserPermissionsDirective } from './permissions.directi
|
||||
|
||||
@Directive({
|
||||
selector: '[deny]',
|
||||
standalone: true,
|
||||
})
|
||||
export class IqserDenyDirective extends IqserPermissionsDirective implements OnDestroy, OnInit {
|
||||
/**
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -3,7 +3,6 @@ import { capitalize } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'capitalize',
|
||||
standalone: true,
|
||||
})
|
||||
export class CapitalizePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
|
||||
@ -3,7 +3,6 @@ import { humanizeCamelCase } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'humanizeCamelCase',
|
||||
standalone: true,
|
||||
})
|
||||
export class HumanizeCamelCasePipe implements PipeTransform {
|
||||
transform(item: string): string {
|
||||
|
||||
@ -3,7 +3,6 @@ import { humanize } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'humanize',
|
||||
standalone: true,
|
||||
})
|
||||
export class HumanizePipe implements PipeTransform {
|
||||
transform(item: string, lowercase = false): string {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -2,7 +2,6 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'replaceNbsp',
|
||||
standalone: true,
|
||||
})
|
||||
export class ReplaceNbspPipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
|
||||
@ -3,7 +3,6 @@ import { size } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'size',
|
||||
standalone: true,
|
||||
})
|
||||
export class SizePipe implements PipeTransform {
|
||||
transform(value: number): string {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>() {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -13,7 +13,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class SmallChipComponent {
|
||||
@Input() color!: string;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,21 +1,42 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { TranslateLoader } from '@ngx-translate/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
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 {
|
||||
[key: string]: string | T;
|
||||
}
|
||||
|
||||
export class PruningTranslationLoader implements TranslateLoader {
|
||||
readonly tenant = inject(GET_TENANT_FROM_PATH_FN)();
|
||||
constructor(
|
||||
private readonly _http: HttpClient,
|
||||
private readonly _tenantService: TenantsService,
|
||||
private readonly _prefix: string,
|
||||
private readonly _suffix: string,
|
||||
) {}
|
||||
|
||||
getTranslation(lang: string): Observable<T> {
|
||||
return this._http.get(`${this._prefix}${lang}${this._suffix}`).pipe(map(result => this._process(result as T)));
|
||||
if (this.tenant) {
|
||||
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 {
|
||||
|
||||
@ -2,7 +2,6 @@ import { Directive, EventEmitter, HostBinding, HostListener, Output } from '@ang
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserDragDropFileUpload]',
|
||||
standalone: true,
|
||||
})
|
||||
export class DragDropFileUploadDirective {
|
||||
@Output() readonly fileDropped = new EventEmitter();
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { DragDropFileUploadDirective } from './drag-drop-file-upload.directive';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-upload-file',
|
||||
templateUrl: './upload-file.component.html',
|
||||
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
|
||||
host: { '[class.iqser-upload-file]': 'true' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [DragDropFileUploadDirective, MatIcon, NgIf, TranslateModule],
|
||||
imports: [DragDropFileUploadDirective, MatIcon, TranslateModule],
|
||||
})
|
||||
export class UploadFileComponent {
|
||||
@ViewChild('attachFileInput', { static: true }) attachFileInput!: ElementRef;
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
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 { IqserUser } from '../../iqser-user.model';
|
||||
import { NamePipe } from '../../name.pipe';
|
||||
import { IqserUserService } from '../../services/iqser-user.service';
|
||||
import { NamePipeOptions } from '../../types/name-pipe-options';
|
||||
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({
|
||||
selector: 'iqser-initials-avatar',
|
||||
templateUrl: './initials-avatar.component.html',
|
||||
styleUrls: ['./initials-avatar.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgIf, NamePipe, MatTooltip],
|
||||
imports: [NamePipe, MatTooltip],
|
||||
})
|
||||
export class InitialsAvatarComponent<
|
||||
Interface extends IIqserUser = IIqserUser,
|
||||
@ -23,11 +21,9 @@ export class InitialsAvatarComponent<
|
||||
> {
|
||||
readonly #userService = inject<IqserUserService<Interface, Class>>(IqserUserService);
|
||||
readonly #translateService = inject(TranslateService);
|
||||
readonly #isSystemUser = computed(() => this._user()?.id?.toLowerCase() === 'system');
|
||||
readonly #users = toSignal(this.#userService.all$);
|
||||
|
||||
readonly color = input('lightgray');
|
||||
readonly size = input<'small' | 'large'>('small');
|
||||
readonly size = input<'small' | 'large' | 'extra-small'>('small');
|
||||
readonly withName = input(false);
|
||||
readonly showYou = input(false);
|
||||
readonly tooltipPosition = input<'below' | 'above'>('above');
|
||||
@ -35,7 +31,6 @@ export class InitialsAvatarComponent<
|
||||
readonly showTooltip = input(true);
|
||||
readonly user = input.required<Class | string>();
|
||||
readonly showBorderCondition = input<<T extends Class = Class>(user: T) => boolean>(user => user.isSpecial);
|
||||
|
||||
readonly _user = computed(() => {
|
||||
const user = this.user();
|
||||
if (typeof user === 'string') {
|
||||
@ -45,6 +40,7 @@ export class InitialsAvatarComponent<
|
||||
}
|
||||
return user;
|
||||
});
|
||||
readonly #isSystemUser = computed(() => this._user()?.id?.toLowerCase() === 'system');
|
||||
readonly isCurrentUser = computed(() => this.#userService.currentUser?.id === this._user()?.id);
|
||||
readonly hasBorder = computed(() => !!this._user() && !this.isCurrentUser() && this.showBorderCondition()(this._user()!));
|
||||
readonly disabled = computed(() => !!this._user() && !this.#isSystemUser() && !this._user()?.hasAnyRole);
|
||||
|
||||
@ -6,6 +6,7 @@ import { IqserUserService } from '../../services/iqser-user.service';
|
||||
templateUrl: './user-button.component.html',
|
||||
styleUrls: ['./user-button.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class UserButtonComponent {
|
||||
@Input() showDot = false;
|
||||
|
||||
@ -19,7 +19,6 @@ function getInitials(name: string) {
|
||||
|
||||
@Pipe({
|
||||
name: 'name',
|
||||
standalone: true,
|
||||
})
|
||||
export class NamePipe implements PipeTransform {
|
||||
readonly #translateService = inject(TranslateService);
|
||||
|
||||
@ -23,11 +23,11 @@ export abstract class IqserUserService<
|
||||
Interface extends IIqserUser = IIqserUser,
|
||||
Class extends IqserUser & Interface = IqserUser & Interface,
|
||||
> extends EntitiesService<Interface, Class> {
|
||||
readonly currentUser$: Observable<Class | undefined>;
|
||||
protected abstract readonly _defaultModelPath: string;
|
||||
readonly #uiRoot = inject(UI_ROOT);
|
||||
protected abstract override readonly _defaultModelPath: string;
|
||||
protected abstract readonly _permissionsFilter: (role: string) => boolean;
|
||||
protected abstract readonly _rolesFilter: (role: string) => boolean;
|
||||
protected abstract readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class;
|
||||
protected abstract override readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class;
|
||||
protected readonly _currentUser$ = new BehaviorSubject<Class | undefined>(undefined);
|
||||
protected readonly _toaster = inject(Toaster);
|
||||
protected readonly _keycloakService = inject(KeycloakService);
|
||||
@ -35,8 +35,8 @@ export abstract class IqserUserService<
|
||||
protected readonly _keycloakStatusService = inject(KeycloakStatusService);
|
||||
protected readonly _permissionsService = inject(IqserPermissionsService, { optional: true });
|
||||
protected readonly _rolesService = inject(IqserRolesService, { optional: true });
|
||||
protected readonly _serviceName: string = 'tenant-user-management';
|
||||
readonly #uiRoot = inject(UI_ROOT);
|
||||
protected override readonly _serviceName: string = 'tenant-user-management';
|
||||
readonly currentUser$: Observable<Class | undefined>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -80,7 +80,7 @@ export abstract class IqserUserService<
|
||||
await this._keycloakService.login({ action: 'UPDATE_PASSWORD' });
|
||||
}
|
||||
|
||||
loadAll() {
|
||||
override loadAll() {
|
||||
return this.getAll().pipe(
|
||||
mapEach(user => new this._entityClass(user, user.roles, user.userId)),
|
||||
tap(users => this.setEntities(users)),
|
||||
@ -121,7 +121,7 @@ export abstract class IqserUserService<
|
||||
return this.find(userId)?.name;
|
||||
}
|
||||
|
||||
getAll(url = this._defaultModelPath): Observable<Interface[]> {
|
||||
override getAll(url = this._defaultModelPath): Observable<Interface[]> {
|
||||
return super.getAll(url, [{ key: 'refreshCache', value: true }]);
|
||||
}
|
||||
|
||||
@ -157,12 +157,12 @@ export abstract class IqserUserService<
|
||||
return this._post(body);
|
||||
}
|
||||
|
||||
delete(userIds: List) {
|
||||
override delete(userIds: List) {
|
||||
const queryParams = userIds.map<QueryParam>(userId => ({ key: 'userId', value: userId }));
|
||||
return super.delete(userIds, this._defaultModelPath, queryParams);
|
||||
}
|
||||
|
||||
find(id: string): Class | undefined {
|
||||
override find(id: string): Class | undefined {
|
||||
if (id?.toLowerCase() === 'system') {
|
||||
return this.newSystemUser();
|
||||
}
|
||||
|
||||
@ -51,3 +51,31 @@ export const ICONS = new Set([
|
||||
'visibility',
|
||||
'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',
|
||||
};
|
||||
|
||||
@ -8,4 +8,5 @@ export interface IqserAppConfig {
|
||||
readonly OAUTH_URL: string;
|
||||
readonly MANUAL_BASE_URL: string;
|
||||
readonly BASE_TRANSLATIONS_DIRECTORY?: string;
|
||||
readonly LANDING_PAGE_THEME: 'redactmanager' | 'documine' | 'mixed';
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user