From 145d0fcce308b9f6c0b0eddd83725e47cf817183 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr>
Date: Wed, 1 Dec 2021 16:36:54 +0100
Subject: [PATCH] Add actions for selected data

---
 .../result/datatable-actions.component.html   |  32 ++++++
 .../result/datatable-actions.component.ts     | 107 ++++++++++++++++++
 .../result/datatable-tab.component.html       |  12 ++
 .../result/datatable-tab.component.spec.ts    |  16 ++-
 .../result/datatable-tab.component.ts         |   6 +-
 .../search/components/result/index.ts         |   2 +
 .../search/containers/result.component.html   |   5 +-
 .../containers/result.component.spec.ts       |   3 +
 client/src/app/shared/shared.module.ts        |   1 +
 9 files changed, 181 insertions(+), 3 deletions(-)
 create mode 100644 client/src/app/instance/search/components/result/datatable-actions.component.html
 create mode 100644 client/src/app/instance/search/components/result/datatable-actions.component.ts

diff --git a/client/src/app/instance/search/components/result/datatable-actions.component.html b/client/src/app/instance/search/components/result/datatable-actions.component.html
new file mode 100644
index 00000000..a412b0e0
--- /dev/null
+++ b/client/src/app/instance/search/components/result/datatable-actions.component.html
@@ -0,0 +1,32 @@
+<div class="btn-group mb-2" dropdown [isDisabled]="selectedData.length < 1">
+    <button id="button-basic" dropdownToggle type="button" class="btn btn-primary dropdown-toggle" aria-controls="dropdown-basic">
+        Actions <span class="caret"></span>
+    </button>
+    <ul id="dropdown-basic" *dropdownMenu class="dropdown-menu" role="menu" aria-labelledby="button-basic">
+        <li *ngIf="getConfigDownloadResultFormat('download_csv')" role="menuitem">
+            <a class="dropdown-item" [href]="getUrl('csv')" (click)="click($event, getUrl('csv'), 'csv')">
+                <span class="fas fa-file-csv"></span> Download CSV
+            </a>
+        </li>
+        <li *ngIf="getConfigDownloadResultFormat('download_ascii')" role="menuitem">
+            <a class="dropdown-item" [href]="getUrl('ascii')" (click)="click($event, getUrl('ascii'), 'txt')">
+                <span class="fas fa-file"></span> Download ASCII
+            </a>
+        </li>
+        <li *ngIf="getConfigDownloadResultFormat('download_vo')" role="menuitem">
+            <a class="dropdown-item" [href]="getUrl('votable')" (click)="click($event, getUrl('votable'), 'xml')">
+                <span class="fas fa-file"></span> VOtable
+            </a>
+        </li>
+        <li *ngIf="getConfigDownloadResultFormat('download_vo')" role="menuitem">
+            <a class="dropdown-item" (click)="broadcastVotable()">
+                <span class="fas fa-broadcast-tower"></span> Broadcast VOtable
+            </a>
+        </li>
+        <li *ngIf="getConfigDownloadResultFormat('download_archive')" role="menuitem">
+            <a class="dropdown-item" [href]="getUrlArchive()" (click)="click($event, getUrlArchive(), 'zip')">
+                <span class="fas fa-archive"></span> Download files archive
+            </a>
+        </li>
+    </ul>
+</div>
diff --git a/client/src/app/instance/search/components/result/datatable-actions.component.ts b/client/src/app/instance/search/components/result/datatable-actions.component.ts
new file mode 100644
index 00000000..fcd508e6
--- /dev/null
+++ b/client/src/app/instance/search/components/result/datatable-actions.component.ts
@@ -0,0 +1,107 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+import { Dataset, Attribute } from 'src/app/metamodel/models';
+import { Criterion, ConeSearch, criterionToString } from 'src/app/instance/store/models';
+import { getHost } from 'src/app/shared/utils';
+import { AppConfigService } from 'src/app/app-config.service';
+
+@Component({
+    selector: 'app-datatable-actions',
+    templateUrl: 'datatable-actions.component.html'
+})
+export class DatatableActionsComponent {
+    @Input() selectedData: any[] = [];
+    @Input() datasetSelected: string;
+    @Input() datasetList: Dataset[];
+    @Input() attributeList: Attribute[];
+    @Input() criteriaList: Criterion[];
+    @Input() outputList: number[];
+    @Input() coneSearch: ConeSearch;
+    @Input() dataLength: number;
+    @Input() sampRegistered: boolean;
+    @Output() broadcast: EventEmitter<string> = new EventEmitter();
+
+    constructor(private appConfig: AppConfigService, private http: HttpClient) { }
+
+    /**
+     * Checks if the download format is allowed by Anis Admin configuration.
+     *
+     * @param  {string} format - The file format to download.
+     *
+     * @return boolean
+     */
+     getConfigDownloadResultFormat(format: string): boolean {
+        const dataset = this.datasetList.find(d => d.name === this.datasetSelected);
+        return dataset.config.download[format];
+    }
+
+    /**
+     * Returns URL to download file for the given format.
+     *
+     * @param  {string} format - The file format to download.
+     *
+     * @return string
+     */
+     getUrl(format: string): string {
+        let query: string = `${getHost(this.appConfig.apiUrl)}/search/${this.datasetSelected}?a=${this.outputList.join(';')}`;
+        if (this.criteriaList.length > 0) {
+            query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')};${this.getCriterionSelectedData()}`;
+        } else {
+            query += `&c=${this.getCriterionSelectedData()}`;
+        }
+        if (this.coneSearch) {
+            query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`;
+        }
+        query += `&f=${format}`;
+        return query;
+    }
+
+    getCriterionSelectedData() {
+        const attributeId = this.attributeList.find(a => a.search_flag === 'ID');
+        return `${attributeId.id}::in::${this.selectedData.join('|')}`;
+    }
+
+    /**
+     * Returns URL to download archive.
+     *
+     * @return boolean
+     */
+    getUrlArchive(): string {
+        let query: string = `${getHost(this.appConfig.apiUrl)}/archive/${this.datasetSelected}?a=${this.outputList.join(';')}`;
+        if (this.criteriaList.length > 0) {
+            query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')};${this.getCriterionSelectedData()}`;
+        } else {
+            query += `&c=${this.getCriterionSelectedData()}`;
+        }
+        if (this.coneSearch) {
+            query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`;
+        }
+        return query;
+    }
+
+    /**
+     * Emits event to  action to broadcast data.
+     *
+     * @fires EventEmitter<string>
+     */
+    broadcastVotable(): void {
+        this.broadcast.emit(this.getUrl('votable'));
+    }
+
+    /**
+     * Allows to download file.
+     */
+    click(event, href, extension): void {
+        event.preventDefault();
+
+        this.http.get(href, {responseType: "blob"}).subscribe(
+            data => {
+                let downloadLink = document.createElement('a');
+                downloadLink.href = window.URL.createObjectURL(data);
+                downloadLink.setAttribute('download', `${this.datasetSelected}.${extension}`);
+                downloadLink.click();
+            }
+        );
+    }
+}
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html
index f9f6b3aa..b0ad6b0c 100644
--- a/client/src/app/instance/search/components/result/datatable-tab.component.html
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.html
@@ -8,6 +8,18 @@
                 <span *ngIf="!ag.isOpen"><span class="fas fa-chevron-down"></span></span>
             </span>
         </button>
+        <app-datatable-actions 
+            [selectedData]="selectedData"
+            [datasetSelected]="datasetSelected"
+            [datasetList]="datasetList"
+            [attributeList]="attributeList"
+            [criteriaList]="criteriaList"
+            [outputList]="outputList"
+            [coneSearch]="coneSearch"
+            [dataLength]="dataLength"
+            [sampRegistered]="sampRegistered"
+            (broadcast)="broadcast.emit($event)">
+        </app-datatable-actions>
         <app-datatable
                 [dataset]="datasetList | datasetByName:datasetSelected"
                 [instance]="instance"
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts b/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts
index 7705a42b..3d261b62 100644
--- a/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.spec.ts
@@ -6,7 +6,7 @@ import { AccordionModule } from 'ngx-bootstrap/accordion';
 
 import { DatatableTabComponent } from './datatable-tab.component';
 import { Attribute, Dataset, Instance } from '../../../../metamodel/models';
-import { SearchQueryParams } from '../../../store/models';
+import { SearchQueryParams, Criterion, ConeSearch } from '../../../store/models';
 import { DatasetByNamePipe } from '../../../../shared/pipes/dataset-by-name.pipe';
 
 describe('[Instance][Search][Component][Result] DatatableTabComponent', () => {
@@ -24,6 +24,19 @@ describe('[Instance][Search][Component][Result] DatatableTabComponent', () => {
         @Input() selectedData: any[] = [];
     }
 
+    @Component({ selector: 'app-datatable-actions', template: '' })
+    class DatatableActionsStubComponent {
+        @Input() selectedData: any[] = [];
+        @Input() datasetSelected: string;
+        @Input() datasetList: Dataset[];
+        @Input() attributeList: Attribute[];
+        @Input() criteriaList: Criterion[];
+        @Input() outputList: number[];
+        @Input() coneSearch: ConeSearch;
+        @Input() dataLength: number;
+        @Input() sampRegistered: boolean;
+    }
+
     let component: DatatableTabComponent;
     let fixture: ComponentFixture<DatatableTabComponent>;
 
@@ -32,6 +45,7 @@ describe('[Instance][Search][Component][Result] DatatableTabComponent', () => {
             declarations: [
                 DatatableTabComponent,
                 DatatableStubComponent,
+                DatatableActionsStubComponent,
                 DatasetByNamePipe
             ],
             imports: [
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts
index ff703b86..c70564ba 100644
--- a/client/src/app/instance/search/components/result/datatable-tab.component.ts
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts
@@ -10,7 +10,7 @@
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
 
 import { Instance, Attribute, Dataset } from 'src/app/metamodel/models';
-import { Pagination, SearchQueryParams } from 'src/app/instance/store/models';
+import { Pagination, SearchQueryParams, Criterion, ConeSearch } from 'src/app/instance/store/models';
 
 /**
  * @class
@@ -27,8 +27,11 @@ export class DatatableTabComponent {
     @Input() datasetList: Dataset[];
     @Input() attributeList: Attribute[];
     @Input() outputList: number[];
+    @Input() criteriaList: Criterion[];
+    @Input() coneSearch: ConeSearch;
     @Input() queryParams: SearchQueryParams;
     @Input() dataLength: number;
+    @Input() sampRegistered: boolean;
     @Input() data: any[];
     @Input() dataIsLoading: boolean;
     @Input() dataIsLoaded: boolean;
@@ -36,4 +39,5 @@ export class DatatableTabComponent {
     @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter();
     @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter();
     @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter();
+    @Output() broadcast: EventEmitter<string> = new EventEmitter();
 }
diff --git a/client/src/app/instance/search/components/result/index.ts b/client/src/app/instance/search/components/result/index.ts
index f8e4657e..fffbcf4e 100644
--- a/client/src/app/instance/search/components/result/index.ts
+++ b/client/src/app/instance/search/components/result/index.ts
@@ -4,6 +4,7 @@ import { ReminderComponent } from './reminder.component';
 import { SampComponent } from './samp.component';
 import { UrlDisplayComponent } from './url-display.component';
 import { DatatableComponent } from './datatable.component';
+import { DatatableActionsComponent } from './datatable-actions.component';
 import { rendererComponents } from './renderer';
 
 export const resultComponents = [
@@ -13,5 +14,6 @@ export const resultComponents = [
     SampComponent,
     UrlDisplayComponent,
     DatatableComponent,
+    DatatableActionsComponent,
     rendererComponents
 ];
diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html
index 93a2c7ac..6ab41d2c 100644
--- a/client/src/app/instance/search/containers/result.component.html
+++ b/client/src/app/instance/search/containers/result.component.html
@@ -65,16 +65,19 @@
                 [instance]="instance | async"
                 [datasetList]="datasetList | async"
                 [attributeList]="attributeList | async | sortByOutputDisplay"
+                [criteriaList]="criteriaList | async"
                 [outputList]="outputList | async"
                 [queryParams]="queryParams | async"
                 [dataLength]="dataLength | async"
+                [sampRegistered]="sampRegistered | async"
                 [data]="data | async"
                 [dataIsLoading]="dataIsLoading | async"
                 [dataIsLoaded]="dataIsLoaded | async"
                 [selectedData]="selectedData | async"
                 (retrieveData)="retrieveData($event)"
                 (addSelectedData)="addSearchData($event)"
-                (deleteSelectedData)="deleteSearchData($event)">
+                (deleteSelectedData)="deleteSearchData($event)"
+                (broadcast)="broadcastVotable($event)">
             </app-datatable-tab>
         </ng-container>
     </div>
diff --git a/client/src/app/instance/search/containers/result.component.spec.ts b/client/src/app/instance/search/containers/result.component.spec.ts
index 16462a1f..37153740 100644
--- a/client/src/app/instance/search/containers/result.component.spec.ts
+++ b/client/src/app/instance/search/containers/result.component.spec.ts
@@ -66,8 +66,11 @@ describe('[Instance][Search][Container] ResultComponent', () => {
         @Input() datasetList: Dataset[];
         @Input() attributeList: Attribute[];
         @Input() outputList: number[];
+        @Input() criteriaList: Criterion[];
+        @Input() coneSearch: ConeSearch;
         @Input() queryParams: SearchQueryParams;
         @Input() dataLength: number;
+        @Input() sampRegistered: boolean;
         @Input() data: any[];
         @Input() dataIsLoading: boolean;
         @Input() dataIsLoaded: boolean;
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index ef7580ab..1278c17f 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -51,6 +51,7 @@ import { sharedPipes } from './pipes';
         CommonModule,
         FormsModule,
         ReactiveFormsModule,
+        BsDropdownModule,
         ModalModule,
         AccordionModule,
         PopoverModule,
-- 
GitLab