Pull request #284: Table component

Merge in RED/ui from table-component to master

* commit '340b06c7ba5edc767c47d326a0cff11f0512d9cb':
  Some fixes & improvements
  Fixed merge
  Cleanup
  Audit screen
  Search screen
  Documents trash
  Downloads listing
  Trash screen
  Active fields listing
  Dossier attributes listing
  File attributes listing
  Default colors
  User listing
  Dossier overview
  Dossier listing
  Moved empty states, templates listing
  Table component WIP - dictionary listing
This commit is contained in:
Adina Teudan 2021-09-15 23:41:32 +02:00
commit 21d3e8e05a
84 changed files with 1774 additions and 2286 deletions

View File

@ -6,70 +6,15 @@
<div class="red-content-inner">
<div class="content-container">
<iqser-table-header
<iqser-table
[actionsTemplate]="actionsTemplate"
[bulkActions]="bulkActions"
[hasEmptyColumn]="true"
[itemSize]="80"
[noDataText]="'downloads-list.no-data.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"
[text]="'downloads-list.no-data.title' | translate"
icon="red:download"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<!-- Table lines -->
<div *cdkVirtualFor="let download of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div (click)="toggleEntitySelected($event, download)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(download)"></iqser-round-checkbox>
</div>
<div>
<div [class.no-bold]="download.lastDownload" class="table-item-title heading">
{{ download.filename }}
</div>
</div>
<div>
<div class="small-label">
{{ download.size }}
</div>
</div>
<div>
<div class="small-label">
{{ download.creationDate | date: 'd MMM. yyyy, hh:mm a' }}
</div>
</div>
<div>
<div class="small-label">
{{ download.status }}
</div>
</div>
<div class="actions-container">
<div [class.active]="download.inProgress" class="action-buttons">
<iqser-circle-button
(action)="downloadItem(download)"
*ngIf="download.status === 'READY' && !download.inProgress"
[tooltip]="'downloads-list.actions.download' | translate"
icon="red:download"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-circle-button
(action)="deleteItems([download])"
[tooltip]="'downloads-list.actions.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<mat-spinner *ngIf="download.inProgress" diameter="15"></mat-spinner>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
emptyColumnWidth="auto"
noDataIcon="red:download"
></iqser-table>
</div>
</div>
</section>
@ -79,7 +24,60 @@
(action)="deleteItems()"
*ngIf="entitiesService.areSomeSelected$ | async"
[tooltip]="'downloads-list.bulk.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</ng-template>
<ng-template #filenameTemplate let-download="entity">
<div class="cell">
<div [class.no-bold]="download.lastDownload" class="table-item-title heading">
{{ download.filename }}
</div>
</div>
</ng-template>
<ng-template #sizeTemplate let-download="entity">
<div class="cell">
<div class="small-label">
{{ download.size }}
</div>
</div>
</ng-template>
<ng-template #creationDateTemplate let-download="entity">
<div class="cell">
<div class="small-label">
{{ download.creationDate | date: 'd MMM. yyyy, hh:mm a' }}
</div>
</div>
</ng-template>
<ng-template #statusTemplate let-download="entity">
<div class="cell">
<div class="small-label">
{{ download.status }}
</div>
</div>
</ng-template>
<ng-template #actionsTemplate let-download="entity">
<div [class.active]="download.inProgress" class="action-buttons">
<iqser-circle-button
(action)="downloadItem(download)"
*ngIf="download.status === 'READY' && !download.inProgress"
[tooltip]="'downloads-list.actions.download' | translate"
[type]="circleButtonTypes.dark"
icon="red:download"
></iqser-circle-button>
<iqser-circle-button
(action)="deleteItems([download])"
[tooltip]="'downloads-list.actions.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
<mat-spinner *ngIf="download.inProgress" diameter="15"></mat-spinner>
</div>
</ng-template>

View File

@ -1,27 +1,3 @@
.content-container {
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr auto 11px;
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 10px;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr auto;
}
}
}
}
.page-header .actions {
justify-content: flex-end;
}
.no-bold {
font-weight: normal !important;
}

View File

