Compare commits
No commits in common. "master" and "RED-9321" have entirely different histories.
@ -206,7 +206,6 @@ module.exports = {
|
||||
],
|
||||
rules: {
|
||||
'rxjs/no-ignored-subscription': 'error',
|
||||
'@angular-eslint/prefer-standalone': 'off',
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
sonarqube:
|
||||
stage: test
|
||||
image:
|
||||
name: sonarsource/sonar-scanner-cli:11.1
|
||||
entrypoint:
|
||||
- ''
|
||||
variables:
|
||||
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
|
||||
GIT_DEPTH: '0'
|
||||
cache:
|
||||
key: "${CI_JOB_NAME}"
|
||||
paths:
|
||||
- ".sonar/cache"
|
||||
script:
|
||||
- sonar-scanner
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
|
||||
- if: "$CI_COMMIT_BRANCH =~ /^release/"
|
||||
@ -1,2 +0,0 @@
|
||||
sonar.projectKey=common-ui
|
||||
sonar.qualitygate.wait=false
|
||||
@ -1,3 +0,0 @@
|
||||
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 7.4L0 1.4L1.4 0L6 4.6L10.6 0L12 1.4L6 7.4Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 184 B |
@ -1,3 +0,0 @@
|
||||
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 2.8L1.4 7.4L0 6L6 0L12 6L10.6 7.4L6 2.8Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 182 B |
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd" id="minimize" stroke="none" stroke-width="1">
|
||||
<path
|
||||
d="M40,60 L40,90 L30,90 L30,77 L7,100 L0,93 L23,70 L10,70 L10,60 L40,60 Z M90,60 L90,70 L77,70 L100,93 L93,100 L70,77 L70,90 L60,90 L60,60 L90,60 Z M93,0 L100,7 L77,30 L90,30 L90,40 L60,40 L60,10 L70,10 L70,23 L93,0 Z M7,0 L30,23 L30,10 L40,10 L40,40 L10,40 L10,30 L23,30 L0,7 L7,0 Z"
|
||||
fill="currentColor" fill-rule="nonzero" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 642 B |
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd" id="full-screen" stroke="none" stroke-width="1">
|
||||
<path
|
||||
d="M36.5,56.5 L43.5,63.5 L17,90 L30,90 L30,100 L0,100 L0,70 L10,70 L10,83 L36.5,56.5 Z M63.5,56.5 L90,83 L90,70 L100,70 L100,100 L70,100 L70,90 L83,90 L56.5,63.5 L63.5,56.5 Z M100,0 L100,30 L90,30 L90,17 L63.5,43.5 L56.5,36.5 L83,10 L70,10 L70,0 L100,0 Z M30,0 L30,10 L17,10 L43.5,36.5 L36.5,43.5 L10,17 L10,30 L0,30 L0,0 L30,0 Z"
|
||||
fill="currentColor" fill-rule="nonzero" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 691 B |
@ -5,7 +5,6 @@ $ripple-size: 26px;
|
||||
flex: 0 0 $checkbox-size;
|
||||
width: $checkbox-size;
|
||||
height: $checkbox-size;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mat-mdc-checkbox,
|
||||
@ -25,14 +24,10 @@ $ripple-size: 26px;
|
||||
--mdc-checkbox-selected-hover-state-layer-color: var(--iqser-primary);
|
||||
--mdc-checkbox-selected-pressed-state-layer-color: var(--iqser-primary);
|
||||
|
||||
.mdc-form-field {
|
||||
align-items: start;
|
||||
|
||||
& > label {
|
||||
padding-left: 8px;
|
||||
line-height: 24px;
|
||||
white-space: normal;
|
||||
}
|
||||
.mdc-form-field > label {
|
||||
padding-left: 8px;
|
||||
line-height: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mdc-checkbox__ripple {
|
||||
|
||||
@ -22,14 +22,6 @@
|
||||
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);
|
||||
|
||||
@ -47,10 +47,7 @@
|
||||
font-weight: bold;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.redaction,
|
||||
&.force-annotation {
|
||||
iqser-details-radio {
|
||||
padding-top: 20px;
|
||||
}
|
||||
@ -75,7 +72,3 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.large-form-dialog .dialog > form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--iqser-alt-background);
|
||||
height: 68px;
|
||||
|
||||
&.drag-over {
|
||||
background-color: var(--iqser-file-drop-drag-over);
|
||||
@ -16,6 +15,7 @@
|
||||
|
||||
.upload-area {
|
||||
gap: 16px;
|
||||
height: 88px;
|
||||
cursor: pointer;
|
||||
padding: 0 32px;
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
|
||||
.file-area {
|
||||
gap: 10px;
|
||||
height: 48px;
|
||||
|
||||
mat-icon:first-child {
|
||||
opacity: 0.5;
|
||||
|
||||
@ -156,15 +156,13 @@ section.settings {
|
||||
box-sizing: border-box;
|
||||
background: var(--iqser-background);
|
||||
overflow: hidden;
|
||||
&.with-transition {
|
||||
transition:
|
||||
width ease-in-out 0.2s,
|
||||
min-width ease-in-out 0.2s;
|
||||
}
|
||||
@include common-mixins.scroll-bar;
|
||||
transition:
|
||||
width ease-in-out 0.2s,
|
||||
min-width ease-in-out 0.2s;
|
||||
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
@include common-mixins.scroll-bar;
|
||||
}
|
||||
|
||||
.collapsed-wrapper {
|
||||
@ -256,10 +254,6 @@ section.settings {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.fit-content {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
.mat-mdc-menu-item {
|
||||
font-size: var(--iqser-font-size);
|
||||
color: var(--iqser-text);
|
||||
padding: 0 26px 0 8px !important;
|
||||
padding: 0 26px 0 8px;
|
||||
margin: var(--iqser-menu-item-margin);
|
||||
border-radius: 4px;
|
||||
width: -webkit-fill-available;
|
||||
@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
&.padding-left {
|
||||
padding-left: 56px !important;
|
||||
padding-left: 56px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
@use 'sass:string';
|
||||
@use 'sass:list';
|
||||
/* Margins, paddings */
|
||||
|
||||
$start: 0;
|
||||
@ -9,19 +7,19 @@ $values: '';
|
||||
$sides: (top, bottom, left, right);
|
||||
|
||||
@for $i from $start + 1 through $end {
|
||||
$values: list.append($values, $i, comma);
|
||||
$values: list.set-nth($values, 1, $start);
|
||||
$values: append($values, $i, comma);
|
||||
$values: set-nth($values, 1, $start);
|
||||
}
|
||||
|
||||
// TODO: Check if !important can be avoided
|
||||
|
||||
@each $space in $values {
|
||||
@each $side in $sides {
|
||||
.m#{string.slice($side, 0, 1)}-#{$space} {
|
||||
.m#{str-slice($side, 0, 1)}-#{$space} {
|
||||
margin-#{$side}: #{$space}px !important;
|
||||
}
|
||||
|
||||
.p#{string.slice($side, 0, 1)}-#{$space} {
|
||||
.p#{str-slice($side, 0, 1)}-#{$space} {
|
||||
padding-#{$side}: #{$space}px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,13 @@ export * from './lib/dialog';
|
||||
export * from './lib/form';
|
||||
export * from './lib/listing';
|
||||
export * from './lib/help-mode';
|
||||
export * from './lib/inputs';
|
||||
export * from './lib/services';
|
||||
export * from './lib/loading';
|
||||
export * from './lib/error';
|
||||
export * from './lib/search';
|
||||
export * from './lib/upload-file';
|
||||
export * from './lib/empty-state';
|
||||
export * from './lib/caching';
|
||||
export * from './lib/translations';
|
||||
export * from './lib/pipes';
|
||||
|
||||
@ -8,6 +8,7 @@ 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 {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from './chevron-button.component';
|
||||
@ -24,6 +24,7 @@ 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 {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<button
|
||||
(click)="!disabled() && emitAction($event)"
|
||||
(click)="!disabled() && action.emit($event)"
|
||||
[disabled]="disabled()"
|
||||
[id]="buttonId()"
|
||||
[iqserStopPropagation]="action.observed && !_hasRouterLink"
|
||||
|
||||
@ -11,6 +11,7 @@ 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 {
|
||||
@ -32,11 +33,4 @@ export class IconButtonComponent {
|
||||
};
|
||||
});
|
||||
@Output() readonly action = new EventEmitter<MouseEvent>();
|
||||
|
||||
emitAction($event: MouseEvent) {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement.tagName?.toLowerCase() === 'button') {
|
||||
this.action.emit($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,3 +3,4 @@ export * from './types/circle-button.type';
|
||||
|
||||
export * from './icon-button/icon-button.component';
|
||||
export * from './circle-button/circle-button.component';
|
||||
export * from './chevron-button/chevron-button.component';
|
||||
|
||||
@ -11,10 +11,6 @@
|
||||
}
|
||||
|
||||
<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>
|
||||
@ -39,20 +35,14 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
}
|
||||
<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>
|
||||
|
||||
@if (config.alternativeConfirmationText) {
|
||||
<iqser-icon-button
|
||||
@ -70,13 +60,9 @@
|
||||
}
|
||||
|
||||
@if (!config.discardChangesText) {
|
||||
@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 (click)="deny()" class="all-caps-label cancel">
|
||||
{{ config.denyText }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@ -6,12 +6,3 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.no-uppercase {
|
||||
text-transform: unset;
|
||||
}
|
||||
|
||||
@ -1,24 +1,15 @@
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostListener,
|
||||
inject,
|
||||
TemplateRef,
|
||||
Type,
|
||||
viewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, HostListener, inject, TemplateRef } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { CircleButtonComponent, IconButtonTypes } from '../../buttons';
|
||||
import { IconButtonComponent } from '../../buttons';
|
||||
import { ValuesOf } from '../../utils';
|
||||
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';
|
||||
|
||||
export const TitleColors = {
|
||||
DEFAULT: 'default',
|
||||
@ -57,9 +48,6 @@ 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>;
|
||||
@ -77,9 +65,6 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -87,6 +72,7 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
|
||||
templateUrl: './confirmation-dialog.component.html',
|
||||
styleUrls: ['./confirmation-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatIconModule,
|
||||
FormsModule,
|
||||
@ -98,14 +84,13 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
|
||||
MatDialogModule,
|
||||
],
|
||||
})
|
||||
export class ConfirmationDialogComponent implements AfterViewInit {
|
||||
export class ConfirmationDialogComponent {
|
||||
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>,
|
||||
@ -130,19 +115,13 @@ export class ConfirmationDialogComponent implements AfterViewInit {
|
||||
return 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();
|
||||
@HostListener('window:keyup.enter')
|
||||
onKeyupEnter(): void {
|
||||
if (this.config.requireInput && !this.confirmationDoesNotMatch()) {
|
||||
this.confirm(ConfirmOptions.CONFIRM);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.#initializeDetailsComponent();
|
||||
}
|
||||
|
||||
confirmationDoesNotMatch(): boolean {
|
||||
return this.inputValue.toLowerCase() !== this.config.confirmationText.toLowerCase();
|
||||
}
|
||||
@ -177,13 +156,9 @@ export class ConfirmationDialogComponent implements AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@HostListener('window:keydown.Enter', ['$event'])
|
||||
onEnter(event: KeyboardEvent): void {
|
||||
event?.stopImmediatePropagation();
|
||||
this.confirm(ConfirmOptions.CONFIRM);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,9 @@ import { Directive, HostListener, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { IconButtonTypes } from '../buttons';
|
||||
import { hasFormChanged, IqserEventTarget } from '../utils';
|
||||
import { IconButtonTypes } from '../buttons/types/icon-button.type';
|
||||
import { hasFormChanged } from '../utils/functions';
|
||||
import { IqserEventTarget } from '../utils/types/events.type';
|
||||
|
||||
const DIALOG_CONTAINER = 'mat-dialog-container';
|
||||
const DATA_TYPE_SYMBOL = Symbol.for('DATA_TYPE');
|
||||
@ -13,7 +14,7 @@ export type DATA_TYPE = typeof DATA_TYPE_SYMBOL;
|
||||
export type RETURN_TYPE = typeof RETURN_TYPE_SYMBOL;
|
||||
|
||||
@Directive()
|
||||
export abstract class IqserDialogComponent<ComponentType, DataType = null, ReturnType = void> {
|
||||
export abstract class IqserDialogComponent<ComponentType, DataType, ReturnType> {
|
||||
readonly [DATA_TYPE_SYMBOL]!: DataType;
|
||||
readonly [RETURN_TYPE_SYMBOL]!: ReturnType;
|
||||
|
||||
@ -22,8 +23,6 @@ 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) {
|
||||
@ -39,7 +38,7 @@ export abstract class IqserDialogComponent<ComponentType, DataType = null, Retur
|
||||
}
|
||||
|
||||
get changed(): boolean {
|
||||
return !this.form || hasFormChanged(this.form, this.initialFormValue, this.ignoredKeys);
|
||||
return !this.form || hasFormChanged(this.form, this.initialFormValue);
|
||||
}
|
||||
|
||||
get disabled(): boolean {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { booleanAttribute, Directive, input } from '@angular/core';
|
||||
import { booleanAttribute, Directive, Input } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserDisableStopPropagation]',
|
||||
standalone: true,
|
||||
})
|
||||
export class DisableStopPropagationDirective {
|
||||
readonly iqserDisableStopPropagation = input(true, { transform: booleanAttribute });
|
||||
@Input({ transform: booleanAttribute }) iqserDisableStopPropagation = true;
|
||||
}
|
||||
|
||||
@ -1,35 +1,39 @@
|
||||
import { Directive, ElementRef, OnDestroy, OnInit, signal } from '@angular/core';
|
||||
import { ChangeDetectorRef, Directive, ElementRef, HostBinding, OnDestroy, OnInit } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserHasScrollbar]',
|
||||
host: {
|
||||
'[class]': '_class()',
|
||||
},
|
||||
standalone: true,
|
||||
})
|
||||
export class HasScrollbarDirective implements OnInit, OnDestroy {
|
||||
@HostBinding('class') class = '';
|
||||
private readonly _resizeObserver: ResizeObserver;
|
||||
protected readonly _class = signal('');
|
||||
|
||||
constructor(protected readonly _elementRef: ElementRef) {
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
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 => {
|
||||
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' : '';
|
||||
this._class.set(newClass);
|
||||
const newClass = this.hasScrollbar ? 'has-scrollbar' : '';
|
||||
if (this.class !== newClass) {
|
||||
this.class = newClass;
|
||||
this._changeDetector.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@ -2,6 +2,7 @@ import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/c
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserHiddenAction]',
|
||||
standalone: true,
|
||||
})
|
||||
export class HiddenActionDirective {
|
||||
@Input() requiredClicks = 4;
|
||||
|
||||
@ -3,6 +3,7 @@ import { NGXLogger } from 'ngx-logger';
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserPreventDefault]',
|
||||
standalone: true,
|
||||
})
|
||||
export class PreventDefaultDirective {
|
||||
readonly #logger = inject(NGXLogger);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { booleanAttribute, Directive, HostListener, inject, Input } from '@angular/core';
|
||||
import { NGXLogger } from 'ngx-logger';
|
||||
import { DisableStopPropagationDirective } from './disable-stop-propagation.directive';
|
||||
import { NGXLogger } from 'ngx-logger';
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserStopPropagation]',
|
||||
standalone: true,
|
||||
})
|
||||
export class StopPropagationDirective {
|
||||
readonly #disableStopPropagation = inject(DisableStopPropagationDirective, { optional: true });
|
||||
@ -12,7 +13,7 @@ export class StopPropagationDirective {
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick($event: Event) {
|
||||
if (this.#disableStopPropagation?.iqserDisableStopPropagation()) {
|
||||
if (this.#disableStopPropagation?.iqserDisableStopPropagation) {
|
||||
this.#logger.info('[CLICK] iqserStopPropagation is disabled by iqserDisableStopPropagation');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/
|
||||
|
||||
@Directive({
|
||||
selector: '[iqserSyncWidth]',
|
||||
standalone: true,
|
||||
})
|
||||
export class SyncWidthDirective implements OnDestroy {
|
||||
@Input() iqserSyncWidth!: string;
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
<div [ngStyle]="styles()" class="empty-state">
|
||||
@if (icon(); as icon) {
|
||||
<div
|
||||
[ngStyle]="{
|
||||
'padding-top': verticalPadding + 'px',
|
||||
'padding-left': horizontalPadding + 'px',
|
||||
'padding-right': horizontalPadding + 'px',
|
||||
}"
|
||||
class="empty-state"
|
||||
>
|
||||
@if (icon) {
|
||||
<mat-icon [svgIcon]="icon"></mat-icon>
|
||||
}
|
||||
|
||||
@ -7,15 +14,15 @@
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
<div [innerHTML]="text()" class="heading-l"></div>
|
||||
<div [innerHTML]="text" class="heading-l"></div>
|
||||
|
||||
@if (showButton() && this.action.observed) {
|
||||
@if (showButton) {
|
||||
<iqser-icon-button
|
||||
(action)="action.emit()"
|
||||
[buttonId]="buttonId()"
|
||||
[icon]="buttonIcon()"
|
||||
[attr.help-mode-key]="helpModeKey()"
|
||||
[label]="buttonLabel()"
|
||||
[buttonId]="buttonId"
|
||||
[icon]="buttonIcon"
|
||||
[attr.help-mode-key]="helpModeKey"
|
||||
[label]="buttonLabel"
|
||||
[type]="iconButtonTypes.primary"
|
||||
></iqser-icon-button>
|
||||
}
|
||||
|
||||
@ -1,42 +1,32 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { IconButtonComponent, IconButtonTypes } from '../buttons';
|
||||
import { randomString } from '../utils';
|
||||
import { NgStyle } from '@angular/common';
|
||||
import {
|
||||
booleanAttribute,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
EventEmitter,
|
||||
input,
|
||||
numberAttribute,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { IconButtonComponent } from '../buttons/icon-button/icon-button.component';
|
||||
import { IconButtonTypes } from '../buttons/types/icon-button.type';
|
||||
import { randomString } from '../utils/functions';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-empty-state',
|
||||
selector: 'iqser-empty-state [text]',
|
||||
templateUrl: './empty-state.component.html',
|
||||
styleUrls: ['./empty-state.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [NgStyle, MatIconModule, IconButtonComponent],
|
||||
})
|
||||
export class EmptyStateComponent {
|
||||
protected readonly iconButtonTypes = IconButtonTypes;
|
||||
export class EmptyStateComponent implements OnInit {
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
|
||||
readonly text = input.required<string>();
|
||||
readonly icon = input<string>();
|
||||
readonly showButton = input(true, { transform: booleanAttribute });
|
||||
readonly buttonIcon = input('iqser:plus');
|
||||
readonly buttonLabel = input<string>();
|
||||
readonly buttonId = input(`${randomString()}-icon-button`);
|
||||
readonly horizontalPadding = input(100, { transform: numberAttribute });
|
||||
readonly verticalPadding = input(120, { transform: numberAttribute });
|
||||
protected readonly styles = computed(() => ({
|
||||
'padding-top': this.verticalPadding() + 'px',
|
||||
'padding-left': this.horizontalPadding() + 'px',
|
||||
'padding-right': this.horizontalPadding() + 'px',
|
||||
}));
|
||||
readonly helpModeKey = input<string>();
|
||||
@Input() text!: string;
|
||||
@Input() icon?: string;
|
||||
@Input() showButton = true;
|
||||
@Input() buttonIcon = 'iqser:plus';
|
||||
@Input() buttonLabel?: string;
|
||||
@Input() buttonId = `${randomString()}-icon-button`;
|
||||
@Input() horizontalPadding = 100;
|
||||
@Input() verticalPadding = 120;
|
||||
@Input() helpModeKey?: string;
|
||||
@Output() readonly action = new EventEmitter();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.showButton = this.showButton && this.action.observed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
@if (connectionStatus(); as status) {
|
||||
@if (errorService.connectionStatus$ | async; as status) {
|
||||
<div [@animateOpenClose]="status" [ngClass]="status" class="indicator flex-align-items-center">
|
||||
<span [translate]="connectionStatusTranslations[status]"></span>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { connectionStatusTranslations } from '../../translations';
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { ErrorService } from '../error.service';
|
||||
|
||||
@Component({
|
||||
@ -17,9 +16,8 @@ import { ErrorService } from '../error.service';
|
||||
]),
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ConnectionStatusComponent {
|
||||
protected readonly connectionStatusTranslations = connectionStatusTranslations;
|
||||
protected readonly connectionStatus = toSignal(inject(ErrorService).connectionStatus$);
|
||||
connectionStatusTranslations = connectionStatusTranslations;
|
||||
protected readonly errorService = inject(ErrorService);
|
||||
}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { IconButtonTypes } from '../../buttons';
|
||||
import { CustomError, ErrorService, ErrorType } from '../error.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-full-page-error',
|
||||
templateUrl: './full-page-error.component.html',
|
||||
styleUrls: ['./full-page-error.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class FullPageErrorComponent {
|
||||
protected readonly iconButtonTypes = IconButtonTypes;
|
||||
protected readonly errorService = inject(ErrorService);
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
|
||||
constructor(readonly errorService: ErrorService) {}
|
||||
|
||||
errorTitle(error: ErrorType): string {
|
||||
return error instanceof CustomError ? error.label : _('error.title');
|
||||
|
||||
@ -9,12 +9,10 @@
|
||||
></iqser-input-with-action>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-container *ngTemplateOutlet="filterHeader"></ng-container>
|
||||
|
||||
@if (primaryFilters$ | async; as filters) {
|
||||
<div class="filter-content">
|
||||
@for (filter of filters; track filter.id) {
|
||||
@for (filter of filters; track filter) {
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
@ -26,14 +24,12 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (secondaryFilterGroup$ | async; as secondaryGroup) {
|
||||
<div class="filter-options">
|
||||
<div class="filter-menu-options">
|
||||
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
|
||||
</div>
|
||||
|
||||
@for (filter of secondaryGroup.filters; track filter.id) {
|
||||
@for (filter of secondaryGroup.filters; track filter) {
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
@ -55,11 +51,7 @@
|
||||
<ng-template #filterHeader>
|
||||
@if (primaryFilterGroup$ | async; as primaryGroup) {
|
||||
<div class="filter-menu-header">
|
||||
<div
|
||||
[translateParams]="{ count: primaryGroup.filters.length }"
|
||||
[translate]="primaryFiltersLabel()"
|
||||
class="all-caps-label"
|
||||
></div>
|
||||
<div [translateParams]="{ count: primaryGroup.filters.length }" [translate]="primaryFiltersLabel" class="all-caps-label"></div>
|
||||
<div class="actions">
|
||||
@if (!primaryGroup.singleSelect) {
|
||||
<div
|
||||
@ -69,9 +61,8 @@
|
||||
translate="actions.all"
|
||||
></div>
|
||||
}
|
||||
|
||||
<div
|
||||
(click)="deactivatePrimaryFilters()"
|
||||
(click)="deactivateFilters()"
|
||||
class="all-caps-label primary pointer"
|
||||
iqserStopPropagation
|
||||
translate="actions.none"
|
||||
@ -113,27 +104,25 @@
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate()"></ng-container>
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
@if (filter.children?.length && filter.expanded) {
|
||||
<div>
|
||||
@for (child of filter.children; track child) {
|
||||
@if (!child.hidden) {
|
||||
<div class="padding-left mat-mdc-menu-item" iqserStopPropagation>
|
||||
<mat-checkbox
|
||||
(click)="filterCheckboxClicked(child, filterGroup, filter)"
|
||||
[checked]="child.checked"
|
||||
iqserStopPropagation
|
||||
>
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: child }"
|
||||
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate()"></ng-container>
|
||||
</div>
|
||||
}
|
||||
<div class="padding-left mat-mdc-menu-item" iqserStopPropagation>
|
||||
<mat-checkbox
|
||||
(click)="filterCheckboxClicked(child, filterGroup, filter)"
|
||||
[checked]="child.checked"
|
||||
iqserStopPropagation
|
||||
>
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: child }"
|
||||
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, effect, ElementRef, inject, input, numberAttribute, OnInit, TemplateRef } from '@angular/core';
|
||||
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, TemplateRef } from '@angular/core';
|
||||
import { MAT_CHECKBOX_DEFAULT_OPTIONS, MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
@ -23,7 +23,7 @@ const atLeastOneIsExpandable = pipe(
|
||||
);
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-filter-card',
|
||||
selector: 'iqser-filter-card [primaryFiltersSlug]',
|
||||
templateUrl: './filter-card.component.html',
|
||||
styleUrls: ['./filter-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -37,29 +37,39 @@ const atLeastOneIsExpandable = pipe(
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [AsyncPipe, InputWithActionComponent, NgTemplateOutlet, TranslateModule, MatIcon, MatCheckbox, StopPropagationDirective],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
InputWithActionComponent,
|
||||
NgTemplateOutlet,
|
||||
TranslateModule,
|
||||
MatIcon,
|
||||
MatCheckbox,
|
||||
StopPropagationDirective,
|
||||
NgIf,
|
||||
NgForOf,
|
||||
],
|
||||
})
|
||||
export class FilterCardComponent implements OnInit {
|
||||
readonly #filterService = inject(FilterService);
|
||||
readonly #elementRef = inject(ElementRef);
|
||||
protected readonly searchService = inject<SearchService<Filter>>(SearchService);
|
||||
readonly primaryFiltersSlug = input.required<string>();
|
||||
readonly fileId = input<string>();
|
||||
readonly actionsTemplate = input<TemplateRef<unknown>>();
|
||||
readonly secondaryFiltersSlug = input('');
|
||||
readonly primaryFiltersLabel = input<string>(_('filter-menu.filter-types'));
|
||||
readonly minWidth = input(350, { transform: numberAttribute });
|
||||
@Input() primaryFiltersSlug!: string;
|
||||
@Input() fileId?: string;
|
||||
@Input() actionsTemplate?: TemplateRef<unknown>;
|
||||
@Input() secondaryFiltersSlug = '';
|
||||
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
|
||||
@Input() minWidth = 350;
|
||||
|
||||
primaryFilterGroup$!: Observable<IFilterGroup | undefined>;
|
||||
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>;
|
||||
primaryFilters$!: Observable<IFilter[] | undefined>;
|
||||
|
||||
atLeastOneFilterIsExpandable$?: Observable<boolean>;
|
||||
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
(this.#elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth()}px`);
|
||||
});
|
||||
}
|
||||
constructor(
|
||||
readonly filterService: FilterService,
|
||||
readonly searchService: SearchService<Filter>,
|
||||
private readonly _elementRef: ElementRef,
|
||||
) {}
|
||||
|
||||
private get _primaryFilters$(): Observable<IFilter[]> {
|
||||
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
|
||||
@ -69,37 +79,37 @@ export class FilterCardComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.primaryFilterGroup$ = this.#filterService.getGroup$(this.primaryFiltersSlug()).pipe(shareLast());
|
||||
this.secondaryFilterGroup$ = this.#filterService.getGroup$(this.secondaryFiltersSlug()).pipe(shareLast());
|
||||
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast());
|
||||
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast());
|
||||
this.primaryFilters$ = this._primaryFilters$;
|
||||
|
||||
this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$);
|
||||
this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$);
|
||||
|
||||
(this._elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth}px`);
|
||||
}
|
||||
|
||||
filterCheckboxClicked(nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void {
|
||||
this.#filterService.filterCheckboxClicked({
|
||||
this.filterService.filterCheckboxClicked({
|
||||
nestedFilter,
|
||||
filterGroup,
|
||||
parent,
|
||||
primaryFiltersSlug: this.primaryFiltersSlug(),
|
||||
primaryFiltersSlug: this.primaryFiltersSlug,
|
||||
fileId: this.fileId,
|
||||
});
|
||||
this.#filterService.updateFiltersInLocalStorage(this.fileId());
|
||||
}
|
||||
|
||||
deactivatePrimaryFilters() {
|
||||
this.#filterService.deactivateFilters({ primaryFiltersSlug: this.primaryFiltersSlug() });
|
||||
this.#filterService.updateFiltersInLocalStorage(this.fileId());
|
||||
deactivateFilters() {
|
||||
this.filterService.deactivateFilters({ primaryFiltersSlug: this.primaryFiltersSlug });
|
||||
}
|
||||
|
||||
activatePrimaryFilters(): void {
|
||||
this.#filterService.setFilters(this.primaryFiltersSlug(), true);
|
||||
this.#filterService.updateFiltersInLocalStorage(this.fileId());
|
||||
this.filterService.setFilters(this.primaryFiltersSlug, true);
|
||||
}
|
||||
|
||||
toggleFilterExpanded(nestedFilter: INestedFilter): void {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nestedFilter.expanded = !nestedFilter.expanded;
|
||||
this.#filterService.refresh();
|
||||
this.filterService.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ 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);
|
||||
|
||||
@ -14,6 +14,7 @@ export interface CheckboxClickedParams {
|
||||
nestedFilter: INestedFilter;
|
||||
filterGroup: IFilterGroup;
|
||||
parent?: INestedFilter;
|
||||
fileId?: string;
|
||||
primaryFiltersSlug: string;
|
||||
}
|
||||
|
||||
@ -23,10 +24,6 @@ export interface DeactivateFiltersParams {
|
||||
exceptedFilterId?: string;
|
||||
}
|
||||
|
||||
const PRIMARY_FILTERS = 'primaryFilters';
|
||||
const SECONDARY_FILTERS = 'secondaryFilters';
|
||||
const WORKLOAD_FILTERS_KEY = 'workload-filters';
|
||||
|
||||
@Injectable()
|
||||
export class FilterService {
|
||||
readonly #singleFilters = new Map<string, BehaviorSubject<IFilter | undefined>>();
|
||||
@ -239,7 +236,7 @@ export class FilterService {
|
||||
}
|
||||
|
||||
filterCheckboxClicked(params: CheckboxClickedParams) {
|
||||
const { filterGroup, nestedFilter, parent, primaryFiltersSlug } = params;
|
||||
const { filterGroup, nestedFilter, parent, fileId, primaryFiltersSlug } = params;
|
||||
|
||||
if (filterGroup.singleSelect) {
|
||||
this.deactivateFilters({ primaryFiltersSlug, exceptedFilterId: nestedFilter.id });
|
||||
@ -262,22 +259,23 @@ export class FilterService {
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
this.#updateFiltersInLocalStorage(fileId);
|
||||
}
|
||||
|
||||
updateFiltersInLocalStorage(fileId?: string) {
|
||||
#updateFiltersInLocalStorage(fileId?: string) {
|
||||
if (fileId) {
|
||||
const primaryFilters = this.getGroup(PRIMARY_FILTERS);
|
||||
const secondaryFilters = this.getGroup(SECONDARY_FILTERS);
|
||||
const primaryFilters = this.getGroup('primaryFilters');
|
||||
const secondaryFilters = this.getGroup('secondaryFilters');
|
||||
|
||||
const filters: LocalStorageFilters = {
|
||||
primaryFilters: extractFilterValues(primaryFilters?.filters),
|
||||
secondaryFilters: extractFilterValues(secondaryFilters?.filters),
|
||||
};
|
||||
|
||||
const workloadFiltersString = localStorage.getItem(WORKLOAD_FILTERS_KEY) ?? '{}';
|
||||
const workloadFiltersString = localStorage.getItem('workload-filters') ?? '{}';
|
||||
const workloadFilters = JSON.parse(workloadFiltersString);
|
||||
workloadFilters[fileId] = filters;
|
||||
localStorage.setItem(WORKLOAD_FILTERS_KEY, JSON.stringify(workloadFilters));
|
||||
localStorage.setItem('workload-filters', JSON.stringify(workloadFilters));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ChevronButtonComponent } from '../buttons/chevron-button/chevron-button.component';
|
||||
import { IconButtonComponent } from '../buttons/icon-button/icon-button.component';
|
||||
import { PreventDefaultDirective } from '../directives/prevent-default.directive';
|
||||
import { StopPropagationDirective } from '../directives/stop-propagation.directive';
|
||||
import { InputWithActionComponent } from '../inputs/input-with-action/input-with-action.component';
|
||||
import { ChevronButtonComponent, IconButtonComponent } from '../buttons';
|
||||
import { QuickFiltersComponent } from './quick-filters/quick-filters.component';
|
||||
import { SingleFilterComponent } from './single-filter/single-filter.component';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { PreventDefaultDirective, StopPropagationDirective } from '../directives';
|
||||
import { InputWithActionComponent } from '../inputs';
|
||||
|
||||
const components = [QuickFiltersComponent, SingleFilterComponent];
|
||||
|
||||
|
||||
@ -10,6 +10,5 @@ export interface IFilter {
|
||||
readonly required?: boolean;
|
||||
readonly disabled?: boolean;
|
||||
readonly helpModeKey?: string;
|
||||
readonly hidden?: boolean;
|
||||
readonly metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ export class Filter implements IFilter, IListable {
|
||||
readonly checker?: (obj?: unknown) => boolean;
|
||||
readonly skipTranslation?: boolean;
|
||||
readonly metadata?: Record<string, any>;
|
||||
readonly hidden?: boolean;
|
||||
|
||||
checked: boolean;
|
||||
matches?: number;
|
||||
@ -26,7 +25,6 @@ export class Filter implements IFilter, IListable {
|
||||
this.required = !!filter.required;
|
||||
this.skipTranslation = !!filter.skipTranslation;
|
||||
this.metadata = filter.metadata;
|
||||
this.hidden = !!filter.hidden;
|
||||
}
|
||||
|
||||
get searchKey(): string {
|
||||
|
||||
@ -8,8 +8,8 @@ export class NestedFilter extends Filter implements INestedFilter, IListable {
|
||||
disabled?: boolean;
|
||||
helpModeKey?: string;
|
||||
readonly children: Filter[];
|
||||
override readonly skipTranslation?: boolean;
|
||||
override readonly metadata?: Record<string, any>;
|
||||
readonly skipTranslation?: boolean;
|
||||
readonly metadata?: Record<string, any>;
|
||||
|
||||
constructor(nestedFilter: INestedFilter) {
|
||||
super(nestedFilter);
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
buttonId="{{ primaryGroup.slug }}"
|
||||
></iqser-icon-button>
|
||||
}
|
||||
|
||||
@if (!primaryGroup.icon) {
|
||||
<iqser-chevron-button
|
||||
[attr.aria-expanded]="expanded$ | async"
|
||||
@ -22,7 +21,6 @@
|
||||
[showDot]="hasActiveFilters$ | async"
|
||||
></iqser-chevron-button>
|
||||
}
|
||||
|
||||
<mat-menu
|
||||
#filterMenu="matMenu"
|
||||
(closed)="expanded.next(false)"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core';
|
||||
import { MatMenu, MatMenuContent, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
@ -28,7 +28,9 @@ import { IFilterGroup } from '../models/filter-group.model';
|
||||
FilterCardComponent,
|
||||
StopPropagationDirective,
|
||||
MatMenuContent,
|
||||
NgIf,
|
||||
],
|
||||
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.id) {
|
||||
@for (filter of filters; track filter) {
|
||||
<div
|
||||
(click)="filterService.toggleFilter('quickFilters', filter.id)"
|
||||
[class.active]="filter.checked"
|
||||
|
||||
@ -6,7 +6,6 @@ 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');
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import { Component, computed, effect, input, output, signal, untracked } from '@angular/core';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CircleButtonComponent, IconButtonComponent } from '../../buttons';
|
||||
import { ChevronButtonComponent } from '../../buttons/chevron-button/chevron-button.component';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { ChevronButtonComponent, CircleButtonComponent, IconButtonComponent } from '../../buttons';
|
||||
import { StopPropagationDirective } from '../../directives';
|
||||
import { InputWithActionComponent } from '../../inputs/input-with-action/input-with-action.component';
|
||||
import { InputWithActionComponent } from '../../inputs';
|
||||
import { SimpleFilterOption } from '../models/simple-filter-option';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-simple-popup-filter',
|
||||
templateUrl: './simple-popup-filter.component.html',
|
||||
styleUrls: ['./simple-popup-filter.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatMenuModule,
|
||||
IconButtonComponent,
|
||||
@ -22,6 +22,7 @@ import { SimpleFilterOption } from '../models/simple-filter-option';
|
||||
TranslateModule,
|
||||
MatCheckbox,
|
||||
IconButtonComponent,
|
||||
ChevronButtonComponent,
|
||||
CircleButtonComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@ -7,7 +7,6 @@ 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;
|
||||
|
||||
@ -2,14 +2,11 @@
|
||||
[id]="buttonId"
|
||||
class="help-mode-slide-toggle"
|
||||
[class.dialog-toggle]="dialogButton"
|
||||
[class.active]="helpModeService.isHelpModeActive()"
|
||||
[matTooltip]="buttonTooltip"
|
||||
#helpModeButton
|
||||
[class.active]="helpModeService.isHelpModeActive"
|
||||
>
|
||||
<input type="checkbox" class="toggle-input" [checked]="helpModeService.isHelpModeActive()" (change)="toggleHelpMode()" />
|
||||
<div class="toggle-track">
|
||||
<div class="toggle-thumb">
|
||||
<mat-icon svgIcon="iqser:help-outline" [class.active-thumb]="helpModeService.isHelpModeActive()"></mat-icon>
|
||||
</div>
|
||||
<input type="checkbox" class="toggle-input" [checked]="helpModeService.isHelpModeActive" (change)="toggleHelpMode()" />
|
||||
<div class="toggle-track"></div>
|
||||
<div class="toggle-thumb">
|
||||
<mat-icon svgIcon="iqser:help-outline" [class.active-thumb]="helpModeService.isHelpModeActive"></mat-icon>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
&.active,
|
||||
&.dialog-toggle {
|
||||
z-index: 1200;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
@ -22,24 +22,27 @@
|
||||
}
|
||||
|
||||
.toggle-track {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
background-color: var(--iqser-grey-4);
|
||||
border-radius: 20px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.toggle-thumb {
|
||||
margin-left: 6%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
transition: margin-left 0.3s ease;
|
||||
transform: translateY(-50%);
|
||||
transition: left 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -57,9 +60,7 @@
|
||||
background: var(--iqser-helpmode-primary);
|
||||
}
|
||||
|
||||
.toggle-input:checked + .toggle-track {
|
||||
.toggle-thumb {
|
||||
margin-left: 43.5%;
|
||||
}
|
||||
.toggle-input:checked + .toggle-track + .toggle-thumb {
|
||||
left: calc(100% - 22px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,55 +1,28 @@
|
||||
/* eslint-disable @angular-eslint/prefer-on-push-component-change-detection */
|
||||
import { Component, effect, ElementRef, HostListener, Input, OnDestroy, OnInit, viewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, ElementRef, HostListener, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { HelpModeService } from '../help-mode.service';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-help-button',
|
||||
templateUrl: './help-button.component.html',
|
||||
styleUrls: ['./help-button.component.scss'],
|
||||
imports: [MatIcon, MatTooltip],
|
||||
standalone: true,
|
||||
imports: [MatIcon],
|
||||
})
|
||||
export class HelpButtonComponent implements OnInit, OnDestroy {
|
||||
#helpModeHasBeenActivated = false;
|
||||
readonly helpModeButton = viewChild.required<ElementRef>('helpModeButton');
|
||||
export class HelpButtonComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Input() dialogButton = true;
|
||||
helpModeButton: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private readonly _elementRef: ElementRef,
|
||||
private readonly _translateService: TranslateService,
|
||||
readonly helpModeService: HelpModeService,
|
||||
) {
|
||||
effect(() => {
|
||||
if (this.helpModeService.isHelpModeActive()) {
|
||||
this.#helpModeHasBeenActivated = true;
|
||||
setTimeout(() => this.#applyActiveButtonStyles(), 300);
|
||||
} else if (this.#helpModeHasBeenActivated) {
|
||||
setTimeout(() => this.#applyInactiveButtonStyles(), 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get buttonTooltip() {
|
||||
const translation = this.helpModeService.isHelpModeActive() ? _('help-button.disable') : _('help-button.enable');
|
||||
return this._translateService.instant(translation);
|
||||
}
|
||||
) {}
|
||||
|
||||
get buttonId() {
|
||||
return `help-mode-button${this.dialogButton ? '-dialog' : ''}`;
|
||||
}
|
||||
|
||||
get currentComponentRect() {
|
||||
return this._elementRef.nativeElement.getBoundingClientRect();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
if (this.helpModeService.isHelpModeActive()) this.#applyActiveButtonStyles();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.dialogButton) {
|
||||
const defaultButton = document.getElementById('help-mode-button') as HTMLElement;
|
||||
@ -57,39 +30,42 @@ export class HelpButtonComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
const currentComponent = this._elementRef.nativeElement;
|
||||
this.helpModeButton = currentComponent.querySelector('.help-mode-slide-toggle');
|
||||
|
||||
if (this.helpModeButton) {
|
||||
setTimeout(() => {
|
||||
const currentComponentRect = currentComponent.getBoundingClientRect();
|
||||
this.helpModeButton.style.setProperty('position', 'absolute');
|
||||
this.helpModeButton.style.setProperty('top', `${currentComponentRect.top}px`);
|
||||
this.helpModeButton.style.setProperty('left', `${currentComponentRect.left}px`);
|
||||
document.body.appendChild(this.helpModeButton);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
document.body.removeChild(this.helpModeButton);
|
||||
if (this.dialogButton) {
|
||||
const defaultButton = document.getElementById('help-mode-button') as HTMLElement;
|
||||
defaultButton.style.removeProperty('z-index');
|
||||
|
||||
if (!this.helpModeService.isHelpModeActive()) {
|
||||
const helpButtonElement = document.querySelectorAll('iqser-help-button')[this.dialogButton ? 1 : 0];
|
||||
if (helpButtonElement.contains(this.helpModeButton().nativeElement))
|
||||
helpButtonElement?.removeChild(this.helpModeButton().nativeElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleHelpMode(): void {
|
||||
if (this.helpModeService.isHelpModeActive()) {
|
||||
if (this.helpModeService.isHelpModeActive) {
|
||||
this.helpModeService.deactivateHelpMode();
|
||||
return;
|
||||
}
|
||||
this.helpModeService.activateHelpMode(this.dialogButton);
|
||||
}
|
||||
|
||||
#applyActiveButtonStyles() {
|
||||
this.helpModeButton().nativeElement.style.setProperty('position', 'absolute');
|
||||
this.helpModeButton().nativeElement.style.setProperty('top', `${this.currentComponentRect.top}px`);
|
||||
this.helpModeButton().nativeElement.style.setProperty('left', `${this.currentComponentRect.left}px`);
|
||||
document.body.appendChild(this.helpModeButton().nativeElement);
|
||||
}
|
||||
|
||||
#applyInactiveButtonStyles() {
|
||||
this.helpModeButton().nativeElement.style.setProperty('position', 'relative');
|
||||
this.helpModeButton().nativeElement.style.setProperty('top', 'unset');
|
||||
this.helpModeButton().nativeElement.style.setProperty('left', 'unset');
|
||||
document.body.removeChild(this.helpModeButton().nativeElement);
|
||||
document.querySelectorAll('iqser-help-button')[this.dialogButton ? 1 : 0]?.appendChild(this.helpModeButton().nativeElement);
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
const currentComponent = this._elementRef.nativeElement;
|
||||
const currentComponentRect = currentComponent.getBoundingClientRect();
|
||||
this.helpModeButton?.style.setProperty('top', `${currentComponentRect.top}px`);
|
||||
this.helpModeButton?.style.setProperty('left', `${currentComponentRect.left}px`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ 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 {
|
||||
|
||||
@ -2,8 +2,8 @@ import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
import { getConfig } from '../services';
|
||||
import { IqserUserPreferenceService } from '../services';
|
||||
import { getConfig } from '../services/iqser-config.service';
|
||||
import { IqserUserPreferenceService } from '../services/iqser-user-preference.service';
|
||||
import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
|
||||
import { HELP_MODE_KEYS, MANUAL_BASE_URL } from './tokens';
|
||||
import { HelpModeKey } from './types';
|
||||
@ -20,7 +20,6 @@ import {
|
||||
ScrollableParentViews,
|
||||
WEB_VIEWER_ELEMENTS,
|
||||
} from './utils/constants';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
export interface Helper {
|
||||
readonly element: HTMLElement;
|
||||
@ -40,7 +39,6 @@ export class HelpModeService {
|
||||
#helpers: Record<string, Helper> = {};
|
||||
#dialogMode = false;
|
||||
readonly isHelpModeActive$ = this.#isHelpModeActive$.asObservable();
|
||||
readonly isHelpModeActive = toSignal(this.isHelpModeActive$, { initialValue: false });
|
||||
readonly helpModeDialogIsOpened$ = this.#helpModeDialogIsOpened$.asObservable();
|
||||
|
||||
constructor(
|
||||
@ -54,6 +52,10 @@ export class HelpModeService {
|
||||
this.#renderer = this._rendererFactory.createRenderer(null, null);
|
||||
}
|
||||
|
||||
get isHelpModeActive(): boolean {
|
||||
return this.#isHelpModeActive$.getValue();
|
||||
}
|
||||
|
||||
get helpModeDialogIsOpened(): boolean {
|
||||
return this.#helpModeDialogIsOpened$.getValue();
|
||||
}
|
||||
@ -76,7 +78,7 @@ export class HelpModeService {
|
||||
}
|
||||
|
||||
activateHelpMode(dialogMode: boolean = false): void {
|
||||
if (!this.isHelpModeActive()) {
|
||||
if (!this.isHelpModeActive) {
|
||||
document.body.style.setProperty('overflow', 'unset');
|
||||
this.#isHelpModeActive$.next(true);
|
||||
this.openHelpModeDialog();
|
||||
@ -90,7 +92,7 @@ export class HelpModeService {
|
||||
}
|
||||
|
||||
deactivateHelpMode(): void {
|
||||
if (this.isHelpModeActive()) {
|
||||
if (this.isHelpModeActive) {
|
||||
document.body.style.removeProperty('overflow');
|
||||
this.#isHelpModeActive$.next(false);
|
||||
this.#disableHelperElements();
|
||||
@ -242,7 +244,6 @@ 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 = `
|
||||
|
||||
@ -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 } from '@angular/common';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
@ -11,7 +11,8 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
templateUrl: './help-mode.component.html',
|
||||
styleUrls: ['./help-mode.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [AsyncPipe, TranslateModule, CircleButtonComponent],
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, TranslateModule, NgIf, CircleButtonComponent],
|
||||
})
|
||||
export class HelpModeComponent {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
@ -23,7 +24,7 @@ export class HelpModeComponent {
|
||||
|
||||
@HostListener('document:keydown.escape', ['$event'])
|
||||
onEscKeydownHandler(event: KeyboardEvent): void {
|
||||
if (!this.helpModeService.helpModeDialogIsOpened && this.helpModeService.isHelpModeActive()) {
|
||||
if (!this.helpModeService.helpModeDialogIsOpened && this.helpModeService.isHelpModeActive) {
|
||||
event?.stopPropagation();
|
||||
this.helpModeService.deactivateHelpMode();
|
||||
}
|
||||
@ -32,7 +33,7 @@ export class HelpModeComponent {
|
||||
@HostListener('document:keydown.h', ['$event'])
|
||||
onHKeydownHandler(event: KeyboardEvent): void {
|
||||
const node = (event.target as IqserEventTarget).localName;
|
||||
if (!this.helpModeService.isHelpModeActive() && node !== 'input' && node !== 'textarea') {
|
||||
if (!this.helpModeService.isHelpModeActive && node !== 'input' && node !== 'textarea') {
|
||||
const dialogMode = !!this._dialog.openDialogs.length;
|
||||
this.helpModeService.activateHelpMode(dialogMode);
|
||||
}
|
||||
@ -40,14 +41,14 @@ export class HelpModeComponent {
|
||||
|
||||
@HostListener('click')
|
||||
onClick(): void {
|
||||
if (this.helpModeService.isHelpModeActive()) {
|
||||
if (this.helpModeService.isHelpModeActive) {
|
||||
this.helpModeService.highlightHelperElements();
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize() {
|
||||
if (this.helpModeService.isHelpModeActive()) {
|
||||
if (this.helpModeService.isHelpModeActive) {
|
||||
this.helpModeService.updateHelperElements();
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export const PDF_TRON_IFRAME_ID = 'webviewer-1';
|
||||
export const WEB_VIEWER_ELEMENTS = [
|
||||
{
|
||||
querySelector: '.HeaderItems',
|
||||
documentKey: 'document_viewer_features',
|
||||
documentKey: 'pdf_features',
|
||||
},
|
||||
];
|
||||
|
||||
@ -18,7 +18,6 @@ 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 = {
|
||||
@ -26,7 +25,6 @@ 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;
|
||||
|
||||
@ -1,29 +1,19 @@
|
||||
interface AdditionalField {
|
||||
interface ExtraOption {
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface AdditionalCheck extends AdditionalField {
|
||||
checked?: boolean;
|
||||
checked: boolean;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface AdditionalInput extends AdditionalField {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
errorCode?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface DetailsRadioOption<I> {
|
||||
id?: string;
|
||||
label: string;
|
||||
description: string;
|
||||
descriptionParams?: Record<string, string | number>;
|
||||
descriptionParams?: Record<string, string>;
|
||||
icon?: string;
|
||||
value: I;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
additionalCheck?: AdditionalCheck;
|
||||
additionalInput?: AdditionalInput;
|
||||
extraOption?: ExtraOption;
|
||||
}
|
||||
|
||||
@ -1,70 +1,41 @@
|
||||
<div [class.row]="displayInRow()" class="iqser-input-group">
|
||||
@for (option of options(); track option) {
|
||||
<div [class.row]="displayInRow" class="iqser-input-group">
|
||||
@for (option of options; track option) {
|
||||
<div
|
||||
(click)="toggleOption(option)"
|
||||
[class.active]="isSelected(option)"
|
||||
[class.active]="option.value === value?.value"
|
||||
[class.disabled]="option.disabled"
|
||||
[id]="groupId(option)"
|
||||
[matTooltipPosition]="'above'"
|
||||
[matTooltip]="option.tooltip || '' | translate"
|
||||
[ngClass]="{ 'mb-8': !displayInRow(), 'mr-8': displayInRow() }"
|
||||
[ngClass]="{ 'mb-8': !displayInRow, 'mr-8': displayInRow }"
|
||||
class="option pointer"
|
||||
>
|
||||
@if (option.icon) {
|
||||
<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 | replaceNbsp
|
||||
}}</label>
|
||||
|
||||
<label class="details-radio-label pointer">{{ option.label | translate: option.descriptionParams }}</label>
|
||||
<span class="hint">{{ option.description | translate: option.descriptionParams | replaceNbsp }}</span>
|
||||
|
||||
@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.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>
|
||||
}
|
||||
@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 (option.extraOption.description) {
|
||||
<span
|
||||
[innerHTML]="option.extraOption.description | translate"
|
||||
class="hint extra-option-description"
|
||||
></span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isSelected(option)) {
|
||||
<mat-icon class="checked" svgIcon="iqser:radio-selected"></mat-icon>
|
||||
}
|
||||
@ -72,11 +43,9 @@
|
||||
} @else {
|
||||
<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 | replaceNbsp }}</label>
|
||||
<label class="details-radio-label pointer">{{ option.label | translate }}</label>
|
||||
</div>
|
||||
|
||||
<span class="hint">{{ option.description | translate | replaceNbsp }}</span>
|
||||
<span class="hint">{{ option.description | translate }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -43,44 +43,10 @@ label {
|
||||
}
|
||||
}
|
||||
|
||||
.additional-check-description {
|
||||
.extra-option-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 {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { NgClass } from '@angular/common';
|
||||
import { booleanAttribute, Component, input, output } from '@angular/core';
|
||||
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@ -14,6 +14,7 @@ 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,
|
||||
@ -39,12 +40,12 @@ 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;
|
||||
@Input({ required: true }) options: DetailsRadioOption<I>[] = [];
|
||||
@Input({ transform: booleanAttribute }) displayInRow = false;
|
||||
|
||||
toggleOption(option: DetailsRadioOption<I>) {
|
||||
@Output() readonly extraOptionChanged: EventEmitter<DetailsRadioOption<I>> = new EventEmitter();
|
||||
|
||||
toggleOption(option: DetailsRadioOption<I>): void {
|
||||
if (option.value !== this._value?.value && !option.disabled) {
|
||||
this.markAsTouched();
|
||||
const currentlyChecked = this.value?.value === option.value;
|
||||
@ -53,20 +54,15 @@ export class DetailsRadioComponent<I> extends FormFieldComponent<DetailsRadioOpt
|
||||
}
|
||||
}
|
||||
|
||||
groupId(option: DetailsRadioOption<I>) {
|
||||
groupId(option: DetailsRadioOption<I>): string {
|
||||
return (option.id ?? option.label.replace('.', '-')) + '-checkbox-details-input';
|
||||
}
|
||||
|
||||
isSelected(option: DetailsRadioOption<I>) {
|
||||
isSelected(option: DetailsRadioOption<I>): boolean {
|
||||
return option.value === this.value?.value;
|
||||
}
|
||||
|
||||
emitExtraOption() {
|
||||
if (!this.value) {
|
||||
console.error('Extra option selected but the value is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
emitExtraOption(): void {
|
||||
this.extraOptionChanged.emit(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,46 +1,37 @@
|
||||
<div [class.datepicker-wrapper]="isDate()" [ngClass]="classList()" class="iqser-input-group">
|
||||
@if (label()) {
|
||||
<label> {{ label() }} </label>
|
||||
<div [class.datepicker-wrapper]="isDate" [ngClass]="classList" class="iqser-input-group">
|
||||
@if (label) {
|
||||
<label> {{ label }} </label>
|
||||
}
|
||||
|
||||
@if (isDate()) {
|
||||
@if (isDate) {
|
||||
<input
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="input"
|
||||
[disabled]="disabled"
|
||||
[id]="id()"
|
||||
[id]="id"
|
||||
[matDatepicker]="picker"
|
||||
[placeholder]="placeholder() || 'dd/mm/yy'"
|
||||
[placeholder]="placeholder || 'dd/mm/yy'"
|
||||
iqserStopPropagation
|
||||
/>
|
||||
|
||||
<mat-datepicker-toggle [for]="picker" matSuffix>
|
||||
<mat-icon matDatepickerToggleIcon svgIcon="iqser:calendar"></mat-icon>
|
||||
</mat-datepicker-toggle>
|
||||
|
||||
<mat-datepicker #picker (closed)="onCloseDatepicker()" (opened)="onOpenDatepicker()"></mat-datepicker>
|
||||
}
|
||||
|
||||
@if (isText()) {
|
||||
@if (isText) {
|
||||
<input
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="input"
|
||||
[disabled]="disabled"
|
||||
[id]="id()"
|
||||
[placeholder]="placeholder() || ''"
|
||||
[id]="id"
|
||||
[placeholder]="placeholder || ''"
|
||||
iqserStopPropagation
|
||||
type="text"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (isNumber()) {
|
||||
<input
|
||||
(ngModelChange)="onChange($event)"
|
||||
[(ngModel)]="input"
|
||||
[disabled]="disabled"
|
||||
[id]="id()"
|
||||
iqserStopPropagation
|
||||
type="number"
|
||||
/>
|
||||
@if (isNumber) {
|
||||
<input (ngModelChange)="onChange($event)" [(ngModel)]="input" [disabled]="disabled" [id]="id" iqserStopPropagation type="number" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { NgClass } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { FormFieldComponent } from '../form-field/form-field-component.directive';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { StopPropagationDirective } from '../../directives';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { StopPropagationDirective } from '../../directives';
|
||||
import { FormFieldComponent } from '../form-field/form-field-component.directive';
|
||||
|
||||
export const InputTypes = {
|
||||
const InputTypes = {
|
||||
DATE: 'DATE',
|
||||
NUMBER: 'NUMBER',
|
||||
TEXT: 'TEXT',
|
||||
@ -23,6 +23,7 @@ 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,
|
||||
@ -38,20 +39,28 @@ type DynamicInput = number | string | Date;
|
||||
imports: [NgClass, FormsModule, MatDatepickerModule, StopPropagationDirective, MatIconModule, MatInputModule],
|
||||
})
|
||||
export class DynamicInputComponent extends FormFieldComponent<DynamicInput> {
|
||||
readonly label = input<string>();
|
||||
readonly type = input.required<InputType>();
|
||||
readonly placeholder = input<string>();
|
||||
readonly id = input<string>();
|
||||
readonly classList = input('');
|
||||
readonly input = model<DynamicInput>();
|
||||
readonly closedDatepicker = output<boolean>();
|
||||
@Input() label?: string;
|
||||
@Input({ required: true }) type!: InputType;
|
||||
@Input() placeholder?: string;
|
||||
@Input() id?: string;
|
||||
@Input() classList = '';
|
||||
@Input() input!: DynamicInput;
|
||||
@Output() readonly closedDatepicker = new EventEmitter<boolean>();
|
||||
|
||||
readonly isDate = computed(() => this.type() === InputTypes.DATE);
|
||||
readonly isNumber = computed(() => this.type() === InputTypes.NUMBER);
|
||||
readonly isText = computed(() => this.type() === InputTypes.TEXT);
|
||||
get isDate() {
|
||||
return this.type === InputTypes.DATE;
|
||||
}
|
||||
|
||||
override writeValue(input: DynamicInput): void {
|
||||
this.input.set(input);
|
||||
get isNumber() {
|
||||
return this.type === InputTypes.NUMBER;
|
||||
}
|
||||
|
||||
get isText() {
|
||||
return this.type === InputTypes.TEXT;
|
||||
}
|
||||
|
||||
writeValue(input: DynamicInput): void {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
onCloseDatepicker() {
|
||||
|
||||
@ -1,53 +1,48 @@
|
||||
@if (!_editing()) {
|
||||
@if (showPreview()) {
|
||||
<div>{{ value() }}</div>
|
||||
@if (!editing) {
|
||||
@if (showPreview) {
|
||||
<div>
|
||||
{{ value }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex">
|
||||
@if (canEdit()) {
|
||||
@if (canEdit) {
|
||||
<iqser-circle-button
|
||||
(action)="editing = true"
|
||||
[tooltip]="editTooltip()"
|
||||
[type]="buttonsType()"
|
||||
[attr.help-mode-key]="helpModeKey()"
|
||||
[tooltip]="editTooltip"
|
||||
[type]="buttonsType"
|
||||
[attr.help-mode-key]="helpModeKey"
|
||||
class="edit-button"
|
||||
icon="iqser:edit"
|
||||
></iqser-circle-button>
|
||||
}
|
||||
|
||||
<ng-content select="[slot=editing]"></ng-content>
|
||||
</div>
|
||||
} @else {
|
||||
}
|
||||
|
||||
@if (editing) {
|
||||
<form (submit)="saveValue()">
|
||||
<div [class]="'iqser-input-group ' + class()">
|
||||
@if (!parentId()) {
|
||||
<input (ngModelChange)="newValue = $event" [ngModel]="value()" [placeholder]="placeholder()" name="name" />
|
||||
<div [class]="'iqser-input-group ' + class">
|
||||
@if (!parentId) {
|
||||
<input (ngModelChange)="newValue = $event" [ngModel]="value" [placeholder]="placeholder" name="name" />
|
||||
} @else {
|
||||
<textarea
|
||||
(ngModelChange)="newValue = $event"
|
||||
[ngModel]="value()"
|
||||
[placeholder]="placeholder()"
|
||||
[id]="id()"
|
||||
[ngModel]="value"
|
||||
[placeholder]="placeholder"
|
||||
[id]="id"
|
||||
name="name"
|
||||
[style.width]="textArea.width + 'px'"
|
||||
[style.height]="textArea.height + 'px'"
|
||||
[style.width]="this.textArea.width + 'px'"
|
||||
[style.height]="this.textArea.height + 'px'"
|
||||
></textarea>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex">
|
||||
<iqser-circle-button
|
||||
(action)="saveValue()"
|
||||
[tooltip]="saveTooltip()"
|
||||
[type]="buttonsType()"
|
||||
icon="iqser:check"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button (action)="saveValue()" [tooltip]="saveTooltip" [type]="buttonsType" icon="iqser:check"></iqser-circle-button>
|
||||
<iqser-circle-button
|
||||
(action)="editing = false"
|
||||
[tooltip]="cancelTooltip()"
|
||||
[type]="buttonsType()"
|
||||
[tooltip]="cancelTooltip"
|
||||
[type]="buttonsType"
|
||||
icon="iqser:close"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
|
||||
@ -1,40 +1,43 @@
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, input, OnChanges, output, signal, SimpleChanges } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CircleButtonComponent } from '../../buttons/circle-button/circle-button.component';
|
||||
import { CircleButtonType, CircleButtonTypes } from '../../buttons/types/circle-button.type';
|
||||
import { CircleButtonComponent, CircleButtonType, CircleButtonTypes } from '../../buttons';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-editable-input',
|
||||
selector: 'iqser-editable-input [value]',
|
||||
templateUrl: './editable-input.component.html',
|
||||
styleUrls: ['./editable-input.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CircleButtonComponent, FormsModule],
|
||||
host: {
|
||||
'[class.editing]': '_editing()',
|
||||
},
|
||||
})
|
||||
export class EditableInputComponent implements OnChanges {
|
||||
protected readonly _editing = signal(false);
|
||||
readonly id = input<string>();
|
||||
readonly parentId = input<string>();
|
||||
readonly value = input.required<string>();
|
||||
readonly editTooltip = input('');
|
||||
readonly saveTooltip = input('');
|
||||
readonly cancelTooltip = input('');
|
||||
readonly placeholder = input('');
|
||||
readonly class = input<string>();
|
||||
readonly showPreview = input(true, { transform: booleanAttribute });
|
||||
readonly canEdit = input(true, { transform: booleanAttribute });
|
||||
readonly buttonsType = input<CircleButtonType>(CircleButtonTypes.default);
|
||||
readonly helpModeKey = input('');
|
||||
readonly lastChild = input(false, { transform: booleanAttribute });
|
||||
readonly save = output<string>();
|
||||
@Input() id?: string;
|
||||
@Input() parentId?: string;
|
||||
@Input() value!: string;
|
||||
@Input() editTooltip?: string;
|
||||
@Input() saveTooltip?: string;
|
||||
@Input() cancelTooltip?: string;
|
||||
@Input() placeholder = '';
|
||||
@Input() class?: string;
|
||||
@Input() showPreview = true;
|
||||
@Input() canEdit = true;
|
||||
@Input() buttonsType: CircleButtonType = CircleButtonTypes.default;
|
||||
@Input() helpModeKey: string = '';
|
||||
@Input() lastChild = false;
|
||||
@Output() readonly save = new EventEmitter<string>();
|
||||
textArea?: { width: number; height: number };
|
||||
newValue = '';
|
||||
|
||||
private _editing = false;
|
||||
|
||||
@HostBinding('class.editing')
|
||||
get editing(): boolean {
|
||||
return this._editing;
|
||||
}
|
||||
|
||||
set editing(value: boolean) {
|
||||
this._editing.set(value);
|
||||
this.newValue = this.value();
|
||||
this._editing = value;
|
||||
this.newValue = this.value;
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
@ -48,12 +51,12 @@ export class EditableInputComponent implements OnChanges {
|
||||
|
||||
setTextAreaSize() {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(this.id() as string) as HTMLElement;
|
||||
const parentElement = document.getElementById(this.parentId() as string) as HTMLElement;
|
||||
const element = document.getElementById(this.id as string) as HTMLElement;
|
||||
const parentElement = document.getElementById(this.parentId as string) as HTMLElement;
|
||||
const width = parentElement.offsetWidth - 98;
|
||||
let height = (this.lastChild() ? parentElement.offsetHeight : element.offsetHeight) - 16;
|
||||
if (this.lastChild()) {
|
||||
const lastChildIndex = Number(this.id()?.split('-')[2]);
|
||||
let height = (this.lastChild ? parentElement.offsetHeight : element.offsetHeight) - 16;
|
||||
if (this.lastChild) {
|
||||
const lastChildIndex = Number(this.id?.split('-')[2]);
|
||||
height = height - lastChildIndex * element.offsetHeight;
|
||||
}
|
||||
this.textArea = { width, height };
|
||||
|
||||
@ -1,47 +1,19 @@
|
||||
import { ChangeDetectorRef, Directive, inject, Injector, OnInit } from '@angular/core';
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
FormControl,
|
||||
FormControlDirective,
|
||||
FormControlName,
|
||||
FormGroupDirective,
|
||||
NgControl,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
} from '@angular/forms';
|
||||
import { ChangeDetectorRef, Directive, inject } from '@angular/core';
|
||||
import { ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms';
|
||||
|
||||
@Directive()
|
||||
export abstract class FormFieldComponent<I> implements ControlValueAccessor, Validator, OnInit {
|
||||
export abstract class FormFieldComponent<I> implements ControlValueAccessor, Validator {
|
||||
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) => {};
|
||||
|
||||
|
||||
7
src/lib/inputs/index.ts
Normal file
7
src/lib/inputs/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './round-checkbox/round-checkbox.component';
|
||||
export * from './editable-input/editable-input.component';
|
||||
export * from './input-with-action/input-with-action.component';
|
||||
export * from './details-radio/details-radio.component';
|
||||
export * from './details-radio/details-radio-option';
|
||||
export * from './form-field/form-field-component.directive';
|
||||
export * from './dynamic-input/dynamic-input.component';
|
||||
@ -1,33 +1,34 @@
|
||||
<form (submit)="executeAction()" [style.max-width]="_computedWidth()" [style.width]="_computedWidth()" class="iqser-input-group">
|
||||
<form (submit)="executeAction()" [style.max-width]="computedWidth" [style.width]="computedWidth" class="iqser-input-group">
|
||||
<input
|
||||
(ngModelChange)="valueChange.emit($event)"
|
||||
[(ngModel)]="value"
|
||||
[autocomplete]="autocomplete()"
|
||||
[disabled]="disabled()"
|
||||
[id]="inputId()"
|
||||
[autocomplete]="autocomplete"
|
||||
[disabled]="disabled"
|
||||
[id]="inputId"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[placeholder]="placeholder()"
|
||||
[placeholder]="placeholder"
|
||||
class="with-icon mt-0"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
@if (hint()) {
|
||||
<span class="hint">{{ hint() }}</span>
|
||||
@if (hint) {
|
||||
<span class="hint">{{ hint }}</span>
|
||||
}
|
||||
|
||||
@if (_isSearch() && !_hasContent()) {
|
||||
@if (isSearch && !hasContent) {
|
||||
<mat-icon class="icon-right" svgIcon="iqser:search"></mat-icon>
|
||||
}
|
||||
|
||||
@if (_isSearch() && _hasContent()) {
|
||||
<iqser-circle-button (action)="reset()" [buttonId]="inputId() + '-clear'" icon="iqser:close"></iqser-circle-button>
|
||||
@if (isSearch && hasContent) {
|
||||
<iqser-circle-button (action)="reset()" [buttonId]="inputId + '-clear'" icon="iqser:close"></iqser-circle-button>
|
||||
}
|
||||
|
||||
@if (icon(); as icon) {
|
||||
@if (!isSearch) {
|
||||
<iqser-circle-button
|
||||
(action)="executeAction()"
|
||||
[buttonId]="actionButtonId()"
|
||||
[disabled]="!_hasContent()"
|
||||
[icon]="icon"
|
||||
[buttonId]="actionButtonId"
|
||||
[disabled]="!hasContent"
|
||||
[icon]="icon!"
|
||||
></iqser-circle-button>
|
||||
}
|
||||
</form>
|
||||
|
||||
@ -1,39 +1,54 @@
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { randomString } from '../../utils';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { CircleButtonComponent } from '../../buttons/circle-button/circle-button.component';
|
||||
import { randomString } from '../../utils/functions';
|
||||
import { CircleButtonComponent } from '../../buttons';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-input-with-action',
|
||||
templateUrl: './input-with-action.component.html',
|
||||
styleUrls: ['./input-with-action.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [FormsModule, MatIconModule, CircleButtonComponent],
|
||||
})
|
||||
export class InputWithActionComponent {
|
||||
readonly inputId = input(`${randomString() + '-search-input'}`);
|
||||
readonly actionButtonId = input(`${randomString() + '-action-input'}`);
|
||||
readonly placeholder = input('');
|
||||
readonly hint = input<string>();
|
||||
readonly width = input<number | 'full'>(250);
|
||||
protected readonly _computedWidth = computed(() => (this.width() === 'full' ? '100%' : `${this.width()}px`));
|
||||
readonly icon = input<string>();
|
||||
protected readonly _isSearch = computed(() => !this.icon());
|
||||
readonly autocomplete = input<'on' | 'off'>('on');
|
||||
readonly value = model('');
|
||||
protected readonly _hasContent = computed(() => !!this.value()?.length);
|
||||
readonly disabled = input(false, { transform: booleanAttribute });
|
||||
readonly action = output<string>();
|
||||
@Input() inputId = `${randomString() + '-search-input'}`;
|
||||
@Input() actionButtonId = `${randomString() + '-action-input'}`;
|
||||
@Input() placeholder = '';
|
||||
@Input() hint?: string;
|
||||
@Input() width: number | 'full' = 250;
|
||||
@Input() icon?: string;
|
||||
@Input() autocomplete: 'on' | 'off' = 'on';
|
||||
@Input() value = '';
|
||||
@Input() disabled = false;
|
||||
@Output() readonly action = new EventEmitter<string>();
|
||||
@Output() readonly valueChange = new EventEmitter<string>();
|
||||
|
||||
reset() {
|
||||
this.value.set('');
|
||||
get hasContent(): boolean {
|
||||
return !!this.value?.length;
|
||||
}
|
||||
|
||||
executeAction() {
|
||||
if (this._hasContent()) {
|
||||
this.action.emit(this.value());
|
||||
get computedWidth(): string {
|
||||
return this.width === 'full' ? '100%' : `${this.width}px`;
|
||||
}
|
||||
|
||||
get isSearch(): boolean {
|
||||
return !this.icon;
|
||||
}
|
||||
|
||||
constructor(private readonly _changeDetectorRef: ChangeDetectorRef) {}
|
||||
|
||||
reset(): void {
|
||||
this.value = '';
|
||||
this.valueChange.emit(this.value);
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
executeAction(): void {
|
||||
if (this.hasContent) {
|
||||
this.action.emit(this.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
<div
|
||||
#wrapper
|
||||
[class.active]="active() && !indeterminate()"
|
||||
[class.inactive]="!active() && !indeterminate()"
|
||||
[class.indeterminate]="indeterminate()"
|
||||
[class.with-bg]="type() === 'with-bg'"
|
||||
[class.active]="active && !indeterminate"
|
||||
[class.inactive]="!active && !indeterminate"
|
||||
[class.indeterminate]="indeterminate"
|
||||
[class.with-bg]="type === 'with-bg'"
|
||||
class="wrapper"
|
||||
>
|
||||
@if (active() && !indeterminate()) {
|
||||
@if (active && !indeterminate) {
|
||||
<mat-icon svgIcon="iqser:radio-selected"></mat-icon>
|
||||
}
|
||||
|
||||
@if (indeterminate()) {
|
||||
@if (indeterminate) {
|
||||
<mat-icon svgIcon="iqser:radio-indeterminate"></mat-icon>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, effect, ElementRef, input, numberAttribute, viewChild } from '@angular/core';
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
@Component({
|
||||
@ -6,19 +6,21 @@ 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 {
|
||||
protected readonly _wrapper = viewChild.required('wrapper', { read: ElementRef });
|
||||
readonly size = input(20, { transform: numberAttribute });
|
||||
readonly active = input(false, { transform: booleanAttribute });
|
||||
readonly indeterminate = input(false, { transform: booleanAttribute });
|
||||
readonly type = input<'default' | 'with-bg'>('default');
|
||||
readonly disabled = input(false, { transform: booleanAttribute });
|
||||
export class RoundCheckboxComponent implements OnInit {
|
||||
@Input() size = 20;
|
||||
@Input({ transform: booleanAttribute }) active = false;
|
||||
@Input() indeterminate = false;
|
||||
@Input() type: 'default' | 'with-bg' = 'default';
|
||||
@HostBinding('class.disabled')
|
||||
@Input()
|
||||
disabled = false;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
(this._wrapper().nativeElement as HTMLElement).style.setProperty('--size', `${this.size()}px`);
|
||||
});
|
||||
@ViewChild('wrapper', { static: true }) private readonly _wrapper!: ElementRef;
|
||||
|
||||
ngOnInit(): void {
|
||||
(this._wrapper.nativeElement as HTMLElement).style.setProperty('--size', `${this.size}px`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,8 +10,7 @@ import { CircleButtonComponent, IconButtonComponent } from '../buttons';
|
||||
import { HasScrollbarDirective, SyncWidthDirective } from '../directives';
|
||||
import { EmptyStateComponent } from '../empty-state';
|
||||
import { IqserFiltersModule, PopupFilterComponent } from '../filtering';
|
||||
import { InputWithActionComponent } from '../inputs/input-with-action/input-with-action.component';
|
||||
import { RoundCheckboxComponent } from '../inputs/round-checkbox/round-checkbox.component';
|
||||
import { InputWithActionComponent, RoundCheckboxComponent } from '../inputs';
|
||||
import { SnakeCasePipe } from '../pipes/snake-case.pipe';
|
||||
import { PageHeaderComponent } from './page-header/page-header.component';
|
||||
import { ScrollButtonComponent } from './scroll-button/scroll-button.component';
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
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,5 +3,4 @@ import { ActionConfig } from './action-config.model';
|
||||
|
||||
export interface ButtonConfig extends ActionConfig {
|
||||
readonly type?: IconButtonType;
|
||||
readonly tooltip?: string;
|
||||
}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
<div class="page-header">
|
||||
@if (pageLabel()) {
|
||||
<div class="breadcrumb">{{ pageLabel() }}</div>
|
||||
@if (pageLabel) {
|
||||
<div class="breadcrumb">{{ pageLabel }}</div>
|
||||
}
|
||||
|
||||
@if (filters$ | async; as filters) {
|
||||
<div class="filters">
|
||||
<ng-content select="[slot=beforeFilters]"></ng-content>
|
||||
@if (filters.length && searchPosition() !== searchPositions.beforeFilters) {
|
||||
@if (filters.length && searchPosition !== searchPositions.beforeFilters) {
|
||||
<div class="text-muted" translate="filters.filter-by"></div>
|
||||
}
|
||||
@if (searchPosition() === searchPositions.beforeFilters) {
|
||||
@if (searchPosition === searchPositions.beforeFilters) {
|
||||
<ng-container [ngTemplateOutlet]="searchBar"></ng-container>
|
||||
}
|
||||
@for (filter of filters; track trackByLabel($index, filter)) {
|
||||
@if (!filter.hide) {
|
||||
<iqser-popup-filter [primaryFiltersSlug]="filter.slug" [attr.help-mode-key]="filterHelpModeKey()"></iqser-popup-filter>
|
||||
<iqser-popup-filter [primaryFiltersSlug]="filter.slug" [attr.help-mode-key]="filterHelpModeKey"></iqser-popup-filter>
|
||||
}
|
||||
}
|
||||
@for (filter$ of filterService.singleFilters; track filter$) {
|
||||
@ -22,13 +22,13 @@
|
||||
<iqser-single-filter [filter]="filter"></iqser-single-filter>
|
||||
}
|
||||
}
|
||||
@if (searchPosition() === searchPositions.afterFilters) {
|
||||
@if (searchPosition === searchPositions.afterFilters) {
|
||||
<ng-container [ngTemplateOutlet]="searchBar"></ng-container>
|
||||
}
|
||||
@if (!hideResetButton() && (showResetFilters$ | async) === true) {
|
||||
@if (!hideResetButton && (showResetFilters$ | async) === true) {
|
||||
<div
|
||||
(click)="resetFilters()"
|
||||
[attr.help-mode-key]="'filter_' + helpModeKey() + '_list'"
|
||||
[attr.help-mode-key]="'filter_' + helpModeKey + '_list'"
|
||||
class="reset-filters"
|
||||
translate="reset-filters"
|
||||
></div>
|
||||
@ -36,13 +36,13 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showCloseButton() || actionConfigs() || buttonConfigs() || viewModeSelection()) {
|
||||
@if (showCloseButton || actionConfigs || buttonConfigs || viewModeSelection) {
|
||||
<div class="actions">
|
||||
@if (searchPosition() === searchPositions.withActions) {
|
||||
@if (searchPosition === searchPositions.withActions) {
|
||||
<ng-container [ngTemplateOutlet]="searchBar"></ng-container>
|
||||
}
|
||||
<ng-container [ngTemplateOutlet]="viewModeSelection()"></ng-container>
|
||||
@for (config of buttonConfigs(); track trackByLabel($index, config)) {
|
||||
<ng-container [ngTemplateOutlet]="viewModeSelection"></ng-container>
|
||||
@for (config of buttonConfigs; track trackByLabel($index, config)) {
|
||||
@if (!config.hide) {
|
||||
<iqser-icon-button
|
||||
(action)="config.action($event)"
|
||||
@ -50,34 +50,30 @@
|
||||
[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>
|
||||
}
|
||||
}
|
||||
<div class="actions">
|
||||
@for (config of actionConfigs(); track trackByLabel($index, config)) {
|
||||
@for (config of actionConfigs; track trackByLabel($index, config)) {
|
||||
@if (!config.hide) {
|
||||
<iqser-circle-button
|
||||
(action)="config.action($event)"
|
||||
[buttonId]="config.id"
|
||||
[disabled]="config.disabled"
|
||||
[disabled]="config.disabled$ && (config.disabled$ | async)"
|
||||
[icon]="config.icon"
|
||||
[tooltip]="config.label | translate"
|
||||
[tooltip]="config.label"
|
||||
[attr.help-mode-key]="config.helpModeKey"
|
||||
[iqserDisableStopPropagation]="config.disableStopPropagation"
|
||||
></iqser-circle-button>
|
||||
}
|
||||
}
|
||||
<!-- Extra custom actions here -->
|
||||
<ng-content select="[slot=right]"></ng-content>
|
||||
@if (showCloseButton()) {
|
||||
@if (showCloseButton) {
|
||||
<iqser-circle-button
|
||||
buttonId="close-view-btn"
|
||||
(action)="closeAction.emit()"
|
||||
[class.ml-6]="actionConfigs()"
|
||||
[class.ml-6]="actionConfigs"
|
||||
[icon]="'iqser:close'"
|
||||
[attr.help-mode-key]="'close_dossier'"
|
||||
[tooltip]="'common.close' | translate"
|
||||
@ -89,14 +85,14 @@
|
||||
</div>
|
||||
|
||||
<ng-template #searchBar>
|
||||
@if (searchPlaceholder() && searchService) {
|
||||
@if (searchPlaceholder && searchService) {
|
||||
<iqser-input-with-action
|
||||
[inputId]="searchInputId()"
|
||||
[inputId]="searchInputId"
|
||||
(valueChange)="searchService.searchValue = $event"
|
||||
[class.mr-8]="searchPosition() === searchPositions.beforeFilters"
|
||||
[placeholder]="searchPlaceholder()"
|
||||
[class.mr-8]="searchPosition === searchPositions.beforeFilters"
|
||||
[placeholder]="searchPlaceholder"
|
||||
[value]="searchService.valueChanges$ | async"
|
||||
[width]="searchWidth()"
|
||||
[width]="searchWidth"
|
||||
></iqser-input-with-action>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@ -1,28 +1,27 @@
|
||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, input, output, TemplateRef } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, Output, TemplateRef } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { combineLatest, Observable, of } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { CircleButtonComponent } from '../../buttons';
|
||||
import { IconButtonComponent } from '../../buttons';
|
||||
import { IconButtonTypes } from '../../buttons';
|
||||
import { FilterService } from '../../filtering';
|
||||
import { IqserFiltersModule } from '../../filtering';
|
||||
import { PopupFilterComponent } from '../../filtering';
|
||||
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 { FilterService } from '../../filtering/filter.service';
|
||||
import { IqserFiltersModule } from '../../filtering/filters.module';
|
||||
import { PopupFilterComponent } from '../../filtering/popup-filter/popup-filter.component';
|
||||
import { InputWithActionComponent } from '../../inputs/input-with-action/input-with-action.component';
|
||||
import { SearchService } from '../../search';
|
||||
import { filterEach } from '../../utils';
|
||||
import { List } from '../../utils';
|
||||
import { IListable } from '../models';
|
||||
import { SearchService } from '../../search/search.service';
|
||||
import { filterEach } from '../../utils/operators';
|
||||
import { List } from '../../utils/types/iqser-types';
|
||||
import { IListable } from '../models/listable';
|
||||
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,
|
||||
@ -32,36 +31,33 @@ import { DisableStopPropagationDirective } from '../../directives';
|
||||
CircleButtonComponent,
|
||||
TranslateModule,
|
||||
InputWithActionComponent,
|
||||
MatTooltip,
|
||||
DisableStopPropagationDirective,
|
||||
],
|
||||
})
|
||||
export class PageHeaderComponent<T extends IListable> {
|
||||
readonly searchPositions = SearchPositions;
|
||||
readonly iconButtonTypes = IconButtonTypes;
|
||||
|
||||
@Input() pageLabel?: string;
|
||||
@Input() searchInputId?: string;
|
||||
@Input() showCloseButton = false;
|
||||
@Input() hideResetButton = false;
|
||||
@Input() actionConfigs?: List<ActionConfig>;
|
||||
@Input() buttonConfigs?: List<ButtonConfig>;
|
||||
@Input() viewModeSelection?: TemplateRef<unknown>;
|
||||
@Input() searchPlaceholder?: string;
|
||||
@Input() searchWidth?: number | 'full';
|
||||
@Input() helpModeKey?: 'dossier' | 'document';
|
||||
@Input() searchPosition: SearchPosition = SearchPositions.afterFilters;
|
||||
@Output() readonly closeAction = new EventEmitter();
|
||||
readonly filterService = inject(FilterService, { optional: true });
|
||||
readonly searchService = inject<SearchService<T>>(SearchService<T>, { optional: true });
|
||||
|
||||
readonly pageLabel = input<string>();
|
||||
readonly searchInputId = input<string>();
|
||||
readonly showCloseButton = input(false);
|
||||
readonly hideResetButton = input(false);
|
||||
readonly actionConfigs = input<List<ActionConfig>>();
|
||||
readonly buttonConfigs = input<List<ButtonConfig>>();
|
||||
readonly viewModeSelection = input<TemplateRef<unknown>>();
|
||||
readonly searchPlaceholder = input<string>();
|
||||
readonly searchWidth = input<number | 'full'>();
|
||||
readonly helpModeKey = input<'dossier' | 'document'>();
|
||||
readonly searchPosition = input<SearchPosition>(SearchPositions.afterFilters);
|
||||
readonly closeAction = output();
|
||||
|
||||
readonly filterHelpModeKey = computed(() =>
|
||||
this.helpModeKey() ? (this.helpModeKey() === 'dossier' ? 'filter_dossier_list' : 'filter_documents') : '',
|
||||
);
|
||||
|
||||
readonly filters$ = this.filterService?.filterGroups$.pipe(filterEach(f => !!f.icon));
|
||||
readonly showResetFilters$ = this.#showResetFilters$;
|
||||
|
||||
get filterHelpModeKey() {
|
||||
return this.helpModeKey ? (this.helpModeKey === 'dossier' ? 'filter_dossier_list' : 'filter_documents') : '';
|
||||
}
|
||||
|
||||
get #showResetFilters$(): Observable<boolean> {
|
||||
if (!this.filterService) {
|
||||
return of(false);
|
||||
|
||||
@ -18,6 +18,7 @@ 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 selectedIds.map(id => this._entitiesService.all.find(a => a.id === id)).filter(a => !!a);
|
||||
return this._entitiesService.all.filter(a => selectedIds.includes(a.id));
|
||||
}
|
||||
|
||||
get selectedIds(): PrimaryKey[] {
|
||||
|
||||
@ -22,14 +22,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type > div {
|
||||
padding: 0 13px 0 10px;
|
||||
}
|
||||
|
||||
&:first-child > div {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
&:last-of-type > div {
|
||||
padding: 0 13px 0 10px;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
min-width: 58px;
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ 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']> {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<!-- 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) {
|
||||
<a
|
||||
<div
|
||||
(mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)"
|
||||
(mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)"
|
||||
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
|
||||
@ -21,12 +21,12 @@
|
||||
>
|
||||
<iqser-table-item
|
||||
(click)="multiSelect(entity, $event)"
|
||||
[entity]="$any(entity)"
|
||||
[entity]="entity"
|
||||
[selectionEnabled]="selectionEnabled"
|
||||
></iqser-table-item>
|
||||
</a>
|
||||
</div>
|
||||
} @else {
|
||||
<a
|
||||
<div
|
||||
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
|
||||
[id]="rowIdPrefix + '-' + ((entity[namePropertyKey] | snakeCase) ?? entity.id)"
|
||||
[ngClass]="getTableItemClasses(entity)"
|
||||
@ -34,10 +34,10 @@
|
||||
>
|
||||
<iqser-table-item
|
||||
(click)="multiSelect(entity, $event)"
|
||||
[entity]="$any(entity)"
|
||||
[entity]="entity"
|
||||
[selectionEnabled]="selectionEnabled"
|
||||
></iqser-table-item>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
:host cdk-virtual-scroll-viewport {
|
||||
height: calc(100vh - 50px - 31px - var(--iqser-top-bar-height) - 50px);
|
||||
overflow-y: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
background-color: var(--iqser-background);
|
||||
@include mixins.scroll-bar;
|
||||
|
||||
@ -10,6 +10,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.has-scrollbar:hover ::ng-deep.cdk-virtual-scroll-content-wrapper {
|
||||
grid-template-columns: var(--gridTemplateColumnsHover);
|
||||
}
|
||||
|
||||
::ng-deep.cdk-virtual-scroll-content-wrapper {
|
||||
grid-template-columns: var(--gridTemplateColumns);
|
||||
display: grid;
|
||||
@ -33,7 +37,6 @@
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:has(iqser-circle-button[aria-expanded='true']),
|
||||
&.help-mode-active {
|
||||
.selection-column iqser-round-checkbox .wrapper {
|
||||
opacity: 1;
|
||||
@ -49,10 +52,6 @@
|
||||
> * > div {
|
||||
background-color: var(--iqser-not-disabled-table-item);
|
||||
}
|
||||
|
||||
.scrollbar-placeholder {
|
||||
background-color: var(--iqser-side-nav);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,6 +64,10 @@
|
||||
right: 0;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.scrollbar-placeholder {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,8 +75,3 @@
|
||||
.display-contents {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
a {
|
||||
display: contents;
|
||||
@include mixins.clear-a;
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ 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,
|
||||
@ -86,7 +87,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
|
||||
getTableItemClasses(entity: Class): Record<string, boolean> {
|
||||
const classes: Record<string, boolean> = {
|
||||
'table-item': true,
|
||||
'cursor-default': !entity.routerLink,
|
||||
pointer: !!entity.routerLink && entity.routerLink.length > 0,
|
||||
};
|
||||
for (const key in this.tableItemClasses) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.tableItemClasses, key)) {
|
||||
|
||||
@ -12,6 +12,7 @@ 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 {
|
||||
@ -33,7 +34,6 @@ export class TableItemComponent<T extends IListable> implements OnChanges {
|
||||
|
||||
toggleEntitySelected($event: MouseEvent, entity: T): void {
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
this.listingService.select(entity, $event.shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div [class.selection-enabled]="selectionEnabled" class="header-item">
|
||||
<div class="header-title">
|
||||
<div [attr.help-mode-key]="helpModeKey" class="header-title">
|
||||
@if (selectionEnabled) {
|
||||
<iqser-round-checkbox
|
||||
(click)="listingService.selectAll()"
|
||||
@ -9,7 +9,7 @@
|
||||
></iqser-round-checkbox>
|
||||
}
|
||||
|
||||
<span [attr.help-mode-key]="helpModeKey" class="all-caps-label">
|
||||
<span class="all-caps-label">
|
||||
{{ tableHeaderLabel | translate: { length: totalSize || (listingService.displayedLength$ | async) } }}
|
||||
@if (listingService.selectedLength$ | async; as selectedItems) {
|
||||
<span> ({{ 'table-header.selected-count' | translate: { count: selectedItems } }}) </span>
|
||||
|
||||
@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component, inject, Input, TemplateRef } from '
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SyncWidthDirective } from '../../directives';
|
||||
import { FilterService, IqserFiltersModule } from '../../filtering';
|
||||
import { RoundCheckboxComponent } from '../../inputs/round-checkbox/round-checkbox.component';
|
||||
import { RoundCheckboxComponent } from '../../inputs';
|
||||
import { Id, IListable, ListingMode, ListingModes, TableColumnConfig } from '../models';
|
||||
import { EntitiesService, ListingService } from '../services';
|
||||
import { TableColumnNameComponent } from '../table-column-name/table-column-name.component';
|
||||
@ -13,6 +13,7 @@ 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,6 +28,7 @@ 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 {
|
||||
@ -86,17 +87,18 @@ export class TableComponent<Class extends IListable<PrimaryKey>, PrimaryKey exte
|
||||
}
|
||||
|
||||
private _setColumnsWidth(element: HTMLElement) {
|
||||
let gridTemplateColumns = '';
|
||||
let gridTemplateColumnsHover = '';
|
||||
if (this.selectionEnabled) {
|
||||
gridTemplateColumns += '30px ';
|
||||
gridTemplateColumnsHover += '30px ';
|
||||
}
|
||||
for (const config of this.tableColumnConfigs) {
|
||||
gridTemplateColumns += `${config.width || '1fr'} `;
|
||||
gridTemplateColumnsHover += `${config.width || '1fr'} `;
|
||||
}
|
||||
gridTemplateColumns += this.emptyColumnWidth || '';
|
||||
gridTemplateColumns = `${gridTemplateColumns} ${SCROLLBAR_WIDTH}px`;
|
||||
gridTemplateColumnsHover += this.emptyColumnWidth || '';
|
||||
const gridTemplateColumns = `${gridTemplateColumnsHover} ${SCROLLBAR_WIDTH}px`;
|
||||
|
||||
element.style.setProperty('--gridTemplateColumns', gridTemplateColumns);
|
||||
element.style.setProperty('--gridTemplateColumnsHover', gridTemplateColumnsHover);
|
||||
}
|
||||
|
||||
private _setItemSize(element: HTMLElement) {
|
||||
|
||||
@ -14,13 +14,11 @@ import {
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { filter, map, tap } from 'rxjs/operators';
|
||||
import { CircleButtonComponent } from '../../../buttons/circle-button/circle-button.component';
|
||||
import { CircleButtonTypes } from '../../../buttons/types/circle-button.type';
|
||||
import { RoundCheckboxComponent } from '../../../inputs/round-checkbox/round-checkbox.component';
|
||||
import { ContextComponent } from '../../../utils/context.component';
|
||||
import { Debounce } from '../../../utils/decorators/debounce.decorator';
|
||||
import { IListable } from '../../models/listable';
|
||||
import { ListingService } from '../../services/listing.service';
|
||||
import { CircleButtonComponent, CircleButtonTypes } from '../../../buttons';
|
||||
import { RoundCheckboxComponent } from '../../../inputs';
|
||||
import { ContextComponent, Debounce } from '../../../utils';
|
||||
import { IListable } from '../../models';
|
||||
import { ListingService } from '../../services';
|
||||
import { WorkflowColumn } from '../models/workflow-column.model';
|
||||
|
||||
interface ColumnHeaderContext {
|
||||
@ -36,6 +34,7 @@ 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 @@
|
||||
@use '../../../assets/styles/common-variables';
|
||||
@use '../../../assets/styles/common-mixins' as mixins;
|
||||
@import '../../../assets/styles/common-variables';
|
||||
@import '../../../assets/styles/common-mixins';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
.cdk-drop-list {
|
||||
overflow-y: auto;
|
||||
@include mixins.no-scroll-bar;
|
||||
@include no-scroll-bar;
|
||||
min-height: calc(100% - 36px);
|
||||
|
||||
&.multi-select-active {
|
||||
@ -135,5 +135,5 @@
|
||||
|
||||
cdk-virtual-scroll-viewport {
|
||||
height: 100%;
|
||||
@include mixins.no-scroll-bar;
|
||||
@include no-scroll-bar;
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ interface WorkflowContext<T> {
|
||||
templateUrl: './workflow.component.html',
|
||||
styleUrls: ['./workflow.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
TableHeaderComponent,
|
||||
AsyncPipe,
|
||||
|
||||
@ -5,7 +5,6 @@ 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,16 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit, Optional } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { FilterService, INestedFilter } from '../../filtering';
|
||||
import { get, shareLast } from '../../utils';
|
||||
import { ChangeDetectionStrategy, Component, Input, Optional, OnInit } from '@angular/core';
|
||||
import { ProgressBarConfigModel } from './progress-bar-config.model';
|
||||
import { FilterService, INestedFilter } from '../../filtering';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { get, shareLast } from '../../utils';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
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,7 +6,6 @@ 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,6 +7,7 @@ import { PaginationSettings } from './pagination-settings';
|
||||
selector: 'iqser-pagination',
|
||||
templateUrl: './pagination.component.html',
|
||||
styleUrls: ['./pagination.component.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TranslateModule],
|
||||
})
|
||||
|
||||
@ -6,6 +6,7 @@ import { assertTemplate, IqserPermissionsDirective } from './permissions.directi
|
||||
|
||||
@Directive({
|
||||
selector: '[allow]',
|
||||
standalone: true,
|
||||
})
|
||||
export class IqserAllowDirective extends IqserPermissionsDirective implements OnDestroy, OnInit {
|
||||
/**
|
||||
|
||||
@ -6,6 +6,7 @@ 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,6 +3,7 @@ import { capitalize } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'capitalize',
|
||||
standalone: true,
|
||||
})
|
||||
export class CapitalizePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
|
||||
@ -3,6 +3,7 @@ import { humanizeCamelCase } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'humanizeCamelCase',
|
||||
standalone: true,
|
||||
})
|
||||
export class HumanizeCamelCasePipe implements PipeTransform {
|
||||
transform(item: string): string {
|
||||
|
||||
@ -3,6 +3,7 @@ import { humanize } from '../utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'humanize',
|
||||
standalone: true,
|
||||
})
|
||||
export class HumanizePipe implements PipeTransform {
|
||||
transform(item: string, lowercase = false): string {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user