From 26a6d477f727575bebb67beb8a5a09ca210b2b90 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Wed, 17 Jul 2024 17:51:44 +0200 Subject: [PATCH 01/14] first draft of the datafiles action --- src/app/app-config.service.ts | 4 +- .../datafiles/datafiles.component.html | 11 +++++- .../datasets/datafiles/datafiles.component.ts | 19 +++------ src/app/datasets/datasets.module.ts | 1 + src/assets/config.json | 39 +++++++++++++++++-- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index c66aef158..b91a3539b 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -36,6 +36,9 @@ export interface AppConfig { archiveWorkflowEnabled: boolean; datasetJsonScientificMetadata: boolean; datasetReduceEnabled: boolean; + datasetDetailsShowMissingProposalId: boolean; + datafilesActionsEnabled: boolean; + datafilesActions: any[]; editDatasetSampleEnabled: boolean; editMetadataEnabled: boolean; editPublishedData: boolean; @@ -84,7 +87,6 @@ export interface AppConfig { tableSciDataEnabled: boolean; fileserverBaseURL: string; fileserverButtonLabel: string | undefined; - datasetDetailsShowMissingProposalId: boolean; helpMessages?: HelpMessages; notificationInterceptorEnabled: boolean; pidSearchMethod?: string; diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index 39877b4ed..83c9def4d 100644 --- a/src/app/datasets/datafiles/datafiles.component.html +++ b/src/app/datasets/datafiles/datafiles.component.html @@ -38,8 +38,10 @@ {{ count }} datafiles.
-

No datafiles linked to this dataset

+

No files associated to this dataset

+ +
No datafiles linked to this dataset
+ + + = []; sourcefolder = ""; datasetPid = ""; + actionDataset: ActionDataset; count = 0; pageSize = 25; @@ -111,7 +103,7 @@ export class DatafilesComponent dateFormat: "yyyy-MM-dd HH:mm", }, ]; - tableData: File[] = []; + tableData: DataFiles_File[] = []; constructor( public appConfigService: AppConfigService, @@ -205,13 +197,14 @@ export class DatafilesComponent if (dataset) { this.sourcefolder = dataset.sourceFolder; this.datasetPid = dataset.pid; + this.actionDataset = dataset; } }), ); this.subscriptions.push( this.datablocks$.subscribe((datablocks) => { if (datablocks) { - const files: File[] = []; + const files: DataFiles_File[] = []; datablocks.forEach((block) => { block.dataFileList.map((file) => { this.totalFileSize += file.size; diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts index 72e36d577..cf8c3a4dd 100644 --- a/src/app/datasets/datasets.module.ts +++ b/src/app/datasets/datasets.module.ts @@ -82,6 +82,7 @@ import { instrumentsReducer } from "state-management/reducers/instruments.reduce import { InstrumentEffects } from "state-management/effects/instruments.effects"; import { RelatedDatasetsComponent } from "./related-datasets/related-datasets.component"; import { FullTextSearchBarComponent } from "./dashboard/full-text-search/full-text-search-bar.component"; +import { DatafilesActionsComponent } from "./datafiles-actions/datafiles-actions.component"; @NgModule({ imports: [ diff --git a/src/assets/config.json b/src/assets/config.json index c32cf212b..facaaea12 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -111,8 +111,8 @@ "maxDirectDownloadSize": 5000000000, "metadataPreviewEnabled": true, "metadataStructure": "", - "multipleDownloadAction": "http://localhost:3012/zip", - "multipleDownloadEnabled": true, + "multipleDownloadAction": "http://localhost:3012/zip", + "multipleDownloadEnabled": true, "oAuth2Endpoints": [ { "authURL": "api/v3/auth/oidc", @@ -131,5 +131,38 @@ "shoppingCartEnabled": true, "shoppingCartOnHeader": true, "tableSciDataEnabled": true, - "datasetDetailsShowMissingProposalId": false + "datasetDetailsShowMissingProposalId": false, + "datafilesActionsEnabled" : true, + "datafilesActions" : [ + { + "id" : "eed8efec-4354-11ef-a3b5-d75573a5d37f", + "order" : 2, + "label" : "Download All", + "files" : "all", + "mat_icon" : "download", + "url" : "", + "target" : "_blank", + "authorization" : ["#datasetAccess", "#datasetPublic" ] + }, + { + "id" : "3072fafc-4363-11ef-b9f9-ebf568222d26", + "order" : 1, + "label" : "Download Selected", + "files" : "selected", + "mat_icon" : "download", + "url" : "", + "target" : "_blank", + "authorization" : ["#datasetAccess", "#datasetPublic" ] + }, + { + "id" : "4f974f0e-4364-11ef-9c63-03d19f813f4e", + "order" : 1, + "label" : "Notebook", + "files" : "selected", + "icon" : "icons/jupyter_logo.png", + "url" : "", + "target" : "_blank", + "authorization" : ["#datasetAccess", "#datasetPublic" ] + } + ] } From 13d4e4224f20482f968e63985eed203f8416eda3 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Mon, 22 Jul 2024 16:54:37 +0200 Subject: [PATCH 02/14] fixed most of the bugs for the new actions. Added test configuration --- .../datafiles-action.component.html | 14 ++ .../datafiles-action.component.scss | 10 ++ .../datafiles-action.component.ts | 128 ++++++++++++++++++ .../datafiles-action.interfaces.ts | 19 +++ .../datafiles-actions.component.html | 12 ++ .../datafiles-actions.component.scss | 3 + .../datafiles-actions.component.spec.ts | 21 +++ .../datafiles-actions.component.ts | 36 +++++ .../datafiles/datafiles.component.html | 2 + .../datafiles/datafiles.component.scss | 5 +- .../datasets/datafiles/datafiles.component.ts | 13 ++ .../datafiles/datafiles.interfaces.ts | 11 ++ src/app/datasets/datasets.module.ts | 3 + src/assets/config.json | 29 ++-- src/assets/icons/jupyter_logo.png | Bin 0 -> 2471 bytes 15 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 src/app/datasets/datafiles-actions/datafiles-action.component.html create mode 100644 src/app/datasets/datafiles-actions/datafiles-action.component.scss create mode 100644 src/app/datasets/datafiles-actions/datafiles-action.component.ts create mode 100644 src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts create mode 100644 src/app/datasets/datafiles-actions/datafiles-actions.component.html create mode 100644 src/app/datasets/datafiles-actions/datafiles-actions.component.scss create mode 100644 src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts create mode 100644 src/app/datasets/datafiles-actions/datafiles-actions.component.ts create mode 100644 src/app/datasets/datafiles/datafiles.interfaces.ts create mode 100644 src/assets/icons/jupyter_logo.png diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.html b/src/app/datasets/datafiles-actions/datafiles-action.component.html new file mode 100644 index 000000000..799932489 --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.html @@ -0,0 +1,14 @@ + + + diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.scss b/src/app/datasets/datafiles-actions/datafiles-action.component.scss new file mode 100644 index 000000000..43d6d664c --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.scss @@ -0,0 +1,10 @@ +button { + margin-left: 4px; + margin-right: 0; + + img { + height: 18px; + width: auto; + vertical-align: middle; + } +} \ No newline at end of file diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.ts new file mode 100644 index 000000000..fc3d8d62d --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.ts @@ -0,0 +1,128 @@ +import { + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from "@angular/core"; + +import { UserApi } from "shared/sdk"; +import { ActionConfig, ActionDataset } from "./datafiles-action.interfaces"; +import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; + +@Component({ + selector: "datafiles-action", + //standalone: true, + //imports: [], + templateUrl: "./datafiles-action.component.html", + styleUrls: ["./datafiles-action.component.scss"], +}) +export class DatafilesActionComponent implements OnInit, OnChanges { + @Input({ required: true }) actionConfig: ActionConfig; + @Input({ required: true }) dataset: ActionDataset; + @Input({ required: true }) files: DataFiles_File[]; + @Input({ required: true }) maxFileSize: number; + + jwt = ""; + visible = true; + use_mat_icon = false; + use_icon = false; + disabled = false; + disabled_condition = "false"; + selectedTotalFileSize = 0; + numberOfFileSelected = 0; + + constructor(private userApi: UserApi) { + this.userApi.jwt().subscribe((jwt) => { + this.jwt = jwt.jwt; + }); + } + + ngOnInit() { + this.use_mat_icon = this.actionConfig.mat_icon !== undefined; + this.use_icon = this.actionConfig.icon !== undefined; + this.prepare_disabled_condition(); + this.update_status(); + this.compute_disabled(); + } + + ngOnChanges(changes: SimpleChanges) { + console.log(changes); + if (changes["files"]) { + this.update_status(); + this.compute_disabled(); + } + } + + update_status() { + this.selectedTotalFileSize = this.files + .filter((item) => item.selected) + .reduce((sum, item) => sum + item.size, 0); + this.numberOfFileSelected = this.files.filter( + (item) => item.selected, + ).length; + } + + prepare_disabled_condition() { + if (this.actionConfig.enabled) { + this.disabled_condition = + "!(" + + this.actionConfig.enabled + .replaceAll( + "#SizeLimit", + "this.maxFileSize > 0 && this.selectedTotalFileSize <= this.maxFileSize", + ) + .replaceAll("#Selected", "this.numberOfFileSelected > 0") + + ")"; + } else if (this.actionConfig.disabled) { + this.disabled_condition = this.actionConfig.enabled + .replaceAll( + "#SizeLimit", + "this.maxFileSize > 0 && this.selectedTotalFileSize <= this.maxFileSize", + ) + .replaceAll("#Selected", "this.numberOfFileSelected > 0"); + } + } + + compute_disabled() { + this.disabled = eval(this.disabled_condition); + } + + add_input(name, value) { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = name; + input.value = value; + return input; + } + + perform_action() { + const form = document.createElement("form"); + form.target = this.actionConfig.target; + form.method = this.actionConfig.method; + form.action = this.actionConfig.url; + + form.appendChild( + this.add_input("auth_token", this.userApi.getCurrentToken().id), + ); + + form.appendChild(this.add_input("jwt", this.jwt)); + + form.appendChild(this.add_input("dataset", this.dataset.pid)); + + form.appendChild(this.add_input("directory", this.dataset.sourceFolder)); + + for (const [index, item] of this.files.entries()) { + if ( + this.actionConfig.files === "all" || + (this.actionConfig.files === "selected" && item.selected) + ) { + form.appendChild(this.add_input("files[" + index + "]", item.path)); + } + } + + document.body.appendChild(form); + form.submit(); + window.open("", "view"); + } +} diff --git a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts b/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts new file mode 100644 index 000000000..946fbe265 --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts @@ -0,0 +1,19 @@ +export interface ActionConfig { + id: string; + order: number; + label: string; + files: string; + mat_icon?: string; + icon?: string; + url: string; + target: string; + authorization: string; + method?: string; + enabled?: string; + disabled?: string; +} + +export interface ActionDataset { + pid: string; + sourceFolder: string; +} diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.html b/src/app/datasets/datafiles-actions/datafiles-actions.component.html new file mode 100644 index 000000000..38de6e286 --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.html @@ -0,0 +1,12 @@ + +
+ + +
+
\ No newline at end of file diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.scss b/src/app/datasets/datafiles-actions/datafiles-actions.component.scss new file mode 100644 index 000000000..cdc435d73 --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.scss @@ -0,0 +1,3 @@ +.dataset-datafiles-actions { + float: right; +} \ No newline at end of file diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts new file mode 100644 index 000000000..4f5d37739 --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DatafilesActionsComponent } from './datafiles-actions.component'; + +describe('DatafilesActionsComponent', () => { + let component: DatafilesActionsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DatafilesActionsComponent] + }); + fixture = TestBed.createComponent(DatafilesActionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts new file mode 100644 index 000000000..7932e61ee --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts @@ -0,0 +1,36 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { ActionConfig, ActionDataset } from "./datafiles-action.interfaces"; +import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; +import { AppConfigService } from "app-config.service"; +//import { DatafilesActionComponent } from "./datafiles-action.component"; + +@Component({ + selector: "datafiles-actions", + //standalone: true, + //imports: [DatafilesActionComponent], + templateUrl: "./datafiles-actions.component.html", + styleUrls: ["./datafiles-actions.component.scss"], +}) +export class DatafilesActionsComponent implements OnInit { + @Input({ required: true }) actionsConfig: ActionConfig[]; + @Input({ required: true }) dataset: ActionDataset; + @Input({ required: true }) files: DataFiles_File[]; + + appConfig = this.appConfigService.getConfig(); + maxFileSize: number | null = this.appConfig.maxDirectDownloadSize || 0; + + sortedActionsConfig: ActionConfig[]; + + constructor(public appConfigService: AppConfigService) {} + + ngOnInit() { + this.sortedActionsConfig = this.actionsConfig; + this.sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => + a.order && b.order ? a.order - b.order : 0, + ); + } + + visible() { + return this.appConfig.datafilesActionsEnabled && this.files.length > 0; + } +} diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index 83c9def4d..bf4b2eec7 100644 --- a/src/app/datasets/datafiles/datafiles.component.html +++ b/src/app/datasets/datafiles/datafiles.component.html @@ -41,6 +41,7 @@

No files associated to this dataset

+ item.selected) + .map((item) => item.path); + const files = this.files.map((item) => { + item.selected = selected.includes(item.path); + return item; + }); + console.log(files); + this.files = [...files]; } onSelectOne(checkboxEvent: CheckboxEvent) { diff --git a/src/app/datasets/datafiles/datafiles.interfaces.ts b/src/app/datasets/datafiles/datafiles.interfaces.ts new file mode 100644 index 000000000..a781da1ee --- /dev/null +++ b/src/app/datasets/datafiles/datafiles.interfaces.ts @@ -0,0 +1,11 @@ +export interface DataFiles_File { + path: string; + size: number; + time: string; + chk: string; + uid: string; + gid: string; + perm: string; + selected: boolean; + hash: string; +} diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts index cf8c3a4dd..72c17a0bf 100644 --- a/src/app/datasets/datasets.module.ts +++ b/src/app/datasets/datasets.module.ts @@ -83,6 +83,7 @@ import { InstrumentEffects } from "state-management/effects/instruments.effects" import { RelatedDatasetsComponent } from "./related-datasets/related-datasets.component"; import { FullTextSearchBarComponent } from "./dashboard/full-text-search/full-text-search-bar.component"; import { DatafilesActionsComponent } from "./datafiles-actions/datafiles-actions.component"; +import { DatafilesActionComponent } from "./datafiles-actions/datafiles-action.component"; @NgModule({ imports: [ @@ -162,6 +163,8 @@ import { DatafilesActionsComponent } from "./datafiles-actions/datafiles-actions DatasetFileUploaderComponent, AdminTabComponent, RelatedDatasetsComponent, + DatafilesActionsComponent, + DatafilesActionComponent, ], providers: [ ArchivingService, diff --git a/src/assets/config.json b/src/assets/config.json index facaaea12..5d70653dc 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -9,8 +9,8 @@ "editPublishedData": false, "addSampleEnabled": true, "externalAuthEndpoint": "/auth/msad", - "facility": "ESS", - "siteIcon": "esslogo-white.png", + "facility": "Local", + "siteIcon": "site-header-logo.png", "loginFacilityLabel": "ESS", "loginLdapLabel": "Ldap", "loginLocalLabel": "Local", @@ -136,32 +136,45 @@ "datafilesActions" : [ { "id" : "eed8efec-4354-11ef-a3b5-d75573a5d37f", - "order" : 2, + "order" : 4, "label" : "Download All", "files" : "all", "mat_icon" : "download", "url" : "", "target" : "_blank", + "enabled" : "#SizeLimit", "authorization" : ["#datasetAccess", "#datasetPublic" ] }, { "id" : "3072fafc-4363-11ef-b9f9-ebf568222d26", - "order" : 1, + "order" : 3, "label" : "Download Selected", "files" : "selected", "mat_icon" : "download", "url" : "", - "target" : "_blank", + "target" : "_blank", + "enabled" : "#Selected && #SizeLimit", "authorization" : ["#datasetAccess", "#datasetPublic" ] }, { "id" : "4f974f0e-4364-11ef-9c63-03d19f813f4e", + "order" : 2, + "label" : "Notebook All", + "files" : "all", + "icon" : "/assets/icons/jupyter_logo.png", + "url" : "", + "target" : "_blank", + "authorization" : ["#datasetAccess", "#datasetPublic" ] + }, + { + "id" : "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", "order" : 1, - "label" : "Notebook", + "label" : "Notebook Selected", "files" : "selected", - "icon" : "icons/jupyter_logo.png", + "icon" : "/assets/icons/jupyter_logo.png", "url" : "", - "target" : "_blank", + "target" : "_blank", + "enabled" : "#Selected", "authorization" : ["#datasetAccess", "#datasetPublic" ] } ] diff --git a/src/assets/icons/jupyter_logo.png b/src/assets/icons/jupyter_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..58623ebfbfd17f0ca8ded37018c740198f9e09f7 GIT binary patch literal 2471 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 W_WyzYa}6~t0GzXSMAw|f{rO*6J7ZD+ literal 0 HcmV?d00001 From 5d09d487e49c584b5f80309b05b147192037fc37 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Tue, 23 Jul 2024 10:03:05 +0200 Subject: [PATCH 03/14] fixed linting issues --- .../datafiles-actions.component.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts index 4f5d37739..0f98477a5 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts @@ -1,21 +1,21 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { DatafilesActionsComponent } from './datafiles-actions.component'; +import { DatafilesActionsComponent } from "./datafiles-actions.component"; -describe('DatafilesActionsComponent', () => { +describe("DatafilesActionsComponent", () => { let component: DatafilesActionsComponent; let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [DatafilesActionsComponent] + declarations: [DatafilesActionsComponent], }); fixture = TestBed.createComponent(DatafilesActionsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it("should create", () => { expect(component).toBeTruthy(); }); }); From 15130205dddb7b5aa18ba044dc36944994540d90 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Tue, 23 Jul 2024 12:35:52 +0200 Subject: [PATCH 04/14] working on fixing unit testing --- src/app/app-config.service.spec.ts | 46 +++++++ .../datafiles-action.interfaces.ts | 2 +- .../datafiles-actions.component.spec.ts | 128 +++++++++++++++++- .../datafiles/datafiles.component.spec.ts | 58 +++++++- src/app/shared/MockStubs.ts | 8 ++ 5 files changed, 238 insertions(+), 4 deletions(-) diff --git a/src/app/app-config.service.spec.ts b/src/app/app-config.service.spec.ts index 3a59efee6..4538eea22 100644 --- a/src/app/app-config.service.spec.ts +++ b/src/app/app-config.service.spec.ts @@ -135,6 +135,52 @@ const appConfig: AppConfig = { datasetDetailsShowMissingProposalId: true, helpMessages: new HelpMessages(), notificationInterceptorEnabled: true, + datafilesActionsEnabled: true, + datafilesActions: [ + { + id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", + order: 4, + label: "Download All", + files: "all", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "3072fafc-4363-11ef-b9f9-ebf568222d26", + order: 3, + label: "Download Selected", + files: "selected", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#Selected && #SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", + order: 2, + label: "Notebook All", + files: "all", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + order: 1, + label: "Notebook Selected", + files: "selected", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + enabled: "#Selected", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + ], }; describe("AppConfigService", () => { diff --git a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts b/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts index 946fbe265..14410d950 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts @@ -7,7 +7,7 @@ export interface ActionConfig { icon?: string; url: string; target: string; - authorization: string; + authorization: string[]; method?: string; enabled?: string; disabled?: string; diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts index 0f98477a5..215b703bf 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts @@ -1,20 +1,144 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { DatafilesActionsComponent } from "./datafiles-actions.component"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { MatTableModule } from "@angular/material/table"; +import { PipesModule } from "shared/pipes/pipes.module"; +import { ReactiveFormsModule } from "@angular/forms"; +import { MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { RouterModule } from "@angular/router"; +import { StoreModule } from "@ngrx/store"; +import { UserApi } from "shared/sdk"; +import { MockMatDialogRef, MockUserApi } from "shared/MockStubs"; +import { AppConfigService } from "app-config.service"; describe("DatafilesActionsComponent", () => { let component: DatafilesActionsComponent; let fixture: ComponentFixture; - beforeEach(() => { + const getConfig = () => ({ + maxDirectDownloadSize: 10000, + datafilesActionsEnabled: true, + }); + + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + MatButtonModule, + MatIconModule, + MatTableModule, + PipesModule, + ReactiveFormsModule, + MatDialogModule, + RouterModule, + RouterModule.forRoot([]), + StoreModule.forRoot({}), + ], declarations: [DatafilesActionsComponent], }); + TestBed.overrideComponent(DatafilesActionsComponent, { + set: { + providers: [ + { provide: UserApi, useClass: MockUserApi }, + { provide: MatDialogRef, useClass: MockMatDialogRef }, + { provide: AppConfigService, useValue: { getConfig } }, + { provide: UserApi, useClass: MockUserApi }, + ], + }, + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { fixture = TestBed.createComponent(DatafilesActionsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); + afterEach(() => { + fixture.destroy(); + }); + + beforeEach(waitForAsync(() => { + component.files = [ + { + path: "test1", + size: 5000, + time: "2019-09-06T13:11:37.102Z", + chk: "string", + uid: "string", + gid: "string", + perm: "string", + selected: false, + hash: "", + }, + { + path: "test2", + size: 10000, + time: "2019-09-06T13:11:37.102Z", + chk: "string", + uid: "string", + gid: "string", + perm: "string", + selected: false, + hash: "", + }, + ]; + component.actionsConfig = [ + { + id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", + order: 4, + label: "Download All", + files: "all", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "3072fafc-4363-11ef-b9f9-ebf568222d26", + order: 3, + label: "Download Selected", + files: "selected", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#Selected && #SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", + order: 2, + label: "Notebook All", + files: "all", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + order: 1, + label: "Notebook Selected", + files: "selected", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + enabled: "#Selected", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + ]; + component.dataset = { + pid: "57eb0ad6-48d4-11ef-814b-df221a8e3571", + sourceFolder: "/level_1/level_2/level3", + }; + fixture.detectChanges(); + })); + it("should create", () => { expect(component).toBeTruthy(); }); diff --git a/src/app/datasets/datafiles/datafiles.component.spec.ts b/src/app/datasets/datafiles/datafiles.component.spec.ts index c96f81a58..33f0d45df 100644 --- a/src/app/datasets/datafiles/datafiles.component.spec.ts +++ b/src/app/datasets/datafiles/datafiles.component.spec.ts @@ -8,18 +8,70 @@ import { PipesModule } from "shared/pipes/pipes.module"; import { RouterModule } from "@angular/router"; import { StoreModule } from "@ngrx/store"; import { CheckboxEvent } from "shared/modules/table/table.component"; -import { MockMatDialogRef, MockUserApi } from "shared/MockStubs"; +import { + MockDatafilesActionsComponent, + MockMatDialogRef, + MockUserApi, +} from "shared/MockStubs"; import { MatCheckboxChange } from "@angular/material/checkbox"; import { MatIconModule } from "@angular/material/icon"; import { MatButtonModule } from "@angular/material/button"; import { AppConfigService } from "app-config.service"; import { MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { DatafilesActionsComponent } from "datasets/datafiles-actions/datafiles-actions.component"; describe("DatafilesComponent", () => { let component: DatafilesComponent; let fixture: ComponentFixture; const getConfig = () => ({}); + // datafilesActionsEnabled: true, + // datafilesActions: [ + // { + // id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", + // order: 4, + // label: "Download All", + // files: "all", + // mat_icon: "download", + // url: "", + // target: "_blank", + // enabled: "#SizeLimit", + // authorization: ["#datasetAccess", "#datasetPublic"], + // }, + // { + // id: "3072fafc-4363-11ef-b9f9-ebf568222d26", + // order: 3, + // label: "Download Selected", + // files: "selected", + // mat_icon: "download", + // url: "", + // target: "_blank", + // enabled: "#Selected && #SizeLimit", + // authorization: ["#datasetAccess", "#datasetPublic"], + // }, + // { + // id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", + // order: 2, + // label: "Notebook All", + // files: "all", + // icon: "/assets/icons/jupyter_logo.png", + // url: "", + // target: "_blank", + // authorization: ["#datasetAccess", "#datasetPublic"], + // }, + // { + // id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + // order: 1, + // label: "Notebook Selected", + // files: "selected", + // icon: "/assets/icons/jupyter_logo.png", + // url: "", + // target: "_blank", + // enabled: "#Selected", + // authorization: ["#datasetAccess", "#datasetPublic"], + // }, + // ], + // }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -44,6 +96,10 @@ describe("DatafilesComponent", () => { { provide: MatDialogRef, useClass: MockMatDialogRef }, { provide: AppConfigService, useValue: { getConfig } }, { provide: UserApi, useClass: MockUserApi }, + { + provide: DatafilesActionsComponent, + useClass: MockDatafilesActionsComponent, + }, ], }, }); diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts index edaa2a986..5b76bc56f 100644 --- a/src/app/shared/MockStubs.ts +++ b/src/app/shared/MockStubs.ts @@ -6,6 +6,8 @@ import { AppConfig } from "app-config.module"; import { SciCatDataSource } from "./services/scicat.datasource"; import { LoopBackAuth } from "./sdk"; import { Injectable } from "@angular/core"; +import { ActionConfig, ActionDataset } from "datasets/datafiles-actions/datafiles-action.interfaces"; +import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; export class MockUserApi { getCurrentId() { @@ -200,3 +202,9 @@ export class MockLoopBackAuth extends LoopBackAuth { getAccessToken = () => ({ id: "test" }); getAccessTokenId = () => "test"; } + +export class MockDatafilesActionsComponent { + actionsConfig: ActionConfig[]; + dataset: ActionDataset; + files: DataFiles_File[]; +} From e06101aa0133e6c9c88288f42e1ce9de991dcf85 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Tue, 23 Jul 2024 12:41:00 +0200 Subject: [PATCH 05/14] fixed linting errors --- src/app/shared/MockStubs.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts index 5b76bc56f..aa257b2e8 100644 --- a/src/app/shared/MockStubs.ts +++ b/src/app/shared/MockStubs.ts @@ -6,7 +6,10 @@ import { AppConfig } from "app-config.module"; import { SciCatDataSource } from "./services/scicat.datasource"; import { LoopBackAuth } from "./sdk"; import { Injectable } from "@angular/core"; -import { ActionConfig, ActionDataset } from "datasets/datafiles-actions/datafiles-action.interfaces"; +import { + ActionConfig, + ActionDataset, +} from "datasets/datafiles-actions/datafiles-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; export class MockUserApi { From d05cdef539000934549f60661e349ccc167f3c7a Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 23 Jul 2024 13:13:07 +0200 Subject: [PATCH 06/14] added missing config for datafiles.component.spec.ts --- .../datafiles/datafiles.component.spec.ts | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/src/app/datasets/datafiles/datafiles.component.spec.ts b/src/app/datasets/datafiles/datafiles.component.spec.ts index 33f0d45df..2e6e0125d 100644 --- a/src/app/datasets/datafiles/datafiles.component.spec.ts +++ b/src/app/datasets/datafiles/datafiles.component.spec.ts @@ -24,54 +24,54 @@ describe("DatafilesComponent", () => { let component: DatafilesComponent; let fixture: ComponentFixture; - const getConfig = () => ({}); - // datafilesActionsEnabled: true, - // datafilesActions: [ - // { - // id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", - // order: 4, - // label: "Download All", - // files: "all", - // mat_icon: "download", - // url: "", - // target: "_blank", - // enabled: "#SizeLimit", - // authorization: ["#datasetAccess", "#datasetPublic"], - // }, - // { - // id: "3072fafc-4363-11ef-b9f9-ebf568222d26", - // order: 3, - // label: "Download Selected", - // files: "selected", - // mat_icon: "download", - // url: "", - // target: "_blank", - // enabled: "#Selected && #SizeLimit", - // authorization: ["#datasetAccess", "#datasetPublic"], - // }, - // { - // id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", - // order: 2, - // label: "Notebook All", - // files: "all", - // icon: "/assets/icons/jupyter_logo.png", - // url: "", - // target: "_blank", - // authorization: ["#datasetAccess", "#datasetPublic"], - // }, - // { - // id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", - // order: 1, - // label: "Notebook Selected", - // files: "selected", - // icon: "/assets/icons/jupyter_logo.png", - // url: "", - // target: "_blank", - // enabled: "#Selected", - // authorization: ["#datasetAccess", "#datasetPublic"], - // }, - // ], - // }); + const getConfig = () => ({ + datafilesActionsEnabled: true, + datafilesActions: [ + { + id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", + order: 4, + label: "Download All", + files: "all", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "3072fafc-4363-11ef-b9f9-ebf568222d26", + order: 3, + label: "Download Selected", + files: "selected", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#Selected && #SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", + order: 2, + label: "Notebook All", + files: "all", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + order: 1, + label: "Notebook Selected", + files: "selected", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + enabled: "#Selected", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + ], + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ From 9bc57cd3fc96cadf2b77b07cdd41e8eef953140d Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 23 Jul 2024 14:35:33 +0200 Subject: [PATCH 07/14] renamed dataset Input to actionDataset to avoid HTML element conflict --- .../datafiles-action.component.ts | 8 +++++--- .../datafiles-actions.component.html | 6 +++--- .../datafiles-actions.component.spec.ts | 16 ++++++---------- .../datafiles-actions.component.ts | 2 +- .../datasets/datafiles/datafiles.component.html | 3 +-- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.ts index fc3d8d62d..fd232736d 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.ts @@ -19,7 +19,7 @@ import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; }) export class DatafilesActionComponent implements OnInit, OnChanges { @Input({ required: true }) actionConfig: ActionConfig; - @Input({ required: true }) dataset: ActionDataset; + @Input({ required: true }) actionDataset: ActionDataset; @Input({ required: true }) files: DataFiles_File[]; @Input({ required: true }) maxFileSize: number; @@ -108,9 +108,11 @@ export class DatafilesActionComponent implements OnInit, OnChanges { form.appendChild(this.add_input("jwt", this.jwt)); - form.appendChild(this.add_input("dataset", this.dataset.pid)); + form.appendChild(this.add_input("dataset", this.actionDataset.pid)); - form.appendChild(this.add_input("directory", this.dataset.sourceFolder)); + form.appendChild( + this.add_input("directory", this.actionDataset.sourceFolder), + ); for (const [index, item] of this.files.entries()) { if ( diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.html b/src/app/datasets/datafiles-actions/datafiles-actions.component.html index 38de6e286..3856ca88a 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.html +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.html @@ -2,11 +2,11 @@
- \ No newline at end of file + diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts index 215b703bf..0d13dffe3 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts @@ -55,14 +55,6 @@ describe("DatafilesActionsComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(DatafilesActionsComponent); component = fixture.componentInstance; - fixture.detectChanges(); - }); - - afterEach(() => { - fixture.destroy(); - }); - - beforeEach(waitForAsync(() => { component.files = [ { path: "test1", @@ -132,12 +124,16 @@ describe("DatafilesActionsComponent", () => { authorization: ["#datasetAccess", "#datasetPublic"], }, ]; - component.dataset = { + component.actionDataset = { pid: "57eb0ad6-48d4-11ef-814b-df221a8e3571", sourceFolder: "/level_1/level_2/level3", }; fixture.detectChanges(); - })); + }); + + afterEach(() => { + fixture.destroy(); + }); it("should create", () => { expect(component).toBeTruthy(); diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts index 7932e61ee..533169a47 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts @@ -13,7 +13,7 @@ import { AppConfigService } from "app-config.service"; }) export class DatafilesActionsComponent implements OnInit { @Input({ required: true }) actionsConfig: ActionConfig[]; - @Input({ required: true }) dataset: ActionDataset; + @Input({ required: true }) actionDataset: ActionDataset; @Input({ required: true }) files: DataFiles_File[]; appConfig = this.appConfigService.getConfig(); diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index bf4b2eec7..8eb776fa3 100644 --- a/src/app/datasets/datafiles/datafiles.component.html +++ b/src/app/datasets/datafiles/datafiles.component.html @@ -137,11 +137,10 @@

No files associated to this dataset

--> - Date: Fri, 26 Jul 2024 16:31:50 +0200 Subject: [PATCH 08/14] datafiles actions tests, data files action test almost done --- .../datafiles-action.component.ts | 90 ++++++---- .../datafiles-actions.component.html | 2 +- .../datafiles-actions.component.spec.ts | 163 ++++++++++++------ .../datafiles-actions.component.ts | 36 ++-- src/app/shared/MockStubs.ts | 54 ++++++ 5 files changed, 245 insertions(+), 100 deletions(-) diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.ts index fd232736d..ff0675f58 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.ts @@ -27,65 +27,79 @@ export class DatafilesActionComponent implements OnInit, OnChanges { visible = true; use_mat_icon = false; use_icon = false; - disabled = false; + //disabled = false; disabled_condition = "false"; selectedTotalFileSize = 0; numberOfFileSelected = 0; + form: HTMLFormElement; + constructor(private userApi: UserApi) { this.userApi.jwt().subscribe((jwt) => { this.jwt = jwt.jwt; }); } + private evaluate_disabled_condition(condition: string) { + return condition + .replaceAll( + "#SizeLimit", + String( + this.maxFileSize > 0 && + this.selectedTotalFileSize <= this.maxFileSize, + ), + ) + .replaceAll("#Selected", String(this.numberOfFileSelected > 0)); + } + + private prepare_disabled_condition() { + if (this.actionConfig.enabled) { + this.disabled_condition = + "!(" + + this.evaluate_disabled_condition(this.actionConfig.enabled) + + ")"; + } else if (this.actionConfig.disabled) { + this.disabled_condition = this.evaluate_disabled_condition( + this.actionConfig.disabled, + ); + } else { + this.disabled_condition = "false"; + } + } + ngOnInit() { this.use_mat_icon = this.actionConfig.mat_icon !== undefined; this.use_icon = this.actionConfig.icon !== undefined; this.prepare_disabled_condition(); this.update_status(); - this.compute_disabled(); + //this.compute_disabled(); } ngOnChanges(changes: SimpleChanges) { console.log(changes); if (changes["files"]) { this.update_status(); - this.compute_disabled(); + //this.compute_disabled(); } } update_status() { this.selectedTotalFileSize = this.files - .filter((item) => item.selected) + .filter((item) => item.selected || this.actionConfig.files === "all") .reduce((sum, item) => sum + item.size, 0); this.numberOfFileSelected = this.files.filter( (item) => item.selected, ).length; } - prepare_disabled_condition() { - if (this.actionConfig.enabled) { - this.disabled_condition = - "!(" + - this.actionConfig.enabled - .replaceAll( - "#SizeLimit", - "this.maxFileSize > 0 && this.selectedTotalFileSize <= this.maxFileSize", - ) - .replaceAll("#Selected", "this.numberOfFileSelected > 0") + - ")"; - } else if (this.actionConfig.disabled) { - this.disabled_condition = this.actionConfig.enabled - .replaceAll( - "#SizeLimit", - "this.maxFileSize > 0 && this.selectedTotalFileSize <= this.maxFileSize", - ) - .replaceAll("#Selected", "this.numberOfFileSelected > 0"); - } - } + // compute_disabled() { + // this.disabled = eval(this.disabled_condition); + // } - compute_disabled() { - this.disabled = eval(this.disabled_condition); + get disabled() { + this.update_status(); + this.prepare_disabled_condition(); + return eval(this.disabled_condition); } add_input(name, value) { @@ -97,20 +111,20 @@ export class DatafilesActionComponent implements OnInit, OnChanges { } perform_action() { - const form = document.createElement("form"); - form.target = this.actionConfig.target; - form.method = this.actionConfig.method; - form.action = this.actionConfig.url; + this.form = document.createElement("form"); + this.form.target = this.actionConfig.target; + this.form.method = this.actionConfig.method; + this.form.action = this.actionConfig.url; - form.appendChild( + this.form.appendChild( this.add_input("auth_token", this.userApi.getCurrentToken().id), ); - form.appendChild(this.add_input("jwt", this.jwt)); + this.form.appendChild(this.add_input("jwt", this.jwt)); - form.appendChild(this.add_input("dataset", this.actionDataset.pid)); + this.form.appendChild(this.add_input("dataset", this.actionDataset.pid)); - form.appendChild( + this.form.appendChild( this.add_input("directory", this.actionDataset.sourceFolder), ); @@ -119,12 +133,14 @@ export class DatafilesActionComponent implements OnInit, OnChanges { this.actionConfig.files === "all" || (this.actionConfig.files === "selected" && item.selected) ) { - form.appendChild(this.add_input("files[" + index + "]", item.path)); + this.form.appendChild( + this.add_input("files[" + index + "]", item.path), + ); } } - document.body.appendChild(form); - form.submit(); + //document.body.appendChild(form); + this.form.submit(); window.open("", "view"); } } diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.html b/src/app/datasets/datafiles-actions/datafiles-actions.component.html index 3856ca88a..b35f645ea 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.html +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.html @@ -1,4 +1,4 @@ - +
{ let component: DatafilesActionsComponent; let fixture: ComponentFixture; + const mockAppConfigService = { + getConfig: () => { + return { + maxDirectDownloadSize: 10000, + datafilesActionsEnabled: true, + }; + }, + }; - const getConfig = () => ({ - maxDirectDownloadSize: 10000, - datafilesActionsEnabled: true, - }); + const actionsConfig = [ + { + id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", + order: 4, + label: "Download All", + files: "all", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "3072fafc-4363-11ef-b9f9-ebf568222d26", + order: 3, + label: "Download Selected", + files: "selected", + mat_icon: "download", + url: "", + target: "_blank", + enabled: "#Selected && #SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", + order: 2, + label: "Notebook All", + files: "all", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + order: 1, + label: "Notebook Selected", + files: "selected", + icon: "/assets/icons/jupyter_logo.png", + url: "", + target: "_blank", + enabled: "#Selected", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + ]; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -44,7 +93,7 @@ describe("DatafilesActionsComponent", () => { providers: [ { provide: UserApi, useClass: MockUserApi }, { provide: MatDialogRef, useClass: MockMatDialogRef }, - { provide: AppConfigService, useValue: { getConfig } }, + { provide: AppConfigService, useValue: mockAppConfigService }, { provide: UserApi, useClass: MockUserApi }, ], }, @@ -79,51 +128,7 @@ describe("DatafilesActionsComponent", () => { hash: "", }, ]; - component.actionsConfig = [ - { - id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", - order: 4, - label: "Download All", - files: "all", - mat_icon: "download", - url: "", - target: "_blank", - enabled: "#SizeLimit", - authorization: ["#datasetAccess", "#datasetPublic"], - }, - { - id: "3072fafc-4363-11ef-b9f9-ebf568222d26", - order: 3, - label: "Download Selected", - files: "selected", - mat_icon: "download", - url: "", - target: "_blank", - enabled: "#Selected && #SizeLimit", - authorization: ["#datasetAccess", "#datasetPublic"], - }, - { - id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", - order: 2, - label: "Notebook All", - files: "all", - icon: "/assets/icons/jupyter_logo.png", - url: "", - target: "_blank", - authorization: ["#datasetAccess", "#datasetPublic"], - }, - { - id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", - order: 1, - label: "Notebook Selected", - files: "selected", - icon: "/assets/icons/jupyter_logo.png", - url: "", - target: "_blank", - enabled: "#Selected", - authorization: ["#datasetAccess", "#datasetPublic"], - }, - ]; + component.actionsConfig = actionsConfig; component.actionDataset = { pid: "57eb0ad6-48d4-11ef-814b-df221a8e3571", sourceFolder: "/level_1/level_2/level3", @@ -138,4 +143,62 @@ describe("DatafilesActionsComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + it("sorted actions should be sorted", () => { + const sortedActionsConfig = component.sortedActionsConfig; + + for (let i = 1; i < sortedActionsConfig.length; i++) { + expect( + sortedActionsConfig[i].order >= sortedActionsConfig[i - 1].order, + ).toEqual(true); + } + }); + + it("actions should be visible when enabled in configuration", () => { + expect(component.visible).toEqual(true); + }); + + it("actions should be visible when disabled in configuration", () => { + spyOn(mockAppConfigService, "getConfig").and.returnValue({ + maxDirectDownloadSize: 10000, + datafilesActionsEnabled: false, + }); + expect(component.visible).toEqual(false); + }); + + it("max file size should be the same as set in configuration, aka 10000", () => { + expect(component.maxFileSize).toEqual(10000); + }); + + it("max file size should be the same as set in configuration, aka 5000", () => { + spyOn(mockAppConfigService, "getConfig").and.returnValue({ + maxDirectDownloadSize: 5000, + datafilesActionsEnabled: true, + }); + expect(component.maxFileSize).toEqual(5000); + }); + + it("actions should be visible with default configuration", () => { + spyOn(mockAppConfigService, "getConfig").and.returnValue({ + maxDirectDownloadSize: 10000, + datafilesActionsEnabled: true, + }); + expect(component.visible).toEqual(true); + }); + + it("there should be 4 actions as defined in default configuration", async () => { + expect(component.sortedActionsConfig.length).toEqual(actionsConfig.length); + const htmlElement: HTMLElement = fixture.nativeElement; + const htmlActions = htmlElement.querySelectorAll("datafiles-action"); + expect(htmlActions.length).toEqual(actionsConfig.length); + }); + + it("there should be 0 actions with no actions configured", async () => { + component.actionsConfig = []; + fixture.detectChanges(); + expect(component.sortedActionsConfig.length).toEqual(0); + const htmlElement: HTMLElement = fixture.nativeElement; + const htmlActions = htmlElement.querySelectorAll("datafiles-action"); + expect(htmlActions.length).toEqual(0); + }); }); diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts index 533169a47..1e23e8e78 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts @@ -11,26 +11,38 @@ import { AppConfigService } from "app-config.service"; templateUrl: "./datafiles-actions.component.html", styleUrls: ["./datafiles-actions.component.scss"], }) -export class DatafilesActionsComponent implements OnInit { +export class DatafilesActionsComponent { + private _sortedActionsConfig: ActionConfig[]; + @Input({ required: true }) actionsConfig: ActionConfig[]; @Input({ required: true }) actionDataset: ActionDataset; @Input({ required: true }) files: DataFiles_File[]; - appConfig = this.appConfigService.getConfig(); - maxFileSize: number | null = this.appConfig.maxDirectDownloadSize || 0; - - sortedActionsConfig: ActionConfig[]; - constructor(public appConfigService: AppConfigService) {} - ngOnInit() { - this.sortedActionsConfig = this.actionsConfig; - this.sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => - a.order && b.order ? a.order - b.order : 0, + // ngOnInit() { + // this.sortedActionsConfig = this.actionsConfig; + // this.sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => + // a.order && b.order ? a.order - b.order : 0, + // ); + // } + + get visible(): boolean { + return ( + this.appConfigService.getConfig().datafilesActionsEnabled && + this.files.length > 0 ); } - visible() { - return this.appConfig.datafilesActionsEnabled && this.files.length > 0; + get maxFileSize(): number { + return this.appConfigService.getConfig().maxDirectDownloadSize || 0; + } + + get sortedActionsConfig(): ActionConfig[] { + this._sortedActionsConfig = this.actionsConfig; + this._sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => + a.order && b.order ? a.order - b.order : 0, + ); + return this._sortedActionsConfig; } } diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts index aa257b2e8..2a123a36d 100644 --- a/src/app/shared/MockStubs.ts +++ b/src/app/shared/MockStubs.ts @@ -211,3 +211,57 @@ export class MockDatafilesActionsComponent { dataset: ActionDataset; files: DataFiles_File[]; } + +export class MockHtmlElement { + id = ""; + tag = "HTML"; + innerHTML = ""; + value = ""; + name = ""; + disabled = false; + style: unknown = { display: "block", backgroundColor: "red" }; + children: MockHtmlElement[] = []; + + constructor(tag = "", id = "") { + this.id = id; + this.tag = tag; + } + createElement(t: string, id = "") { + return new MockHtmlElement(t, id); + } + appendChild(x: MockHtmlElement) { + this.children.push(x); + return x; + } + clear() { + this.children = []; + } + querySelector(sel: string) { + // too hard to implement correctly, so just hack something + const list = this.getElementsByTagName(sel); + return list.length > 0 ? list[0] : this.children[0]; + } + querySelectorAll(sel: string) { + // too hard to implement correctly, so just return all children + return this.children; + } + getElementById(id: string): any { + // if not found, just CREATE one!! + return ( + this.children.find((x) => x.id == id) || + this.appendChild(this.createElement("", id)) + ); + } + getElementsByClassName(classname: string): any[] { + return this.children.filter((x: any) => x.classList.contains(classname)); + } + getElementsByName(name: string): any[] { + return this.children.filter((x: any) => x.name == name); + } + getElementsByTagName(tag: string): any[] { + return this.children.filter((x: any) => x.tag == tag.toUpperCase()); + } + submit() { + return true; + } +} From 9ceb2a7da8425ec9242f45446ff34e9f6f766e04 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Mon, 29 Jul 2024 11:21:05 +0200 Subject: [PATCH 09/14] Completed tests for datafiles action --- karma.conf.js | 3 +- .../datafiles-action.component.spec.ts | 798 ++++++++++++++++++ src/app/shared/MockStubs.ts | 4 +- 3 files changed, 801 insertions(+), 4 deletions(-) create mode 100644 src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts diff --git a/karma.conf.js b/karma.conf.js index ac60f4230..804a75171 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -42,7 +42,8 @@ module.exports = function (config) { dir: require('path').join(__dirname, './coverage'), reporters: [ { type: 'html', subdir: 'report-html' }, - { type: 'lcovonly', subdir: '.', file: 'lcov.info' } + { type: 'lcovonly', subdir: '.', file: 'lcov.info' }, + { type: 'text-summary' } ], fixWebpackSourcePaths: true }, diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts new file mode 100644 index 000000000..4c3e4cd68 --- /dev/null +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts @@ -0,0 +1,798 @@ +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; + +import { DatafilesActionComponent } from "./datafiles-action.component"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { MatTableModule } from "@angular/material/table"; +import { PipesModule } from "shared/pipes/pipes.module"; +import { ReactiveFormsModule } from "@angular/forms"; +import { MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { RouterModule } from "@angular/router"; +import { StoreModule } from "@ngrx/store"; +import { UserApi } from "shared/sdk"; +import { + MockHtmlElement, + MockMatDialogRef, + MockUserApi, +} from "shared/MockStubs"; +import { ActionDataset } from "./datafiles-action.interfaces"; + +describe("DatafilesActionComponent", () => { + let component: DatafilesActionComponent; + let fixture: ComponentFixture; + + const actionsConfig = [ + { + id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", + order: 4, + label: "Download All", + files: "all", + mat_icon: "download", + url: "https://download.scicat.org", + target: "_blank", + enabled: "#SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "3072fafc-4363-11ef-b9f9-ebf568222d26", + order: 3, + label: "Download Selected", + files: "selected", + mat_icon: "download", + url: "https://download.scicat.org", + target: "_blank", + enabled: "#Selected && #SizeLimit", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", + order: 2, + label: "Notebook All", + files: "all", + icon: "/assets/icons/jupyter_logo.png", + url: "https://notebook.scicat.org", + target: "_blank", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + { + id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + order: 1, + label: "Notebook Selected", + files: "selected", + icon: "/assets/icons/jupyter_logo.png", + url: "https://notebook.scicat.org", + target: "_blank", + enabled: "#Selected", + authorization: ["#datasetAccess", "#datasetPublic"], + }, + ]; + + const actionDataset: ActionDataset = { + pid: "1c7298da-4a7c-11ef-a2ce-2fdb7a34e7eb", + sourceFolder: "/folder_1/folder_2/folder_3", + }; + + const actionFiles = [ + { + path: "file1", + size: 5000, + time: "2019-09-06T13:11:37.102Z", + chk: "string", + uid: "string", + gid: "string", + perm: "string", + selected: false, + hash: "", + }, + { + path: "file2", + size: 10000, + time: "2019-09-06T13:11:37.102Z", + chk: "string", + uid: "string", + gid: "string", + perm: "string", + selected: false, + hash: "", + }, + ]; + + const lowerMaxFileSizeLimit = 9999; + const higherMaxFileSizeLimit = 20000; + enum maxSizeType { + lower = "lower", + higher = "higher", + } + + enum selectedFilesType { + none = "none", + file1 = "file1", + file2 = "file2", + all = "all", + } + + enum actionSelectorType { + download_all = 0, + download_selected = 1, + notebook_all = 2, + notebook_selected = 3, + } + + const jwt = () => ({ + subscribe: (f: any) => ({ + jwt: "9a2322a8-4a7d-11ef-a0f5-d7c40fcf1693", + }), + }); + + const getCurrentToken = () => ({ + id: "4ac45f3e-4d79-11ef-856c-6339dab93bee", + }); + + + const browserWindowMock = { + document: { + write() {}, + body: { + setAttribute() {}, + }, + }, + } as unknown as Window; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + MatButtonModule, + MatIconModule, + MatTableModule, + PipesModule, + ReactiveFormsModule, + MatDialogModule, + RouterModule, + RouterModule.forRoot([]), + StoreModule.forRoot({}), + ], + declarations: [DatafilesActionComponent], + }); + TestBed.overrideComponent(DatafilesActionComponent, { + set: { + providers: [ + { provide: UserApi, useClass: MockUserApi }, + { provide: MatDialogRef, useClass: MockMatDialogRef }, + { provide: UserApi, useValue: { jwt, getCurrentToken } }, + ], + }, + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DatafilesActionComponent); + component = fixture.componentInstance; + component.files = structuredClone(actionFiles); + component.actionConfig = actionsConfig[0]; + component.actionDataset = actionDataset; + component.maxFileSize = lowerMaxFileSizeLimit; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + /* + * Test cases + * ------------------------ + * Action , Max Size , Selected , Status + * ---------------------------------------------------------------- + * Download All , low max file size , no selected files , disabled + * Download All , low max file size , file 1 selected , disabled + * Download All , low max file size , file 2 selected , disabled + * Download All , low max file size , all files selected , disabled + * Download All , high max file size , no selected files , enabled + * Download All , high max file size , file 1 selected , enabled + * Download All , high max file size , file 2 selected , enabled + * Download All , high max file size , all files selected , enabled + * + * Download Selected , low max file size , no selected files , disabled + * Download Selected , low max file size , file 1 selected , enabled + * Download Selected , low max file size , file 2 selected , disabled + * Download Selected , low max file size , all files selected , disabled + * Download Selected , high max file size , no selected files , disabled + * Download Selected , high max file size , file 1 selected , enabled + * Download Selected , high max file size , file 2 selected , enabled + * Download Selected , high max file size , all files selected , enabled + * + * Notebook All , low max file size , no selected files , enabled + * Notebook All , low max file size , file 1 selected , enabled + * Notebook All , low max file size , file 2 selected , enabled + * Notebook All , low max file size , all files selected , enabled + * Notebook All , high max file size , no selected files , enabled + * Notebook All , high max file size , file 1 selected , enabled + * Notebook All , high max file size , file 2 selected , enabled + * Notebook All , high max file size , all files selected , enabled + * + * Notebook Selected , low max file size , no selected files , disbaled + * Notebook Selected , low max file size , file 1 selected , enabled + * Notebook Selected , low max file size , file 2 selected , enabled + * Notebook Selected , low max file size , all files selected , enabled + * Notebook Selected , high max file size , no selected files , disabled + * Notebook Selected , high max file size , file 1 selected , enabled + * Notebook Selected , high max file size , file 2 selected , enabled + * Notebook Selected , high max file size , all files selected , enabled + */ + + function selectTestCase( + action: actionSelectorType, + maxSize: maxSizeType, + selectedFiles: selectedFilesType, + ) { + component.actionConfig = actionsConfig[action]; + switch (maxSize) { + case maxSizeType.higher: + component.maxFileSize = higherMaxFileSizeLimit; + break; + case maxSizeType.lower: + default: + component.maxFileSize = lowerMaxFileSizeLimit; + break; + } + component.files = structuredClone(actionFiles); + switch (selectedFiles) { + case selectedFilesType.file1: + component.files[0].selected = true; + //component.files[1].selected = false; + break; + case selectedFilesType.file2: + //component.files[0].selected = false; + component.files[1].selected = true; + break; + case selectedFilesType.all: + component.files[0].selected = true; + component.files[1].selected = true; + break; + //case selectedFilesType.none: + //default: + //component.files[0].selected = false; + //component.files[1].selected = false; + } + fixture.detectChanges(); + } + + it("Download All should be disabled with lowest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.lower, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download All should be disabled with lowest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.lower, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download All should be disabled with lowest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.lower, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download All should be disabled with lowest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.lower, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download All should be enabled with highest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Download All should be enabled with highest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Download All should be enabled with highest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Download All should be enabled with highest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Download Selected should be disabled with lowest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.lower, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download Selected should be enabled with lowest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.lower, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Download Selected should be disabled with lowest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.lower, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download Selected should be disabled with lowest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.lower, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download Selected should be disabled with highest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.higher, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Download Selected should be enabled with highest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.higher, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Download Selected should be enabled with highest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.higher, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Download Selected should be enabled with highest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.higher, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with lowest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.lower, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with lowest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.lower, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with lowest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.lower, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with lowest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.lower, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with highest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.higher, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with highest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.higher, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with highest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.higher, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook All should be enabled with highest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.higher, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook Selected should be disabled with lowest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.lower, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Notebook Selected should be enabled with lowest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.lower, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook Selected should be enabled with lowest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.lower, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook Selected should be enabled with lowest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.lower, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook Selected should be disabled with highest max size limit and no files selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.higher, + selectedFilesType.none, + ); + + expect(component.disabled).toEqual(true); + }); + + it("Notebook Selected should be enabled with highest max size limit and file 1 selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.higher, + selectedFilesType.file1, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook Selected should be enabled with highest max size limit and file 2 selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.higher, + selectedFilesType.file2, + ); + + expect(component.disabled).toEqual(false); + }); + + it("Notebook Selected should be enabled with highest max size limit and all files selected", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.higher, + selectedFilesType.all, + ); + + expect(component.disabled).toEqual(false); + }); + + function getFakeElement(elementType: string): HTMLElement { + const element = new MockHtmlElement(elementType); + return element as unknown as HTMLElement; + } + + it("Form submission should have all files when Download All is clicked", async () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.none, + ); + spyOn(document, "createElement").and.callFake(getFakeElement); + spyOn(window, "open").and.returnValue(browserWindowMock); + + component.perform_action(); + + // eslint-disable-next-line @typescript-eslint/quotes + const formChildren = Array.from(component.form.children).map( + (item) => item as unknown as MockHtmlElement, + ); + const formFiles = formChildren.filter((item) => + item.name.includes("files"), + ); + expect(formFiles.length).toEqual(2); + }); + + it("Form submission should have correct url when Download All is clicked", async () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.none, + ); + spyOn(document, "createElement").and.callFake(getFakeElement); + spyOn(window, "open").and.returnValue(browserWindowMock); + + component.perform_action(); + + expect(component.form.action).toEqual( + actionsConfig[actionSelectorType.download_all].url, + ); + }); + + it("Form submission should have correct dataset when Download All is clicked", async () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.none, + ); + spyOn(document, "createElement").and.callFake(getFakeElement); + spyOn(window, "open").and.returnValue(browserWindowMock); + + component.perform_action(); + + // eslint-disable-next-line @typescript-eslint/quotes + const formChildren = Array.from(component.form.children).map( + (item) => item as unknown as MockHtmlElement, + ); + const formDataset = formChildren.filter((item) => + item.name.includes("dataset"), + ); + expect(formDataset.length).toEqual(1); + const datasetPid = formDataset[0].value; + expect(datasetPid).toEqual(actionDataset.pid); + }); + + it("Form submission should have correct file when Download Selected is clicked", async () => { + const selectedFile = selectedFilesType.file1; + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.higher, + selectedFile, + ); + spyOn(document, "createElement").and.callFake(getFakeElement); + spyOn(window, "open").and.returnValue(browserWindowMock); + + component.perform_action(); + + // eslint-disable-next-line @typescript-eslint/quotes + const formChildren = Array.from(component.form.children).map( + (item) => item as unknown as MockHtmlElement, + ); + const formFiles = formChildren.filter((item) => + item.name.includes("files"), + ); + expect(formFiles.length).toEqual(1); + const formFilePath = formFiles[0].value; + const selectedFilePath = actionFiles.filter( + (item) => item.path == selectedFile, + )[0].path; + expect(formFilePath).toEqual(selectedFilePath); + }); + + it("Form submission should have all files when Notebook All is clicked", async () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.higher, + selectedFilesType.none, + ); + spyOn(document, "createElement").and.callFake(getFakeElement); + spyOn(window, "open").and.returnValue(browserWindowMock); + + component.perform_action(); + + // eslint-disable-next-line @typescript-eslint/quotes + const formChildren = Array.from(component.form.children).map( + (item) => item as unknown as MockHtmlElement, + ); + const formFiles = formChildren.filter((item) => + item.name.includes("files"), + ); + expect(formFiles.length).toEqual(2); + }); + + it("Form submission should have correct url when Notebook All is clicked", async () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.higher, + selectedFilesType.none, + ); + spyOn(document, "createElement").and.callFake(getFakeElement); + spyOn(window, "open").and.returnValue(browserWindowMock); + + component.perform_action(); + + expect(component.form.action).toEqual( + actionsConfig[actionSelectorType.notebook_all].url, + ); + }); + + it("Form submission should have correct file when Notebook Selected is clicked", async () => { + const selectedFile = selectedFilesType.file2; + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.higher, + selectedFile, + ); + spyOn(document, "createElement").and.callFake(getFakeElement); + spyOn(window, "open").and.returnValue(browserWindowMock); + + component.perform_action(); + + // eslint-disable-next-line @typescript-eslint/quotes + const formChildren = Array.from(component.form.children).map( + (item) => item as unknown as MockHtmlElement, + ); + const formFiles = formChildren.filter((item) => + item.name.includes("files"), + ); + expect(formFiles.length).toEqual(1); + const formFilePath = formFiles[0].value; + const selectedFilePath = actionFiles.filter( + (item) => item.path == selectedFile, + )[0].path; + expect(formFilePath).toEqual(selectedFilePath); + }); + + it("Download All action button should contain the correct label", () => { + selectTestCase( + actionSelectorType.download_all, + maxSizeType.higher, + selectedFilesType.none, + ); + + const componentElement: HTMLElement = fixture.nativeElement; + const actionButton = componentElement.querySelector(".action-button"); + expect(actionButton.innerHTML).toContain( + actionsConfig[actionSelectorType.download_all].label, + ); + }); + + it("Download Selected action button should contain the correct label", () => { + selectTestCase( + actionSelectorType.download_selected, + maxSizeType.higher, + selectedFilesType.none, + ); + + const componentElement: HTMLElement = fixture.nativeElement; + const actionButton = componentElement.querySelector(".action-button"); + expect(actionButton.innerHTML).toContain( + actionsConfig[actionSelectorType.download_selected].label, + ); + }); + + it("Notebook All action button should contain the correct label", () => { + selectTestCase( + actionSelectorType.notebook_all, + maxSizeType.higher, + selectedFilesType.none, + ); + + const componentElement: HTMLElement = fixture.nativeElement; + const actionButton = componentElement.querySelector(".action-button"); + expect(actionButton.innerHTML).toContain( + actionsConfig[actionSelectorType.notebook_all].label, + ); + }); + + it("Notebook Selected action button should contain the correct label", () => { + selectTestCase( + actionSelectorType.notebook_selected, + maxSizeType.higher, + selectedFilesType.none, + ); + + const componentElement: HTMLElement = fixture.nativeElement; + const actionButton = componentElement.querySelector(".action-button"); + expect(actionButton.innerHTML).toContain( + actionsConfig[actionSelectorType.notebook_selected].label, + ); + }); +}); diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts index 2a123a36d..3e1dc2776 100644 --- a/src/app/shared/MockStubs.ts +++ b/src/app/shared/MockStubs.ts @@ -261,7 +261,5 @@ export class MockHtmlElement { getElementsByTagName(tag: string): any[] { return this.children.filter((x: any) => x.tag == tag.toUpperCase()); } - submit() { - return true; - } + submit() {} } From 73590ebd2e2709351ffe61209d5412f7c29cc92b Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Mon, 29 Jul 2024 17:17:16 +0200 Subject: [PATCH 10/14] fixed linting. Added documentation --- docs/configuration/datafiles-actions.md | 153 ++++++++++++++++++ .../datafiles-action.component.spec.ts | 5 - .../datafiles-actions.component.ts | 2 +- 3 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 docs/configuration/datafiles-actions.md diff --git a/docs/configuration/datafiles-actions.md b/docs/configuration/datafiles-actions.md new file mode 100644 index 000000000..7254d6f5b --- /dev/null +++ b/docs/configuration/datafiles-actions.md @@ -0,0 +1,153 @@ +--- +title: Datafiles Action Configuration +created_by: Max Novelli +created_on: 2024/07/29 +--- +# Datafiles Action Configuration + +This page describes how to configure the datafiles actions. +They are action available to users when vieweing the list of files associated to a dataset which can be viewed under the the Datafiles tab of the dataset details page. +This actions are shown as button between the page header and the table listing the files. + +There are two properties in the frontend configuration structure that control the datafiles actions: +- __datafilesActionsEnabled__ + _Type: boolean_ + This property enables the action sfeature on the datafiles page. +- __datafilesActions__ + _Type: array of ActionConfig_ + This property contains an array of action definition. Each element defines an individual action that is rendered as button. Each button can have a label or an icon or both. +- __maxDirectDownloadSize__ + _Type: numeric_ + This property specify the maximum that total size of the files included in any datafiles action can reach. The quantity is expressed in bytes + + +The type _ActionConfig_ define how the button triggring the action is rendered and what the action does. +The structure of _ActionConfig_ is the following: +- __id__ + _Type: string_ + This is a unique id for this action. It does not have any effect on the rendering nor the action. It is included for traceability, management and debugging purposes. +- __order__ + _Type: number_ + This property indicates the order in which this action will be rendered in comparison to the others. +- __label__ + _Type: string_ + This property provides the label rendered in the button. If a label is not needed, leave it empty. +- __files__ + _Type: string_ + This property indicates which files should be submitted with the requested action. + Currently on the following two values are accepted: + - _all_: all the files will be submitted with the action + - _selected_: only the selected files will be submitted with the action +- __mat_icon__ + _Type: string_ + _Optional_ + If specified, this is the name of the mat icon that shouls be rendered in the button. + If it is not present, no mat icon is shown in the button. + This icon takes precedence over the property _icon_ explained next. +- __icon__ + _Type: string_ + _Optional_ + If specified, this is the relative path to the icon file to be shown in the related button. It is shown only if property _mat_icon_ is not defined. + If not present, no icon is shown. +- __url__ + _Type: string_ + This specify the URL where the POST submission should be send. +- __target__ + _Type: string_ + Select what is the behaviour when the action is triggered, as if it should reload th epage, open a different tab or browser window. + For more information, pleae refer to the official HTML form documentation available at this URL: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#target +- __method__ + _Type: string_ + _Optional_ + _Default: POST_ + Underlying form submission method. + It is strongly suggested to leaving undefined and use the default which is POST. + If the action uses a different one, please refer to the official HTML form documentation available at this URL https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#method +- __enabled__ + _Type: string_ + _Optional_ + This property specify the condition when the action is enabled. It takes precedence over the following property _disabled_. It may can contains the following two keywords alone or combined in a logical expression. + - #SizeLimit: true if the total size of the files that will be used in the action is less or equal to the size specified in the configuration property _maxDirectDownloadSize_ + - #Selected: true if any file has been selected in the list. +- __disabled__ + _Type: string_ + _Optional_ + This property specify when the action is disabled. It is used only if the property _enabled_ is not define. Please check property _enabled_ for the possible values. +- __authorization__ + _Type: string[]_ + _Optional_ + __IMPORTANT__: This value is for future use and not yet implemented. + The intented use is to be able to enable/disable the button based on the groups the user belongs to. + +### Behavior +When the action is triggered by a click on the rendered button, a call of the defined tpye, aka POST or GET, is submitted to the URL provided together with the appropriate list of files. +The action acts like a form submission with all the following arguments: +- target: as defined in the action +- method: as defined in the action or POST if not +- action: set as th eurl provided in the action +and inputs: +- auth_token: user current token id +- jwt: a jwt token defined for the user +- dataset: dataset pid of the dataset shown +- directory: dataset source folder +- files[]: array of the selected files if action property _files_ is set to _selected_, or all files if action property _files_ is set to _all_ + + +### Examples +This example define the following 4 type of actions: +1. Download All files + It renders a button with the download icon and label _Download All_. When clicked, the action will send a POST request to the url https://download.scicat.org with the list of all files associated with the dataset. It is enabled only when the total size is lower than the max size defined in configuration. +2. Download Selected Files + It renders a button with the download icon and label _Download Selected_. When clicked, the action will send a POST request to the url https://download.scicat.org with the list of the dataset's files selected by the user. It is enabled only when the total size is lower than the max size defined in configuration and at least one file is selected. +3. Create and download a notebook which load the dataset metadata and download locally all the files + It renders a button with the Jupyter icon and label _Notebook All_. When clicked, the action will send a POST request to the url https://notebook.scicat.org with the list of all the files associated with the dataset. It is always enabled. +4. Create and download a notebook which load the dataset metadata and download locally only the selected files. + It renders a button with the Jupyter icon and label _Notebook Selected_. When clicked, the action will send a POST request to the url https://notebook.scicat.org with the list of the dataset's files selected by the user. It is enabled only when at least one file is selected. + +Configuration properties are set to the following values: +``` +datafilesActionsEnabled = true + +datafilesActions = [ + { + id: "eed8efec-4354-11ef-a3b5-d75573a5d37f", + order: 4, + label: "Download All", + files: "all", + mat_icon: "download", + url: "https://download.scicat.org", + target: "_blank", + enabled: "#SizeLimit", + }, + { + id: "3072fafc-4363-11ef-b9f9-ebf568222d26", + order: 3, + label: "Download Selected", + files: "selected", + mat_icon: "download", + url: "https://download.scicat.org", + target: "_blank", + enabled: "#Selected && #SizeLimit", + }, + { + id: "4f974f0e-4364-11ef-9c63-03d19f813f4e", + order: 2, + label: "Notebook All", + files: "all", + icon: "/assets/icons/jupyter_logo.png", + url: "https://notebook.scicat.org", + target: "_blank", + }, + { + id: "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + order: 1, + label: "Notebook Selected", + files: "selected", + icon: "/assets/icons/jupyter_logo.png", + url: "https://notebook.scicat.org", + target: "_blank", + enabled: "#Selected", + }, + ] + ``` + diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts index 4c3e4cd68..1454ec470 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts @@ -129,7 +129,6 @@ describe("DatafilesActionComponent", () => { id: "4ac45f3e-4d79-11ef-856c-6339dab93bee", }); - const browserWindowMock = { document: { write() {}, @@ -256,10 +255,6 @@ describe("DatafilesActionComponent", () => { component.files[0].selected = true; component.files[1].selected = true; break; - //case selectedFilesType.none: - //default: - //component.files[0].selected = false; - //component.files[1].selected = false; } fixture.detectChanges(); } diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts index 1e23e8e78..f449b4c35 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts @@ -34,7 +34,7 @@ export class DatafilesActionsComponent { ); } - get maxFileSize(): number { + get maxFileSize(): number { return this.appConfigService.getConfig().maxDirectDownloadSize || 0; } From 71cef162133cfac048275a45b03d4e7e4afc9128 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Mon, 29 Jul 2024 17:41:48 +0200 Subject: [PATCH 11/14] updated documentation with images --- docs/configuration/datafiles-actions.md | 13 ++++++++++++- .../datafiles_actions_file_selected.png | Bin 0 -> 24991 bytes .../datafiles_actions_no_file_selected.png | Bin 0 -> 25236 bytes 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/configuration/datafiles_actions_file_selected.png create mode 100644 docs/configuration/datafiles_actions_no_file_selected.png diff --git a/docs/configuration/datafiles-actions.md b/docs/configuration/datafiles-actions.md index 7254d6f5b..7d14263c0 100644 --- a/docs/configuration/datafiles-actions.md +++ b/docs/configuration/datafiles-actions.md @@ -149,5 +149,16 @@ datafilesActions = [ enabled: "#Selected", }, ] - ``` + +``` + +This configuration renders to the following buttons if no files are selected: +![Datafiles actions when no files are selected](./datafiles_actions_no_file_selected.png "Datafiles actions with no selected files") + +or in this other one when at least one file is selected: +![Datafiles actions when at least one files are selected](./datafiles_actions_file_selected.png "Datafiles actions with selected files") + + + + diff --git a/docs/configuration/datafiles_actions_file_selected.png b/docs/configuration/datafiles_actions_file_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..81bd42f69c1b58f4f834fae8f072f250524396bd GIT binary patch literal 24991 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 RUtCy*G_^WZcJOq}e*m4w5(WSO literal 0 HcmV?d00001 diff --git a/docs/configuration/datafiles_actions_no_file_selected.png b/docs/configuration/datafiles_actions_no_file_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..0f92986de85db92cd3be217fbc7b26f62c85941f GIT binary patch literal 25236 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
BjvjF2Hd9kh0E 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 V=rj1Y5?i+Xrf>2qLD%ul{{a2baM%C< literal 0 HcmV?d00001 From 2099d8a0cb8929078ae108a5bcc239b46a835d05 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Mon, 29 Jul 2024 17:46:11 +0200 Subject: [PATCH 12/14] trying to figure out why images are not pushed to repo --- docs/configuration/datafiles-actions.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/configuration/datafiles-actions.md b/docs/configuration/datafiles-actions.md index 7d14263c0..903870ecc 100644 --- a/docs/configuration/datafiles-actions.md +++ b/docs/configuration/datafiles-actions.md @@ -159,6 +159,4 @@ or in this other one when at least one file is selected: ![Datafiles actions when at least one files are selected](./datafiles_actions_file_selected.png "Datafiles actions with selected files") - - - +-- From eadb5755a4ec4051e962d91c7b0b9039761349c2 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Tue, 30 Jul 2024 11:11:35 +0200 Subject: [PATCH 13/14] implemented test solution suggested in review and updated documentation --- docs/configuration/datafiles-actions.md | 41 +- .../datafiles-action.component.spec.ts | 643 ++++++++---------- 2 files changed, 319 insertions(+), 365 deletions(-) diff --git a/docs/configuration/datafiles-actions.md b/docs/configuration/datafiles-actions.md index 903870ecc..4aa75b3cd 100644 --- a/docs/configuration/datafiles-actions.md +++ b/docs/configuration/datafiles-actions.md @@ -159,4 +159,43 @@ or in this other one when at least one file is selected: ![Datafiles actions when at least one files are selected](./datafiles_actions_file_selected.png "Datafiles actions with selected files") --- +### Tests +The previous examples are the settings used for the associated unit tests with the addition of the following settings: +``` +files = [ + { + path: "file1", + size: 5000, + time: "2019-09-06T13:11:37.102Z", + chk: "string", + uid: "string", + gid: "string", + perm: "string", + selected: false, + hash: "", + }, + { + path: "file2", + size: 10000, + time: "2019-09-06T13:11:37.102Z", + chk: "string", + uid: "string", + gid: "string", + perm: "string", + selected: false, + hash: "", + }, +] + +lowerMaxFileSizeLimit = 9999; +higherMaxFileSizeLimit = 20000; + +``` + +There are three group of tests: +1. testing enabling/disabling of the rendered buttons +2. testing that action and related form submissions contains the correct files and urls +3. testing that the buttons contain the correct label as specified in the configuration + + + diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts index 1454ec470..4fb215328 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts @@ -18,7 +18,7 @@ import { } from "shared/MockStubs"; import { ActionDataset } from "./datafiles-action.interfaces"; -describe("DatafilesActionComponent", () => { +describe("1000: DatafilesActionComponent", () => { let component: DatafilesActionComponent; let fixture: ComponentFixture; @@ -185,47 +185,274 @@ describe("DatafilesActionComponent", () => { }); /* - * Test cases + * Unit tests for enabled/disabled cases performed * ------------------------ - * Action , Max Size , Selected , Status - * ---------------------------------------------------------------- - * Download All , low max file size , no selected files , disabled - * Download All , low max file size , file 1 selected , disabled - * Download All , low max file size , file 2 selected , disabled - * Download All , low max file size , all files selected , disabled - * Download All , high max file size , no selected files , enabled - * Download All , high max file size , file 1 selected , enabled - * Download All , high max file size , file 2 selected , enabled - * Download All , high max file size , all files selected , enabled + * Test # , Action , Max Size , Selected , Status + * ------------------------------------------------------------------------------- + * 0010 , Download All , low max file size , no selected files , disabled + * 0020 , Download All , low max file size , file 1 selected , disabled + * 0030 , Download All , low max file size , file 2 selected , disabled + * 0040 , Download All , low max file size , all files selected , disabled + * 0050 , Download All , high max file size , no selected files , enabled + * 0060 , Download All , high max file size , file 1 selected , enabled + * 0070 , Download All , high max file size , file 2 selected , enabled + * 0080 , Download All , high max file size , all files selected , enabled * - * Download Selected , low max file size , no selected files , disabled - * Download Selected , low max file size , file 1 selected , enabled - * Download Selected , low max file size , file 2 selected , disabled - * Download Selected , low max file size , all files selected , disabled - * Download Selected , high max file size , no selected files , disabled - * Download Selected , high max file size , file 1 selected , enabled - * Download Selected , high max file size , file 2 selected , enabled - * Download Selected , high max file size , all files selected , enabled + * 0090 , Download Selected , low max file size , no selected files , disabled + * 0100 , Download Selected , low max file size , file 1 selected , enabled + * 0110 , Download Selected , low max file size , file 2 selected , disabled + * 0120 , Download Selected , low max file size , all files selected , disabled + * 0130 , Download Selected , high max file size , no selected files , disabled + * 0140 , Download Selected , high max file size , file 1 selected , enabled + * 0150 , Download Selected , high max file size , file 2 selected , enabled + * 0160 , Download Selected , high max file size , all files selected , enabled * - * Notebook All , low max file size , no selected files , enabled - * Notebook All , low max file size , file 1 selected , enabled - * Notebook All , low max file size , file 2 selected , enabled - * Notebook All , low max file size , all files selected , enabled - * Notebook All , high max file size , no selected files , enabled - * Notebook All , high max file size , file 1 selected , enabled - * Notebook All , high max file size , file 2 selected , enabled - * Notebook All , high max file size , all files selected , enabled + * 0170 , Notebook All , low max file size , no selected files , enabled + * 0180 , Notebook All , low max file size , file 1 selected , enabled + * 0190 , Notebook All , low max file size , file 2 selected , enabled + * 0200 , Notebook All , low max file size , all files selected , enabled + * 0210 , Notebook All , high max file size , no selected files , enabled + * 0220 , Notebook All , high max file size , file 1 selected , enabled + * 0230 , Notebook All , high max file size , file 2 selected , enabled + * 0240 , Notebook All , high max file size , all files selected , enabled * - * Notebook Selected , low max file size , no selected files , disbaled - * Notebook Selected , low max file size , file 1 selected , enabled - * Notebook Selected , low max file size , file 2 selected , enabled - * Notebook Selected , low max file size , all files selected , enabled - * Notebook Selected , high max file size , no selected files , disabled - * Notebook Selected , high max file size , file 1 selected , enabled - * Notebook Selected , high max file size , file 2 selected , enabled - * Notebook Selected , high max file size , all files selected , enabled + * 0250 , Notebook Selected , low max file size , no selected files , disbaled + * 0260 , Notebook Selected , low max file size , file 1 selected , enabled + * 0270 , Notebook Selected , low max file size , file 2 selected , enabled + * 0280 , Notebook Selected , low max file size , all files selected , enabled + * 0290 , Notebook Selected , high max file size , no selected files , disabled + * 0300 , Notebook Selected , high max file size , file 1 selected , enabled + * 0310 , Notebook Selected , high max file size , file 2 selected , enabled + * 0320 , Notebook Selected , high max file size , all files selected , enabled */ + const testEnabledDisabledCases = [ + { + test: "0010: Download All should be disabled with lowest max size limit and no files selected", + action: actionSelectorType.download_all, + limit: maxSizeType.lower, + selection: selectedFilesType.none, + result: true, + }, + { + test: "0020: Download All should be disabled with lowest max size limit and file 1 selected", + action: actionSelectorType.download_all, + limit: maxSizeType.lower, + selection: selectedFilesType.file1, + result: true, + }, + { + test: "0030: Download All should be disabled with lowest max size limit and file 2 selected", + action: actionSelectorType.download_all, + limit: maxSizeType.lower, + selection: selectedFilesType.file2, + result: true, + }, + { + test: "0040: Download All should be disabled with lowest max size limit and all files selected", + action: actionSelectorType.download_all, + limit: maxSizeType.lower, + selection: selectedFilesType.all, + result: true, + }, + { + test: "0050: Download All should be enabled with highest max size limit and no files selected", + action: actionSelectorType.download_all, + limit: maxSizeType.higher, + selection: selectedFilesType.none, + result: false, + }, + { + test: "0060: Download All should be enabled with highest max size limit and file 1 selected", + action: actionSelectorType.download_all, + limit: maxSizeType.higher, + selection: selectedFilesType.file1, + result: false, + }, + { + test: "0070: Download All should be enabled with highest max size limit and file 2 selected", + action: actionSelectorType.download_all, + limit: maxSizeType.higher, + selection: selectedFilesType.file2, + result: false, + }, + { + test: "0080: Download All should be enabled with highest max size limit and all files selected", + action: actionSelectorType.download_all, + limit: maxSizeType.higher, + selection: selectedFilesType.all, + result: false, + }, + { + test: "0090: Download Selected should be disabled with lowest max size limit and no files selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.none, + result: true, + }, + { + test: "0100: Download Selected should be enabled with lowest max size limit and file 1 selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.file1, + result: false, + }, + { + test: "0110: Download Selected should be disabled with lowest max size limit and file 2 selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.file2, + result: true, + }, + { + test: "0120: Download Selected should be disabled with lowest max size limit and all files selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.all, + result: true, + }, + { + test: "0130: Download Selected should be disabled with highest max size limit and no files selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.none, + result: true, + }, + { + test: "0140: Download Selected should be enabled with highest max size limit and file 1 selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.file1, + result: false, + }, + { + test: "0150: Download Selected should be enabled with highest max size limit and file 2 selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.file2, + result: false, + }, + { + test: "0160: Download Selected should be enabled with highest max size limit and all files selected", + action: actionSelectorType.download_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.all, + result: false, + }, + { + test: "0170: Notebook All should be enabled with lowest max size limit and no files selected", + action: actionSelectorType.notebook_all, + limit: maxSizeType.lower, + selection: selectedFilesType.none, + result: false, + }, + { + test: "0180: Notebook All should be enabled with lowest max size limit and file 1 selected", + action: actionSelectorType.notebook_all, + limit: maxSizeType.lower, + selection: selectedFilesType.file1, + result: false, + }, + { + test: "0190: Notebook All should be enabled with lowest max size limit and file 2 selected", + action: actionSelectorType.notebook_all, + limit: maxSizeType.lower, + selection: selectedFilesType.file2, + result: false, + }, + { + test: "0200: Notebook All should be enabled with lowest max size limit and all files selected", + action: actionSelectorType.notebook_all, + limit: maxSizeType.lower, + selection :selectedFilesType.all, + result: false, + }, + { + test: "0210: Notebook All should be enabled with highest max size limit and no files selected", + action : actionSelectorType.notebook_all, + limit: maxSizeType.higher, + selection: selectedFilesType.none, + result: false, + }, + { + test: "0220: Notebook All should be enabled with highest max size limit and file 1 selected", + action: actionSelectorType.notebook_all, + limit: maxSizeType.higher, + selection: selectedFilesType.file1, + result: false, + }, + { + test: "0230: Notebook All should be enabled with highest max size limit and file 2 selected", + action: actionSelectorType.notebook_all, + limit: maxSizeType.higher, + selection: selectedFilesType.file2, + result: false, + }, + { + test: "0240: Notebook All should be enabled with highest max size limit and all files selected", + action: actionSelectorType.notebook_all, + limit: maxSizeType.higher, + selection: selectedFilesType.all, + result: false, + }, + { + test: "0250: Notebook Selected should be disabled with lowest max size limit and no files selected", + action: actionSelectorType.notebook_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.none, + result: true, + }, + { + test: "0260: Notebook Selected should be enabled with lowest max size limit and file 1 selected", + action: actionSelectorType.notebook_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.file1, + result: false, + }, + { + test: "0270: Notebook Selected should be enabled with lowest max size limit and file 2 selected", + action: actionSelectorType.notebook_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.file2, + result: false, + }, + { + test: "0280: Notebook Selected should be enabled with lowest max size limit and all files selected", + action: actionSelectorType.notebook_selected, + limit: maxSizeType.lower, + selection: selectedFilesType.all, + result: false, + }, + { + test: "0290: Notebook Selected should be disabled with highest max size limit and no files selected", + action : actionSelectorType.notebook_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.none, + result: true, + }, + { + test: "0300: Notebook Selected should be enabled with highest max size limit and file 1 selected", + action: actionSelectorType.notebook_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.file1, + result: false, + }, + { + test: "0310: Notebook Selected should be enabled with highest max size limit and file 2 selected", + action: actionSelectorType.notebook_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.file2, + result: false, + }, + { + test: "0320: Notebook Selected should be enabled with highest max size limit and all files selected", + action: actionSelectorType.notebook_selected, + limit: maxSizeType.higher, + selection: selectedFilesType.all, + result: false, + }, + ]; + function selectTestCase( action: actionSelectorType, maxSize: maxSizeType, @@ -259,324 +486,12 @@ describe("DatafilesActionComponent", () => { fixture.detectChanges(); } - it("Download All should be disabled with lowest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.lower, - selectedFilesType.none, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download All should be disabled with lowest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.lower, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download All should be disabled with lowest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.lower, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download All should be disabled with lowest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.lower, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download All should be enabled with highest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.higher, - selectedFilesType.none, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Download All should be enabled with highest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.higher, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Download All should be enabled with highest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.higher, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Download All should be enabled with highest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.download_all, - maxSizeType.higher, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Download Selected should be disabled with lowest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.lower, - selectedFilesType.none, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download Selected should be enabled with lowest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.lower, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Download Selected should be disabled with lowest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.lower, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download Selected should be disabled with lowest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.lower, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download Selected should be disabled with highest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.higher, - selectedFilesType.none, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Download Selected should be enabled with highest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.higher, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Download Selected should be enabled with highest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.higher, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Download Selected should be enabled with highest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.download_selected, - maxSizeType.higher, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with lowest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.lower, - selectedFilesType.none, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with lowest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.lower, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with lowest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.lower, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with lowest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.lower, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with highest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.higher, - selectedFilesType.none, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with highest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.higher, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with highest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.higher, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook All should be enabled with highest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.notebook_all, - maxSizeType.higher, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook Selected should be disabled with lowest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.lower, - selectedFilesType.none, - ); - - expect(component.disabled).toEqual(true); - }); - - it("Notebook Selected should be enabled with lowest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.lower, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook Selected should be enabled with lowest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.lower, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook Selected should be enabled with lowest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.lower, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook Selected should be disabled with highest max size limit and no files selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.higher, - selectedFilesType.none, - ); + testEnabledDisabledCases.forEach((testCase) => { + it(testCase.test, () => { + selectTestCase(testCase.action, testCase.limit, testCase.selection); - expect(component.disabled).toEqual(true); - }); - - it("Notebook Selected should be enabled with highest max size limit and file 1 selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.higher, - selectedFilesType.file1, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook Selected should be enabled with highest max size limit and file 2 selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.higher, - selectedFilesType.file2, - ); - - expect(component.disabled).toEqual(false); - }); - - it("Notebook Selected should be enabled with highest max size limit and all files selected", () => { - selectTestCase( - actionSelectorType.notebook_selected, - maxSizeType.higher, - selectedFilesType.all, - ); - - expect(component.disabled).toEqual(false); + expect(component.disabled).toEqual(testCase.result); + }); }); function getFakeElement(elementType: string): HTMLElement { @@ -584,7 +499,7 @@ describe("DatafilesActionComponent", () => { return element as unknown as HTMLElement; } - it("Form submission should have all files when Download All is clicked", async () => { + it("0400: Form submission should have all files when Download All is clicked", async () => { selectTestCase( actionSelectorType.download_all, maxSizeType.higher, @@ -605,7 +520,7 @@ describe("DatafilesActionComponent", () => { expect(formFiles.length).toEqual(2); }); - it("Form submission should have correct url when Download All is clicked", async () => { + it("0410: Form submission should have correct url when Download All is clicked", async () => { selectTestCase( actionSelectorType.download_all, maxSizeType.higher, @@ -621,7 +536,7 @@ describe("DatafilesActionComponent", () => { ); }); - it("Form submission should have correct dataset when Download All is clicked", async () => { + it("0420: Form submission should have correct dataset when Download All is clicked", async () => { selectTestCase( actionSelectorType.download_all, maxSizeType.higher, @@ -644,7 +559,7 @@ describe("DatafilesActionComponent", () => { expect(datasetPid).toEqual(actionDataset.pid); }); - it("Form submission should have correct file when Download Selected is clicked", async () => { + it("0430: Form submission should have correct file when Download Selected is clicked", async () => { const selectedFile = selectedFilesType.file1; selectTestCase( actionSelectorType.download_selected, @@ -671,7 +586,7 @@ describe("DatafilesActionComponent", () => { expect(formFilePath).toEqual(selectedFilePath); }); - it("Form submission should have all files when Notebook All is clicked", async () => { + it("0440: Form submission should have all files when Notebook All is clicked", async () => { selectTestCase( actionSelectorType.notebook_all, maxSizeType.higher, @@ -692,7 +607,7 @@ describe("DatafilesActionComponent", () => { expect(formFiles.length).toEqual(2); }); - it("Form submission should have correct url when Notebook All is clicked", async () => { + it("0450: Form submission should have correct url when Notebook All is clicked", async () => { selectTestCase( actionSelectorType.notebook_all, maxSizeType.higher, @@ -708,7 +623,7 @@ describe("DatafilesActionComponent", () => { ); }); - it("Form submission should have correct file when Notebook Selected is clicked", async () => { + it("0460: Form submission should have correct file when Notebook Selected is clicked", async () => { const selectedFile = selectedFilesType.file2; selectTestCase( actionSelectorType.notebook_selected, @@ -735,7 +650,7 @@ describe("DatafilesActionComponent", () => { expect(formFilePath).toEqual(selectedFilePath); }); - it("Download All action button should contain the correct label", () => { + it("0500: Download All action button should contain the correct label", () => { selectTestCase( actionSelectorType.download_all, maxSizeType.higher, @@ -749,7 +664,7 @@ describe("DatafilesActionComponent", () => { ); }); - it("Download Selected action button should contain the correct label", () => { + it("0510: Download Selected action button should contain the correct label", () => { selectTestCase( actionSelectorType.download_selected, maxSizeType.higher, @@ -763,7 +678,7 @@ describe("DatafilesActionComponent", () => { ); }); - it("Notebook All action button should contain the correct label", () => { + it("0520: Notebook All action button should contain the correct label", () => { selectTestCase( actionSelectorType.notebook_all, maxSizeType.higher, @@ -777,7 +692,7 @@ describe("DatafilesActionComponent", () => { ); }); - it("Notebook Selected action button should contain the correct label", () => { + it("0530: Notebook Selected action button should contain the correct label", () => { selectTestCase( actionSelectorType.notebook_selected, maxSizeType.higher, From a33279e496ca0fb245e7d2e2778583c4a0347b2e Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Tue, 30 Jul 2024 11:19:42 +0200 Subject: [PATCH 14/14] fixed linting issues --- .../datafiles-actions/datafiles-action.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts index 4fb215328..90a0aa67b 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts @@ -364,12 +364,12 @@ describe("1000: DatafilesActionComponent", () => { test: "0200: Notebook All should be enabled with lowest max size limit and all files selected", action: actionSelectorType.notebook_all, limit: maxSizeType.lower, - selection :selectedFilesType.all, + selection: selectedFilesType.all, result: false, }, { test: "0210: Notebook All should be enabled with highest max size limit and no files selected", - action : actionSelectorType.notebook_all, + action: actionSelectorType.notebook_all, limit: maxSizeType.higher, selection: selectedFilesType.none, result: false, @@ -425,7 +425,7 @@ describe("1000: DatafilesActionComponent", () => { }, { test: "0290: Notebook Selected should be disabled with highest max size limit and no files selected", - action : actionSelectorType.notebook_selected, + action: actionSelectorType.notebook_selected, limit: maxSizeType.higher, selection: selectedFilesType.none, result: true,