@ -1,4 +1,4 @@
import { Component, Injector, OnInit } from '@angular/core';
import { Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { FileDownloadService } from '@upload-download/services/file-download.service';
import { DownloadStatusWrapper } from '@upload-download/model/download-status.wrapper';
import { DownloadControllerService } from '@redaction/red-ui-http';
@ -11,18 +11,16 @@ import { RouterHistoryService } from '@services/router-history.service';
selector: 'redaction-downloads-list-screen',
templateUrl: './downloads-list-screen.component.html',
styleUrls: ['./downloads-list-screen.component.scss'],
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DownloadsListScreenComponent) }]
})
export class DownloadsListScreenComponent extends ListingComponent<DownloadStatusWrapper> implements OnInit {
readonly itemSize = 80;
readonly circleButtonTypes = CircleButtonTypes;
readonly tableHeaderLabel = _('downloads-list.table-header.title');
readonly tableColumnConfigs: readonly TableColumnConfig<DownloadStatusWrapper>[] = [
{ label: _('downloads-list.table-col-names.name') },
{ label: _('downloads-list.table-col-names.size') },
{ label: _('downloads-list.table-col-names.date') },
{ label: _('downloads-list.table-col-names.status') }
];
tableColumnConfigs: TableColumnConfig<DownloadStatusWrapper>[];
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<never>;
@ViewChild('sizeTemplate', { static: true }) sizeTemplate: TemplateRef<never>;
@ViewChild('creationDateTemplate', { static: true }) creationDateTemplate: TemplateRef<never>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'storageId';
constructor(
@ -36,10 +34,13 @@ export class DownloadsListScreenComponent extends ListingComponent<DownloadStatu
}
async ngOnInit() {
this._loadingService.loadWhile(this._loadData());
this._configureTableColumns();
this._loadingService.start();
await this._loadData();
this.addSubscription = timer(0, 5000).subscribe(async () => {
await this._loadData();
});
this._loadingService.stop();
}
downloadItem(download: DownloadStatusWrapper) {
@ -50,6 +51,15 @@ export class DownloadsListScreenComponent extends ListingComponent<DownloadStatu
this._loadingService.loadWhile(this._deleteItems(downloads));
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{ label: _('downloads-list.table-col-names.name'), width: '2fr', template: this.filenameTemplate },
{ label: _('downloads-list.table-col-names.size'), template: this.sizeTemplate },
{ label: _('downloads-list.table-col-names.date'), template: this.creationDateTemplate },
{ label: _('downloads-list.table-col-names.status'), template: this.statusTemplate }
];
}
private async _deleteItems(downloads?: DownloadStatusWrapper[]) {
const storageIds = (downloads || this.entitiesService.selected).map(d => d.storageId);
await this._downloadControllerService.deleteDownload({ storageIds }).toPromise();

View File

@ -1,11 +1,11 @@
<iqser-circle-button [matMenuTriggerFor]="overlay" [showDot]="hasUnreadNotifications" icon="red:notification"></iqser-circle-button>
<mat-menu #overlay="matMenu" backdropClass="notifications-backdrop" class="notifications-menu" xPosition="before">
<redaction-empty-state
<iqser-empty-state
*ngIf="!groupedNotifications.length"
[horizontalPadding]="40"
[text]="'notifications.no-data' | translate"
[verticalPadding]="0"
></redaction-empty-state>
></iqser-empty-state>
<div *ngFor="let group of groupedNotifications">
<div class="all-caps-label">{{ group.dateString }}</div>

View File

@ -0,0 +1,38 @@
import { AuditModel } from '@redaction/red-ui-http';
import { Listable } from '@iqser/common-ui';
export class AuditModelWrapper implements Listable {
constructor(public auditModel: AuditModel) {}
get category(): string {
return this.auditModel.category;
}
get details(): any {
return this.auditModel.details;
}
get message(): string {
return this.auditModel.message;
}
get recordId(): string {
return this.auditModel.recordId;
}
get recordDate(): string {
return this.auditModel.recordDate;
}
get objectId(): string {
return this.auditModel.objectId;
}
get userId(): string {
return this.auditModel.userId;
}
get id() {
return this.auditModel.recordDate;
}
}

View File

@ -63,8 +63,8 @@
<textarea
[placeholder]="'add-edit-dictionary.form.description-placeholder' | translate"
formControlName="description"
iqserHasScrollbar
name="description"
redactionHasScrollbar
rows="4"
type="text"
></textarea>

View File

@ -1,74 +1,14 @@
<iqser-table-header
<iqser-table
[actionsTemplate]="actionsTemplate"
[bulkActions]="bulkActions"
[hasEmptyColumn]="true"
[itemMouseEnterFn]="itemMouseEnterFn"
[itemMouseLeaveFn]="itemMouseLeaveFn"
[itemSize]="50"
[noDataText]="'file-attributes-csv-import.no-data.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"
[text]="'file-attributes-csv-import.no-data.title' | translate"
icon="red:attribute"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<div
(mouseenter)="setHoveredColumn.emit(field.csvColumn)"
(mouseleave)="setHoveredColumn.emit()"
*cdkVirtualFor="let field of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="table-item"
>
<div (click)="toggleEntitySelected($event, field)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(field)"></iqser-round-checkbox>
</div>
<div>
<iqser-editable-input
(save)="field.name = $event"
[buttonsType]="circleButtonTypes.dark"
[cancelTooltip]="'file-attributes-csv-import.action.cancel-edit-name' | translate"
[class]="'w-200'"
[editTooltip]="'file-attributes-csv-import.action.edit-name' | translate"
[saveTooltip]="'file-attributes-csv-import.action.save-name' | translate"
[value]="field.name"
></iqser-editable-input>
</div>
<div>
<div class="iqser-input-group">
<mat-form-field class="no-label">
<mat-select [(ngModel)]="field.type">
<mat-option *ngFor="let type of typeOptions" [value]="type">
{{ translations[type] | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="center">
<mat-slide-toggle [(ngModel)]="field.readonly" color="primary"></mat-slide-toggle>
</div>
<div class="center">
<iqser-round-checkbox (click)="togglePrimary(field)" [active]="field.primaryAttribute"></iqser-round-checkbox>
</div>
<div class="actions-container">
<div class="action-buttons">
<iqser-circle-button
(action)="field.primaryAttribute = false; toggleFieldActive.emit(field)"
[removeTooltip]="true"
[tooltip]="'file-attributes-csv-import.action.remove' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
emptyColumnWidth="auto"
noDataIcon="red:attribute"
></iqser-table>
<ng-template #bulkActions>
<ng-container *ngIf="entitiesService.areSomeSelected$ | async">
@ -113,3 +53,55 @@
</mat-menu>
</ng-container>
</ng-template>
<ng-template #actionsTemplate let-field="entity">
<div class="action-buttons">
<iqser-circle-button
(action)="field.primaryAttribute = false; toggleFieldActive.emit(field)"
[removeTooltip]="true"
[tooltip]="'file-attributes-csv-import.action.remove' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</ng-template>
<ng-template #labelTemplate let-field="entity">
<div class="cell">
<iqser-editable-input
(save)="field.name = $event"
[buttonsType]="circleButtonTypes.dark"
[cancelTooltip]="'file-attributes-csv-import.action.cancel-edit-name' | translate"
[class]="'w-200'"
[editTooltip]="'file-attributes-csv-import.action.edit-name' | translate"
[saveTooltip]="'file-attributes-csv-import.action.save-name' | translate"
[value]="field.name"
></iqser-editable-input>
</div>
</ng-template>
<ng-template #typeTemplate let-field="entity">
<div class="cell">
<div class="iqser-input-group">
<mat-form-field class="no-label">
<mat-select [(ngModel)]="field.type">
<mat-option *ngFor="let type of typeOptions" [value]="type">
{{ translations[type] | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</ng-template>
<ng-template #readonlyTemplate let-field="entity">
<div class="cell center">
<mat-slide-toggle [(ngModel)]="field.readonly" color="primary"></mat-slide-toggle>
</div>
</ng-template>
<ng-template #primaryTemplate let-field="entity">
<div class="cell center">
<iqser-round-checkbox (click)="togglePrimary(field)" [active]="field.primaryAttribute"></iqser-round-checkbox>
</div>
</ng-template>

View File

@ -1,47 +1,37 @@
@import '../../../../../../assets/styles/variables';
iqser-table-header::ng-deep iqser-table-column-name .name {
padding-left: 22px;
}
:host ::ng-deep iqser-table {
iqser-table-header {
iqser-table-column-name .name {
padding-left: 22px;
}
iqser-table-header::ng-deep .header-item {
box-shadow: none;
border-top: 1px solid $separator;
.header-item {
box-shadow: none;
border-top: 1px solid $separator;
.all-caps-label {
margin-right: 10px;
}
iqser-circle-button {
margin-right: 2px;
}
.separator {
margin-left: 14px;
background-color: $separator;
width: 1px;
height: 30px;
margin-right: 16px;
}
}
cdk-virtual-scroll-viewport {
height: calc(100% - 80px);
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 30px minmax(0, 350px) 150px auto auto auto 11px;
.table-item > div {
height: 50px;
&:not(.scrollbar-placeholder) {
padding-left: 10px;
&.center {
align-items: center;
}
.all-caps-label {
margin-right: 10px;
}
iqser-circle-button {
margin-right: 2px;
}
.separator {
margin-left: 14px;
background-color: $separator;
width: 1px;
height: 30px;
margin-right: 16px;
}
}
}
cdk-virtual-scroll-viewport {
height: calc(100% - 80px) !important;
.cdk-virtual-scroll-content-wrapper .table-item > div.cell {
iqser-editable-input:not(.editing) {
padding-left: 12px;
}
@ -55,10 +45,4 @@ cdk-virtual-scroll-viewport {
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 30px minmax(0, 350px) 150px auto auto auto;
}
}
}

View File

@ -1,4 +1,16 @@
import { Component, EventEmitter, Injector, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import {
Component,
EventEmitter,
forwardRef,
Injector,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
TemplateRef,
ViewChild
} from '@angular/core';
import { Field } from '../file-attributes-csv-import-dialog.component';
import { FileAttributeConfig } from '@redaction/red-ui-http';
import { CircleButtonTypes, DefaultListingServices, ListingComponent, TableColumnConfig } from '@iqser/common-ui';
@ -9,46 +21,36 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
selector: 'redaction-active-fields-listing',
templateUrl: './active-fields-listing.component.html',
styleUrls: ['./active-fields-listing.component.scss'],
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => ActiveFieldsListingComponent) }]
})
export class ActiveFieldsListingComponent extends ListingComponent<Field> implements OnChanges {
protected readonly _primaryKey = 'csvColumn';
export class ActiveFieldsListingComponent extends ListingComponent<Field> implements OnChanges, OnInit {
readonly circleButtonTypes = CircleButtonTypes;
readonly translations = fileAttributeTypesTranslations;
readonly tableHeaderLabel = _('file-attributes-csv-import.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<Field>[] = [
{
label: _('file-attributes-csv-import.table-col-names.name'),
class: 'name'
},
{ label: _('file-attributes-csv-import.table-col-names.type') },
{
label: _('file-attributes-csv-import.table-col-names.read-only'),
class: 'flex-center',
leftIcon: 'red:read-only'
},
{
label: _('file-attributes-csv-import.table-col-names.primary'),
class: 'flex-center',
rightIcon: 'red:status-info',
rightIconTooltip: _('file-attributes-csv-import.table-col-names.primary-info-tooltip')
}
];
tableColumnConfigs: TableColumnConfig<Field>[];
readonly typeOptions = [
FileAttributeConfig.TypeEnum.TEXT,
FileAttributeConfig.TypeEnum.NUMBER,
FileAttributeConfig.TypeEnum.DATE
] as const;
@ViewChild('labelTemplate', { static: true }) labelTemplate: TemplateRef<never>;
@ViewChild('typeTemplate', { static: true }) typeTemplate: TemplateRef<never>;
@ViewChild('readonlyTemplate', { static: true }) readonlyTemplate: TemplateRef<never>;
@ViewChild('primaryTemplate', { static: true }) primaryTemplate: TemplateRef<never>;
@Input() entities: Field[];
@Output() entitiesChange = new EventEmitter<Field[]>();
@Output() setHoveredColumn = new EventEmitter<string>();
@Output() toggleFieldActive = new EventEmitter<Field>();
protected readonly _primaryKey = 'csvColumn';
constructor(protected readonly _injector: Injector) {
super(_injector);
}
ngOnInit(): void {
this._configureTableColumns();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.entities) {
this.entitiesService.setEntities(this.entities);
@ -80,4 +82,38 @@ export class ActiveFieldsListingComponent extends ListingComponent<Field> implem
}
field.primaryAttribute = true;
}
itemMouseEnterFn = (field: Field) => this.setHoveredColumn.emit(field.csvColumn);
itemMouseLeaveFn = () => this.setHoveredColumn.emit();
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('file-attributes-csv-import.table-col-names.name'),
class: 'name',
template: this.labelTemplate,
width: 'minmax(0, 350px)'
},
{
label: _('file-attributes-csv-import.table-col-names.type'),
template: this.typeTemplate,
width: '150px'
},
{
label: _('file-attributes-csv-import.table-col-names.read-only'),
class: 'flex-center',
leftIcon: 'red:read-only',
template: this.readonlyTemplate,
width: 'auto'
},
{
label: _('file-attributes-csv-import.table-col-names.primary'),
class: 'flex-center',
rightIcon: 'red:status-info',
rightIconTooltip: _('file-attributes-csv-import.table-col-names.primary-info-tooltip'),
template: this.primaryTemplate,
width: 'auto'
}
];
}
}

View File

@ -34,6 +34,7 @@ export class FileAttributesCsvImportDialogComponent extends ListingComponent<Fie
keepPreview = false;
columnSample = [];
initialParseConfig: { delimiter?: string; encoding?: string } = {};
readonly tableHeaderLabel = '';
protected readonly _primaryKey = 'csvColumn';
constructor(

View File

@ -20,115 +20,101 @@
</div>
<div class="red-content-inner">
<div class="content-container">
<div class="header-item">
<span class="all-caps-label">
{{ 'audit-screen.table-header.title' | translate: { length: logs?.totalHits || 0 } }}
</span>
<div class="actions-wrapper">
<redaction-pagination
(pageChanged)="pageChanged($event)"
[settings]="{ currentPage: logs?.page || 0, totalPages: totalPages }"
></redaction-pagination>
<div class="separator">·</div>
<form [formGroup]="filterForm">
<div class="iqser-input-group w-150 mr-20">
<mat-form-field class="no-label">
<mat-select formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category">
{{ translations[category] | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="iqser-input-group w-150">
<mat-form-field class="no-label">
<mat-select formControlName="userId">
<mat-select-trigger>
<redaction-initials-avatar
*ngIf="filterForm.get('userId').value !== ALL_USERS"
[userId]="filterForm.get('userId').value"
[withName]="true"
size="small"
></redaction-initials-avatar>
<div *ngIf="filterForm.get('userId').value === ALL_USERS" [translate]="ALL_USERS"></div>
</mat-select-trigger>
<mat-option *ngFor="let userId of userIds" [value]="userId">
<redaction-initials-avatar
*ngIf="userId !== ALL_USERS"
[userId]="userId"
[withName]="true"
size="small"
></redaction-initials-avatar>
<div *ngIf="userId === ALL_USERS" [translate]="ALL_USERS"></div>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="separator">·</div>
<div class="iqser-input-group datepicker-wrapper mr-20">
<input [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>
<mat-datepicker #fromPicker></mat-datepicker>
</div>
<div class="mr-20" translate="audit-screen.to"></div>
<div class="iqser-input-group datepicker-wrapper">
<input [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>
<mat-datepicker #toPicker></mat-datepicker>
</div>
</form>
</div>
</div>
<div class="table-header" iqserSyncWidth="table-item">
<iqser-table-column-name
[label]="'audit-screen.table-col-names.message' | translate"
column="message"
></iqser-table-column-name>
<iqser-table-column-name
[label]="'audit-screen.table-col-names.date' | translate"
column="date"
></iqser-table-column-name>
<iqser-table-column-name
[label]="'audit-screen.table-col-names.user' | translate"
class="user-column"
column="user"
></iqser-table-column-name>
<iqser-table-column-name
[label]="'audit-screen.table-col-names.category' | translate"
column="category"
></iqser-table-column-name>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state
*ngIf="!logs?.totalHits"
[text]="'audit-screen.no-data.title' | translate"
icon="red:document"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div *cdkVirtualFor="let log of logs?.data" class="table-item">
<div>
{{ log.message }}
</div>
<div class="small-label">
{{ log.recordDate | date: 'd MMM. yyyy, hh:mm a' }}
</div>
<div class="user-column">
<redaction-initials-avatar [userId]="log.userId" [withName]="true" size="small"></redaction-initials-avatar>
</div>
<div [translate]="translations[log.category]"></div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
<iqser-table
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataIcon]="'red:document'"
[noDataText]="'audit-screen.no-data.title' | translate"
[totalSize]="logs?.totalHits || 0"
>
</iqser-table>
</div>
</div>
</div>
</section>
<ng-template #headerTemplate>
<div class="table-header-actions">
<redaction-pagination
(pageChanged)="pageChanged($event)"
[settings]="{ currentPage: logs?.page || 0, totalPages: totalPages }"
class="mr-0"
></redaction-pagination>
<div class="separator">·</div>
<form [formGroup]="filterForm">
<div class="iqser-input-group w-150 mr-20">
<mat-form-field class="no-label">
<mat-select formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category">
{{ translations[category] | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="iqser-input-group w-150">
<mat-form-field class="no-label">
<mat-select formControlName="userId">
<mat-select-trigger>
<redaction-initials-avatar
*ngIf="filterForm.get('userId').value !== ALL_USERS"
[userId]="filterForm.get('userId').value"
[withName]="true"
size="small"
></redaction-initials-avatar>
<div *ngIf="filterForm.get('userId').value === ALL_USERS" [translate]="ALL_USERS"></div>
</mat-select-trigger>
<mat-option *ngFor="let userId of userIds" [value]="userId">
<redaction-initials-avatar
*ngIf="userId !== ALL_USERS"
[userId]="userId"
[withName]="true"
size="small"
></redaction-initials-avatar>
<div *ngIf="userId === ALL_USERS" [translate]="ALL_USERS"></div>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="separator">·</div>
<div class="iqser-input-group datepicker-wrapper mr-20">
<input [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>
<mat-datepicker #fromPicker></mat-datepicker>
</div>
<div class="mr-20" translate="audit-screen.to"></div>
<div class="iqser-input-group datepicker-wrapper">
<input [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>
<mat-datepicker #toPicker></mat-datepicker>
</div>
</form>
</div>
</ng-template>
<ng-template #messageTemplate let-log="entity">
<div class="cell">
{{ log.message }}
</div>
</ng-template>
<ng-template #dateTemplate let-log="entity">
<div class="small-label cell">
{{ log.recordDate | date: 'd MMM. yyyy, hh:mm a' }}
</div>
</ng-template>
<ng-template #userTemplate let-log="entity">
<div class="user-column cell">
<redaction-initials-avatar [userId]="log.userId" [withName]="true" size="small"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #categoryTemplate let-log="entity">
<div [translate]="translations[log.category]" class="cell"></div>
</ng-template>

View File

@ -1,44 +1,27 @@
.content-container {
.header-item {
justify-content: space-between;
}
:host ::ng-deep iqser-table iqser-table-header .header-item {
justify-content: space-between;
}
.actions-wrapper,
form {
display: flex;
align-items: center;
form {
display: flex;
align-items: center;
.iqser-input-group {
margin-top: 0 !important;
}
.separator {
margin: 0 20px;
font-weight: bold;
font-size: 16px;
opacity: 0.7;
}
.mr-20 {
margin-right: 20px;
}
}
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 1fr 1fr 1fr 1fr 11px;
.table-item {
> div {
padding: 0 24px;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
}
.iqser-input-group {
margin-top: 0 !important;
}
}
.separator {
margin: 0 20px;
font-weight: bold;
font-size: 16px;
opacity: 0.7;
}
.mr-0 {
margin-right: 0;
}
.mr-20 {
margin-right: 20px;
}

View File

@ -1,41 +1,49 @@
import { Component, OnDestroy } from '@angular/core';
import { Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuditControllerService, AuditResponse, AuditSearchRequest } from '@redaction/red-ui-http';
import { AuditControllerService, AuditModel, AuditResponse, AuditSearchRequest } from '@redaction/red-ui-http';
import { Moment } from 'moment';
import { applyIntervalConstraints } from '@utils/date-inputs-utils';
import { AutoUnsubscribe, LoadingService } from '@iqser/common-ui';
import { DefaultListingServices, KeysOf, ListingComponent, LoadingService, TableColumnConfig } from '@iqser/common-ui';
import { auditCategoriesTranslations } from '../../translations/audit-categories-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { AuditModelWrapper } from '../../../../models/audit-model-wrapper.model';
const PAGE_SIZE = 50;
@Component({
selector: 'redaction-audit-screen',
templateUrl: './audit-screen.component.html',
styleUrls: ['./audit-screen.component.scss']
styleUrls: ['./audit-screen.component.scss'],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => AuditScreenComponent) }]
})
export class AuditScreenComponent extends AutoUnsubscribe implements OnDestroy {
export class AuditScreenComponent extends ListingComponent<AuditModelWrapper> implements OnDestroy, OnInit {
readonly ALL_CATEGORIES = 'allCategories';
readonly ALL_USERS = _('audit-screen.all-users');
readonly translations = auditCategoriesTranslations;
readonly currentUser = this._userService.currentUser;
@ViewChild('messageTemplate', { static: true }) messageTemplate: TemplateRef<never>;
@ViewChild('dateTemplate', { static: true }) dateTemplate: TemplateRef<never>;
@ViewChild('userTemplate', { static: true }) userTemplate: TemplateRef<never>;
@ViewChild('categoryTemplate', { static: true }) categoryTemplate: TemplateRef<never>;
filterForm: FormGroup;
categories: string[] = [];
userIds: Set<string>;
logs: AuditResponse;
tableColumnConfigs: TableColumnConfig<AuditModelWrapper>[];
readonly tableHeaderLabel = _('audit-screen.table-header.title');
protected readonly _primaryKey: KeysOf<AuditModelWrapper> = 'recordDate';
private _previousFrom: Moment;
private _previousTo: Moment;
constructor(
private readonly _userService: UserService,
protected readonly _injector: Injector,
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _auditControllerService: AuditControllerService
) {
super();
super(_injector);
this.filterForm = this._formBuilder.group({
category: [this.ALL_CATEGORIES],
userId: [this.ALL_USERS],
@ -43,13 +51,11 @@ export class AuditScreenComponent extends AutoUnsubscribe implements OnDestroy {
to: []
});
this.addSubscription = this.filterForm.valueChanges.subscribe(value => {
this.addSubscription = this.filterForm.valueChanges.subscribe(async value => {
if (!this._updateDateFilters(value)) {
this._fetchData();
await this._fetchData();
}
});
this._fetchData();
}
get totalPages(): number {
@ -59,8 +65,35 @@ export class AuditScreenComponent extends AutoUnsubscribe implements OnDestroy {
return Math.ceil(this.logs.totalHits / PAGE_SIZE);
}
pageChanged(page: number) {
this._fetchData(page);
async pageChanged(page: number) {
await this._fetchData(page);
}
async ngOnInit() {
this._configureTableColumns();
await this._fetchData();
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('audit-screen.table-col-names.message'),
template: this.messageTemplate
},
{
label: _('audit-screen.table-col-names.date'),
template: this.dateTemplate
},
{
label: _('audit-screen.table-col-names.user'),
class: 'user-column',
template: this.userTemplate
},
{
label: _('audit-screen.table-col-names.category'),
template: this.categoryTemplate
}
];
}
private _updateDateFilters(value): boolean {
@ -73,7 +106,7 @@ export class AuditScreenComponent extends AutoUnsubscribe implements OnDestroy {
return false;
}
private _fetchData(page?: number) {
private async _fetchData(page?: number) {
this._loadingService.start();
const promises = [];
const category = this.filterForm.get('category').value;
@ -96,15 +129,16 @@ export class AuditScreenComponent extends AutoUnsubscribe implements OnDestroy {
promises.push(this._auditControllerService.getAuditCategories().toPromise());
promises.push(this._auditControllerService.searchAuditLog(logsRequestBody).toPromise());
Promise.all(promises).then(data => {
this.categories = data[0].map(c => c.category);
this.categories.splice(0, 0, this.ALL_CATEGORIES);
this.logs = data[1];
this.userIds = new Set<string>([this.ALL_USERS]);
for (const id of this.logs.data.map(log => log.userId).filter(uid => !!uid)) {
this.userIds.add(id);
}
this._loadingService.stop();
});
const data = await Promise.all(promises);
this.categories = data[0].map(c => c.category);
this.categories.splice(0, 0, this.ALL_CATEGORIES);
this.logs = data[1];
const entities = this.logs.data.map((log: AuditModel) => new AuditModelWrapper(log));
this.entitiesService.setEntities(entities);
this.userIds = new Set<string>([this.ALL_USERS]);
for (const id of this.logs.data.map(log => log.userId).filter(uid => !!uid)) {
this.userIds.add(id);
}
this._loadingService.stop();
}
}

View File

@ -20,36 +20,31 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table-header
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div *cdkVirtualFor="let color of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div>
<div [translate]="translations[color.key]" class="table-item-title heading"></div>
</div>
<div class="color-wrapper">
<div [ngStyle]="{ 'background-color': color.value }" class="color-square"></div>
</div>
<div class="actions-container">
<div class="action-buttons">
<iqser-circle-button
(action)="openEditColorDialog($event, color)"
*ngIf="currentUser.isAdmin"
[tooltip]="'default-colors-screen.action.edit' | translate"
icon="iqser:edit"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
<iqser-table [actionsTemplate]="actionsTemplate" [itemSize]="80" emptyColumnWidth="2fr"></iqser-table>
</div>
</div>
</section>
<ng-template #nameTemplate let-color="entity">
<div class="cell">
<div [translate]="translations[color.key]" class="table-item-title heading"></div>
</div>
</ng-template>
<ng-template #colorTemplate let-color="entity">
<div class="cell color-wrapper">
<div [ngStyle]="{ 'background-color': color.value }" class="color-square"></div>
</div>
</ng-template>
<ng-template #actionsTemplate let-color="entity">
<div class="action-buttons">
<iqser-circle-button
(action)="openEditColorDialog($event, color)"
*ngIf="currentUser.isAdmin"
[tooltip]="'default-colors-screen.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
</div>
</ng-template>

View File

@ -1,30 +1,15 @@
.content-container cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 2fr 1fr 2fr 11px;
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell {
&:not(.scrollbar-placeholder) {
padding-left: 24px;
}
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 24px;
}
&.color-wrapper {
align-items: center;
.color-wrapper {
align-items: center;
.color-square {
width: 16px;
height: 16px;
min-width: 16px;
}
}
.color-square {
width: 16px;
height: 16px;
min-width: 16px;
}
}
&.has-scrollbar:hover ::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 2fr 1fr 2fr;
}
}
.page-header .actions {
display: flex;
justify-content: flex-end;
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { Colors, DictionaryControllerService } from '@redaction/red-ui-http';
import { ActivatedRoute } from '@angular/router';
@ -18,20 +18,16 @@ interface ListItem extends Listable {
templateUrl: './default-colors-screen.component.html',
styleUrls: ['./default-colors-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DefaultColorsScreenComponent) }]
})
export class DefaultColorsScreenComponent extends ListingComponent<ListItem> implements OnInit {
@ViewChild('nameTemplate', { static: true }) nameTemplate: TemplateRef<never>;
@ViewChild('colorTemplate', { static: true }) colorTemplate: TemplateRef<never>;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
readonly translations = defaultColorsTranslations;
readonly tableHeaderLabel = _('default-colors-screen.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<ListItem>[] = [
{
label: _('default-colors-screen.table-col-names.key'),
sortByKey: 'key'
},
{ label: _('default-colors-screen.table-col-names.color'), class: 'flex-center' }
];
tableColumnConfigs: TableColumnConfig<ListItem>[];
protected readonly _primaryKey = 'key';
private _colorsObj: Colors;
@ -49,6 +45,7 @@ export class DefaultColorsScreenComponent extends ListingComponent<ListItem> imp
}
async ngOnInit() {
this._configureTableColumns();
await this._loadColors();
}
@ -68,6 +65,22 @@ export class DefaultColorsScreenComponent extends ListingComponent<ListItem> imp
);
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('default-colors-screen.table-col-names.key'),
sortByKey: 'key',
template: this.nameTemplate,
width: '2fr'
},
{
label: _('default-colors-screen.table-col-names.color'),
class: 'flex-center',
template: this.colorTemplate
}
];
}
private async _loadColors() {
this._loadingService.start();
const data = await this._dictionaryControllerService.getColors(this._appStateService.activeDossierTemplateId).toPromise();

View File

@ -20,88 +20,23 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table-header
<iqser-table
(noDataAction)="openAddEditDictionaryDialog()"
[actionsTemplate]="actionsTemplate"
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataButtonLabel]="'dictionary-listing.no-data.action' | translate"
[noDataText]="'dictionary-listing.no-data.title' | translate"
[noMatchText]="'dictionary-listing.no-match.title' | translate"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state
(action)="openAddEditDictionaryDialog()"
*ngIf="entitiesService.noData$ | async"
[buttonLabel]="'dictionary-listing.no-data.action' | translate"
[showButton]="currentUser.isAdmin"
[text]="'dictionary-listing.no-data.title' | translate"
icon="red:dictionary"
></redaction-empty-state>
<redaction-empty-state
*ngIf="noMatch$ | async"
[text]="'dictionary-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div
*cdkVirtualFor="let dict of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[routerLink]="[dict.type]"
class="table-item pointer"
>
<div (click)="toggleEntitySelected($event, dict)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(dict)"></iqser-round-checkbox>
</div>
<div>
<div [ngStyle]="{ 'background-color': dict.hexColor }" class="color-square"></div>
<div class="dict-name">
<div class="table-item-title heading">
{{ dict.label }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ dict.entries?.length }}
</div>
<div *ngIf="!dict.caseInsensitive">
<mat-icon svgIcon="red:case-sensitive"></mat-icon>
{{ 'dictionary-listing.case-sensitive' | translate }}
</div>
</div>
</div>
</div>
<div class="center small-label">
{{ dict.rank }}
</div>
<div class="center">
<redaction-annotation-icon [dictType]="dict" [type]="dict.hint ? 'circle' : 'square'"></redaction-annotation-icon>
</div>
<div class="actions-container">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openDeleteDictionariesDialog($event, [dict])"
[tooltip]="'dictionary-listing.action.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-circle-button
(action)="openAddEditDictionaryDialog($event, dict)"
[tooltip]="'dictionary-listing.action.edit' | translate"
icon="iqser:edit"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
[showNoDataButton]="currentUser.isAdmin"
emptyColumnWidth="1fr"
noDataIcon="red:dictionary"
></iqser-table>
</div>
<div class="right-container" redactionHasScrollbar>
<div class="right-container" iqserHasScrollbar>
<redaction-simple-doughnut-chart
*ngIf="(entitiesService.noData$ | async) === false"
[config]="chartData"
@ -120,11 +55,13 @@
(action)="openDeleteDictionariesDialog($event)"
*ngIf="currentUser.isAdmin && (entitiesService.areSomeSelected$ | async)"
[tooltip]="'dictionary-listing.bulk.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</ng-template>
<div class="attributes-actions-container">
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dictionary-listing.search' | translate"
@ -134,9 +71,60 @@
(action)="openAddEditDictionaryDialog()"
*ngIf="currentUser.isAdmin"
[label]="'dictionary-listing.add-new' | translate"
icon="red:plus"
[type]="iconButtonTypes.primary"
icon="red:plus"
></iqser-icon-button>
</div>
</div>
</ng-template>
<ng-template #labelTemplate let-entity="entity">
<div class="cell">
<div [ngStyle]="{ 'background-color': entity.hexColor }" class="color-square"></div>
<div class="dict-name">
<div class="table-item-title heading">
{{ entity.label }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
{{ entity.entries?.length }}
</div>
<div *ngIf="!entity.caseInsensitive">
<mat-icon svgIcon="red:case-sensitive"></mat-icon>
{{ 'dictionary-listing.case-sensitive' | translate }}
</div>
</div>
</div>
</div>
</ng-template>
<ng-template #rankTemplate let-dict="entity">
<div class="cell center small-label">
{{ dict.rank }}
</div>
</ng-template>
<ng-template #iconTemplate let-dict="entity">
<div class="cell center">
<redaction-annotation-icon [dictType]="dict" [type]="dict.hint ? 'circle' : 'square'"></redaction-annotation-icon>
</div>
</ng-template>
<ng-template #actionsTemplate let-dict="entity">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openDeleteDictionariesDialog($event, [dict])"
[tooltip]="'dictionary-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
<iqser-circle-button
(action)="openAddEditDictionaryDialog($event, dict)"
[tooltip]="'dictionary-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
</div>
</ng-template>

View File

@ -1,58 +1,39 @@
iqser-table-header::ng-deep .header-item {
padding-right: 16px;
}
:host {
::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell {
flex-direction: row;
align-items: center;
justify-content: flex-start;
.content-container cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr 11px;
&.center {
justify-content: center;
}
.table-item > div:not(.scrollbar-placeholder) {
display: flex;
flex-direction: row;
padding-left: 10px;
align-items: center;
justify-content: flex-start;
.color-square {
width: 16px;
height: 16px;
min-width: 16px;
margin-right: 16px;
}
&.center {
justify-content: center;
}
.dict-name {
z-index: 1;
max-width: 100%;
}
.color-square {
width: 16px;
height: 16px;
min-width: 16px;
margin-right: 16px;
}
.dict-name {
z-index: 1;
max-width: 100%;
}
.stats-subtitle {
margin-top: 4px;
}
.stats-subtitle {
margin-top: 4px;
}
}
&.has-scrollbar:hover ::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr;
.right-container {
display: flex;
width: 353px;
min-width: 353px;
justify-content: center;
padding: 50px 20px 30px 20px;
&.has-scrollbar:hover {
padding-right: 9px;
}
}
}
.right-container {
display: flex;
width: 353px;
min-width: 353px;
justify-content: center;
padding: 50px 20px 30px 20px;
&.has-scrollbar:hover {
padding-right: 9px;
}
}
.page-header .actions {
display: flex;
justify-content: flex-end;
}

View File

@ -1,4 +1,4 @@
import { Component, Injector, OnInit } from '@angular/core';
import { Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { DictionaryControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
@ -29,29 +29,18 @@ const toChartConfig = (dict: TypeValueWrapper): DoughnutChartConfig => ({
@Component({
templateUrl: './dictionary-listing-screen.component.html',
styleUrls: ['./dictionary-listing-screen.component.scss'],
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DictionaryListingScreenComponent) }]
})
export class DictionaryListingScreenComponent extends ListingComponent<TypeValueWrapper> implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
readonly tableHeaderLabel = _('dictionary-listing.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<TypeValueWrapper>[] = [
{
label: _('dictionary-listing.table-col-names.type'),
sortByKey: 'label'
},
{
label: _('dictionary-listing.table-col-names.order-of-importance'),
sortByKey: 'rank',
class: 'flex-center'
},
{
label: _('dictionary-listing.table-col-names.hint-redaction'),
class: 'flex-center'
}
];
tableColumnConfigs: TableColumnConfig<TypeValueWrapper>[];
chartData: DoughnutChartConfig[] = [];
@ViewChild('labelTemplate', { static: true }) labelTemplate: TemplateRef<never>;
@ViewChild('rankTemplate', { static: true }) rankTemplate: TemplateRef<never>;
@ViewChild('iconTemplate', { static: true }) iconTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'type';
constructor(
@ -69,7 +58,10 @@ export class DictionaryListingScreenComponent extends ListingComponent<TypeValue
_appStateService.activateDossierTemplate(_activatedRoute.snapshot.params.dossierTemplateId);
}
routerLinkFn = (entity: TypeValueWrapper) => [entity.type];
ngOnInit(): void {
this._configureTableColumns();
this._loadDictionaryData();
}
@ -108,6 +100,28 @@ export class DictionaryListingScreenComponent extends ListingComponent<TypeValue
);
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('dictionary-listing.table-col-names.type'),
sortByKey: 'label',
width: '2fr',
template: this.labelTemplate
},
{
label: _('dictionary-listing.table-col-names.order-of-importance'),
sortByKey: 'rank',
class: 'flex-center',
template: this.rankTemplate
},
{
label: _('dictionary-listing.table-col-names.hint-redaction'),
class: 'flex-center',
template: this.iconTemplate
}
];
}
private _loadDictionaryData(loadEntries = true): void {
const appStateDictionaryData = this._appStateService.dictionaryData[this._appStateService.activeDossierTemplateId];
const entities = Object.values(appStateDictionaryData).filter(d => !d.virtual);

View File

@ -16,13 +16,13 @@
<form *ngIf="digitalSignatureForm" [formGroup]="digitalSignatureForm" autocomplete="off">
<input #fileInput (change)="fileChanged($event, fileInput)" class="file-upload-input" hidden type="file" />
<redaction-empty-state
<iqser-empty-state
(action)="fileInput.click()"
*ngIf="!hasDigitalSignatureSet"
[buttonLabel]="'digital-signature-screen.no-data.action' | translate"
[text]="'digital-signature-screen.no-data.title' | translate"
buttonIcon="red:upload"
></redaction-empty-state>
></iqser-empty-state>
<div [class.hidden]="!hasDigitalSignatureSet" class="iqser-input-group required w-300">
<label translate="digital-signature-screen.certificate-name.label"></label>

View File

@ -20,68 +20,20 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table-header
<iqser-table
(noDataAction)="openAddEditAttributeDialog(null)"
[actionsTemplate]="actionsTemplate"
[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]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state
(action)="openAddEditAttributeDialog($event)"
*ngIf="entitiesService.noData$ | async"
[buttonLabel]="'dossier-attributes-listing.no-data.action' | translate"
[showButton]="currentUser.isAdmin"
[text]="'dossier-attributes-listing.no-data.title' | translate"
icon="red:attribute"
></redaction-empty-state>
<redaction-empty-state
*ngIf="noMatch$ | async"
[text]="'dossier-attributes-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<div
*cdkVirtualFor="let attribute of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="table-item pointer"
>
<div (click)="toggleEntitySelected($event, attribute)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(attribute)"></iqser-round-checkbox>
</div>
<div>
{{ attribute.label }}
</div>
<div class="small-label">
{{ attribute.placeholder }}
</div>
<div class="small-label">
{{ translations[attribute.type] | translate }}
</div>
<div class="actions-container">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openAddEditAttributeDialog($event, attribute)"
[tooltip]="'dossier-attributes-listing.action.edit' | translate"
icon="iqser:edit"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteAttributeDialog($event, attribute)"
[tooltip]="'dossier-attributes-listing.action.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
[showNoDataButton]="currentUser.isAdmin"
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
</div>
</section>
@ -91,11 +43,13 @@
(action)="openConfirmDeleteAttributeDialog($event)"
*ngIf="currentUser.isAdmin && entitiesService.areSomeSelected$ | async"
[tooltip]="'dossier-attributes-listing.bulk.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</ng-template>
<div class="attributes-actions-container">
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-attributes-listing.search' | translate"
@ -105,8 +59,44 @@
(action)="openAddEditAttributeDialog($event)"
*ngIf="currentUser.isAdmin"
[label]="'dossier-attributes-listing.add-new' | translate"
icon="red:plus"
[type]="iconButtonTypes.primary"
icon="red:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #actionsTemplate let-attribute="entity">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openAddEditAttributeDialog($event, attribute)"
[tooltip]="'dossier-attributes-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteAttributeDialog($event, attribute)"
[tooltip]="'dossier-attributes-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</ng-template>
<ng-template #labelTemplate let-attribute="entity">
<div class="cell">
{{ attribute.label }}
</div>
</ng-template>
<ng-template #placeholderTemplate let-attribute="entity">
<div class="cell small-label">
{{ attribute.placeholder }}
</div>
</ng-template>
<ng-template #typeTemplate let-attribute="entity">
<div class="cell small-label">
{{ translations[attribute.type] | translate }}
</div>
</ng-template>

View File

@ -1,34 +0,0 @@
.page-header .actions {
display: flex;
justify-content: flex-end;
}
iqser-table-header::ng-deep .attributes-actions-container {
display: flex;
flex: 1;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 10px;
}
}
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 2fr 1fr 1fr 11px;
.table-item > div {
height: 50px;
&:not(.scrollbar-placeholder) {
padding-left: 10px;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 2fr 1fr 1fr;
}
}
}

View File

@ -1,4 +1,4 @@
import { Component, Injector, OnInit } from '@angular/core';
import { Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import {
CircleButtonTypes,
DefaultListingServices,
@ -19,7 +19,10 @@ import { UserService } from '@services/user.service';
@Component({
templateUrl: './dossier-attributes-listing-screen.component.html',
styleUrls: ['./dossier-attributes-listing-screen.component.scss'],
providers: [...DefaultListingServices]
providers: [
...DefaultListingServices,
{ provide: ListingComponent, useExisting: forwardRef(() => DossierAttributesListingScreenComponent) }
]
})
export class DossierAttributesListingScreenComponent extends ListingComponent<DossierAttributeConfig> implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
@ -27,17 +30,10 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
readonly currentUser = this._userService.currentUser;
readonly translations = dossierAttributeTypesTranslations;
readonly tableHeaderLabel = _('dossier-attributes-listing.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<DossierAttributeConfig>[] = [
{
label: _('dossier-attributes-listing.table-col-names.label'),
sortByKey: 'label'
},
{ label: _('dossier-attributes-listing.table-col-names.placeholder') },
{
label: _('dossier-attributes-listing.table-col-names.type'),
sortByKey: 'type'
}
];
tableColumnConfigs: TableColumnConfig<DossierAttributeConfig>[];
@ViewChild('labelTemplate', { static: true }) labelTemplate: TemplateRef<never>;
@ViewChild('placeholderTemplate', { static: true }) placeholderTemplate: TemplateRef<never>;
@ViewChild('typeTemplate', { static: true }) typeTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'label';
constructor(
@ -54,6 +50,7 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
}
async ngOnInit() {
this._configureTableColumns();
await this._loadData();
}
@ -62,6 +59,7 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
this._loadingService.start();
const ids = dossierAttribute ? [dossierAttribute.id] : this.entitiesService.selected.map(item => item.id);
await this._dossierAttributesService.deleteConfigs(ids);
this.entitiesService.setSelected([]);
await this._loadData();
});
}
@ -77,6 +75,27 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
);
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('dossier-attributes-listing.table-col-names.label'),
sortByKey: 'label',
width: '2fr',
template: this.labelTemplate
},
{
label: _('dossier-attributes-listing.table-col-names.placeholder'),
width: '2fr',
template: this.placeholderTemplate
},
{
label: _('dossier-attributes-listing.table-col-names.type'),
sortByKey: 'type',
template: this.typeTemplate
}
];
}
private async _loadData() {
this._loadingService.start();
const attributes = await this._dossierAttributesService.getConfig();

View File

@ -12,83 +12,15 @@
<div class="red-content-inner">
<div class="content-container">
<iqser-table-header
<iqser-table
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'dossier-templates-listing.no-data.title' | translate"
[noMatchText]="'dossier-templates-listing.no-match.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
>
<div class="actions flex-1">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-templates-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddDossierTemplateDialog()"
*ngIf="currentUser.isAdmin && userPreferenceService.areDevFeaturesEnabled"
[label]="'dossier-templates-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="red:plus"
></iqser-icon-button>
</div>
</iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"
[text]="'dossier-templates-listing.no-data.title' | translate"
icon="red:template"
></redaction-empty-state>
<redaction-empty-state
*ngIf="noMatch$ | async"
[text]="'dossier-templates-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div
*cdkVirtualFor="let dossierTemplate of sortedDisplayedEntities$ | async"
[routerLink]="[dossierTemplate.dossierTemplateId, 'dictionaries']"
class="table-item pointer"
>
<div (click)="toggleEntitySelected($event, dossierTemplate)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(dossierTemplate)"></iqser-round-checkbox>
</div>
<div>
<div class="table-item-title heading">
{{ dossierTemplate.name }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:dictionary"></mat-icon>
{{
'dossier-templates-listing.dictionaries' | translate: { length: dossierTemplate.dictionariesCount }
}}
</div>
</div>
</div>
<div class="user-column">
<redaction-initials-avatar [userId]="dossierTemplate.createdBy" [withName]="true"></redaction-initials-avatar>
</div>
<div class="small-label">
{{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }}
</div>
<div>
<div class="small-label">
{{ dossierTemplate.dateModified | date: 'd MMM. yyyy' }}
</div>
<redaction-dossier-template-actions
(loadDossierTemplatesData)="loadDossierTemplatesData()"
[dossierTemplateId]="dossierTemplate.dossierTemplateId"
class="actions-container"
></redaction-dossier-template-actions>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
noDataIcon="red:template"
></iqser-table>
</div>
</div>
</div>
@ -103,3 +35,63 @@
icon="red:trash"
></iqser-circle-button>
</ng-template>
<ng-template #actionsTemplate let-dossierTemplate="entity">
<redaction-dossier-template-actions
(loadDossierTemplatesData)="loadDossierTemplatesData()"
[dossierTemplateId]="dossierTemplate.dossierTemplateId"
class="actions-container"
></redaction-dossier-template-actions>
</ng-template>
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'dossier-templates-listing.search' | translate"
></iqser-input-with-action>
<iqser-icon-button
(action)="openAddDossierTemplateDialog()"
*ngIf="currentUser.isAdmin && userPreferenceService.areDevFeaturesEnabled"
[label]="'dossier-templates-listing.add-new' | translate"
[type]="iconButtonTypes.primary"
icon="red:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #nameTemplate let-dossierTemplate="entity">
<div class="cell">
<div class="table-item-title heading">
{{ dossierTemplate.name }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:dictionary"></mat-icon>
{{ 'dossier-templates-listing.dictionaries' | translate: { length: dossierTemplate.dictionariesCount } }}
</div>
</div>
</div>
</ng-template>
<ng-template #userTemplate let-dossierTemplate="entity">
<div class="cell user-column">
<redaction-initials-avatar [userId]="dossierTemplate.createdBy" [withName]="true"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #dateAddedTemplate let-dossierTemplate="entity">
<div class="cell small-label">
{{ dossierTemplate.dateAdded | date: 'd MMM. yyyy' }}
</div>
</ng-template>
<ng-template #dateModifiedTemplate let-dossierTemplate="entity">
<div class="cell">
<div class="small-label">
{{ dossierTemplate.dateModified | date: 'd MMM. yyyy' }}
</div>
<ng-container *ngTemplateOutlet="actionsTemplate; context: { entity: dossierTemplate }"></ng-container>
</div>
</ng-template>

View File

@ -1,29 +1,15 @@
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 1fr 1fr 11px;
:host {
::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell {
.stats-subtitle {
margin-top: 4px;
}
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 10px;
.stats-subtitle {
margin-top: 4px;
}
.table-item-title {
max-width: 100%;
}
}
.table-item-title {
max-width: 100%;
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 1fr 1fr;
}
.page-header .actions > *:not(:last-child) {
margin-right: 16px;
}
}
.page-header .actions > *:not(:last-child) {
margin-right: 16px;
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { AdminDialogService } from '../../services/admin-dialog.service';
@ -21,28 +21,21 @@ import { RouterHistoryService } from '@services/router-history.service';
templateUrl: './dossier-templates-listing-screen.component.html',
styleUrls: ['./dossier-templates-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [...DefaultListingServices]
providers: [
...DefaultListingServices,
{ provide: ListingComponent, useExisting: forwardRef(() => DossierTemplatesListingScreenComponent) }
]
})
export class DossierTemplatesListingScreenComponent extends ListingComponent<DossierTemplateModelWrapper> implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
readonly tableHeaderLabel = _('dossier-templates-listing.table-header.title');
readonly tableColumnConfigs: readonly TableColumnConfig<DossierTemplateModelWrapper>[] = [
{
label: _('dossier-templates-listing.table-col-names.name'),
sortByKey: 'name'
},
{ label: _('dossier-templates-listing.table-col-names.created-by'), class: 'user-column' },
{
label: _('dossier-templates-listing.table-col-names.created-on'),
sortByKey: 'dateAdded'
},
{
label: _('dossier-templates-listing.table-col-names.modified-on'),
sortByKey: 'dateModified'
}
];
tableColumnConfigs: TableColumnConfig<DossierTemplateModelWrapper>[];
@ViewChild('nameTemplate', { static: true }) nameTemplate: TemplateRef<never>;
@ViewChild('userTemplate', { static: true }) userTemplate: TemplateRef<never>;
@ViewChild('dateAddedTemplate', { static: true }) dateAddedTemplate: TemplateRef<never>;
@ViewChild('dateModifiedTemplate', { static: true }) dateModifiedTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'name';
constructor(
@ -59,7 +52,10 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
super(_injector);
}
routerLinkFn = (dossierTemplate: DossierTemplateModelWrapper) => [dossierTemplate.dossierTemplateId, 'dictionaries'];
ngOnInit(): void {
this._configureTableColumns();
this.loadDossierTemplatesData();
}
@ -81,6 +77,31 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
});
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('dossier-templates-listing.table-col-names.name'),
sortByKey: 'name',
template: this.nameTemplate
},
{
label: _('dossier-templates-listing.table-col-names.created-by'),
class: 'user-column',
template: this.userTemplate
},
{
label: _('dossier-templates-listing.table-col-names.created-on'),
sortByKey: 'dateAdded',
template: this.dateAddedTemplate
},
{
label: _('dossier-templates-listing.table-col-names.modified-on'),
sortByKey: 'dateModified',
template: this.dateModifiedTemplate
}
];
}
private async _deleteTemplates(templateIds: string[] = this.entitiesService.selected.map(d => d.dossierTemplateId)) {
await this._dossierTemplateControllerService
.deleteDossierTemplates(templateIds)

View File

@ -20,76 +20,17 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<iqser-table-header
<iqser-table
[actionsTemplate]="actionsTemplate"
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="80"
[noDataText]="'file-attributes-listing.no-data.title' | translate"
[noMatchText]="'file-attributes-listing.no-match.title' | translate"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"
[text]="'file-attributes-listing.no-data.title' | translate"
icon="red:attribute"
></redaction-empty-state>
<redaction-empty-state
*ngIf="noMatch$ | async"
[text]="'file-attributes-listing.no-match.title' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div *cdkVirtualFor="let attribute of sortedDisplayedEntities$ | async" class="table-item">
<div (click)="toggleEntitySelected($event, attribute)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(attribute)"></iqser-round-checkbox>
</div>
<div class="label">
<span>{{ attribute.label }}</span>
</div>
<div [translate]="translations[attribute.type]" class="small-label"></div>
<div class="center read-only">
<mat-icon
*ngIf="!attribute.editable"
[matTooltip]="'file-attributes-listing.read-only' | translate"
matTooltipPosition="above"
svgIcon="red:read-only"
></mat-icon>
</div>
<div class="small-label">
{{ attribute.csvColumnHeader }}
</div>
<div class="center">
<iqser-round-checkbox *ngIf="attribute.filterable" [active]="true" [size]="18"></iqser-round-checkbox>
</div>
<div class="center">
<iqser-round-checkbox *ngIf="attribute.displayedInFileList" [active]="true" [size]="18"></iqser-round-checkbox>
</div>
<div class="center">
<iqser-round-checkbox *ngIf="attribute.primaryAttribute" [active]="true" [size]="18"></iqser-round-checkbox>
</div>
<div class="actions-container">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openAddEditAttributeDialog($event, attribute)"
[tooltip]="'file-attributes-listing.action.edit' | translate"
icon="iqser:edit"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteAttributeDialog($event, attribute)"
[tooltip]="'file-attributes-listing.action.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
emptyColumnWidth="1fr"
noDataIcon="red:attribute"
></iqser-table>
</div>
<div class="right-container"></div>
@ -101,11 +42,13 @@
(click)="openConfirmDeleteAttributeDialog($event)"
*ngIf="currentUser.isAdmin && (entitiesService.areSomeSelected$ | async)"
[tooltip]="'file-attributes-listing.bulk-actions.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</ng-template>
<div class="attributes-actions-container">
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[placeholder]="'file-attributes-listing.search' | translate"
@ -117,17 +60,79 @@
(action)="fileInput.click()"
*ngIf="currentUser.isAdmin"
[tooltip]="'file-attributes-listing.upload-csv' | translate"
[type]="circleButtonTypes.dark"
icon="red:upload"
tooltipPosition="above"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-icon-button
(action)="openAddEditAttributeDialog($event)"
*ngIf="currentUser.isAdmin"
[label]="'file-attributes-listing.add-new' | translate"
icon="red:plus"
[type]="iconButtonTypes.primary"
icon="red:plus"
></iqser-icon-button>
</div>
</ng-template>
<ng-template #actionsTemplate let-attribute="entity">
<div *ngIf="currentUser.isAdmin" class="action-buttons">
<iqser-circle-button
(action)="openAddEditAttributeDialog($event, attribute)"
[tooltip]="'file-attributes-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openConfirmDeleteAttributeDialog($event, attribute)"
[tooltip]="'file-attributes-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</ng-template>
<ng-template #labelTemplate let-attribute="entity">
<div class="label cell">
<span>{{ attribute.label }}</span>
</div>
</ng-template>
<ng-template #typeTemplate let-attribute="entity">
<div [translate]="translations[attribute.type]" class="small-label cell"></div>
</ng-template>
<ng-template #readonlyTemplate let-attribute="entity">
<div class="center read-only cell">
<mat-icon
*ngIf="!attribute.editable"
[matTooltip]="'file-attributes-listing.read-only' | translate"
matTooltipPosition="above"
svgIcon="red:read-only"
></mat-icon>
</div>
</ng-template>
<ng-template #csvColumnHeaderTemplate let-attribute="entity">
<div class="small-label cell">
{{ attribute.csvColumnHeader }}
</div>
</ng-template>
<ng-template #filterableTemplate let-attribute="entity">
<div class="center cell">
<iqser-round-checkbox *ngIf="attribute.filterable" [active]="true" [size]="18"></iqser-round-checkbox>
</div>
</ng-template>
<ng-template #displayedInFileListTemplate let-attribute="entity">
<div class="center cell">
<iqser-round-checkbox *ngIf="attribute.displayedInFileList" [active]="true" [size]="18"></iqser-round-checkbox>
</div>
</ng-template>
<ng-template #primaryAttributeTemplate let-attribute="entity">
<div class="center cell">
<iqser-round-checkbox *ngIf="attribute.primaryAttribute" [active]="true" [size]="18"></iqser-round-checkbox>
</div>
</ng-template>

View File

@ -1,38 +1,21 @@
@import 'libs/common-ui/src/assets/styles/mixins';
.page-header .actions {
display: flex;
justify-content: flex-end;
}
.content-container cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 11px;
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 10px;
}
> div {
&.center {
align-items: center;
}
&.label span {
@include line-clamp(1);
}
&.read-only mat-icon {
width: 14px;
height: 34px;
}
}
}
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell {
&.center {
align-items: center;
}
&.has-scrollbar:hover ::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
&.label span {
@include line-clamp(1);
}
&.read-only mat-icon {
width: 14px;
height: 34px;
}
iqser-round-checkbox {
cursor: default;
}
}
@ -40,7 +23,3 @@
display: none;
visibility: hidden;
}
.table-item > div:not(.selection-column) iqser-round-checkbox {
cursor: default;
}

