Commit 8bec6bcd authored by François Agneray's avatar François Agneray
Browse files

Merge branch '58-manage-attributes-by-dataset' into 'develop'

Resolve "Manage attributes by dataset"

Closes #58

See merge request !38
parents 90ea14bf b27328f3
Pipeline #5231 passed with stages
in 11 minutes and 26 seconds
......@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #55: Add image renderer configuration
### Changed
- #57: GUI improvments
- #52: Update dependencies (Angular v11, ngrx v11, ...)
- #56: Cone-search configuration improvement
......
......@@ -2,12 +2,14 @@
<table class="table table-bordered">
<thead>
<tr>
<th style="min-width:50px">ID</th>
<th style="min-width:150px">Name</th>
<th style="min-width:150px">Search flag</th>
<th style="min-width:150px">Label</th>
<th style="min-width:150px">FormLabel</th>
<th style="min-width:150px">Description</th>
<th style="width:50px">Save</th>
<th style="width:50px">Delete</th>
</tr>
</thead>
<tbody>
......
<td>
<input type="text" class="form-control" name="name" [formControl]="designForm.controls.name">
<input type="number" class="form-control" name="id" [formControl]="designForm.controls.id" required>
</td>
<td>
<input type="text" class="form-control" name="name" [formControl]="designForm.controls.name" required>
</td>
<td>
<select class="form-control" name="search_flag" [formControl]="designForm.controls.search_flag">
......@@ -21,3 +24,22 @@
<i class="fas fa-save"></i>
</button>
</td>
<td class="text-center align-middle">
<button title="Delete this attribute" (click)="openModal(template)" class="btn btn-outline-danger">
<i class="fas fa-trash-alt"></i>
</button>
</td>
<ng-template #template>
<div class="modal-header">
<h4 class="modal-title pull-left">Confirm</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to delete attribute with id <strong>{{ _attribute.id }}</strong> : <strong>{{ _attribute.name }}</strong> ?</p>
<p>
<button (click)="modalRef.hide()" class="btn btn-outline-primary">No</button>
&nbsp;
<button (click)="confirmDel()" class="btn btn-outline-danger">Yes</button>
</p>
</div>
</ng-template>
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, TemplateRef } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
import { SettingsSelectOption } from '../../../../settings/store/model';
import { Attribute } from '../../../store/model';
......@@ -13,6 +16,7 @@ import { Attribute } from '../../../store/model';
export class TrDesignComponent {
@Input() set attribute(attribute: Attribute) {
this._attribute = attribute;
this.designForm.controls.id.setValue(attribute.id);
this.designForm.controls.name.setValue(attribute.name);
this.designForm.controls.search_flag.setValue(attribute.search_flag);
this.designForm.controls.label.setValue(attribute.label);
......@@ -21,20 +25,35 @@ export class TrDesignComponent {
}
@Input() public searchFlags: SettingsSelectOption[];
@Output() public save: EventEmitter<Attribute> = new EventEmitter();
@Output() public delete: EventEmitter<Attribute> = new EventEmitter();
_attribute: Attribute;
designForm = new FormGroup({
name: new FormControl({value: '', disabled: true}),
id: new FormControl({value: '', disabled: true}),
name: new FormControl(),
search_flag: new FormControl(),
label: new FormControl(),
form_label: new FormControl(),
description: new FormControl()
});
modalRef: BsModalRef;
constructor(private modalService: BsModalService) { }
emitSave(): void {
this.save.emit({
...this._attribute,
...this.designForm.value
})
}
openModal(template: TemplateRef<any>) {
this.modalRef = this.modalService.show(template);
}
confirmDel() {
this.delete.emit(this._attribute);
this.modalRef.hide();
}
}
<div class="card">
<div class="btn-toolbar mb-3" role="toolbar" aria-label="Toolbar with button groups">
<div class="btn-group mr-2" role="group" aria-label="First group">
<button (click)="openModal(templateForNew)" title="Add new attribute" class="btn btn-outline-success">
<span class="fas fa-plus"></span> New attribute
</button>
</div>
</div>
<div *ngIf="attributeList.length < 1" class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle"></i> You must add at least one attribute to use this dataset
</div>
<div *ngIf="attributeList.length > 0" class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
......@@ -29,8 +41,12 @@
</div>
<div class="card-body" [ngSwitch]="tabSelected">
<app-table-design *ngSwitchCase="'design'">
<tr *ngFor="let attribute of attributeList" design [attribute]="attribute"
[searchFlags]="getSettingsSelectOptions('search_flag')" (save)="editAttribute.emit($event)">
<tr *ngFor="let attribute of attributeList"
design
[attribute]="attribute"
[searchFlags]="getSettingsSelectOptions('search_flag')"
(save)="editAttribute.emit($event)"
(delete)="deleteAttribute.emit($event)">
</tr>
</app-table-design>
<app-table-criteria *ngSwitchCase="'criteria'">
......@@ -42,6 +58,7 @@
[searchTypeList]="getSettingsSelectOptions('search_type')"
[operatorList]="getSettingsSelectOptions('operator')"
(save)="editAttribute.emit($event)"
(delete)="deleteAttribute($event)"
(generateOptionList)="generateAttributeOptionList.emit($event)">
</tr>
</app-table-criteria>
......@@ -84,3 +101,35 @@
</app-output-family-list>
</div>
</div>
<ng-template #templateForNew>
<div class="modal-header">
<h4 class="modal-title pull-left"><strong>Available columns</strong></h4>
</div>
<div class="modal-body">
<table class="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let column of columnList">
<td>{{ column.name }}</td>
<td>{{ column.type }}</td>
<td>
<span *ngIf="alreadyExists(column.name)" class="badge badge-secondary">Already exists</span>
<button *ngIf="!alreadyExists(column.name)" (click)="addNewAttribute(column)" class="btn btn-outline-primary">Add</button>
</td>
</tr>
</tbody>
</table>
<p>
<button (click)="modalRef.hide()" class="btn btn-outline-primary">
Close
</button>
</p>
</div>
</ng-template>
\ No newline at end of file
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, TemplateRef } from '@angular/core';
import { BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
import { SettingsSelect, SettingsSelectOption } from '../../../settings/store/model';
import { Attribute, CriteriaFamily, OutputCategory, OutputFamily } from '../../store/model';
import { Attribute, Column, CriteriaFamily, OutputCategory, OutputFamily } from '../../store/model';
@Component({
selector: 'app-form-attribute-list',
......@@ -11,6 +14,7 @@ import { Attribute, CriteriaFamily, OutputCategory, OutputFamily } from '../../s
})
export class FormAttributeListComponent {
@Input() attributeList: Attribute[];
@Input() columnList: Column[];
@Input() optionListGenerated: string[];
@Input() criteriaFamilyList: CriteriaFamily[];
@Input() outputFamilyList: OutputFamily[];
......@@ -27,9 +31,43 @@ export class FormAttributeListComponent {
@Output() addOutputCategory: EventEmitter<OutputCategory> = new EventEmitter();
@Output() editOutputCategory: EventEmitter<OutputCategory> = new EventEmitter();
@Output() deleteOutputCategory: EventEmitter<OutputCategory> = new EventEmitter();
@Output() addAttribute: EventEmitter<Attribute> = new EventEmitter();
@Output() editAttribute: EventEmitter<Attribute> = new EventEmitter();
@Output() deleteAttribute: EventEmitter<Attribute> = new EventEmitter();
@Output() generateAttributeOptionList: EventEmitter<Attribute> = new EventEmitter();
modalRef: BsModalRef;
constructor(private modalService: BsModalService) { }
openModal(template: TemplateRef<any>) {
this.modalRef = this.modalService.show(template);
}
alreadyExists(columnName: string): boolean {
return this.attributeList.map(a => a.name).includes(columnName);
}
addNewAttribute(column: Column) {
let id = 1;
if (this.attributeList.length > 0) {
id = Math.max(...this.attributeList.map(a => a.id)) + 1;
}
this.addAttribute.emit({
id,
name: column.name,
label: column.name,
form_label: column.name,
type: column.type,
criteria_display: id * 10,
output_display: id * 10,
display_detail: id * 10,
order_display: id * 10,
selected: true
})
}
getSettingsSelectOptions(settingsSelectName: string): SettingsSelectOption[] {
const settingsSelect = this.settingsSelectList.find(o => o.name === settingsSelectName);
if (!settingsSelect) {
......
......@@ -17,7 +17,9 @@
<div *ngIf="(attributeListIsLoaded | async)">
<div class="row mt-1">
<div class="col-12">
<app-form-attribute-list [attributeList]="attributeList | async"
<app-form-attribute-list
[attributeList]="attributeList | async"
[columnList]="columnList | async"
[optionListGenerated]="optionListGenerated | async"
[criteriaFamilyList]="criteriaFamilyList | async"
[outputFamilyList]="outputFamilyList | async"
......@@ -31,7 +33,9 @@
(addOutputFamily)="addOutputFamily($event)"
(editOutputFamily)="editOutputFamily($event)"
(deleteOutputFamily)="deleteOutputFamily($event)"
(addAttribute)="addAttribute($event)"
(editAttribute)="editAttribute($event)"
(deleteAttribute)="deleteAttribute($event)"
(addOutputCategory)="addOutputCategory($event)"
(editOutputCategory)="editOutputCategory($event)"
(deleteOutputCategory)="deleteOutputCategory($event)"
......
......@@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import { Dataset, Attribute, CriteriaFamily, OutputCategory, OutputFamily } from '../../store/model';
import { Dataset, Attribute, Column, CriteriaFamily, OutputCategory, OutputFamily } from '../../store/model';
import { SettingsSelect, SettingsSelectOption } from '../../../settings/store/model';
import * as fromMetamodel from '../../store/reducer';
import * as fromSettings from '../../../settings/store/reducer';
......@@ -22,6 +22,8 @@ import * as selectActions from '../../../settings/store/action/select.action';
import * as selectSelector from '../../../settings/store/selector/select.selector';
import * as optionActions from '../../../settings/store/action/option.action';
import * as optionSelector from '../../../settings/store/selector/option.selector';
import * as databaseActions from '../../store/action/database.action';
import * as databaseSelector from '../../store/selector/database.selector';
@Component({
selector: 'app-attribute',
......@@ -38,6 +40,9 @@ export class AttributeComponent implements OnInit {
public attributeList: Observable<Attribute[]>;
public attributeListIsLoading: Observable<boolean>;
public attributeListIsLoaded: Observable<boolean>;
public columnList: Observable<Column[]>;
public columnListIsLoading: Observable<boolean>;
public columnListIsLoaded: Observable<boolean>;
public optionListGenerated: Observable<string[]>;
public criteriaFamilyList: Observable<CriteriaFamily[]>;
public outputFamilyList: Observable<OutputFamily[]>;
......@@ -55,6 +60,9 @@ export class AttributeComponent implements OnInit {
this.attributeList = store.select(attributeSelector.getAttributeList);
this.attributeListIsLoading = store.select(attributeSelector.getAttributeListIsLoading);
this.attributeListIsLoaded = store.select(attributeSelector.getAttributeListIsLoaded);
this.columnList = store.select(databaseSelector.getColumnList);
this.columnListIsLoading = store.select(databaseSelector.getColumnListIsLoading);
this.columnListIsLoaded = store.select(databaseSelector.getColumnListIsLoaded);
this.optionListGenerated = store.select(attributeSelector.getOptionListGenerated);
this.criteriaFamilyList = store.select(criteriaFamilySelector.getCriteriaFamilyList);
this.outputFamilyList = store.select(outputFamilySelector.getOutputFamilyList);
......@@ -71,6 +79,7 @@ export class AttributeComponent implements OnInit {
this.store.dispatch(new criteriaFamilyActions.LoadCriteriaFamilyListAction());
this.store.dispatch(new outputFamilyActions.LoadOutputFamilyListAction());
this.store.dispatch(new outputCategoryActions.LoadOutputCategoryListAction());
this.store.dispatch(new databaseActions.LoadColumnListAction());
}
addCriteriaFamily(criteriaFamily: CriteriaFamily) {
......@@ -109,10 +118,18 @@ export class AttributeComponent implements OnInit {
this.store.dispatch(new outputCategoryActions.DeleteOutputCategoryAction(outputCategory));
}
addAttribute(attribute: Attribute) {
this.store.dispatch(new attributeActions.AddNewAttributeAction(attribute));
}
editAttribute(attribute: Attribute) {
this.store.dispatch(new attributeActions.EditAttributeAction(attribute));
}
deleteAttribute(attribute: Attribute) {
this.store.dispatch(new attributeActions.DeleteAttributeAction(attribute));
}
generateAttributeOptionList(attribute: Attribute) {
this.store.dispatch(new attributeActions.GenerateOptionListAction(attribute));
}
......
......@@ -5,9 +5,15 @@ import { Attribute } from '../model';
export const LOAD_ATTRIBUTE_LIST = '[Attribute] Load Attribute List';
export const LOAD_ATTRIBUTE_LIST_SUCCESS = '[Attribute] Load Attribute List Sucess';
export const LOAD_ATTRIBUTE_LIST_FAIL = '[Attribute] Load Attribute List Fail';
export const ADD_NEW_ATTRIBUTE = '[Attribute] Add New Attribute';
export const ADD_NEW_ATTRIBUTE_SUCCESS = '[Attribute] Add New Attribute Success';
export const ADD_NEW_ATTRIBUTE_FAIL = '[Attribute] Add New Attribute Fail';
export const EDIT_ATTRIBUTE = '[Attribute] Edit Attribute';
export const EDIT_ATTRIBUTE_SUCCESS = '[Attribute] Edit Attribute Success';
export const EDIT_ATTRIBUTE_FAIL = '[Attribute] Edit Attribute Fail';
export const DELETE_ATTRIBUTE = '[Attribute] Delete Attribute';
export const DELETE_ATTRIBUTE_SUCCESS = '[Attribute] Delete Attribute Success';
export const DELETE_ATTRIBUTE_FAIL = '[Attribute] Delete Attribute Fail';
export const GENERATE_OPTION_LIST = '[Attribute] Generate Option List';
export const GENERATE_OPTION_LIST_SUCCESS = '[Attribute] Generate Option List Success';
export const GENERATE_OPTION_LIST_FAIL = '[Attribute] Generate Option List Fail';
......@@ -30,6 +36,24 @@ export class LoadAttributeListFailAction implements Action {
constructor(public payload: {} = null) { }
}
export class AddNewAttributeAction implements Action {
type = ADD_NEW_ATTRIBUTE;
constructor(public payload: Attribute) { }
}
export class AddNewAttributeSuccessAction implements Action {
type = ADD_NEW_ATTRIBUTE_SUCCESS;
constructor(public payload: Attribute) { }
}
export class AddNewAttributeFailAction implements Action {
type = ADD_NEW_ATTRIBUTE_FAIL;
constructor(public payload: {} = null) { }
}
export class EditAttributeAction implements Action {
type = EDIT_ATTRIBUTE;
......@@ -48,6 +72,24 @@ export class EditAttributeFailAction implements Action {
constructor(public payload: {} = null) { }
}
export class DeleteAttributeAction implements Action {
type = DELETE_ATTRIBUTE;
constructor(public payload: Attribute) { }
}
export class DeleteAttributeSuccessAction implements Action {
type = DELETE_ATTRIBUTE_SUCCESS;
constructor(public payload: Attribute) { }
}
export class DeleteAttributeFailAction implements Action {
type = DELETE_ATTRIBUTE_FAIL;
constructor(public payload: {} = null) { }
}
export class GenerateOptionListAction implements Action {
type = GENERATE_OPTION_LIST;
......@@ -70,9 +112,15 @@ export type Actions
= LoadAttributeListAction
| LoadAttributeListSuccessAction
| LoadAttributeListFailAction
| AddNewAttributeAction
| AddNewAttributeSuccessAction
| AddNewAttributeFailAction
| EditAttributeAction
| EditAttributeSuccessAction
| EditAttributeFailAction
| DeleteAttributeAction
| DeleteAttributeSuccessAction
| DeleteAttributeFailAction
| GenerateOptionListAction
| GenerateOptionListSuccessAction
| GenerateOptionListFailAction;
import { Action } from '@ngrx/store';
import { Database } from '../model';
import { Database, Column } from '../model';
export const LOAD_DATABASE_LIST = '[Database] Load Database List';
export const LOAD_DATABASE_LIST_WIP = '[Database] Load Database List WIP';
export const LOAD_DATABASE_LIST_SUCCESS = '[Database] Load Database List Success';
export const LOAD_DATABASE_LIST_FAIL = '[Database] Load Database List Fail';
export const LOAD_TABLE_LIST = 'Load Table List';
export const LOAD_TABLE_LIST_SUCCESS = 'Load Table List Success';
export const LOAD_TABLE_LIST_FAIL = 'Load Table List Fail';
export const LOAD_TABLE_LIST = '[Database] Load Table List';
export const LOAD_TABLE_LIST_SUCCESS = '[Database] Load Table List Success';
export const LOAD_TABLE_LIST_FAIL = '[Database] Load Table List Fail';
export const LOAD_COLUMN_LIST = '[Database] Load Column List';
export const LOAD_COLUMN_LIST_SUCCESS = '[Database] Load Column List Success';
export const LOAD_COLUMN_LIST_FAIL = '[Database] Load Column List Fail';
export const ADD_NEW_DATABASE = '[Database] Add New Database';
export const ADD_NEW_DATABASE_SUCCESS = '[Database] Add New Database Success';
export const ADD_NEW_DATABASE_FAIL = '[Database] Add New Database Fail';
......@@ -61,6 +64,24 @@ export class LoadTableListFailAction implements Action {
constructor(public payload: {} = null) { }
}
export class LoadColumnListAction implements Action {
type = LOAD_COLUMN_LIST;
constructor(public payload: {} = null) { }
}
export class LoadColumnListSuccessAction implements Action {
type = LOAD_COLUMN_LIST_SUCCESS;
constructor(public payload: Column[]) { }
}
export class LoadColumnListFailAction implements Action {
type = LOAD_COLUMN_LIST_FAIL;
constructor(public payload: {} = null) { }
}
export class AddNewDatabaseAction implements Action {
type = ADD_NEW_DATABASE;
......@@ -123,6 +144,9 @@ export type Actions
| LoadTableListAction
| LoadTableListSuccessAction
| LoadTableListFailAction
| LoadColumnListAction
| LoadColumnListSuccessAction
| LoadColumnListFailAction
| AddNewDatabaseAction
| AddNewDatabaseSuccessAction
| AddNewDatabaseFailAction
......
......@@ -61,6 +61,34 @@ export class AttributeEffects {
map(_ => this.toastr.error('Loading Failed!', 'Generate option list failed'))
);
@Effect()
addNewAttributeAction$ = this.actions$.pipe(
ofType(attributeActions.ADD_NEW_ATTRIBUTE),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
const datasetName = state.router.state.params.dname;
const addNewAttributeAction = action as attributeActions.AddNewAttributeAction;
return this.attributeService.addAttribute(datasetName, addNewAttributeAction.payload).pipe(
map((attribute: Attribute) => new attributeActions.AddNewAttributeSuccessAction(attribute)),
catchError(() => of(new attributeActions.AddNewAttributeFailAction()))
)
})
);
@Effect({dispatch: false})
addNewAttributeSuccessAction$ = this.actions$.pipe(
ofType(attributeActions.ADD_NEW_ATTRIBUTE_SUCCESS),
map(action => {
this.toastr.success('Add attribute success!', 'The new attribute has been created!');
})
);
@Effect({dispatch: false})
addNewAttributeFailedAction$ = this.actions$.pipe(
ofType(attributeActions.ADD_NEW_ATTRIBUTE_FAIL),
map(_ => this.toastr.error('Add attribute failed!', 'The new attribute could not be created into the database'))
);
@Effect()
editAttributeAction$ = this.actions$.pipe(
ofType(attributeActions.EDIT_ATTRIBUTE),
......@@ -88,4 +116,32 @@ export class AttributeEffects {
ofType(attributeActions.EDIT_ATTRIBUTE_FAIL),
map(_ => this.toastr.error('Edit attribute failed!', 'The existing entities could not be edited into the database'))
);
@Effect()
deleteAttributeAction$ = this.actions$.pipe(
ofType(attributeActions.DELETE_ATTRIBUTE),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
const datasetName = state.router.state.params.dname;
const deleteAttributeAction = action as attributeActions.DeleteAttributeAction;
return this.attributeService.deleteAttribute(datasetName, deleteAttributeAction.payload).pipe(
map(_ => new attributeActions.DeleteAttributeSuccessAction(deleteAttributeAction.payload)),
catchError(() => of(new attributeActions.DeleteAttributeFailAction()))
)
})
);
@Effect({dispatch: false})
deleteDatabaseSuccessAction$ = this.actions$.pipe(
ofType(attributeActions.DELETE_ATTRIBUTE_SUCCESS),
map(_ => {
this.toastr.success('Delete attribute success!', 'The attribute has been deleted!');
})
);
@Effect({dispatch: false})
deleteAttributeFailedAction$ = this.actions$.pipe(
ofType(attributeActions.DELETE_ATTRIBUTE_FAIL),
map(_ => this.toastr.error('Delete attribute failed!', 'The attribute could not be deleted into the database'))
);
}
......@@ -8,7 +8,8 @@ import { of } from 'rxjs';
import { withLatestFrom, switchMap, map, catchError, tap } from 'rxjs/operators';
import * as fromMetamodel from '../reducer';
import { Database } from '../model';
import * as fromRouter from '../../../shared/utils';
import { Database, Column } from '../model';
import * as databaseActions from '../action/database.action';
import { DatabaseService } from '../service/database.service';
......@@ -16,7 +17,7 @@ import { DatabaseService } from '../service/database.service';
export class DatabaseEffects {
constructor(
private actions$: Actions,
private store$: Store<{metamodel: fromMetamodel.State}>,
private store$: Store<{router: fromRouter.RouterReducerState, metamodel: fromMetamodel.State}>,
private databaseService: DatabaseService,
private router: Router,
private toastr: ToastrService
......@@ -70,6 +71,25 @@ export class DatabaseEffects {
map(_ => this.toastr.error('Loading Failed!', 'Table list loading failed'))
);
@Effect()
loadColumnListAction$ = this.actions$.pipe(
ofType(databaseActions.LOAD_COLUMN_LIST),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
const datasetName = state.router.state.params.dname;
return this.databaseService.retrieveColumns(datasetName).pipe(
map((columnList: Column[]) => new databaseActions.LoadColumnListSuccessAction(columnList)),
catchError(() => of(new databaseActions.LoadColumnListFailAction()))
)
})
);