From 24ab1b577f87f6ca6c5df61c7b5e2e68d568accf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr>
Date: Mon, 12 Jul 2021 22:06:21 +0200
Subject: [PATCH] Attribute design, criteria => done

---
 client/src/app/admin/admin-routing.module.ts  |   3 +
 .../attribute/add-attribute.component.html    |  41 ++++++
 .../attribute/add-attribute.component.ts      |  53 +++++++
 .../generate-option-list.component.html       |  24 ++++
 .../generate-option-list.component.ts         |  31 ++++
 .../components/attribute/criteria/index.ts    |  13 ++
 .../criteria/option-form.component.html       |   8 ++
 .../criteria/option-form.component.scss       |   4 +
 .../criteria/option-form.component.ts         |  12 ++
 .../criteria/option-list.component.html       |  20 +++
 .../criteria/option-list.component.ts         |  65 +++++++++
 .../criteria/table-criteria.component.html    |  20 +++
 .../criteria/table-criteria.component.ts      |   7 +
 .../criteria/tr-criteria.component.html       | 109 ++++++++++++++
 .../criteria/tr-criteria.component.ts         |  84 +++++++++++
 .../components/attribute/design/index.ts      |   7 +
 .../design/table-design.component.html        |  19 +++
 .../design/table-design.component.ts          |   7 +
 .../attribute/design/tr-design.component.html |  35 +++++
 .../attribute/design/tr-design.component.ts   |  39 +++++
 .../app/admin/components/attribute/index.ts   |   9 ++
 .../components/attribute/tr.component.css     |   5 +
 .../dataset/dataset-card.component.html       |   2 +-
 client/src/app/admin/components/index.ts      |  14 +-
 .../attribute/attribute-list.component.html   | 134 ++++++++++++++----
 .../attribute/attribute-list.component.ts     |  56 +++++---
 .../actions/attribute-distinct.actions.ts     |  16 +++
 .../effects/attribute-distinct.effects.ts     |  41 ++++++
 .../metamodel/effects/attribute.effects.ts    |  11 --
 client/src/app/metamodel/effects/index.ts     |   4 +-
 client/src/app/metamodel/metamodel.reducer.ts |   5 +-
 .../reducers/attribute-distinct.reducer.ts    |  69 +++++++++
 .../metamodel/reducers/attribute.reducer.ts   |   2 +-
 .../selectors/attribute-distinct.selector.ts  |  48 +++++++
 .../services/attribute-distinct.service.ts    |  27 ++++
 .../metamodel/services/attribute.service.ts   |   4 -
 client/src/app/metamodel/services/index.ts    |   4 +-
 client/src/app/shared/pipes/index.ts          |   4 +-
 .../pipes/option-list-by-select.pipe.ts       |  19 +++
 .../app/shared/pipes/survey-by-name.pipe.ts   |   2 +-
 client/src/app/shared/utils.ts                |  11 ++
 41 files changed, 1013 insertions(+), 75 deletions(-)
 create mode 100644 client/src/app/admin/components/attribute/add-attribute.component.html
 create mode 100644 client/src/app/admin/components/attribute/add-attribute.component.ts
 create mode 100644 client/src/app/admin/components/attribute/criteria/generate-option-list.component.html
 create mode 100644 client/src/app/admin/components/attribute/criteria/generate-option-list.component.ts
 create mode 100644 client/src/app/admin/components/attribute/criteria/index.ts
 create mode 100644 client/src/app/admin/components/attribute/criteria/option-form.component.html
 create mode 100644 client/src/app/admin/components/attribute/criteria/option-form.component.scss
 create mode 100644 client/src/app/admin/components/attribute/criteria/option-form.component.ts
 create mode 100644 client/src/app/admin/components/attribute/criteria/option-list.component.html
 create mode 100644 client/src/app/admin/components/attribute/criteria/option-list.component.ts
 create mode 100644 client/src/app/admin/components/attribute/criteria/table-criteria.component.html
 create mode 100644 client/src/app/admin/components/attribute/criteria/table-criteria.component.ts
 create mode 100644 client/src/app/admin/components/attribute/criteria/tr-criteria.component.html
 create mode 100644 client/src/app/admin/components/attribute/criteria/tr-criteria.component.ts
 create mode 100644 client/src/app/admin/components/attribute/design/index.ts
 create mode 100644 client/src/app/admin/components/attribute/design/table-design.component.html
 create mode 100644 client/src/app/admin/components/attribute/design/table-design.component.ts
 create mode 100644 client/src/app/admin/components/attribute/design/tr-design.component.html
 create mode 100644 client/src/app/admin/components/attribute/design/tr-design.component.ts
 create mode 100644 client/src/app/admin/components/attribute/index.ts
 create mode 100644 client/src/app/admin/components/attribute/tr.component.css
 create mode 100644 client/src/app/metamodel/actions/attribute-distinct.actions.ts
 create mode 100644 client/src/app/metamodel/effects/attribute-distinct.effects.ts
 create mode 100644 client/src/app/metamodel/reducers/attribute-distinct.reducer.ts
 create mode 100644 client/src/app/metamodel/selectors/attribute-distinct.selector.ts
 create mode 100644 client/src/app/metamodel/services/attribute-distinct.service.ts
 create mode 100644 client/src/app/shared/pipes/option-list-by-select.pipe.ts

diff --git a/client/src/app/admin/admin-routing.module.ts b/client/src/app/admin/admin-routing.module.ts
index 4cb1a0f1..7c82c2aa 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 00000000..ea32a9c4
--- /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 00000000..f7934d61
--- /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 00000000..c84d0ff8
--- /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>
+                &nbsp;
+                <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 00000000..1b6905a2
--- /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 00000000..5f6d2258
--- /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 00000000..10ee6368
--- /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 00000000..541a82b9
--- /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 00000000..5e2a620d
--- /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 00000000..58dc5550
--- /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 00000000..02e3d205
--- /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 00000000..89cad34c
--- /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 00000000..fe5c7db6
--- /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 00000000..60a63b35
--- /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 00000000..87109e23
--- /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 00000000..97b51ae6
--- /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 00000000..faee164d
--- /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 00000000..ece9403e
--- /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 00000000..6591f5dc
--- /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 00000000..72d1dced
--- /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 00000000..cac2f891
--- /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 00000000..72949e30
--- /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 356e7be9..0774b7e5 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>
         &nbsp;
diff --git a/client/src/app/admin/components/index.ts b/client/src/app/admin/components/index.ts
index 36eebde7..4aa69ec8 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 f476ec3a..75b64590 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 1254ca6e..efd9fa76 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 00000000..949cb7f8
--- /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 00000000..aaa51d7c
--- /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 2f5ed8c9..25cf0dd2 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 43a2ef4e..311be4c1 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 80032007..6df176d1 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 00000000..657d0500
--- /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 9276122e..769d7e32 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 00000000..a4acc8c7
--- /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 00000000..bc89ba7c
--- /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 0ce74347..08c84d6c 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 12c463f5..407158e8 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 469cd5b5..8945fc1d 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 00000000..58c9e9de
--- /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 3b5e75a5..ed7021c0 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 c6c45255..d5250e7a 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;
-- 
GitLab