View File

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, Component, ElementRef, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
forwardRef,
Injector,
OnDestroy,
OnInit,
TemplateRef,
ViewChild
} from '@angular/core';
import { FileAttributeConfig, FileAttributesConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { ActivatedRoute } from '@angular/router';
@ -19,7 +29,10 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
templateUrl: './file-attributes-listing-screen.component.html',
styleUrls: ['./file-attributes-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [...DefaultListingServices]
providers: [
...DefaultListingServices,
{ provide: ListingComponent, useExisting: forwardRef(() => FileAttributesListingScreenComponent) }
]
})
export class FileAttributesListingScreenComponent extends ListingComponent<FileAttributeConfig> implements OnInit, OnDestroy {
readonly iconButtonTypes = IconButtonTypes;
@ -27,36 +40,14 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
readonly currentUser = this._userService.currentUser;
readonly translations = fileAttributeTypesTranslations;
readonly tableHeaderLabel = _('file-attributes-listing.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<FileAttributeConfig>[] = [
{
label: _('file-attributes-listing.table-col-names.name'),
sortByKey: 'label'
},
{
label: _('file-attributes-listing.table-col-names.type'),
sortByKey: 'type'
},
{
label: _('file-attributes-listing.table-col-names.read-only'),
sortByKey: 'editable',
class: 'flex-center'
},
{ label: _('file-attributes-listing.table-col-names.csv-column') },
{
label: _('file-attributes-listing.table-col-names.filterable'),
class: 'flex-center'
},
{
label: _('file-attributes-listing.table-col-names.displayed-in-file-list'),
class: 'flex-center'
},
{
label: _('file-attributes-listing.table-col-names.primary'),
class: 'flex-center',
rightIcon: 'red:status-info',
rightIconTooltip: _('file-attributes-listing.table-col-names.primary-info-tooltip')
}
];
tableColumnConfigs: TableColumnConfig<FileAttributeConfig>[];
@ViewChild('labelTemplate', { static: true }) labelTemplate: TemplateRef<never>;
@ViewChild('typeTemplate', { static: true }) typeTemplate: TemplateRef<never>;
@ViewChild('readonlyTemplate', { static: true }) readonlyTemplate: TemplateRef<never>;
@ViewChild('csvColumnHeaderTemplate', { static: true }) csvColumnHeaderTemplate: TemplateRef<never>;
@ViewChild('filterableTemplate', { static: true }) filterableTemplate: TemplateRef<never>;
@ViewChild('displayedInFileListTemplate', { static: true }) displayedInFileListTemplate: TemplateRef<never>;
@ViewChild('primaryAttributeTemplate', { static: true }) primaryAttributeTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'label';
private _existingConfiguration: FileAttributesConfig;
@ViewChild('fileInput') private _fileInput: ElementRef;
@ -75,6 +66,7 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
}
async ngOnInit() {
this._configureTableColumns();
await this._loadData();
}
@ -130,6 +122,46 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
);
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('file-attributes-listing.table-col-names.name'),
sortByKey: 'label',
width: '2fr',
template: this.labelTemplate
},
{
label: _('file-attributes-listing.table-col-names.type'),
sortByKey: 'type',
template: this.typeTemplate
},
{
label: _('file-attributes-listing.table-col-names.read-only'),
sortByKey: 'editable',
class: 'flex-center',
template: this.readonlyTemplate
},
{ label: _('file-attributes-listing.table-col-names.csv-column'), template: this.csvColumnHeaderTemplate },
{
label: _('file-attributes-listing.table-col-names.filterable'),
class: 'flex-center',
template: this.filterableTemplate
},
{
label: _('file-attributes-listing.table-col-names.displayed-in-file-list'),
class: 'flex-center',
template: this.displayedInFileListTemplate
},
{
label: _('file-attributes-listing.table-col-names.primary'),
class: 'flex-center',
rightIcon: 'red:status-info',
rightIconTooltip: _('file-attributes-listing.table-col-names.primary-info-tooltip'),
template: this.primaryAttributeTemplate
}
];
}
private async _loadData() {
this._loadingService.start();

View File

@ -41,12 +41,6 @@
<div translate="license-info-screen.end-user-license-text"></div>
</div>
<!-- TODO-->
<!-- <div class="row">-->
<!-- <div translate="license-info-screen.3rd-party-title"></div>-->
<!-- <div>Future feature: we will provide that with the /info endpoint</div>-->
<!-- </div>-->
<div class="section-title all-caps-label" translate="license-info-screen.licensing-details"></div>
<div class="row">
@ -95,12 +89,6 @@
<div translate="license-info-screen.unlicensed-analyzed"></div>
<div>{{ unlicensedInfo.numberOfAnalyzedPages }}</div>
</div>
<!-- TODO-->
<!-- <div class="row">-->
<!-- <div>Progress bar</div>-->
<!-- <div></div>-->
<!-- </div>-->
</div>
<combo-chart-component

View File

@ -19,7 +19,7 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container" redactionHasScrollbar>
<div class="content-container" iqserHasScrollbar>
<div class="heading-xl" translate="reports-screen.title"></div>
<div class="description" translate="reports-screen.description"></div>
@ -42,7 +42,7 @@
</div>
</div>
<div class="right-container" redactionHasScrollbar>
<div class="right-container" iqserHasScrollbar>
<div class="header">
<div class="heading" translate="reports-screen.report-documents"></div>
<iqser-circle-button

View File

@ -1,11 +1,6 @@
@import '../../../../../assets/styles/variables';
@import 'libs/common-ui/src/assets/styles/mixins';
.page-header .actions {
display: flex;
justify-content: flex-end;
}
.content-container,
.right-container {
flex: 1;

View File

@ -3,11 +3,6 @@
padding: 15px;
}
.page-header .actions {
display: flex;
justify-content: flex-end;
}
ngx-monaco-editor {
height: 100%;
width: 100%;

View File

@ -9,106 +9,93 @@
<div class="red-content-inner">
<div class="content-container">
<iqser-table-header
<iqser-table
[bulkActions]="bulkActions"
[itemSize]="80"
[noDataText]="'trash.no-data.title' | translate"
[noMatchText]="'trash.no-match.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"
[text]="'trash.no-data.title' | translate"
icon="red:template"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'trash.no-match.title' | translate"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="let entity of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="table-item"
[class.disabled]="!entity.canRestore"
>
<div (click)="toggleEntitySelected($event, entity)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(entity)"></iqser-round-checkbox>
</div>
<div class="filename">
<div [matTooltip]="entity.dossierName" class="table-item-title heading" matTooltipPosition="above">
{{ entity.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ entity.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ entity.date | date: 'mediumDate' }}
</div>
<div *ngIf="entity.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ entity.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
<div class="user-column">
<redaction-initials-avatar [userId]="entity.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
<div>
<div class="small-label">
{{ entity.softDeletedTime === '-' ? '-' : (entity.softDeletedTime | date: 'd MMM. yyyy, hh:mm a') }}
</div>
</div>
<div>
<div class="small-label">
{{ entity.restoreDate | date: 'timeFromNow' }}
</div>
<div class="action-buttons">
<iqser-circle-button
(action)="restore([entity])"
*ngIf="entity.canRestore"
[tooltip]="'trash.action.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete([entity])"
[tooltip]="'trash.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
[tableItemClasses]="{ disabled: disabledFn }"
noDataIcon="red:template"
></iqser-table>
</div>
</div>
</section>
<ng-template #bulkActions>
<div class="bulk-actions">
<iqser-circle-button
(action)="restore()"
*ngIf="canRestoreSelected$ | async"
[tooltip]="'trash.bulk.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="restore()"
*ngIf="canRestoreSelected$ | async"
[tooltip]="'trash.bulk.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete()"
*ngIf="entitiesService.areSomeSelected$ | async"
[tooltip]="'trash.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete()"
*ngIf="entitiesService.areSomeSelected$ | async"
[tooltip]="'trash.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</ng-template>
<ng-template #filenameTemplate let-entity="entity">
<div class="cell filename">
<div [matTooltip]="entity.dossierName" class="table-item-title heading" matTooltipPosition="above">
{{ entity.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ entity.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ entity.date | date: 'mediumDate' }}
</div>
<div *ngIf="entity.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ entity.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
</ng-template>
<ng-template #ownerTemplate let-entity="entity">
<div class="cell user-column">
<redaction-initials-avatar [userId]="entity.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #deletedTimeTemplate let-entity="entity">
<div class="cell">
<span class="small-label">
{{ entity.softDeletedTime | date: 'd MMM. yyyy, hh:mm a' }}
</span>
</div>
</ng-template>
<ng-template #restoreDateTemplate let-entity="entity">
<div class="cell">
<div class="small-label">
{{ entity.restoreDate | date: 'timeFromNow' }}
</div>
<div class="action-buttons">
<iqser-circle-button
(action)="restore([entity])"
*ngIf="entity.canRestore"
[tooltip]="'trash.action.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete([entity])"
[tooltip]="'trash.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</div>
</ng-template>

View File

@ -1,46 +0,0 @@
@import '../../../../../assets/styles/variables';
.bulk-actions {
display: flex;
align-items: center;
> *:not(:last-child) {
margin-right: 2px;
}
}
.content-container {
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 1fr 1fr 11px;
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 10px;
.table-item-title {
max-width: 100%;
}
}
> div {
height: 80px;
padding: 0 24px;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 1fr 1fr;
}
}
}
}
.disabled {
> div {
background-color: $grey-2;
color: $grey-7;
}
}

