From 267cab9fd22073a3c6334c6210b62ae601ef6cbc Mon Sep 17 00:00:00 2001 From: Igor Khokhriakov Date: Wed, 21 Aug 2024 13:11:46 +0200 Subject: [PATCH 01/18] Configurable facets (#1465) * Full width search bar (#1426) * First steps * Beautify * Use shared search bar component * Remove search bar from facets * Some clean ups * Some clean ups * Some clean ups * Code formatting * Separating full text search from shared search bar * Add search and clear buttons; some clean ups * Fix lint issue * Fix lint issue * Fix lint issue * Explicit action button search facets (#1457) * Change label * Add button * Add button * Full width search bar (#1426) * First steps * Beautify * Use shared search bar component * Remove search bar from facets * Some clean ups * Some clean ups * Some clean ups * Code formatting * Fix layout * Add functionality to the button * Fix lint issue, finally?! * Fix lint issue, finally! * Extract PID filter, WIP * Fix css; some cleanups * Further progress * Remove search icon * Add edit mode * Add/remove filter * Extract ClearableInputComponent * Add location-filter.component.ts * Add group-filter.component.ts * Add type-filter.component.ts * Add keyword-filter.component.ts * Add date-range-filter.component.ts * Add text-filter.component.ts * Show addable entities by default * Show default filters * Remove edit mode; some cleanups * Cleanup * Fix tests * build(deps-dev): bump the eslint group with 2 updates Bumps the eslint group with 2 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) and [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser). Updates `@typescript-eslint/eslint-plugin` from 7.7.0 to 7.7.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.7.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 7.7.0 to 7.7.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.7.1/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: eslint - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: eslint ... Signed-off-by: dependabot[bot] * build(deps-dev): bump cypress from 13.7.3 to 13.8.0 Bumps [cypress](https://github.com/cypress-io/cypress) from 13.7.3 to 13.8.0. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v13.7.3...v13.8.0) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * feat: Updated logo configuration (#1459) * added configuration for site logos and updated default images * fixed linting, added documentation and named images correctly * Fix tests * Introduce dialog * Click Search button after typing search query * Refactor full text search * Apply prettier * Convert onChange to reactive stream * Post merge * Fix elastic search test * Implementing Filter Settings dialog concept * Implementing Filter Settings dialog concept * Resolve post merge issues * Prettify * Prettify * Fix undefined input * Fix tests compilation * Resolve TODO: improve readability * Fix tests * Clean up unused imports * Prettify scientific conditions * Prettify scientific conditions * Prettify scientific conditions * Resolve #1466 * Remove redundant behavior * fix tests * fix tests * Fix tests * Add tests * Progress: move filters into a dedicated module; store filters state in User state * Progress: move filters into a dedicated module; store filters state in User state * Fix eslint * fix tests * make sonar happy(ier) * Replace Filter by... with Filters and Conditions * Extract filter html templates and css into separate files * Fix tests * Fix tests * Do not call fixture.destroy manually * Fix tests * Remove inheritance as it makes Angular testing unhappy * Revert "Do not call fixture.destroy manually" This reverts commit fb201ff9b6a4e85e890dca0063b9b6322d7f5964. * Add fixture.destroy * Replace static label with external function call * Remove trivial tests to avoid code duplications in case inheritance has to be removed * Fix eslint * Fix tests * Avoid @ViewChild * Try to fix test as per [SO](https://stackoverflow.com/a/66695890) * Try to fix test as per [SO](https://stackoverflow.com/a/66695890) * Revert "Remove trivial tests to avoid code duplications in case inheritance has to be removed" This reverts commit 04666e42dba4df0750778698c56079ad71f998a5. * Try to fix test as per [SO](https://stackoverflow.com/a/66695890) * Revert "Remove inheritance as it makes Angular testing unhappy" This reverts commit d75db609 * Revert "Remove inheritance as it makes Angular testing unhappy" This reverts commit d75db609 * Change Labels * Separate Filters and Conditions sections in the filter settings dialog * Remove text filter * Auto enable new condition * Allow condition editing in the filter settings dialog * Fix clear function * Fix tests --------- Signed-off-by: dependabot[bot] Co-authored-by: Max Novelli Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 12 +- .../full-text-search-bar.component.html | 10 +- .../full-text-search-bar.component.ts | 2 +- .../datasets-filter.component.html | 215 +------- .../datasets-filter.component.scss | 21 + .../datasets-filter.component.spec.ts | 474 +++--------------- .../datasets-filter.component.ts | 383 +++----------- .../datasets-filter-settings.component.html | 74 +++ .../datasets-filter-settings.component.scss | 58 +++ ...datasets-filter-settings.component.spec.ts | 183 +++++++ .../datasets-filter-settings.component.ts | 155 ++++++ src/app/datasets/datasets.module.ts | 12 + src/app/shared/MockStubs.ts | 2 +- .../filters/clearable-input.component.ts | 14 + .../filters/condition-filter.component.html | 4 + .../filters/condition-filter.component.scss | 3 + .../filters/condition-filter.component.ts | 40 ++ .../filters/date-range-filter.component.html | 19 + .../filters/date-range-filter.component.scss | 3 + .../date-range-filter.component.spec.ts | 174 +++++++ .../filters/date-range-filter.component.ts | 62 +++ .../shared/modules/filters/filters.module.ts | 80 +++ .../filters/group-filter.component.html | 29 ++ .../filters/group-filter.component.scss | 3 + .../filters/group-filter.component.spec.ts | 135 +++++ .../modules/filters/group-filter.component.ts | 60 +++ .../filters/keyword-filter.component.html | 29 ++ .../filters/keyword-filter.component.scss | 3 + .../filters/keyword-filter.component.spec.ts | 137 +++++ .../filters/keyword-filter.component.ts | 84 ++++ .../filters/location-filter.component.html | 31 ++ .../filters/location-filter.component.scss | 3 + .../filters/location-filter.component.spec.ts | 137 +++++ .../filters/location-filter.component.ts | 62 +++ .../pid-filter-contains.component.html | 9 + .../pid-filter-contains.component.scss | 3 + .../pid-filter-contains.component.spec.ts | 106 ++++ .../filters/pid-filter-contains.component.ts | 13 + .../pid-filter-startsWith.component.html | 9 + .../pid-filter-startsWith.component.scss | 3 + .../pid-filter-startsWith.component.spec.ts | 109 ++++ .../pid-filter-startsWith.component.ts | 15 + .../modules/filters/pid-filter.component.html | 9 + .../modules/filters/pid-filter.component.scss | 3 + .../filters/pid-filter.component.spec.ts | 130 +++++ .../modules/filters/pid-filter.component.ts | 65 +++ .../filters/text-filter.component.html | 10 + .../filters/text-filter.component.scss | 3 + .../filters/text-filter.component.spec.ts | 112 +++++ .../modules/filters/text-filter.component.ts | 48 ++ .../filters/type-filter.component.html | 30 ++ .../filters/type-filter.component.scss | 3 + .../filters/type-filter.component.spec.ts | 137 +++++ .../modules/filters/type-filter.component.ts | 61 +++ src/app/shared/modules/filters/utils.spec.ts | 39 ++ src/app/shared/modules/filters/utils.ts | 48 ++ .../search-parameters-dialog.component.html | 2 +- ...search-parameters-dialog.component.spec.ts | 17 +- .../search-parameters-dialog.component.ts | 24 +- src/app/shared/shared.module.ts | 2 + .../actions/datasets.actions.spec.ts | 11 +- .../actions/datasets.actions.ts | 2 +- .../state-management/actions/user.actions.ts | 14 + .../effects/datasets.effects.spec.ts | 8 +- .../reducers/datasets.reducer.spec.ts | 4 +- .../reducers/datasets.reducer.ts | 3 +- .../state-management/reducers/user.reducer.ts | 14 + .../selectors/user.selectors.spec.ts | 23 + .../selectors/user.selectors.ts | 10 + src/app/state-management/state/user.store.ts | 30 ++ 70 files changed, 2898 insertions(+), 944 deletions(-) create mode 100644 src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html create mode 100644 src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.scss create mode 100644 src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts create mode 100644 src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts create mode 100644 src/app/shared/modules/filters/clearable-input.component.ts create mode 100644 src/app/shared/modules/filters/condition-filter.component.html create mode 100644 src/app/shared/modules/filters/condition-filter.component.scss create mode 100644 src/app/shared/modules/filters/condition-filter.component.ts create mode 100644 src/app/shared/modules/filters/date-range-filter.component.html create mode 100644 src/app/shared/modules/filters/date-range-filter.component.scss create mode 100644 src/app/shared/modules/filters/date-range-filter.component.spec.ts create mode 100644 src/app/shared/modules/filters/date-range-filter.component.ts create mode 100644 src/app/shared/modules/filters/filters.module.ts create mode 100644 src/app/shared/modules/filters/group-filter.component.html create mode 100644 src/app/shared/modules/filters/group-filter.component.scss create mode 100644 src/app/shared/modules/filters/group-filter.component.spec.ts create mode 100644 src/app/shared/modules/filters/group-filter.component.ts create mode 100644 src/app/shared/modules/filters/keyword-filter.component.html create mode 100644 src/app/shared/modules/filters/keyword-filter.component.scss create mode 100644 src/app/shared/modules/filters/keyword-filter.component.spec.ts create mode 100644 src/app/shared/modules/filters/keyword-filter.component.ts create mode 100644 src/app/shared/modules/filters/location-filter.component.html create mode 100644 src/app/shared/modules/filters/location-filter.component.scss create mode 100644 src/app/shared/modules/filters/location-filter.component.spec.ts create mode 100644 src/app/shared/modules/filters/location-filter.component.ts create mode 100644 src/app/shared/modules/filters/pid-filter-contains.component.html create mode 100644 src/app/shared/modules/filters/pid-filter-contains.component.scss create mode 100644 src/app/shared/modules/filters/pid-filter-contains.component.spec.ts create mode 100644 src/app/shared/modules/filters/pid-filter-contains.component.ts create mode 100644 src/app/shared/modules/filters/pid-filter-startsWith.component.html create mode 100644 src/app/shared/modules/filters/pid-filter-startsWith.component.scss create mode 100644 src/app/shared/modules/filters/pid-filter-startsWith.component.spec.ts create mode 100644 src/app/shared/modules/filters/pid-filter-startsWith.component.ts create mode 100644 src/app/shared/modules/filters/pid-filter.component.html create mode 100644 src/app/shared/modules/filters/pid-filter.component.scss create mode 100644 src/app/shared/modules/filters/pid-filter.component.spec.ts create mode 100644 src/app/shared/modules/filters/pid-filter.component.ts create mode 100644 src/app/shared/modules/filters/text-filter.component.html create mode 100644 src/app/shared/modules/filters/text-filter.component.scss create mode 100644 src/app/shared/modules/filters/text-filter.component.spec.ts create mode 100644 src/app/shared/modules/filters/text-filter.component.ts create mode 100644 src/app/shared/modules/filters/type-filter.component.html create mode 100644 src/app/shared/modules/filters/type-filter.component.scss create mode 100644 src/app/shared/modules/filters/type-filter.component.spec.ts create mode 100644 src/app/shared/modules/filters/type-filter.component.ts create mode 100644 src/app/shared/modules/filters/utils.spec.ts create mode 100644 src/app/shared/modules/filters/utils.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31ea6b31b..223e5bca8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,9 +117,9 @@ jobs: - name: Run docker-compose run: | cp CI/ESS/e2e/docker-compose.e2e.yaml docker-compose.yaml - docker-compose pull - docker-compose build --no-cache - docker-compose up -d + docker compose pull + docker compose build --no-cache + docker compose up -d - name: Wait for Backend run: | @@ -136,13 +136,13 @@ jobs: - name: docker logs if: ${{ failure() }} run: | - docker-compose logs es01 - docker-compose logs backend + docker compose logs es01 + docker compose logs backend - name: Stop docker-compose if: ${{ !cancelled() }} run: | - docker-compose down -v + docker compose down -v - uses: actions/upload-artifact@v4 if: ${{ failure() }} diff --git a/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html b/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html index 71be7f287..f9895119c 100644 --- a/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html +++ b/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html @@ -21,15 +21,15 @@ search Search - diff --git a/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.ts b/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.ts index 4fe17d6d8..f30011865 100644 --- a/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.ts +++ b/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.ts @@ -87,7 +87,7 @@ export class FullTextSearchBarComponent implements OnInit, OnDestroy { onClear(): void { this.searchTerm = ""; this.searchTermSubject.next(undefined); - //this.searchClickSubject.next(); + this.searchClickSubject.next(); } ngOnDestroy(): void { diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.html b/src/app/datasets/datasets-filter/datasets-filter.component.html index 119eb9814..350228347 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.html +++ b/src/app/datasets/datasets-filter/datasets-filter.component.html @@ -1,200 +1,35 @@ - Filter by... + Filters and Conditions - - + + + + + + + - - Location - - {{ location || "No Location" }} - cancel - - - - - - - {{ getFacetId(fc, "No Location") }} | - {{ getFacetCount(fc) }} - - - - - - Group - - {{ group }}cancel - - - - - - - {{ getFacetId(fc, "No Group") }} | - {{ getFacetCount(fc) }} - - - - - - Type - - {{ type }}cancel - - - - - - - {{ getFacetId(fc, "No Type") }} | - {{ getFacetCount(fc) }} - - - - - - Keywords - - {{ keyword }}cancel - - - - - - {{ getFacetId(fc, "No Keywords") }} - : {{ getFacetCount(fc) }} - - - - - - Start Date - End Date - - - - - - -
- - - - - - {{ condition.lhs }} - - -  =  - - -  =  - - -  <  - - -  >  - - - {{ - condition.relation === "EQUAL_TO_STRING" - ? '"' + condition.rhs + '"' - : condition.rhs - }} - {{ condition.unit | prettyUnit }} - - cancel - - + + +
+ +
+ +
diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.scss b/src/app/datasets/datasets-filter/datasets-filter.component.scss index d3c8fb819..92dbd784e 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.scss +++ b/src/app/datasets/datasets-filter/datasets-filter.component.scss @@ -5,6 +5,15 @@ mat-card { width: 100%; } + .filter-container { + display: flex; + align-items: center; + + :first-child { + flex-grow: 1; + } + } + .section-container { font-size: 1.25rem; font-weight: 425; @@ -20,6 +29,18 @@ mat-card { margin-left: auto; } + .full-width-button { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 0; + + mat-icon { + margin-right: 8px; + } + } + .scientific-chips { ::ng-deep .mat-mdc-chip-list-wrapper { margin: 0; diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts b/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts index 5c4d78247..cfc4f6053 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts +++ b/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts @@ -11,30 +11,13 @@ import { MockStore } from "shared/MockStubs"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { FacetCount } from "state-management/state/datasets.store"; import { - setSearchTermsAction, - addLocationFilterAction, - removeLocationFilterAction, - addGroupFilterAction, - removeGroupFilterAction, - addKeywordFilterAction, - removeKeywordFilterAction, - addTypeFilterAction, - removeTypeFilterAction, clearFacetsAction, - removeScientificConditionAction, - setDateRangeFilterAction, - addScientificConditionAction, - setPidTermsAction, + fetchDatasetsAction, + fetchFacetCountsAction, } from "state-management/actions/datasets.actions"; import { of } from "rxjs"; -import { - selectColumnAction, - deselectColumnAction, - deselectAllCustomColumnsAction, -} from "state-management/actions/user.actions"; -import { ScientificCondition } from "state-management/models"; +import { deselectAllCustomColumnsAction } from "state-management/actions/user.actions"; import { SharedScicatFrontendModule } from "shared/shared.module"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatDialogModule, MatDialog } from "@angular/material/dialog"; @@ -43,25 +26,51 @@ import { MatInputModule } from "@angular/material/input"; import { MatSelectModule } from "@angular/material/select"; import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; import { AsyncPipe } from "@angular/common"; -import { DateTime } from "luxon"; -import { - MatDatepickerInputEvent, - MatDatepickerModule, -} from "@angular/material/datepicker"; +import { MatDatepickerModule } from "@angular/material/datepicker"; import { MatChipsModule } from "@angular/material/chips"; import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; import { MatCardModule } from "@angular/material/card"; import { MatButtonModule } from "@angular/material/button"; import { MatIconModule } from "@angular/material/icon"; import { AppConfigService } from "app-config.service"; +import { DatasetsFilterSettingsComponent } from "./settings/datasets-filter-settings.component"; +import { LocationFilterComponent } from "../../shared/modules/filters/location-filter.component"; +import { PidFilterComponent } from "../../shared/modules/filters/pid-filter.component"; +import { PidFilterContainsComponent } from "../../shared/modules/filters/pid-filter-contains.component"; +import { PidFilterStartsWithComponent } from "../../shared/modules/filters/pid-filter-startsWith.component"; +import { GroupFilterComponent } from "../../shared/modules/filters/group-filter.component"; +import { TypeFilterComponent } from "../../shared/modules/filters/type-filter.component"; +import { KeywordFilterComponent } from "../../shared/modules/filters/keyword-filter.component"; +import { DateRangeFilterComponent } from "../../shared/modules/filters/date-range-filter.component"; +import { TextFilterComponent } from "../../shared/modules/filters/text-filter.component"; +import { FilterConfig } from "../../shared/modules/filters/filters.module"; +import { selectFilters } from "../../state-management/selectors/user.selectors"; + +const filterConfigs: FilterConfig[] = [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + { type: "PidFilterContainsComponent", visible: false }, + { type: "PidFilterStartsWithComponent", visible: false }, + { type: "GroupFilterComponent", visible: true }, + { type: "TypeFilterComponent", visible: true }, + { type: "KeywordFilterComponent", visible: true }, + { type: "DateRangeFilterComponent", visible: true }, + { type: "TextFilterComponent", visible: true }, +]; + +export class MockStoreWithFilters extends MockStore { + public select(selector: any) { + if (selector === selectFilters) { + return of(filterConfigs); + } + return of(null); + } +} export class MockMatDialog { open() { return { - afterClosed: () => - of({ - data: { lhs: "", rhs: "", relation: "EQUAL_TO_STRING", unit: "" }, - }), + afterClosed: () => of(filterConfigs), }; } } @@ -74,7 +83,7 @@ describe("DatasetsFilterComponent", () => { let component: DatasetsFilterComponent; let fixture: ComponentFixture; - let store: MockStore; + let store: MockStoreWithFilters; let dispatchSpy; beforeEach(waitForAsync(() => { @@ -100,7 +109,10 @@ describe("DatasetsFilterComponent", () => { StoreModule.forRoot({}), ], declarations: [DatasetsFilterComponent, SearchParametersDialogComponent], - providers: [AsyncPipe], + providers: [ + AsyncPipe, + { provide: Store, useClass: MockStoreWithFilters }, + ], }); TestBed.overrideComponent(DatasetsFilterComponent, { set: { @@ -124,7 +136,7 @@ describe("DatasetsFilterComponent", () => { fixture.detectChanges(); }); - beforeEach(inject([Store], (mockStore: MockStore) => { + beforeEach(inject([Store], (mockStore: MockStoreWithFilters) => { store = mockStore; })); @@ -163,418 +175,50 @@ describe("DatasetsFilterComponent", () => { it("should contain a clear all button", () => { const compiled = fixture.debugElement.nativeElement; const btn = compiled.querySelector(".datasets-filters-clear-all-button"); - expect(btn.textContent).toContain("Clear All Filters"); + expect(btn.textContent).toContain("undo Reset"); }); it("should contain a search button", () => { const compiled = fixture.debugElement.nativeElement; const btn = compiled.querySelector(".datasets-filters-search-button"); - expect(btn.textContent).toContain("Search"); - }); - - describe("#getFacetId()", () => { - it("should return the FacetCount id if present", () => { - const facetCount: FacetCount = { - _id: "test1", - count: 0, - }; - const fallback = "test2"; - - const id = component.getFacetId(facetCount, fallback); - - expect(id).toEqual("test1"); - }); - - it("should return the FacetCount id if present", () => { - const facetCount: FacetCount = { - count: 0, - }; - const fallback = "test"; - - const id = component.getFacetId(facetCount, fallback); - - expect(id).toEqual(fallback); - }); - }); - - describe("#getFacetCount()", () => { - it("should return the FacetCount", () => { - const facetCount: FacetCount = { - count: 0, - }; - - const count = component.getFacetCount(facetCount); - - expect(count).toEqual(facetCount.count); - }); - }); - - describe("#textSearchChanged()", () => { - it("should dispatch a SetSearchTermsAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const terms = "test"; - component.textSearchChanged(terms); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith(setSearchTermsAction({ terms })); - }); - }); - - describe("#onLocationInput()", () => { - it("should call next on locationInput$", () => { - const nextSpy = spyOn(component.locationInput$, "next"); - - const event = { - target: { - value: "location", - }, - }; - - component.onLocationInput(event); - - expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); - }); - }); - - describe("#onGroupInput()", () => { - it("should call next on groupInput$", () => { - const nextSpy = spyOn(component.groupInput$, "next"); - - const event = { - target: { - value: "group", - }, - }; - - component.onGroupInput(event); - - expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); - }); - }); - - describe("#onKeywordInput()", () => { - it("should call next on keywordsInput$", () => { - const nextSpy = spyOn(component.keywordsInput$, "next"); - - const event = { - target: { - value: "keyword", - }, - }; - - component.onKeywordInput(event); - - expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); - }); - }); - - describe("#onTypeInput()", () => { - it("should call next on typeInput$", () => { - const nextSpy = spyOn(component.typeInput$, "next"); - - const event = { - target: { - value: "type", - }, - }; - - component.onTypeInput(event); - - expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); - }); - }); - - describe("#locationSelected()", () => { - it("should dispatch an AddLocationFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const location = "test"; - component.locationSelected(location); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - addLocationFilterAction({ location }), - ); - }); + expect(btn.textContent).toContain("search Apply"); }); - describe("#locationRemoved()", () => { - it("should dispatch a RemoveLocationFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const location = "test"; - component.locationRemoved(location); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeLocationFilterAction({ location }), - ); - }); - }); - - describe("#groupSelected()", () => { - it("should dispatch an AddGroupFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const group = "test"; - component.groupSelected(group); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith(addGroupFilterAction({ group })); - }); - }); - - describe("#groupRemoved()", () => { - it("should dispatch a RemoveGroupFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const group = "test"; - component.groupRemoved(group); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeGroupFilterAction({ group }), - ); - }); - }); - - describe("#keywordSelected()", () => { - it("should dispatch an AddKeywordFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const keyword = "test"; - component.keywordSelected(keyword); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - addKeywordFilterAction({ keyword }), - ); - }); - }); - - describe("#keywordRemoved()", () => { - it("should dispatch a RemoveKeywordFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const keyword = "test"; - component.keywordRemoved(keyword); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeKeywordFilterAction({ keyword }), - ); - }); - }); - - describe("#typeSelected()", () => { - it("should dispatch an AddTypeFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const datasetType = "string"; - component.typeSelected(datasetType); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - addTypeFilterAction({ datasetType }), - ); - }); - }); - - describe("#typeRemoved()", () => { - it("should dispatch a RemoveTypeFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const datasetType = "string"; - component.typeRemoved(datasetType); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeTypeFilterAction({ datasetType }), - ); - }); - }); - - describe("#dateChanged()", () => { - it("should dispatch setDateRangeFilterAction with empty string values if event.value is null", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const event = { - targetElement: { - getAttribute: (name: string) => "begin", - }, - value: null, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - setDateRangeFilterAction({ begin: "", end: "" }), - ); - }); - - it("should set dateRange.begin if event has value and event.targetElement name is begin", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const beginDate = DateTime.fromJSDate(new Date("2021-01-01")); - const event = { - targetElement: { - getAttribute: (name: string) => "begin", - }, - value: beginDate, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - const expected = beginDate.toUTC().toISO(); - expect(component.dateRange.begin).toEqual(expected); - expect(dispatchSpy).not.toHaveBeenCalled(); - }); - - it("should set dateRange.end if event has value and event.targetElement name is end", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const endDate = DateTime.fromJSDate(new Date("2021-07-08")); - const event = { - targetElement: { - getAttribute: (name: string) => "end", - }, - value: endDate, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - const expected = endDate.toUTC().plus({ days: 1 }).toISO(); - expect(component.dateRange.end).toEqual(expected); - expect(dispatchSpy).not.toHaveBeenCalled(); - }); - - it("should dispatch a setDateRangeFilterAction if dateRange.begin and dateRange.end have values", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const beginDate = DateTime.fromJSDate(new Date("2021-01-01")); - const endDate = DateTime.fromJSDate(new Date("2021-07-08")); - component.dateRange.begin = beginDate.toUTC().toISO(); - const event = { - targetElement: { - getAttribute: (name: string) => "end", - }, - value: endDate, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - const expected = { - begin: beginDate.toUTC().toISO(), - end: endDate.toUTC().plus({ days: 1 }).toISO(), - }; - expect(dispatchSpy).toHaveBeenCalledOnceWith( - setDateRangeFilterAction(expected), - ); - }); - }); - - describe("#clearFacets()", () => { + describe("#reset()", () => { it("should dispatch a ClearFacetsAction and a deselectAllCustomColumnsAction", () => { dispatchSpy = spyOn(store, "dispatch"); - component.clearFacets(); + component.reset(); - expect(dispatchSpy).toHaveBeenCalledTimes(3); + expect(dispatchSpy).toHaveBeenCalledTimes(4); expect(dispatchSpy).toHaveBeenCalledWith(clearFacetsAction()); - expect(dispatchSpy).toHaveBeenCalledWith(setPidTermsAction({ pid: "" })); expect(dispatchSpy).toHaveBeenCalledWith( deselectAllCustomColumnsAction(), ); + expect(dispatchSpy).toHaveBeenCalledWith(fetchDatasetsAction()); + expect(dispatchSpy).toHaveBeenCalledWith(fetchFacetCountsAction()); }); }); - describe("#showAddConditionDialog()", () => { - it("should open SearchParametersDialog, dispatch addScientificConditionAction and selectColumnAction if dialog returns data", () => { + describe("#showDatasetsFilterSettingsDialog()", () => { + it("should open DatasetsFilterSettingsComponent", () => { spyOn(component.dialog, "open").and.callThrough(); dispatchSpy = spyOn(store, "dispatch"); - component.metadataKeys$ = of(["test", "keys"]); - component.showAddConditionDialog(); + // component.metadataKeys$ = of(["test", "keys"]); + component.showDatasetsFilterSettingsDialog(); expect(component.dialog.open).toHaveBeenCalledTimes(1); expect(component.dialog.open).toHaveBeenCalledWith( - SearchParametersDialogComponent, + DatasetsFilterSettingsComponent, { + width: "60%", data: { - parameterKeys: component["asyncPipe"].transform( - component.metadataKeys$, - ), + filterConfigs: filterConfigs, + conditionConfigs: null, }, }, ); - expect(dispatchSpy).toHaveBeenCalledTimes(2); - expect(dispatchSpy).toHaveBeenCalledWith( - addScientificConditionAction({ - condition: { - lhs: "", - rhs: "", - relation: "EQUAL_TO_STRING", - unit: "", - }, - }), - ); - expect(dispatchSpy).toHaveBeenCalledWith( - selectColumnAction({ name: "", columnType: "custom" }), - ); - }); - }); - - describe("#removeCondition()", () => { - it("should dispatch a removeScientificConditionAction and a deselectColumnAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const condition: ScientificCondition = { - lhs: "test", - relation: "EQUAL_TO_NUMERIC", - rhs: 5, - unit: "s", - }; - const index = 0; - component.removeCondition(condition, index); - - expect(dispatchSpy).toHaveBeenCalledTimes(2); - expect(dispatchSpy).toHaveBeenCalledWith( - removeScientificConditionAction({ index }), - ); - expect(dispatchSpy).toHaveBeenCalledWith( - deselectColumnAction({ name: condition.lhs, columnType: "custom" }), - ); - }); - }); - - describe("#pidSearchChanged()", () => { - it("should dispatch a SetSearchTermsAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const pid = "1"; - component.pidSearchChanged(pid); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith(setPidTermsAction({ pid })); - }); - }); - - describe("#buildPidTermsCondition()", () => { - const tests = [ - ["", "", ""], - ["1", "startsWith", { $regex: "^1" }], - ["1", "contains", { $regex: "1" }], - ["1", "equals", "1"], - ["1", "", "1"], - ]; - tests.forEach((t, i) => { - it(`should return buildPidTermsCondition ${i}`, () => { - component.appConfig.pidSearchMethod = t[1] as string; - const condition = component["buildPidTermsCondition"](t[0] as string); - expect(condition).toEqual(t[2]); - }); }); }); }); diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.ts b/src/app/datasets/datasets-filter/datasets-filter.component.ts index 3dcb54baa..4d1f8aee7 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.ts +++ b/src/app/datasets/datasets-filter/datasets-filter.component.ts @@ -1,358 +1,137 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectorRef, Component, OnDestroy } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { Store } from "@ngrx/store"; -import { - debounceTime, - distinctUntilChanged, - skipWhile, - map, -} from "rxjs/operators"; -import { FacetCount } from "state-management/state/datasets.store"; import { - selectCreationTimeFilter, - selectGroupFacetCounts, - selectGroupFilter, selectHasAppliedFilters, - selectKeywordFacetCounts, - selectKeywordsFilter, - selectLocationFacetCounts, - selectLocationFilter, selectScientificConditions, - selectSearchTerms, - selectTypeFacetCounts, - selectTypeFilter, - selectKeywordsTerms, - selectMetadataKeys, - selectPidTerms, } from "state-management/selectors/datasets.selectors"; import { - setTextFilterAction, - addKeywordFilterAction, - setSearchTermsAction, - addLocationFilterAction, - removeLocationFilterAction, - addGroupFilterAction, - removeGroupFilterAction, - removeKeywordFilterAction, - addTypeFilterAction, - removeTypeFilterAction, - setDateRangeFilterAction, clearFacetsAction, - addScientificConditionAction, - removeScientificConditionAction, - setPidTermsAction, - setPidTermsFilterAction, fetchDatasetsAction, fetchFacetCountsAction, } from "state-management/actions/datasets.actions"; -import { combineLatest, BehaviorSubject, Observable, Subscription } from "rxjs"; +import { Subscription } from "rxjs"; import { - selectColumnAction, - deselectColumnAction, deselectAllCustomColumnsAction, + updateConditionsConfigs, + updateFilterConfigs, } from "state-management/actions/user.actions"; -import { ScientificCondition } from "state-management/models"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerInputEvent } from "@angular/material/datepicker"; -import { DateTime } from "luxon"; import { AppConfigService } from "app-config.service"; - -interface DateRange { - begin: string; - end: string; -} -enum PidTermsSearchCondition { - startsWith = "startsWith", - contains = "contains", - equals = "equals", -} +import { DatasetsFilterSettingsComponent } from "./settings/datasets-filter-settings.component"; +import { + selectConditions, + selectFilters, +} from "state-management/selectors/user.selectors"; +import { AsyncPipe } from "@angular/common"; +import { ConditionFilterComponent } from "../../shared/modules/filters/condition-filter.component"; +import { PidFilterComponent } from "../../shared/modules/filters/pid-filter.component"; +import { PidFilterContainsComponent } from "../../shared/modules/filters/pid-filter-contains.component"; +import { PidFilterStartsWithComponent } from "../../shared/modules/filters/pid-filter-startsWith.component"; +import { LocationFilterComponent } from "../../shared/modules/filters/location-filter.component"; +import { GroupFilterComponent } from "../../shared/modules/filters/group-filter.component"; +import { TypeFilterComponent } from "../../shared/modules/filters/type-filter.component"; +import { KeywordFilterComponent } from "../../shared/modules/filters/keyword-filter.component"; +import { DateRangeFilterComponent } from "../../shared/modules/filters/date-range-filter.component"; +import { TextFilterComponent } from "../../shared/modules/filters/text-filter.component"; + +const COMPONENT_MAP: { [key: string]: any } = { + PidFilterComponent: PidFilterComponent, + PidFilterContainsComponent: PidFilterContainsComponent, + PidFilterStartsWithComponent: PidFilterStartsWithComponent, + LocationFilterComponent: LocationFilterComponent, + GroupFilterComponent: GroupFilterComponent, + TypeFilterComponent: TypeFilterComponent, + KeywordFilterComponent: KeywordFilterComponent, + DateRangeFilterComponent: DateRangeFilterComponent, + TextFilterComponent: TextFilterComponent, + ConditionFilterComponent: ConditionFilterComponent, +}; @Component({ selector: "datasets-filter", templateUrl: "datasets-filter.component.html", styleUrls: ["datasets-filter.component.scss"], }) -export class DatasetsFilterComponent implements OnInit, OnDestroy { +export class DatasetsFilterComponent implements OnDestroy { private subscriptions: Subscription[] = []; - locationFacetCounts$ = this.store.select(selectLocationFacetCounts); - groupFacetCounts$ = this.store.select(selectGroupFacetCounts); - typeFacetCounts$ = this.store.select(selectTypeFacetCounts); - keywordFacetCounts$ = this.store.select(selectKeywordFacetCounts); + protected readonly ConditionFilterComponent = ConditionFilterComponent; - searchTerms$ = this.store.select(selectSearchTerms); - pidTerms$ = this.store.select(selectPidTerms); - keywordsTerms$ = this.store.select(selectKeywordsTerms); - locationFilter$ = this.store.select(selectLocationFilter); - groupFilter$ = this.store.select(selectGroupFilter); - typeFilter$ = this.store.select(selectTypeFilter); - keywordsFilter$ = this.store.select(selectKeywordsFilter); - creationTimeFilter$ = this.store.select(selectCreationTimeFilter); - scientificConditions$ = this.store.select(selectScientificConditions); - metadataKeys$ = this.store.select(selectMetadataKeys); + filterConfigs$ = this.store.select(selectFilters); - locationInput$ = new BehaviorSubject(""); - groupInput$ = new BehaviorSubject(""); - typeInput$ = new BehaviorSubject(""); - keywordsInput$ = new BehaviorSubject(""); + conditionConfigs$ = this.store.select(selectConditions); + + scientificConditions$ = this.store.select(selectScientificConditions); appConfig = this.appConfigService.getConfig(); clearSearchBar = false; - groupSuggestions$ = this.createSuggestionObserver( - this.groupFacetCounts$, - this.groupInput$, - this.groupFilter$, - ); - - locationSuggestions$ = this.createSuggestionObserver( - this.locationFacetCounts$, - this.locationInput$, - this.locationFilter$, - ); - - typeSuggestions$ = this.createSuggestionObserver( - this.typeFacetCounts$, - this.typeInput$, - this.typeFilter$, - ); - - keywordsSuggestions$ = this.createSuggestionObserver( - this.keywordFacetCounts$, - this.keywordsInput$, - this.keywordsFilter$, - ); hasAppliedFilters$ = this.store.select(selectHasAppliedFilters); - dateRange: DateRange = { - begin: "", - end: "", - }; + isInEditMode = false; constructor( public appConfigService: AppConfigService, - private asyncPipe: AsyncPipe, public dialog: MatDialog, private store: Store, + private asyncPipe: AsyncPipe, + private cdr: ChangeDetectorRef, ) {} - private buildPidTermsCondition(terms: string) { - if (!terms) return ""; - switch (this.appConfig.pidSearchMethod) { - case PidTermsSearchCondition.startsWith: { - return { $regex: `^${terms}` }; - } - case PidTermsSearchCondition.contains: { - return { $regex: terms }; - } - default: { - return terms; - } - } - } - - createSuggestionObserver( - facetCounts$: Observable, - input$: BehaviorSubject, - currentFilters$: Observable, - ): Observable { - return combineLatest([facetCounts$, input$, currentFilters$]).pipe( - map(([counts, filterString, currentFilters]) => { - if (!counts) { - return []; - } - return counts.filter( - (count) => - typeof count._id === "string" && - count._id.toLowerCase().includes(filterString.toLowerCase()) && - currentFilters.indexOf(count._id) < 0, - ); - }), - ); - } - - getFacetId(facetCount: FacetCount, fallback = ""): string { - const id = facetCount._id; - return id ? String(id) : fallback; - } - - getFacetCount(facetCount: FacetCount): number { - return facetCount.count; - } - - textSearchChanged(terms: string) { - if ("string" != typeof terms) return; - this.clearSearchBar = false; - this.store.dispatch(setSearchTermsAction({ terms })); - } - - pidSearchChanged(pid: string) { - if ("string" != typeof pid) return; - this.clearSearchBar = false; - this.store.dispatch(setPidTermsAction({ pid })); - } - - onLocationInput(event: any) { - const value = (event.target).value; - this.locationInput$.next(value); - } - - onGroupInput(event: any) { - const value = (event.target).value; - this.groupInput$.next(value); - } - - onKeywordInput(event: any) { - const value = (event.target).value; - this.keywordsInput$.next(value); - } - - onTypeInput(event: any) { - const value = (event.target).value; - this.typeInput$.next(value); - } - - locationSelected(location: string | null) { - const loc = location || ""; - this.store.dispatch(addLocationFilterAction({ location: loc })); - this.locationInput$.next(""); - } - - locationRemoved(location: string) { - this.store.dispatch(removeLocationFilterAction({ location })); - } - - groupSelected(group: string) { - this.store.dispatch(addGroupFilterAction({ group })); - this.groupInput$.next(""); - } - - groupRemoved(group: string) { - this.store.dispatch(removeGroupFilterAction({ group })); - } - - keywordSelected(keyword: string) { - this.store.dispatch(addKeywordFilterAction({ keyword })); - this.keywordsInput$.next(""); - } - - keywordRemoved(keyword: string) { - this.store.dispatch(removeKeywordFilterAction({ keyword })); - } - - typeSelected(type: string) { - this.store.dispatch(addTypeFilterAction({ datasetType: type })); - this.typeInput$.next(""); - } - - typeRemoved(type: string) { - this.store.dispatch(removeTypeFilterAction({ datasetType: type })); - } - - dateChanged(event: MatDatepickerInputEvent) { - if (event.value) { - const name = event.targetElement.getAttribute("name"); - if (name === "begin") { - this.dateRange.begin = event.value.toUTC().toISO(); - this.dateRange.end = ""; - } - if (name === "end") { - this.dateRange.end = event.value.toUTC().plus({ days: 1 }).toISO(); - } - if (this.dateRange.begin.length > 0 && this.dateRange.end.length > 0) { - this.store.dispatch(setDateRangeFilterAction(this.dateRange)); - } - } else { - this.store.dispatch(setDateRangeFilterAction({ begin: "", end: "" })); - } - } - - clearFacets() { + reset() { this.clearSearchBar = true; - this.dateRange = { - begin: "", - end: "", - }; + this.store.dispatch(clearFacetsAction()); - this.store.dispatch(setPidTermsAction({ pid: "" })); this.store.dispatch(deselectAllCustomColumnsAction()); - } + this.applyFilters(); + // we need to treat JS event loop here, otherwise this.clearSearchBar is false for the components + setTimeout(() => { + this.clearSearchBar = false; // reset value so it will be triggered again + }, 0); + } + + showDatasetsFilterSettingsDialog() { + const dialogRef = this.dialog.open(DatasetsFilterSettingsComponent, { + width: "60%", + data: { + filterConfigs: this.asyncPipe.transform(this.filterConfigs$), + conditionConfigs: this.asyncPipe.transform(this.conditionConfigs$), + }, + }); + + dialogRef.afterClosed().subscribe((result) => { + console.log("The dialog was closed"); + if (result) { + // Handle the selected filter + console.log(`Selected filter: ${result}`); + this.store.dispatch( + updateFilterConfigs({ filterConfigs: result.filterConfigs }), + ); + this.store.dispatch( + updateConditionsConfigs({ + conditionConfigs: result.conditionConfigs, + }), + ); - showAddConditionDialog() { - this.dialog - .open(SearchParametersDialogComponent, { - data: { parameterKeys: this.asyncPipe.transform(this.metadataKeys$) }, - }) - .afterClosed() - .subscribe((res) => { - if (res) { - const { data } = res; - this.store.dispatch( - addScientificConditionAction({ condition: data }), - ); - this.store.dispatch( - selectColumnAction({ name: data.lhs, columnType: "custom" }), - ); - } - }); + // this.cdr.detectChanges(); + } + }); } applyFilters() { + this.isInEditMode = false; this.store.dispatch(fetchDatasetsAction()); this.store.dispatch(fetchFacetCountsAction()); } - removeCondition(condition: ScientificCondition, index: number) { - this.store.dispatch(removeScientificConditionAction({ index })); - this.store.dispatch( - deselectColumnAction({ name: condition.lhs, columnType: "custom" }), - ); - } - - ngOnInit() { - this.subscriptions.push( - this.searchTerms$ - .pipe( - skipWhile((terms) => terms === ""), - debounceTime(500), - distinctUntilChanged(), - ) - .subscribe((terms) => { - this.store.dispatch(setTextFilterAction({ text: terms })); - }), - ); - - this.subscriptions.push( - this.keywordsTerms$ - .pipe( - skipWhile((terms) => terms === ""), - debounceTime(500), - distinctUntilChanged(), - ) - .subscribe((terms) => { - this.store.dispatch(addKeywordFilterAction({ keyword: terms })); - }), - ); - - this.subscriptions.push( - this.pidTerms$ - .pipe( - skipWhile((terms) => terms.length < 5), - debounceTime(500), - distinctUntilChanged(), - ) - .subscribe((terms) => { - const condition = this.buildPidTermsCondition(terms); - this.store.dispatch(setPidTermsFilterAction({ pid: condition })); - }), - ); - } - ngOnDestroy() { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } + + resolveComponentType(typeAsString: string): any { + return COMPONENT_MAP[typeAsString]; + } } diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html new file mode 100644 index 000000000..535a9f052 --- /dev/null +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html @@ -0,0 +1,74 @@ +

Configure Filters

+ +
Filters
+ + + + sort + + + + {{ getFilterLabel(filter.type) }} + + + +
Conditions
+ + + + + + {{ condition.condition.lhs }} + + +  =  + + +  =  + + +  <  + + +  >  + + + {{ + condition.condition.relation === "EQUAL_TO_STRING" + ? '"' + condition.condition.rhs + '"' + : condition.condition.rhs + }} + {{ condition.condition.unit | prettyUnit }} + + + edit + delete + + +
+ +
+ +
+
+ + + + diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.scss b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.scss new file mode 100644 index 000000000..49e9d5afe --- /dev/null +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.scss @@ -0,0 +1,58 @@ +mat-dialog-title { + background-color: #1976d2; /* Example: Material Indigo 500 */ + color: white; + padding: 12px 24px; + width: 60%; +} + +.filter-dialog-content { + max-height: 400px; + overflow: auto; + padding: 16px; + background-color: #f0f0f0; /* Light grey background for contrast */ +} + +mat-nav-list { + width: 100%; + max-height: 400px; + overflow: auto; +} + +mat-list-item { + display: flex; + align-items: center; /* Centers items vertically */ + justify-content: start; /* Aligns items to the start */ + padding: 10px; /* Provides padding within each list item */ + border-bottom: 1px solid #ccc; /* Adds a subtle line between items for better visual separation */ +} + + +.filter-item { + display: flex; + align-items: center; /* Align items vertically in the center */ + justify-content: space-between; /* Spread out the items to fill the horizontal space */ + padding: 8px 16px; /* Add some padding around the items */ + border-bottom: 1px solid #e0e0e0; /* Optional: adds a separator line between items */ +} + +.filter-toggle { + flex-shrink: 0; /* Prevents the toggle from shrinking */ +} + +.filter-name { + margin-left: 16px; /* Space between the toggle and the name */ + flex-grow: 1; /* Allows the name to take up any available space */ + white-space: nowrap; /* Prevents the text from wrapping */ + overflow: hidden; /* Keeps the text within the container */ + text-overflow: ellipsis; /* Adds an ellipsis if the text is too long */ +} + +.spacer { + flex-grow: 2; /* Forces any extra space to be added here, pushing the drag handle to the right */ +} + +.drag-handle { + cursor: grab; /* Changes the cursor to indicate draggable */ + margin-left: auto; + margin-right: 20px; +} diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts new file mode 100644 index 000000000..3171707b3 --- /dev/null +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts @@ -0,0 +1,183 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockMatDialogRef, MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { removeScientificConditionAction } from "state-management/actions/datasets.actions"; +import { of } from "rxjs"; +import { + deselectColumnAction, + deselectAllCustomColumnsAction, +} from "state-management/actions/user.actions"; +import { ScientificCondition } from "state-management/models"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { + MatDialogModule, + MatDialog, + MAT_DIALOG_DATA, + MatDialogRef, +} from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { AppConfigService } from "app-config.service"; +import { DatasetsFilterSettingsComponent } from "./datasets-filter-settings.component"; +import { ConditionConfig } from "../../../shared/modules/filters/filters.module"; + +export class MockMatDialog { + open() { + return { + afterClosed: () => of([]), + }; + } +} + +const getConfig = () => ({ + scienceSearchEnabled: false, +}); + +const condition: ScientificCondition = { + lhs: "test", + relation: "EQUAL_TO_NUMERIC", + rhs: 5, + unit: "s", +}; + +describe("DatasetsFilterSettingsComponent", () => { + let component: DatasetsFilterSettingsComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot({}), + ], + declarations: [ + DatasetsFilterSettingsComponent, + SearchParametersDialogComponent, + ], + providers: [AsyncPipe], + }); + TestBed.overrideComponent(DatasetsFilterSettingsComponent, { + set: { + providers: [ + { provide: AppConfigService, useValue: { getConfig } }, + { provide: MatDialog, useClass: MockMatDialog }, + { provide: MatDialogRef, useClass: MockMatDialogRef }, + { + provide: MAT_DIALOG_DATA, + useValue: { + conditionConfigs: [ + { + condition, + enabled: true, + }, + ], + }, + }, + ], + }, + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DatasetsFilterSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + it("should be created", () => { + expect(component).toBeTruthy(); + }); + + describe("#showDatasetsFilterSettingsDialog()", () => { + it("should open DatasetsFilterSettingsComponent", () => { + spyOn(component.dialog, "open").and.callThrough(); + dispatchSpy = spyOn(store, "dispatch"); + + // Spy or stub other side effects in addCondition as needed + spyOn(component, "toggleCondition").and.callFake( + (ignored: ConditionConfig) => ignored, + ); + + component.metadataKeys$ = of(["test", "keys"]); + component.addCondition(); + + expect(component.dialog.open).toHaveBeenCalledTimes(1); + expect(component.dialog.open).toHaveBeenCalledWith( + SearchParametersDialogComponent, + { + data: { + parameterKeys: ["test", "keys"], + }, + }, + ); + }); + }); + + describe("#removeCondition()", () => { + it("should dispatch a removeScientificConditionAction and a deselectColumnAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const conditionConfig: ConditionConfig = { + condition, + enabled: true, + }; + + component.removeCondition(conditionConfig, 0); + + expect(dispatchSpy).toHaveBeenCalledTimes(2); + expect(dispatchSpy).toHaveBeenCalledWith( + removeScientificConditionAction({ condition }), + ); + expect(dispatchSpy).toHaveBeenCalledWith( + deselectColumnAction({ name: condition.lhs, columnType: "custom" }), + ); + }); + }); +}); diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts new file mode 100644 index 000000000..f55af1307 --- /dev/null +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts @@ -0,0 +1,155 @@ +import { ChangeDetectorRef, Component, Inject } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogRef, +} from "@angular/material/dialog"; +import { SearchParametersDialogComponent } from "../../../shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AppConfigService } from "app-config.service"; +import { AsyncPipe } from "@angular/common"; +import { + addScientificConditionAction, + removeScientificConditionAction, +} from "../../../state-management/actions/datasets.actions"; +import { + deselectColumnAction, + selectColumnAction, +} from "../../../state-management/actions/user.actions"; +import { Store } from "@ngrx/store"; +import { selectMetadataKeys } from "../../../state-management/selectors/datasets.selectors"; +import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop"; +import { + ConditionConfig, + FilterConfig, +} from "../../../shared/modules/filters/filters.module"; +import { getFilterLabel } from "../../../shared/modules/filters/utils"; +import { ScientificCondition } from "../../../state-management/models"; + +@Component({ + selector: "app-type-datasets-filter-settings", + templateUrl: `./datasets-filter-settings.component.html`, + styleUrls: [`./datasets-filter-settings.component.scss`], +}) +export class DatasetsFilterSettingsComponent { + protected readonly getFilterLabel = getFilterLabel; + + metadataKeys$ = this.store.select(selectMetadataKeys); + + appConfig = this.appConfigService.getConfig(); + + constructor( + public dialogRef: MatDialogRef, + public dialog: MatDialog, + private store: Store, + private asyncPipe: AsyncPipe, + private appConfigService: AppConfigService, + @Inject(MAT_DIALOG_DATA) public data: any, + ) {} + + addCondition() { + this.dialog + .open(SearchParametersDialogComponent, { + data: { + parameterKeys: this.asyncPipe.transform(this.metadataKeys$), + }, + }) + .afterClosed() + .subscribe((res) => { + if (res) { + const { data } = res; + const condition = this.toggleCondition({ + condition: data, + enabled: false, + }); + this.data.conditionConfigs.push(condition); + } + }); + } + + editCondition(condition: ConditionConfig, i: number) { + this.store.dispatch( + removeScientificConditionAction({ condition: condition.condition }), + ); + this.store.dispatch( + deselectColumnAction({ + name: condition.condition.lhs, + columnType: "custom", + }), + ); + this.dialog + .open(SearchParametersDialogComponent, { + data: { + parameterKeys: this.asyncPipe.transform(this.metadataKeys$), + condition: condition.condition, + }, + }) + .afterClosed() + .subscribe((res) => { + if (res) { + const { data } = res; + this.data.conditionConfigs[i] = { + ...condition, + condition: data, + }; + this.store.dispatch( + addScientificConditionAction({ condition: data }), + ); + this.store.dispatch( + selectColumnAction({ name: data.lhs, columnType: "custom" }), + ); + } + }); + } + + removeCondition(condition: ConditionConfig, index: number) { + this.data.conditionConfigs.splice(index, 1); + if (condition.enabled) { + this.store.dispatch( + removeScientificConditionAction({ condition: condition.condition }), + ); + this.store.dispatch( + deselectColumnAction({ + name: condition.condition.lhs, + columnType: "custom", + }), + ); + } + } + + toggleCondition(condition: ConditionConfig) { + condition.enabled = !condition.enabled; + const data = condition.condition; + if (condition.enabled) { + this.store.dispatch(addScientificConditionAction({ condition: data })); + this.store.dispatch( + selectColumnAction({ name: data.lhs, columnType: "custom" }), + ); + } else { + this.store.dispatch(removeScientificConditionAction({ condition: data })); + this.store.dispatch( + deselectColumnAction({ name: data.lhs, columnType: "custom" }), + ); + } + return condition; + } + + toggleVisibility(filter: FilterConfig) { + filter.visible = !filter.visible; + } + + drop(event: CdkDragDrop): void { + moveItemInArray( + this.data.filterConfigs, + event.previousIndex, + event.currentIndex, + ); + } + + onApply() { + this.dialogRef.close(this.data); + } + + onCancel() { + this.dialogRef.close(); + } +} diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts index 72c17a0bf..d20615983 100644 --- a/src/app/datasets/datasets.module.ts +++ b/src/app/datasets/datasets.module.ts @@ -84,6 +84,11 @@ import { RelatedDatasetsComponent } from "./related-datasets/related-datasets.co 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"; +import { MatMenuModule } from "@angular/material/menu"; +import { DatasetsFilterSettingsComponent } from "./datasets-filter/settings/datasets-filter-settings.component"; +import { CdkDrag, CdkDragHandle, CdkDropList } from "@angular/cdk/drag-drop"; +import { FiltersModule } from "shared/modules/filters/filters.module"; +import { userReducer } from "state-management/reducers/user.reducer"; @NgModule({ imports: [ @@ -138,8 +143,14 @@ import { DatafilesActionComponent } from "./datafiles-actions/datafiles-action.c StoreModule.forFeature("samples", samplesReducer), StoreModule.forFeature("publishedData", publishedDataReducer), StoreModule.forFeature("logbooks", logbooksReducer), + StoreModule.forFeature("users", userReducer), LogbooksModule, FullTextSearchBarComponent, + MatMenuModule, + CdkDropList, + CdkDrag, + CdkDragHandle, + FiltersModule, ], declarations: [ BatchViewComponent, @@ -165,6 +176,7 @@ import { DatafilesActionComponent } from "./datafiles-actions/datafiles-action.c RelatedDatasetsComponent, DatafilesActionsComponent, DatafilesActionComponent, + DatasetsFilterSettingsComponent, ], providers: [ ArchivingService, diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts index 3e1dc2776..cfd964f0a 100644 --- a/src/app/shared/MockStubs.ts +++ b/src/app/shared/MockStubs.ts @@ -125,7 +125,7 @@ export class MockAppConfigService { export class MockStore { public dispatch() {} - public select() { + public select(selector) { return of([]); } diff --git a/src/app/shared/modules/filters/clearable-input.component.ts b/src/app/shared/modules/filters/clearable-input.component.ts new file mode 100644 index 000000000..f600e9ed1 --- /dev/null +++ b/src/app/shared/modules/filters/clearable-input.component.ts @@ -0,0 +1,14 @@ +import { Component, ElementRef, Input, ViewChild } from "@angular/core"; + +//TODO move to common +@Component({ template: "" }) +export class ClearableInputComponent { + @ViewChild("input", { static: true }) input!: ElementRef; + + @Input() + set clear(value: boolean) { + if (value) { + this.input.nativeElement.value = ""; + } + } +} diff --git a/src/app/shared/modules/filters/condition-filter.component.html b/src/app/shared/modules/filters/condition-filter.component.html new file mode 100644 index 000000000..4b82a5f6a --- /dev/null +++ b/src/app/shared/modules/filters/condition-filter.component.html @@ -0,0 +1,4 @@ + + Condition + + diff --git a/src/app/shared/modules/filters/condition-filter.component.scss b/src/app/shared/modules/filters/condition-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/condition-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/condition-filter.component.ts b/src/app/shared/modules/filters/condition-filter.component.ts new file mode 100644 index 000000000..c8f15a0c8 --- /dev/null +++ b/src/app/shared/modules/filters/condition-filter.component.ts @@ -0,0 +1,40 @@ +import { Component, Input } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { ScientificCondition } from "state-management/models"; + +@Component({ + selector: "app-condition-filter", + templateUrl: "condition-filter.component.html", + styleUrls: ["condition-filter.component.scss"], +}) +export class ConditionFilterComponent { + @Input() condition: ScientificCondition; + + constructor(private store: Store) {} + + formatCondition() { + const condition = this.condition; + let relationSymbol = ""; + switch (condition.relation) { + case "EQUAL_TO_NUMERIC": + case "EQUAL_TO_STRING": + relationSymbol = "="; + break; + case "LESS_THAN": + relationSymbol = "<"; + break; + case "GREATER_THAN": + relationSymbol = ">"; + break; + default: + relationSymbol = ""; + } + + const rhsValue = + condition.relation === "EQUAL_TO_STRING" + ? `"${condition.rhs}"` + : condition.rhs; + + return `${condition.lhs} ${relationSymbol} ${rhsValue} ${condition.unit}`; + } +} diff --git a/src/app/shared/modules/filters/date-range-filter.component.html b/src/app/shared/modules/filters/date-range-filter.component.html new file mode 100644 index 000000000..61a2b1679 --- /dev/null +++ b/src/app/shared/modules/filters/date-range-filter.component.html @@ -0,0 +1,19 @@ + + {{ label }} + + + + + + + diff --git a/src/app/shared/modules/filters/date-range-filter.component.scss b/src/app/shared/modules/filters/date-range-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/date-range-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/date-range-filter.component.spec.ts b/src/app/shared/modules/filters/date-range-filter.component.spec.ts new file mode 100644 index 000000000..6534d2308 --- /dev/null +++ b/src/app/shared/modules/filters/date-range-filter.component.spec.ts @@ -0,0 +1,174 @@ +import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { setDateRangeFilterAction } from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { DateTime } from "luxon"; +import { + MatDatepickerInputEvent, + MatDatepickerModule, +} from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { DateRangeFilterComponent } from "./date-range-filter.component"; + +describe("DateRangeFilterComponent", () => { + let component: DateRangeFilterComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot( + {}, + { + runtimeChecks: { + strictActionImmutability: false, + strictActionSerializability: false, + strictActionTypeUniqueness: false, + strictActionWithinNgZone: false, + strictStateImmutability: false, + strictStateSerializability: false, + }, + }, + ), + ], + declarations: [DateRangeFilterComponent, SearchParametersDialogComponent], + providers: [AsyncPipe], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DateRangeFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#dateChanged()", () => { + it("should dispatch setDateRangeFilterAction with empty string values if event.value is null", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const event = { + targetElement: { + getAttribute: (name: string) => "begin", + }, + value: null, + } as MatDatepickerInputEvent; + + component.dateChanged(event); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + setDateRangeFilterAction({ begin: "", end: "" }), + ); + }); + + it("should set dateRange.begin if event has value and event.targetElement name is begin", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const beginDate = DateTime.fromJSDate(new Date("2021-01-01")); + const event = { + targetElement: { + getAttribute: (name: string) => "begin", + }, + value: beginDate, + } as MatDatepickerInputEvent; + + component.dateChanged(event); + + const expected = beginDate.toUTC().toISO(); + expect(component.dateRange.begin).toEqual(expected); + expect(dispatchSpy).not.toHaveBeenCalled(); + }); + + it("should set dateRange.end if event has value and event.targetElement name is end", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const endDate = DateTime.fromJSDate(new Date("2021-07-08")); + const event = { + targetElement: { + getAttribute: (name: string) => "end", + }, + value: endDate, + } as MatDatepickerInputEvent; + + component.dateChanged(event); + + const expected = endDate.toUTC().plus({ days: 1 }).toISO(); + expect(component.dateRange.end).toEqual(expected); + expect(dispatchSpy).not.toHaveBeenCalled(); + }); + + it("should dispatch a setDateRangeFilterAction if dateRange.begin and dateRange.end have values", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const beginDate = DateTime.fromJSDate(new Date("2021-01-01")); + const endDate = DateTime.fromJSDate(new Date("2021-07-08")); + component.dateRange.begin = beginDate.toUTC().toISO(); + const event = { + targetElement: { + getAttribute: (name: string) => "end", + }, + value: endDate, + } as MatDatepickerInputEvent; + + component.dateChanged(event); + + const expected = { + begin: beginDate.toUTC().toISO(), + end: endDate.toUTC().plus({ days: 1 }).toISO(), + }; + expect(dispatchSpy).toHaveBeenCalledOnceWith( + setDateRangeFilterAction(expected), + ); + }); + }); +}); diff --git a/src/app/shared/modules/filters/date-range-filter.component.ts b/src/app/shared/modules/filters/date-range-filter.component.ts new file mode 100644 index 000000000..dddd4ae46 --- /dev/null +++ b/src/app/shared/modules/filters/date-range-filter.component.ts @@ -0,0 +1,62 @@ +import { Component, Input } from "@angular/core"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { MatDatepickerInputEvent } from "@angular/material/datepicker"; +import { DateTime } from "luxon"; +import { setDateRangeFilterAction } from "state-management/actions/datasets.actions"; +import { selectCreationTimeFilter } from "state-management/selectors/datasets.selectors"; +import { Store } from "@ngrx/store"; +import { getFilterLabel } from "./utils"; + +interface DateRange { + begin: string; + end: string; +} + +@Component({ + selector: "app-date-range-filter", + templateUrl: "date-range-filter.component.html", + styleUrls: ["date-range-filter.component.scss"], +}) +export class DateRangeFilterComponent extends ClearableInputComponent { + creationTimeFilter$ = this.store.select(selectCreationTimeFilter); + + dateRange: DateRange = { + begin: "", + end: "", + }; + + constructor(private store: Store) { + super(); + } + + get label() { + return getFilterLabel(this.constructor.name); + } + + dateChanged(event: MatDatepickerInputEvent) { + if (event.value) { + const name = event.targetElement.getAttribute("name"); + if (name === "begin") { + this.dateRange.begin = event.value.toUTC().toISO(); + this.dateRange.end = ""; + } + if (name === "end") { + this.dateRange.end = event.value.toUTC().plus({ days: 1 }).toISO(); + } + if (this.dateRange.begin.length > 0 && this.dateRange.end.length > 0) { + this.store.dispatch(setDateRangeFilterAction(this.dateRange)); + } + } else { + this.store.dispatch(setDateRangeFilterAction({ begin: "", end: "" })); + } + } + + @Input() + set clear(value: boolean) { + if (value) + this.dateRange = { + begin: "", + end: "", + }; + } +} diff --git a/src/app/shared/modules/filters/filters.module.ts b/src/app/shared/modules/filters/filters.module.ts new file mode 100644 index 000000000..783e0a507 --- /dev/null +++ b/src/app/shared/modules/filters/filters.module.ts @@ -0,0 +1,80 @@ +import { NgModule } from "@angular/core"; +import { PidFilterContainsComponent } from "./pid-filter-contains.component"; +import { PidFilterComponent } from "./pid-filter.component"; +import { PidFilterStartsWithComponent } from "./pid-filter-startsWith.component"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { LocationFilterComponent } from "./location-filter.component"; +import { GroupFilterComponent } from "./group-filter.component"; +import { ConditionFilterComponent } from "./condition-filter.component"; +import { TypeFilterComponent } from "./type-filter.component"; +import { TextFilterComponent } from "./text-filter.component"; +import { KeywordFilterComponent } from "./keyword-filter.component"; +import { DateRangeFilterComponent } from "./date-range-filter.component"; +import { ScientificCondition } from "state-management/models"; +import { MatInputModule } from "@angular/material/input"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { AsyncPipe, NgForOf } from "@angular/common"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatIconModule } from "@angular/material/icon"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; + +@NgModule({ + declarations: [ + ClearableInputComponent, + PidFilterComponent, + PidFilterContainsComponent, + PidFilterStartsWithComponent, + LocationFilterComponent, + GroupFilterComponent, + TypeFilterComponent, + KeywordFilterComponent, + DateRangeFilterComponent, + TextFilterComponent, + ConditionFilterComponent, + ], + imports: [ + MatInputModule, + MatDatepickerModule, + AsyncPipe, + MatChipsModule, + MatIconModule, + MatAutocompleteModule, + NgForOf, + ], + exports: [ + ClearableInputComponent, + PidFilterComponent, + PidFilterContainsComponent, + PidFilterStartsWithComponent, + LocationFilterComponent, + GroupFilterComponent, + TypeFilterComponent, + KeywordFilterComponent, + DateRangeFilterComponent, + TextFilterComponent, + ConditionFilterComponent, + ], +}) +export class FiltersModule {} + +type Filter = + | "PidFilterComponent" + | "PidFilterContainsComponent" + | "PidFilterStartsWithComponent" + | "LocationFilterComponent" + | "GroupFilterComponent" + | "TypeFilterComponent" + | "KeywordFilterComponent" + | "DateRangeFilterComponent" + | "TextFilterComponent" + | "ConditionFilterComponent"; + +export interface FilterConfig { + type: Filter; + visible: boolean; +} + +export interface ConditionConfig { + condition: ScientificCondition; + enabled: boolean; +} diff --git a/src/app/shared/modules/filters/group-filter.component.html b/src/app/shared/modules/filters/group-filter.component.html new file mode 100644 index 000000000..0f202875b --- /dev/null +++ b/src/app/shared/modules/filters/group-filter.component.html @@ -0,0 +1,29 @@ + + {{ label }} + + {{ group }}cancel + + + + + + {{ getFacetId(fc, "No Group") }} | + {{ getFacetCount(fc) }} + + + diff --git a/src/app/shared/modules/filters/group-filter.component.scss b/src/app/shared/modules/filters/group-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/group-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/group-filter.component.spec.ts b/src/app/shared/modules/filters/group-filter.component.spec.ts new file mode 100644 index 000000000..7e9604d8c --- /dev/null +++ b/src/app/shared/modules/filters/group-filter.component.spec.ts @@ -0,0 +1,135 @@ +import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { + addGroupFilterAction, + removeGroupFilterAction, +} from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { GroupFilterComponent } from "./group-filter.component"; + +describe("GroupFilterComponent", () => { + let component: GroupFilterComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot( + {}, + { + runtimeChecks: { + strictActionImmutability: false, + strictActionSerializability: false, + strictActionTypeUniqueness: false, + strictActionWithinNgZone: false, + strictStateImmutability: false, + strictStateSerializability: false, + }, + }, + ), + ], + declarations: [GroupFilterComponent, SearchParametersDialogComponent], + providers: [AsyncPipe], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#onGroupInput()", () => { + it("should call next on groupInput$", () => { + const nextSpy = spyOn(component.groupInput$, "next"); + + const event = { + target: { + value: "group", + }, + }; + + component.onGroupInput(event); + + expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); + }); + }); + + describe("#groupSelected()", () => { + it("should dispatch an AddGroupFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const group = "test"; + component.groupSelected(group); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith(addGroupFilterAction({ group })); + }); + }); + + describe("#groupRemoved()", () => { + it("should dispatch a RemoveGroupFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const group = "test"; + component.groupRemoved(group); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + removeGroupFilterAction({ group }), + ); + }); + }); +}); diff --git a/src/app/shared/modules/filters/group-filter.component.ts b/src/app/shared/modules/filters/group-filter.component.ts new file mode 100644 index 000000000..101e968f5 --- /dev/null +++ b/src/app/shared/modules/filters/group-filter.component.ts @@ -0,0 +1,60 @@ +import { Component } from "@angular/core"; +import { + selectGroupFacetCounts, + selectGroupFilter, +} from "state-management/selectors/datasets.selectors"; +import { Store } from "@ngrx/store"; +import { + addGroupFilterAction, + removeGroupFilterAction, +} from "state-management/actions/datasets.actions"; +import { + createSuggestionObserver, + getFacetCount, + getFacetId, + getFilterLabel, +} from "./utils"; +import { BehaviorSubject } from "rxjs"; +import { ClearableInputComponent } from "./clearable-input.component"; + +@Component({ + selector: "app-group-filter", + templateUrl: "group-filter.component.html", + styleUrls: ["group-filter.component.scss"], +}) +export class GroupFilterComponent extends ClearableInputComponent { + protected readonly getFacetId = getFacetId; + protected readonly getFacetCount = getFacetCount; + + groupFilter$ = this.store.select(selectGroupFilter); + + groupFacetCounts$ = this.store.select(selectGroupFacetCounts); + groupInput$ = new BehaviorSubject(""); + + groupSuggestions$ = createSuggestionObserver( + this.groupFacetCounts$, + this.groupInput$, + this.groupFilter$, + ); + + constructor(private store: Store) { + super(); + } + + get label() { + return getFilterLabel(this.constructor.name); + } + + onGroupInput(event: any) { + const value = (event.target).value; + this.groupInput$.next(value); + } + groupSelected(group: string) { + this.store.dispatch(addGroupFilterAction({ group })); + this.groupInput$.next(""); + } + + groupRemoved(group: string) { + this.store.dispatch(removeGroupFilterAction({ group })); + } +} diff --git a/src/app/shared/modules/filters/keyword-filter.component.html b/src/app/shared/modules/filters/keyword-filter.component.html new file mode 100644 index 000000000..0b8ab5580 --- /dev/null +++ b/src/app/shared/modules/filters/keyword-filter.component.html @@ -0,0 +1,29 @@ + + {{ label }} + + {{ keyword }}cancel + + + + + + {{ getFacetId(fc, "No Keywords") }} + : {{ getFacetCount(fc) }} + + + diff --git a/src/app/shared/modules/filters/keyword-filter.component.scss b/src/app/shared/modules/filters/keyword-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/keyword-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/keyword-filter.component.spec.ts b/src/app/shared/modules/filters/keyword-filter.component.spec.ts new file mode 100644 index 000000000..7dbd13b6a --- /dev/null +++ b/src/app/shared/modules/filters/keyword-filter.component.spec.ts @@ -0,0 +1,137 @@ +import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { + addKeywordFilterAction, + removeKeywordFilterAction, +} from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { KeywordFilterComponent } from "./keyword-filter.component"; + +describe("KeywordFilterComponent", () => { + let component: KeywordFilterComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot( + {}, + { + runtimeChecks: { + strictActionImmutability: false, + strictActionSerializability: false, + strictActionTypeUniqueness: false, + strictActionWithinNgZone: false, + strictStateImmutability: false, + strictStateSerializability: false, + }, + }, + ), + ], + declarations: [KeywordFilterComponent, SearchParametersDialogComponent], + providers: [AsyncPipe], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KeywordFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#onKeywordInput()", () => { + it("should call next on keywordsInput$", () => { + const nextSpy = spyOn(component.keywordsInput$, "next"); + + const event = { + target: { + value: "keyword", + }, + }; + + component.onKeywordInput(event); + + expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); + }); + }); + + describe("#keywordSelected()", () => { + it("should dispatch an AddKeywordFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const keyword = "test"; + component.keywordSelected(keyword); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + addKeywordFilterAction({ keyword }), + ); + }); + }); + + describe("#keywordRemoved()", () => { + it("should dispatch a RemoveKeywordFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const keyword = "test"; + component.keywordRemoved(keyword); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + removeKeywordFilterAction({ keyword }), + ); + }); + }); +}); diff --git a/src/app/shared/modules/filters/keyword-filter.component.ts b/src/app/shared/modules/filters/keyword-filter.component.ts new file mode 100644 index 000000000..b6faf5802 --- /dev/null +++ b/src/app/shared/modules/filters/keyword-filter.component.ts @@ -0,0 +1,84 @@ +import { Component, OnDestroy } from "@angular/core"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { + createSuggestionObserver, + getFacetCount, + getFacetId, + getFilterLabel, +} from "./utils"; +import { + selectKeywordFacetCounts, + selectKeywordsFilter, + selectKeywordsTerms, +} from "state-management/selectors/datasets.selectors"; +import { Store } from "@ngrx/store"; +import { BehaviorSubject } from "rxjs"; +import { + addKeywordFilterAction, + removeKeywordFilterAction, +} from "state-management/actions/datasets.actions"; +import { debounceTime, distinctUntilChanged, skipWhile } from "rxjs/operators"; + +@Component({ + selector: "app-keyword-filter", + templateUrl: "keyword-filter.component.html", + styleUrls: ["keyword-filter.component.scss"], +}) +export class KeywordFilterComponent + extends ClearableInputComponent + implements OnDestroy +{ + protected readonly getFacetCount = getFacetCount; + protected readonly getFacetId = getFacetId; + + keywordsTerms$ = this.store.select(selectKeywordsTerms); + + keywordsFilter$ = this.store.select(selectKeywordsFilter); + + keywordsInput$ = new BehaviorSubject(""); + keywordFacetCounts$ = this.store.select(selectKeywordFacetCounts); + + subscription = undefined; + + keywordsSuggestions$ = createSuggestionObserver( + this.keywordFacetCounts$, + this.keywordsInput$, + this.keywordsFilter$, + ); + + constructor(private store: Store) { + super(); + + this.subscription = this.keywordsTerms$ + .pipe( + skipWhile((terms) => terms === ""), + debounceTime(500), + distinctUntilChanged(), + ) + .subscribe((terms) => { + this.store.dispatch(addKeywordFilterAction({ keyword: terms })); + }); + } + + get label() { + return getFilterLabel(this.constructor.name); + } + + onKeywordInput(event: any) { + const value = (event.target).value; + this.keywordsInput$.next(value); + } + + keywordSelected(keyword: string) { + this.store.dispatch(addKeywordFilterAction({ keyword })); + this.keywordsInput$.next(""); + } + + keywordRemoved(keyword: string) { + this.store.dispatch(removeKeywordFilterAction({ keyword })); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/src/app/shared/modules/filters/location-filter.component.html b/src/app/shared/modules/filters/location-filter.component.html new file mode 100644 index 000000000..2e9b02434 --- /dev/null +++ b/src/app/shared/modules/filters/location-filter.component.html @@ -0,0 +1,31 @@ + + {{ label }} + + {{ location || "No Location" }} + cancel + + + + + + + {{ getFacetId(fc, "No Location") }} | + {{ getFacetCount(fc) }} + + + diff --git a/src/app/shared/modules/filters/location-filter.component.scss b/src/app/shared/modules/filters/location-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/location-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/location-filter.component.spec.ts b/src/app/shared/modules/filters/location-filter.component.spec.ts new file mode 100644 index 000000000..b5a0e5a52 --- /dev/null +++ b/src/app/shared/modules/filters/location-filter.component.spec.ts @@ -0,0 +1,137 @@ +import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { + addLocationFilterAction, + removeLocationFilterAction, +} from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { LocationFilterComponent } from "./location-filter.component"; + +describe("LocationFilterComponent", () => { + let component: LocationFilterComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot( + {}, + { + runtimeChecks: { + strictActionImmutability: false, + strictActionSerializability: false, + strictActionTypeUniqueness: false, + strictActionWithinNgZone: false, + strictStateImmutability: false, + strictStateSerializability: false, + }, + }, + ), + ], + declarations: [LocationFilterComponent, SearchParametersDialogComponent], + providers: [AsyncPipe], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LocationFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#onLocationInput()", () => { + it("should call next on locationInput$", () => { + const nextSpy = spyOn(component.locationInput$, "next"); + + const event = { + target: { + value: "location", + }, + }; + + component.onLocationInput(event); + + expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); + }); + }); + + describe("#locationSelected()", () => { + it("should dispatch an AddLocationFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const location = "test"; + component.locationSelected(location); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + addLocationFilterAction({ location }), + ); + }); + }); + + describe("#locationRemoved()", () => { + it("should dispatch a RemoveLocationFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const location = "test"; + component.locationRemoved(location); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + removeLocationFilterAction({ location }), + ); + }); + }); +}); diff --git a/src/app/shared/modules/filters/location-filter.component.ts b/src/app/shared/modules/filters/location-filter.component.ts new file mode 100644 index 000000000..0d6b44b8e --- /dev/null +++ b/src/app/shared/modules/filters/location-filter.component.ts @@ -0,0 +1,62 @@ +import { Component } from "@angular/core"; +import { + selectLocationFacetCounts, + selectLocationFilter, +} from "state-management/selectors/datasets.selectors"; +import { + createSuggestionObserver, + getFacetCount, + getFacetId, + getFilterLabel, +} from "./utils"; +import { BehaviorSubject } from "rxjs"; +import { + addLocationFilterAction, + removeLocationFilterAction, +} from "state-management/actions/datasets.actions"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { Store } from "@ngrx/store"; + +@Component({ + selector: "app-location-filter", + templateUrl: "location-filter.component.html", + styleUrls: ["location-filter.component.scss"], +}) +export class LocationFilterComponent extends ClearableInputComponent { + protected readonly getFacetId = getFacetId; + protected readonly getFacetCount = getFacetCount; + + locationFacetCounts$ = this.store.select(selectLocationFacetCounts); + locationFilter$ = this.store.select(selectLocationFilter); + + locationInput$ = new BehaviorSubject(""); + + locationSuggestions$ = createSuggestionObserver( + this.locationFacetCounts$, + this.locationInput$, + this.locationFilter$, + ); + + constructor(private store: Store) { + super(); + } + + get label() { + return getFilterLabel(this.constructor.name); + } + + locationSelected(location: string | null) { + const loc = location || ""; + this.store.dispatch(addLocationFilterAction({ location: loc })); + this.locationInput$.next(""); + } + + locationRemoved(location: string) { + this.store.dispatch(removeLocationFilterAction({ location })); + } + + onLocationInput(event: any) { + const value = (event.target).value; + this.locationInput$.next(value); + } +} diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.html b/src/app/shared/modules/filters/pid-filter-contains.component.html new file mode 100644 index 000000000..3cd8cdf13 --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-contains.component.html @@ -0,0 +1,9 @@ + + {{ label }} + + diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.scss b/src/app/shared/modules/filters/pid-filter-contains.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-contains.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.spec.ts b/src/app/shared/modules/filters/pid-filter-contains.component.spec.ts new file mode 100644 index 000000000..7e478c22c --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-contains.component.spec.ts @@ -0,0 +1,106 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { AppConfigService } from "app-config.service"; +import { PidFilterContainsComponent } from "./pid-filter-contains.component"; +import { PidFilterComponent } from "./pid-filter.component"; + +const getConfig = () => ({ + scienceSearchEnabled: false, +}); + +describe("PidFilterContainsComponent", () => { + let component: PidFilterContainsComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot({}), + ], + declarations: [ + PidFilterContainsComponent, + PidFilterComponent, + SearchParametersDialogComponent, + ], + providers: [AsyncPipe], + }); + TestBed.overrideComponent(PidFilterContainsComponent, { + set: { + providers: [ + { + provide: AppConfigService, + useValue: { + getConfig, + }, + }, + ], + }, + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PidFilterContainsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#buildPidTermsCondition()", () => { + const tests = [ + { input: "1", method: "contains", expected: { $regex: "1" } }, + ]; + + tests.forEach((test, index) => { + it(`should return correct condition for test case #${index + 1}`, () => { + component.appConfig.pidSearchMethod = test.method; + const condition = component.buildPidTermsCondition(test.input); + expect(condition).toEqual(test.expected); + }); + }); + }); +}); diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.ts b/src/app/shared/modules/filters/pid-filter-contains.component.ts new file mode 100644 index 000000000..bdd4e1ef8 --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-contains.component.ts @@ -0,0 +1,13 @@ +import { PidFilterComponent } from "./pid-filter.component"; +import { Component } from "@angular/core"; + +@Component({ + selector: "app-pid-contains-filter", + templateUrl: "./pid-filter-contains.component.html", + styleUrls: ["./pid-filter-contains.component.scss"], +}) +export class PidFilterContainsComponent extends PidFilterComponent { + buildPidTermsCondition(terms: string): { $regex: string } { + return { $regex: terms }; + } +} diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.html b/src/app/shared/modules/filters/pid-filter-startsWith.component.html new file mode 100644 index 000000000..3cd8cdf13 --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-startsWith.component.html @@ -0,0 +1,9 @@ + + {{ label }} + + diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.scss b/src/app/shared/modules/filters/pid-filter-startsWith.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-startsWith.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.spec.ts b/src/app/shared/modules/filters/pid-filter-startsWith.component.spec.ts new file mode 100644 index 000000000..6c80cab7f --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-startsWith.component.spec.ts @@ -0,0 +1,109 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, + fakeAsync, + tick, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { setPidTermsFilterAction } from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { AppConfigService } from "app-config.service"; +import { PidFilterComponent } from "./pid-filter.component"; +import { PidFilterStartsWithComponent } from "./pid-filter-startsWith.component"; + +const getConfig = () => ({ + scienceSearchEnabled: false, +}); + +describe("PidFilterStartsWithComponent", () => { + let component: PidFilterStartsWithComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot({}), + ], + declarations: [ + PidFilterStartsWithComponent, + PidFilterComponent, + SearchParametersDialogComponent, + ], + providers: [AsyncPipe], + }); + TestBed.overrideComponent(PidFilterStartsWithComponent, { + set: { + providers: [ + { + provide: AppConfigService, + useValue: { + getConfig, + }, + }, + ], + }, + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PidFilterStartsWithComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#buildPidTermsCondition()", () => { + const tests = [ + { input: "1", method: "startsWith", expected: { $regex: "^1" } }, + ]; + + tests.forEach((test, index) => { + it(`should return correct condition for test case #${index + 1}`, () => { + component.appConfig.pidSearchMethod = test.method; + const condition = component["buildPidTermsCondition"](test.input); + expect(condition).toEqual(test.expected); + }); + }); + }); +}); diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.ts b/src/app/shared/modules/filters/pid-filter-startsWith.component.ts new file mode 100644 index 000000000..5fbfe7321 --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter-startsWith.component.ts @@ -0,0 +1,15 @@ +import { PidFilterComponent } from "./pid-filter.component"; +import { Component } from "@angular/core"; + +@Component({ + selector: "app-pid-startsWith-filter", + templateUrl: "./pid-filter-startsWith.component.html", + styleUrls: ["./pid-filter-startsWith.component.scss"], +}) +export class PidFilterStartsWithComponent extends PidFilterComponent { + static kLabel = "PID filter (Starts With)"; + + buildPidTermsCondition(terms: string): { $regex: string } { + return { $regex: `^${terms}` }; + } +} diff --git a/src/app/shared/modules/filters/pid-filter.component.html b/src/app/shared/modules/filters/pid-filter.component.html new file mode 100644 index 000000000..3cd8cdf13 --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter.component.html @@ -0,0 +1,9 @@ + + {{ label }} + + diff --git a/src/app/shared/modules/filters/pid-filter.component.scss b/src/app/shared/modules/filters/pid-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/pid-filter.component.spec.ts b/src/app/shared/modules/filters/pid-filter.component.spec.ts new file mode 100644 index 000000000..9a847223d --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter.component.spec.ts @@ -0,0 +1,130 @@ +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, + fakeAsync, + tick, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { setPidTermsFilterAction } from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { AppConfigService } from "app-config.service"; +import { PidFilterComponent } from "./pid-filter.component"; + +const getConfig = () => ({ + scienceSearchEnabled: false, +}); + +describe("PidFilterComponent", () => { + let component: PidFilterComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot({}), + ], + declarations: [PidFilterComponent, SearchParametersDialogComponent], + providers: [AsyncPipe], + }); + TestBed.overrideComponent(PidFilterComponent, { + set: { + providers: [ + { + provide: AppConfigService, + useValue: { + getConfig, + }, + }, + ], + }, + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PidFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#onPidInput()", () => { + it("should dispatch a SetSearchTermsAction", fakeAsync(() => { + dispatchSpy = spyOn(store, "dispatch"); + + const pid = "xxxxxx"; + const event = { target: { value: pid } }; + component.onPidInput(event); + + tick(500); //wait for it + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + setPidTermsFilterAction({ pid }), + ); + })); + }); + + describe("#buildPidTermsCondition()", () => { + const tests = [ + { input: "", method: "", expected: "" }, + { input: "1", method: "equals", expected: "1" }, + { input: "1", method: "", expected: "1" }, + ]; + + tests.forEach((test, index) => { + it(`should return correct condition for test case #${index + 1}`, () => { + component.appConfig.pidSearchMethod = test.method; + const condition = component.buildPidTermsCondition(test.input); + expect(condition).toEqual(test.expected); + }); + }); + }); +}); diff --git a/src/app/shared/modules/filters/pid-filter.component.ts b/src/app/shared/modules/filters/pid-filter.component.ts new file mode 100644 index 000000000..7e28d3a8a --- /dev/null +++ b/src/app/shared/modules/filters/pid-filter.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, OnDestroy } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { Subject, Subscription } from "rxjs"; +import { + setPidTermsAction, + setPidTermsFilterAction, +} from "state-management/actions/datasets.actions"; +import { debounceTime, distinctUntilChanged, skipWhile } from "rxjs/operators"; +import { AppConfigService } from "app-config.service"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { getFilterLabel } from "./utils"; + +@Component({ + selector: "app-pid-filter", + templateUrl: `./pid-filter.component.html`, + styleUrls: [`./pid-filter.component.scss`], +}) +export class PidFilterComponent + extends ClearableInputComponent + implements OnDestroy +{ + private pidSubject = new Subject(); + private subscription: Subscription; + + appConfig = this.appConfigService.getConfig(); + + constructor( + public appConfigService: AppConfigService, + private store: Store, + ) { + super(); + this.subscription = this.pidSubject + .pipe(debounceTime(500), distinctUntilChanged()) + .subscribe((pid) => { + const condition = !pid ? "" : this.buildPidTermsCondition(pid); + this.store.dispatch(setPidTermsFilterAction({ pid: condition })); + }); + } + + get label() { + return getFilterLabel((this.constructor as typeof PidFilterComponent).name); + } + + buildPidTermsCondition(terms: string): string | { $regex: string } { + return terms; + } + + ngOnDestroy() { + // Unsubscribe to avoid memory leaks + this.subscription.unsubscribe(); + this.pidSubject.complete(); + } + + onPidInput(event: any) { + const pid = (event.target as HTMLInputElement).value; + this.pidSubject.next(pid); + } + + @Input() + set clear(value: boolean) { + super.clear = value; + + if (value) this.store.dispatch(setPidTermsAction({ pid: "" })); + } +} diff --git a/src/app/shared/modules/filters/text-filter.component.html b/src/app/shared/modules/filters/text-filter.component.html new file mode 100644 index 000000000..db628b284 --- /dev/null +++ b/src/app/shared/modules/filters/text-filter.component.html @@ -0,0 +1,10 @@ + + {{ label }} + + diff --git a/src/app/shared/modules/filters/text-filter.component.scss b/src/app/shared/modules/filters/text-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/text-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/text-filter.component.spec.ts b/src/app/shared/modules/filters/text-filter.component.spec.ts new file mode 100644 index 000000000..12416345b --- /dev/null +++ b/src/app/shared/modules/filters/text-filter.component.spec.ts @@ -0,0 +1,112 @@ +import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, + fakeAsync, + tick, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { + setSearchTermsAction, + setTextFilterAction, +} from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { TextFilterComponent } from "./text-filter.component"; + +describe("TextFilterComponent", () => { + let component: TextFilterComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot( + {}, + { + runtimeChecks: { + strictActionImmutability: false, + strictActionSerializability: false, + strictActionTypeUniqueness: false, + strictActionWithinNgZone: false, + strictStateImmutability: false, + strictStateSerializability: false, + }, + }, + ), + ], + declarations: [TextFilterComponent, SearchParametersDialogComponent], + providers: [AsyncPipe], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TextFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#textSearchChanged()", () => { + it("should dispatch a SetSearchTermsAction", fakeAsync(() => { + dispatchSpy = spyOn(store, "dispatch"); + + const terms = "test"; + const event = { target: { value: terms } }; + component.textSearchChanged(event); + + tick(500); //wait for it + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + setTextFilterAction({ text: terms }), + ); + })); + }); +}); diff --git a/src/app/shared/modules/filters/text-filter.component.ts b/src/app/shared/modules/filters/text-filter.component.ts new file mode 100644 index 000000000..fd77eb764 --- /dev/null +++ b/src/app/shared/modules/filters/text-filter.component.ts @@ -0,0 +1,48 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { Store } from "@ngrx/store"; +import { setTextFilterAction } from "state-management/actions/datasets.actions"; +import { debounceTime, distinctUntilChanged, skipWhile } from "rxjs/operators"; +import { Subject, Subscription } from "rxjs"; +import { getFilterLabel } from "./utils"; + +@Component({ + selector: "app-text-filter", + templateUrl: "text-filter.component.html", + styleUrls: ["text-filter.component.scss"], +}) +export class TextFilterComponent + extends ClearableInputComponent + implements OnDestroy +{ + private textSubject = new Subject(); + + subscription: Subscription; + + constructor(private store: Store) { + super(); + this.subscription = this.textSubject + .pipe( + skipWhile((terms) => terms === ""), + debounceTime(500), + distinctUntilChanged(), + ) + .subscribe((terms) => { + this.store.dispatch(setTextFilterAction({ text: terms })); + }); + } + + get label() { + return getFilterLabel(this.constructor.name); + } + + textSearchChanged(event: any) { + const pid = (event.target as HTMLInputElement).value; + this.textSubject.next(pid); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + this.textSubject.complete(); + } +} diff --git a/src/app/shared/modules/filters/type-filter.component.html b/src/app/shared/modules/filters/type-filter.component.html new file mode 100644 index 000000000..c9b6a1640 --- /dev/null +++ b/src/app/shared/modules/filters/type-filter.component.html @@ -0,0 +1,30 @@ + + {{ label }} + + {{ type }}cancel + + + + + + + {{ getFacetId(fc, "No Type") }} | + {{ getFacetCount(fc) }} + + + diff --git a/src/app/shared/modules/filters/type-filter.component.scss b/src/app/shared/modules/filters/type-filter.component.scss new file mode 100644 index 000000000..9c04bc08b --- /dev/null +++ b/src/app/shared/modules/filters/type-filter.component.scss @@ -0,0 +1,3 @@ +.mat-mdc-form-field { + width: 100%; +} diff --git a/src/app/shared/modules/filters/type-filter.component.spec.ts b/src/app/shared/modules/filters/type-filter.component.spec.ts new file mode 100644 index 000000000..a157ef39c --- /dev/null +++ b/src/app/shared/modules/filters/type-filter.component.spec.ts @@ -0,0 +1,137 @@ +import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; +import { + ComponentFixture, + TestBed, + inject, + waitForAsync, +} from "@angular/core/testing"; +import { Store, StoreModule } from "@ngrx/store"; +import { MockStore } from "shared/MockStubs"; + +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { + addTypeFilterAction, + removeTypeFilterAction, +} from "state-management/actions/datasets.actions"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatDialogModule, MatDialog } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; +import { AsyncPipe } from "@angular/common"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; +import { MatCardModule } from "@angular/material/card"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { TypeFilterComponent } from "./type-filter.component"; + +describe("TypeFilterComponent", () => { + let component: TypeFilterComponent; + let fixture: ComponentFixture; + + let store: MockStore; + let dispatchSpy; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [ + BrowserAnimationsModule, + FormsModule, + MatAutocompleteModule, + MatButtonModule, + MatCardModule, + MatChipsModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatNativeDateModule, + ReactiveFormsModule, + SharedScicatFrontendModule, + StoreModule.forRoot( + {}, + { + runtimeChecks: { + strictActionImmutability: false, + strictActionSerializability: false, + strictActionTypeUniqueness: false, + strictActionWithinNgZone: false, + strictStateImmutability: false, + strictStateSerializability: false, + }, + }, + ), + ], + declarations: [TypeFilterComponent, SearchParametersDialogComponent], + providers: [AsyncPipe], + }); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TypeFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(inject([Store], (mockStore: MockStore) => { + store = mockStore; + })); + + afterEach(() => { + fixture.destroy(); + }); + + describe("#onTypeInput()", () => { + it("should call next on typeInput$", () => { + const nextSpy = spyOn(component.typeInput$, "next"); + + const event = { + target: { + value: "type", + }, + }; + + component.onTypeInput(event); + + expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); + }); + }); + + describe("#typeSelected()", () => { + it("should dispatch an AddTypeFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const datasetType = "string"; + component.typeSelected(datasetType); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + addTypeFilterAction({ datasetType }), + ); + }); + }); + + describe("#typeRemoved()", () => { + it("should dispatch a RemoveTypeFilterAction", () => { + dispatchSpy = spyOn(store, "dispatch"); + + const datasetType = "string"; + component.typeRemoved(datasetType); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + removeTypeFilterAction({ datasetType }), + ); + }); + }); +}); diff --git a/src/app/shared/modules/filters/type-filter.component.ts b/src/app/shared/modules/filters/type-filter.component.ts new file mode 100644 index 000000000..cd9ee2199 --- /dev/null +++ b/src/app/shared/modules/filters/type-filter.component.ts @@ -0,0 +1,61 @@ +import { Component } from "@angular/core"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { + createSuggestionObserver, + getFacetCount, + getFacetId, + getFilterLabel, +} from "./utils"; +import { + selectTypeFacetCounts, + selectTypeFilter, +} from "state-management/selectors/datasets.selectors"; +import { Store } from "@ngrx/store"; +import { BehaviorSubject } from "rxjs"; +import { + addTypeFilterAction, + removeTypeFilterAction, +} from "state-management/actions/datasets.actions"; + +@Component({ + selector: "app-type-filter", + templateUrl: "type-filter.component.html", + styleUrls: ["type-filter.component.scss"], +}) +export class TypeFilterComponent extends ClearableInputComponent { + protected readonly getFacetCount = getFacetCount; + protected readonly getFacetId = getFacetId; + + typeFacetCounts$ = this.store.select(selectTypeFacetCounts); + + typeFilter$ = this.store.select(selectTypeFilter); + typeInput$ = new BehaviorSubject(""); + + typeSuggestions$ = createSuggestionObserver( + this.typeFacetCounts$, + this.typeInput$, + this.typeFilter$, + ); + + constructor(private store: Store) { + super(); + } + + get label() { + return getFilterLabel(this.constructor.name); + } + + onTypeInput(event: any) { + const value = (event.target).value; + this.typeInput$.next(value); + } + + typeSelected(type: string) { + this.store.dispatch(addTypeFilterAction({ datasetType: type })); + this.typeInput$.next(""); + } + + typeRemoved(type: string) { + this.store.dispatch(removeTypeFilterAction({ datasetType: type })); + } +} diff --git a/src/app/shared/modules/filters/utils.spec.ts b/src/app/shared/modules/filters/utils.spec.ts new file mode 100644 index 000000000..a241259b8 --- /dev/null +++ b/src/app/shared/modules/filters/utils.spec.ts @@ -0,0 +1,39 @@ +import { FacetCount } from "../../../state-management/state/datasets.store"; +import { getFacetCount, getFacetId } from "./utils"; + +describe("#getFacetId()", () => { + it("should return the FacetCount id if present", () => { + const facetCount: FacetCount = { + _id: "test1", + count: 0, + }; + const fallback = "test2"; + + const id = getFacetId(facetCount, fallback); + + expect(id).toEqual("test1"); + }); + + it("should return the FacetCount id if present", () => { + const facetCount: FacetCount = { + count: 0, + }; + const fallback = "test"; + + const id = getFacetId(facetCount, fallback); + + expect(id).toEqual(fallback); + }); +}); + +describe("#getFacetCount()", () => { + it("should return the FacetCount", () => { + const facetCount: FacetCount = { + count: 0, + }; + + const count = getFacetCount(facetCount); + + expect(count).toEqual(facetCount.count); + }); +}); diff --git a/src/app/shared/modules/filters/utils.ts b/src/app/shared/modules/filters/utils.ts new file mode 100644 index 000000000..bfbab0782 --- /dev/null +++ b/src/app/shared/modules/filters/utils.ts @@ -0,0 +1,48 @@ +import { BehaviorSubject, combineLatest, Observable } from "rxjs"; +import { FacetCount } from "../../../state-management/state/datasets.store"; +import { map } from "rxjs/operators"; + +export function createSuggestionObserver( + facetCounts$: Observable, + input$: BehaviorSubject, + currentFilters$: Observable, +): Observable { + return combineLatest([facetCounts$, input$, currentFilters$]).pipe( + map(([counts, filterString, currentFilters]) => { + if (!counts) { + return []; + } + return counts.filter( + (count) => + typeof count._id === "string" && + count._id.toLowerCase().includes(filterString.toLowerCase()) && + currentFilters.indexOf(count._id) < 0, + ); + }), + ); +} + +export function getFacetId(facetCount: FacetCount, fallback = ""): string { + const id = facetCount._id; + return id ? String(id) : fallback; +} + +export function getFacetCount(facetCount: FacetCount): number { + return facetCount.count; +} + +const labelMap: Map = new Map([ + ["DateRangeFilterComponent", "Start Date - End Date"], + ["GroupFilterComponent", "Group"], + ["KeywordFilterComponent", "Keyword"], + ["LocationFilterComponent", "Location"], + ["PidFilterStartsWithComponent", "PID filter (Starts With)"], + ["PidFilterComponent", "PID filter (Equals)"], + ["PidFilterContainsComponent", "PID filter (Contains)"], + ["TextFilterComponent", "Text filter"], + ["TypeFilterComponent", "Type filter"], +]); + +export function getFilterLabel(type: string): string { + return labelMap.get(type) || "Default Label"; +} diff --git a/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.html b/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.html index 9be96519c..61c2149a3 100644 --- a/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.html +++ b/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.html @@ -87,7 +87,7 @@

Add Characteristic

color="primary" [disabled]="isInvalid()" > - Add + Apply diff --git a/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.spec.ts b/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.spec.ts index 5865322f8..321d1102e 100644 --- a/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.spec.ts +++ b/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.spec.ts @@ -17,6 +17,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { AppConfigService } from "app-config.service"; import { SearchParametersDialogComponent } from "./search-parameters-dialog.component"; +import { ScientificCondition } from "../../../state-management/models"; const getConfig = () => ({ scienceSearchUnitsEnabled: true, @@ -75,7 +76,7 @@ describe("SearchParametersDialogComponent", () => { rhs: 5, unit: "gram", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); component.add(); @@ -114,7 +115,7 @@ describe("SearchParametersDialogComponent", () => { rhs: 5, unit: "", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); component.toggleUnitField(); @@ -127,7 +128,7 @@ describe("SearchParametersDialogComponent", () => { rhs: 5, unit: "", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); component.toggleUnitField(); @@ -140,7 +141,7 @@ describe("SearchParametersDialogComponent", () => { rhs: 5, unit: "", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); component.toggleUnitField(); @@ -156,7 +157,7 @@ describe("SearchParametersDialogComponent", () => { rhs: 5, unit: "", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); const isInvalid = component.isInvalid(); @@ -169,7 +170,7 @@ describe("SearchParametersDialogComponent", () => { rhs: "test", unit: "gram", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); const isInvalid = component.isInvalid(); @@ -182,7 +183,7 @@ describe("SearchParametersDialogComponent", () => { rhs: "", unit: "gram", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); const isInvalid = component.isInvalid(); @@ -195,7 +196,7 @@ describe("SearchParametersDialogComponent", () => { rhs: 5, unit: "gram", }; - component.parametersForm.setValue(formValues); + component.parametersForm.setValue(formValues as ScientificCondition); const isInvalid = component.isInvalid(); diff --git a/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.ts b/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.ts index e5af6d850..d2680dc1a 100644 --- a/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.ts +++ b/src/app/shared/modules/search-parameters-dialog/search-parameters-dialog.component.ts @@ -1,9 +1,10 @@ -import { Component, Inject } from "@angular/core"; +import { ChangeDetectorRef, Component, Inject } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; import { AppConfigService } from "app-config.service"; import { map, startWith } from "rxjs/operators"; import { UnitsService } from "shared/services/units.service"; +import { ScientificCondition } from "../../../state-management/models"; @Component({ selector: "search-parameters-dialog", @@ -17,12 +18,15 @@ export class SearchParametersDialogComponent { units: string[] = []; parametersForm = new FormGroup({ - lhs: new FormControl("", [Validators.required, Validators.minLength(2)]), - relation: new FormControl("GREATER_THAN", [ + lhs: new FormControl(this.data.condition?.lhs || "", [ + Validators.required, + Validators.minLength(2), + ]), + relation: new FormControl(this.data.condition?.relation || "GREATER_THAN", [ Validators.required, Validators.minLength(9), ]), - rhs: new FormControl("", [ + rhs: new FormControl(this.data.condition?.rhs || "", [ Validators.required, Validators.minLength(1), ]), @@ -49,10 +53,18 @@ export class SearchParametersDialogComponent { constructor( public appConfigService: AppConfigService, - @Inject(MAT_DIALOG_DATA) public data: { parameterKeys: string[] }, + @Inject(MAT_DIALOG_DATA) + public data: { + parameterKeys: string[]; + condition?: ScientificCondition; + }, public dialogRef: MatDialogRef, private unitsService: UnitsService, - ) {} + ) { + if (this.data.condition?.lhs) { + this.getUnits(this.data.condition.lhs); + } + } add = (): void => { const { lhs, relation, unit } = this.parametersForm.value; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 55d1ac1af..6ed28f27f 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -16,6 +16,7 @@ import { CommonModule } from "@angular/common"; import { SharedTableModule } from "./modules/shared-table/shared-table.module"; import { ScicatDataService } from "./services/scicat-data-service"; import { ScientificMetadataTreeModule } from "./modules/scientific-metadata-tree/scientific-metadata-tree.modules"; +import { FiltersModule } from "./modules/filters/filters.module"; @NgModule({ imports: [ BreadcrumbModule, @@ -48,6 +49,7 @@ import { ScientificMetadataTreeModule } from "./modules/scientific-metadata-tree FormsModule, SharedTableModule, ScientificMetadataTreeModule, + FiltersModule, ], }) export class SharedScicatFrontendModule {} diff --git a/src/app/state-management/actions/datasets.actions.spec.ts b/src/app/state-management/actions/datasets.actions.spec.ts index c09c38afa..9cb46268d 100644 --- a/src/app/state-management/actions/datasets.actions.spec.ts +++ b/src/app/state-management/actions/datasets.actions.spec.ts @@ -731,11 +731,16 @@ describe("Dataset Actions", () => { describe("removeScientificConditionAction", () => { it("should create an action", () => { - const index = 0; - const action = fromActions.removeScientificConditionAction({ index }); + const condition: ScientificCondition = { + lhs: "lhsTest", + relation: "LESS_THAN", + rhs: 5, + unit: "s", + }; + const action = fromActions.removeScientificConditionAction({ condition }); expect({ ...action }).toEqual({ type: "[Dataset] Remove Scientific Condition", - index, + condition, }); }); }); diff --git a/src/app/state-management/actions/datasets.actions.ts b/src/app/state-management/actions/datasets.actions.ts index 8d31223ee..5c6ce0d93 100644 --- a/src/app/state-management/actions/datasets.actions.ts +++ b/src/app/state-management/actions/datasets.actions.ts @@ -324,7 +324,7 @@ export const addScientificConditionAction = createAction( ); export const removeScientificConditionAction = createAction( "[Dataset] Remove Scientific Condition", - props<{ index: number }>(), + props<{ condition: ScientificCondition }>(), ); export const clearDatasetsStateAction = createAction("[Dataset] Clear State"); diff --git a/src/app/state-management/actions/user.actions.ts b/src/app/state-management/actions/user.actions.ts index c38083ddf..f04c262a0 100644 --- a/src/app/state-management/actions/user.actions.ts +++ b/src/app/state-management/actions/user.actions.ts @@ -2,6 +2,10 @@ import { HttpErrorResponse } from "@angular/common/http"; import { createAction, props } from "@ngrx/store"; import { User, AccessToken, UserIdentity, UserSetting } from "shared/sdk"; import { Message, Settings, TableColumn } from "state-management/models"; +import { + ConditionConfig, + FilterConfig, +} from "../../shared/modules/filters/filters.module"; export const setDatasetTableColumnsAction = createAction( "[User] Set Dataset Table Columns", @@ -160,3 +164,13 @@ export const saveSettingsAction = createAction( export const loadingAction = createAction("[User] Loading"); export const loadingCompleteAction = createAction("[User] Loading Complete"); + +export const updateFilterConfigs = createAction( + "[User] Update Filter Configs", + props<{ filterConfigs: FilterConfig[] }>(), +); + +export const updateConditionsConfigs = createAction( + "[User] Update Conditions Configs", + props<{ conditionConfigs: ConditionConfig[] }>(), +); diff --git a/src/app/state-management/effects/datasets.effects.spec.ts b/src/app/state-management/effects/datasets.effects.spec.ts index b2cef6a19..bf9563fbc 100644 --- a/src/app/state-management/effects/datasets.effects.spec.ts +++ b/src/app/state-management/effects/datasets.effects.spec.ts @@ -247,8 +247,14 @@ describe("DatasetEffects", () => { describe("ofType removeScientificConditionAction", () => { it("should result in a fetchMetadataKeysAction", () => { + const condition: ScientificCondition = { + lhs: "test", + relation: "EQUAL_TO_NUMERIC", + rhs: 1000, + unit: "s", + }; const action = fromActions.removeScientificConditionAction({ - index: 0, + condition, }); const outcome = fromActions.fetchMetadataKeysAction(); diff --git a/src/app/state-management/reducers/datasets.reducer.spec.ts b/src/app/state-management/reducers/datasets.reducer.spec.ts index 5b7fbe4ca..2185bf67d 100644 --- a/src/app/state-management/reducers/datasets.reducer.spec.ts +++ b/src/app/state-management/reducers/datasets.reducer.spec.ts @@ -526,9 +526,7 @@ describe("DatasetsReducer", () => { expect(sta.filters.scientific).toContain(condition); - const index = 0; - - const action = fromActions.removeScientificConditionAction({ index }); + const action = fromActions.removeScientificConditionAction({ condition }); const state = fromDatasets.datasetsReducer(initialDatasetState, action); expect(state.filters.scientific).not.toContain(condition); diff --git a/src/app/state-management/reducers/datasets.reducer.ts b/src/app/state-management/reducers/datasets.reducer.ts index cd09424ab..7c6783d88 100644 --- a/src/app/state-management/reducers/datasets.reducer.ts +++ b/src/app/state-management/reducers/datasets.reducer.ts @@ -462,9 +462,10 @@ const reducer = createReducer( ), on( fromActions.removeScientificConditionAction, - (state, { index }): DatasetState => { + (state, { condition }): DatasetState => { const currentFilters = state.filters; const scientific = [...currentFilters.scientific]; + const index = scientific.indexOf(condition); scientific.splice(index, 1); const filters = { ...currentFilters, scientific }; return { ...state, filters }; diff --git a/src/app/state-management/reducers/user.reducer.ts b/src/app/state-management/reducers/user.reducer.ts index 0f36625a8..13c16002a 100644 --- a/src/app/state-management/reducers/user.reducer.ts +++ b/src/app/state-management/reducers/user.reducer.ts @@ -222,6 +222,20 @@ const reducer = createReducer( isLoading: false, }), ), + on( + fromActions.updateFilterConfigs, + (state, { filterConfigs }): UserState => ({ + ...state, + filters: filterConfigs, + }), + ), + on( + fromActions.updateConditionsConfigs, + (state, { conditionConfigs }): UserState => ({ + ...state, + conditions: conditionConfigs, + }), + ), ); export const userReducer = (state: UserState | undefined, action: Action) => { diff --git a/src/app/state-management/selectors/user.selectors.spec.ts b/src/app/state-management/selectors/user.selectors.spec.ts index 254028e2b..d7f5cd503 100644 --- a/src/app/state-management/selectors/user.selectors.spec.ts +++ b/src/app/state-management/selectors/user.selectors.spec.ts @@ -3,6 +3,15 @@ import * as fromSelectors from "./user.selectors"; import { UserState } from "../state/user.store"; import { User, UserIdentity, Settings } from "../models"; import { AccessToken } from "shared/sdk"; +import { LocationFilterComponent } from "../../shared/modules/filters/location-filter.component"; +import { PidFilterComponent } from "../../shared/modules/filters/pid-filter.component"; +import { PidFilterContainsComponent } from "../../shared/modules/filters/pid-filter-contains.component"; +import { PidFilterStartsWithComponent } from "../../shared/modules/filters/pid-filter-startsWith.component"; +import { GroupFilterComponent } from "../../shared/modules/filters/group-filter.component"; +import { TypeFilterComponent } from "../../shared/modules/filters/type-filter.component"; +import { KeywordFilterComponent } from "../../shared/modules/filters/keyword-filter.component"; +import { DateRangeFilterComponent } from "../../shared/modules/filters/date-range-filter.component"; +import { TextFilterComponent } from "../../shared/modules/filters/text-filter.component"; const user = new User({ id: "testId", @@ -68,6 +77,20 @@ const initialUserState: UserState = { isLoading: false, columns: [{ name: "datasetName", order: 1, type: "standard", enabled: true }], + + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + { type: "PidFilterContainsComponent", visible: false }, + { type: "PidFilterStartsWithComponent", visible: false }, + { type: "GroupFilterComponent", visible: true }, + { type: "TypeFilterComponent", visible: true }, + { type: "KeywordFilterComponent", visible: true }, + { type: "DateRangeFilterComponent", visible: true }, + { type: "TextFilterComponent", visible: true }, + ], + + conditions: [], }; describe("User Selectors", () => { diff --git a/src/app/state-management/selectors/user.selectors.ts b/src/app/state-management/selectors/user.selectors.ts index 643057e67..f03a901c1 100644 --- a/src/app/state-management/selectors/user.selectors.ts +++ b/src/app/state-management/selectors/user.selectors.ts @@ -94,6 +94,16 @@ export const selectColumns = createSelector( (state) => state.columns, ); +export const selectFilters = createSelector( + selectUserState, + (state) => state.filters, +); + +export const selectConditions = createSelector( + selectUserState, + (state) => state.conditions, +); + export const selectSampleDialogPageViewModel = createSelector( selectCurrentUser, selectProfile, diff --git a/src/app/state-management/state/user.store.ts b/src/app/state-management/state/user.store.ts index 2de8988ab..747a7b367 100644 --- a/src/app/state-management/state/user.store.ts +++ b/src/app/state-management/state/user.store.ts @@ -1,5 +1,18 @@ import { Settings, Message, User, TableColumn } from "../models"; import { AccessToken } from "shared/sdk"; +import { + ConditionConfig, + FilterConfig, +} from "../../shared/modules/filters/filters.module"; +import { LocationFilterComponent } from "../../shared/modules/filters/location-filter.component"; +import { PidFilterComponent } from "../../shared/modules/filters/pid-filter.component"; +import { PidFilterContainsComponent } from "../../shared/modules/filters/pid-filter-contains.component"; +import { PidFilterStartsWithComponent } from "../../shared/modules/filters/pid-filter-startsWith.component"; +import { GroupFilterComponent } from "../../shared/modules/filters/group-filter.component"; +import { TypeFilterComponent } from "../../shared/modules/filters/type-filter.component"; +import { KeywordFilterComponent } from "../../shared/modules/filters/keyword-filter.component"; +import { DateRangeFilterComponent } from "../../shared/modules/filters/date-range-filter.component"; +import { TextFilterComponent } from "../../shared/modules/filters/text-filter.component"; // NOTE It IS ok to make up a state of other sub states export interface UserState { @@ -19,6 +32,10 @@ export interface UserState { isLoading: boolean; columns: TableColumn[]; + + filters: FilterConfig[]; + + conditions: ConditionConfig[]; } export const initialUserState: UserState = { @@ -49,4 +66,17 @@ export const initialUserState: UserState = { isLoading: false, columns: [], + + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + { type: "PidFilterContainsComponent", visible: false }, + { type: "PidFilterStartsWithComponent", visible: false }, + { type: "GroupFilterComponent", visible: true }, + { type: "TypeFilterComponent", visible: true }, + { type: "KeywordFilterComponent", visible: true }, + { type: "DateRangeFilterComponent", visible: true }, + ], + + conditions: [], }; From d4c3deda529624290bdaafd2e97644e78c685792 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:12:41 +0000 Subject: [PATCH 02/18] chore(deps-dev): bump axios from 1.6.8 to 1.7.4 Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.4. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.6.8...v1.7.4) --- updated-dependencies: - dependency-name: axios dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 134 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10835b9ef..0c5d08912 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4752,6 +4752,102 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.5.1.tgz", + "integrity": "sha512-q98TFI4B/9N9PmKUr1jcbtD4yAFs1HfYd9jUXXTQOlfO9SbDjnrYJgZ4Fp9rMNfrBhgIQ4x1qx0AukZccKmH9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-16.5.1.tgz", + "integrity": "sha512-j9HmL1l8k7EVJ3eOM5y8COF93gqrydpxCDoz23ZEtsY+JHY77VAiRQsmqBgEx9GGA2dXi9VEdS67B0+1vKariw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-16.5.1.tgz", + "integrity": "sha512-CXSPT01aVS869tvCCF2tZ7LnCa8l41wJ3mTVtWBkjmRde68E5Up093hklRMyXb3kfiDYlfIKWGwrV4r0eH6x1A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.5.1.tgz", + "integrity": "sha512-BhrumqJSZCWFfLFUKl4CAUwR0Y0G2H5EfFVGKivVecEQbb+INAek1aa6c89evg2/OvetQYsJ+51QknskwqvLsA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.5.1.tgz", + "integrity": "sha512-x7MsSG0W+X43WVv7JhiSq2eKvH2suNKdlUHEG09Yt0vm3z0bhtym1UCMUg3IUAK7jy9hhLeDaFVFkC6zo+H/XQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.5.1.tgz", + "integrity": "sha512-J+/v/mFjOm74I0PNtH5Ka+fDd+/dWbKhpcZ2R1/6b9agzZk+Ff/SrwJcSYFXXWKbPX+uQ4RcJoytT06Zs3s0ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nx/nx-linux-x64-gnu": { "version": "16.5.1", "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.5.1.tgz", @@ -4784,6 +4880,38 @@ "node": ">= 10" } }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.5.1.tgz", + "integrity": "sha512-qtqiLS9Y9TYyAbbpq58kRoOroko4ZXg5oWVqIWFHoxc5bGPweQSJCROEqd1AOl2ZDC6BxfuVHfhDDop1kK05WA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.5.1.tgz", + "integrity": "sha512-kUJBLakK7iyA9WfsGGQBVennA4jwf5XIgm0lu35oMOphtZIluvzItMt0EYBmylEROpmpEIhHq0P6J9FA+WH0Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@parcel/watcher": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", @@ -6691,9 +6819,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dev": true, "dependencies": { "follow-redirects": "^1.15.6", From 73346a8a1a65a1ae35c3756f5158e95eba8c232a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:21:01 +0000 Subject: [PATCH 03/18] chore(deps): bump luxon from 3.4.4 to 3.5.0 Bumps [luxon](https://github.com/moment/luxon) from 3.4.4 to 3.5.0. - [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md) - [Commits](https://github.com/moment/luxon/compare/3.4.4...3.5.0) --- updated-dependencies: - dependency-name: luxon dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0c5d08912..59b6bcb5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13545,9 +13545,9 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } From 16045f0c9c5826f532a34a56557ba257caf52f52 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Thu, 22 Aug 2024 19:11:37 +0200 Subject: [PATCH 04/18] solved form submission --- .../datafiles-action.component.ts | 62 ++++++++++++++----- .../datafiles-action.interfaces.ts | 1 + src/assets/config.json | 12 ++-- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.ts index ff0675f58..1eeae9118 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.ts @@ -32,8 +32,6 @@ export class DatafilesActionComponent implements OnInit, OnChanges { selectedTotalFileSize = 0; numberOfFileSelected = 0; - form: HTMLFormElement; - constructor(private userApi: UserApi) { this.userApi.jwt().subscribe((jwt) => { this.jwt = jwt.jwt; @@ -111,36 +109,66 @@ export class DatafilesActionComponent implements OnInit, OnChanges { } perform_action() { - this.form = document.createElement("form"); - this.form.target = this.actionConfig.target; - this.form.method = this.actionConfig.method; - this.form.action = this.actionConfig.url; + const action_type = this.actionConfig.type || "form"; + switch (action_type) { + case "form": + default: + return this.type_form(); + } + } - this.form.appendChild( + type_form() { + const form = document.createElement("form"); + form.target = this.actionConfig.target || "_self"; + form.method = this.actionConfig.method || "POST"; + form.action = this.actionConfig.url; + form.style.display = "none"; + + form.appendChild( this.add_input("auth_token", this.userApi.getCurrentToken().id), ); - this.form.appendChild(this.add_input("jwt", this.jwt)); + form.appendChild(this.add_input("jwt", this.jwt)); - this.form.appendChild(this.add_input("dataset", this.actionDataset.pid)); + form.appendChild(this.add_input("dataset", this.actionDataset.pid)); - this.form.appendChild( + form.appendChild( this.add_input("directory", this.actionDataset.sourceFolder), ); - for (const [index, item] of this.files.entries()) { + let index = 0; + for (const item of this.files) { if ( this.actionConfig.files === "all" || (this.actionConfig.files === "selected" && item.selected) ) { - this.form.appendChild( - this.add_input("files[" + index + "]", item.path), - ); + form.appendChild(this.add_input("files[" + index + "]", item.path)); + index = index + 1; } } - //document.body.appendChild(form); - this.form.submit(); - window.open("", "view"); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); + + return true; + } + + /* + * future development + * + type_fetch() { + const data = new URLSearchParams(); + for (const pair of new FormData(formElement)) { + data.append(pair[0], pair[1]); + } + + fetch(url, { + method: 'post', + body: data, + }) + .then(…); + } } + */ } diff --git a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts b/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts index 14410d950..f2f02fcbf 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts @@ -5,6 +5,7 @@ export interface ActionConfig { files: string; mat_icon?: string; icon?: string; + type?: string; url: string; target: string; authorization: string[]; diff --git a/src/assets/config.json b/src/assets/config.json index 228e405b8..759ea9507 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -140,7 +140,8 @@ "label": "Download All", "files": "all", "mat_icon": "download", - "url": "", + "type": "form", + "url": "https://www.scicat.info/download/all", "target": "_blank", "enabled": "#SizeLimit", "authorization": ["#datasetAccess", "#datasetPublic"] @@ -151,7 +152,8 @@ "label": "Download Selected", "files": "selected", "mat_icon": "download", - "url": "", + "type": "form", + "url": "https://www.scicat.info/download/selected", "target": "_blank", "enabled": "#Selected && #SizeLimit", "authorization": ["#datasetAccess", "#datasetPublic"] @@ -162,7 +164,8 @@ "label": "Notebook All", "files": "all", "icon": "/assets/icons/jupyter_logo.png", - "url": "", + "type": "form", + "url": "https://www.scicat.info/notebook/all", "target": "_blank", "authorization": ["#datasetAccess", "#datasetPublic"] }, @@ -172,7 +175,8 @@ "label": "Notebook Selected", "files": "selected", "icon": "/assets/icons/jupyter_logo.png", - "url": "", + "type": "form", + "url": "https://www.scicat.info/notebook/selected", "target": "_blank", "enabled": "#Selected", "authorization": ["#datasetAccess", "#datasetPublic"] From 8d15458b137699f737e01907b8ac2a72285944e8 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Fri, 23 Aug 2024 15:45:23 +0200 Subject: [PATCH 05/18] fixed tests --- .../datafiles-action.component.spec.ts | 80 ++++++++++++------- .../datafiles-action.component.ts | 38 ++++----- .../datafiles-actions.component.ts | 2 +- 3 files changed, 72 insertions(+), 48 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 90a0aa67b..bfb2e2ce0 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.spec.ts @@ -21,6 +21,8 @@ import { ActionDataset } from "./datafiles-action.interfaces"; describe("1000: DatafilesActionComponent", () => { let component: DatafilesActionComponent; let fixture: ComponentFixture; + let htmlForm: HTMLFormElement; + let htmlInput: HTMLInputElement; const actionsConfig = [ { @@ -129,14 +131,20 @@ describe("1000: DatafilesActionComponent", () => { id: "4ac45f3e-4d79-11ef-856c-6339dab93bee", }); - const browserWindowMock = { - document: { - write() {}, - body: { - setAttribute() {}, - }, - }, - } as unknown as Window; + // const browserWindowMock = { + // document: { + // write() {}, + // body: { + // setAttribute() {}, + // }, + // }, + // } as unknown as Window; + + beforeAll(() => { + htmlForm = document.createElement("form"); + (htmlForm as HTMLFormElement).submit = () => {}; + htmlInput = document.createElement("input"); + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -494,9 +502,22 @@ describe("1000: DatafilesActionComponent", () => { }); }); - function getFakeElement(elementType: string): HTMLElement { - const element = new MockHtmlElement(elementType); - return element as unknown as HTMLElement; + function createFakeElement(elementType: string): HTMLElement { + //const element = new MockHtmlElement(elementType); + //return element as unknown as HTMLElement; + let element: HTMLElement = null; + + switch (elementType) { + case "form": + element = htmlForm.cloneNode(true) as HTMLElement; + break; + case "input": + element = htmlInput.cloneNode(true) as HTMLElement; + break; + default: + element = null; + } + return element; } it("0400: Form submission should have all files when Download All is clicked", async () => { @@ -505,8 +526,9 @@ describe("1000: DatafilesActionComponent", () => { maxSizeType.higher, selectedFilesType.none, ); - spyOn(document, "createElement").and.callFake(getFakeElement); - spyOn(window, "open").and.returnValue(browserWindowMock); + + spyOn(document, "createElement").and.callFake(createFakeElement); + //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -526,13 +548,13 @@ describe("1000: DatafilesActionComponent", () => { maxSizeType.higher, selectedFilesType.none, ); - spyOn(document, "createElement").and.callFake(getFakeElement); - spyOn(window, "open").and.returnValue(browserWindowMock); + spyOn(document, "createElement").and.callFake(createFakeElement); + //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); - expect(component.form.action).toEqual( - actionsConfig[actionSelectorType.download_all].url, + expect(component.form.action.replace(/\/$/, "")).toEqual( + actionsConfig[actionSelectorType.download_all].url.replace(/\/$/, ""), ); }); @@ -542,8 +564,8 @@ describe("1000: DatafilesActionComponent", () => { maxSizeType.higher, selectedFilesType.none, ); - spyOn(document, "createElement").and.callFake(getFakeElement); - spyOn(window, "open").and.returnValue(browserWindowMock); + spyOn(document, "createElement").and.callFake(createFakeElement); + //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -566,8 +588,8 @@ describe("1000: DatafilesActionComponent", () => { maxSizeType.higher, selectedFile, ); - spyOn(document, "createElement").and.callFake(getFakeElement); - spyOn(window, "open").and.returnValue(browserWindowMock); + spyOn(document, "createElement").and.callFake(createFakeElement); + //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -592,8 +614,8 @@ describe("1000: DatafilesActionComponent", () => { maxSizeType.higher, selectedFilesType.none, ); - spyOn(document, "createElement").and.callFake(getFakeElement); - spyOn(window, "open").and.returnValue(browserWindowMock); + spyOn(document, "createElement").and.callFake(createFakeElement); + //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -613,13 +635,13 @@ describe("1000: DatafilesActionComponent", () => { maxSizeType.higher, selectedFilesType.none, ); - spyOn(document, "createElement").and.callFake(getFakeElement); - spyOn(window, "open").and.returnValue(browserWindowMock); + spyOn(document, "createElement").and.callFake(createFakeElement); + //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); - expect(component.form.action).toEqual( - actionsConfig[actionSelectorType.notebook_all].url, + expect(component.form.action.replace(/\/$/, "")).toEqual( + actionsConfig[actionSelectorType.notebook_all].url.replace(/\/$/, ""), ); }); @@ -630,8 +652,8 @@ describe("1000: DatafilesActionComponent", () => { maxSizeType.higher, selectedFile, ); - spyOn(document, "createElement").and.callFake(getFakeElement); - spyOn(window, "open").and.returnValue(browserWindowMock); + spyOn(document, "createElement").and.callFake(createFakeElement); + //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.ts b/src/app/datasets/datafiles-actions/datafiles-action.component.ts index 1eeae9118..9ede93d88 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-action.component.ts @@ -27,11 +27,12 @@ export class DatafilesActionComponent implements OnInit, OnChanges { visible = true; use_mat_icon = false; use_icon = false; - //disabled = false; disabled_condition = "false"; selectedTotalFileSize = 0; numberOfFileSelected = 0; + form: HTMLFormElement = null; + constructor(private userApi: UserApi) { this.userApi.jwt().subscribe((jwt) => { this.jwt = jwt.jwt; @@ -90,10 +91,6 @@ export class DatafilesActionComponent implements OnInit, OnChanges { ).length; } - // compute_disabled() { - // this.disabled = eval(this.disabled_condition); - // } - get disabled() { this.update_status(); this.prepare_disabled_condition(); @@ -118,21 +115,25 @@ export class DatafilesActionComponent implements OnInit, OnChanges { } type_form() { - const form = document.createElement("form"); - form.target = this.actionConfig.target || "_self"; - form.method = this.actionConfig.method || "POST"; - form.action = this.actionConfig.url; - form.style.display = "none"; + if (this.form !== null) { + document.body.removeChild(this.form); + } + + this.form = document.createElement("form"); + this.form.target = this.actionConfig.target || "_self"; + this.form.method = this.actionConfig.method || "POST"; + this.form.action = this.actionConfig.url; + this.form.style.display = "none"; - 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), ); @@ -142,14 +143,15 @@ 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), + ); index = index + 1; } } - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); + document.body.appendChild(this.form); + this.form.submit(); return true; } diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts index f449b4c35..32d8eb1a2 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts +++ b/src/app/datasets/datafiles-actions/datafiles-actions.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { ActionConfig, ActionDataset } from "./datafiles-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; import { AppConfigService } from "app-config.service"; From 17681c3496a62b4582d3bee8d9f78acde5fb5c7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:26:36 +0000 Subject: [PATCH 06/18] chore(deps): bump tslib from 2.6.3 to 2.7.0 Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.3 to 2.7.0. - [Release notes](https://github.com/Microsoft/tslib/releases) - [Commits](https://github.com/Microsoft/tslib/compare/v2.6.3...v2.7.0) --- updated-dependencies: - dependency-name: tslib dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59b6bcb5a..8519d117f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18217,9 +18217,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/tsutils": { "version": "3.21.0", From 5ecdf9d57c1dc28b480387638834509d3d654471 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:34:36 +0000 Subject: [PATCH 07/18] chore(deps): bump mathjs from 13.0.3 to 13.1.0 Bumps [mathjs](https://github.com/josdejong/mathjs) from 13.0.3 to 13.1.0. - [Changelog](https://github.com/josdejong/mathjs/blob/develop/HISTORY.md) - [Commits](https://github.com/josdejong/mathjs/compare/v13.0.3...v13.1.0) --- updated-dependencies: - dependency-name: mathjs dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8519d117f..a2691c7e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13784,11 +13784,11 @@ "dev": true }, "node_modules/mathjs": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.0.3.tgz", - "integrity": "sha512-GpP9OW6swA5POZXvgpc/1FYkAr8lKgV04QHS1tIU60klFfplVCYaNzn6qy0vSp0hAQQN7shcx9CeB507dlLujA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.1.0.tgz", + "integrity": "sha512-5BI//cQdcKtDgstJ3QbKYyrWLyUEEHY9ZQgA3laaPyTis+ca8Y7GCxbLFFLYv8nkpOFTIF55FLKoiqQaufj1dQ==", "dependencies": { - "@babel/runtime": "^7.24.8", + "@babel/runtime": "^7.25.4", "complex.js": "^2.1.1", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", @@ -13806,9 +13806,9 @@ } }, "node_modules/mathjs/node_modules/@babel/runtime": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", - "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz", + "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==", "dependencies": { "regenerator-runtime": "^0.14.0" }, From a1507d25982491eea24e8f4cbd72361dbb087121 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:43:05 +0000 Subject: [PATCH 08/18] chore(deps-dev): bump @types/node in the types group across 1 directory Bumps the types group with 1 update in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 22.0.0 to 22.5.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: types ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2691c7e5..1c33f1faf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5371,12 +5371,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", - "integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==", + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", "dev": true, "dependencies": { - "undici-types": "~6.11.1" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-forge": { @@ -18464,9 +18464,9 @@ } }, "node_modules/undici-types": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", - "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "node_modules/unicode-canonical-property-names-ecmascript": { From 9d5a92ceecad690f690eee2533b106fdc03c649f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:50:52 +0000 Subject: [PATCH 09/18] chore(deps-dev): bump cypress from 13.13.1 to 13.13.3 Bumps [cypress](https://github.com/cypress-io/cypress) from 13.13.1 to 13.13.3. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v13.13.1...v13.13.3) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c33f1faf..8760ec889 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8303,13 +8303,13 @@ "dev": true }, "node_modules/cypress": { - "version": "13.13.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.1.tgz", - "integrity": "sha512-8F9UjL5MDUdgC/S5hr8CGLHbS5gGht5UOV184qc2pFny43fnkoaKxlzH/U6//zmGu/xRTaKimNfjknLT8+UDFg==", + "version": "13.13.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.3.tgz", + "integrity": "sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^3.0.0", + "@cypress/request": "^3.0.1", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", From 7d5457b540c58048e681541e3214206515b30514 Mon Sep 17 00:00:00 2001 From: Igor Khokhriakov Date: Wed, 28 Aug 2024 11:00:25 +0200 Subject: [PATCH 10/18] Fix #1568 (#1569) --- src/app/shared/modules/filters/pid-filter.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/modules/filters/pid-filter.component.ts b/src/app/shared/modules/filters/pid-filter.component.ts index 7e28d3a8a..0d5e066b7 100644 --- a/src/app/shared/modules/filters/pid-filter.component.ts +++ b/src/app/shared/modules/filters/pid-filter.component.ts @@ -30,7 +30,7 @@ export class PidFilterComponent ) { super(); this.subscription = this.pidSubject - .pipe(debounceTime(500), distinctUntilChanged()) + .pipe(debounceTime(500)) .subscribe((pid) => { const condition = !pid ? "" : this.buildPidTermsCondition(pid); this.store.dispatch(setPidTermsFilterAction({ pid: condition })); From 244c514a21137db220392210802f54d3969ad078 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 18:49:15 +0000 Subject: [PATCH 11/18] chore(deps-dev): bump cypress from 13.13.3 to 13.14.1 Bumps [cypress](https://github.com/cypress-io/cypress) from 13.13.3 to 13.14.1. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v13.13.3...v13.14.1) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8760ec889..5ed64837e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8303,9 +8303,9 @@ "dev": true }, "node_modules/cypress": { - "version": "13.13.3", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.3.tgz", - "integrity": "sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==", + "version": "13.14.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.1.tgz", + "integrity": "sha512-Wo+byPmjps66hACEH5udhXINEiN3qS3jWNGRzJOjrRJF3D0+YrcP2LVB1T7oYaVQM/S+eanqEvBWYc8cf7Vcbg==", "dev": true, "hasInstallScript": true, "dependencies": { From a703c894fdf6aef6b7ef2dbba2d56a266af7678e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:40:38 +0000 Subject: [PATCH 12/18] chore(deps-dev): bump cypress from 13.14.1 to 13.14.2 Bumps [cypress](https://github.com/cypress-io/cypress) from 13.14.1 to 13.14.2. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v13.14.1...v13.14.2) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ed64837e..6ba563ad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8303,9 +8303,9 @@ "dev": true }, "node_modules/cypress": { - "version": "13.14.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.1.tgz", - "integrity": "sha512-Wo+byPmjps66hACEH5udhXINEiN3qS3jWNGRzJOjrRJF3D0+YrcP2LVB1T7oYaVQM/S+eanqEvBWYc8cf7Vcbg==", + "version": "13.14.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.2.tgz", + "integrity": "sha512-lsiQrN17vHMB2fnvxIrKLAjOr9bPwsNbPZNrWf99s4u+DVmCY6U+w7O3GGG9FvP4EUVYaDu+guWeNLiUzBrqvA==", "dev": true, "hasInstallScript": true, "dependencies": { From e2d2240e835aae87efbdcf593770bff88c82e156 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:49:00 +0000 Subject: [PATCH 13/18] chore(deps-dev): bump jasmine-core from 5.2.0 to 5.3.0 Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/jasmine/jasmine/releases) - [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md) - [Commits](https://github.com/jasmine/jasmine/compare/v5.2.0...v5.3.0) --- updated-dependencies: - dependency-name: jasmine-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ba563ad8..ae60b8a81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12256,9 +12256,9 @@ } }, "node_modules/jasmine-core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", - "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.3.0.tgz", + "integrity": "sha512-zsOmeBKESky4toybvWEikRiZ0jHoBEu79wNArLfMdSnlLMZx3Xcp6CSm2sUcYyoJC+Uyj8LBJap/MUbVSfJ27g==", "dev": true }, "node_modules/jasmine-marbles": { @@ -12304,6 +12304,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jasmine/node_modules/jasmine-core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", + "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "dev": true + }, "node_modules/jasmine/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", From dd58b66ae8df3e0bd86863e54808f985d9146e36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:56:56 +0000 Subject: [PATCH 14/18] chore(deps-dev): bump jasmine from 5.2.0 to 5.3.0 Bumps [jasmine](https://github.com/jasmine/jasmine-npm) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/jasmine/jasmine-npm/releases) - [Changelog](https://github.com/jasmine/jasmine-npm/blob/main/RELEASE.md) - [Commits](https://github.com/jasmine/jasmine-npm/compare/v5.2.0...v5.3.0) --- updated-dependencies: - dependency-name: jasmine dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae60b8a81..8a5de27bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12243,13 +12243,13 @@ } }, "node_modules/jasmine": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.2.0.tgz", - "integrity": "sha512-il+noV96N1BGU9/FMmc8QtAMxC8lPnXUiAvgb0o9MDZATRdxglTQe9wo6UdL049ropQL6MopDYwDlludKR6wJQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.3.0.tgz", + "integrity": "sha512-Vrv5VWTXVZ/5xcNawlYCmE24pOaZu3KduLr9iAaENoMJ8W8Ryvhfpw2cf3rI4Unc2ajvu2t4tCKjS72TnraBGQ==", "dev": true, "dependencies": { "glob": "^10.2.2", - "jasmine-core": "~5.2.0" + "jasmine-core": "~5.3.0" }, "bin": { "jasmine": "bin/jasmine.js" @@ -12304,12 +12304,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jasmine/node_modules/jasmine-core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", - "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", - "dev": true - }, "node_modules/jasmine/node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", From 47e3e58b42d6c706194cf39a73e7c8e429d413b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:04:23 +0000 Subject: [PATCH 15/18] chore(deps): bump mathjs from 13.1.0 to 13.1.1 Bumps [mathjs](https://github.com/josdejong/mathjs) from 13.1.0 to 13.1.1. - [Changelog](https://github.com/josdejong/mathjs/blob/develop/HISTORY.md) - [Commits](https://github.com/josdejong/mathjs/compare/v13.1.0...v13.1.1) --- updated-dependencies: - dependency-name: mathjs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a5de27bc..40506a265 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13784,9 +13784,9 @@ "dev": true }, "node_modules/mathjs": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.1.0.tgz", - "integrity": "sha512-5BI//cQdcKtDgstJ3QbKYyrWLyUEEHY9ZQgA3laaPyTis+ca8Y7GCxbLFFLYv8nkpOFTIF55FLKoiqQaufj1dQ==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-13.1.1.tgz", + "integrity": "sha512-duaSAy7m4F+QtP1Dyv8MX2XuxcqpNDDlGly0SdVTCqpAmwdOFWilDdQKbLdo9RfD6IDNMOdo9tIsEaTXkconlQ==", "dependencies": { "@babel/runtime": "^7.25.4", "complex.js": "^2.1.1", From 64b82b46ed130833ffd86a5e7b7282479974a00b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:13:08 +0000 Subject: [PATCH 16/18] chore(deps): bump filesize from 10.1.4 to 10.1.6 Bumps [filesize](https://github.com/avoidwork/filesize.js) from 10.1.4 to 10.1.6. - [Changelog](https://github.com/avoidwork/filesize.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/avoidwork/filesize.js/compare/10.1.4...10.1.6) --- updated-dependencies: - dependency-name: filesize dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40506a265..b76e4cd3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10243,9 +10243,9 @@ } }, "node_modules/filesize": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.4.tgz", - "integrity": "sha512-ryBwPIIeErmxgPnm6cbESAzXjuEFubs+yKYLBZvg3CaiNcmkJChoOGcBSrZ6IwkMwPABwPpVXE6IlNdGJJrvEg==", + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", "engines": { "node": ">= 10.4.0" } From fe3ef03a733ab8c8a83a93b85ac2feb92c69c31d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:21:13 +0000 Subject: [PATCH 17/18] chore(deps-dev): bump @types/node in the types group across 1 directory Bumps the types group with 1 update in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 22.5.0 to 22.5.4 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b76e4cd3d..50427f80c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5371,9 +5371,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dev": true, "dependencies": { "undici-types": "~6.19.2" From c7fd4be2bf5611832ddccb054af04bd0c0840d2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:30:19 +0000 Subject: [PATCH 18/18] chore(deps-dev): bump the angular group with 3 updates Bumps the angular group with 3 updates: [@angular-devkit/build-angular](https://github.com/angular/angular-cli), [@angular-devkit/core](https://github.com/angular/angular-cli) and [@angular/cli](https://github.com/angular/angular-cli). Updates `@angular-devkit/build-angular` from 16.2.14 to 16.2.15 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/16.2.14...16.2.15) Updates `@angular-devkit/core` from 16.2.14 to 16.2.15 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/16.2.14...16.2.15) Updates `@angular/cli` from 16.2.14 to 16.2.15 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/16.2.14...16.2.15) --- updated-dependencies: - dependency-name: "@angular-devkit/build-angular" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular-devkit/core" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular - dependency-name: "@angular/cli" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: angular ... Signed-off-by: dependabot[bot] --- package-lock.json | 171 ++++++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50427f80c..98472fb92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,12 +114,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1602.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.14.tgz", - "integrity": "sha512-eSdONEV5dbtLNiOMBy9Ue9DdJ1ct6dH9RdZfYiedq6VZn0lejePAjY36MYVXgq2jTE+v/uIiaNy7caea5pt55A==", + "version": "0.1602.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.15.tgz", + "integrity": "sha512-+yPlUG5c8l7Z/A6dyeV7NQjj4WDWnWWQt+8eW/KInwVwoYiM32ntTJ0M4uU/aDdHuwKQnMLly28AcSWPWKYf2Q==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.14", + "@angular-devkit/core": "16.2.15", "rxjs": "7.8.1" }, "engines": { @@ -138,15 +138,15 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "16.2.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.14.tgz", - "integrity": "sha512-bXQ6i7QPhwmYHuh+DSNkBhjTIHQF0C6fqZEg2ApJA3NmnzE98oQnmJ9AnGnAkdf1Mjn3xi2gxoZWPDDxGEINMw==", + "version": "16.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.15.tgz", + "integrity": "sha512-gw9wQENYVNUCB2bnzk0yKd6YGlemDwuwKnrPnSm4myyMuScZpW+e+zliGW+JXRuVWZqiTJNcdd58e4CrrreILg==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1602.14", - "@angular-devkit/build-webpack": "0.1602.14", - "@angular-devkit/core": "16.2.14", + "@angular-devkit/architect": "0.1602.15", + "@angular-devkit/build-webpack": "0.1602.15", + "@angular-devkit/core": "16.2.15", "@babel/core": "7.22.9", "@babel/generator": "7.22.9", "@babel/helper-annotate-as-pure": "7.22.5", @@ -158,7 +158,7 @@ "@babel/runtime": "7.22.6", "@babel/template": "7.22.5", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "16.2.14", + "@ngtools/webpack": "16.2.15", "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.14", @@ -202,7 +202,7 @@ "tree-kill": "1.2.2", "tslib": "2.6.1", "vite": "4.5.3", - "webpack": "5.88.2", + "webpack": "5.94.0", "webpack-dev-middleware": "6.1.2", "webpack-dev-server": "4.15.1", "webpack-merge": "5.9.0", @@ -275,12 +275,12 @@ "dev": true }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1602.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.14.tgz", - "integrity": "sha512-f+ZTCjOoA1SCQEaX3L/63ubqr/vlHkwDXAtKjBsQgyz6srnETcjy96Us5k/LoK7/hPc85zFneqLinfqOMVWHJQ==", + "version": "0.1602.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.15.tgz", + "integrity": "sha512-ms1+vCDdV0KX8BplJ7JoKH3wKjWHxxZTOX+mSPIjt4wS1uAk5DnezXHIjpBiJ3HY9XVHFI9C0HT4n7o6kFIOEQ==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.14", + "@angular-devkit/architect": "0.1602.15", "rxjs": "7.8.1" }, "engines": { @@ -303,9 +303,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.2.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.14.tgz", - "integrity": "sha512-Ui14/d2+p7lnmXlK/AX2ieQEGInBV75lonNtPQgwrYgskF8ufCuN0DyVZQUy9fJDkC+xQxbJyYrby/BS0R0e7w==", + "version": "16.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.15.tgz", + "integrity": "sha512-68BgPWpcjNKz++uvLFG8IZaOH3ti2BWQVqaE3yTIYaMoNt0y0A0X2MUVd7EGbAGUk2JdloWJv5LTPVZMzCuK4w==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -339,12 +339,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.2.14", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.14.tgz", - "integrity": "sha512-B6LQKInCT8w5zx5Pbroext5eFFRTCJdTwHN8GhcVS8IeKCnkeqVTQLjB4lBUg7LEm8Y7UHXwzrVxmk+f+MBXhw==", + "version": "16.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.15.tgz", + "integrity": "sha512-C/j2EwapdBMf1HWDuH89bA9B2e511iEYImkyZ+vCSXRwGiWUaZCrhl18bvztpErTrdOLM3mCwNXWEAMXI4zUXA==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.14", + "@angular-devkit/core": "16.2.15", "jsonc-parser": "3.2.0", "magic-string": "0.30.1", "ora": "5.4.1", @@ -510,15 +510,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.2.14", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.14.tgz", - "integrity": "sha512-0y71jtitigVolm4Rim1b8xPQ+B22cGp4Spef2Wunpqj67UowN6tsZaVuWBEQh4u5xauX8LAHKqsvy37ZPWCc4A==", + "version": "16.2.15", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.15.tgz", + "integrity": "sha512-nNUmt0ZRj2xHH8tGXSJUiusP5rmakAz0f6cc6T4p03OyeShOKdvs9+/F4hzzsM79/ylZofBlFfwYVCBTbOtMqw==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.14", - "@angular-devkit/core": "16.2.14", - "@angular-devkit/schematics": "16.2.14", - "@schematics/angular": "16.2.14", + "@angular-devkit/architect": "0.1602.15", + "@angular-devkit/core": "16.2.15", + "@angular-devkit/schematics": "16.2.15", + "@schematics/angular": "16.2.15", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -4467,9 +4467,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "16.2.14", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.14.tgz", - "integrity": "sha512-3+zPP3Wir46qrZ3FEiTz5/emSoVHYUCH+WgBmJ57mZCx1qBOYh2VgllnPr/Yusl1sc/jUZjdwq/es/9ZNw+zDQ==", + "version": "16.2.15", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.15.tgz", + "integrity": "sha512-rD4IHt3nS6PdIKvmoqwIadMIGKsemBSz412kD8Deetl0TiCVhD/Tn1M00dxXzMSHSFCQcOKxdZAeD53yRwTOOA==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0", @@ -4988,13 +4988,13 @@ } }, "node_modules/@schematics/angular": { - "version": "16.2.14", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.14.tgz", - "integrity": "sha512-YqIv727l9Qze8/OL6H9mBHc2jVXzAGRNBYnxYWqWhLbfvuVbbldo6NNIIjgv6lrl2LJSdPAAMNOD5m/f6210ug==", + "version": "16.2.15", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.15.tgz", + "integrity": "sha512-T7wEGYxidpLAkis+hO5nsVfnWsy6sXf1T9GS8uztC8IYYsnqB9jTVfjVyfhASugZasdmx7+jWv3oCGy6Z5ZehA==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.14", - "@angular-devkit/schematics": "16.2.14", + "@angular-devkit/core": "16.2.15", + "@angular-devkit/schematics": "16.2.15", "jsonc-parser": "3.2.0" }, "engines": { @@ -5274,21 +5274,13 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -5308,9 +5300,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", - "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", "dev": true, "dependencies": { "@types/node": "*", @@ -5332,9 +5324,9 @@ "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -5389,9 +5381,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", - "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", "dev": true }, "node_modules/@types/range-parser": { @@ -5488,9 +5480,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -6333,10 +6325,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -9118,9 +9110,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -11549,9 +11541,9 @@ "dev": true }, "node_modules/ipaddr.js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", - "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, "engines": { "node": ">= 10" @@ -12954,9 +12946,9 @@ } }, "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -18869,34 +18861,33 @@ } }, "node_modules/webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -19026,9 +19017,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0"