diff --git a/client/src/app/admin/admin-routing.module.ts b/client/src/app/admin/admin-routing.module.ts index 4cb1a0f134f9cf1d320bbf544970c4576a8b1898..7c82c2aa53e8153c63b9947e4481d51b39a65108 100644 --- a/client/src/app/admin/admin-routing.module.ts +++ b/client/src/app/admin/admin-routing.module.ts @@ -19,6 +19,7 @@ import { GroupListComponent } from './containers/group/group-list.component'; import { NewGroupComponent } from './containers/group/new-group.component'; import { EditGroupComponent } from './containers/group/edit-group.component'; import { NewDatasetComponent } from './containers/dataset/new-dataset.component'; +import { AttributeListComponent } from './containers/attribute/attribute-list.component'; import { SurveyListComponent } from './containers/survey/survey-list.component'; import { NewSurveyComponent } from './containers/survey/new-survey.component'; import { EditSurveyComponent } from './containers/survey/edit-survey.component'; @@ -39,6 +40,7 @@ const routes: Routes = [ { path: 'configure-instance/:iname/new-group', component: NewGroupComponent }, { path: 'configure-instance/:iname/edit-group/:id', component: EditGroupComponent }, { path: 'configure-instance/:iname/new-dataset', component: NewDatasetComponent }, + { path: 'configure-instance/:iname/configure-dataset/:dname', component: AttributeListComponent }, { path: 'survey-list', component: SurveyListComponent }, { path: 'new-survey', component: NewSurveyComponent }, { path: 'edit-survey/:name', component: EditSurveyComponent }, @@ -67,6 +69,7 @@ export const routedComponents = [ NewGroupComponent, EditGroupComponent, NewDatasetComponent, + AttributeListComponent, SurveyListComponent, NewSurveyComponent, EditSurveyComponent, diff --git a/client/src/app/admin/components/attribute/add-attribute.component.html b/client/src/app/admin/components/attribute/add-attribute.component.html new file mode 100644 index 0000000000000000000000000000000000000000..ea32a9c40ef2200c96dd4adbfe78dae5e00554cf --- /dev/null +++ b/client/src/app/admin/components/attribute/add-attribute.component.html @@ -0,0 +1,41 @@ +<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(template)" title="Add new attribute" class="btn btn-outline-success"> + <span class="fas fa-plus"></span> New attribute + </button> + </div> +</div> + +<ng-template #template> + <div class="modal-header"> + <h4 class="modal-title pull-left"><strong>Available columns</strong></h4> + </div> + <div class="modal-body"> + <app-spinner *ngIf="columnListIsLoading"></app-spinner> + + <table *ngIf="columnListIsLoaded" 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> diff --git a/client/src/app/admin/components/attribute/add-attribute.component.ts b/client/src/app/admin/components/attribute/add-attribute.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7934d61f05212f998fcdcf66c10556de52e0046 --- /dev/null +++ b/client/src/app/admin/components/attribute/add-attribute.component.ts @@ -0,0 +1,53 @@ +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 { Column, Attribute } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-add-attribute', + templateUrl: 'add-attribute.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AddAttributeComponent { + @Input() columnList: Column[]; + @Input() columnListIsLoading: boolean; + @Input() columnListIsLoaded: boolean; + @Input() attributeList: Attribute[]; + @Output() loadColumnList: EventEmitter<{}> = new EventEmitter(); + @Output() add: EventEmitter<Attribute> = new EventEmitter(); + + modalRef: BsModalRef; + + constructor(private modalService: BsModalService) { } + + openModal(template: TemplateRef<any>) { + this.loadColumnList.emit(); + 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.add.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 + }) + } +} diff --git a/client/src/app/admin/components/attribute/criteria/generate-option-list.component.html b/client/src/app/admin/components/attribute/criteria/generate-option-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c84d0ff801abcb5698068e4b3ca1d9fa8f6b2de3 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/generate-option-list.component.html @@ -0,0 +1,24 @@ +<div class="text-center mt-2"> + <button (click)="openModal(template); $event.stopPropagation()" class="btn btn-outline-primary">Generate values</button> +</div> + +<ng-template #template> + <div class="modal-header"> + <h4 class="modal-title pull-left">Attribute option list generated</h4> + </div> + <div class="modal-body"> + <app-spinner *ngIf="attributeDistinctListIsLoading"></app-spinner> + + <ng-container *ngIf="attributeDistinctListIsLoaded"> + <ul> + <li *ngFor="let attributeDistinct of attributeDistinctList">{{ attributeDistinct }}</li> + </ul> + <p>Are you sure you want to generate option list with this attribute distinct list ?</p> + <p> + <button (click)="modalRef.hide()" class="btn btn-outline-danger">No</button> + + <button (click)="generate()" class="btn btn-outline-primary">Yes</button> + </p> + </ng-container> + </div> +</ng-template> diff --git a/client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts b/client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b6905a2bbcd1f55707332bf561aa085ea78005d --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts @@ -0,0 +1,31 @@ +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'; + +@Component({ + selector: 'app-generate-option-list', + templateUrl: 'generate-option-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class GenerateOptionListComponent { + @Input() attributeDistinctList: string[]; + @Input() attributeDistinctListIsLoading: boolean; + @Input() attributeDistinctListIsLoaded: boolean; + @Output() loadAttributeDistinctList: EventEmitter<{}> = new EventEmitter(); + @Output() generateOptionList: EventEmitter<string[]> = new EventEmitter(); + + modalRef: BsModalRef; + + constructor(private modalService: BsModalService) { } + + openModal(template: TemplateRef<any>) { + this.modalRef = this.modalService.show(template); + this.loadAttributeDistinctList.emit(); + } + + generate() { + this.generateOptionList.emit(this.attributeDistinctList); + this.modalRef.hide(); + } +} diff --git a/client/src/app/admin/components/attribute/criteria/index.ts b/client/src/app/admin/components/attribute/criteria/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f6d2258a0ea7507ebd5834eb91b3c0c389b36b2 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/index.ts @@ -0,0 +1,13 @@ +import { TableCriteriaComponent } from './table-criteria.component'; +import { TrCriteriaComponent } from './tr-criteria.component'; +import { OptionListComponent } from './option-list.component'; +import { OptionFormComponent } from './option-form.component'; +import { GenerateOptionListComponent } from './generate-option-list.component'; + +export const criteriaComponents = [ + TableCriteriaComponent, + TrCriteriaComponent, + OptionListComponent, + OptionFormComponent, + GenerateOptionListComponent +]; diff --git a/client/src/app/admin/components/attribute/criteria/option-form.component.html b/client/src/app/admin/components/attribute/criteria/option-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..10ee6368c150b0c9d47c65ecff312500786551fa --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/option-form.component.html @@ -0,0 +1,8 @@ +<form [formGroup]="form" novalidate> + <div class="values"> + <input type="text" class="form-control" name="label" placeholder="Label" formControlName="label"> + <input type="text" class="form-control" name="value" placeholder="Value" formControlName="value"> + <input type="number" class="form-control" name="display" placeholder="Display" formControlName="display"> + <ng-content></ng-content> + </div> +</form> diff --git a/client/src/app/admin/components/attribute/criteria/option-form.component.scss b/client/src/app/admin/components/attribute/criteria/option-form.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..541a82b99ddb9c78d8f9a1343ead1ac64e9163d4 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/option-form.component.scss @@ -0,0 +1,4 @@ +.values input { + display: inline-block; + width: 26%; +} diff --git a/client/src/app/admin/components/attribute/criteria/option-form.component.ts b/client/src/app/admin/components/attribute/criteria/option-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e2a620d0a6985db4690cf84108b4e59b85d2ee2 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/option-form.component.ts @@ -0,0 +1,12 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-option-form', + templateUrl: 'option-form.component.html', + styleUrls: [ 'option-form.component.scss' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OptionFormComponent { + @Input() form: FormGroup; +} diff --git a/client/src/app/admin/components/attribute/criteria/option-list.component.html b/client/src/app/admin/components/attribute/criteria/option-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..58dc55509f86600fdec76521f922e652ca72e38c --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/option-list.component.html @@ -0,0 +1,20 @@ +<app-option-form *ngFor="let option of form.controls; index as i" [form]="getOptionFormGroup(option)"> + <button (click)="removeOption(i)" class="btn btn-default"> + <i class="fas fa-trash-alt"></i> + </button> +</app-option-form> + +<app-option-form [form]="newOptionFormGroup" #f> + <button (click)="addOption()" [disabled]="!f.form.valid || f.form.pristine" class="btn btn-default"> + <i class="fas fa-plus"></i> + </button> +</app-option-form> + +<app-generate-option-list + *ngIf="form.controls.length === 0" + [attributeDistinctList]="attributeDistinctList" + [attributeDistinctListIsLoading]="attributeDistinctListIsLoading" + [attributeDistinctListIsLoaded]="attributeDistinctListIsLoaded" + (loadAttributeDistinctList)="loadAttributeDistinctList.emit()" + (generateOptionList)="generateOptionList($event)"> +</app-generate-option-list> \ No newline at end of file diff --git a/client/src/app/admin/components/attribute/criteria/option-list.component.ts b/client/src/app/admin/components/attribute/criteria/option-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..02e3d2054935bfc018c153bc1bab7ba48a206553 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/option-list.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core'; +import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms'; + +import { Option } from 'src/app/metamodel/models'; + +@Component({ + selector: 'app-option-list', + templateUrl: 'option-list.component.html' +}) +export class OptionListComponent implements OnInit { + @Input() form: FormArray; + @Input() optionList: Option[]; + @Input() attributeDistinctList: string[]; + @Input() attributeDistinctListIsLoading: boolean; + @Input() attributeDistinctListIsLoaded: boolean; + @Output() loadAttributeDistinctList: EventEmitter<{}> = new EventEmitter(); + + newOptionFormGroup: FormGroup; + + ngOnInit() { + if (this.optionList && this.optionList.length > 0) { + for (const option of this.optionList) { + const optionForm = this.buildFormGroup(); + optionForm.patchValue(option); + this.form.push(optionForm); + } + } + this.newOptionFormGroup = this.buildFormGroup(); + } + + buildFormGroup() { + return new FormGroup({ + label: new FormControl('', [Validators.required]), + value: new FormControl('', [Validators.required]), + display: new FormControl('', [Validators.required]), + }); + } + + getOptionFormGroup(option) { + return option as FormGroup; + } + + addOption() { + this.form.push(this.newOptionFormGroup); + this.newOptionFormGroup = this.buildFormGroup(); + this.form.markAsDirty(); + } + + removeOption(index: number) { + this.form.removeAt(index); + this.form.markAsDirty(); + } + + generateOptionList(attributeDistinctList: string[]) { + for (let i = 0; i < attributeDistinctList.length; i++) { + const optionForm = this.buildFormGroup(); + optionForm.patchValue({ + label: attributeDistinctList[i], + value: attributeDistinctList[i], + display: (i + 1) * 10 + }); + this.form.push(optionForm); + } + } +} diff --git a/client/src/app/admin/components/attribute/criteria/table-criteria.component.html b/client/src/app/admin/components/attribute/criteria/table-criteria.component.html new file mode 100644 index 0000000000000000000000000000000000000000..89cad34c1ef92b1c870e9a7c5d1827baba841bda --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/table-criteria.component.html @@ -0,0 +1,20 @@ +<div class="table-responsive"> + <table class="table table-bordered"> + <thead> + <tr> + <th style="min-width:150px">Name</th> + <th style="min-width:110px">Type</th> + <th style="min-width:150px">Criteria family</th> + <th style="min-width:150px">Search Type</th> + <th style="width:50px">Operator</th> + <th style="min-width:350px">Default Value(s)</th> + <th style="min-width:150px">Placeholder</th> + <th style="min-width:100px;width:100px;">Display</th> + <th style="width:50px">Save</th> + </tr> + </thead> + <tbody> + <ng-content></ng-content> + </tbody> + </table> +</div> diff --git a/client/src/app/admin/components/attribute/criteria/table-criteria.component.ts b/client/src/app/admin/components/attribute/criteria/table-criteria.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe5c7db6f47a4db658929ac124354620ee5286f4 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/table-criteria.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-table-criteria', + templateUrl: 'table-criteria.component.html' +}) +export class TableCriteriaComponent { } diff --git a/client/src/app/admin/components/attribute/criteria/tr-criteria.component.html b/client/src/app/admin/components/attribute/criteria/tr-criteria.component.html new file mode 100644 index 0000000000000000000000000000000000000000..60a63b3507183345265e83b6591f73d4ad63cf23 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/tr-criteria.component.html @@ -0,0 +1,109 @@ +<ng-container [formGroup]="form"> + <td> + <input type="text" class="form-control" name="name" formControlName="name"> + </td> + <td> + <input type="text" class="form-control" name="type" formControlName="type"> + </td> + <td> + <select class="form-control" + name="id_criteria_family" + formControlName="id_criteria_family" + (change)="criteriaFamilyOnChange()"> + <option></option> + <option *ngFor="let family of criteriaFamilyList" [ngValue]="family.id">{{family.label}}</option> + </select> + </td> + <td> + <select *ngIf="form.controls.id_criteria_family.value" + class="form-control" + name="search_type" + formControlName="search_type" + (change)="searchTypeOnChange()"> + <option></option> + <option *ngFor="let type of searchTypeList" [ngValue]="type.value">{{type.label}}</option> + </select> + </td> + <td> + <select *ngIf="form.controls.search_type.value + && form.controls.search_type.value != 'between' + && form.controls.search_type.value != 'between-date' + && form.controls.search_type.value != 'json' + && form.controls.search_type.value != 'list'" + class="form-control" + name="operator" + formControlName="operator" + (change)="operatorOnChange()"> + <option></option> + <option *ngFor="let operator of operatorList" [ngValue]="operator.value">{{ operator.label }}</option> + </select> + </td> + <td> + <app-option-list *ngIf="form.controls.operator.value + && (form.controls.search_type.value == 'datalist' + || form.controls.search_type.value == 'radio' + || form.controls.search_type.value == 'checkbox' + || form.controls.search_type.value == 'select' + || form.controls.search_type.value == 'select-multiple')" + [form]="optionsFormArray" + [optionList]="attribute.options" + [attributeDistinctList]="attributeDistinctList" + [attributeDistinctListIsLoading]="attributeDistinctListIsLoading" + [attributeDistinctListIsLoaded]="attributeDistinctListIsLoaded" + (loadAttributeDistinctList)="loadAttributeDistinctList.emit()"> + </app-option-list> + <input *ngIf="(form.controls.operator.value && form.controls.search_type.value == 'field') + || (form.controls.operator.value && form.controls.search_type.value == 'date') + || (form.controls.operator.value && form.controls.search_type.value == 'time') + || (form.controls.operator.value && form.controls.search_type.value == 'date-time') + || form.controls.search_type.value == 'between' + || form.controls.search_type.value == 'between-date' + || form.controls.search_type.value == 'list'" + type="text" + class="form-control" + name="min" + [placeholder]="getMinValuePlaceholder(form.controls.search_type.value)" + formControlName="min"> + <input *ngIf="form.controls.search_type.value == 'between' + || form.controls.search_type.value == 'between-date'" + type="text" + class="form-control" + name="max" + placeholder="Default max value (optional)" + formControlName="max"> + </td> + <td> + <input *ngIf="(form.controls.operator.value && form.controls.search_type.value == 'field') + || (form.controls.operator.value && form.controls.search_type.value == 'date') + || (form.controls.operator.value && form.controls.search_type.value == 'time') + || (form.controls.operator.value && form.controls.search_type.value == 'date-time') + || form.controls.search_type.value == 'between' + || form.controls.search_type.value == 'between-date' + || form.controls.search_type.value == 'list'" + type="text" + class="form-control" + name="placeholder_min" + [placeholder]="getMinValuePlaceholder(form.controls.search_type.value)" + formControlName="placeholder_min"> + <input *ngIf="form.controls.search_type.value == 'between' || form.controls.search_type.value == 'between-date'" + type="text" class="form-control" + name="placeholder_max" + placeholder="Default max value (optional)" + formControlName="placeholder_max"> + </td> + <td> + <input *ngIf="form.controls.operator.value + || form.controls.search_type.value == 'between' + || form.controls.search_type.value == 'between-date' + || form.controls.search_type.value == 'json'" + type="number" + class="form-control" + name="criteria_display" + formControlName="criteria_display"> + </td> + <td class="text-center align-middle"> + <button (click)="submit()" [disabled]="form.invalid || form.pristine" class="btn btn-outline-primary"> + <i class="fas fa-save"></i> + </button> + </td> +</ng-container> diff --git a/client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts b/client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..87109e23f306817016211fcf8c23f33ee25ba656 --- /dev/null +++ b/client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts @@ -0,0 +1,84 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; + +import { Attribute, Option, CriteriaFamily, SelectOption } from 'src/app/metamodel/models'; + +@Component({ + selector: '[criteria]', + templateUrl: 'tr-criteria.component.html', + styleUrls: [ '../tr.component.css' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TrCriteriaComponent implements OnInit { + @Input() attribute: Attribute; + @Input() criteriaFamilyList: CriteriaFamily[]; + @Input() searchTypeList: SelectOption[]; + @Input() operatorList: SelectOption[]; + @Input() attributeDistinctList: string[]; + @Input() attributeDistinctListIsLoading: boolean; + @Input() attributeDistinctListIsLoaded: boolean; + @Output() save: EventEmitter<Attribute> = new EventEmitter(); + @Output() loadAttributeDistinctList: EventEmitter<{}> = new EventEmitter(); + + optionsFormArray = new FormArray([]); + + public form = new FormGroup({ + name: new FormControl({ value: '', disabled: true }), + type: new FormControl({ value: '', disabled: true }), + id_criteria_family: new FormControl(), + search_type: new FormControl(), + operator: new FormControl(), + min: new FormControl(), + max: new FormControl(), + options: this.optionsFormArray, + placeholder_min: new FormControl(), + placeholder_max: new FormControl(), + criteria_display: new FormControl() + }); + + ngOnInit() { + if (this.attribute) { + this.form.patchValue(this.attribute); + } + } + + criteriaFamilyOnChange(): void { + if (this.form.controls.id_criteria_family.value === '') { + this.form.controls.id_criteria_family.setValue(null); + this.searchTypeOnChange(); + } + } + + searchTypeOnChange(): void { + if (this.form.controls.search_type.value === '') { + this.form.controls.search_type.setValue(null); + this.operatorOnChange(); + } + } + + operatorOnChange(): void { + if (this.form.controls.operator.value === '') { + this.form.controls.operator.setValue(null); + this.form.controls.min.setValue(null); + this.form.controls.max.setValue(null); + this.form.controls.placeholder_min.setValue(null); + this.form.controls.placeholder_max.setValue(null); + this.optionsFormArray.clear(); + } + } + + getMinValuePlaceholder(searchType: string): string { + if (searchType === 'between' || searchType === 'between-date') { + return 'Default min value (optional)'; + } else { + return 'Default value (optional)'; + } + } + + submit(): void { + this.save.emit({ + ...this.attribute, + ...this.form.value + }) + } +} diff --git a/client/src/app/admin/components/attribute/design/index.ts b/client/src/app/admin/components/attribute/design/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..97b51ae6380e1ab54108323c607878012b8500c0 --- /dev/null +++ b/client/src/app/admin/components/attribute/design/index.ts @@ -0,0 +1,7 @@ +import { TableDesignComponent } from './table-design.component'; +import { TrDesignComponent } from './tr-design.component'; + +export const designComponents = [ + TableDesignComponent, + TrDesignComponent +]; diff --git a/client/src/app/admin/components/attribute/design/table-design.component.html b/client/src/app/admin/components/attribute/design/table-design.component.html new file mode 100644 index 0000000000000000000000000000000000000000..faee164dacf0b62396d980305de47e5f9955070d --- /dev/null +++ b/client/src/app/admin/components/attribute/design/table-design.component.html @@ -0,0 +1,19 @@ +<div class="table-responsive"> + <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> + <ng-content></ng-content> + </tbody> + </table> +</div> diff --git a/client/src/app/admin/components/attribute/design/table-design.component.ts b/client/src/app/admin/components/attribute/design/table-design.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ece9403ec5149fcac9cddc7211434f8cbbb49ee1 --- /dev/null +++ b/client/src/app/admin/components/attribute/design/table-design.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-table-design', + templateUrl: 'table-design.component.html' +}) +export class TableDesignComponent { } diff --git a/client/src/app/admin/components/attribute/design/tr-design.component.html b/client/src/app/admin/components/attribute/design/tr-design.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6591f5dc759e24ce97f2d193f65e57f4acc523c3 --- /dev/null +++ b/client/src/app/admin/components/attribute/design/tr-design.component.html @@ -0,0 +1,35 @@ +<ng-container [formGroup]="form"> + <td> + <input type="number" class="form-control" name="id" formControlName="id"> + </td> + <td> + <input type="text" class="form-control" name="name" formControlName="name"> + </td> + <td> + <select class="form-control" name="search_flag" formControlName="search_flag"> + <option></option> + <option *ngFor="let flag of searchFlags" [ngValue]="flag.value">{{flag.label}}</option> + </select> + </td> + <td> + <input type="text" class="form-control" name="label" formControlName="label"> + </td> + <td> + <input type="text" class="form-control" name="form_label" formControlName="form_label"> + </td> + <td> + <input type="text" class="form-control" name="description" formControlName="description"> + </td> + <td class="text-center align-middle"> + <button (click)="submit()" [disabled]="form.invalid || form.pristine" class="btn btn-outline-primary"> + <i class="fas fa-save"></i> + </button> + </td> + <td class="text-center align-middle"> + <app-delete-btn + [type]="'attribute'" + [label]="attribute.label" + (confirm)="delete.emit(attribute)"> + </app-delete-btn> + </td> +</ng-container> diff --git a/client/src/app/admin/components/attribute/design/tr-design.component.ts b/client/src/app/admin/components/attribute/design/tr-design.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..72d1dcedc79ed6c4bde11309d5be0cb3db93df8e --- /dev/null +++ b/client/src/app/admin/components/attribute/design/tr-design.component.ts @@ -0,0 +1,39 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +import { Attribute, SelectOption } from 'src/app/metamodel/models'; + +@Component({ + selector: '[design]', + templateUrl: 'tr-design.component.html', + styleUrls: [ '../tr.component.css' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TrDesignComponent implements OnInit { + @Input() attribute: Attribute + @Input() searchFlags: SelectOption[]; + @Output() save: EventEmitter<Attribute> = new EventEmitter(); + @Output() delete: EventEmitter<Attribute> = new EventEmitter(); + + public form = new FormGroup({ + id: new FormControl({value: '', disabled: true}), + name: new FormControl('', [Validators.required]), + search_flag: new FormControl(), + label: new FormControl('', [Validators.required]), + form_label: new FormControl('', [Validators.required]), + description: new FormControl() + }); + + ngOnInit() { + if (this.attribute) { + this.form.patchValue(this.attribute); + } + } + + submit(): void { + this.save.emit({ + ...this.attribute, + ...this.form.value + }); + } +} diff --git a/client/src/app/admin/components/attribute/index.ts b/client/src/app/admin/components/attribute/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cac2f891407ebd1eb874bfd1565e6ad56a1cc140 --- /dev/null +++ b/client/src/app/admin/components/attribute/index.ts @@ -0,0 +1,9 @@ +import { AddAttributeComponent } from './add-attribute.component'; +import { designComponents } from './design'; +import { criteriaComponents } from './criteria'; + +export const attributeComponents = [ + AddAttributeComponent, + designComponents, + criteriaComponents +]; diff --git a/client/src/app/admin/components/attribute/tr.component.css b/client/src/app/admin/components/attribute/tr.component.css new file mode 100644 index 0000000000000000000000000000000000000000..72949e30b558bc33c7bf02c59341e69651aab50d --- /dev/null +++ b/client/src/app/admin/components/attribute/tr.component.css @@ -0,0 +1,5 @@ +.btn-outline-primary:disabled { + color: #6c757d; + border-color: #6c757d; + cursor: not-allowed; +} diff --git a/client/src/app/admin/components/dataset/dataset-card.component.html b/client/src/app/admin/components/dataset/dataset-card.component.html index 356e7be9b3fd21bf7831bf2a6abed4576bb83ab5..0774b7e59a5857c7775c18bc1cc005eeb9ca3564 100644 --- a/client/src/app/admin/components/dataset/dataset-card.component.html +++ b/client/src/app/admin/components/dataset/dataset-card.component.html @@ -13,7 +13,7 @@ </p> </div> <div class="card-footer bg-transparent text-right"> - <a routerLink="configure-dataset/{{dataset.name}}" class="btn btn-outline-primary" title="Configure this dataset"> + <a routerLink="configure-dataset/{{dataset.name}}" [queryParams]="{tab_selected: 'design'}" class="btn btn-outline-primary" title="Configure this dataset"> <span class="fas fa-cog"></span> </a> diff --git a/client/src/app/admin/components/index.ts b/client/src/app/admin/components/index.ts index 36eebde7d9ef4c86720b293fff290b502df093cb..4aa69ec8fb818a896e4417177a2e0fcf3783175d 100644 --- a/client/src/app/admin/components/index.ts +++ b/client/src/app/admin/components/index.ts @@ -7,14 +7,15 @@ * file that was distributed with this source code. */ -import { databaseComponents } from "./database"; -import { datasetComponents } from "./dataset"; -import { datasetFamilyComponents } from "./dataset-family"; +import { databaseComponents } from './database'; +import { datasetComponents } from './dataset'; +import { datasetFamilyComponents } from './dataset-family'; import { groupComponents } from './group'; import { instanceComponents } from './instance'; import { settingsComponents } from './settings'; -import { sharedComponents } from "./shared"; -import { surveyComponents } from "./survey"; +import { sharedComponents } from './shared'; +import { surveyComponents } from './survey'; +import { attributeComponents } from './attribute'; export const dummiesComponents = [ databaseComponents, @@ -24,5 +25,6 @@ export const dummiesComponents = [ instanceComponents, settingsComponents, sharedComponents, - surveyComponents + surveyComponents, + attributeComponents ]; diff --git a/client/src/app/admin/containers/attribute/attribute-list.component.html b/client/src/app/admin/containers/attribute/attribute-list.component.html index f476ec3ad5cde560e6ef0ca194c606787ce4cfef..75b64590d3f236b1d6d1c53f1b05d6b4800ec8bc 100644 --- a/client/src/app/admin/containers/attribute/attribute-list.component.html +++ b/client/src/app/admin/containers/attribute/attribute-list.component.html @@ -2,10 +2,10 @@ <nav aria-label="breadcrumb"> <ol class="breadcrumb"> <li class="breadcrumb-item"> - <a routerLink="/instance-list">Instances</a> + <a routerLink="/admin/instance-list">Instances</a> </li> <li class="breadcrumb-item"> - <a routerLink="/configure-instance/{{ instanceSelected | async }}"> + <a routerLink="/admin/configure-instance/{{ instanceSelected | async }}"> Configure instance {{ instanceSelected | async }} </a> </li> @@ -19,31 +19,113 @@ <div *ngIf="(attributeListIsLoaded | async) && (datasetListIsLoaded | async)" class="row mt-1"> <div class="col-12"> - <app-form-attribute-list - [dataset]="dataset | async" - [attributeList]="attributeList | async" + <app-add-attribute [columnList]="columnList | async" - [optionListGenerated]="optionListGenerated | async" - [criteriaFamilyList]="criteriaFamilyList | async" - [outputFamilyList]="outputFamilyList | async" - [outputCategoryList]="outputCategoryList | async" - [settingsSelectList]="settingsSelectList | async" - [settingsSelectOptionList]="settingsSelectOptionList | async" - [tabSelected]="tabSelected | async" - (addCriteriaFamily)="addCriteriaFamily($event)" - (editCriteriaFamily)="editCriteriaFamily($event)" - (deleteCriteriaFamily)="deleteCriteriaFamily($event)" - (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)" - (generateAttributeOptionList)="generateAttributeOptionList($event)"> - </app-form-attribute-list> + [columnListIsLoading]="columnListIsLoading | async" + [columnListIsLoaded]="columnListIsLoaded | async" + [attributeList]="attributeList | async" + (loadColumnList)="loadColumnList()" + (add)="addAttribute($event)"> + </app-add-attribute> + </div> + + <div class="col-12"> + <div *ngIf="(attributeList | async).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 | async).length > 0" class="card"> + <div class="card-header"> + <ul class="nav nav-tabs card-header-tabs"> + <li class="nav-item"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'design'}" [ngClass]="{'active': (tabSelected | async) === 'design'}">Design</a> + </li> + <li class="nav-item"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'criteria'}" [ngClass]="{'active': (tabSelected | async) === 'criteria'}">Criteria</a> + </li> + <li class="nav-item"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'output'}" [ngClass]="{'active': (tabSelected | async) === 'output'}">Output</a> + </li> + <li class="nav-item"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'result'}" [ngClass]="{'active': (tabSelected | async) === 'result'}">Result</a> + </li> + <li class="nav-item"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'detail'}" [ngClass]="{'active': (tabSelected | async) === 'detail'}">Detail</a> + </li> + <li class="nav-item"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'vo'}" [ngClass]="{'disabled': !getVoEnabled(), 'active': (tabSelected | async) === 'vo'}">VO</a> + </li> + <li class="nav-item ml-auto"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'cfamilies'}" [ngClass]="{'active': (tabSelected | async) === 'cfamilies'}">Criteria Families</a> + </li> + <li class="nav-item"> + <a class="nav-link" routerLink="./" [queryParams]="{tab_selected: 'ofamilies'}" [ngClass]="{'active': (tabSelected | async) === 'ofamilies'}">Output Families</a> + </li> + </ul> + </div> + <div class="card-body" [ngSwitch]="tabSelected | async"> + <app-table-design *ngSwitchCase="'design'"> + <tr *ngFor="let attribute of (attributeList | async)" + design + [attribute]="attribute" + [searchFlags]="settingsSelectOptionList | async | optionListBySelect:'search_flag'" + (save)="editAttribute($event)" + (delete)="deleteAttribute($event)"> + </tr> + </app-table-design> + <app-table-criteria *ngSwitchCase="'criteria'"> + <tr *ngFor="let attribute of (attributeList | async)" + criteria + [attribute]="attribute" + [criteriaFamilyList]="criteriaFamilyList | async" + [searchTypeList]="settingsSelectOptionList | async | optionListBySelect:'search_type'" + [operatorList]="settingsSelectOptionList | async | optionListBySelect:'operator'" + [attributeDistinctList]="attributeDistinctList | async" + [attributeDistinctListIsLoading]="attributeDistinctListIsLoading | async" + [attributeDistinctListIsLoaded]="attributeDistinctListIsLoaded | async" + (loadAttributeDistinctList)="loadAttributeDistinctList(attribute)" + (save)="editAttribute($event)"> + </tr> + </app-table-criteria> + <!-- <app-table-output *ngSwitchCase="'output'"> + <tr *ngFor="let attribute of attributeList" output [attribute]="attribute" + [outputCategoryList]="outputCategoryList" (save)="editAttribute.emit($event)"> + </tr> + </app-table-output> + <app-table-result *ngSwitchCase="'result'"> + <tr *ngFor="let attribute of attributeList" result [attribute]="attribute" + [rendererList]="getSettingsSelectOptions('renderer')" (save)="editAttribute.emit($event)"> + </tr> + </app-table-result> + <app-table-detail *ngSwitchCase="'detail'"> + <tr *ngFor="let attribute of attributeList" detail [attribute]="attribute" + [rendererDetailList]="getSettingsSelectOptions('renderer_detail')" (save)="editAttribute.emit($event)"> + </tr> + </app-table-detail> + <app-table-vo *ngSwitchCase="'vo'"> + <tr *ngFor="let attribute of attributeList" vo [attribute]="attribute" (save)="editAttribute.emit($event)"> + </tr> + </app-table-vo> + <app-criteria-family-list + *ngSwitchCase="'cfamilies'" + [criteriaFamilyList]="criteriaFamilyList" + (addCriteriaFamily)="addCriteriaFamily.emit($event)" + (editCriteriaFamily)="editCriteriaFamily.emit($event)" + (deleteCriteriaFamily)="deleteCriteriaFamily.emit($event)"> + </app-criteria-family-list> + <app-output-family-list + *ngSwitchCase="'ofamilies'" + [outputFamilyList]="outputFamilyList" + [outputCategoryList]="outputCategoryList" + (addOutputFamily)="addOutputFamily.emit($event)" + (editOutputFamily)="editOutputFamily.emit($event)" + (deleteOutputFamily)="deleteOutputFamily.emit($event)" + (addOutputCategory)="addOutputCategory.emit($event)" + (editOutputCategory)="editOutputCategory.emit($event)" + (deleteOutputCategory)="deleteOutputCategory.emit($event)"> + </app-output-family-list> --> + </div> + </div> </div> </div> </div> diff --git a/client/src/app/admin/containers/attribute/attribute-list.component.ts b/client/src/app/admin/containers/attribute/attribute-list.component.ts index 1254ca6e84e9d14a2dc022e3ad9f2b5594ec05aa..efd9fa76ea02b951157d6b899e0a841169409289 100644 --- a/client/src/app/admin/containers/attribute/attribute-list.component.ts +++ b/client/src/app/admin/containers/attribute/attribute-list.component.ts @@ -32,48 +32,54 @@ import * as optionActions from 'src/app/metamodel/actions/select-option.actions' import * as optionSelector from 'src/app/metamodel/selectors/select-option.selector'; import * as columnActions from 'src/app/metamodel/actions/column.actions'; import * as columnSelector from 'src/app/metamodel/selectors/column.selector'; +import * as attributeDistinctActions from 'src/app/metamodel/actions/attribute-distinct.actions'; +import * as attributeDistinctSelector from 'src/app/metamodel/selectors/attribute-distinct.selector'; @Component({ - selector: 'app-attribute', + selector: 'app-attribute-list', templateUrl: 'attribute-list.component.html', styleUrls: [ 'attribute-list.component.scss' ] }) -export class AttributeComponent implements OnInit { - public instanceName: Observable<string>; - public datasetName: Observable<string>; +export class AttributeListComponent implements OnInit { + public instanceSelected: Observable<string>; + public datasetSelected: Observable<string>; + public tabSelected: Observable<string>; public dataset: Observable<Dataset>; public datasetListIsLoading: Observable<boolean>; public datasetListIsLoaded: Observable<boolean>; - public tabSelected: Observable<string>; 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[]>; public outputCategoryList: Observable<OutputCategory[]>; + public columnList: Observable<Column[]>; + public columnListIsLoading: Observable<boolean>; + public columnListIsLoaded: Observable<boolean>; + public attributeDistinctList: Observable<string[]>; + public attributeDistinctListIsLoading: Observable<boolean>; + public attributeDistinctListIsLoaded: Observable<boolean>; public settingsSelectList: Observable<Select[]>; public settingsSelectOptionList: Observable<SelectOption[]>; constructor(private store: Store<{ }>, private route: ActivatedRoute) { - this.instanceName = store.select(instanceSelector.selectInstanceNameByRoute); - this.datasetName = store.select(datasetSelector.selectDatasetNameByRoute); + this.instanceSelected = store.select(instanceSelector.selectInstanceNameByRoute); + this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute); this.dataset = store.select(datasetSelector.selectDatasetByRouteName); this.datasetListIsLoading = store.select(datasetSelector.selectDatasetListIsLoading); this.datasetListIsLoaded = store.select(datasetSelector.selectDatasetListIsLoaded); this.attributeList = store.select(attributeSelector.selectAllAttributes); this.attributeListIsLoading = store.select(attributeSelector.selectAttributeListIsLoading); this.attributeListIsLoaded = store.select(attributeSelector.selectAttributeListIsLoaded); + this.criteriaFamilyList = store.select(criteriaFamilySelector.selectAllCriteriaFamilies); + this.outputFamilyList = store.select(outputFamilySelector.selectAllOutputFamilies); + this.outputCategoryList = store.select(outputCategorySelector.selectAllOutputCategories); this.columnList = store.select(columnSelector.selectAllColumns); this.columnListIsLoading = store.select(columnSelector.selectColumnListIsLoading); this.columnListIsLoaded = store.select(columnSelector.selectColumnListIsLoaded); - this.optionListGenerated = store.select(attributeSelector.getOptionListGenerated); - this.criteriaFamilyList = store.select(criteriaFamilySelector.selectAllCriteriaFamilys); - this.outputFamilyList = store.select(outputFamilySelector.selectAllOutputFamilys); - this.outputCategoryList = store.select(outputCategorySelector.selectAllOutputCategorys); + this.attributeDistinctList = store.select(attributeDistinctSelector.selectAllAttributeDistincts); + this.attributeDistinctListIsLoading = store.select(attributeDistinctSelector.selectAttributeDistinctListIsLoading); + this.attributeDistinctListIsLoaded = store.select(attributeDistinctSelector.selectAttributeDistinctListIsLoaded); this.settingsSelectList = store.select(selectSelector.selectAllSelects); this.settingsSelectOptionList = store.select(optionSelector.selectAllSelectOptions); } @@ -81,16 +87,28 @@ export class AttributeComponent implements OnInit { ngOnInit() { this.store.dispatch(datasetActions.loadDatasetList()); this.store.dispatch(attributeActions.loadAttributeList()); - this.store.dispatch(selectActions.loadSelectList()); - this.store.dispatch(optionActions.loadSelectOptionList()); this.store.dispatch(criteriaFamilyActions.loadCriteriaFamilyList()); this.store.dispatch(outputFamilyActions.loadOutputFamilyList()); this.store.dispatch(outputCategoryActions.loadOutputCategoryList()); + this.store.dispatch(selectActions.loadSelectList()); + this.store.dispatch(optionActions.loadSelectOptionList()); this.tabSelected = this.route.queryParamMap.pipe( map(params => params.get('tab_selected')) ); } + loadColumnList() { + this.store.dispatch(columnActions.loadColumnList()); + } + + loadAttributeDistinctList(attribute: Attribute) { + this.store.dispatch(attributeDistinctActions.loadAttributeDistinctList({ attribute })); + } + + getVoEnabled(): boolean { + return true; + } + addCriteriaFamily(criteriaFamily: CriteriaFamily) { this.store.dispatch(criteriaFamilyActions.addCriteriaFamily({ criteriaFamily })); } @@ -138,8 +156,4 @@ export class AttributeComponent implements OnInit { deleteAttribute(attribute: Attribute) { this.store.dispatch(attributeActions.deleteAttribute({ attribute })); } - - generateAttributeOptionList(attribute: Attribute) { - this.store.dispatch(new attributeActions.GenerateOptionListAction(attribute)); - } } diff --git a/client/src/app/metamodel/actions/attribute-distinct.actions.ts b/client/src/app/metamodel/actions/attribute-distinct.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..949cb7f862bf31f83694dcb058a7f3121b880247 --- /dev/null +++ b/client/src/app/metamodel/actions/attribute-distinct.actions.ts @@ -0,0 +1,16 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createAction, props } from '@ngrx/store'; + +import { Attribute } from '../models'; + +export const loadAttributeDistinctList = createAction('[Metamodel] Load Attribute Distinct List', props<{ attribute: Attribute }>()); +export const loadAttributeDistinctListSuccess = createAction('[Metamodel] Load Attribute List Distinct Success', props<{ values: string[] }>()); +export const loadAttributeDistinctListFail = createAction('[Metamodel] Load Attribute List Distinct Fail'); diff --git a/client/src/app/metamodel/effects/attribute-distinct.effects.ts b/client/src/app/metamodel/effects/attribute-distinct.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaa51d7cdc83ba114efe06823526c7ae0395310c --- /dev/null +++ b/client/src/app/metamodel/effects/attribute-distinct.effects.ts @@ -0,0 +1,41 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; + +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { map, mergeMap, catchError } from 'rxjs/operators'; + +import * as attributeDistinctActions from '../actions/attribute-distinct.actions'; +import { AttributeDistinctService } from '../services/attribute-distinct.service'; +import * as datasetSelector from '../selectors/dataset.selector'; + +@Injectable() +export class AttributeDistinctEffects { + loadAttributeDistinct$ = createEffect(() => + this.actions$.pipe( + ofType(attributeDistinctActions.loadAttributeDistinctList), + concatLatestFrom(() => this.store.select(datasetSelector.selectDatasetNameByRoute)), + mergeMap(([action, datasetName]) => this.attributeDistinctService.retrieveAttributeDistinctList(datasetName, action.attribute) + .pipe( + map(values => attributeDistinctActions.loadAttributeDistinctListSuccess({ values })), + catchError(() => of(attributeDistinctActions.loadAttributeDistinctListFail())) + ) + ) + ) + ); + + constructor( + private actions$: Actions, + private attributeDistinctService: AttributeDistinctService, + private store: Store<{ }> + ) {} +} diff --git a/client/src/app/metamodel/effects/attribute.effects.ts b/client/src/app/metamodel/effects/attribute.effects.ts index 2f5ed8c94714431f3c92b547b668d3c9f2573975..25cf0dd26a418101b9053a588f584b9d30b8a18a 100644 --- a/client/src/app/metamodel/effects/attribute.effects.ts +++ b/client/src/app/metamodel/effects/attribute.effects.ts @@ -52,7 +52,6 @@ export class AttributeEffects { this.actions$.pipe( ofType(attributeActions.addAttributeSuccess), tap(() => { - this.router.navigate(['/admin/attribute/attribute-list']); this.toastr.success('Attribute successfully added', 'The new attribute was added into the database') }) ), { dispatch: false} @@ -78,16 +77,6 @@ export class AttributeEffects { ) ); - editAttributeSuccess$ = createEffect(() => - this.actions$.pipe( - ofType(attributeActions.editAttributeSuccess), - tap(() => { - this.router.navigate(['/admin/attribute/attribute-list']); - this.toastr.success('Attribute successfully edited', 'The existing attribute has been edited into the database') - }) - ), { dispatch: false} - ); - editAttributeFail$ = createEffect(() => this.actions$.pipe( ofType(attributeActions.editAttributeFail), diff --git a/client/src/app/metamodel/effects/index.ts b/client/src/app/metamodel/effects/index.ts index 43a2ef4e6232de8303994b7f5544468ee0a036a6..311be4c188842a5b0ec3f2e36c839326e7953bb4 100644 --- a/client/src/app/metamodel/effects/index.ts +++ b/client/src/app/metamodel/effects/index.ts @@ -22,6 +22,7 @@ import { OutputFamilyEffects } from './output-family.effects'; import { RootDirectoryEffects } from './root-directory.effects' import { SelectEffects } from './select.effects'; import { SelectOptionEffects } from './select-option.effects'; +import { AttributeDistinctEffects } from './attribute-distinct.effects'; export const metamodelEffects = [ DatabaseEffects, @@ -38,5 +39,6 @@ export const metamodelEffects = [ OutputFamilyEffects, RootDirectoryEffects, SelectEffects, - SelectOptionEffects + SelectOptionEffects, + AttributeDistinctEffects ]; diff --git a/client/src/app/metamodel/metamodel.reducer.ts b/client/src/app/metamodel/metamodel.reducer.ts index 80032007aa3c37e18043438bcb0306e23cbfbc48..6df176d13fce0b8849defbc9de0d4cb327036f93 100644 --- a/client/src/app/metamodel/metamodel.reducer.ts +++ b/client/src/app/metamodel/metamodel.reducer.ts @@ -25,6 +25,7 @@ import * as outputFamily from './reducers/output-family.reducer'; import * as rootDirectory from './reducers/root-directory.reducer'; import * as select from './reducers/select.reducer'; import * as selectOption from './reducers/select-option.reducer'; +import * as attributeDistinct from './reducers/attribute-distinct.reducer'; export interface State { database: database.State; @@ -42,6 +43,7 @@ export interface State { rootDirectory: rootDirectory.State; select: select.State; selectOption: selectOption.State; + attributeDistinct: attributeDistinct.State; } const reducers = { @@ -59,7 +61,8 @@ const reducers = { outputFamily: outputFamily.outputFamilyReducer, rootDirectory: rootDirectory.rootDirectoryReducer, select: select.selectReducer, - selectOption: selectOption.selectOptionReducer + selectOption: selectOption.selectOptionReducer, + attributeDistinct: attributeDistinct.attributeDistinctReducer }; export const metamodelReducer = combineReducers(reducers); diff --git a/client/src/app/metamodel/reducers/attribute-distinct.reducer.ts b/client/src/app/metamodel/reducers/attribute-distinct.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..657d050033e7cb11b6ad3c23023731930c57f393 --- /dev/null +++ b/client/src/app/metamodel/reducers/attribute-distinct.reducer.ts @@ -0,0 +1,69 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createReducer, on } from '@ngrx/store'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + +import * as attributeDistinctActions from '../actions/attribute-distinct.actions'; + +export interface State extends EntityState<string> { + attributeDistinctListIsLoading: boolean; + attributeDistinctListIsLoaded: boolean; +} + +export const adapter: EntityAdapter<string> = createEntityAdapter<string>({ + selectId: (attributeDistinct: string) => attributeDistinct, + sortComparer: (a: string, b: string) => a.localeCompare(b) +}); + +export const initialState: State = adapter.getInitialState({ + attributeDistinctListIsLoading: false, + attributeDistinctListIsLoaded: false +}); + +export const attributeDistinctReducer = createReducer( + initialState, + on(attributeDistinctActions.loadAttributeDistinctList, (state) => { + return { + ...state, + attributeDistinctListIsLoading: true + } + }), + on(attributeDistinctActions.loadAttributeDistinctListSuccess, (state, { values }) => { + return adapter.setAll( + values, + { + ...state, + attributeDistinctListIsLoading: false, + attributeDistinctListIsLoaded: true + } + ); + }), + on(attributeDistinctActions.loadAttributeDistinctListFail, (state) => { + return { + ...state, + attributeDistinctListIsLoading: false + } + }) +); + +const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); + +export const selectAttributeDistinctIds = selectIds; +export const selectAttributeDistinctEntities = selectEntities; +export const selectAllAttributeDistincts = selectAll; +export const selectAttributeDistinctTotal = selectTotal; + +export const selectAttributeDistinctListIsLoading = (state: State) => state.attributeDistinctListIsLoading; +export const selectAttributeDistinctListIsLoaded = (state: State) => state.attributeDistinctListIsLoaded; diff --git a/client/src/app/metamodel/reducers/attribute.reducer.ts b/client/src/app/metamodel/reducers/attribute.reducer.ts index 9276122e8180a0d006d9b7bdadd62bb1c3619cae..769d7e329f6b32c2cc12e9699f11cb0ad62c8965 100644 --- a/client/src/app/metamodel/reducers/attribute.reducer.ts +++ b/client/src/app/metamodel/reducers/attribute.reducer.ts @@ -57,7 +57,7 @@ export const attributeReducer = createReducer( return adapter.setOne(attribute, state) }), on(attributeActions.deleteAttributeSuccess, (state, { attribute }) => { - return adapter.removeOne(attribute.name, state) + return adapter.removeOne(attribute.id, state) }) ); diff --git a/client/src/app/metamodel/selectors/attribute-distinct.selector.ts b/client/src/app/metamodel/selectors/attribute-distinct.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4acc8c7e62cd7d4ed9c4946a12d825a0591c839 --- /dev/null +++ b/client/src/app/metamodel/selectors/attribute-distinct.selector.ts @@ -0,0 +1,48 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createSelector } from '@ngrx/store'; + +import * as reducer from '../metamodel.reducer'; +import * as fromAttributeDistinct from '../reducers/attribute-distinct.reducer'; + +export const selectAttributeDistinctState = createSelector( + reducer.getMetamodelState, + (state: reducer.State) => state.attributeDistinct +); + +export const selectAttributeDistinctIds = createSelector( + selectAttributeDistinctState, + fromAttributeDistinct.selectAttributeDistinctIds +); + +export const selectAttributeDistinctEntities = createSelector( + selectAttributeDistinctState, + fromAttributeDistinct.selectAttributeDistinctEntities +); + +export const selectAllAttributeDistincts = createSelector( + selectAttributeDistinctState, + fromAttributeDistinct.selectAllAttributeDistincts +); + +export const selectAttributeDistinctTotal = createSelector( + selectAttributeDistinctState, + fromAttributeDistinct.selectAttributeDistinctTotal +); + +export const selectAttributeDistinctListIsLoading = createSelector( + selectAttributeDistinctState, + fromAttributeDistinct.selectAttributeDistinctListIsLoading +); + +export const selectAttributeDistinctListIsLoaded = createSelector( + selectAttributeDistinctState, + fromAttributeDistinct.selectAttributeDistinctListIsLoaded +); diff --git a/client/src/app/metamodel/services/attribute-distinct.service.ts b/client/src/app/metamodel/services/attribute-distinct.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc89ba7c5f2127f4eda6d7fb5b24e91b66eb42f7 --- /dev/null +++ b/client/src/app/metamodel/services/attribute-distinct.service.ts @@ -0,0 +1,27 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { Attribute } from '../models'; +import { environment } from 'src/environments/environment'; + +@Injectable() +export class AttributeDistinctService { + private API_PATH: string = environment.apiUrl + '/'; + + constructor(private http: HttpClient) { } + + retrieveAttributeDistinctList(datasetName: string, attribute: Attribute): Observable<string[]> { + return this.http.get<string[]>(this.API_PATH + 'dataset/' + datasetName + '/attribute/' + attribute.id + '/distinct'); + } +} diff --git a/client/src/app/metamodel/services/attribute.service.ts b/client/src/app/metamodel/services/attribute.service.ts index 0ce743472c3560e3446edbc73a5a311203117d3e..08c84d6cfbc6ac70cfa3c755d00c82d4bce46860 100644 --- a/client/src/app/metamodel/services/attribute.service.ts +++ b/client/src/app/metamodel/services/attribute.service.ts @@ -36,8 +36,4 @@ export class AttributeService { deleteAttribute(datasetName: string, attribute: Attribute) { return this.http.delete(this.API_PATH + 'dataset/' + datasetName + '/attribute/' + attribute.id); } - - generateOptionList(datasetName: string, attribute: Attribute): Observable<string[]> { - return this.http.get<string[]>(this.API_PATH + 'dataset/' + datasetName + '/attribute/' + attribute.id + '/distinct'); - } } diff --git a/client/src/app/metamodel/services/index.ts b/client/src/app/metamodel/services/index.ts index 12c463f5050b669ddfb7aa0097d14101f9c958f7..407158e82c33ff9a9ee9790edcfcdf9220765ed7 100644 --- a/client/src/app/metamodel/services/index.ts +++ b/client/src/app/metamodel/services/index.ts @@ -22,6 +22,7 @@ import { OutputFamilyService } from './output-family.service'; import { RootDirectoryService } from './root-directory.service'; import { SelectService } from './select.service'; import { SelectOptionService } from './select-option.service'; +import { AttributeDistinctService } from './attribute-distinct.service'; export const metamodelServices = [ DatabaseService, @@ -38,5 +39,6 @@ export const metamodelServices = [ OutputFamilyService, RootDirectoryService, SelectService, - SelectOptionService + SelectOptionService, + AttributeDistinctService ]; diff --git a/client/src/app/shared/pipes/index.ts b/client/src/app/shared/pipes/index.ts index 469cd5b5d9e6f2a41e68a78203fd4669010c368e..8945fc1d27f073ea10c9742e2087de6fa182a85b 100644 --- a/client/src/app/shared/pipes/index.ts +++ b/client/src/app/shared/pipes/index.ts @@ -10,9 +10,11 @@ import { DatasetListByFamilyPipe } from './dataset-list-by-family.pipe'; import { AttributeListByFamilyPipe } from './attribute-list-by-family.pipe'; import { SurveyByNamePipe } from './survey-by-name.pipe'; +import { OptionListBySelectPipe } from './option-list-by-select.pipe'; export const sharedPipes = [ DatasetListByFamilyPipe, AttributeListByFamilyPipe, - SurveyByNamePipe + SurveyByNamePipe, + OptionListBySelectPipe ]; diff --git a/client/src/app/shared/pipes/option-list-by-select.pipe.ts b/client/src/app/shared/pipes/option-list-by-select.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..58c9e9ded1a8fe35d9378911f8a50ef6e1bfdcbe --- /dev/null +++ b/client/src/app/shared/pipes/option-list-by-select.pipe.ts @@ -0,0 +1,19 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +import { SelectOption } from 'src/app/metamodel/models'; + +@Pipe({name: 'optionListBySelect'}) +export class OptionListBySelectPipe implements PipeTransform { + transform(optionList: SelectOption[], selectName: string): SelectOption[] { + return optionList.filter(option => option.select_name === selectName); + } +} diff --git a/client/src/app/shared/pipes/survey-by-name.pipe.ts b/client/src/app/shared/pipes/survey-by-name.pipe.ts index 3b5e75a51ba48f03e870ee49c92281dcb0d9150f..ed7021c0dddf30dc6c4f872764e7302264d36eca 100644 --- a/client/src/app/shared/pipes/survey-by-name.pipe.ts +++ b/client/src/app/shared/pipes/survey-by-name.pipe.ts @@ -14,6 +14,6 @@ import { Survey } from 'src/app/metamodel/models'; @Pipe({name: 'surveyByName'}) export class SurveyByNamePipe implements PipeTransform { transform(surveyList: Survey[], surveyName: string): Survey { - return surveyList.find(survey => survey.name === surveyName); + return surveyList.find(survey => survey.name === surveyName); } } diff --git a/client/src/app/shared/utils.ts b/client/src/app/shared/utils.ts index c6c452553b83ba3927d0ecb85ef22d9a76b9939b..d5250e7ad656835ccfe3b3e758efbe9b0614cca0 100644 --- a/client/src/app/shared/utils.ts +++ b/client/src/app/shared/utils.ts @@ -15,3 +15,14 @@ export const getHost = (): string => { } return environment.apiUrl; } + +/** + * Sorts objects by the display property. + * + * @param {number} a - The first object to sort. + * @param {number} b - The second object to sort. + * + * @example + * [objects].sortByDisplay() + */ +export const sortByDisplay = (a, b) => a.display - b.display;