View File

@ -1,13 +1,13 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier } from '@redaction/red-ui-http';
import {
CircleButtonTypes,
DefaultListingServices,
Listable,
ListingComponent,
LoadingService,
Listable,
TableColumnConfig,
SortingOrders
SortingOrders,
TableColumnConfig
} from '@iqser/common-ui';
import { AppConfigKey, AppConfigService } from '@app-config/app-config.service';
import * as moment from 'moment';
@ -29,31 +29,21 @@ interface DossierListItem extends Dossier, Listable {
templateUrl: './trash-screen.component.html',
styleUrls: ['./trash-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [...DefaultListingServices, DossiersService]
providers: [
...DefaultListingServices,
DossiersService,
{ provide: ListingComponent, useExisting: forwardRef(() => TrashScreenComponent) }
]
})
export class TrashScreenComponent extends ListingComponent<DossierListItem> implements OnInit {
readonly itemSize = 80;
readonly circleButtonTypes = CircleButtonTypes;
readonly tableHeaderLabel = _('trash.table-header.title');
readonly canRestoreSelected$ = this._canRestoreSelected$;
readonly tableColumnConfigs: readonly TableColumnConfig<DossierListItem>[] = [
{
label: _('trash.table-col-names.name'),
sortByKey: 'dossierName'
},
{
label: _('trash.table-col-names.owner'),
class: 'user-column'
},
{
label: _('trash.table-col-names.deleted-on'),
sortByKey: 'softDeletedTime'
},
{
label: _('trash.table-col-names.time-to-restore'),
sortByKey: 'softDeletedTime'
}
];
tableColumnConfigs: TableColumnConfig<DossierListItem>[];
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<never>;
@ViewChild('ownerTemplate', { static: true }) ownerTemplate: TemplateRef<never>;
@ViewChild('deletedTimeTemplate', { static: true }) deletedTimeTemplate: TemplateRef<never>;
@ViewChild('restoreDateTemplate', { static: true }) restoreDateTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'dossierName';
private readonly _deleteRetentionHours = this._appConfigService.getConfig(AppConfigKey.DELETE_RETENTION_HOURS);
@ -75,7 +65,10 @@ export class TrashScreenComponent extends ListingComponent<DossierListItem> impl
);
}
disabledFn = (dossier: DossierListItem) => !dossier.canRestore;
async ngOnInit(): Promise<void> {
this._configureTableColumns();
this._loadingService.start();
await this._loadDossiersData();
this.sortingService.setSortingOption({
@ -108,6 +101,31 @@ export class TrashScreenComponent extends ListingComponent<DossierListItem> impl
this._loadingService.loadWhile(this._restore(dossiers));
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('trash.table-col-names.name'),
sortByKey: 'dossierName',
template: this.filenameTemplate
},
{
label: _('trash.table-col-names.owner'),
class: 'user-column',
template: this.ownerTemplate
},
{
label: _('trash.table-col-names.deleted-on'),
sortByKey: 'softDeletedTime',
template: this.deletedTimeTemplate
},
{
label: _('trash.table-col-names.time-to-restore'),
sortByKey: 'softDeletedTime',
template: this.restoreDateTemplate
}
];
}
private _getRestoreDate(softDeletedTime: string): string {
return moment(softDeletedTime).add(this._deleteRetentionHours, 'hours').toISOString();
}

