Merge branch 'master' into VM/RED-6240

This commit is contained in:
Valentin Mihai 2023-03-28 11:43:53 +03:00
commit 0eefb3efca
229 changed files with 3690 additions and 2377 deletions

7
.eslintignore Normal file
View File

@ -0,0 +1,7 @@
.angular
.dev
.husky
dist
coverage
node_modules
bamboo-specs

View File

@ -45,12 +45,14 @@
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/recommended--extra",
"plugin:@angular-eslint/template/process-inline-templates",
"plugin:prettier/recommended"
"plugin:prettier/recommended",
"plugin:rxjs/recommended"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"rxjs/no-ignored-subscription": "error",
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",

View File

@ -1,5 +1,7 @@
# Add files here to ignore them from prettier formatting
/.angular
/.dev
/.husky
/dist
/coverage
/node_modules

View File

@ -0,0 +1,26 @@
import type { Config } from 'jest';
import { defaults } from 'jest-config';
export default {
...defaults,
displayName: 'red-ui',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['jest-preset-angular/setup-jest.js', 'jest-extended/all'],
coverageDirectory: '../../coverage/apps/angular-jest',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '../../tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
testEnvironment: 'jest-environment-jsdom',
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
} as Config;

View File

@ -146,6 +146,7 @@ const routes: IqserRoutes = [
},
{
path: 'downloads',
// TODO: transform into a lazy loaded module
component: DownloadsListScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
@ -161,7 +162,7 @@ const routes: IqserRoutes = [
loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
routeGuards: [IqserAuthGuard, RedRoleGuard, DossiersGuard],
permissions: {
allow: [ROLES.search],
redirectTo: '/auth-error',
@ -224,7 +225,7 @@ const routes: IqserRoutes = [
@NgModule({
imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled' })],
providers: [{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }],
providers: [{ provide: RouteReuseStrategy, useExisting: CustomRouteReuseStrategy }],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@ -1,4 +1,4 @@
import { Component, Inject, Renderer2, ViewContainerRef } from '@angular/core';
import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core';
import { RouterHistoryService } from '@services/router-history.service';
import { REDDocumentViewer } from './modules/pdf-viewer/services/document-viewer.service';
import { DossiersChangesService } from '@services/dossiers/dossier-changes.service';
@ -7,6 +7,8 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { getConfig, IqserPermissionsService } from '@iqser/common-ui';
import { ROLES } from '@users/roles';
import { AppConfig } from '@red/domain';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs';
function loadCustomTheme() {
const cssFileName = getConfig<AppConfig>().THEME;
@ -27,24 +29,49 @@ function loadCustomTheme() {
selector: 'redaction-root',
templateUrl: './app.component.html',
})
export class AppComponent {
export class AppComponent implements OnDestroy {
readonly #subscription = new Subscription();
constructor(
/** ViewContainerRef needs to be injected for the color picker to work */
readonly viewContainerRef: ViewContainerRef,
/** RouterHistoryService needs to be injected for last dossiers screen to be updated on first app load */
private readonly _routerHistoryService: RouterHistoryService,
private readonly _userPreferenceService: UserPreferenceService,
userPreferenceService: UserPreferenceService,
readonly documentViewer: REDDocumentViewer,
private readonly _dossierChangesService: DossiersChangesService,
@Inject(DOCUMENT) private readonly _document: Document,
private readonly _renderer: Renderer2,
private readonly _permissionsService: IqserPermissionsService,
dossierChangesService: DossiersChangesService,
@Inject(DOCUMENT) document: Document,
renderer: Renderer2,
permissionsService: IqserPermissionsService,
private readonly _router: Router,
route: ActivatedRoute,
) {
this._renderer.addClass(this._document.body, _userPreferenceService.getTheme());
renderer.addClass(document.body, userPreferenceService.getTheme());
loadCustomTheme();
// TODO: Find a better place to initialize dossiers refresh
if (_permissionsService.has(ROLES.dossiers.read)) {
_dossierChangesService.initializeRefresh();
if (permissionsService.has(ROLES.dossiers.read)) {
const refreshSub = dossierChangesService.initializeRefresh().subscribe();
this.#subscription.add(refreshSub);
}
const sub = route.queryParamMap.subscribe(queryParams => this.#navigate(queryParams));
this.#subscription.add(sub);
}
ngOnDestroy() {
this.#subscription.unsubscribe();
}
#navigate(queryParams: ParamMap) {
if (queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) {
return this._router.navigate([], {
queryParams: {
state: null,
session_state: null,
code: null,
},
queryParamsHandling: 'merge',
});
}
}
}

View File

@ -1,7 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ActivatedRoute, Router } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
@ -9,18 +8,27 @@ import { MissingTranslationHandler } from '@ngx-translate/core';
import {
BASE_HREF,
CachingModule,
CircleButtonComponent,
CommonUiModule,
EmptyStateComponent,
HiddenActionDirective,
InputWithActionComponent,
IqserAllowDirective,
IqserDenyDirective,
IqserHelpModeModule,
IqserListingModule,
IqserLoadingModule,
IqserPermissionsModule,
IqserPermissionsService,
IqserSharedModule,
IqserTranslateModule,
IqserUsersModule,
LanguageService,
LogoComponent,
MAX_RETRIES_ON_SERVER_ERROR,
RoundCheckboxComponent,
SERVER_ERROR_SKIP_PATHS,
ServerErrorInterceptor,
SkeletonComponent,
StopPropagationDirective,
ToastComponent,
} from '@iqser/common-ui';
import { ToastrModule } from 'ngx-toastr';
@ -101,7 +109,6 @@ export const appModuleFactory = (config: AppConfig) => {
existingUserService: UserService,
existingRoleGuard: RedRoleGuard,
}),
IqserSharedModule,
CachingModule.forRoot(UI_CACHES),
IqserHelpModeModule.forRoot(links),
PdfViewerModule,
@ -114,7 +121,6 @@ export const appModuleFactory = (config: AppConfig) => {
}),
IqserTranslateModule.forRoot({ pathPrefix: config.BASE_TRANSLATIONS_DIRECTORY || '/assets/i18n/redact/' }),
IqserLoadingModule.forRoot(),
IqserPermissionsModule.forRoot(),
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
LoggerModule.forRoot(undefined, {
ruleProvider: {
@ -125,7 +131,7 @@ export const appModuleFactory = (config: AppConfig) => {
provide: TOKEN_LOGGER_CONFIG,
useValue: {
level: environment.production ? NgxLoggerLevel.ERROR : NgxLoggerLevel.DEBUG,
enableSourceMaps: true,
enableSourceMaps: false,
timestampFormat: 'mm:ss:SSS',
disableFileDetails: true,
features: {
@ -138,7 +144,7 @@ export const appModuleFactory = (config: AppConfig) => {
enabled: false,
},
PDF: {
enabled: true,
enabled: false,
},
FILE: {
enabled: false,
@ -153,6 +159,17 @@ export const appModuleFactory = (config: AppConfig) => {
} as ILoggerConfig,
},
}),
CircleButtonComponent,
EmptyStateComponent,
SkeletonComponent,
LogoComponent,
HiddenActionDirective,
StopPropagationDirective,
InputWithActionComponent,
RoundCheckboxComponent,
IqserAllowDirective,
IqserDenyDirective,
IqserListingModule,
],
providers: [
{
@ -223,26 +240,7 @@ export const appModuleFactory = (config: AppConfig) => {
],
bootstrap: [AppComponent],
})
class AppModule {
constructor(private readonly _router: Router, private readonly _route: ActivatedRoute) {
this._configureKeyCloakRouteHandling();
}
private _configureKeyCloakRouteHandling() {
this._route.queryParamMap.subscribe(queryParams => {
if (queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) {
this._router.navigate([], {
queryParams: {
state: null,
session_state: null,
code: null,
},
queryParamsHandling: 'merge',
});
}
});
}
}
class AppModule {}
return AppModule;
};

View File

@ -11,9 +11,7 @@
<a [matTooltip]="'top-bar.navigation-items.back-to-dashboard' | translate" [routerLink]="['/']" class="logo">
<div [iqserHelpMode]="'home'" class="actions">
<iqser-hidden-action (action)="userPreferenceService.toggleDevFeatures()">
<iqser-logo icon="red:logo"></iqser-logo>
</iqser-hidden-action>
<iqser-logo (iqserHiddenAction)="userPreferenceService.toggleDevFeatures()" icon="red:logo"></iqser-logo>
<div class="app-name">{{ titleService.getTitle() }}</div>
</div>
</a>
@ -29,7 +27,10 @@
<iqser-help-button *deny="roles.getRss" [iqserHelpMode]="'help_mode'" id="help-mode-button"></iqser-help-button>
<redaction-notifications [iqserHelpMode]="'open_notifications'"></redaction-notifications>
<redaction-notifications
*ngIf="currentUser.isUser || currentUser.isManager"
[iqserHelpMode]="'open_notifications'"
></redaction-notifications>
</div>
<iqser-user-button [iqserHelpMode]="'open_usermenu'" [matMenuTriggerFor]="userMenu" id="userMenu"></iqser-user-button>

View File

@ -5,11 +5,11 @@
<div class="content-container">
<iqser-table
[bulkActions]="bulkActions"
[headerHelpModeKey]="'my_downloads'"
[itemSize]="80"
[noDataText]="'downloads-list.no-data.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[headerHelpModeKey]="'my_downloads'"
noDataIcon="iqser:download"
></iqser-table>
</div>
@ -26,6 +26,7 @@
></iqser-circle-button>
</ng-template>
<!--TODO: move to a separate component-->
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as download">
<div class="cell">
@ -42,7 +43,7 @@
<div class="cell">
<div class="small-label">
{{ download.creationDate | date: 'd MMM yyyy, hh:mm a' }}
{{ download.creationDate | date : 'd MMM yyyy, hh:mm a' }}
</div>
</div>

View File

@ -1,4 +1,9 @@
<iqser-circle-button [matMenuTriggerFor]="menu" [showDot]="hasUnreadNotifications$ | async" icon="red:notification"></iqser-circle-button>
<iqser-circle-button
[matMenuTriggerFor]="menu"
[showDot]="hasUnreadNotifications$ | async"
buttonId="notification-button"
icon="red:notification"
></iqser-circle-button>
<mat-menu #menu="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before">
<ng-template matMenuContent>
@ -13,17 +18,25 @@
<div *ngFor="let group of groups; let first = first">
<div class="all-caps-label flex-align-items-center">
<div>{{ group.date }}</div>
<div (click)="markRead($event)" *ngIf="(hasUnreadNotifications$ | async) && first" class="view-all">
<div
(click)="markRead()"
*ngIf="(hasUnreadNotifications$ | async) && first"
class="view-all"
id="notifications-mark-all-as-read-btn"
stopPropagation
>
{{ 'notifications.mark-all-as-read' | translate }}
</div>
</div>
<div
(click)="markRead($event, [notification], true)"
*ngFor="let notification of group.notifications"
(click)="markRead([notification], true)"
*ngFor="let notification of group.notifications; trackBy: trackBy"
[class.unread]="!notification.readDate"
[id]="'notifications-mark-as-read-' + notification.id + '-btn'"
class="notification"
mat-menu-item
stopPropagation
>
<iqser-initials-avatar [user]="notification.userId"></iqser-initials-avatar>
@ -31,11 +44,14 @@
<div [innerHTML]="notification.message"></div>
<div class="small-label mt-2">{{ notification.creationDate | date : 'exactDate' }}</div>
</div>
<div
(click)="markRead($event, [notification], !notification.readDate)"
(click)="markRead([notification], !notification.readDate)"
[id]="'notifications-mark-' + notification.id"
class="dot"
matTooltip="{{ 'notifications.mark-as' | translate : { type: notification.readDate ? 'unread' : 'read' } }}"
matTooltipPosition="before"
stopPropagation
></div>
</div>
</div>

View File

@ -4,7 +4,7 @@ import { NotificationsService } from '@services/notifications.service';
import { Notification } from '@red/domain';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { isToday, shareLast } from '@iqser/common-ui';
import { isToday, shareLast, trackByFactory } from '@iqser/common-ui';
import dayjs, { Dayjs } from 'dayjs';
import { TranslateService } from '@ngx-translate/core';
@ -29,6 +29,7 @@ function chronologically(first: string, second: string) {
export class NotificationsComponent {
readonly hasUnreadNotifications$: Observable<boolean>;
readonly groupedNotifications$: Observable<NotificationsGroup[]>;
readonly trackBy = trackByFactory();
constructor(
private readonly _notificationsService: NotificationsService,
@ -47,9 +48,7 @@ export class NotificationsComponent {
);
}
async markRead($event, notifications: Notification[] = this._notificationsService.all, isRead = true): Promise<void> {
$event.stopPropagation();
async markRead(notifications: Notification[] = this._notificationsService.all, isRead = true): Promise<void> {
if (!notifications.find(notification => !!notification.readDate !== isRead)) {
// If no notification changes status after the request, abort
return;
@ -71,11 +70,10 @@ export class NotificationsComponent {
return n.creationDate.split('T')[0];
});
const grouped = [...groupedMap.entries()];
const sorted = grouped.sort(([aDate], [bDate]) => chronologically(aDate, bDate));
return sorted.map(([date, _notifications]) => ({
const sortedGroups = [...groupedMap.entries()].sort(([aDate], [bDate]) => chronologically(aDate, bDate));
return sortedGroups.map(([date, _notifications]) => ({
date: isToday(date) ? todayTranslation : this._datePipe.transform(date, 'sophisticatedDate'),
notifications: _notifications.sort((a, b) => chronologically(a.creationDate, b.creationDate)),
notifications: [..._notifications].sort((a, b) => chronologically(a.creationDate, b.creationDate)),
}));
}
}

View File

@ -1,6 +1,6 @@
import { CanDeactivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { map, Observable } from 'rxjs';
import { map } from 'rxjs';
import { ConfirmationDialogService, ConfirmOptions } from '@iqser/common-ui';
export interface ComponentCanDeactivate {
@ -15,23 +15,24 @@ export interface ComponentCanDeactivate {
export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {
constructor(private _dialogService: ConfirmationDialogService) {}
canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
if (component.changed) {
component.isLeavingPage = true;
const dialogRef = this._dialogService.openDialog({ disableConfirm: component.valid === false });
return dialogRef.afterClosed().pipe(
map(result => {
if (result === ConfirmOptions.CONFIRM) {
component.save().then();
} else {
component.discard?.().then();
}
component.isLeavingPage = false;
return !!result;
}),
);
canDeactivate(component: ComponentCanDeactivate) {
if (!component.changed) {
return true;
}
return true;
component.isLeavingPage = true;
const dialogRef = this._dialogService.open({ disableConfirm: component.valid === false });
return dialogRef.afterClosed().pipe(
map(result => {
if (result === ConfirmOptions.CONFIRM) {
component.save().then();
} else {
component.discard?.().then();
}
component.isLeavingPage = false;
return !!result;
}),
);
}
}

View File

@ -6,6 +6,7 @@ import { firstValueFrom } from 'rxjs';
import { DOSSIER_ID, DOSSIER_TEMPLATE_ID } from '@red/domain';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { DossierDictionariesMapService } from '@services/entity-services/dossier-dictionaries-map.service';
@Injectable({ providedIn: 'root' })
export class DossierFilesGuard implements CanActivate {
@ -14,6 +15,7 @@ export class DossierFilesGuard implements CanActivate {
private readonly _filesMapService: FilesMapService,
private readonly _filesService: FilesService,
private readonly _dictionaryService: DictionaryService,
private readonly _dictionaryMapService: DossierDictionariesMapService,
private readonly _router: Router,
) {}
@ -41,8 +43,10 @@ export class DossierFilesGuard implements CanActivate {
async loadDossierData(dossierId: string, dossierTemplateId: string) {
const promises = [];
const dictionary$ = this._dictionaryService.loadDossierDictionary(dossierTemplateId, dossierId);
promises.push(firstValueFrom(dictionary$));
if (!this._dictionaryMapService.has(dossierId)) {
const dictionary$ = this._dictionaryService.loadDossierDictionary(dossierTemplateId, dossierId);
promises.push(firstValueFrom(dictionary$));
}
if (!this._filesMapService.has(dossierId)) {
promises.push(firstValueFrom(this._filesService.loadAll(dossierId)));

View File

@ -6,13 +6,20 @@ import { AccountSideNavComponent } from './account-side-nav/account-side-nav.com
import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component';
import { NotificationPreferencesService } from './services/notification-preferences.service';
import { TranslateModule } from '@ngx-translate/core';
import { IqserSharedModule } from '@iqser/common-ui';
import { IqserHelpModeModule } from '@iqser/common-ui';
import { IconButtonComponent, IqserHelpModeModule, SideNavComponent } from '@iqser/common-ui';
import { PreferencesComponent } from './screens/preferences/preferences.component';
@NgModule({
declarations: [AccountSideNavComponent, BaseAccountScreenComponent, PreferencesComponent],
imports: [CommonModule, SharedModule, AccountRoutingModule, TranslateModule, IqserSharedModule, IqserHelpModeModule],
imports: [
CommonModule,
SharedModule,
AccountRoutingModule,
TranslateModule,
IqserHelpModeModule,
IconButtonComponent,
SideNavComponent,
],
providers: [NotificationPreferencesService],
})
export class AccountModule {}

View File

@ -5,11 +5,12 @@ import { SharedModule } from '@shared/shared.module';
import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { TranslateModule } from '@ngx-translate/core';
import { IconButtonComponent } from '@iqser/common-ui';
const routes = [{ path: '', component: NotificationsScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [NotificationsScreenComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, TranslateModule],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, TranslateModule, IconButtonComponent],
})
export class NotificationsModule {}

View File

@ -86,7 +86,7 @@ export class UserProfileScreenComponent extends BaseFormComponent implements OnI
const value = this.form.getRawValue() as IProfile;
if (this.emailChanged) {
const dialogRef = this._dialogService.openDialog('confirmPassword', null, null);
const dialogRef = this._dialogService.openDialog('confirmPassword');
const password = await firstValueFrom(dialogRef.afterClosed());
if (!password) {
return;

View File

@ -7,12 +7,13 @@ import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { TranslateModule } from '@ngx-translate/core';
import { ConfirmPasswordDialogComponent } from './confirm-password-dialog/confirm-password-dialog.component';
import { UserProfileDialogService } from './services/user-profile-dialog.service';
import { CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
const routes = [{ path: '', component: UserProfileScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [UserProfileScreenComponent, ConfirmPasswordDialogComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, TranslateModule],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, TranslateModule, IconButtonComponent, CircleButtonComponent],
providers: [UserProfileDialogService],
})
export class UserProfileModule {}

View File

@ -3,13 +3,11 @@ import { CompositeRouteGuard, IqserAuthGuard, IqserPermissionsGuard, IqserRoutes
import { RedRoleGuard } from '@users/red-role.guard';
import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { FileAttributesListingScreenComponent } from './screens/file-attributes-listing/file-attributes-listing-screen.component';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { RouterModule } from '@angular/router';
import { DossierAttributesListingScreenComponent } from './screens/dossier-attributes-listing/dossier-attributes-listing-screen.component';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
@ -17,7 +15,6 @@ import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain';
import { DossierTemplateExistsGuard } from '@guards/dossier-template-exists.guard';
import { EntityExistsGuard } from '@guards/entity-exists-guard.service';
import { DossierStatesListingScreenComponent } from './screens/dossier-states-listing/dossier-states-listing-screen.component';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { PermissionsGuard } from '@guards/permissions-guard';
import { ROLES } from '@users/roles';
@ -65,7 +62,9 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
{
path: 'file-attributes',
component: FileAttributesListingScreenComponent,
component: BaseDossierTemplateScreenComponent,
loadChildren: () =>
import('./screens/file-attributes-listing/file-attributes-listing.module').then(m => m.FileAttributesListingModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
@ -90,7 +89,9 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
{
path: 'dossier-attributes',
component: DossierAttributesListingScreenComponent,
component: BaseDossierTemplateScreenComponent,
loadChildren: () =>
import('./screens/dossier-attributes-listing/dossier-attributes-listing.module').then(m => m.DossierAttributesListingModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
@ -98,7 +99,9 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
{
path: 'dossier-states',
component: DossierStatesListingScreenComponent,
component: BaseDossierTemplateScreenComponent,
loadChildren: () =>
import('./screens/dossier-states-listing/dossier-states-listing.module').then(m => m.DossierStatesListingModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],

View File

@ -6,10 +6,8 @@ import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
import { FileAttributesListingScreenComponent } from './screens/file-attributes-listing/file-attributes-listing-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { DossierTemplateBreadcrumbsComponent } from './components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { AddEditFileAttributeDialogComponent } from './dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import { DossierTemplateBreadcrumbsComponent } from './shared/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { AddEditCloneDossierTemplateDialogComponent } from './dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component';
import { AddEntityDialogComponent } from './dialogs/add-entity-dialog/add-entity-dialog.component';
import { EditColorDialogComponent } from './dialogs/edit-color-dialog/edit-color-dialog.component';
@ -18,12 +16,8 @@ import { GeneralConfigScreenComponent } from './screens/general-config/general-c
import { SmtpAuthDialogComponent } from './dialogs/smtp-auth-dialog/smtp-auth-dialog.component';
import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-edit-user-dialog.component';
import { UsersStatsComponent } from './components/users-stats/users-stats.component';
import { FileAttributesCsvImportDialogComponent } from './dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component';
import { ActiveFieldsListingComponent } from './dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component';
import { ResetPasswordComponent } from './dialogs/add-edit-user-dialog/reset-password/reset-password.component';
import { UserDetailsComponent } from './dialogs/add-edit-user-dialog/user-details/user-details.component';
import { AddEditDossierAttributeDialogComponent } from './dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component';
import { DossierAttributesListingScreenComponent } from './screens/dossier-attributes-listing/dossier-attributes-listing-screen.component';
import { AuditService } from './services/audit.service';
import { DigitalSignatureService } from './services/digital-signature.service';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
@ -32,48 +26,43 @@ import { SmtpConfigService } from './services/smtp-config.service';
import { UploadDictionaryDialogComponent } from './dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component';
import { GeneralConfigFormComponent } from './screens/general-config/general-config-form/general-config-form.component';
import { SmtpFormComponent } from './screens/general-config/smtp-form/smtp-form.component';
import { FileAttributesConfigurationsDialogComponent } from './dialogs/file-attributes-configurations-dialog/file-attributes-configurations-dialog.component';
import { SharedAdminModule } from './shared/shared-admin.module';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
import { DossierStatesListingScreenComponent } from './screens/dossier-states-listing/dossier-states-listing-screen.component';
import { AddEditDossierStateDialogComponent } from './dialogs/add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component';
import { A11yModule } from '@angular/cdk/a11y';
import { ConfirmDeleteDossierStateDialogComponent } from './dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { AdminSideNavComponent } from './admin-side-nav/admin-side-nav.component';
import { AdminSideNavComponent } from './shared/components/admin-side-nav/admin-side-nav.component';
import { SystemPreferencesFormComponent } from './screens/general-config/system-preferences-form/system-preferences-form.component';
import { ConfigureCertificateDialogComponent } from './dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component';
import { PkcsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component';
import { KmsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component';
import {
ChevronButtonComponent,
CircleButtonComponent,
DetailsRadioComponent,
EditableInputComponent,
EmptyStateComponent,
HasScrollbarDirective,
HumanizePipe,
IqserButtonsModule,
IqserEmptyStatesModule,
IconButtonComponent,
InputWithActionComponent,
IqserAllowDirective,
IqserDenyDirective,
IqserHelpModeModule,
IqserInputsModule,
IqserListingModule,
IqserPermissionsModule,
IqserScrollbarModule,
IqserSharedModule,
IqserUploadFileModule,
IqserUsersModule,
RoundCheckboxComponent,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { AuditInfoDialogComponent } from './dialogs/audit-info-dialog/audit-info-dialog.component';
import { DossierTemplateActionsComponent } from './shared/components/dossier-template-actions/dossier-template-actions.component';
const dialogs = [
AddEditCloneDossierTemplateDialogComponent,
AddEntityDialogComponent,
AddEditFileAttributeDialogComponent,
EditColorDialogComponent,
SmtpAuthDialogComponent,
AddEditUserDialogComponent,
FileAttributesConfigurationsDialogComponent,
FileAttributesCsvImportDialogComponent,
AddEditDossierAttributeDialogComponent,
UploadDictionaryDialogComponent,
AddEditDossierStateDialogComponent,
ConfirmDeleteDossierStateDialogComponent,
ConfigureCertificateDialogComponent,
AuditInfoDialogComponent,
];
@ -83,18 +72,12 @@ const screens = [
DefaultColorsScreenComponent,
EntitiesListingScreenComponent,
DigitalSignatureScreenComponent,
FileAttributesListingScreenComponent,
UserListingScreenComponent,
GeneralConfigScreenComponent,
DossierAttributesListingScreenComponent,
DossierStatesListingScreenComponent,
];
const components = [
DossierTemplateBreadcrumbsComponent,
UsersStatsComponent,
AdminSideNavComponent,
ActiveFieldsListingComponent,
ResetPasswordComponent,
UserDetailsComponent,
BaseAdminScreenComponent,
@ -117,20 +100,27 @@ const components = [
CommonModule,
SharedModule,
AdminRoutingModule,
SharedAdminModule,
A11yModule,
IqserUsersModule,
TranslateModule,
HumanizePipe,
IqserButtonsModule,
IqserListingModule,
IqserScrollbarModule,
IqserInputsModule,
IqserUploadFileModule,
IqserEmptyStatesModule,
IqserSharedModule,
IqserHelpModeModule,
IqserPermissionsModule,
AdminSideNavComponent,
DossierTemplateActionsComponent,
DossierTemplateBreadcrumbsComponent,
IconButtonComponent,
CircleButtonComponent,
ChevronButtonComponent,
EmptyStateComponent,
HasScrollbarDirective,
RoundCheckboxComponent,
InputWithActionComponent,
EditableInputComponent,
DetailsRadioComponent,
IqserAllowDirective,
IqserDenyDirective,
],
})
export class AdminModule {}

View File

@ -10,6 +10,7 @@
<iqser-circle-button
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
buttonId="close-view-btn"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>

View File

@ -17,8 +17,8 @@ import { PermissionsService } from '@services/permissions.service';
export class BaseEntityScreenComponent {
readonly disabledItems$: Observable<string[]>;
readonly canDeleteEntity$: Observable<boolean>;
readonly #dossierTemplateId: string = getParam(DOSSIER_TEMPLATE_ID);
readonly #entityType: string = getParam(ENTITY_TYPE);
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly #entityType = getParam(ENTITY_TYPE);
constructor(
private readonly _router: Router,
@ -37,7 +37,7 @@ export class BaseEntityScreenComponent {
}
openDeleteDictionariesDialog() {
this._dialogService.openDialog('confirm', null, null, async () => {
this._dialogService.openDialog('confirm', null, async () => {
this._loadingService.start();
const dossierTemplate = this._dossierTemplatesService.find(this.#dossierTemplateId);
await firstValueFrom(this._dictionaryService.deleteDictionaries([this.#entityType], this.#dossierTemplateId));

View File

@ -103,7 +103,7 @@ export class UserDetailsComponent extends BaseFormComponent implements OnChanges
}
delete() {
this._dialogService.deleteUsers([this.user.id], null, () => this.closeDialog.emit(true));
this._dialogService.deleteUsers([this.user.id], () => this.closeDialog.emit(true));
}
setRolesRequirements(checked: boolean, role: string): void {

View File

@ -27,8 +27,12 @@
<form [formGroup]="form">
<div class="iqser-input-group w-150 mr-20">
<mat-form-field>
<mat-select (selectionChange)="filterChange()" formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category">
<mat-select (selectionChange)="filterChange()" formControlName="category" id="select-category">
<mat-option
*ngFor="let category of categories"
[value]="category"
[id]="'select-category-' + category.toLocaleLowerCase()"
>
{{ (translations[category] | translate) || category }}
</mat-option>
</mat-select>
@ -37,7 +41,7 @@
<div class="iqser-input-group w-150">
<mat-form-field>
<mat-select (selectionChange)="filterChange()" formControlName="userId">
<mat-select (selectionChange)="filterChange()" formControlName="userId" id="select-users">
<mat-select-trigger>
<iqser-initials-avatar
*ngIf="form.get('userId').value !== ALL_USERS"
@ -48,7 +52,7 @@
<div *ngIf="form.get('userId').value === ALL_USERS" [translate]="ALL_USERS"></div>
</mat-select-trigger>
<mat-option *ngFor="let userId of userIds" [value]="userId">
<mat-option *ngFor="let userId of userIds" [value]="userId" [id]="'select-user-' + userId">
<iqser-initials-avatar *ngIf="userId !== ALL_USERS" [user]="userId" [withName]="true"></iqser-initials-avatar>
<div *ngIf="userId === ALL_USERS" [translate]="ALL_USERS"></div>
@ -60,7 +64,13 @@
<div class="separator">·</div>
<div class="iqser-input-group datepicker-wrapper mr-20">
<input (dateChange)="filterChange()" [matDatepicker]="fromPicker" formControlName="from" placeholder="dd/mm/yy" />
<input
id="start-date-input"
(dateChange)="filterChange()"
[matDatepicker]="fromPicker"
formControlName="from"
placeholder="dd/mm/yy"
/>
<mat-datepicker-toggle [for]="fromPicker" matSuffix>
<mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon>
</mat-datepicker-toggle>
@ -70,7 +80,13 @@
<div class="mr-20" translate="audit-screen.to"></div>
<div class="iqser-input-group datepicker-wrapper">
<input (dateChange)="filterChange()" [matDatepicker]="toPicker" formControlName="to" placeholder="dd/mm/yy" />
<input
id="end-date-input"
(dateChange)="filterChange()"
[matDatepicker]="toPicker"
formControlName="to"
placeholder="dd/mm/yy"
/>
<mat-datepicker-toggle [for]="toPicker" matSuffix>
<mat-icon matDatepickerToggleIcon svgIcon="red:calendar"></mat-icon>
</mat-datepicker-toggle>
@ -99,7 +115,7 @@
<div class="cell">
<div class="action-buttons">
<iqser-circle-button
(action)="openAuditDetails($event, log)"
(action)="openAuditDetails(log)"
*ngIf="log.hasDetails"
[tooltip]="'audit-screen.action.info' | translate"
[type]="circleButtonTypes.dark"

View File

@ -1,5 +1,11 @@
:host ::ng-deep iqser-table iqser-table-header .header-item {
justify-content: space-between;
:host ::ng-deep {
iqser-table iqser-table-header .header-item {
justify-content: space-between;
}
.mat-mdc-form-field .mat-mdc-text-field-wrapper {
border-radius: 10px;
}
}
form {

View File

@ -83,6 +83,10 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnI
}
}
openAuditDetails(log: Audit) {
this._dialogService.openDialog('auditInfo', { auditEntry: log });
}
private _getForm(): UntypedFormGroup {
return this._formBuilder.group({
category: [this.ALL_CATEGORIES],
@ -136,8 +140,4 @@ export class AuditScreenComponent extends ListingComponent<Audit> implements OnI
}
this._loadingService.stop();
}
openAuditDetails($event: MouseEvent, log: Audit) {
this._dialogService.openDialog('auditInfo', $event, { auditEntry: log });
}
}

View File

@ -38,7 +38,7 @@
<div class="cell">
<div class="action-buttons">
<iqser-circle-button
(action)="openEditColorDialog($event, entity)"
(action)="openEditColorDialog(entity)"
*allow="roles.colors.write; if: currentUser.isAdmin"
[iqserHelpMode]="'default_colors'"
[overlappingElements]="['USER_MENU']"

View File

@ -53,7 +53,7 @@ export class DefaultColorsScreenComponent extends ListingComponent<ListItem> {
);
}
openEditColorDialog($event: MouseEvent, color: ListItem) {
this._dialogService.openDialog('editColor', $event, { colorKey: color.key, dossierTemplateId: this.#dossierTemplateId });
openEditColorDialog(color: ListItem) {
this._dialogService.openDialog('editColor', { colorKey: color.key, dossierTemplateId: this.#dossierTemplateId });
}
}

View File

@ -81,7 +81,7 @@ export class DigitalSignatureScreenComponent implements OnInit {
}
openConfigureCertificate(): void {
const dialogRef = this._dialogService.openDialog('configureCertificate', null, null);
const dialogRef = this._dialogService.openDialog('configureCertificate');
firstValueFrom(dialogRef.afterClosed()).then(async res => {
if (res) {
await this.loadDigitalSignature();

View File

@ -0,0 +1,5 @@
@use 'common-mixins';
.dialog-header {
@include common-mixins.line-clamp(1);
}

View File

@ -1,20 +1,36 @@
import { Component, HostListener, Inject, OnDestroy } from '@angular/core';
import { UntypedFormGroup, Validators } from '@angular/forms';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { DossierAttributeConfigTypes, FileAttributeConfigTypes, IDossierAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { BaseDialogComponent, IqserEventTarget } from '@iqser/common-ui';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent, IqserEventTarget } from '@iqser/common-ui';
import { HttpErrorResponse } from '@angular/common/http';
import { DossierAttributesService } from '@services/entity-services/dossier-attributes.service';
import { dossierAttributeTypesTranslations } from '@translations/dossier-attribute-types-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateModule } from '@ngx-translate/core';
import { NgForOf, NgIf } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
export interface AddEditDossierAttributeDialogData {
readonly dossierAttribute: IDossierAttributeConfig;
dossierTemplateId: string;
readonly dossierTemplateId: string;
}
@Component({
templateUrl: './add-edit-dossier-attribute-dialog.component.html',
styleUrls: ['./add-edit-dossier-attribute-dialog.component.scss'],
standalone: true,
imports: [
TranslateModule,
ReactiveFormsModule,
NgIf,
MatFormFieldModule,
MatSelectModule,
NgForOf,
IconButtonComponent,
CircleButtonComponent,
],
})
export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent implements OnDestroy {
readonly dossierAttribute = this.data.dossierAttribute;
@ -31,7 +47,7 @@ export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent
@Inject(MAT_DIALOG_DATA) readonly data: AddEditDossierAttributeDialogData,
) {
super(_dialogRef, !!data.dossierAttribute);
this.form = this._getForm(this.dossierAttribute);
this.form = this.#form;
this.initialFormValue = this.form.getRawValue();
}
@ -49,6 +65,20 @@ export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent
return false;
}
get #form() {
const dossierAttribute = this.data.dossierAttribute;
return this._formBuilder.group({
label: [dossierAttribute?.label, Validators.required],
...(!!dossierAttribute && {
placeholder: {
value: dossierAttribute.placeholder,
disabled: true,
},
}),
type: [dossierAttribute?.type || FileAttributeConfigTypes.TEXT, Validators.required],
});
}
async save() {
this._loadingService.start();
@ -77,17 +107,4 @@ export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent
await this.save();
}
}
private _getForm(dossierAttribute: IDossierAttributeConfig): UntypedFormGroup {
return this._formBuilder.group({
label: [dossierAttribute?.label, Validators.required],
...(!!dossierAttribute && {
placeholder: {
value: dossierAttribute.placeholder,
disabled: true,
},
}),
type: [dossierAttribute?.type || FileAttributeConfigTypes.TEXT, Validators.required],
});
}
}

View File

@ -1,42 +1,19 @@
<section>
<div class="page-header">
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>
<iqser-circle-button
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>
</div>
</div>
<div class="content-inner">
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table
(noDataAction)="openAddEditAttributeDialog(null)"
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="50"
[noDataButtonLabel]="'dossier-attributes-listing.no-data.action' | translate"
[noDataText]="'dossier-attributes-listing.no-data.title' | translate"
[noMatchText]="'dossier-attributes-listing.no-match.title' | translate"
[selectionEnabled]="canEditDossierAttributes"
[showNoDataButton]="canEditDossierAttributes"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
</div>
</section>
<div class="content-container">
<iqser-table
(noDataAction)="openAddEditAttributeDialog()"
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="50"
[noDataButtonLabel]="'dossier-attributes-listing.no-data.action' | translate"
[noDataText]="'dossier-attributes-listing.no-data.title' | translate"
[noMatchText]="'dossier-attributes-listing.no-match.title' | translate"
[selectionEnabled]="canEditDossierAttributes"
[showNoDataButton]="canEditDossierAttributes"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<ng-template #bulkActions>
<iqser-circle-button
@ -56,11 +33,11 @@
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddEditAttributeDialog($event)"
(action)="openAddEditAttributeDialog()"
*ngIf="canEditDossierAttributes"
[iqserHelpMode]="'create_new_dossier_attribute'"
[overlappingElements]="['USER_MENU']"
[label]="'dossier-attributes-listing.add-new' | translate"
[overlappingElements]="['USER_MENU']"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
@ -73,6 +50,7 @@
</ul>
</ng-template>
<!--TODO: move to a separate component-->
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as attribute">
<div [matTooltip]="attribute.label" class="cell" matTooltipPosition="above">
@ -91,7 +69,7 @@
<div *ngIf="canEditDossierAttributes" class="action-buttons">
<div [iqserHelpMode]="'edit_delete_dossier_attributes'" [overlappingElements]="['USER_MENU']">
<iqser-circle-button
(action)="openAddEditAttributeDialog($event, attribute)"
(action)="openAddEditAttributeDialog(attribute)"
[tooltip]="'dossier-attributes-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"

View File

@ -1,6 +1,7 @@
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import {
CircleButtonTypes,
defaultDialogConfig,
getCurrentUser,
getParam,
IconButtonTypes,
@ -16,6 +17,11 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DOSSIER_TEMPLATE_ID, DossierAttributeConfig, IDossierAttributeConfig, User } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { PermissionsService } from '@services/permissions.service';
import { MatDialog } from '@angular/material/dialog';
import {
AddEditDossierAttributeDialogComponent,
AddEditDossierAttributeDialogData,
} from './add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component';
@Component({
templateUrl: './dossier-attributes-listing-screen.component.html',
@ -44,40 +50,45 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
readonly permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _dialogService: AdminDialogService,
private readonly _dialog: MatDialog,
private readonly _dossierAttributesService: DossierAttributesService,
) {
super();
}
async ngOnInit(): Promise<void> {
await this._loadData();
await this.#loadData();
}
async openConfirmDeleteAttributeDialog($event: MouseEvent, attributes: DossierAttributeConfig[] = this.listingService.selected) {
await this._dialogService.deleteAttributes(
attributes,
this.#dossierTemplateId,
this.impactedTemplatesRef,
'dossier',
$event,
async () => {
this._loadingService.start();
const ids = attributes.map(a => a.id);
await firstValueFrom(this._dossierAttributesService.delete(ids, this.#dossierTemplateId));
await this._loadData();
},
);
await this._dialogService.deleteAttributes(attributes, this.#dossierTemplateId, this.impactedTemplatesRef, 'dossier', async () => {
this._loadingService.start();
const ids = attributes.map(a => a.id);
await firstValueFrom(this._dossierAttributesService.delete(ids, this.#dossierTemplateId));
await this.#loadData();
});
}
openAddEditAttributeDialog($event: MouseEvent, dossierAttribute?: IDossierAttributeConfig) {
async openAddEditAttributeDialog(dossierAttribute?: IDossierAttributeConfig) {
const dossierTemplateId = this.#dossierTemplateId;
this._dialogService.openDialog('addEditDossierAttribute', $event, { dossierAttribute, dossierTemplateId }, async () =>
this._loadData(),
const ref = this._dialog.open<AddEditDossierAttributeDialogComponent, AddEditDossierAttributeDialogData, boolean>(
AddEditDossierAttributeDialogComponent,
{
...defaultDialogConfig,
autoFocus: true,
data: { dossierAttribute, dossierTemplateId },
},
);
const result = await firstValueFrom(ref.afterClosed());
if (result) {
await this.#loadData();
}
}
private async _loadData() {
async #loadData() {
this._loadingService.start();
await firstValueFrom(this._dossierAttributesService.loadAll(this.#dossierTemplateId));
this._loadingService.stop();

View File

@ -0,0 +1,37 @@
import { NgModule } from '@angular/core';
import { DossierAttributesListingScreenComponent } from './dossier-attributes-listing-screen.component';
import { CommonModule } from '@angular/common';
import {
CircleButtonComponent,
IconButtonComponent,
InputWithActionComponent,
IqserHelpModeModule,
IqserListingModule,
IqserRoutes,
} from '@iqser/common-ui';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatTooltipModule } from '@angular/material/tooltip';
const routes: IqserRoutes = [
{
path: '',
component: DossierAttributesListingScreenComponent,
},
];
@NgModule({
declarations: [DossierAttributesListingScreenComponent],
imports: [
CommonModule,
RouterModule.forChild(routes),
TranslateModule,
CircleButtonComponent,
InputWithActionComponent,
IconButtonComponent,
IqserHelpModeModule,
MatTooltipModule,
IqserListingModule,
],
})
export class DossierAttributesListingModule {}

View File

@ -51,8 +51,7 @@
[label]="'add-edit-dossier-state.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
>
</iqser-icon-button>
></iqser-icon-button>
</div>
</form>

View File

@ -6,8 +6,8 @@ import { DossierStatesService } from '@services/entity-services/dossier-states.s
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
interface DialogData {
readonly dossierState: IDossierState;
export interface AddEditDossierStateDialogData {
readonly dossierState?: IDossierState;
readonly dossierTemplateId: string;
}
@ -25,7 +25,7 @@ export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
constructor(
private readonly _dossierStatesService: DossierStatesService,
protected readonly _dialogRef: MatDialogRef<AddEditDossierStateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
@Inject(MAT_DIALOG_DATA) readonly data: AddEditDossierStateDialogData,
) {
super(_dialogRef, !!data.dossierState);
this.form = this.#getForm();

View File

@ -4,7 +4,7 @@
</div>
<div class="dialog-content">
<div class="heading">{{ 'confirm-delete-dossier-state.warning' | translate : translateArgs }}</div>
<div [innerHTML]="'confirm-delete-dossier-state.warning' | translate : translateArgs" class="heading"></div>
<form *ngIf="data.dossierCount !== 0 && data.otherStates.length > 0" [formGroup]="form" class="mt-16">
<div class="iqser-input-group">

View File

@ -1,74 +1,66 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { DossierState } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { FormBuilder } from '@angular/forms';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom, forkJoin } from 'rxjs';
import { combineLatest, firstValueFrom } from 'rxjs';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { take } from 'rxjs/operators';
interface DialogData {
export interface ConfirmDeleteDossierStateDialogData {
readonly toBeDeletedState: DossierState;
readonly otherStates: DossierState[];
readonly dossierCount: number;
}
@Component({
selector: 'redaction-confirm-delete-dossier-state-dialog',
templateUrl: './confirm-delete-dossier-state-dialog.component.html',
styleUrls: ['./confirm-delete-dossier-state-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmDeleteDossierStateDialogComponent {
readonly iconButtonTypes = IconButtonTypes;
readonly form: UntypedFormGroup;
readonly form = this.#form;
readonly translateArgs = {
name: this.data.toBeDeletedState.name,
count: this.data.dossierCount,
};
constructor(
private readonly _formBuilder: UntypedFormBuilder,
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
private readonly _dossierStatesService: DossierStatesService,
private readonly _dialogRef: MatDialogRef<ConfirmDeleteDossierStateDialogComponent>,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _archivedDossiersService: ArchivedDossiersService,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
) {
this.form = this.#getForm();
}
get translateArgs() {
return {
name: this.data.toBeDeletedState.name,
count: this.data.dossierCount,
};
}
@Inject(MAT_DIALOG_DATA) readonly data: ConfirmDeleteDossierStateDialogData,
) {}
get label(): string {
return this.#replaceDossierStatusId ? _('confirm-delete-dossier-state.delete-replace') : _('confirm-delete-dossier-state.delete');
}
get #replaceDossierStatusId(): string {
return this.form.get('replace').value ? this.form.get('replaceDossierStatusId').value : undefined;
return this.form.controls.replace.value ? this.form.controls.replaceDossierStatusId.value : undefined;
}
async save(): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._dossierStatesService.deleteState(this.data.toBeDeletedState, this.#replaceDossierStatusId));
await firstValueFrom(
forkJoin([this._activeDossiersService.loadAll().pipe(take(1)), this._archivedDossiersService.loadAll().pipe(take(1))]),
);
this._toaster.success(_('confirm-delete-dossier-state.success'));
this._dialogRef.close();
this._loadingService.stop();
}
#getForm(): UntypedFormGroup {
get #form() {
return this._formBuilder.group({
replace: [false],
replaceDossierStatusId: [null],
});
}
async save() {
this._loadingService.start();
await this._dossierStatesService.deleteState(this.data.toBeDeletedState, this.#replaceDossierStatusId);
await firstValueFrom(combineLatest([this._activeDossiersService.loadAll(), this._archivedDossiersService.loadAll()]));
this._toaster.success(_('confirm-delete-dossier-state.success'));
this._dialogRef.close();
this._loadingService.stop();
}
}

View File

@ -1,101 +0,0 @@
<section>
<div class="page-header">
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>
<iqser-circle-button
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>
</div>
</div>
<div class="content-inner">
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'dossier-states-listing.no-data.title' | translate"
[noMatchText]="'dossier-states-listing.no-match.title' | translate"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<div class="right-container">
<redaction-donut-chart
*ngIf="chartConfig$ | async as chartConfig"
[config]="chartConfig"
[radius]="80"
[strokeWidth]="15"
[subtitles]="['dossier-states-listing.chart.dossier-states' | translate: { count: chartConfig.length }]"
[totalType]="'simpleLabel'"
></redaction-donut-chart>
</div>
</div>
</section>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-states-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddEditStateDialog($event)"
*ngIf="permissionsService.canPerformDossierStatesActions()"
[iqserHelpMode]="'create_new_dossier_state'"
[label]="'dossier-states-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as state">
<div class="cell">
<div class="flex-align-items-center">
<div [style.background-color]="state.color" class="dossier-state-square"></div>
<div [matTooltip]="state.name" class="state-name clamp-1" matTooltipPosition="above">{{ state.name }}</div>
</div>
</div>
<div class="cell center">
<span class="small-label">{{ state.rank }}</span>
</div>
<div class="cell center">
<span class="small-label">{{ state.dossierCount }}</span>
</div>
<div class="cell">
<div *ngIf="permissionsService.canPerformDossierStatesActions()" class="action-buttons">
<div [iqserHelpMode]="'edit_delete_dossier_state'">
<iqser-circle-button
(action)="openAddEditStateDialog($event, state)"
[tooltip]="'dossier-states-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteStateDialog($event, state)"
[tooltip]="'dossier-states-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
</div>
</div>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,44 @@
<div class="content-container">
<iqser-table
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'dossier-states-listing.no-data.title' | translate"
[noMatchText]="'dossier-states-listing.no-match.title' | translate"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<div class="right-container">
<redaction-donut-chart
*ngIf="chartConfig$ | async as chartConfig"
[config]="chartConfig"
[radius]="80"
[strokeWidth]="15"
[subtitles]="['dossier-states-listing.chart.dossier-states' | translate : { count: chartConfig.length }]"
[totalType]="'simpleLabel'"
></redaction-donut-chart>
</div>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-states-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddStateDialog()"
*ngIf="permissionsService.canPerformDossierStatesActions()"
[iqserHelpMode]="'create_new_dossier_state'"
[label]="'dossier-states-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #tableItemTemplate let-entity="entity">
<redaction-dossier-states-table-item [state]="entity"></redaction-dossier-states-table-item>
</ng-template>

View File

@ -0,0 +1,8 @@
:host {
flex-direction: row !important;
}
.right-container {
padding: 50px 26px 0;
width: 250px;
}

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import {
CircleButtonTypes,
defaultDialogConfig,
getParam,
IconButtonTypes,
ListingComponent,
@ -8,35 +8,37 @@ import {
SortingOrders,
TableColumnConfig,
} from '@iqser/common-ui';
import { DonutChartConfig, DossierState, IDossierState } from '@red/domain';
import { type DonutChartConfig, DOSSIER_TEMPLATE_ID, type DossierState } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom, Observable } from 'rxjs';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { DossierStatesMapService } from '@services/entity-services/dossier-states-map.service';
import { map, tap } from 'rxjs/operators';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
import { PermissionsService } from '@services/permissions.service';
import { MatDialog } from '@angular/material/dialog';
import {
AddEditDossierStateDialogComponent,
AddEditDossierStateDialogData,
} from '../add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component';
@Component({
templateUrl: './dossier-states-listing-screen.component.html',
styleUrls: ['./dossier-states-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: listingProvidersFactory(DossierStatesListingScreenComponent),
})
export class DossierStatesListingScreenComponent extends ListingComponent<DossierState> implements OnInit, OnDestroy {
readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes;
readonly tableHeaderLabel = _('dossier-states-listing.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<DossierState>[] = [
{ label: _('dossier-states-listing.table-col-names.name'), sortByKey: 'name', width: '3fr' },
{ label: _('dossier-states-listing.table-col-names.rank'), sortByKey: 'rank', class: 'flex-center' },
{ label: _('dossier-states-listing.table-col-names.dossiers-count'), class: 'flex-center' },
];
chartConfig$: Observable<DonutChartConfig[]>;
readonly #dossierTemplateId = getParam('dossierTemplateId');
readonly chartConfig$: Observable<DonutChartConfig[]>;
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
constructor(
private readonly _dialogService: AdminDialogService,
private readonly _dialog: MatDialog,
private readonly _dossierStatesMapService: DossierStatesMapService,
private readonly _dossierStatesService: DossierStatesService,
readonly permissionsService: PermissionsService,
@ -56,21 +58,13 @@ export class DossierStatesListingScreenComponent extends ListingComponent<Dossie
await firstValueFrom(this._dossierStatesService.loadAllForTemplate(this.#dossierTemplateId));
}
openAddEditStateDialog($event: MouseEvent, dossierState?: IDossierState) {
const data = {
dossierState,
dossierTemplateId: this.#dossierTemplateId,
};
this._dialogService.openDialog('addEditDossierState', $event, data);
}
openConfirmDeleteStateDialog($event: MouseEvent, dossierState: DossierState) {
const data = {
toBeDeletedState: dossierState,
otherStates: this.entitiesService.all.filter(state => state.id !== dossierState.id),
dossierCount: dossierState.dossierCount,
};
this._dialogService.openDialog('deleteDossierState', $event, data);
openAddStateDialog() {
this._dialog.open<AddEditDossierStateDialogComponent, AddEditDossierStateDialogData>(AddEditDossierStateDialogComponent, {
...defaultDialogConfig,
data: {
dossierTemplateId: this.#dossierTemplateId,
},
});
}
#chartConfig(states: DossierState[]): DonutChartConfig[] {

View File

@ -0,0 +1,62 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DossierStatesListingScreenComponent } from './dossier-states-listing-screen/dossier-states-listing-screen.component';
import { DossierStatesTableItemComponent } from './dossier-states-table-item/dossier-states-table-item.component';
import { RouterModule } from '@angular/router';
import {
CircleButtonComponent,
IconButtonComponent,
InputWithActionComponent,
IqserHelpModeModule,
IqserListingModule,
} from '@iqser/common-ui';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
import { AdminSideNavComponent } from '../../shared/components/admin-side-nav/admin-side-nav.component';
import { DossierTemplateActionsComponent } from '../../shared/components/dossier-template-actions/dossier-template-actions.component';
import { DossierTemplateBreadcrumbsComponent } from '../../shared/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { AddEditDossierStateDialogComponent } from './add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component';
import { ReactiveFormsModule } from '@angular/forms';
import { ColorPickerModule } from 'ngx-color-picker';
import { MatIconModule } from '@angular/material/icon';
import { ConfirmDeleteDossierStateDialogComponent } from './confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component';
import { MatLegacyCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatSelectModule } from '@angular/material/select';
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
DossierStatesListingScreenComponent,
DossierStatesTableItemComponent,
AddEditDossierStateDialogComponent,
ConfirmDeleteDossierStateDialogComponent,
],
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: DossierStatesListingScreenComponent,
},
]),
MatDialogModule,
CircleButtonComponent,
IqserHelpModeModule,
MatTooltipModule,
IconButtonComponent,
TranslateModule,
IqserListingModule,
DonutChartComponent,
AdminSideNavComponent,
DossierTemplateActionsComponent,
InputWithActionComponent,
DossierTemplateBreadcrumbsComponent,
ReactiveFormsModule,
ColorPickerModule,
MatIconModule,
MatLegacyCheckboxModule,
MatSelectModule,
],
})
export class DossierStatesListingModule {}

View File

@ -0,0 +1,34 @@
<div class="cell">
<div class="flex-align-items-center">
<div [style.background-color]="state.color" class="dossier-state-square"></div>
<div [matTooltip]="state.name" class="state-name clamp-1" matTooltipPosition="above">{{ state.name }}</div>
</div>
</div>
<div class="cell center">
<span class="small-label">{{ state.rank }}</span>
</div>
<div class="cell center">
<span class="small-label">{{ state.dossierCount }}</span>
</div>
<div class="cell">
<div *ngIf="permissionsService.canPerformDossierStatesActions()" class="action-buttons">
<div [iqserHelpMode]="'edit_delete_dossier_state'">
<iqser-circle-button
(action)="openEditStateDialog(state)"
[tooltip]="'dossier-states-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteStateDialog(state)"
[tooltip]="'dossier-states-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
></iqser-circle-button>
</div>
</div>
</div>

View File

@ -0,0 +1,53 @@
import { Component, inject, Input } from '@angular/core';
import { CircleButtonTypes, defaultDialogConfig, EntitiesService } from '@iqser/common-ui';
import { DossierState, IDossierState } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import {
AddEditDossierStateDialogComponent,
AddEditDossierStateDialogData,
} from '../add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import {
ConfirmDeleteDossierStateDialogComponent,
ConfirmDeleteDossierStateDialogData,
} from '../confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component';
@Component({
selector: 'redaction-dossier-states-table-item',
templateUrl: './dossier-states-table-item.component.html',
styleUrls: ['./dossier-states-table-item.component.scss'],
})
export class DossierStatesTableItemComponent {
@Input() state: DossierState;
readonly circleButtonTypes = CircleButtonTypes;
readonly permissionsService = inject(PermissionsService);
readonly #dialog = inject(MatDialog);
readonly #entitiesService = inject(EntitiesService);
openConfirmDeleteStateDialog(dossierState: DossierState) {
const data: ConfirmDeleteDossierStateDialogData = {
toBeDeletedState: dossierState,
otherStates: this.#entitiesService.all.filter(s => s.id !== dossierState.id),
dossierCount: dossierState.dossierCount,
};
this.#dialog.open<ConfirmDeleteDossierStateDialogComponent, ConfirmDeleteDossierStateDialogData>(
ConfirmDeleteDossierStateDialogComponent,
{
...defaultDialogConfig,
data,
},
);
}
openEditStateDialog(dossierState: IDossierState) {
this.#dialog.open<AddEditDossierStateDialogComponent, AddEditDossierStateDialogData>(AddEditDossierStateDialogComponent, {
...defaultDialogConfig,
data: {
dossierState,
dossierTemplateId: dossierState.dossierTemplateId,
},
});
}
}

View File

@ -18,21 +18,21 @@
<ng-template #bulkActions>
<iqser-circle-button
buttonId="dossier-template-listing-bulk-delete-btn"
(action)="openBulkDeleteTemplatesDialog($event)"
(action)="openBulkDeleteTemplatesDialog()"
*allow="roles.templates.write; if: currentUser.isAdmin && (listingService.areSomeSelected$ | async)"
[icon]="'iqser:trash'"
[tooltip]="'dossier-templates-listing.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
buttonId="dossier-template-listing-bulk-delete-btn"
></iqser-circle-button>
</ng-template>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
inputId="dossier-template-listing-search-input"
[(value)]="searchService.searchValue"
[placeholder]="'dossier-templates-listing.search' | translate"
inputId="dossier-template-listing-search-input"
></iqser-input-with-action>
<iqser-icon-button

View File

@ -54,14 +54,14 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
super();
}
openBulkDeleteTemplatesDialog($event?: MouseEvent) {
return this._dialogService.openDialog('confirm', $event, null, () => {
openBulkDeleteTemplatesDialog() {
return this._dialogService.openDialog('confirm', null, () => {
this._loadingService.loadWhile(this._deleteTemplates());
});
}
openAddDossierTemplateDialog() {
this._dialogService.openDialog('addEditCloneDossierTemplate', null, null);
this._dialogService.openDialog('addEditCloneDossierTemplate');
}
private async _deleteTemplates(templateIds = this.listingService.selected.map(d => d.dossierTemplateId)) {

View File

@ -4,18 +4,20 @@ import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { TableItemComponent } from './table-item/table-item.component';
import { DossierTemplatesListingScreenComponent } from './dossier-templates-listing-screen/dossier-templates-listing-screen.component';
import { SharedAdminModule } from '../../shared/shared-admin.module';
import {
IqserButtonsModule,
CircleButtonComponent,
IconButtonComponent,
InputWithActionComponent,
IqserAllowDirective,
IqserHelpModeModule,
IqserInputsModule,
IqserListingModule,
IqserPermissionsModule,
IqserRoutes,
IqserUsersModule,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DossierTemplateActionsComponent } from '../../shared/components/dossier-template-actions/dossier-template-actions.component';
const routes = [{ path: '', component: DossierTemplatesListingScreenComponent }];
const routes: IqserRoutes = [{ path: '', component: DossierTemplatesListingScreenComponent }];
@NgModule({
declarations: [TableItemComponent, DossierTemplatesListingScreenComponent],
@ -23,14 +25,15 @@ const routes = [{ path: '', component: DossierTemplatesListingScreenComponent }]
RouterModule.forChild(routes),
CommonModule,
SharedModule,
SharedAdminModule,
IqserUsersModule,
TranslateModule,
IqserInputsModule,
IqserButtonsModule,
IqserListingModule,
IqserHelpModeModule,
IqserPermissionsModule,
DossierTemplateActionsComponent,
CircleButtonComponent,
IconButtonComponent,
InputWithActionComponent,
IqserAllowDirective,
],
})
export class DossierTemplatesListingModule {}

View File

@ -6,7 +6,7 @@
<div class="small-label stats-subtitle">
<div *ngIf="stats$ | async as stats">
<mat-icon svgIcon="red:dictionary"></mat-icon>
{{ 'dossier-templates-listing.entities' | translate: { length: stats.numberOfDictionaries } }}
{{ 'dossier-templates-listing.entities' | translate : { length: stats.numberOfDictionaries } }}
</div>
</div>
</div>
@ -22,25 +22,25 @@
<div class="cell">
<div class="small-label">
{{ dossierTemplate.dateAdded | date: 'd MMM yyyy' }}
{{ dossierTemplate.dateAdded | date : 'd MMM yyyy' }}
</div>
</div>
<div class="cell">
<div class="small-label">
{{ dossierTemplate.dateModified | date: 'd MMM yyyy' }}
{{ dossierTemplate.dateModified | date : 'd MMM yyyy' }}
</div>
</div>
<div class="cell">
<div class="small-label">
{{ dossierTemplate.validFrom | date: 'd MMM yyyy' }}
{{ dossierTemplate.validFrom | date : 'd MMM yyyy' }}
</div>
</div>
<div class="cell">
<div class="small-label">
{{ dossierTemplate.validTo | date: 'd MMM yyyy' }}
{{ dossierTemplate.validTo | date : 'd MMM yyyy' }}
</div>
</div>
@ -48,6 +48,7 @@
<div class="small-label">
{{ translations[dossierTemplate.dossierTemplateStatus] | translate }}
</div>
<redaction-dossier-template-actions
[dossierTemplateId]="dossierTemplate.dossierTemplateId"
class="actions-container"

View File

@ -39,7 +39,7 @@
<ng-template #bulkActions>
<iqser-circle-button
(action)="openDeleteEntitiesDialog($event)"
(action)="openDeleteEntitiesDialog()"
*ngIf="permissionsService.canDeleteEntities(listingService.selected)"
[tooltip]="'entities-listing.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
@ -58,8 +58,8 @@
(action)="openAddEntityDialog()"
*ngIf="permissionsService.canEditEntities()"
[iqserHelpMode]="'create_new_entity'"
[overlappingElements]="['USER_MENU']"
[label]="'entities-listing.add-new' | translate"
[overlappingElements]="['USER_MENU']"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
></iqser-icon-button>
@ -101,7 +101,7 @@
<div class="action-buttons">
<div [iqserHelpMode]="'edit_delete_entities'" [overlappingElements]="['USER_MENU']">
<iqser-circle-button
(action)="openDeleteEntitiesDialog($event, [dict])"
(action)="openDeleteEntitiesDialog([dict])"
*ngIf="permissionsService.canDeleteEntities(dict)"
[tooltip]="'entities-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"

View File

@ -48,8 +48,8 @@ export class EntitiesListingScreenComponent extends ListingComponent<Dictionary>
this.templateStats$ = this._dossierTemplateStatsService.watch$(this.#dossierTemplateId).pipe(tap(() => this._loadDictionaryData()));
}
openDeleteEntitiesDialog($event?: MouseEvent, types = this.listingService.selected) {
this._dialogService.openDialog('confirm', $event, null, async () => {
openDeleteEntitiesDialog(types = this.listingService.selected) {
this._dialogService.openDialog('confirm', null, async () => {
this._loadingService.start();
await firstValueFrom(
this._dictionaryService.deleteDictionaries(
@ -62,8 +62,8 @@ export class EntitiesListingScreenComponent extends ListingComponent<Dictionary>
});
}
openAddEntityDialog($event?: MouseEvent) {
this._dialogService.openDialog('addEntity', $event, { dossierTemplateId: this.#dossierTemplateId });
openAddEntityDialog() {
this._dialogService.openDialog('addEntity', { dossierTemplateId: this.#dossierTemplateId });
}
private _loadDictionaryData(): void {

View File

@ -6,9 +6,8 @@ import { DictionaryScreenComponent } from './screens/dictionary/dictionary-scree
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { EntityInfoComponent } from './screens/entity-info/entity-info.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { SharedAdminModule } from '../../shared/shared-admin.module';
import { TranslateModule } from '@ngx-translate/core';
import { IqserHelpModeModule, IqserPermissionsModule, IqserScrollbarModule } from '@iqser/common-ui';
import { HasScrollbarDirective, IconButtonComponent, IqserDenyDirective, IqserHelpModeModule } from '@iqser/common-ui';
const routes: Routes = [
{ path: '', redirectTo: 'info', pathMatch: 'full' },
@ -38,14 +37,14 @@ const routes: Routes = [
declarations: [DictionaryScreenComponent, EntityInfoComponent],
imports: [
RouterModule.forChild(routes),
SharedAdminModule,
CommonModule,
SharedModule,
MonacoEditorModule,
TranslateModule,
IqserScrollbarModule,
IqserHelpModeModule,
IqserPermissionsModule,
IconButtonComponent,
HasScrollbarDirective,
IqserDenyDirective,
],
})
export class EntitiesModule {}

View File

@ -1,3 +1,5 @@
@use 'common-mixins';
.options-wrapper {
display: flex;
align-items: center;
@ -7,3 +9,7 @@
margin-right: 32px;
}
}
.dialog-header {
@include common-mixins.line-clamp(1);
}

View File

@ -1,9 +1,16 @@
import { Component, Inject } from '@angular/core';
import { Validators } from '@angular/forms';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { FileAttributeConfigTypes, IFileAttributeConfig } from '@red/domain';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { fileAttributeTypesTranslations } from '@translations/file-attribute-types-translations';
import { BaseDialogComponent } from '@iqser/common-ui';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { NgForOf } from '@angular/common';
import { MatLegacySlideToggleModule } from '@angular/material/legacy-slide-toggle';
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatTooltipModule } from '@angular/material/tooltip';
export interface AddEditFileAttributeDialogData {
readonly fileAttribute: IFileAttributeConfig;
@ -15,6 +22,19 @@ export interface AddEditFileAttributeDialogData {
@Component({
templateUrl: './add-edit-file-attribute-dialog.component.html',
styleUrls: ['./add-edit-file-attribute-dialog.component.scss'],
standalone: true,
imports: [
ReactiveFormsModule,
TranslateModule,
MatFormFieldModule,
MatSelectModule,
NgForOf,
MatLegacySlideToggleModule,
MatCheckboxModule,
MatTooltipModule,
IconButtonComponent,
CircleButtonComponent,
],
})
export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
readonly DISPLAYED_FILTERABLE_LIMIT = 3;
@ -30,7 +50,7 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
};
constructor(
protected readonly _dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
protected readonly _dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent, IFileAttributeConfig>,
@Inject(MAT_DIALOG_DATA) readonly data: AddEditFileAttributeDialogData,
) {
super(_dialogRef, !!data.fileAttribute);

View File

@ -1,23 +1,37 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormGroup, Validators } from '@angular/forms';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { FileAttributeEncodingTypes, IFileAttributesConfig } from '@red/domain';
import { fileAttributeEncodingTypesTranslations } from '@translations/file-attribute-encoding-types-translations';
import { BaseDialogComponent } from '@iqser/common-ui';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
import { MatLegacySlideToggleModule } from '@angular/material/legacy-slide-toggle';
import { NgForOf, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
@Component({
templateUrl: './file-attributes-configurations-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
ReactiveFormsModule,
MatLegacySlideToggleModule,
NgIf,
TranslateModule,
MatFormFieldModule,
MatSelectModule,
NgForOf,
IconButtonComponent,
CircleButtonComponent,
],
})
export class FileAttributesConfigurationsDialogComponent extends BaseDialogComponent {
readonly encodingTypeOptions = Object.keys(FileAttributeEncodingTypes);
readonly translations = fileAttributeEncodingTypesTranslations;
readonly #configuration = this._data.config;
readonly #configuration = inject<IFileAttributesConfig>(MAT_DIALOG_DATA);
constructor(
protected readonly _dialogRef: MatDialogRef<FileAttributesConfigurationsDialogComponent>,
@Inject(MAT_DIALOG_DATA) private readonly _data: { config: IFileAttributesConfig; dossierTemplateId: string },
) {
constructor(protected readonly _dialogRef: MatDialogRef<FileAttributesConfigurationsDialogComponent, IFileAttributesConfig>) {
super(_dialogRef, true);
this.form = this.#getForm();
this.initialFormValue = this.form.getRawValue();
@ -39,7 +53,7 @@ export class FileAttributesConfigurationsDialogComponent extends BaseDialogCompo
this._dialogRef.close(this.#getConfiguration());
}
#getConfiguration() {
#getConfiguration(): IFileAttributesConfig {
const supportCsvMapping = this.form.get('supportCsvMapping').value;
if (supportCsvMapping) {
return {
@ -56,7 +70,7 @@ export class FileAttributesConfigurationsDialogComponent extends BaseDialogCompo
};
}
#getForm(): UntypedFormGroup {
#getForm() {
return this._formBuilder.group({
supportCsvMapping: [!!this.#configuration.filenameMappingColumnHeaderName],
keyColumn: [this.#configuration.filenameMappingColumnHeaderName || '', [Validators.required]],

View File

@ -10,7 +10,7 @@ import { FileAttributeConfig, FileAttributeConfigTypes, FileAttributeEncodingTyp
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { fileAttributeEncodingTypesTranslations } from '@translations/file-attribute-encoding-types-translations';
interface IFileAttributesCSVImportData {
export interface IFileAttributesCSVImportData {
readonly csv: File;
readonly dossierTemplateId: string;
readonly existingConfiguration: IFileAttributesConfig;
@ -43,7 +43,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<IFi
constructor(
private readonly _toaster: Toaster,
private readonly _formBuilder: UntypedFormBuilder,
readonly dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
readonly dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent, boolean>,
private readonly _fileAttributesService: FileAttributesService,
@Inject(MAT_DIALOG_DATA) readonly data: IFileAttributesCSVImportData,
) {

View File

@ -0,0 +1,47 @@
import { NgModule } from '@angular/core';
import { FileAttributesCsvImportDialogComponent } from './file-attributes-csv-import-dialog.component';
import { ActiveFieldsListingComponent } from './active-fields-listing/active-fields-listing.component';
import {
ChevronButtonComponent,
CircleButtonComponent,
EditableInputComponent,
IconButtonComponent,
InputWithActionComponent,
IqserListingModule,
RoundCheckboxComponent,
} from '@iqser/common-ui';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatLegacySlideToggleModule } from '@angular/material/legacy-slide-toggle';
@NgModule({
declarations: [FileAttributesCsvImportDialogComponent, ActiveFieldsListingComponent],
imports: [
CommonModule,
CircleButtonComponent,
TranslateModule,
MatDialogModule,
IconButtonComponent,
InputWithActionComponent,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatInputModule,
IqserListingModule,
MatMenuModule,
ChevronButtonComponent,
EditableInputComponent,
FormsModule,
MatLegacySlideToggleModule,
RoundCheckboxComponent,
],
})
export class FileAttributesCsvImportDialogModule {}

View File

@ -1,41 +1,18 @@
<section>
<div class="page-header">
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>
<div class="content-container">
<iqser-table
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'file-attributes-listing.no-data.title' | translate"
[noMatchText]="'file-attributes-listing.no-match.title' | translate"
[selectionEnabled]="permissionsService.canEditGlobalFileAttributes()"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<div class="actions flex-1">
<redaction-dossier-template-actions></redaction-dossier-template-actions>
<iqser-circle-button
[routerLink]="['../..']"
[tooltip]="'common.close' | translate"
icon="iqser:close"
tooltipPosition="below"
></iqser-circle-button>
</div>
</div>
<div class="content-inner">
<div class="overlay-shadow"></div>
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'file-attributes-listing.no-data.title' | translate"
[noMatchText]="'file-attributes-listing.no-match.title' | translate"
[selectionEnabled]="permissionsService.canEditGlobalFileAttributes()"
[tableColumnConfigs]="tableColumnConfigs"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<div class="right-container"></div>
</div>
</section>
<div class="right-container"></div>
<ng-template #impactedTemplates let-data="data">
<ul class="templates-container flex">
@ -45,7 +22,7 @@
<ng-template #bulkActions>
<iqser-circle-button
(action)="openConfirmDeleteAttributeDialog($event)"
(action)="openConfirmDeleteAttributeDialog()"
*ngIf="permissionsService.canEditGlobalFileAttributes() && (listingService.areSomeSelected$ | async)"
[tooltip]="'file-attributes-listing.bulk-actions.delete' | translate"
[type]="circleButtonTypes.dark"
@ -53,6 +30,7 @@
></iqser-circle-button>
</ng-template>
<!--TODO: move to a separate component-->
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
@ -73,7 +51,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="openConfigurationsDialog($event)"
(action)="openConfigurationsDialog()"
*allow="roles.fileAttributes.writeConfig; if: currentUser.isAdmin"
[iqserHelpMode]="'upload_file_attribute'"
[overlappingElements]="['USER_MENU']"
@ -84,7 +62,7 @@
></iqser-circle-button>
<iqser-icon-button
(action)="openAddEditAttributeDialog($event)"
(action)="openAddEditAttributeDialog()"
*ngIf="permissionsService.canEditGlobalFileAttributes()"
[iqserHelpMode]="'create_new_file_attribute'"
[label]="'file-attributes-listing.add-new' | translate"
@ -95,6 +73,7 @@
</div>
</ng-template>
<!--TODO: move to a separate component-->
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as attribute">
<div class="label cell">
@ -134,13 +113,14 @@
<div *ngIf="permissionsService.canEditGlobalFileAttributes()" class="action-buttons">
<div [iqserHelpMode]="'edit_delete_file_attribute'" [overlappingElements]="['USER_MENU']">
<iqser-circle-button
(action)="openAddEditAttributeDialog($event, attribute)"
(action)="openAddEditAttributeDialog(attribute)"
[tooltip]="'file-attributes-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteAttributeDialog($event, [attribute])"
(action)="openConfirmDeleteAttributeDialog([attribute])"
[tooltip]="'file-attributes-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"

View File

@ -1,10 +1,12 @@
import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AdminDialogService } from '../../services/admin-dialog.service';
import {
CircleButtonTypes,
defaultDialogConfig,
getCurrentUser,
getParam,
IconButtonTypes,
largeDialogConfig,
ListingComponent,
listingProvidersFactory,
LoadingService,
@ -20,11 +22,20 @@ import { firstValueFrom } from 'rxjs';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { PermissionsService } from '@services/permissions.service';
import { ROLES } from '@users/roles';
import { MatDialog } from '@angular/material/dialog';
import {
AddEditFileAttributeDialogComponent,
AddEditFileAttributeDialogData,
} from './add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import {
FileAttributesCsvImportDialogComponent,
IFileAttributesCSVImportData,
} from './file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component';
import { FileAttributesConfigurationsDialogComponent } from './file-attributes-configurations-dialog/file-attributes-configurations-dialog.component';
@Component({
templateUrl: './file-attributes-listing-screen.component.html',
styleUrls: ['./file-attributes-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: listingProvidersFactory(FileAttributesListingScreenComponent),
})
export class FileAttributesListingScreenComponent extends ListingComponent<FileAttributeConfig> implements OnInit, OnDestroy {
@ -58,6 +69,7 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
private readonly _toaster: Toaster,
private readonly _loadingService: LoadingService,
private readonly _dialogService: AdminDialogService,
private readonly _dialog: MatDialog,
private readonly _fileAttributesService: FileAttributesService,
private readonly _dossierTemplatesService: DossierTemplatesService,
) {
@ -76,65 +88,74 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
await this.#loadData();
}
openAddEditAttributeDialog($event: MouseEvent, fileAttribute?: IFileAttributeConfig) {
async openAddEditAttributeDialog(fileAttribute?: IFileAttributeConfig) {
const data = {
fileAttribute,
dossierTemplateId: this.#dossierTemplateId,
numberOfDisplayedAttrs: this._numberOfDisplayedAttrs,
numberOfFilterableAttrs: this._numberOfFilterableAttrs,
};
this._dialogService.openDialog('addEditFileAttribute', $event, data, (newValue: IFileAttributeConfig) => {
this._loadingService.loadWhile(this.#createNewFileAttributeAndRefreshView(newValue));
const ref = this._dialog.open<AddEditFileAttributeDialogComponent, AddEditFileAttributeDialogData, IFileAttributeConfig>(
AddEditFileAttributeDialogComponent,
{
...defaultDialogConfig,
autoFocus: true,
data,
},
);
const result = await firstValueFrom(ref.afterClosed());
this._loadingService.loadWhile(this.#createNewFileAttributeAndRefreshView(result));
}
async openConfirmDeleteAttributeDialog(attributes: FileAttributeConfig[] = this.listingService.selected): Promise<void> {
await this._dialogService.deleteAttributes(attributes, this.#dossierTemplateId, this._impactedTemplatesRef, 'file', async () => {
this._loadingService.start();
const ids = attributes.map(a => a.id);
await firstValueFrom(this._fileAttributesService.deleteFileAttributes(ids, this.#dossierTemplateId));
await firstValueFrom(this._dossierTemplatesService.refreshDossierTemplate(this.#dossierTemplateId));
await this.#loadData();
});
}
async openConfirmDeleteAttributeDialog(
$event: MouseEvent,
attributes: FileAttributeConfig[] = this.listingService.selected,
): Promise<void> {
await this._dialogService.deleteAttributes(
attributes,
this.#dossierTemplateId,
this._impactedTemplatesRef,
'file',
$event,
async () => {
this._loadingService.start();
const ids = attributes.map(a => a.id);
await firstValueFrom(this._fileAttributesService.deleteFileAttributes(ids, this.#dossierTemplateId));
await firstValueFrom(this._dossierTemplatesService.refreshDossierTemplate(this.#dossierTemplateId));
await this.#loadData();
},
);
}
importCSV(files: FileList | File[]) {
async importCSV(files: FileList | File[]) {
const csv = files[0];
this._fileInput.nativeElement.value = null;
this._dialogService.openDialog(
'importFileAttributes',
null,
const ref = this._dialog.open<FileAttributesCsvImportDialogComponent, IFileAttributesCSVImportData, boolean>(
FileAttributesCsvImportDialogComponent,
{
csv,
dossierTemplateId: this.#dossierTemplateId,
existingConfiguration: this.#existingConfiguration,
...largeDialogConfig,
disableClose: false,
data: {
csv,
dossierTemplateId: this.#dossierTemplateId,
existingConfiguration: this.#existingConfiguration,
},
},
async () => this.#loadData(),
);
const result = await firstValueFrom(ref.afterClosed());
if (result) {
await this.#loadData();
}
}
openConfigurationsDialog($event: MouseEvent) {
const ref = this._dialogService.openDialog('fileAttributesConfigurations', $event, {
config: this.#existingConfiguration,
dossierTemplateId: this.#dossierTemplateId,
});
async openConfigurationsDialog() {
const ref = this._dialog.open<FileAttributesConfigurationsDialogComponent, IFileAttributesConfig, IFileAttributesConfig>(
FileAttributesConfigurationsDialogComponent,
{
...defaultDialogConfig,
data: this.#existingConfiguration,
},
);
ref.afterClosed().subscribe(async (configuration: IFileAttributesConfig) => {
if (configuration) {
await this.#setConfigAndLoadData(configuration);
}
});
const configuration = await firstValueFrom(ref.afterClosed());
if (configuration) {
await this.#setConfigAndLoadData(configuration);
}
}
async #setConfigAndLoadData(configuration: IFileAttributesConfig) {

View File

@ -0,0 +1,43 @@
import { NgModule } from '@angular/core';
import { FileAttributesListingScreenComponent } from './file-attributes-listing-screen.component';
import { RouterModule } from '@angular/router';
import {
CircleButtonComponent,
IconButtonComponent,
InputWithActionComponent,
IqserAllowDirective,
IqserHelpModeModule,
IqserListingModule,
IqserRoutes,
RoundCheckboxComponent,
} from '@iqser/common-ui';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
const routes: IqserRoutes = [
{
path: '',
component: FileAttributesListingScreenComponent,
},
];
@NgModule({
declarations: [FileAttributesListingScreenComponent],
imports: [
CommonModule,
RouterModule.forChild(routes),
IqserListingModule,
TranslateModule,
CircleButtonComponent,
InputWithActionComponent,
IqserHelpModeModule,
IqserAllowDirective,
IconButtonComponent,
MatIconModule,
MatTooltipModule,
RoundCheckboxComponent,
],
})
export class FileAttributesListingModule {}

View File

@ -42,7 +42,7 @@ export class SmtpFormComponent extends BaseFormComponent implements OnInit {
}
openAuthConfigDialog(skipDisableOnCancel?: boolean) {
this._dialogService.openDialog('smtpAuthConfig', null, this.form.getRawValue(), null, authConfig => {
this._dialogService.openDialog('smtpAuthConfig', this.form.getRawValue(), null, authConfig => {
if (authConfig) {
this.form.patchValue(authConfig);
} else if (!skipDisableOnCancel) {

View File

@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { DossierTemplateInfoScreenComponent } from './info-screen/dossier-template-info-screen.component';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { IqserButtonsModule, IqserHelpModeModule, IqserScrollbarModule, IqserUsersModule } from '@iqser/common-ui';
import { HasScrollbarDirective, IqserHelpModeModule, IqserUsersModule } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
const routes = [{ path: '', component: DossierTemplateInfoScreenComponent }];
@ -16,9 +16,8 @@ const routes = [{ path: '', component: DossierTemplateInfoScreenComponent }];
SharedModule,
IqserUsersModule,
TranslateModule,
IqserButtonsModule,
IqserScrollbarModule,
IqserHelpModeModule,
HasScrollbarDirective,
],
})
export class DossierTemplateInfoModule {}

View File

@ -48,8 +48,7 @@
[label]="'add-edit-justification.actions.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
>
</iqser-icon-button>
></iqser-icon-button>
<div (click)="close()" class="all-caps-label cancel" translate="add-edit-justification.actions.cancel"></div>
</div>

View File

@ -2,9 +2,9 @@ import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import {
ConfirmationDialogComponent,
ConfirmationDialogInput,
DialogConfig,
DialogService,
IConfirmationDialogData,
LoadingService,
TitleColors,
} from '@iqser/common-ui';
@ -39,7 +39,7 @@ export class JustificationsDialogService extends DialogService<DialogType> {
}
confirmDelete(justifications: Justification[], dossierTemplateId: string) {
const data = new ConfirmationDialogInput({
const data: IConfirmationDialogData = {
title: _('confirmation-dialog.delete-justification.title'),
titleColor: TitleColors.WARN,
question: _('confirmation-dialog.delete-justification.question'),
@ -47,8 +47,9 @@ export class JustificationsDialogService extends DialogService<DialogType> {
count: justifications.length,
justificationName: justifications[0].name,
},
});
this.openDialog('confirm', null, data, async () => {
};
this.openDialog('confirm', data, async () => {
this._loadingService.start();
const justificationIds = justifications.map(j => j.id);
await firstValueFrom(this._justificationService.delete(justificationIds, dossierTemplateId));

View File

@ -60,7 +60,7 @@ export class JustificationsScreenComponent extends ListingComponent<Justificatio
}
openAddJustificationDialog(): void {
this._dialogService.openDialog('addEditJustification', null, { justification: null, dossierTemplateId: this.#dossierTemplateId });
this._dialogService.openDialog('addEditJustification', { justification: null, dossierTemplateId: this.#dossierTemplateId });
}
openConfirmDeleteDialog() {

View File

@ -7,13 +7,22 @@ import { TableItemComponent } from './table-item/table-item.component';
import { JustificationsDialogService } from './justifications-dialog.service';
import { AddEditJustificationDialogComponent } from './add-edit-justification-dialog/add-edit-justification-dialog.component';
import { TranslateModule } from '@ngx-translate/core';
import { IqserButtonsModule, IqserListingModule } from '@iqser/common-ui';
import { CircleButtonComponent, HasScrollbarDirective, IconButtonComponent, IqserListingModule } from '@iqser/common-ui';
const routes = [{ path: '', component: JustificationsScreenComponent }];
@NgModule({
declarations: [JustificationsScreenComponent, TableItemComponent, AddEditJustificationDialogComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, TranslateModule, IqserButtonsModule, IqserListingModule],
imports: [
RouterModule.forChild(routes),
CommonModule,
SharedModule,
TranslateModule,
IqserListingModule,
IconButtonComponent,
CircleButtonComponent,
HasScrollbarDirective,
],
providers: [JustificationsDialogService],
})
export class JustificationsModule {}

View File

@ -18,7 +18,7 @@ export class TableItemComponent {
constructor(private readonly _dialogService: JustificationsDialogService) {}
openEditJustificationDialog() {
this._dialogService.openDialog('addEditJustification', null, {
this._dialogService.openDialog('addEditJustification', {
justification: this.justification,
dossierTemplateId: this.#dossierTemplateId,
});

View File

@ -61,9 +61,14 @@
<div>{{ licenseService.totalLicensedNumberOfPages }}</div>
</div>
<div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div>
<div class="row">
<div translate="license-info-screen.analyzed-pages"></div>
<div>{{ licenseService.currentInfo.numberOfAnalyzedPages }}</div>
<div translate="license-info-screen.current-analyzed"></div>
<div>
{{ licenseService.analyzedPagesInCurrentLicensingPeriod }}
({{ analysisPercentageOfLicense$ | async | number : '1.0-2' }}%)
</div>
</div>
<div class="row">
@ -71,27 +76,26 @@
<div>{{ licenseService.currentInfo.numberOfOcrPages }}</div>
</div>
<div class="section-title all-caps-label" translate="license-info-screen.usage-details"></div>
<div *ngIf="!!licenseService.unlicensedPages" class="row">
<div translate="license-info-screen.unlicensed-analyzed"></div>
<div>{{ licenseService.unlicensedPages }}</div>
</div>
<div class="row">
<div
*ngIf="licenseService.totalInfo.startDate | date : 'longDate' as startDate"
*ngIf="licenseService.annualInfo.startDate | date : 'longDate' as startDate"
[innerHTML]="'license-info-screen.total-analyzed' | translate : { date: startDate }"
></div>
<div>{{ licenseService.totalInfo.numberOfAnalyzedPages }}</div>
<div>{{ licenseService.annualInfo.numberOfAnalyzedPages }}</div>
</div>
<div class="row">
<div translate="license-info-screen.current-analyzed"></div>
<div>
{{ licenseService.currentInfo.numberOfAnalyzedPages }}
({{ analysisPercentageOfLicense$ | async | number : '1.0-2' }}%)
</div>
</div>
<div *ngIf="!!licenseService.unlicensedInfo" class="row">
<div translate="license-info-screen.unlicensed-analyzed"></div>
<div>{{ licenseService.unlicensedInfo.numberOfAnalyzedPages }}</div>
<div
*ngIf="licenseService.annualInfo.startDate | date : 'longDate' as startDate"
[innerHTML]="'license-info-screen.total-ocr-analyzed' | translate : { date: startDate }"
></div>
<div>{{ licenseService.annualInfo.numberOfOcrPages }}</div>
</div>
</div>

View File

@ -50,7 +50,7 @@ export class LicenseScreenComponent {
getAnalysisPercentageOfLicense() {
const totalLicensedNumberOfPages = this.licenseService.totalLicensedNumberOfPages;
const numberOfAnalyzedPages = this.licenseService.currentInfo.numberOfAnalyzedPages;
const numberOfAnalyzedPages = this.licenseService.analyzedPagesInCurrentLicensingPeriod;
return totalLicensedNumberOfPages > 0 ? (numberOfAnalyzedPages / totalLicensedNumberOfPages) * 100 : 100;
}

View File

@ -12,7 +12,8 @@
<div *ngFor="let permission of mappedPermissions" class="center cell">
<mat-slide-toggle
(toggleChange)="togglePermission(config.searchKey, permission)"
[checked]="config.getValue(permission)"
[disabled]="config.isDisabled(permission)"
[checked]="config.isChecked(permission)"
[iqserHelpMode]="'dossier_permissions'"
[overlappingElements]="['USER_MENU']"
color="primary"

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { DOSSIER_TEMPLATE_ID, IPlaceholdersResponse, IReportTemplate, User } from '@red/domain';
import { download } from '@utils/file-download-utils';
import { ConfirmationDialogInput, getCurrentUser, getParam, LoadingService, Toaster } from '@iqser/common-ui';
import { getCurrentUser, getParam, IConfirmationDialogData, LoadingService, Toaster } from '@iqser/common-ui';
import { PermissionsService } from '@services/permissions.service';
import {
generalPlaceholdersDescriptionsTranslations,
@ -67,7 +67,7 @@ export class ReportsScreenComponent implements OnInit {
}
deleteTemplate(template: IReportTemplate) {
this._dialogService.openDialog('confirm', null, null, () => {
this._dialogService.openDialog('confirm', null, () => {
this._loadingService.loadWhile(this._deleteTemplate(template));
});
}
@ -80,7 +80,7 @@ export class ReportsScreenComponent implements OnInit {
return;
}
const data = new ConfirmationDialogInput({
const data: IConfirmationDialogData = {
title: _('confirmation-dialog.upload-report-template.title'),
question: _('confirmation-dialog.upload-report-template.question'),
confirmationText: _('confirmation-dialog.upload-report-template.confirmation-text'),
@ -89,8 +89,9 @@ export class ReportsScreenComponent implements OnInit {
translateParams: {
fileName: file.name,
},
});
this._dialogService.openDialog('confirm', null, data, null, async result => {
};
this._dialogService.openDialog('confirm', data, null, async result => {
if (result) {
const multiFileReport = result > 1;
if (
@ -119,7 +120,7 @@ export class ReportsScreenComponent implements OnInit {
}
private async _openOverwriteConfirmationDialog(file: File, multiFileReport: boolean): Promise<void> {
const data = new ConfirmationDialogInput({
const data: IConfirmationDialogData = {
title: _('confirmation-dialog.report-template-same-name.title'),
question: _('confirmation-dialog.report-template-same-name.question'),
confirmationText: _('confirmation-dialog.report-template-same-name.confirmation-text'),
@ -127,9 +128,9 @@ export class ReportsScreenComponent implements OnInit {
translateParams: {
fileName: file.name,
},
});
};
this._dialogService.openDialog('confirm', null, data, null, async result => {
this._dialogService.openDialog('confirm', data, null, async result => {
if (result) {
await this._uploadTemplateForm(file, multiFileReport);
}

View File

@ -1,24 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { ReportsScreenComponent } from './reports-screen/reports-screen.component';
import { TranslateModule } from '@ngx-translate/core';
import { IqserButtonsModule, IqserHelpModeModule, IqserPermissionsModule, IqserScrollbarModule } from '@iqser/common-ui';
import { CircleButtonComponent, HasScrollbarDirective, IqserAllowDirective, IqserHelpModeModule, IqserRoutes } from '@iqser/common-ui';
const routes = [{ path: '', component: ReportsScreenComponent }];
const routes: IqserRoutes = [{ path: '', component: ReportsScreenComponent }];
@NgModule({
declarations: [ReportsScreenComponent],
imports: [
RouterModule.forChild(routes),
CommonModule,
SharedModule,
TranslateModule,
IqserButtonsModule,
IqserScrollbarModule,
IqserHelpModeModule,
IqserPermissionsModule,
CircleButtonComponent,
HasScrollbarDirective,
IqserAllowDirective,
],
})
export class ReportsModule {}

View File

@ -13,7 +13,6 @@ import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
@Component({
selector: 'redaction-rules-screen',
templateUrl: './rules-screen.component.html',
styleUrls: ['./rules-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -1,17 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { RulesScreenComponent } from './rules-screen/rules-screen.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { TranslateModule } from '@ngx-translate/core';
import { IqserButtonsModule } from '@iqser/common-ui';
import { IconButtonComponent } from '@iqser/common-ui';
import { FormsModule } from '@angular/forms';
const routes = [{ path: '', component: RulesScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [RulesScreenComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, MonacoEditorModule, TranslateModule, IqserButtonsModule],
imports: [RouterModule.forChild(routes), CommonModule, MonacoEditorModule, TranslateModule, IconButtonComponent, FormsModule],
})
export class RulesModule {}

View File

@ -1,5 +1,4 @@
<iqser-page-header
searchInputId="user-management-search-input"
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
[buttonConfigs]="buttonConfigs"
[hideResetButton]="true"
@ -7,6 +6,7 @@
[searchPlaceholder]="'user-listing.search' | translate"
[searchPosition]="searchPositions.withActions"
[showCloseButton]="true"
searchInputId="user-management-search-input"
></iqser-page-header>
<div class="content-inner">
@ -28,7 +28,6 @@
<ng-template #bulkActions>
<iqser-circle-button
buttonId="bulk-delete-users-btn"
(action)="bulkDelete()"
*ngIf="listingService.areSomeSelected$ | async"
[disabled]="(canDeleteSelected$ | async) === false"
@ -36,6 +35,7 @@
(canDeleteSelected$ | async) ? ('user-listing.bulk.delete' | translate) : ('user-listing.bulk.delete-disabled' | translate)
"
[type]="circleButtonTypes.dark"
buttonId="bulk-delete-users-btn"
icon="iqser:trash"
tooltipPosition="after"
></iqser-circle-button>
@ -53,10 +53,10 @@
<div class="center cell">
<mat-slide-toggle
[id]="'toggle-active-' + user.id"
(toggleChange)="toggleActive(user)"
[checked]="user.active"
[disabled]="!canDeactivate(user)"
[id]="'toggle-active-' + user.id"
[iqserHelpMode]="'activate_deactivate_user'"
color="primary"
></mat-slide-toggle>
@ -71,14 +71,14 @@
<div class="action-buttons">
<div *allow="roles.users.write" [iqserHelpMode]="'edit_delete_user'">
<iqser-circle-button
(action)="openAddEditUserDialog($event, user)"
(action)="openAddEditUserDialog(user)"
[tooltip]="'user-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openDeleteUsersDialog([user.id], $event)"
(action)="openDeleteUsersDialog([user.id])"
[disabled]="deleteDisabled(user)"
[tooltip]="'user-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"

View File

@ -92,14 +92,14 @@ export class UserListingScreenComponent extends ListingComponent<User> implement
await this.#loadData();
}
openAddEditUserDialog(event?: MouseEvent, user?: User) {
this._dialogService.openDialog('addEditUser', event, user, async () => {
openAddEditUserDialog(user?: User) {
this._dialogService.openDialog('addEditUser', user, async () => {
await this.#loadData();
});
}
openDeleteUsersDialog(userIds: string[], $event?: MouseEvent) {
this._dialogService.deleteUsers(userIds, $event, async () => {
openDeleteUsersDialog(userIds: string[]) {
this._dialogService.deleteUsers(userIds, async () => {
await this.#loadData();
});
}

View File

@ -0,0 +1,13 @@
<div class="pagination noselect">
<div (click)="changePage.emit(1)" class="page-button" id="portraitPage">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
Portrait
</div>
<div class="separator">/</div>
<div (click)="changePage.emit(2)" class="page-button" id="landscapePage">
Landscape
<mat-icon class="chevron-icon" svgIcon="red:nav-next"></mat-icon>
</div>
</div>

View File

@ -0,0 +1,42 @@
.pagination {
z-index: 1;
position: absolute;
bottom: 20px;
right: calc(50% - (var(--viewer-width) / 2));
transform: translate(-50%);
background: var(--iqser-background);
color: var(--iqser-grey-7);
border: 1px solid var(--iqser-grey-7);
border-radius: 8px;
padding: 6px 2px;
display: flex;
justify-content: center;
align-items: center;
> div {
height: 16px;
cursor: default;
}
.separator {
padding-left: 2px;
padding-right: 2px;
}
.page-button {
cursor: pointer;
align-items: center;
display: inline-flex;
&:hover {
color: var(--iqser-text);
}
}
.chevron-icon {
height: 16px;
transform: rotate(-90deg);
padding-left: 4px;
padding-right: 4px;
}
}

View File

@ -0,0 +1,10 @@
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'redaction-paginator',
templateUrl: './paginator.component.html',
styleUrls: ['./paginator.component.scss'],
})
export class PaginatorComponent {
@Output() readonly changePage = new EventEmitter<number>();
}

View File

@ -1,7 +1,9 @@
<div class="content-container">
<div class="viewer" id="viewer"></div>
<div #viewer class="viewer" id="viewer"></div>
<div *ngIf="changed && currentUser.isAdmin" class="changes-box">
<redaction-paginator (changePage)="navigateTo($event)" *ngIf="loaded$ | async"></redaction-paginator>
<div *ngIf="!!instance && changed && currentUser.isAdmin" class="changes-box">
<iqser-icon-button
(action)="save()"
[disabled]="!valid"
@ -9,6 +11,7 @@
[type]="iconButtonTypes.primary"
icon="iqser:check"
></iqser-icon-button>
<div (click)="revert()" [translate]="'watermark-screen.action.revert'" class="all-caps-label cancel"></div>
</div>
</div>

View File

@ -5,6 +5,7 @@
flex-direction: row !important;
flex-grow: 1;
overflow: hidden;
--viewer-width: 380px;
}
.content-container {
@ -12,6 +13,7 @@
.viewer {
height: 100%;
width: 100%;
}
}

View File

@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core';
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms';
@ -18,14 +18,14 @@ import { DOSSIER_TEMPLATE_ID, type IWatermark, type User, WATERMARK_ID, Watermar
import { stampPDFPage } from '@utils/page-stamper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { firstValueFrom, Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { BehaviorSubject, firstValueFrom, Observable, of } from 'rxjs';
import { LicenseService } from '@services/license.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { Router } from '@angular/router';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.service';
import { ROLES } from '@users/roles';
import { environment } from '@environments/environment';
import { tap } from 'rxjs/operators';
export const DEFAULT_WATERMARK: Partial<IWatermark> = {
text: 'Watermark',
@ -51,10 +51,10 @@ interface WatermarkForm {
templateUrl: './watermark-screen.component.html',
styleUrls: ['./watermark-screen.component.scss'],
})
export class WatermarkScreenComponent {
export class WatermarkScreenComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
readonly currentUser = getCurrentUser<User>();
readonly form = this._getForm();
readonly form = this.#form;
readonly watermark$: Observable<Partial<IWatermark>>;
readonly fontOptions = [
{ value: 'times-new-roman', display: 'Times' },
@ -62,10 +62,12 @@ export class WatermarkScreenComponent {
{ value: 'courier', display: 'Courier' },
];
readonly orientationOptions = ['DIAGONAL', 'HORIZONTAL', 'VERTICAL'];
instance: WebViewerInstance;
readonly loaded$ = new BehaviorSubject(false);
@ViewChild('viewer', { static: true }) private readonly _viewer: ElementRef<HTMLDivElement>;
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly #watermarkId = Number(getParam(WATERMARK_ID));
private _instance: WebViewerInstance;
private _watermark: Partial<IWatermark> = {};
#watermark: Partial<IWatermark> = {};
constructor(
private readonly _http: HttpClient,
@ -78,17 +80,16 @@ export class WatermarkScreenComponent {
private readonly _watermarkService: WatermarkService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _router: Router,
private readonly _watermarksMapService: WatermarksMapService,
watermarksMapService: WatermarksMapService,
) {
const obs$: Observable<Partial<IWatermark>> = this.#watermarkId
? _watermarksMapService.watch$(this.#dossierTemplateId, this.#watermarkId)
: of(DEFAULT_WATERMARK);
this.watermark$ = obs$.pipe(tap(wm => this._initForm(wm)));
const watermark$ = watermarksMapService.watch$(this.#dossierTemplateId, this.#watermarkId);
const obs$: Observable<Partial<IWatermark>> = this.#watermarkId ? watermark$ : of(DEFAULT_WATERMARK);
this.watermark$ = obs$.pipe(tap(watermark => this.#initForm(watermark)));
}
get changed(): boolean {
for (const key of Object.keys(this.form.getRawValue())) {
if (this._watermark[key] !== this.form.get(key)?.value) {
if (this.#watermark[key] !== this.form.get(key)?.value) {
return true;
}
}
@ -102,113 +103,7 @@ export class WatermarkScreenComponent {
return this.form.valid;
}
@Debounce()
async configChanged() {
await this._drawWatermark();
}
async save(): Promise<void> {
const watermark: IWatermark = {
id: this._watermark.id,
enabled: this._watermark.id ? this._watermark.enabled : true,
dossierTemplateId: this.#dossierTemplateId,
...this.form.getRawValue(),
};
this._loadingService.start();
try {
const updatedWatermark = await this._watermarkService.saveWatermark(watermark);
this._toaster.success(
watermark.id ? _('watermark-screen.action.change-success') : _('watermark-screen.action.created-success'),
);
if (!watermark.id) {
await this._router.navigate([`/main/admin/dossier-templates/${this.#dossierTemplateId}/watermarks/${updatedWatermark.id}`]);
}
} catch (error) {
this._toaster.error(_('watermark-screen.action.error'));
}
this._loadingService.stop();
}
async revert() {
this.form.patchValue({ ...this._watermark });
await this.configChanged();
}
async setValue(type: 'fontType' | 'orientation' | 'hexColor', value: any) {
if (!this.form.get(type).disabled) {
this.form.get(type).setValue(value);
await this.configChanged();
}
}
private async _initForm(watermark: Partial<IWatermark>) {
this._watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId };
this.form.patchValue({ ...watermark });
await this._loadViewer();
}
private async _loadViewer() {
if (this._instance) {
return;
}
this._instance = await WebViewer(
{
licenseKey: this._licenseService.activeLicenseKey,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
fullAPI: true,
isReadOnly: true,
backendType: 'ems',
},
document.getElementById('viewer'),
);
this._instance.UI.setTheme(this._userPreferenceService.getTheme());
this._instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
this._loadingService.stop();
await this._drawWatermark();
});
if (environment.production) {
this._instance.Core.setCustomFontURL('https://' + window.location.host + this._convertPath('/assets/pdftron'));
}
this._disableElements();
const request = this._http.get('/assets/pdftron/blank.pdf', {
responseType: 'blob',
});
const blobData = await firstValueFrom(request);
this._instance.UI.loadDocument(blobData, { filename: 'blank.pdf' });
}
private _disableElements() {
this._instance.UI.disableElements(['header', 'toolsHeader', 'pageNavOverlay', 'textPopup']);
}
private async _drawWatermark() {
const pdfNet = this._instance.Core.PDFNet;
const document = await this._instance.Core.documentViewer.getDocument().getPDFDoc();
await stampPDFPage(
document,
pdfNet,
this.form.controls.text.value || '',
this.form.controls.fontSize.value,
this.form.controls.fontType.value,
this.form.controls.orientation.value,
this.form.controls.opacity.value,
this.form.controls.hexColor.value,
[1],
this._licenseService.activeLicenseKey,
);
this._instance.Core.documentViewer.refreshAll();
this._instance.Core.documentViewer.updateView([0], 0);
}
private _getForm() {
get #form() {
const form: FormGroup<AsControl<WatermarkForm>> = this._formBuilder.group({
name: [null],
text: [null],
@ -225,4 +120,122 @@ export class WatermarkScreenComponent {
return form;
}
async ngOnInit() {
await this.#loadViewer();
}
@Debounce()
async configChanged() {
await this.#drawWatermark();
}
async save(): Promise<void> {
const watermark: IWatermark = {
id: this.#watermark.id,
enabled: this.#watermark.id ? this.#watermark.enabled : true,
dossierTemplateId: this.#dossierTemplateId,
...this.form.getRawValue(),
};
this._loadingService.start();
try {
const updatedWatermark = await this._watermarkService.saveWatermark(watermark);
this._toaster.success(
watermark.id ? _('watermark-screen.action.change-success') : _('watermark-screen.action.created-success'),
);
if (!watermark.id) {
await this._router.navigate([`/main/admin/dossier-templates/${this.#dossierTemplateId}/watermarks/${updatedWatermark.id}`]);
}
} catch (error) {
this._toaster.error(_('watermark-screen.action.error'));
}
this._loadingService.stop();
}
async revert() {
this.form.patchValue({ ...this.#watermark });
await this.configChanged();
}
async setValue(type: 'fontType' | 'orientation' | 'hexColor', value: any) {
if (!this.form.get(type).disabled) {
this.form.get(type).setValue(value);
await this.configChanged();
}
}
navigateTo($event: number) {
this.instance.Core.documentViewer.displayPageLocation($event, 0, 0);
}
async #initForm(watermark: Partial<IWatermark>) {
this.#watermark = { ...watermark, dossierTemplateId: this.#dossierTemplateId };
this.form.patchValue({ ...watermark });
}
async #loadViewer() {
this.instance = await WebViewer(
{
licenseKey: this._licenseService.activeLicenseKey,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
fullAPI: true,
isReadOnly: true,
backendType: 'ems',
},
// use nativeElement instead of document.getElementById('viwer')
// because WebViewer works better with this approach
this._viewer.nativeElement,
);
this.instance.UI.setTheme(this._userPreferenceService.getTheme());
this.instance.Core.documentViewer.addEventListener('documentLoaded', async () => {
this.loaded$.next(true);
this._loadingService.stop();
await this.#drawWatermark();
});
if (environment.production) {
this.instance.Core.setCustomFontURL('https://' + window.location.host + this._convertPath('/assets/pdftron'));
}
this.#disableElements();
await this.#loadDocument();
}
async #loadDocument() {
const request = this._http.get('/assets/pdftron/blank.pdf', {
responseType: 'blob',
});
const blobData = await firstValueFrom(request);
this.instance.UI.loadDocument(blobData, { filename: 'blank.pdf' });
}
#disableElements() {
this.instance.UI.disableElements(['header', 'toolsHeader', 'pageNavOverlay', 'textPopup']);
}
async #drawWatermark() {
const pdfNet = this.instance.Core.PDFNet;
const document = await this.instance.Core.documentViewer.getDocument().getPDFDoc();
await stampPDFPage(
document,
pdfNet,
this.form.controls.text.value || '',
this.form.controls.fontSize.value,
this.form.controls.fontType.value,
this.form.controls.orientation.value,
this.form.controls.opacity.value,
this.form.controls.hexColor.value,
[1, 2],
this._licenseService.activeLicenseKey,
);
this.instance.Core.documentViewer.refreshAll();
this.instance.Core.documentViewer.updateView([0, 1], 0);
}
}

View File

@ -1,25 +1,32 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { WatermarkScreenComponent } from './watermark-screen/watermark-screen.component';
import { WatermarksListingScreenComponent } from './watermarks-listing/watermarks-listing-screen.component';
import {
CircleButtonComponent,
CompositeRouteGuard,
HasScrollbarDirective,
IconButtonComponent,
IqserAllowDirective,
IqserAuthGuard,
IqserButtonsModule,
IqserHelpModeModule,
IqserListingModule,
IqserPermissionsModule,
IqserScrollbarModule,
IqserRoutes,
IqserUsersModule,
} from '@iqser/common-ui';
import { RedRoleGuard } from '@users/red-role.guard';
import { WATERMARK_ID } from '@red/domain';
import { WatermarkExistsGuard } from '@guards/watermark-exists.guard';
import { TranslateModule } from '@ngx-translate/core';
import { PaginatorComponent } from './paginator/paginator.component';
import { MatIconModule } from '@angular/material/icon';
import { ReactiveFormsModule } from '@angular/forms';
import { MatLegacySliderModule } from '@angular/material/legacy-slider';
import { ColorPickerModule } from 'ngx-color-picker';
import { MatLegacySlideToggleModule } from '@angular/material/legacy-slide-toggle';
const routes = [
const routes: IqserRoutes = [
{
path: '',
component: WatermarksListingScreenComponent,
@ -47,18 +54,23 @@ const routes = [
];
@NgModule({
declarations: [WatermarkScreenComponent, WatermarksListingScreenComponent],
declarations: [WatermarkScreenComponent, WatermarksListingScreenComponent, PaginatorComponent],
imports: [
RouterModule.forChild(routes),
CommonModule,
SharedModule,
IqserUsersModule,
TranslateModule,
IqserButtonsModule,
IqserListingModule,
IqserScrollbarModule,
IqserHelpModeModule,
IqserPermissionsModule,
MatIconModule,
IconButtonComponent,
ReactiveFormsModule,
MatLegacySliderModule,
ColorPickerModule,
MatLegacySlideToggleModule,
CircleButtonComponent,
HasScrollbarDirective,
IqserAllowDirective,
],
})
export class WatermarkModule {}

View File

@ -14,8 +14,8 @@
<iqser-icon-button
*allow="roles.watermarks.write; if: currentUser.isAdmin"
[iqserHelpMode]="'create_new_watermark'"
[overlappingElements]="['USER_MENU']"
[label]="'watermarks-listing.add-new' | translate"
[overlappingElements]="['USER_MENU']"
[routerLink]="getRouterLink()"
[type]="iconButtonTypes.primary"
icon="iqser:plus"
@ -66,7 +66,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteWatermarkDialog($event, entity)"
(action)="openConfirmDeleteWatermarkDialog(entity)"
*allow="roles.watermarks.write; if: currentUser.isAdmin"
[tooltip]="'watermarks-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"

View File

@ -1,10 +1,10 @@
import { Component } from '@angular/core';
import {
CircleButtonTypes,
ConfirmationDialogInput,
getCurrentUser,
getParam,
IconButtonTypes,
IConfirmationDialogData,
IqserPermissionsService,
ListingComponent,
listingProvidersFactory,
@ -51,13 +51,14 @@ export class WatermarksListingScreenComponent extends ListingComponent<Watermark
this.entitiesService.setEntities(this._watermarksMapService.get(this.#dossierTemplateId));
}
async openConfirmDeleteWatermarkDialog($event: MouseEvent, watermark: Watermark) {
async openConfirmDeleteWatermarkDialog(watermark: Watermark) {
const isUsed = await this._watermarkService.isWatermarkUsed(watermark.id);
const data = new ConfirmationDialogInput({
const data: IConfirmationDialogData = {
question: isUsed ? _('watermarks-listing.watermark-is-used') : null,
});
this._dialogService.openDialog('confirm', $event, data, async () => {
};
this._dialogService.openDialog('confirm', data, async () => {
await this._deleteWatermark(watermark);
});
}

View File

@ -1,27 +1,20 @@
import { Injectable, TemplateRef } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AddEditFileAttributeDialogComponent } from '../dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import { AddEntityDialogComponent } from '../dialogs/add-entity-dialog/add-entity-dialog.component';
import { AddEditCloneDossierTemplateDialogComponent } from '../dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component';
import { EditColorDialogComponent } from '../dialogs/edit-color-dialog/edit-color-dialog.component';
import { SmtpAuthDialogComponent } from '../dialogs/smtp-auth-dialog/smtp-auth-dialog.component';
import { AddEditUserDialogComponent } from '../dialogs/add-edit-user-dialog/add-edit-user-dialog.component';
import { FileAttributesCsvImportDialogComponent } from '../dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component';
import { AddEditDossierAttributeDialogComponent } from '../dialogs/add-edit-dossier-attribute-dialog/add-edit-dossier-attribute-dialog.component';
import {
ConfirmationDialogComponent,
ConfirmationDialogInput,
ConfirmOptions,
DialogConfig,
DialogService,
largeDialogConfig,
IConfirmationDialogData,
LoadingService,
TitleColors,
} from '@iqser/common-ui';
import { UploadDictionaryDialogComponent } from '../dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component';
import { FileAttributesConfigurationsDialogComponent } from '../dialogs/file-attributes-configurations-dialog/file-attributes-configurations-dialog.component';
import { AddEditDossierStateDialogComponent } from '../dialogs/add-edit-dossier-state-dialog/add-edit-dossier-state-dialog.component';
import { ConfirmDeleteDossierStateDialogComponent } from '../dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom, forkJoin } from 'rxjs';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
@ -35,17 +28,11 @@ type DialogType =
| 'confirm'
| 'addEntity'
| 'editColor'
| 'addEditFileAttribute'
| 'importFileAttributes'
| 'fileAttributesConfigurations'
| 'addEditUser'
| 'smtpAuthConfig'
| 'addEditCloneDossierTemplate'
| 'auditInfo'
| 'addEditDossierAttribute'
| 'uploadDictionary'
| 'addEditDossierState'
| 'deleteDossierState'
| 'configureCertificate';
@Injectable()
@ -63,17 +50,6 @@ export class AdminDialogService extends DialogService<DialogType> {
component: EditColorDialogComponent,
dialogConfig: { autoFocus: true },
},
addEditFileAttribute: {
component: AddEditFileAttributeDialogComponent,
dialogConfig: { autoFocus: true },
},
fileAttributesConfigurations: {
component: FileAttributesConfigurationsDialogComponent,
},
importFileAttributes: {
component: FileAttributesCsvImportDialogComponent,
dialogConfig: { ...largeDialogConfig, ...{ disableClose: false } },
},
addEditUser: {
component: AddEditUserDialogComponent,
dialogConfig: { autoFocus: true },
@ -86,19 +62,9 @@ export class AdminDialogService extends DialogService<DialogType> {
component: AddEditCloneDossierTemplateDialogComponent,
dialogConfig: { width: '900px', autoFocus: true },
},
addEditDossierAttribute: {
component: AddEditDossierAttributeDialogComponent,
dialogConfig: { autoFocus: true },
},
uploadDictionary: {
component: UploadDictionaryDialogComponent,
},
addEditDossierState: {
component: AddEditDossierStateDialogComponent,
},
deleteDossierState: {
component: ConfirmDeleteDossierStateDialogComponent,
},
configureCertificate: {
component: ConfigureCertificateDialogComponent,
dialogConfig: { disableClose: false, maxHeight: '100vh' },
@ -118,8 +84,8 @@ export class AdminDialogService extends DialogService<DialogType> {
super(_dialog);
}
deleteUsers(userIds: string[], $event?: MouseEvent, cb?: () => Promise<void> | void): void {
const data = new ConfirmationDialogInput({
deleteUsers(userIds: string[], cb?: () => Promise<void> | void): void {
const data: IConfirmationDialogData = {
title: _('confirm-delete-users.title'),
question: _('confirm-delete-users.warning'),
confirmationText: _('confirm-delete-users.delete'),
@ -131,9 +97,9 @@ export class AdminDialogService extends DialogService<DialogType> {
{ value: false, label: _('confirm-delete-users.impacted-documents') },
],
toastMessage: _('confirm-delete-users.toast-error'),
});
};
this.openDialog('confirm', $event, data, async result => {
this.openDialog('confirm', data, async result => {
if (result === ConfirmOptions.CONFIRM) {
this._loadingService.start();
await firstValueFrom(this._userService.delete(userIds));
@ -148,7 +114,6 @@ export class AdminDialogService extends DialogService<DialogType> {
dossierTemplateId: string,
impactedTemplatesRef: TemplateRef<unknown>,
type: 'dossier' | 'file',
$event: MouseEvent,
cb: () => Promise<void> | void,
): Promise<void> {
this._loadingService.start();
@ -160,7 +125,7 @@ export class AdminDialogService extends DialogService<DialogType> {
const uniqueTemplates = Array.from(templateIds).map(id => templates.find(t => t.templateId === id));
this._loadingService.stop();
const data = new ConfirmationDialogInput({
const data: IConfirmationDialogData = {
title: _('confirm-delete-attribute.title'),
question: _('confirm-delete-attribute.warning'),
confirmationText: _('confirm-delete-attribute.delete'),
@ -184,7 +149,8 @@ export class AdminDialogService extends DialogService<DialogType> {
],
toastMessage: _('confirm-delete-attribute.toast-error'),
translateParams: { reportsCount: uniqueTemplates.length, count: attributes.length, name: attributes[0].label },
});
};
if (templates.length) {
data.checkboxes.push({
value: false,
@ -193,7 +159,8 @@ export class AdminDialogService extends DialogService<DialogType> {
extraContentData: { templates: uniqueTemplates },
});
}
this.openDialog('confirm', $event, data, async () => {
this.openDialog('confirm', data, async () => {
await cb();
});
}

View File

@ -2,10 +2,12 @@ import { Component, HostBinding, Input, OnInit } from '@angular/core';
import { UserPreferenceService } from '@users/user-preference.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { adminSideNavTranslations } from '@translations/admin-side-nav-translations';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router';
import { AdminSideNavType, AdminSideNavTypes, ENTITY_TYPE, User } from '@red/domain';
import { ROLES } from '@users/roles';
import { getCurrentUser, IqserPermissionsService } from '@iqser/common-ui';
import { getCurrentUser, IqserHelpModeModule, IqserPermissionsService, SideNavComponent } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { NgForOf, NgIf } from '@angular/common';
interface NavItem {
readonly label: string;
@ -18,6 +20,8 @@ interface NavItem {
selector: 'redaction-admin-side-nav [type]',
templateUrl: './admin-side-nav.component.html',
styleUrls: ['./admin-side-nav.component.scss'],
standalone: true,
imports: [TranslateModule, NgIf, IqserHelpModeModule, RouterLink, RouterLinkActive, NgForOf, SideNavComponent],
})
export class AdminSideNavComponent implements OnInit {
@Input() type: AdminSideNavType;

View File

@ -1,27 +1,27 @@
<div class="action-buttons" *ngIf="currentUser.isAdmin">
<div *ngIf="currentUser.isAdmin" [id]="'actions-for-' + dossierTemplateId" class="action-buttons">
<div [iqserHelpMode]="'edit_clone_delete_dossier_templates'" [overlappingElements]="['USER_MENU']">
<iqser-circle-button
[buttonId]="'delete-dossier-template-btn-' + dossierTemplateId"
(action)="openDeleteDossierTemplateDialog($event)"
(action)="openDeleteDossierTemplateDialog()"
[buttonId]="'delete-dossier-template-btn'"
[tooltip]="'dossier-templates-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:trash"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-circle-button
[buttonId]="'copy-dossier-template-btn-' + dossierTemplateId"
(action)="openEditCloneDossierTemplateDialog($event, true)"
(action)="openEditCloneDossierTemplateDialog(true)"
[buttonId]="'copy-dossier-template-btn'"
[tooltip]="'dossier-templates-listing.action.clone' | translate"
icon="iqser:copy"
[type]="circleButtonTypes.dark"
icon="iqser:copy"
></iqser-circle-button>
<iqser-circle-button
[buttonId]="'edit-dossier-template-btn-' + dossierTemplateId"
(action)="openEditCloneDossierTemplateDialog($event)"
(action)="openEditCloneDossierTemplateDialog()"
[buttonId]="'edit-dossier-template-btn'"
[tooltip]="'dossier-templates-listing.action.edit' | translate"
icon="iqser:edit"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
</div>
</div>

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