Commit dd8237c2 authored by PECQUOT's avatar PECQUOT

[enh] add own AuthGuardService that use quadrige auth form

[enh] referential.table.ts: url is updated after a filter to allow permalink
[enh] referential.table.ts: columns position saved after move
[enh] make resizable directive active by property
parent 35d35558
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {AuthGuardService, SHARED_ROUTE_OPTIONS, SharedRoutingModule} from "sumaris-lib";
import {SHARED_ROUTE_OPTIONS, SharedRoutingModule} from "sumaris-lib";
import {HomePage} from "@app/core/home/home";
import {AuthGuardService} from "@app/core/services/auth-guard.service";
const routes: Routes = [
// Core path
......
import {Inject, Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
import {Observable} from 'rxjs';
import {ModalController} from "@ionic/angular";
import {AuthModal} from "../auth/modal/modal-auth";
import {AccountService, EnvironmentService} from "sumaris-lib";
@Injectable({providedIn: 'root'})
export class AuthGuardService implements CanActivate {
private readonly _debug: boolean;
constructor(private accountService: AccountService,
private modalCtrl: ModalController,
private router: Router,
@Inject(EnvironmentService) protected environment
) {
this._debug = !environment.production;
}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean | UrlTree> | boolean | UrlTree {
// If account not started: loop after started
if (!this.accountService.started) {
return this.accountService.ready()
// Iterate
.then(() => this.canActivate(next, state) as Promise<boolean | UrlTree>);
}
// Force login
if (!this.accountService.isLogin()) {
if (this._debug) console.debug("[auth-gard] Need authentication for page /" + next.url.join('/'));
return this.login(next)
.then(res => {
if (!res) {
if (this._debug) console.debug("[auth-gard] Authentication cancelled. Could not access to /" + next.url.join('/'));
return this.router.parseUrl('/home');
}
// Iterate
return this.canActivate(next, state) as Promise<boolean | UrlTree>;
});
}
if (next.data && next.data.profile && !this.accountService.hasMinProfile(next.data.profile)) {
if (this._debug) console.debug("[auth-gard] Not authorized access to /" + next.url.join('/') + ". Missing required profile: " + next.data.profile);
return false;
}
if (this._debug) console.debug("[auth-gard] Authorized access to /" + next.url.join('/'));
return true;
}
login(next?: ActivatedRouteSnapshot): Promise<boolean> {
return new Promise<boolean>(async (resolve) => {
const modal = await this.modalCtrl.create({component: AuthModal, componentProps: {next: next}});
modal.onDidDismiss()
.then(() => {
if (this.accountService.isLogin()) {
resolve(true);
return;
}
resolve(false);
});
return modal.present();
});
}
}
......@@ -10,7 +10,6 @@ const routes: Routes = [
path: '',
pathMatch: 'full',
component: SamplingEquipmentTable,
// canActivate: [AuthGuardService], useful ?
canDeactivate: [ComponentDirtyGuard],
data: {
profile: 'ADMIN'
......
......@@ -133,27 +133,19 @@
<!-- Id/code column -->
<ng-container matColumnDef="id" sticky>
<th mat-header-cell *matHeaderCellDef>
<th mat-header-cell *matHeaderCellDef [resizable]="true">
<app-loading-spinner [loading]="loadingSubject|async">
<span mat-sort-header><ion-label translate>REFERENTIAL.ID</ion-label></span>
</app-loading-spinner>
</th>
<td mat-cell *matCellDef="let row">
<mat-form-field *ngIf="idEditable; else readOnlyId" floatLabel="never">
<input matInput [formControl]="row.validator.controls['id']" [placeholder]="idLabel$|async|translate"
[appAutofocus]="row.id == -1 && row.editing"
[readonly]="!row.editing">
<mat-error *ngIf="row.validator.controls['id'].hasError('required')" translate>ERROR.FIELD_REQUIRED</mat-error>
</mat-form-field>
<ng-template #readOnlyId>
<ion-label>{{ row.currentData?.id }}</ion-label>
</ng-template>
<ion-label>{{ row.currentData?.id }}</ion-label>
</td>
</ng-container>
<!-- Name column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.NAME</ion-label></span>
</th>
<td mat-cell *matCellDef="let row" [class.mat-form-field-disabled]="!row.editing">
......@@ -168,7 +160,7 @@
<!-- Label column -->
<ng-container matColumnDef="label">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.LABEL</ion-label></span>
</th>
<td mat-cell *matCellDef="let row">
......@@ -182,7 +174,7 @@
<!-- Description column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.DESCRIPTION</ion-label></span>
</th>
<td mat-cell *matCellDef="let row">
......@@ -196,7 +188,7 @@
<!-- Size column -->
<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.SAMPLING_EQUIPMENT.SIZE</ion-label></span>
</th>
<td mat-cell *matCellDef="let row">
......@@ -210,7 +202,7 @@
<!-- Unit column -->
<ng-container matColumnDef="unit">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.SAMPLING_EQUIPMENT.UNIT</ion-label></span>
</th>
<td mat-cell *matCellDef="let row">
......@@ -282,28 +274,30 @@
<!-- Actions buttons column -->
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef [hidden]="!inlineEdition">
<th mat-header-cell *matHeaderCellDef>
<button mat-icon-button [title]=" 'COMMON.DISPLAYED_COLUMNS'|translate" (click)="openSelectColumnsModal($event)">
<mat-icon>more_vert</mat-icon>
</button>
</th>
<td mat-cell *matCellDef="let row" [hidden]="!inlineEdition">
<td mat-cell *matCellDef="let row">
<!-- undo or delete -->
<button mat-icon-button color="light"
*ngIf="row.validator.invalid"
*ngIf="inlineEdition && 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 color="light" *ngIf="row.validator.valid && row.id !== -1"
<button mat-icon-button color="light"
*ngIf="inlineEdition && 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 color="light" *ngIf="row.validator.valid && row.id === -1"
<button mat-icon-button color="light"
*ngIf="inlineEdition && row.validator.valid && row.id === -1"
[title]="'COMMON.BTN_ADD'|translate"
(click)="confirmAndAddRow($event, row)">
<mat-icon>add</mat-icon>
......
......@@ -5,7 +5,6 @@ import {SamplingEquipment, SamplingEquipmentFilter} from "@app/referential/sampl
import {ActivatedRoute} from "@angular/router";
import {SamplingEquipmentService} from "@app/referential/sampling-equipment/sampling-equipment.service";
import {SamplingEquipmentValidator} from "@app/referential/sampling-equipment/sampling-equipment.validator";
import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop";
import {ReferentialGenericService} from "@app/referential/services/referential-generic.service";
@Component({
......@@ -16,6 +15,8 @@ import {ReferentialGenericService} from "@app/referential/services/referential-g
})
export class SamplingEquipmentTable extends ReferentialTable<SamplingEquipment, SamplingEquipmentValidator, SamplingEquipmentFilter> {
searchFields: string;
constructor(
protected injector: Injector,
protected samplingEquipmentService: SamplingEquipmentService,
......@@ -48,10 +49,6 @@ export class SamplingEquipmentTable extends ReferentialTable<SamplingEquipment,
);
this.autoLoad = false;
this.filterForm = this.formBuilder.group({
'searchText': [null],
'statusId': [null]
});
}
ngOnInit() {
......
......@@ -55,7 +55,7 @@
<input matInput
formControlName="searchText"
autocomplete="off"
[placeholder]="'REFERENTIAL.LIST.FILTER.SEARCH_TEXT'|translate: {fields: searchFields}">
[placeholder]="'REFERENTIAL.LIST.FILTER.SEARCH_TEXT'|translate: {fields: searchFields$|async}">
<button mat-icon-button matSuffix tabindex="-1"
type="button"
(click)="clearControlValue($event, filterForm.controls.searchText)"
......@@ -132,14 +132,14 @@
</ng-container>
<!-- Id/code column -->
<ng-container matColumnDef="id" sticky>
<th mat-header-cell *matHeaderCellDef>
<ng-container matColumnDef="id" sticky >
<th mat-header-cell *matHeaderCellDef [resizable]="true" [class.code]="idIsString$|async">
<app-loading-spinner [loading]="loadingSubject|async">
<span mat-sort-header><ion-label translate>{{idLabel$|async}}</ion-label></span>
</app-loading-spinner>
</th>
<td mat-cell *matCellDef="let row">
<mat-form-field *ngIf="idEditable; else readOnlyId" floatLabel="never">
<mat-form-field *ngIf="idIsString$|async; else readOnlyId" floatLabel="never">
<input matInput [formControl]="row.validator.controls['id']" [placeholder]="idLabel$|async|translate"
[appAutofocus]="row.id == -1 && row.editing"
[readonly]="!row.editing">
......@@ -153,7 +153,7 @@
<!-- Name column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.NAME</ion-label></span>
</th>
<td mat-cell *matCellDef="let row" [class.mat-form-field-disabled]="!row.editing">
......@@ -168,7 +168,7 @@
<!-- Label column -->
<ng-container matColumnDef="label">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.LABEL</ion-label></span>
</th>
<td mat-cell *matCellDef="let row">
......@@ -182,7 +182,7 @@
<!-- Description column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef cdkDrag resizable>
<th mat-header-cell *matHeaderCellDef cdkDrag [resizable]="true">
<span mat-sort-header><ion-label translate>REFERENTIAL.DESCRIPTION</ion-label></span>
</th>
<td mat-cell *matCellDef="let row">
......@@ -253,28 +253,30 @@
<!-- Actions buttons column -->
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef [hidden]="!inlineEdition">
<th mat-header-cell *matHeaderCellDef >
<button mat-icon-button [title]=" 'COMMON.DISPLAYED_COLUMNS'|translate" (click)="openSelectColumnsModal($event)">
<mat-icon>more_vert</mat-icon>
</button>
</th>
<td mat-cell *matCellDef="let row" [hidden]="!inlineEdition">
<td mat-cell *matCellDef="let row">
<!-- undo or delete -->
<button mat-icon-button color="light"
*ngIf="row.validator.invalid"
*ngIf="inlineEdition && 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 color="light" *ngIf="row.validator.valid && row.id !== -1"
<button mat-icon-button color="light"
*ngIf="inlineEdition && 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 color="light" *ngIf="row.validator.valid && row.id === -1"
<button mat-icon-button color="light"
*ngIf="inlineEdition && row.validator.valid && row.id === -1"
[title]="'COMMON.BTN_ADD'|translate"
(click)="confirmAndAddRow($event, row)">
<mat-icon>add</mat-icon>
......
import {ChangeDetectionStrategy, Component, Inject, Injector} from '@angular/core';
import {EntitiesTableDataSource, EnvironmentService, isNil, isNotNil, Referential, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS} from "sumaris-lib";
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 {CanLeave} from "@app/shared/table/component-dirty.guard";
import {ReferentialTable} from "@app/referential/table/referential.table";
import {ReferentialGenericService} from "@app/referential/services/referential-generic.service";
import {BehaviorSubject} from "rxjs";
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";
@Component({
selector: 'app-referential-generic-table',
......@@ -22,7 +23,10 @@ export class ReferentialGenericTable extends ReferentialTable {
entityName: string;
entityTypes$ = new BehaviorSubject<ReferentialType[]>(undefined);
entityType: ReferentialType;
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,
......@@ -53,24 +57,13 @@ export class ReferentialGenericTable extends ReferentialTable {
validatorService
);
this.filterForm = this.formBuilder.group({
'entityName': [null],
'searchText': [null],
'statusId': [null]
});
// add entityName in filter form
this.filterForm.addControl('entityName', this.formBuilder.control(null, Validators.required));
// Listen route parameters
this.registerSubscription(
this.route.params
.subscribe(({entity, q, status}) => {
if (entity) {
// this.filterForm.setValue({
// searchText: q || null,
// statusId: isNotNil(status) ? +status : null
// });
this.setEntityName(entity, /*{skipLocationChange: true}*/);
}
})
.subscribe(({entity}) => this.setEntityName(entity, /*{skipLocationChange: true}*/))
);
}
......@@ -83,13 +76,35 @@ export class ReferentialGenericTable extends ReferentialTable {
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/*; skipLocationChange?: boolean*/ }) {
opts = opts || {emitEvent: true, /*skipLocationChange: false*/};
// 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, /*skipLocationChange: false*/};
// this.canOpenDetail = false;
......@@ -107,18 +122,12 @@ export class ReferentialGenericTable extends ReferentialTable {
this.settingsId = this.generateTableId();
this.entityName = entityName;
this.entityType = entityTypes.find(e => e.name === entityName);
if (!this.entityType) {
const entityType = entityTypes.find(e => e.name === entityName);
if (!entityType) {
throw new Error(`[referential] Entity type for {${entityName}} not found !`);
}
await this.updateColumns();
this.entityType$.next(entityType);
this.idEditable = this.entityType.idIsString;
this.idLabel$.next(this.getI18nEntityIdType());
this.title$.next(this.getI18nEntityName(entityName));
this.searchFields = this.getFilterSearchFields();
// this.$entity.next(entity);
this.paginator.pageIndex = 0;
this.filterForm.get('entityName').setValue(entityName);
......@@ -137,11 +146,11 @@ export class ReferentialGenericTable extends ReferentialTable {
}
async updateColumns() {
this.setShowColumn('label', this.entityType.labelPresent);
this.setShowColumn('description', this.entityType.descriptionPresent);
this.setShowColumn('comment', this.entityType.commentPresent);
this.setShowColumn('creationDate', this.entityType.creationDatePresent);
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();
}
......@@ -168,20 +177,26 @@ export class ReferentialGenericTable extends ReferentialTable {
return message;
}
getI18nEntityIdType(): string {
return (this.entityType && this.entityType.idIsString) ? "REFERENTIAL.CODE" : "REFERENTIAL.ID";
getI18nEntityIdType(entityType: ReferentialType): string {
return (entityType.idIsString) ? "REFERENTIAL.CODE" : "REFERENTIAL.ID";
}
getFilterSearchFields(): string {
const i18nFields = [this.getI18nEntityIdType()];
if (this.entityType && this.entityType.labelPresent)
updateFilterSearchFields(entityType: ReferentialType) {
const i18nFields = [this.getI18nEntityIdType(entityType)];
if (entityType.labelPresent)
i18nFields.push("REFERENTIAL.LABEL");
i18nFields.push("REFERENTIAL.NAME");
return i18nFields.map(value => this.translate.instant(value)).join(', ');
this.searchFields$.next(
i18nFields.map(value => this.translate.instant(value)).join(', ')
);
}
protected onStartEditRow(row: TableElement<Referential>) {
row.validator = this.validatorService.getRowValidator(row.currentData, {type: this.entityType});
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 {
......
<
import {ChangeDetectorRef, Directive, Injector} from '@angular/core';
import {AccountService, AppTable, AppValidatorService, DefaultStatusList, EntitiesTableDataSource, isNotNilOrBlank, LocalSettingsService, Referential, RESERVED_START_COLUMNS} from "sumaris-lib";
import {
AccountService,
AppTable,
AppValidatorService,
DefaultStatusList,
EntitiesTableDataSource,
isNotNil,
isNotNilOrBlank,
LocalSettingsService,
Referential,
RESERVED_START_COLUMNS,
SETTINGS_DISPLAY_COLUMNS
} from "sumaris-lib";
import {BaseReferentialFilter, ReferentialFilter} from "@app/referential/model/referential.model";
import {ActivatedRoute, Router} from "@angular/router";
import {ModalController, Platform, PopoverController} from "@ionic/angular";
......@@ -14,6 +26,8 @@ import {CanLeave} from "@app/shared/table/component-dirty.guard";
import {CommentModal} from "@app/shared/comment/comment.modal";
import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop";
const SEARCH_QUERY_PARAM = 'search';
const STATUS_QUERY_PARAM = 'status';
@Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix
......@@ -22,23 +36,21 @@ export abstract class ReferentialTable<E extends Referential<E> = Referential,
F extends BaseReferentialFilter = ReferentialFilter>
extends AppTable<E, F> implements CanLeave {
title$ = new BehaviorSubject<string>('REFERENTIAL.LIST.TITLE');
idLabel$ = new BehaviorSubject<string>(undefined);
canEdit = false;
idEditable = false;
statusList = DefaultStatusList;
statusById: any;
filterForm: FormGroup;
filterIsEmpty = true;
searchFields: string;
searchFields$ = new BehaviorSubject<string>(undefined);
protected popoverController: PopoverController;
protected accountService: AccountService;
protected formBuilder: FormBuilder;
protected translate: TranslateService;
protected cd: ChangeDetectorRef;
protected location: Location;
protected constructor(
protected injector: Injector,
......@@ -56,6 +68,7 @@ export abstract class ReferentialTable<E extends Referential<E> = Referential,
this.formBuilder = injector.get(FormBuilder);
this.translate = injector.get(TranslateService);
this.cd = injector.get(ChangeDetectorRef);
this.location = injector.get(Location);
this.allowRowDetail = false;
this.confirmBeforeDelete = true;
......@@ -66,6 +79,11 @@ export abstract class ReferentialTable<E extends Referential<E> = Referential,
this.i18nColumnPrefix = 'REFERENTIAL.';
// build default filter form
this.filterForm = this.formBuilder.group({
'searchText': [null],
'statusId': [null]
});
// Fill statusById
this.statusById = {};
......@@ -102,6 +120,17 @@ export abstract class ReferentialTable<E extends Referential<E> = Referential,
super.ngOnInit();
// Apply filter from query params
this.registerSubscription(
this.route.queryParams
.subscribe(value => {
this.filterForm.patchValue({
searchText: value[SEARCH_QUERY_PARAM],
statusId: isNotNil(value[STATUS_QUERY_PARAM]) ? +value[STATUS_QUERY_PARAM] : null
});
})
);
// Update filter when changes
this.registerSubscription(
this.filterForm.valueChanges
......@@ -128,14 +157,26 @@ export abstract class ReferentialTable<E extends Referential<E> = Referential,
}
protected onStartEditRow(row: TableElement<Referential>) {
}
submitFilter() {
if (this.filterForm.valid)
if (this.filterForm.valid) {
this.setFilter(this.filterForm.value);
this.updateUrl();
}
}
// update current url query parameters
updateUrl() {
const newParams = {};
if (isNotNilOrBlank(this.filterForm.value.searchText))
newParams[SEARCH_QUERY_PARAM] = this.filterForm.value.searchText;
if (isNotNil(this.filterForm.value.statusId))
newParams[STATUS_QUERY_PARAM] = this.filterForm.value.statusId;