View File

@ -16,8 +16,8 @@
(action)="openAddEditUserDialog($event)"
*ngIf="currentUser.isUserAdmin"
[label]="'user-listing.add-new' | translate"
icon="red:plus"
[type]="iconButtonTypes.primary"
icon="red:plus"
></iqser-icon-button>
<iqser-circle-button
*ngIf="currentUser.isUser"
@ -32,58 +32,17 @@
<div class="red-content-inner">
<div [class.extended]="collapsedDetails" class="content-container">
<iqser-table-header
<iqser-table
[actionsTemplate]="actionsTemplate"
[bulkActions]="bulkActions"
[itemSize]="80"
[noMatchText]="'user-listing.no-match.title' | translate"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'user-listing.no-match.title' | translate"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div *cdkVirtualFor="let user of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div (click)="toggleEntitySelected($event, user)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(user)"></iqser-round-checkbox>
</div>
<div>
<redaction-initials-avatar [showYou]="true" [userId]="user.id" [withName]="true"></redaction-initials-avatar>
</div>
<div class="small-label">{{ user.email || '-' }}</div>
<div class="center">
<mat-slide-toggle
(toggleChange)="toggleActive(user)"
[checked]="user.isActive"
color="primary"
></mat-slide-toggle>
</div>
<div class="small-label">{{ getDisplayRoles(user) }}</div>
<div class="actions-container">
<div class="action-buttons">
<iqser-circle-button
(action)="openAddEditUserDialog($event, user)"
[tooltip]="'user-listing.action.edit' | translate"
icon="iqser:edit"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-circle-button
(action)="openDeleteUsersDialog([user], $event)"
[disabled]="user.id === userService.currentUser.id"
[tooltip]="'user-listing.action.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
emptyColumnWidth="1fr"
></iqser-table>
</div>
<div [class.collapsed]="collapsedDetails" class="right-container" redactionHasScrollbar>
<div [class.collapsed]="collapsedDetails" class="right-container" iqserHasScrollbar>
<redaction-users-stats
(toggleCollapse)="collapsedDetails = !collapsedDetails"
[chartData]="chartData"
@ -101,8 +60,46 @@
[tooltip]="
(canDeleteSelected$ | async) ? ('user-listing.bulk.delete' | translate) : ('user-listing.bulk.delete-disabled' | translate)
"
[type]="circleButtonTypes.dark"
icon="red:trash"
tooltipPosition="after"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
</ng-template>
<ng-template #nameTemplate let-user="entity">
<div class="cell">
<redaction-initials-avatar [showYou]="true" [userId]="user.id" [withName]="true"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #emailTemplate let-user="entity">
<div class="small-label cell">{{ user.email || '-' }}</div>
</ng-template>
<ng-template #activeTemplate let-user="entity">
<div class="center cell">
<mat-slide-toggle (toggleChange)="toggleActive(user)" [checked]="user.isActive" color="primary"></mat-slide-toggle>
</div>
</ng-template>
<ng-template #rolesTemplate let-user="entity">
<div class="small-label cell">{{ getDisplayRoles(user) }}</div>
</ng-template>
<ng-template #actionsTemplate let-user="entity">
<div class="action-buttons">
<iqser-circle-button
(action)="openAddEditUserDialog($event, user)"
[tooltip]="'user-listing.action.edit' | translate"
[type]="circleButtonTypes.dark"
icon="iqser:edit"
></iqser-circle-button>
<iqser-circle-button
(action)="openDeleteUsersDialog([user], $event)"
[disabled]="user.id === userService.currentUser.id"
[tooltip]="'user-listing.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</ng-template>

View File

@ -1,23 +1,5 @@
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr auto 11px;
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 10px;
&.center {
align-items: center;
}
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 2fr 1fr 1fr 1fr auto;
}
}
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell.center {
align-items: center;
}
.right-container {
@ -36,8 +18,6 @@ cdk-virtual-scroll-viewport {
}
.page-header .actions {
justify-content: flex-end;
iqser-input-with-action:not(:last-child) {
margin-right: 16px;
}

View File

@ -1,4 +1,4 @@
import { Component, Injector, OnInit, QueryList, ViewChildren } from '@angular/core';
import { Component, forwardRef, Injector, OnInit, QueryList, TemplateRef, ViewChild, ViewChildren } from '@angular/core';
import { UserService, UserWrapper } from '@services/user.service';
import { UserControllerService } from '@redaction/red-ui-http';
import { AdminDialogService } from '../../services/admin-dialog.service';
@ -22,7 +22,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
templateUrl: './user-listing-screen.component.html',
styleUrls: ['./user-listing-screen.component.scss'],
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => UserListingScreenComponent) }]
})
export class UserListingScreenComponent extends ListingComponent<UserWrapper> implements OnInit {
readonly translations = rolesTranslations;
@ -31,14 +31,13 @@ export class UserListingScreenComponent extends ListingComponent<UserWrapper> im
readonly currentUser = this.userService.currentUser;
readonly canDeleteSelected$ = this._canDeleteSelected$;
readonly tableHeaderLabel = _('user-listing.table-header.title');
readonly tableColumnConfigs: TableColumnConfig<UserWrapper>[] = [
{ label: _('user-listing.table-col-names.name') },
{ label: _('user-listing.table-col-names.email') },
{ label: _('user-listing.table-col-names.active'), class: 'flex-center' },
{ label: _('user-listing.table-col-names.roles') }
];
tableColumnConfigs: TableColumnConfig<UserWrapper>[];
collapsedDetails = false;
chartData: DoughnutChartConfig[] = [];
@ViewChild('nameTemplate', { static: true }) nameTemplate: TemplateRef<never>;
@ViewChild('emailTemplate', { static: true }) emailTemplate: TemplateRef<never>;
@ViewChild('activeTemplate', { static: true }) activeTemplate: TemplateRef<never>;
@ViewChild('rolesTemplate', { static: true }) rolesTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'id';
@ViewChildren(InitialsAvatarComponent)
private readonly _avatars: QueryList<InitialsAvatarComponent>;
@ -61,6 +60,7 @@ export class UserListingScreenComponent extends ListingComponent<UserWrapper> im
}
async ngOnInit() {
this._configureTableColumns();
await this._loadData();
this.searchService.setSearchKey('searchKey');
}
@ -97,6 +97,15 @@ export class UserListingScreenComponent extends ListingComponent<UserWrapper> im
this.openDeleteUsersDialog(this.entitiesService.all.filter(u => this.isSelected(u)));
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{ label: _('user-listing.table-col-names.name'), template: this.nameTemplate, width: '2fr' },
{ label: _('user-listing.table-col-names.email'), template: this.emailTemplate },
{ label: _('user-listing.table-col-names.active'), class: 'flex-center', template: this.activeTemplate },
{ label: _('user-listing.table-col-names.roles'), template: this.rolesTemplate }
];
}
private async _loadData() {
this.entitiesService.setEntities(await this.userService.loadAllUsers());
await this.userService.loadAllUsers();

View File

@ -26,14 +26,14 @@
(action)="save()"
[disabled]="configForm.invalid"
[label]="'watermark-screen.action.save' | translate"
icon="iqser:check"
[type]="iconButtonTypes.primary"
icon="iqser:check"
></iqser-icon-button>
<div (click)="revert()" class="all-caps-label cancel" translate="watermark-screen.action.revert"></div>
</div>
</div>
<div class="right-container" redactionHasScrollbar>
<div class="right-container" iqserHasScrollbar>
<div class="heading-xl" translate="watermark-screen.title"></div>
<form (keyup)="configChanged()" [formGroup]="configForm">
<div class="iqser-input-group w-300">
@ -42,8 +42,8 @@
[placeholder]="'watermark-screen.form.text-placeholder' | translate"
class="w-full"
formControlName="text"
iqserHasScrollbar
name="text"
redactionHasScrollbar
rows="4"
type="text"
></textarea>

View File

@ -1,10 +1,5 @@
@import '../../../../../assets/styles/variables';
.page-header .actions {
display: flex;
justify-content: flex-end;
}
.content-container {
order: 1;

View File

@ -15,7 +15,7 @@
</div>
</div>
<div class="right-content" redactionHasScrollbar>
<div class="right-content" iqserHasScrollbar>
<div class="section">
<div *ngFor="let attr of fileAttributesConfig?.fileAttributeConfigs" class="attribute">
<div class="small-label">{{ attr.label }}:</div>

View File

@ -61,8 +61,8 @@
</div>
<iqser-circle-button
(action)="multiSelectActive = false"
icon="iqser:close"
[type]="circleButtonTypes.primary"
icon="iqser:close"
></iqser-circle-button>
</div>
@ -134,11 +134,11 @@
(keyup)="preventKeyDefault($event)"
[class.active-panel]="!pagesPanelActive"
class="annotations"
redactionHasScrollbar
iqserHasScrollbar
tabindex="1"
>
<ng-container *ngIf="activeViewerPage && !displayedAnnotations.get(activeViewerPage)?.length">
<redaction-empty-state
<iqser-empty-state
[horizontalPadding]="24"
[text]="'file-preview.no-data.title' | translate"
[verticalPadding]="40"
@ -154,22 +154,22 @@
</a
>.
</ng-container>
</redaction-empty-state>
</iqser-empty-state>
<div class="no-annotations-buttons-container mt-32">
<iqser-icon-button
(action)="jumpToPreviousWithAnnotations()"
[disabled]="activeViewerPage <= displayedPages[0]"
[label]="'file-preview.tabs.annotations.jump-to-previous' | translate"
icon="red:nav-prev"
[type]="iconButtonTypes.dark"
icon="red:nav-prev"
></iqser-icon-button>
<iqser-icon-button
(action)="jumpToNextWithAnnotations()"
[disabled]="activeViewerPage >= displayedPages[displayedPages.length - 1]"
[label]="'file-preview.tabs.annotations.jump-to-next' | translate"
[type]="iconButtonTypes.dark"
class="mt-8"
icon="red:nav-next"
[type]="iconButtonTypes.dark"
></iqser-icon-button>
</div>
</ng-container>

View File

@ -11,6 +11,7 @@
}
mat-icon {
min-width: 16px;
width: 16px;
height: 16px;
fill-opacity: 0.6;

View File

@ -13,7 +13,7 @@
<div class="all-caps-label" translate="file-preview.tabs.exclude-pages.removed-from-redaction"></div>
</div>
<div class="ranges" redactionHasScrollbar>
<div class="ranges" iqserHasScrollbar>
<div *ngFor="let range of excludedPagesRanges" class="range">
<ng-container *ngIf="range.startPage === range.endPage">
{{ range.startPage }}

View File

@ -1,7 +0,0 @@
<button (click)="scroll(buttonType.top)" [hidden]="(showScrollUp$ | async) === false" class="scroll-button top pointer">
<mat-icon svgIcon="red:arrow-down-o"></mat-icon>
</button>
<button (click)="scroll(buttonType.bottom)" [hidden]="(showScrollDown$ | async) === false" class="scroll-button bottom pointer">
<mat-icon svgIcon="red:arrow-down-o"></mat-icon>
</button>

View File

@ -1,30 +0,0 @@
@import '../../../../../assets/styles/variables';
.scroll-button {
background-color: $white;
position: absolute;
right: 0;
height: 40px;
width: 44px;
border: none;
border-radius: 8px 0 0 8px;
box-shadow: -1px 1px 5px 0 rgba(40, 50, 65, 0.25);
&.bottom {
bottom: 30px;
}
&.top {
top: 100px;
mat-icon {
transform: rotate(180deg);
}
}
}
mat-icon {
width: 22px;
height: 22px;
color: $grey-7;
}

View File

@ -1,60 +0,0 @@
import { Component, HostListener, Input, OnInit } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { concatMap, delay, distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
const ButtonTypes = {
top: 'top',
bottom: 'bottom'
} as const;
type ButtonType = keyof typeof ButtonTypes;
@Component({
selector: 'redaction-scroll-button',
templateUrl: './scroll-button.component.html',
styleUrls: ['./scroll-button.component.scss']
})
export class ScrollButtonComponent implements OnInit {
readonly buttonType = ButtonTypes;
@Input()
scrollViewport: CdkVirtualScrollViewport;
@Input()
itemSize: number;
showScrollUp$: Observable<boolean>;
showScrollDown$: Observable<boolean>;
ngOnInit() {
const scrollSize = () => this.scrollViewport.getDataLength() * this.itemSize;
const scrollIsNeeded = () => this.scrollViewport.getViewportSize() < scrollSize();
const reachedEnd = (type: ButtonType) => this.scrollViewport.measureScrollOffset(type) === 0;
const showScrollUp = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.top);
const showScrollDown = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.bottom);
const scroll$ = this.scrollViewport.elementScrolled().pipe(
startWith(''),
/** Delay first value so that we can wait for items to be rendered in viewport and get correct values */
concatMap((value, index) => (index === 0 ? of(value).pipe(delay(0)) : of(value)))
);
this.showScrollUp$ = scroll$.pipe(map(showScrollUp), distinctUntilChanged());
this.showScrollDown$ = scroll$.pipe(map(showScrollDown), distinctUntilChanged());
}
scroll(type: ButtonType): void {
const viewportSize = (this.scrollViewport?.getViewportSize() - this.itemSize) * (type === ButtonTypes.top ? -1 : 1);
const scrollOffset = this.scrollViewport?.measureScrollOffset('top');
this.scrollViewport?.scrollToOffset(scrollOffset + viewportSize, 'smooth');
}
@HostListener('document:keyup', ['$event'])
spaceAndPageDownScroll(event: KeyboardEvent): void {
if (['Space', 'PageDown'].includes(event.code) && (event.target as any).tagName === 'BODY') {
this.scroll(ButtonTypes.bottom);
} else if (['PageUp'].includes(event.code) && (event.target as any).tagName === 'BODY') {
this.scroll(ButtonTypes.top);
}
}
}

View File

@ -34,8 +34,8 @@
<textarea
[placeholder]="'add-dossier-dialog.form.description.placeholder' | translate"
formControlName="description"
iqserHasScrollbar
name="description"
redactionHasScrollbar
rows="5"
type="text"
></textarea>
@ -63,8 +63,8 @@
<div class="d-flex">
<redaction-select
[label]="'report-type.label' | translate: { length: reportTemplateIdsLength }"
[options]="availableReportTypes"
[optionTemplate]="reportTemplateOptionTemplate"
[options]="availableReportTypes"
[valueMapper]="reportTemplateValueMapper"
class="mr-16"
formControlName="reportTemplateIds"
@ -86,8 +86,8 @@
(action)="saveDossierAndAddMembers()"
[disabled]="disabled"
[label]="'add-dossier-dialog.actions.save-and-add-members' | translate"
icon="red:assign"
[type]="iconButtonTypes.dark"
icon="red:assign"
></iqser-icon-button>
</div>
</form>

View File

@ -23,7 +23,7 @@
<div [class.required]="!isDocumentAdmin" class="iqser-input-group w-300">
<label translate="change-legal-basis-dialog.content.comment"></label>
<textarea formControlName="comment" name="comment" redactionHasScrollbar rows="4" type="text"></textarea>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
</div>

View File

@ -4,11 +4,11 @@
{{ 'edit-dossier-dialog.attributes.custom-attributes' | translate }}
</div>
<redaction-empty-state
<iqser-empty-state
*ngIf="!customAttributes.length"
[text]="'edit-dossier-dialog.attributes.no-custom-attributes' | translate"
icon="red:attribute"
></redaction-empty-state>
></iqser-empty-state>
<div *ngFor="let attr of customAttributes" [class.datepicker-wrapper]="isDate(attr)" class="iqser-input-group">
<label>{{ attr.label }}</label>
@ -34,11 +34,11 @@
{{ 'edit-dossier-dialog.attributes.image-attributes' | translate }}
</div>
<redaction-empty-state
<iqser-empty-state
*ngIf="!imageAttributes.length"
[text]="'edit-dossier-dialog.attributes.no-image-attributes' | translate"
icon="red:attribute"
></redaction-empty-state>
></iqser-empty-state>
<div
*ngFor="let attr of imageAttributes"

View File

@ -1,76 +1,75 @@
<iqser-table-header
<iqser-table
[bulkActions]="bulkActions"
[headerTemplate]="headerTemplate"
[itemSize]="50"
[noDataText]="'edit-dossier-dialog.deleted-documents.no-data.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
>
noDataIcon="red:document"
></iqser-table>
<ng-template #headerTemplate>
<div
[translateParams]="{ hours: deleteRetentionHours }"
[translate]="'edit-dossier-dialog.deleted-documents.instructions'"
class="instructions"
></div>
</iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"
[text]="'edit-dossier-dialog.deleted-documents.no-data.title' | translate"
icon="red:document"
></redaction-empty-state>
<cdk-virtual-scroll-viewport *ngIf="(entitiesService.noData$ | async) === false" [itemSize]="itemSize" redactionHasScrollbar>
<div *cdkVirtualFor="let file of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div (click)="toggleEntitySelected($event, file)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(file)"></iqser-round-checkbox>
</div>
<div class="filename">
<span>{{ file.filename }}</span>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ file.numberOfPages }}
</div>
</div>
<div class="small-label">{{ file.softDeleted | date: 'exactDate' }}</div>
<div>
<div class="small-label">{{ file.restoreDate | date: 'timeFromNow' }}</div>
<div class="action-buttons">
<iqser-circle-button
(action)="restore([file])"
*ngIf="file.canRestore"
[tooltip]="'edit-dossier-dialog.deleted-documents.action.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete([file])"
[tooltip]="'edit-dossier-dialog.deleted-documents.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
</ng-template>
<ng-template #bulkActions>
<div class="bulk-actions">
<iqser-circle-button
(action)="restore()"
*ngIf="canRestoreSelected$ | async"
[tooltip]="'edit-dossier-dialog.deleted-documents.bulk.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="restore()"
*ngIf="canRestoreSelected$ | async"
[tooltip]="'edit-dossier-dialog.deleted-documents.bulk.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete()"
*ngIf="entitiesService.areSomeSelected$ | async"
[tooltip]="'edit-dossier-dialog.deleted-documents.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete()"
*ngIf="entitiesService.areSomeSelected$ | async"
[tooltip]="'edit-dossier-dialog.deleted-documents.bulk.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</ng-template>
<ng-template #filenameTemplate let-file="entity">
<div class="cell filename">
<span>{{ file.filename }}</span>
</div>
</ng-template>
<ng-template #pagesTemplate let-file="entity">
<div class="cell small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ file.numberOfPages }}
</div>
</div>
</ng-template>
<ng-template #deletedDateTemplate let-file="entity">
<div class="cell small-label">{{ file.softDeleted | date: 'exactDate' }}</div>
</ng-template>
<ng-template #restoreDateTemplate let-file="entity">
<div class="cell">
<div class="small-label">{{ file.restoreDate | date: 'timeFromNow' }}</div>
<div class="action-buttons">
<iqser-circle-button
(action)="restore([file])"
*ngIf="file.canRestore"
[tooltip]="'edit-dossier-dialog.deleted-documents.action.restore' | translate"
[type]="circleButtonTypes.dark"
icon="red:put-back"
></iqser-circle-button>
<iqser-circle-button
(action)="hardDelete([file])"
[tooltip]="'edit-dossier-dialog.deleted-documents.action.delete' | translate"
[type]="circleButtonTypes.dark"
icon="red:trash"
></iqser-circle-button>
</div>
</div>
</ng-template>

