Commit f5b0e00f authored by PECQUOT's avatar PECQUOT

[enh] parameter: add prototype view for qualitative values

[enh] add generic memory table for referential
[enh] ReferentialFilter refactored
[enh] change all table view: calculated height and sticky header
parent 607e52fb
import {ChangeDetectionStrategy, Component, Inject, Injector, Input} from '@angular/core';
import {
EntitiesTableDataSource,
EnvironmentService,
filterNotNil,
InMemoryEntitiesService,
isNil,
isNilOrBlank,
isNotNil,
Referential,
RESERVED_END_COLUMNS,
RESERVED_START_COLUMNS
} from "sumaris-lib";
import {ReferentialFilter, ReferentialType} from "@app/referential/model/referential.model";
import {ActivatedRoute} from "@angular/router";
import {ReferentialValidatorService} from "@app/referential/validator/referential.validator";
import {ReferentialTable} from "@app/referential/table/referential.table";
import {ReferentialGenericService} from "@app/referential/generic/referential.generic.service";
import {BehaviorSubject, Subject} from "rxjs";
import {filter} from "rxjs/operators";
import {first} from "rxjs/internal/operators";
import {TableElement} from "@e-is/ngx-material-table";
import {TranslateService} from "@ngx-translate/core";
import {Validators} from "@angular/forms";
import {T} from "@angular/cdk/keycodes";
import {ReferentialMemoryTable} from "@app/referential/table/referential.memory.table";
@Component({
selector: 'app-referential-generic-memory-table',
templateUrl: './referential.generic.table.html',
styleUrls: ['./referential.generic.table.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReferentialGenericMemoryTable extends ReferentialMemoryTable {
@Input() canGoBack = false;
@Input() showPaginator = false;
@Input() showFooter = false;
@Input() set entityName(entityName: string) {
this.setEntityName(entityName);
}
get entityName(): string {
return this.filter?.entityName;
}
entityTypes$ = new BehaviorSubject<ReferentialType[]>(undefined);
entityType$ = new BehaviorSubject<ReferentialType>(undefined);
idIsString$ = new BehaviorSubject<boolean>(undefined);
idLabel$ = new BehaviorSubject<string>(undefined);
title$ = new BehaviorSubject<string>('REFERENTIAL.LIST.TITLE');
constructor(
protected injector: Injector,
protected referentialService: ReferentialGenericService,
protected validatorService: ReferentialValidatorService,
@Inject(EnvironmentService) protected environment
) {
super(injector,
// columns
RESERVED_START_COLUMNS
.concat([
'label',
'name',
'description',
'comments',
'status',
'creationDate',
'updateDate'
])
.concat(RESERVED_END_COLUMNS),
Referential,
new InMemoryEntitiesService<Referential, ReferentialFilter>(Referential, {
filterFnFactory: ReferentialFilter.searchFilter
}),
validatorService,
{
onRowCreated: (row) => this.onRowCreated(row),
prependNewElements: false,
suppressErrors: true
}
);
// add entityName in filter form
this.filterForm.addControl('entityName', this.formBuilder.control(null, Validators.required));
if (!this.showPaginator) {
this.defaultPageSize = -1;
}
}
ngOnInit() {
super.ngOnInit();
// Load entity type
this.registerSubscription(
this.referentialService.loadTypes().subscribe(value => this.entityTypes$.next(value))
);
// Listen current entity type
this.registerSubscription(
filterNotNil(this.entityType$).subscribe(entityType => {
// update title
this.title$.next(this.getI18nEntityName(entityType.name));
// update id header
this.idLabel$.next(this.getI18nEntityIdType(entityType));
this.idIsString$.next(entityType.idIsString);
// update columns visibility
this.updateColumns(entityType);
// update filter search placeholder
this.updateFilterSearchFields(entityType);
})
);
}
async setEntityName(entityName: string, opts?: { emitEvent?: boolean }) {
// No entityName: error
if (isNilOrBlank(entityName)) {
throw new Error(`[referential] Entity name not provided !`);
}
// No change: skip
if (this.entityName === entityName) return;
opts = opts || {emitEvent: true};
// this.canOpenDetail = false;
// Wait end of entity loading
const entityTypes = this.entityTypes$.getValue();
if (isNil(entityTypes) || !entityTypes.length) {
console.debug("[referential] Waiting entity types to be loaded...");
return this.entityTypes$.pipe(filter(isNotNil), first())
// Loop
.subscribe((entities) => {
this.setEntityName(entityName);
});
}
this.settingsId = this.generateTableId();
const entityType = entityTypes.find(e => e.name === entityName);
if (!entityType) {
throw new Error(`[referential] Entity type for {${entityName}} not found !`);
}
this.entityType$.next(entityType);
if (this.paginator)
this.paginator.pageIndex = 0;
this.filterForm.get('entityName').setValue(entityName);
// prevent save on setFilter
this.markAsPristine();
this.setFilter(this.filterForm.value, {emitEvent: false});
if (opts.emitEvent !== false) {
console.info(`[referential] Loading ${entityName}...`);
this.onRefresh.emit();
}
}
updateColumns(entityType: ReferentialType) {
this.setShowColumn('label', entityType.labelPresent);
this.setShowColumn('description', entityType.descriptionPresent);
this.setShowColumn('comment', entityType.commentPresent);
this.setShowColumn('creationDate', entityType.creationDatePresent);
this.displayedColumns = this.getDisplayColumns();
this.markForCheck();
}
getI18nEntityName(entityName: string): string {
if (isNil(entityName)) return undefined;
const tableName = entityName.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
const key = `REFERENTIAL.ENTITY.${tableName}`;
let message = this.injector.get(TranslateService).instant(key);
if (message !== key) return message;
// No I18n translation: continue
// Use tableName, but replace underscore with space
message = tableName.replace(/[_-]+/g, " ").toUpperCase() || '';
// First letter as upper case
if (message.length > 1) {
return message.substring(0, 1) + message.substring(1).toLowerCase();
}
return message;
}
getI18nEntityIdType(entityType: ReferentialType): string {
return (entityType.idIsString) ? "REFERENTIAL.CODE" : "REFERENTIAL.ID";
}
updateFilterSearchFields(entityType: ReferentialType) {
const i18nFields = [this.getI18nEntityIdType(entityType)];
if (entityType.labelPresent)
i18nFields.push("REFERENTIAL.LABEL");
i18nFields.push("REFERENTIAL.NAME");
this.searchFields$.next(
i18nFields.map(value => this.translate.instant(value)).join(', ')
);
}
protected onRowCreated(row: TableElement<Referential>) {
const defaultValues = {
entityName: this.entityName
};
if (row.validator) {
row.validator.patchValue(defaultValues);
}
else {
Object.assign(row.currentData, defaultValues);
}
}
protected onStartEditRow(row: TableElement<Referential>) {
const entityType = this.entityType$.value;
if (isNil(entityType))
throw Error("Can't edit a row with undefined entity type");
row.validator = this.validatorService.getRowValidator(row.currentData, {type: entityType});
}
addRow(event?: any): boolean {
// Create new row
const result = super.addRow(event);
if (!result) return result;
const row = this.dataSource.getRow(-1);
row.validator.controls['entityName'].setValue(this.entityName);
return true;
}
}
......@@ -5,7 +5,7 @@ import {ReferentialQueries} from "@app/referential/services/referential.queries"
import gql from "graphql-tag";
import {referentialFragments} from "@app/referential/services/referential.fragments";
import {Observable} from "rxjs";
import {BaseReferentialFilter, ReferentialFilter, ReferentialType} from "@app/referential/model/referential.model";
import {ReferentialFilter, ReferentialType} from "@app/referential/model/referential.model";
import {ErrorCodes} from "@app/referential/services/errors";
import {map} from "rxjs/operators";
import {SortDirection} from "@angular/material/sort";
......@@ -119,7 +119,7 @@ export class ReferentialGenericService extends BaseEntityService<Referential>
size: size || 100,
sortBy: sortBy,
sortDirection: sortDirection || 'asc',
filter: BaseReferentialFilter.asPodObject(filter)
filter: ReferentialFilter.asObject(filter)
};
let now = new Date();
......@@ -182,7 +182,7 @@ export class ReferentialGenericService extends BaseEntityService<Referential>
size: size || 100,
sortBy: sortBy || filter.searchAttribute,
sortDirection: sortDirection || 'asc',
filter: BaseReferentialFilter.asPodObject(filter)
filter: ReferentialFilter.asObject(filter)
};
const now = Date.now();
......
<app-toolbar [title]="title$|async|translate" color="primary">
<app-toolbar [title]="title$|async|translate" color="primary" [canGoBack]="canGoBack">
</app-toolbar>
<ion-content class="ion-no-padding">
......@@ -284,7 +284,7 @@
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
[class.mat-row-error]="row.validator.invalid"
[class.mat-row-disabled]="!row.editing"
......@@ -295,17 +295,12 @@
</div>
<!-- Paginator -->
<ion-row class="ion-no-padding">
<ion-col></ion-col>
<ion-col class="ion-no-padding" size="auto">
<mat-paginator [length]="resultsLength" [pageSize]="20" [pageSizeOptions]="[20, 50, 100, 200]" showFirstLastButtons>
</mat-paginator>
</ion-col>
</ion-row>
<mat-paginator *ngIf="showPaginator" [length]="resultsLength" [pageSize]="20" [pageSizeOptions]="[20, 50, 100, 200]" showFirstLastButtons>
</mat-paginator>
</ion-content>
<ion-footer hidden-xs hidden-sm hidden-mobile *ngIf="canEdit">
<ion-footer hidden-xs hidden-sm hidden-mobile *ngIf="canEdit && showFooter">
<form-buttons-bar (onCancel)="onRefresh.emit()" (onSave)="save()" [disabled]="(loadingSubject|async) || !dirty">
<!-- error -->
<ion-item *ngIf="error" lines="none">
......
import {ChangeDetectionStrategy, Component, Inject, Injector} from '@angular/core';
import {ChangeDetectionStrategy, Component, Inject, Injector, Input} from '@angular/core';
import {EntitiesTableDataSource, EnvironmentService, filterNotNil, isNil, isNilOrBlank, isNotNil, Referential, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS} from "sumaris-lib";
import {ReferentialFilter, ReferentialType} from "@app/referential/model/referential.model";
import {ActivatedRoute} from "@angular/router";
import {ReferentialValidatorService} from "@app/referential/validator/referential.validator";
import {ReferentialTable} from "@app/referential/table/referential.table";
import {ReferentialGenericService} from "@app/referential/generic/referential-generic.service";
import {ReferentialGenericService} from "@app/referential/generic/referential.generic.service";
import {BehaviorSubject, Subject} from "rxjs";
import {filter} from "rxjs/operators";
import {first} from "rxjs/internal/operators";
......@@ -28,6 +28,11 @@ export class ReferentialGenericTable extends ReferentialTable {
idLabel$ = new BehaviorSubject<string>(undefined);
title$ = new BehaviorSubject<string>('REFERENTIAL.LIST.TITLE');
// Default properties (shared with referential.generic.memory.table.ts)
canGoBack = true;
showPaginator = true;
showFooter = true;
constructor(
protected injector: Injector,
protected referentialService: ReferentialGenericService,
......@@ -128,7 +133,7 @@ export class ReferentialGenericTable extends ReferentialTable {
}
this.entityType$.next(entityType);
this.paginator.pageIndex = 0;
// this.paginator.pageIndex = 0;
this.filterForm.get('entityName').setValue(entityName);
// prevent save on setFilter
......@@ -156,14 +161,12 @@ export class ReferentialGenericTable extends ReferentialTable {
}
getI18nEntityName(entityName: string, self?: ReferentialGenericTable): string {
self = self || this;
getI18nEntityName(entityName: string): string {
if (isNil(entityName)) return undefined;
const tableName = entityName.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
const key = `REFERENTIAL.ENTITY.${tableName}`;
let message = self.injector.get(TranslateService).instant(key);
let message = this.injector.get(TranslateService).instant(key);
if (message !== key) return message;
// No I18n translation: continue
......
<link rel="stylesheet" href="referential-memory.table.scss">
<mat-toolbar>
<button mat-icon-button
*ngIf="canEdit && !selection.hasValue() && enabled" [title]="'COMMON.BTN_ADD'|translate" (click)="addRow()"
hidden-xs hidden-sm hidden-mobile>
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button small color="light" *ngIf="canEdit && selection.hasValue() && enabled" [title]="'COMMON.BTN_DELETE'|translate"
(click)="deleteSelection($event)">
<mat-icon>delete</mat-icon>
</button>
<!-- refresh (debug only) -->
<button mat-icon-button small color="light" *ngIf="debug && !selection.hasValue()" [title]="'COMMON.BTN_REFRESH'|translate"
(click)="onRefresh.emit()">
<mat-icon>refresh</mat-icon>
</button>
<ion-item *ngIf="error" hidden-xs hidden-sm hidden-mobile lines="none">
<ion-icon color="danger" slot="start" name="alert"></ion-icon>
<ion-label color="danger" [innerHTML]="error|translate"></ion-label>
</ion-item>
<div class="toolbar-spacer"></div>
<button mat-icon-button [title]="'COMMON.DISPLAYED_COLUMNS'|translate" (click)="openSelectColumnsModal($event)">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<!-- error -->
<ion-item *ngIf="error" visible-xs visible-sm visible-mobile lines="none">
<ion-icon color="danger" slot="start" name="alert"></ion-icon>
<ion-label color="danger" [innerHTML]="error|translate"></ion-label>
</ion-item>
<mat-table [dataSource]="dataSource" matSort matSortActive="id" matSortDirection="asc"
matSortDisableClear [trackBy]="trackByFn" >
<ng-container matColumnDef="select">
<mat-header-cell class="hidden-xs hidden-sm" *matHeaderCellDef>
<mat-checkbox (change)="$event ? masterToggle() : null" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</mat-header-cell>
<mat-cell class="hidden-xs hidden-sm" *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</mat-cell>
</ng-container>
<!-- Id column -->
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header>
<app-loading-spinner [loading]="loadingSubject|async">
<ion-label>#</ion-label>
</app-loading-spinner>
</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.currentData?.id }}</mat-cell>
</ng-container>
<!-- Label column -->
<ng-container matColumnDef="label">
<mat-header-cell *matHeaderCellDef mat-sort-header >
<span translate>REFERENTIAL.LABEL</span>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-form-field floatLabel="never">
<input matInput [formControl]="row.validator.controls['label']" [placeholder]="'REFERENTIAL.LABEL'|translate"
[appAutofocus]="row.id == -1 && row.editing" [readonly]="!row.editing">
<mat-error *ngIf="row.validator.controls['label'].hasError('required')" translate>ERROR.FIELD_REQUIRED</mat-error>
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Name column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef >
<span translate>REFERENTIAL.NAME</span>
</mat-header-cell>
<mat-cell *matCellDef="let row" [class.mat-form-field-disabled]="!row.editing">
<mat-form-field floatLabel="never">
<input matInput [formControl]="row.validator.controls['name']" [placeholder]="'REFERENTIAL.NAME'|translate"
[readonly]="!row.editing">
<mat-error *ngIf="row.validator.controls['name'].hasError('required')" translate>ERROR.FIELD_REQUIRED</mat-error>
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Description column -->
<ng-container matColumnDef="description">
<mat-header-cell *matHeaderCellDef>
<span translate>REFERENTIAL.DESCRIPTION</span>
</mat-header-cell>
<mat-cell *matCellDef="let row" [class.mat-form-field-disabled]="!row.editing">
<mat-form-field floatLabel="never">
<input matInput [formControl]="row.validator.controls.description" [placeholder]="'REFERENTIAL.DESCRIPTION'|translate"
[readonly]="!row.editing">
<mat-error *ngIf="row.validator.controls.description.hasError('required')" translate>ERROR.FIELD_REQUIRED</mat-error>
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Status column -->
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header>
<span translate>REFERENTIAL.STATUS</span>
</mat-header-cell>
<mat-cell *matCellDef="let row" [class.mat-form-field-disabled]="!row.editing">
<mat-form-field floatLabel="never">
<ion-icon matPrefix *ngIf="row.validator.controls.statusId.value >=0" [name]="statusById[row.validator.controls.statusId.value]?.icon"></ion-icon>
<mat-select [formControl]="row.validator.controls.statusId" [placeholder]="'REFERENTIAL.STATUS'|translate">
<mat-select-trigger>
<span *ngIf="row.validator.controls.statusId.value &gt;=0">
{{ statusById[row.validator.controls.statusId.value]?.label | translate}}</span>
</mat-select-trigger>
<mat-option *ngFor="let item of statusList" [value]="item.id">
<ion-icon [name]="item.icon"></ion-icon>
{{ item.label |translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="row.validator.controls.statusId.hasError('required')" translate>ERROR.FIELD_REQUIRED</mat-error>
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Comment column -->
<ng-container matColumnDef="comments">
<mat-header-cell *matHeaderCellDef class="hidden-xs hidden-sm">
<span translate>REFERENTIAL.COMMENTS</span>
</mat-header-cell>
<mat-cell *matCellDef="let row" class="hidden-xs hidden-sm" [class.mat-form-field-disabled]="!row.editing">
<mat-form-field floatLabel="never" *ngIf="row.editing; else iconComment">
<input matInput [formControl]="row.validator.controls.comments" [placeholder]="'REFERENTIAL.COMMENTS'|translate"
[readonly]="!row.editing">
</mat-form-field>
<ng-template #iconComment>
<ion-icon color="medium" name="chatbox" *ngIf="row.validator.controls.comments.value" slot="icon-only" [title]="row.validator.controls.comments.value"></ion-icon>
</ng-template>
</mat-cell>
</ng-container>
<!-- Actions buttons column -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef [hidden]="!inlineEdition">
</mat-header-cell>
<mat-cell *matCellDef="let row" [hidden]="!inlineEdition">
<ng-container *ngIf="row.validator">
<!-- undo or delete -->
<button mat-icon-button small color="light" *ngIf="row.validator.invalid"
[title]="(row.id !== -1 ? 'COMMON.BTN_UNDO': 'COMMON.BTN_DELETE')|translate"
(click)="cancelOrDelete($event, row)">
<mat-icon *ngIf="row.id !== -1">undo</mat-icon>
<mat-icon *ngIf="row.id === -1">delete_outline</mat-icon>
</button>
<!-- validate -->
<button mat-icon-button small color="light" *ngIf="row.validator.valid && row.id !== -1"
[title]="'COMMON.BTN_VALIDATE'|translate"
(click)="confirmEditCreate($event, row)">
<mat-icon>check</mat-icon>
</button>
<!-- add -->
<button mat-icon-button small color="light" *ngIf="row.validator.valid && row.id === -1"
[title]="'COMMON.BTN_ADD'|translate"
(click)="confirmAndAddRow($event, row)">
<mat-icon>add</mat-icon>
</button>
</ng-container>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;" [class.mat-row-error]="row.validator.invalid"
[class.mat-row-dirty]="row.validator.dirty" [class.mat-row-disabled]="!row.editing" (click)="clickRow($event, row)"></mat-row>
</mat-table>
<!-- Add Button
<ion-fab vertical="bottom" horizontal="end"
*ngIf="enabled"
visible-xs visible-sm visible-mobile>
<ion-fab-button color="tertiary" (click)="addRow()">
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab> -->
.mat-table {
.mat-column-label {
min-width: 50px;
max-width: 150px;
}
.mat-column-status {
max-width: 110px;
}
.mat-column-comments {
max-width: 110px;
}
mat-header-cell.hidden,
mat-cell.hidden {
visibility: hidden;
display: none;
}
}
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Injector, Input} from "@angular/core";
import {TableElement, ValidatorService} from "@e-is/ngx-material-table";
import {ActivatedRoute, Router} from "@angular/router";
import {ModalController, Platform} from "@ionic/angular";