View File

@ -7,35 +7,10 @@
text-align: end;
}
cdk-virtual-scroll-viewport {
height: calc(100% - 81px);
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport {
height: calc(100% - 81px) !important;
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 3fr 1fr 2fr 2fr 11px;
.table-item > div {
height: 50px;
&.filename span {
@include line-clamp(1);
}
&.stats-subtitle > div {
width: fit-content;
}
}
}
&.has-scrollbar:hover ::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 3fr 1fr 2fr 2fr;
}
}
.bulk-actions {
display: flex;
align-items: center;
> *:not(:last-child) {
margin-right: 2px;
.cdk-virtual-scroll-content-wrapper .table-item > div.cell.filename span {
@include line-clamp(1);
}
}

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, Injector, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, forwardRef, Injector, Input, OnInit, Output, TemplateRef, ViewChild } from '@angular/core';
import { EditDossierSectionInterface } from '../edit-dossier-section.interface';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { CircleButtonTypes, DefaultListingServices, ListingComponent, Listable, LoadingService, TableColumnConfig } from '@iqser/common-ui';
import { CircleButtonTypes, DefaultListingServices, Listable, ListingComponent, LoadingService, TableColumnConfig } from '@iqser/common-ui';
import { FileManagementControllerService, FileStatus, StatusControllerService } from '@redaction/red-ui-http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import * as moment from 'moment';
@ -22,7 +22,10 @@ interface FileListItem extends FileStatus, Listable {
selector: 'redaction-edit-dossier-deleted-documents',
templateUrl: './edit-dossier-deleted-documents.component.html',
styleUrls: ['./edit-dossier-deleted-documents.component.scss'],
providers: [...DefaultListingServices]
providers: [
...DefaultListingServices,
{ provide: ListingComponent, useExisting: forwardRef(() => EditDossierDeletedDocumentsComponent) }
]
})
export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileListItem> implements EditDossierSectionInterface, OnInit {
@Input() dossierWrapper: DossierWrapper;
@ -30,16 +33,14 @@ export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileL
readonly changed = false;
readonly canRestoreSelected$ = this._canRestoreSelected$;
disabled: boolean;
readonly tableColumnConfigs: TableColumnConfig<FileListItem>[] = [
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.name') },
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.pages') },
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.deleted-on') },
{ label: _('edit-dossier-dialog.deleted-documents.table-col-names.time-to-restore') }
];
tableColumnConfigs: TableColumnConfig<FileListItem>[];
readonly tableHeaderLabel = _('edit-dossier-dialog.deleted-documents.table-header.label');
readonly itemSize = 50;
readonly circleButtonTypes = CircleButtonTypes;
readonly deleteRetentionHours = this._appConfigService.getConfig(AppConfigKey.DELETE_RETENTION_HOURS);
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<never>;
@ViewChild('pagesTemplate', { static: true }) pagesTemplate: TemplateRef<never>;
@ViewChild('deletedDateTemplate', { static: true }) deletedDateTemplate: TemplateRef<never>;
@ViewChild('restoreDateTemplate', { static: true }) restoreDateTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'fileId';
constructor(
@ -80,6 +81,7 @@ export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileL
}
async ngOnInit() {
this._configureTableColumns();
this._loadingService.start();
const files = await this._statusController.getDeletedFileStatus(this.dossierWrapper.dossierId).toPromise();
this.entitiesService.setEntities(this._toListItems(files));
@ -94,6 +96,30 @@ export class EditDossierDeletedDocumentsComponent extends ListingComponent<FileL
this._loadingService.loadWhile(this._restore(files));
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('edit-dossier-dialog.deleted-documents.table-col-names.name'),
template: this.filenameTemplate,
width: '3fr'
},
{
label: _('edit-dossier-dialog.deleted-documents.table-col-names.pages'),
template: this.pagesTemplate
},
{
label: _('edit-dossier-dialog.deleted-documents.table-col-names.deleted-on'),
template: this.deletedDateTemplate,
width: '2fr'
},
{
label: _('edit-dossier-dialog.deleted-documents.table-col-names.time-to-restore'),
template: this.restoreDateTemplate,
width: '2fr'
}
];
}
private async _restore(files: FileListItem[]): Promise<void> {
const fileIds = files.map(f => f.fileId);
await this._fileManagementController.restoreFiles(fileIds, this.dossierWrapper.dossierId).toPromise();

View File

@ -30,8 +30,8 @@
<textarea
[placeholder]="'edit-dossier-dialog.general-info.form.description.placeholder' | translate"
formControlName="description"
iqserHasScrollbar
name="description"
redactionHasScrollbar
rows="5"
type="text"
></textarea>
@ -60,8 +60,8 @@
(action)="deleteDossier()"
*ngIf="permissionsService.canDeleteDossier(dossierWrapper)"
[label]="'dossier-listing.delete.action' | translate"
icon="red:trash"
[type]="iconButtonTypes.dark"
icon="red:trash"
></iqser-icon-button>
</div>
</form>

View File

@ -23,7 +23,7 @@
<div [class.required]="!isDocumentAdmin" class="iqser-input-group w-300">
<label translate="manual-annotation.dialog.content.comment"></label>
<textarea formControlName="comment" name="comment" redactionHasScrollbar rows="4" type="text"></textarea>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
</div>

View File

@ -41,7 +41,7 @@
<div [class.required]="!isDocumentAdmin" class="iqser-input-group w-300">
<label translate="manual-annotation.dialog.content.comment"></label>
<textarea formControlName="comment" name="comment" redactionHasScrollbar rows="4" type="text"></textarea>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
<div *ngIf="isDictionaryRequest && !isFalsePositiveRequest" class="iqser-input-group required w-300">

View File

@ -18,7 +18,7 @@
<div [class.required]="!isDocumentAdmin" class="iqser-input-group w-300">
<label translate="recategorize-image-dialog.content.comment"></label>
<textarea formControlName="comment" name="comment" redactionHasScrollbar rows="4" type="text"></textarea>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
</div>

View File

@ -41,12 +41,12 @@
<div [class.required]="!permissionsService.isApprover()" class="iqser-input-group w-300">
<label translate="manual-annotation.dialog.content.comment"></label>
<textarea formControlName="comment" name="comment" redactionHasScrollbar rows="4" type="text"></textarea>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button type="submit" [disabled]="!redactionForm.valid">
<button [disabled]="!redactionForm.valid" color="primary" mat-flat-button type="submit">
{{ 'remove-annotations-dialog.confirm' | translate }}
</button>
<button (click)="deny()" color="primary" mat-flat-button>

View File

@ -41,7 +41,6 @@ import { UserPreferenceControllerService } from '@redaction/red-ui-http';
import { EditDossierDictionaryComponent } from './dialogs/edit-dossier-dialog/dictionary/edit-dossier-dictionary.component';
import { EditDossierTeamMembersComponent } from './dialogs/edit-dossier-dialog/team-members/edit-dossier-team-members.component';
import { TeamMembersManagerComponent } from './components/team-members-manager/team-members-manager.component';
import { ScrollButtonComponent } from './components/scroll-button/scroll-button.component';
import { ChangeLegalBasisDialogComponent } from './dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component';
import { RecategorizeImageDialogComponent } from './dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
@ -87,7 +86,6 @@ const components = [
EditDossierTeamMembersComponent,
EditDossierAttributesComponent,
TeamMembersManagerComponent,
ScrollButtonComponent,
PageExclusionComponent,
DossierDetailsStatsComponent,
EditDossierDeletedDocumentsComponent,

View File

@ -5,79 +5,19 @@
<div class="red-content-inner">
<div class="content-container">
<iqser-table-header [tableColumnConfigs]="tableColumnConfigs" [tableHeaderLabel]="tableHeaderLabel"></iqser-table-header>
<redaction-empty-state
(action)="openAddDossierDialog()"
*ngIf="entitiesService.noData$ | async"
[buttonLabel]="'dossier-listing.no-data.action' | translate"
[showButton]="currentUser.isManager"
[text]="'dossier-listing.no-data.title' | translate"
icon="red:folder"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'dossier-listing.no-match.title' | translate"></redaction-empty-state>
<cdk-virtual-scroll-viewport #scrollViewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="let dossier of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.pointer]="!!dossier"
[routerLink]="['/main/dossiers/' + dossier.dossierId.toString()]"
class="table-item"
>
<div class="filename">
<div [matTooltip]="dossier.dossierName" class="table-item-title heading" matTooltipPosition="above">
{{ dossier.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ dossier.dossierTemplateName }}
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ dossier.filesLength }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ dossier.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberCount }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
<div>
<redaction-needs-work-badge [needsWorkInput]="dossier"></redaction-needs-work-badge>
</div>
<div class="user-column">
<redaction-initials-avatar [userId]="dossier.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
<div class="status-container">
<redaction-dossier-listing-actions
(actionPerformed)="calculateData()"
[dossier]="dossier"
></redaction-dossier-listing-actions>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
<redaction-scroll-button [itemSize]="itemSize" [scrollViewport]="scrollViewport"></redaction-scroll-button>
<iqser-table
(noDataAction)="openAddDossierDialog()"
[hasScrollButton]="true"
[itemSize]="85"
[noDataButtonLabel]="'dossier-listing.no-data.action' | translate"
[noDataText]="'dossier-listing.no-data.title' | translate"
[noMatchText]="'dossier-listing.no-match.title' | translate"
[showNoDataButton]="currentUser.isManager"
noDataIcon="red:folder"
></iqser-table>
</div>
<div class="right-container" redactionHasScrollbar>
<div class="right-container" iqserHasScrollbar>
<redaction-dossier-listing-details
*ngIf="(entitiesService.noData$ | async) === false"
[documentsChartData]="documentsChartData"
@ -87,6 +27,60 @@
</div>
</section>
<ng-template #needsWorkTemplate let-filter="filter">
<ng-template #needsWorkFilterTemplate let-filter="filter">
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</ng-template>
<ng-template #nameTemplate let-dossier="entity">
<div class="cell">
<div [matTooltip]="dossier.dossierName" class="table-item-title heading" matTooltipPosition="above">
{{ dossier.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ dossier.dossierTemplateName }}
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ dossier.filesLength }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ dossier.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ dossier.memberCount }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dossier.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
</ng-template>
<ng-template #needsWorkTemplate let-dossier="entity">
<div class="cell">
<redaction-needs-work-badge [needsWorkInput]="dossier"></redaction-needs-work-badge>
</div>
</ng-template>
<ng-template #ownerTemplate let-dossier="entity">
<div class="cell user-column">
<redaction-initials-avatar [userId]="dossier.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #statusTemplate let-dossier="entity">
<div class="cell status-container">
<redaction-dossier-listing-actions (actionPerformed)="calculateData()" [dossier]="dossier"></redaction-dossier-listing-actions>
</div>
</ng-template>

View File

@ -1,44 +1,22 @@
@import '../../../../../assets/styles/variables';
.content-container {
position: relative;
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 2fr 1fr 1fr auto 11px;
.table-item {
> div {
height: 85px;
padding: 0 24px;
}
.status-container {
width: 160px;
padding-right: 13px;
}
}
:host {
::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell {
&.status-container {
width: 160px;
}
}
.right-container {
display: flex;
width: 466px;
min-width: 466px;
padding-right: 11px;
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 2fr 1fr 1fr auto;
}
padding-right: 0;
}
redaction-dossier-listing-details {
min-width: 466px;
}
}
}
.right-container {
display: flex;
width: 466px;
min-width: 466px;
padding-right: 11px;
&.has-scrollbar:hover {
padding-right: 0;
}
redaction-dossier-listing-details {
min-width: 466px;
}
}

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AfterViewInit, Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
@ -16,25 +16,22 @@ import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { UserPreferenceService } from '@services/user-preference.service';
import { ButtonConfig } from '@shared/components/page-header/models/button-config.model';
import { DefaultListingServices, keyChecker, ListingComponent, NestedFilter, TableColumnConfig } from '@iqser/common-ui';
import { DefaultListingServices, keyChecker, ListingComponent, NestedFilter, TableColumnConfig, TableComponent } from '@iqser/common-ui';
import { workloadTranslations } from '../../translations/workload-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { annotationFilterChecker, dossierMemberChecker, dossierStatusChecker, dossierTemplateChecker } from '@utils/filter-utils';
import { PermissionsService } from '@services/permissions.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
templateUrl: './dossier-listing-screen.component.html',
styleUrls: ['./dossier-listing-screen.component.scss'],
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossierListingScreenComponent) }]
})
export class DossierListingScreenComponent
extends ListingComponent<DossierWrapper>
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach
{
readonly itemSize = 85;
protected readonly _primaryKey = 'dossierName';
readonly currentUser = this._userService.currentUser;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs: readonly ButtonConfig[] = [
@ -46,32 +43,21 @@ export class DossierListingScreenComponent
type: 'primary'
}
];
readonly tableColumnConfigs: readonly TableColumnConfig<DossierWrapper>[] = [
{
label: _('dossier-listing.table-col-names.name'),
sortByKey: 'dossierName'
},
{
label: _('dossier-listing.table-col-names.needs-work')
},
{
label: _('dossier-listing.table-col-names.owner'),
class: 'user-column'
},
{
label: _('dossier-listing.table-col-names.status'),
class: 'flex-end'
}
];
tableColumnConfigs: TableColumnConfig<DossierWrapper>[];
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
@ViewChild('nameTemplate', { static: true }) nameTemplate: TemplateRef<never>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<never>;
@ViewChild('ownerTemplate', { static: true }) ownerTemplate: TemplateRef<never>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'dossierName';
private _lastScrolledIndex: number;
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<unknown>;
@ViewChild(CdkVirtualScrollViewport)
private readonly _scrollViewport: CdkVirtualScrollViewport;
@ViewChild('needsWorkFilterTemplate', {
read: TemplateRef,
static: true
})
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<DossierWrapper>;
constructor(
private readonly _router: Router,
@ -97,7 +83,10 @@ export class DossierListingScreenComponent
return this.entitiesService.all.length - this._activeDossiersCount;
}
routerLinkFn = (dossier: DossierWrapper) => ['/main/dossiers/' + dossier.dossierId];
ngOnInit(): void {
this._configureTableColumns();
this.calculateData();
this.addSubscription = timer(0, 10000).subscribe(async () => {
@ -112,7 +101,9 @@ export class DossierListingScreenComponent
}
ngAfterViewInit(): void {
this.addSubscription = this._scrollViewport.scrolledIndexChange.pipe(tap(index => (this._lastScrolledIndex = index))).subscribe();
this.addSubscription = this._tableComponent.scrollViewport.scrolledIndexChange
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
}
ngOnAttach(): void {
@ -120,7 +111,7 @@ export class DossierListingScreenComponent
this._loadEntitiesFromState();
this.ngOnInit();
this.ngAfterViewInit();
this._scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach(): void {
@ -161,6 +152,32 @@ export class DossierListingScreenComponent
this.documentsChartData = this._translateChartService.translateStatus(this.documentsChartData);
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{
label: _('dossier-listing.table-col-names.name'),
sortByKey: 'dossierName',
template: this.nameTemplate,
width: '2fr'
},
{
label: _('dossier-listing.table-col-names.needs-work'),
template: this.needsWorkTemplate
},
{
label: _('dossier-listing.table-col-names.owner'),
class: 'user-column',
template: this.ownerTemplate
},
{
label: _('dossier-listing.table-col-names.status'),
class: 'flex-end',
template: this.statusTemplate,
width: 'auto'
}
];
}
private _loadEntitiesFromState() {
this.entitiesService.setEntities(this._appStateService.allDossiers);
}
@ -232,7 +249,7 @@ export class DossierListingScreenComponent
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: this._needsWorkTemplate,
filterTemplate: this._needsWorkFilterTemplate,
filters: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true

View File

@ -16,18 +16,18 @@
*ngIf="permissionsService.displayReanalyseBtn()"
[tooltipClass]="'small ' + ((entitiesService.areSomeSelected$ | async) ? '' : 'warn')"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[type]="circleButtonTypes.warn"
icon="iqser:refresh"
tooltipPosition="below"
[type]="circleButtonTypes.warn"
></iqser-circle-button>
<iqser-circle-button
(action)="fileInput.click()"
[tooltip]="'dossier-overview.header-actions.upload-document' | translate"
[type]="circleButtonTypes.primary"
class="ml-14"
icon="red:upload"
tooltipPosition="below"
[type]="circleButtonTypes.primary"
></iqser-circle-button>
</redaction-page-header>
@ -35,146 +35,23 @@
<div class="red-content-inner">
<div [class.extended]="collapsedDetails" class="content-container">
<iqser-table-header
<iqser-table
(noDataAction)="fileInput.click()"
[bulkActions]="bulkActions"
[hasScrollButton]="true"
[itemSize]="80"
[noDataButtonLabel]="'dossier-overview.no-data.action' | translate"
[noDataText]="'dossier-overview.no-data.title' | translate"
[noMatchText]="'dossier-overview.no-match.title' | translate"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<redaction-empty-state
(action)="fileInput.click()"
*ngIf="entitiesService.noData$ | async"
[buttonLabel]="'dossier-overview.no-data.action' | translate"
[text]="'dossier-overview.no-data.title' | translate"
buttonIcon="red:upload"
icon="red:document"
></redaction-empty-state>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'dossier-overview.no-match.title' | translate"></redaction-empty-state>
<cdk-virtual-scroll-viewport #scrollViewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="let fileStatus of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.disabled]="fileStatus.excluded"
[class.last-opened]="fileStatus.lastOpened"
[class.pointer]="fileStatus.canBeOpened"
[routerLink]="fileLink(fileStatus)"
class="table-item"
>
<div (click)="toggleEntitySelected($event, fileStatus)" class="selection-column">
<iqser-round-checkbox [active]="isSelected(fileStatus)"></iqser-round-checkbox>
</div>
<div>
<div class="filename-wrapper">
<div
[class.disabled]="fileStatus.isPending"
[class.error]="fileStatus.isError"
class="table-item-title text-overflow"
>
<span [matTooltip]="fileStatus.filename" matTooltipPosition="above">
{{ fileStatus.filename }}
</span>
</div>
</div>
<div *ngIf="fileStatus.primaryAttribute" class="small-label stats-subtitle">
<div class="primary-attribute text-overflow">
<span [matTooltip]="fileStatus.primaryAttribute" matTooltipPosition="above">
{{ fileStatus.primaryAttribute }}
</span>
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ fileStatus.numberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:exclude-pages"></mat-icon>
{{ fileStatus.excludedPagesCount }}
</div>
<div
*ngIf="fileStatus.lastOCRTime"
[matTooltipPosition]="'above'"
[matTooltip]="'dossier-overview.ocr-performed' | translate"
>
<mat-icon svgIcon="red:ocr"></mat-icon>
{{ fileStatus.lastOCRTime | date: 'mediumDate' }}
</div>
</div>
</div>
<div>
<div [class.error]="fileStatus.isError" class="small-label">
{{ fileStatus.added | date: 'd MMM. yyyy, hh:mm a' }}
</div>
</div>
<div *ngFor="let config of displayedInFileListAttributes">
{{ fileStatus.fileAttributes.attributeIdToValue[config.id] }}
</div>
<!-- always show A for error-->
<div *ngIf="fileStatus.isError">
<redaction-annotation-icon color="#dd4d50" label="A" type="square"></redaction-annotation-icon>
</div>
<div *ngIf="!fileStatus.isError">
<redaction-needs-work-badge [needsWorkInput]="fileStatus"></redaction-needs-work-badge>
</div>
<div *ngIf="!fileStatus.isError" class="user-column">
<redaction-initials-avatar [userId]="fileStatus.currentReviewer" [withName]="true"></redaction-initials-avatar>
</div>
<div *ngIf="!fileStatus.isError">
<div class="quick-navigation">
<mat-icon svgIcon="red:pages"></mat-icon>
{{ fileStatus.numberOfPages }}
</div>
</div>
<div [class.extend-cols]="fileStatus.isError" class="status-container">
<div
*ngIf="fileStatus.isError"
class="small-label error"
translate="dossier-overview.file-listing.file-entry.file-error"
></div>
<div
*ngIf="fileStatus.isPending"
class="small-label"
translate="dossier-overview.file-listing.file-entry.file-pending"
></div>
<div
*ngIf="fileStatus.isProcessing"
class="small-label loading"
translate="dossier-overview.file-listing.file-entry.file-processing"
></div>
<iqser-status-bar
*ngIf="fileStatus.isWorkable"
[configs]="[
{
color: fileStatus.status,
length: 1
}
]"
></iqser-status-bar>
<redaction-file-actions
(actionPerformed)="calculateData()"
*ngIf="!fileStatus.isProcessing"
[fileStatus]="fileStatus"
class="mr-4"
></redaction-file-actions>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
<redaction-scroll-button [itemSize]="itemSize" [scrollViewport]="scrollViewport"></redaction-scroll-button>
[showNoDataButton]="true"
[tableItemClasses]="{ disabled: disabledFn, 'last-opened': lastOpenedFn }"
noDataButtonIcon="red:upload"
noDataIcon="red:document"
></iqser-table>
</div>
<div [class.collapsed]="collapsedDetails" class="right-container" redactionHasScrollbar>
<div [class.collapsed]="collapsedDetails" class="right-container" iqserHasScrollbar>
<redaction-dossier-details
(openAssignDossierMembersDialog)="openAssignDossierMembersDialog()"
(openDossierDictionaryDialog)="openDossierDictionaryDialog()"
@ -185,7 +62,7 @@
</div>
</section>
<ng-template #needsWorkTemplate let-filter="filter">
<ng-template #needsWorkFilterTemplate let-filter="filter">
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</ng-template>
@ -194,3 +71,106 @@
<ng-template #bulkActions>
<redaction-dossier-overview-bulk-actions (reload)="bulkActionPerformed()"></redaction-dossier-overview-bulk-actions>
</ng-template>
<ng-template #filenameTemplate let-fileStatus="entity">
<div class="cell">
<div class="filename-wrapper">
<div [class.error]="fileStatus.isError" class="table-item-title text-overflow">
<span [matTooltip]="fileStatus.filename" matTooltipPosition="above">
{{ fileStatus.filename }}
</span>
</div>
</div>
<div *ngIf="fileStatus.primaryAttribute" class="small-label">
<div class="primary-attribute text-overflow">
<span [matTooltip]="fileStatus.primaryAttribute" matTooltipPosition="above">
{{ fileStatus.primaryAttribute }}
</span>
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ fileStatus.numberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:exclude-pages"></mat-icon>
{{ fileStatus.excludedPagesCount }}
</div>
<div *ngIf="fileStatus.lastOCRTime" [matTooltipPosition]="'above'" [matTooltip]="'dossier-overview.ocr-performed' | translate">
<mat-icon svgIcon="red:ocr"></mat-icon>
{{ fileStatus.lastOCRTime | date: 'mediumDate' }}
</div>
</div>
</div>
</ng-template>
<ng-template #addedOnTemplate let-fileStatus="entity">
<div class="cell">
<div [class.error]="fileStatus.isError" class="small-label">
{{ fileStatus.added | date: 'd MMM. yyyy, hh:mm a' }}
</div>
</div>
</ng-template>
<ng-template #attributeTemplate let-config="extra" let-fileStatus="entity">
<div class="cell">
{{ fileStatus.fileAttributes.attributeIdToValue[config.id] }}
</div>
</ng-template>
<ng-template #needsWorkTemplate let-fileStatus="entity">
<!-- always show A for error-->
<div *ngIf="fileStatus.isError" class="cell">
<redaction-annotation-icon color="#dd4d50" label="A" type="square"></redaction-annotation-icon>
</div>
<div *ngIf="!fileStatus.isError" class="cell">
<redaction-needs-work-badge [needsWorkInput]="fileStatus"></redaction-needs-work-badge>
</div>
</ng-template>
<ng-template #reviewerTemplate let-fileStatus="entity">
<div *ngIf="!fileStatus.isError" class="user-column cell">
<redaction-initials-avatar [userId]="fileStatus.currentReviewer" [withName]="true"></redaction-initials-avatar>
</div>
</ng-template>
<ng-template #pagesTemplate let-fileStatus="entity">
<div *ngIf="!fileStatus.isError" class="cell">
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ fileStatus.numberOfPages }}
</div>
</div>
</div>
</ng-template>
<ng-template #statusTemplate let-fileStatus="entity">
<div [class.extend-cols]="fileStatus.isError" class="status-container cell">
<div *ngIf="fileStatus.isError" class="small-label error" translate="dossier-overview.file-listing.file-entry.file-error"></div>
<div *ngIf="fileStatus.isPending" class="small-label" translate="dossier-overview.file-listing.file-entry.file-pending"></div>
<div
*ngIf="fileStatus.isProcessing"
class="small-label loading"
translate="dossier-overview.file-listing.file-entry.file-processing"
></div>
<iqser-status-bar
*ngIf="fileStatus.isWorkable"
[configs]="[
{
color: fileStatus.status,
length: 1
}
]"
></iqser-status-bar>
<redaction-file-actions
(actionPerformed)="calculateData()"
*ngIf="!fileStatus.isProcessing"
[fileStatus]="fileStatus"
class="mr-4"
></redaction-file-actions>
</div>
</ng-template>

View File

@ -1,66 +1,41 @@
@import '../../../../../assets/styles/variables';
:root {
--dynamic-columns: '1fr';
}
.file-upload-input {
display: none;
}
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 3fr 2fr repeat(var(--dynamic-columns, 1), 1fr) 2fr 1fr auto 11px;
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item {
&.last-opened {
> .selection-column {
padding-left: 6px !important;
border-left: 4px solid $primary;
}
.table-item {
> div {
padding-left: 10px;
}
.disabled {
color: $grey-7;
}
.error {
color: $primary;
}
.extend-cols {
grid-column-end: span 3;
align-items: flex-end;
}
.table-item-title {
max-width: 25vw;
}
.quick-navigation {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
opacity: 0.7;
color: $accent;
font-size: 11px;
letter-spacing: 0;
line-height: 14px;
.mat-icon {
width: 10px;
height: 10px;
margin-right: 4px;
}
}
.status-container {
align-items: flex-end;
}
> div {
animation: red-fading-background 3s 1;
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 3fr 2fr repeat(var(--dynamic-columns, 1), 1fr) 2fr 1fr auto;
> div.cell {
.error {
color: $primary;
}
.table-item-title {
max-width: 25vw;
}
.primary-attribute {
padding-top: 6px;
}
&.extend-cols {
grid-column-end: span 3;
align-items: flex-end;
}
&.status-container {
align-items: flex-end;
}
}
}
@ -80,45 +55,6 @@ cdk-virtual-scroll-viewport {
}
}
.reanalyse-link {
color: $accent;
text-decoration: underline;
&:hover {
color: lighten($accent, 10%);
}
}
.mr-4 {
margin-right: 4px;
}
.disabled {
> div {
background-color: $grey-2;
color: $grey-7;
}
redaction-file-actions {
color: initial;
}
}
.primary-attribute {
padding-top: 6px;
}
.last-opened {
> .selection-column {
padding-left: 6px !important;
border-left: 4px solid $primary;
}
> div {
animation: red-fading-background 3s 1;
}
}
@keyframes red-fading-background {
0% {
background-color: rgba($primary, 0.1);

View File

@ -1,4 +1,15 @@
import { ChangeDetectorRef, Component, ElementRef, HostListener, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
HostListener,
Injector,
OnDestroy,
OnInit,
TemplateRef,
ViewChild
} from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
import { FileUploadModel } from '@upload-download/model/file-upload.model';
@ -26,6 +37,7 @@ import {
LoadingService,
NestedFilter,
TableColumnConfig,
TableComponent,
Toaster
} from '@iqser/common-ui';
import { DossierAttributesService } from '@shared/services/controller-wrappers/dossier-attributes.service';
@ -36,17 +48,16 @@ import { fileStatusTranslations } from '../../translations/file-status-translati
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { annotationFilterChecker } from '@utils/filter-utils';
import { PermissionsService } from '@services/permissions.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { RouterHistoryService } from '@services/router-history.service';
import { FileAttributeConfig } from '@redaction/red-ui-http';
import { DossierWrapper } from '../../../../state/model/dossier.wrapper';
@Component({
templateUrl: './dossier-overview-screen.component.html',
styleUrls: ['./dossier-overview-screen.component.scss'],
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => DossierOverviewScreenComponent) }]
})
export class DossierOverviewScreenComponent extends ListingComponent<FileStatusWrapper> implements OnInit, OnDestroy, OnDetach, OnAttach {
readonly itemSize = 80;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
currentDossier = this._appStateService.activeDossier;
@ -63,43 +74,21 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
collapsedDetails = false;
dossierAttributes: DossierAttributeWithValue[] = [];
fileAttributeConfigs: FileAttributeConfig[];
dynamicColumnsCount = 0;
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<never>;
@ViewChild('addedOnTemplate', { static: true }) addedOnTemplate: TemplateRef<never>;
@ViewChild('attributeTemplate', { static: true }) attributeTemplate: TemplateRef<never>;
@ViewChild('needsWorkTemplate', { static: true }) needsWorkTemplate: TemplateRef<never>;
@ViewChild('reviewerTemplate', { static: true }) reviewerTemplate: TemplateRef<never>;
@ViewChild('pagesTemplate', { static: true }) pagesTemplate: TemplateRef<never>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<never>;
protected readonly _primaryKey = 'filename';
private readonly _defaultTableConfigs: readonly TableColumnConfig<FileStatusWrapper>[] = [
{
label: _('dossier-overview.table-col-names.name'),
sortByKey: 'filename'
},
{
label: _('dossier-overview.table-col-names.added-on'),
sortByKey: 'added'
},
{
label: _('dossier-overview.table-col-names.needs-work')
},
{
label: _('dossier-overview.table-col-names.assigned-to'),
class: 'user-column',
sortByKey: 'reviewerName'
},
{
label: _('dossier-overview.table-col-names.pages'),
sortByKey: 'pages'
},
{
label: _('dossier-overview.table-col-names.status'),
class: 'flex-end',
sortByKey: 'statusSort'
}
];
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _lastScrolledIndex: number;
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<unknown>;
@ViewChild('needsWorkFilterTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkFilterTemplate: TemplateRef<unknown>;
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
@ViewChild(CdkVirtualScrollViewport)
private readonly _scrollViewport: CdkVirtualScrollViewport;
@ViewChild(TableComponent) private readonly _tableComponent: TableComponent<DossierWrapper>;
constructor(
private readonly _toaster: Toaster,
@ -138,7 +127,14 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
return this.fileAttributeConfigs.filter(config => config.displayedInFileList);
}
routerLinkFn = (fileStatus: FileStatusWrapper) =>
fileStatus.canBeOpened ? [`/main/dossiers/${this.currentDossier.dossierId}/file/${fileStatus.fileId}`] : [];
disabledFn = (fileStatus: FileStatusWrapper) => fileStatus.excluded;
lastOpenedFn = (fileStatus: FileStatusWrapper) => fileStatus.lastOpened;
async ngOnInit(): Promise<void> {
this._configureTableColumns();
this._loadingService.start();
try {
this._fileDropOverlayService.initFileDropHandling();
@ -160,12 +156,11 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
).fileAttributeConfigs;
});
this.addSubscription = this._scrollViewport.scrolledIndexChange
this.addSubscription = this._tableComponent.scrollViewport.scrolledIndexChange
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
this.searchService.setSearchKey('filename');
this._configureTableColumns();
this.dossierAttributes = await this._dossierAttributesService.getValues(this.currentDossier);
} catch (e) {
@ -183,7 +178,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
await this._appStateService.reloadActiveDossierFiles();
this._loadEntitiesFromState();
await this.ngOnInit();
this._scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
this._tableComponent.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach() {
@ -233,10 +228,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
this._fileInput.nativeElement.value = null;
}
fileLink(fileStatus: FileStatusWrapper) {
return fileStatus.canBeOpened ? [`/main/dossiers/${this.currentDossier.dossierId}/file/${fileStatus.fileId}`] : [];
}
async bulkActionPerformed() {
this.entitiesService.setSelected([]);
await this.reloadDossiers();
@ -271,17 +262,46 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
const dynamicColumns: TableColumnConfig<FileStatusWrapper>[] = [];
for (const config of this.displayedInFileListAttributes) {
if (config.displayedInFileList) {
dynamicColumns.push({ label: config.label, notTranslatable: true });
dynamicColumns.push({ label: config.label, notTranslatable: true, template: this.attributeTemplate, extra: config });
}
}
this.tableColumnConfigs = [
this._defaultTableConfigs[0],
this._defaultTableConfigs[1],
{
label: _('dossier-overview.table-col-names.name'),
sortByKey: 'filename',
template: this.filenameTemplate,
width: '3fr'
},
{
label: _('dossier-overview.table-col-names.added-on'),
sortByKey: 'added',
template: this.addedOnTemplate,
width: '2fr'
},
...dynamicColumns,
...this._defaultTableConfigs.slice(2)
{
label: _('dossier-overview.table-col-names.needs-work'),
template: this.needsWorkTemplate
},
{
label: _('dossier-overview.table-col-names.assigned-to'),
class: 'user-column',
sortByKey: 'reviewerName',
template: this.reviewerTemplate,
width: '2fr'
},
{
label: _('dossier-overview.table-col-names.pages'),
sortByKey: 'pages',
template: this.pagesTemplate
},
{
label: _('dossier-overview.table-col-names.status'),
class: 'flex-end',
sortByKey: 'statusSort',
template: this.statusTemplate
}
];
this.dynamicColumnsCount = dynamicColumns.length + 1; // zero is not allowed in repeat, by default we repeat 1 column
document.documentElement.style.setProperty('--dynamic-columns', `${this.dynamicColumnsCount}`);
}
private _loadEntitiesFromState() {
@ -404,7 +424,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work',
filterTemplate: this._needsWorkTemplate,
filterTemplate: this._needsWorkFilterTemplate,
filters: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true

View File

@ -147,12 +147,12 @@
</div>
<div class="right-container">
<redaction-empty-state
<iqser-empty-state
*ngIf="appStateService.activeFile.excluded && !viewDocumentInfo && !excludePages"
[horizontalPadding]="40"
[text]="'file-preview.tabs.is-excluded' | translate"
icon="red:needs-work"
></redaction-empty-state>
></iqser-empty-state>
<redaction-document-info
(closeDocumentInfoView)="viewDocumentInfo = false"

View File

@ -2,97 +2,85 @@
<redaction-page-header
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
[searchPlaceholder]="'search.placeholder' | translate"
[searchPosition]="searchPositions.beforeFilters"
[searchWidth]="600"
[showCloseButton]="true"
[searchPosition]="searchPositions.beforeFilters"
></redaction-page-header>
<div class="overlay-shadow"></div>
<div class="red-content-inner">
<div class="content-container">
<iqser-table-header [tableColumnConfigs]="tableColumnConfigs" [tableHeaderLabel]="tableHeaderLabel"></iqser-table-header>
<redaction-empty-state
*ngIf="searchResult.length === 0"
[icon]="'iqser:search'"
[text]="'search-screen.no-data' | translate"
></redaction-empty-state>
<cdk-virtual-scroll-viewport #scrollViewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="let item of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.pointer]="true"
[routerLink]="item.routerLink"
class="table-item"
>
<div class="filename">
<div [matTooltip]="item.filename" class="table-item-title heading" matTooltipPosition="above">
<span
*ngIf="item.highlights.filename; else defaultFilename"
[innerHTML]="item.highlights.filename[0]"
class="highlights"
></span>
<ng-template #defaultFilename>{{ item.filename }}</ng-template>
</div>
<ng-container *ngIf="item.highlights['sections.text'] as highlights">
<div *ngIf="highlights.length > 0" class="small-label">
<span [innerHTML]="highlights[0]" class="highlights"></span>
</div>
<div *ngIf="highlights.length > 1" class="small-label">
<span [innerHTML]="highlights[1]" class="highlights"></span>
</div>
</ng-container>
<div *ngIf="item.unmatched?.length && item.unmatched as unmatched" class="small-label">
<span>
{{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched"
>&nbsp;<s>{{ term }}</s></span
>.&nbsp;{{ 'search-screen.must-contain' | translate }}:
<span
(click)="$event.stopPropagation(); updateNavigation(search$.getValue().query, term)"
*ngFor="let term of unmatched"
>&nbsp;<u>{{ term }}</u></span
>
</span>
</div>
</div>
<div>
<iqser-status-bar
[configs]="[
{
color: item.status,
label: fileStatusTranslations[item.status] | translate,
length: 1,
cssClass: 'all-caps-label'
}
]"
[small]="true"
></iqser-status-bar>
</div>
<div class="small-label">
{{ item.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ item.numberOfPages }}
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
<redaction-scroll-button
*ngIf="searchResult.length"
[itemSize]="itemSize"
[scrollViewport]="scrollViewport"
></redaction-scroll-button>
<iqser-table
[hasScrollButton]="true"
[itemSize]="85"
[noDataText]="'search-screen.no-data' | translate"
noDataIcon="iqser:search"
></iqser-table>
</div>
</div>
</section>
<ng-template #filenameTemplate let-item="entity">
<div class="cell filename">
<div [matTooltip]="item.filename" class="table-item-title heading" matTooltipPosition="above">
<span
*ngIf="item.highlights.filename; else defaultFilename"
[innerHTML]="item.highlights.filename[0]"
class="highlights"
></span>
<ng-template #defaultFilename>{{ item.filename }}</ng-template>
</div>
<ng-container *ngIf="item.highlights['sections.text'] as highlights">
<div *ngIf="highlights.length > 0" class="small-label">
<span [innerHTML]="highlights[0]" class="highlights"></span>
</div>
<div *ngIf="highlights.length > 1" class="small-label">
<span [innerHTML]="highlights[1]" class="highlights"></span>
</div>
</ng-container>
<div *ngIf="item.unmatched?.length && item.unmatched as unmatched" class="small-label">
<span>
{{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched"
>&nbsp;<s>{{ term }}</s></span
>.&nbsp;{{ 'search-screen.must-contain' | translate }}:
<span (click)="$event.stopPropagation(); updateNavigation(search$.getValue().query, term)" *ngFor="let term of unmatched"
>&nbsp;<u>{{ term }}</u></span
>
</span>
</div>
</div>
</ng-template>
<ng-template #statusTemplate let-item="entity">
<div class="cell">
<iqser-status-bar
[configs]="[
{
color: item.status,
label: fileStatusTranslations[item.status] | translate,
length: 1,
cssClass: 'all-caps-label'
}
]"
[small]="true"
></iqser-status-bar>
</div>
</ng-template>
<ng-template #dossierTemplate let-item="entity">
<div class="cell small-label">
{{ item.dossierName }}
</div>
</ng-template>
<ng-template #pagesTemplate let-item="entity">
<div class="cell small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ item.numberOfPages }}
</div>
</div>
</ng-template>

View File

@ -1,42 +1,12 @@
@import 'libs/common-ui/src/assets/styles/mixins';
@import '../../../../../assets/styles/variables';
.content-container {
position: relative;
:host ::ng-deep iqser-table cdk-virtual-scroll-viewport .cdk-virtual-scroll-content-wrapper .table-item > div.cell {
.highlights {
@include line-clamp(1);
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 2fr 1fr 1fr auto 11px;
.table-item {
> div {
height: 85px;
padding: 0 24px;
}
.status-container {
width: 160px;
padding-right: 13px;
}
.highlights em {
background-color: #fffcc4;
}
.highlights {
@include line-clamp(1);
}
.stats-subtitle > div {
width: fit-content;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 2fr 1fr 1fr auto;
}
em {
background-color: #fffcc4;
}
}
}

View File

@ -1,4 +1,4 @@
import { Component, Injector, OnDestroy } from '@angular/core';
import { Component, forwardRef, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { DefaultListingServices, keyChecker, Listable, ListingComponent, LoadingService, TableColumnConfig } from '@iqser/common-ui';
import { MatchedDocument, SearchControllerService, SearchResult } from '@redaction/red-ui-http';
import { BehaviorSubject, Observable } from 'rxjs';
@ -32,20 +32,17 @@ interface SearchInput {
@Component({
templateUrl: './search-screen.component.html',
styleUrls: ['./search-screen.component.scss'],
providers: [...DefaultListingServices]
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => SearchScreenComponent) }]
})
export class SearchScreenComponent extends ListingComponent<ListItem> implements OnDestroy {
export class SearchScreenComponent extends ListingComponent<ListItem> implements OnDestroy, OnInit {
readonly fileStatusTranslations = fileStatusTranslations;
readonly searchPositions = SearchPositions;
readonly itemSize = 85;
@ViewChild('filenameTemplate', { static: true }) filenameTemplate: TemplateRef<never>;
@ViewChild('statusTemplate', { static: true }) statusTemplate: TemplateRef<never>;
@ViewChild('dossierTemplate', { static: true }) dossierTemplate: TemplateRef<never>;
@ViewChild('pagesTemplate', { static: true }) pagesTemplate: TemplateRef<never>;
readonly tableHeaderLabel = _('search-screen.table-header');
readonly tableColumnConfigs: TableColumnConfig<ListItem>[] = [
{ label: _('search-screen.cols.document') },
{ label: _('search-screen.cols.status') },
{ label: _('search-screen.cols.dossier') },
{ label: _('search-screen.cols.pages') }
];
tableColumnConfigs: TableColumnConfig<ListItem>[];
readonly search$ = new BehaviorSubject<SearchInput>(null);
readonly searchResults$: Observable<ListItem[]> = this.search$.asObservable().pipe(
switchMap(query => this._search(query)),
@ -94,6 +91,8 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
});
}
routerLinkFn = (entity: ListItem) => [entity.routerLink];
setInitialConfig(): void {
return;
}
@ -104,6 +103,19 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
this._router.navigate([], { queryParams }).then();
}
ngOnInit(): void {
this._configureTableColumns();
}
private _configureTableColumns() {
this.tableColumnConfigs = [
{ label: _('search-screen.cols.document'), template: this.filenameTemplate, width: '2fr' },
{ label: _('search-screen.cols.status'), template: this.statusTemplate },
{ label: _('search-screen.cols.dossier'), template: this.dossierTemplate },
{ label: _('search-screen.cols.pages'), template: this.pagesTemplate, width: 'auto' }
];
}
private _search(searchInput: SearchInput): Observable<SearchResult> {
return this._searchControllerService.search({
dossierIds: searchInput.dossierIds,

View File

@ -14,7 +14,6 @@ export class IconsModule {
'add',
'analyse',
'approved',
'arrow-down-o',
'arrow-right',
'arrow-up',
'assign',

View File

@ -1,21 +0,0 @@
<div
[ngStyle]="{
'padding-top': verticalPadding + 'px',
'padding-left': horizontalPadding + 'px',
'padding-right': horizontalPadding + 'px'
}"
class="empty-state"
>
<mat-icon *ngIf="icon" [svgIcon]="icon"></mat-icon>
<div class="ng-content-wrapper heading-l">
<ng-content></ng-content>
</div>
<div class="heading-l">{{ text }}</div>
<iqser-icon-button
(action)="action.emit()"
*ngIf="showButton"
[icon]="buttonIcon"
[label]="buttonLabel"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
</div>

View File

@ -1,27 +0,0 @@
@import '../../../../../assets/styles/variables';
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
> mat-icon {
height: 60px;
width: 60px;
opacity: 0.1;
}
.heading-l {
color: $grey-7;
}
> .heading-l,
iqser-icon-button {
margin-top: 24px;
}
.ng-content-wrapper:not(:empty) + .heading-l {
display: none;
}
}

View File

@ -1,25 +0,0 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { IconButtonTypes } from '@iqser/common-ui';
@Component({
selector: 'redaction-empty-state',
templateUrl: './empty-state.component.html',
styleUrls: ['./empty-state.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
@Input() text: string;
@Input() icon: string;
@Input() showButton = true;
@Input() buttonIcon = 'red:plus';
@Input() buttonLabel: string;
@Input() horizontalPadding = 100;
@Input() verticalPadding = 120;
@Output() action = new EventEmitter();
ngOnInit(): void {
this.showButton = this.showButton && this.action.observers.length > 0;
}
}

View File

@ -1,4 +1,4 @@
<div (click)="selectPage(currentPage - 1)" [class.disabled]="currentPage < 2" class="page" translate="pagination.previous"></div>
<div (click)="selectPage(currentPage - 1)" [class.disabled]="currentPage < 1" class="page" translate="pagination.previous"></div>
<span>|</span>
<div
(click)="selectPage(page)"

View File

@ -1,11 +1,10 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
const DISPLAYED_ITEMS = 5;
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'redaction-pagination',
templateUrl: './pagination.component.html',
styleUrls: ['./pagination.component.scss']
styleUrls: ['./pagination.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PaginationComponent {
displayedPages: (number | string)[];
@ -31,10 +30,6 @@ export class PaginationComponent {
this._updatePagesArray();
}
get allDisplayed(): boolean {
return this.totalPages > DISPLAYED_ITEMS;
}
selectPage(page: number | string) {
if (page !== '...') {
this.pageChanged.emit(page as number);

View File

@ -1,26 +0,0 @@
import { AfterContentChecked, Directive, ElementRef, HostBinding } from '@angular/core';
@Directive({
selector: '[redactionHasScrollbar]',
exportAs: 'redactionHasScrollbar'
})
export class HasScrollbarDirective implements AfterContentChecked {
@HostBinding('class') class = '';
constructor(private readonly _elementRef: ElementRef) {}
get hasScrollbar() {
return this._elementRef?.nativeElement.clientHeight < this._elementRef?.nativeElement.scrollHeight;
}
ngAfterContentChecked() {
this._process();
}
_process() {
const newClass = this.hasScrollbar ? 'has-scrollbar' : '';
if (this.class !== newClass) {
this.class = newClass;
}
}
}

View File

@ -10,11 +10,9 @@ import { IconsModule } from '../icons/icons.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component';
import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart/simple-doughnut-chart.component';
import { HasScrollbarDirective } from './directives/has-scrollbar.directive';
import { DictionaryAnnotationIconComponent } from './components/dictionary-annotation-icon/dictionary-annotation-icon.component';
import { HiddenActionComponent } from './components/hidden-action/hidden-action.component';
import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component';
import { EmptyStateComponent } from './components/empty-state/empty-state.component';
import { CommonUiModule } from '@iqser/common-ui';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
@ -37,7 +35,6 @@ const components = [
DictionaryAnnotationIconComponent,
HiddenActionComponent,
ConfirmationDialogComponent,
EmptyStateComponent,
SelectComponent,
SideNavComponent,
DictionaryManagerComponent,
@ -47,7 +44,7 @@ const components = [
...buttons
];
const utils = [DatePipe, HasScrollbarDirective, NavigateLastDossiersScreenDirective];
const utils = [DatePipe, NavigateLastDossiersScreenDirective];
const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule, CommonUiModule];

View File

@ -28,7 +28,7 @@ export class PermissionsService {
}
canToggleAnalysis(fileStatus: FileStatusWrapper): boolean {
return this._userService.currentUser.isManager && ['UNASSIGNED', 'UNDER_REVIEW', 'UNDER_APPROVAL'].includes(fileStatus.status);
return this.isReviewerOrApprover(fileStatus) && ['UNASSIGNED', 'UNDER_REVIEW', 'UNDER_APPROVAL'].includes(fileStatus.status);
}
canReanalyseFile(fileStatus = this._activeFile): boolean {

View File

@ -1,18 +0,0 @@
<svg
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.8285 12.0259L16.2427 13.4402L12 17.6828L7.7574 13.4402L9.17161 12.0259L11 13.8544V6.31724H13V13.8544L14.8285 12.0259Z"
fill="currentColor"
/>
<path
clip-rule="evenodd"
d="M19.7782 19.7782C15.4824 24.0739 8.51759 24.0739 4.22183 19.7782C-0.0739417 15.4824 -0.0739417 8.51759 4.22183 4.22183C8.51759 -0.0739419 15.4824 -0.0739419 19.7782 4.22183C24.0739 8.51759 24.0739 15.4824 19.7782 19.7782ZM18.364 18.364C14.8492 21.8787 9.15076 21.8787 5.63604 18.364C2.12132 14.8492 2.12132 9.15076 5.63604 5.63604C9.15076 2.12132 14.8492 2.12132 18.364 5.63604C21.8787 9.15076 21.8787 14.8492 18.364 18.364Z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>

Before

Width:  |  Height:  |  Size: 841 B

View File

@ -77,6 +77,7 @@
display: flex;
justify-content: center;
align-items: center;
width: fit-content;
mat-icon {
width: 10px;

View File

@ -66,6 +66,7 @@ section.settings {
.actions {
display: flex;
align-items: center;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 2px;
@ -349,6 +350,10 @@ section.settings {
cursor: pointer;
}
.mr-4 {
margin-right: 4px !important;
}
.mr-8 {
margin-right: 8px !important;
}

View File

@ -12,121 +12,10 @@
}
}
cdk-virtual-scroll-viewport {
height: calc(100vh - 50px - 31px - 111px);
overflow-y: hidden !important;
.cdk-virtual-scroll-content-wrapper {
display: grid;
.table-item {
display: contents;
> div {
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
box-sizing: border-box;
height: 80px;
border-bottom: 1px solid $separator;
padding: 0 13px;
&:not(.scrollbar-placeholder):not(.selection-column) {
min-width: 110px;
}
&.selection-column {
padding-right: 0 !important;
iqser-round-checkbox .wrapper {
opacity: 0;
transition: opacity 0.2s;
&.active {
opacity: 1;
}
}
}
}
.table-item-title {
font-weight: 600;
@include line-clamp(1);
}
.action-buttons {
position: absolute;
display: none;
right: -11px;
top: 0;
height: 100%;
width: fit-content;
flex-direction: row;
align-items: center;
padding-left: 100px;
padding-right: 24px;
z-index: 1;
background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, $grey-2 35%);
mat-icon {
width: 14px;
}
iqser-circle-button:not(:last-child) {
margin-right: 2px;
}
&.active {
display: flex;
// compensate for scroll
padding-right: 23px;
}
}
input,
mat-select {
margin-top: 0;
}
&:hover {
> div {
background-color: $grey-8;
&.selection-column iqser-round-checkbox .wrapper {
opacity: 1;
}
}
.action-buttons {
display: flex;
}
}
}
}
&:hover {
overflow-y: auto !important;
@include scroll-bar;
&.has-scrollbar {
.table-item {
.action-buttons {
right: 0;
padding-right: 13px;
}
.scrollbar-placeholder {
display: none;
}
}
}
}
}
.attributes-actions-container {
.table-header-actions {
display: flex;
flex: 1;
align-items: center;
justify-content: flex-end;
> *:not(:last-child) {

@ -1 +1 @@
Subproject commit 8b9c96692d5336ae2fdaa843d5adb1d3d9364c47
Subproject commit 6c0f123bd97148f8696038f63c9951c241b71990