diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7d808ab3d2..20628eaa05 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -70,7 +70,7 @@ jobs: - name: Npm install run: npm install - name: npx build - run: npx nx build datahub --base-href=/catalogue/ + run: npx nx build datahub --base-href=/catalogue/ --skip-nx-cache # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action diff --git a/apps/datahub-e2e/src/e2e/organization-page.cy.ts b/apps/datahub-e2e/src/e2e/organization-page.cy.ts new file mode 100644 index 0000000000..5cddc7935c --- /dev/null +++ b/apps/datahub-e2e/src/e2e/organization-page.cy.ts @@ -0,0 +1,118 @@ +import 'cypress-real-events' + +describe('organizations', () => { + beforeEach(() => { + cy.visit('organization/Barbie%20Inc.') + + // aliases + cy.get('gn-ui-navigation-button').as('backButton') + cy.get('[data-test="organizationHeaderName"]').as('organizationHeaderName') + cy.get('[data-test="organizationHeaderWebsiteLink"]').as( + 'organizationHeaderWebsiteLink' + ) + cy.get('[data-test="organizationDescription"]').as( + 'organizationDescription' + ) + cy.get('gn-ui-max-lines').contains('Read more').as('readMoreButton') + cy.get('[data-test="organizationLogo"]').as('organizationLogo') + cy.get('[data-test="organizationDatasetCount"]').as( + 'organizationDatasetCount' + ) + cy.get('[data-test="organizationEmail"]').as('organizationEmail') + cy.get('[data-test="orgPageLasPubDat"]').as('orgPageLasPubDat') + cy.get('[data-test="orgDetailsSearchAllBtn"]').as('orgDetailsSearchAllBtn') + }) + + describe('general display', () => { + describe('header', () => { + describe('back button', () => { + beforeEach(() => { + cy.visit('organisations') + cy.visit('organization/Barbie%20Inc.') + }) + + it('back button goes to the previous visited page', () => { + cy.get('@backButton').click() + cy.url().should('include', '/organisations') + }) + }) + + it('should display the organization name', () => { + cy.get('@organizationHeaderName').should('contain', 'Barbie Inc.') + }) + + it('should display the organization website link', () => { + cy.get('@organizationHeaderWebsiteLink') + .should('be.visible') + .should('have.attr', 'href', 'https://www.barbie-inc.com/') + .and('have.attr', 'target', '_blank') + }) + }) + + describe('details', () => { + describe('left column', () => { + it('should display the organization description', () => { + cy.get('@organizationDescription').should('be.visible') + }) + + it('click on read more should expand the organization description', () => { + let initialDescription + let newDescription + + cy.get('@organizationDescription').then((firstDescription) => { + initialDescription = firstDescription + cy.get('@readMoreButton').trigger('click') + cy.get('@organizationDescription').then((secondDescription) => { + newDescription = secondDescription + expect(newDescription).to.not.equal(initialDescription) + }) + }) + }) + }) + + describe('right column', () => { + it('should display the organization logo', () => { + cy.get('@organizationLogo').should('be.visible') + }) + + it('should display the organization dataset count', () => { + cy.get('@organizationDatasetCount').should('be.visible') + }) + + it('a click on the organization dataset count should open the dataset search page filtered on the organization', () => { + cy.get('@organizationDatasetCount').then(($link) => { + const url = $link.prop('href') + cy.wrap($link).click() + + cy.url().should('eq', url) + }) + }) + + it('should display the organization email', () => { + cy.get('@organizationEmail') + .should('be.visible') + .and('have.attr', 'href', 'mailto:contact@barbie-inc.com') + }) + }) + + describe('last published datasets', () => { + it('should display the last published datasets', () => { + cy.get('@orgPageLasPubDat').should('be.visible') + }) + + it('should display the search all button', () => { + cy.get('@orgDetailsSearchAllBtn').should('be.visible') + }) + + it('a click on the search all button should open the dataset search page filtered on the organization', () => { + cy.get('@orgDetailsSearchAllBtn').then(($link) => { + const url = $link.prop('href') + cy.wrap($link).click() + + cy.url().should('eq', url) + }) + }) + }) + }) + }) +}) diff --git a/apps/datahub-e2e/src/e2e/organizations.cy.ts b/apps/datahub-e2e/src/e2e/organizations.cy.ts index b39da4ca39..ded2dc026d 100644 --- a/apps/datahub-e2e/src/e2e/organizations.cy.ts +++ b/apps/datahub-e2e/src/e2e/organizations.cy.ts @@ -77,14 +77,15 @@ describe('organizations', () => { }) describe('list features', () => { - it('should search with a filter on the selected org on click', () => { + it('should open the organization page', () => { cy.get('@organizationsName') .eq(10) .then(($clickedName) => { cy.get('@organizations').eq(10).click() - cy.url() - .should('include', 'publisher=') - .and('include', encodeURIComponent($clickedName.text().trim())) + cy.url().should( + 'contain', + `organization/${encodeURIComponent($clickedName.text().trim())}` + ) }) }) }) diff --git a/apps/datahub/src/app/app.component.html b/apps/datahub/src/app/app.component.html index aa87a5178d..ad6c36c01d 100644 --- a/apps/datahub/src/app/app.component.html +++ b/apps/datahub/src/app/app.component.html @@ -3,63 +3,128 @@ class="selection:bg-primary-lightest selection:text-primary-darker" > diff --git a/apps/datahub/src/app/app.component.ts b/apps/datahub/src/app/app.component.ts index d99f20845c..1f69e6ed3e 100644 --- a/apps/datahub/src/app/app.component.ts +++ b/apps/datahub/src/app/app.component.ts @@ -12,13 +12,4 @@ export class AppComponent implements OnInit { const favicon = getThemeConfig().FAVICON if (favicon) ThemeService.setFavicon(favicon) } - - // methodes pour le header - searchChange(toggled: boolean): void {} - - searchSelect(toggled: boolean): void {} - - linkSelect(toggled: boolean): void {} - - langChange(toggled: boolean): void {} } diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index d5a046fa35..d92c0fc49b 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -5,6 +5,7 @@ import { BrowserModule } from '@angular/platform-browser' import { Router, RouterModule } from '@angular/router' import { FeatureCatalogModule, + ORGANIZATION_PAGE_URL_TOKEN, ORGANIZATION_URL_TOKEN, } from '@geonetwork-ui/feature/catalog' import { @@ -16,6 +17,7 @@ import { DefaultRouterModule, ROUTE_PARAMS, ROUTER_ROUTE_DATASET, + ROUTER_ROUTE_ORGANIZATION, ROUTER_ROUTE_SEARCH, RouterService, } from '@geonetwork-ui/feature/router' @@ -94,6 +96,7 @@ import { MatTabsModule } from '@angular/material/tabs' import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { RecordUserFeedbacksComponent } from './record/record-user-feedbacks/record-user-feedbacks.component' import { LetDirective } from '@ngrx/component' +import { OrganizationPageComponent } from './organization/organization-page/organization-page.component' import { MatButtonToggleModule } from '@angular/material/button-toggle' import { DsfrHeaderModule } from '@edugouvfr/ngx-dsfr' import { DsfrFooterModule } from '@edugouvfr/ngx-dsfr' @@ -150,6 +153,7 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] searchStateId: 'mainSearch', searchRouteComponent: SearchPageComponent, recordRouteComponent: RecordPageComponent, + organizationRouteComponent: OrganizationPageComponent, }), FeatureRecordModule, FeatureCatalogModule, @@ -225,6 +229,10 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] }, }, { provide: RECORD_URL_TOKEN, useValue: `${ROUTER_ROUTE_DATASET}/\${uuid}` }, + { + provide: ORGANIZATION_PAGE_URL_TOKEN, + useValue: `${ROUTER_ROUTE_ORGANIZATION}/\${name}`, + }, { provide: ORGANIZATION_URL_TOKEN, useValue: `${ROUTER_ROUTE_SEARCH}?${ROUTE_PARAMS.PUBLISHER}=\${name}`, diff --git a/apps/datahub/src/app/home/home-header/home-header.component.html b/apps/datahub/src/app/home/home-header/home-header.component.html index 382468afaa..5d55e822f3 100644 --- a/apps/datahub/src/app/home/home-header/home-header.component.html +++ b/apps/datahub/src/app/home/home-header/home-header.component.html @@ -1,10 +1,10 @@
-
+
diff --git a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts index 7abef2b1c8..20e073e7ff 100644 --- a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts +++ b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts @@ -1,15 +1,15 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { - RouterFacade, ROUTER_ROUTE_SEARCH, + RouterFacade, } from '@geonetwork-ui/feature/router' import { TranslateModule } from '@ngx-translate/core' import { readFirst } from '@nx/angular/testing' import { BehaviorSubject } from 'rxjs' import { ROUTER_ROUTE_NEWS, - ROUTER_ROUTE_ORGANISATIONS, + ROUTER_ROUTE_ORGANIZATIONS, } from '../../router/constants' import { NavigationMenuComponent } from './navigation-menu.component' @@ -73,7 +73,7 @@ describe('NavigationMenuComponent', () => { describe('navigate to organisations route', () => { beforeEach(() => { routerFacadeMock.currentRoute$.next({ - url: [{ path: ROUTER_ROUTE_ORGANISATIONS }], + url: [{ path: ROUTER_ROUTE_ORGANIZATIONS }], }) }) it('displays activeLabel for organisations', async () => { diff --git a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts index dd96ada53e..c10c9ac278 100644 --- a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts +++ b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts @@ -1,13 +1,13 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core' +import { ChangeDetectionStrategy, Component } from '@angular/core' import { marker } from '@biesbjerg/ngx-translate-extract-marker' import { - RouterFacade, ROUTER_ROUTE_SEARCH, + RouterFacade, } from '@geonetwork-ui/feature/router' import { map } from 'rxjs/operators' import { ROUTER_ROUTE_NEWS, - ROUTER_ROUTE_ORGANISATIONS, + ROUTER_ROUTE_ORGANIZATIONS, } from '../../router/constants' import { getThemeConfig } from '@geonetwork-ui/util/app-config' @@ -33,7 +33,7 @@ export class NavigationMenuComponent { label: 'datahub.header.datasets', }, { - link: `${ROUTER_ROUTE_ORGANISATIONS}`, + link: `${ROUTER_ROUTE_ORGANIZATIONS}`, label: 'datahub.header.organisations', }, ] diff --git a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.html b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.html index 3bc2976030..aaf2ae7e23 100644 --- a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.html +++ b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.html @@ -4,7 +4,7 @@ class="py-[37px] pl-[47px] rounded-lg border bg-white mb-5 card-shadow cursor-pointer" [figure]="recordsCount$ | async" [icon]="'folder_open'" - title="catalog.figures.datasets" + [title]="'catalog.figures.datasets'" [color]="'secondary'" > @@ -13,7 +13,7 @@ class="py-[37px] pl-[47px] rounded-lg bg-white border card-shadow cursor-pointer" [figure]="orgsCount$ | async" [icon]="'corporate_fare'" - title="catalog.figures.organisations" + [title]="'catalog.figures.organizations'" [color]="'secondary'" > diff --git a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts index bee90f8cfe..30880371a7 100644 --- a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts +++ b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { startWith } from 'rxjs/operators' import { RecordsService } from '@geonetwork-ui/feature/catalog' import { ROUTER_ROUTE_SEARCH } from '@geonetwork-ui/feature/router' -import { ROUTER_ROUTE_ORGANISATIONS } from '../../../router/constants' +import { ROUTER_ROUTE_ORGANIZATIONS } from '../../../router/constants' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { marker } from '@biesbjerg/ngx-translate-extract-marker' @@ -19,7 +19,7 @@ export class KeyFiguresComponent { recordsCount$ = this.catalogRecords.recordsCount$.pipe(startWith('-')) orgsCount$ = this.catalogOrgs.organisationsCount$.pipe(startWith('-')) ROUTE_SEARCH = `/${ROUTER_ROUTE_SEARCH}` - ROUTE_ORGANISATIONS = `/${ROUTER_ROUTE_ORGANISATIONS}` + ROUTE_ORGANISATIONS = `/${ROUTER_ROUTE_ORGANIZATIONS}` constructor( private catalogRecords: RecordsService, diff --git a/apps/datahub/src/app/home/organisations-page/organisations-page.component.html b/apps/datahub/src/app/home/organisations-page/organisations-page.component.html index 9ce96ecafb..454d3b8d4d 100644 --- a/apps/datahub/src/app/home/organisations-page/organisations-page.component.html +++ b/apps/datahub/src/app/home/organisations-page/organisations-page.component.html @@ -1,5 +1,5 @@
diff --git a/apps/datahub/src/app/home/organisations-page/organisations-page.component.spec.ts b/apps/datahub/src/app/home/organisations-page/organisations-page.component.spec.ts index e719f1e097..67be0fccc8 100644 --- a/apps/datahub/src/app/home/organisations-page/organisations-page.component.spec.ts +++ b/apps/datahub/src/app/home/organisations-page/organisations-page.component.spec.ts @@ -2,27 +2,19 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { OrganisationsPageComponent } from './organisations-page.component' -import { SearchService } from '@geonetwork-ui/feature/search' -import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' -import { of } from 'rxjs' +import { RouterFacade } from '@geonetwork-ui/feature/router' +import { ORGANISATIONS_FIXTURE } from '@geonetwork-ui/common/fixtures' -class SearchServiceMock { - setFilters = jest.fn() -} - -class OrganisationsServiceMock { - getFiltersForOrgs = jest.fn((orgs) => - of({ - orgs: orgs.reduce((prev, curr) => ({ ...prev, [curr.name]: true }), {}), - }) - ) +class RouterFacadeMock { + goToOrganization = jest.fn() } describe('OrganisationsPageComponent', () => { let component: OrganisationsPageComponent let fixture: ComponentFixture - let searchService: SearchService - let orgsService: OrganizationsServiceInterface + let routerFacade: RouterFacade + + const selectedOrganization = ORGANISATIONS_FIXTURE[0] beforeEach(async () => { await TestBed.configureTestingModule({ @@ -30,18 +22,13 @@ describe('OrganisationsPageComponent', () => { schemas: [NO_ERRORS_SCHEMA], providers: [ { - provide: SearchService, - useClass: SearchServiceMock, - }, - { - provide: OrganizationsServiceInterface, - useClass: OrganisationsServiceMock, + provide: RouterFacade, + useClass: RouterFacadeMock, }, ], }).compileComponents() - searchService = TestBed.inject(SearchService) - orgsService = TestBed.inject(OrganizationsServiceInterface) + routerFacade = TestBed.inject(RouterFacade) fixture = TestBed.createComponent(OrganisationsPageComponent) component = fixture.componentInstance @@ -52,23 +39,15 @@ describe('OrganisationsPageComponent', () => { expect(component).toBeTruthy() }) - describe('#searchByOrganisation', () => { - beforeEach(() => { - component.searchByOrganisation({ - name: 'MyOrg', - }) - }) - it('generates filters for the org', () => { - expect(orgsService.getFiltersForOrgs).toHaveBeenCalledWith([ - { name: 'MyOrg' }, - ]) - }) - it('updates filters to filter on the org', () => { - expect(searchService.setFilters).toHaveBeenCalledWith({ - orgs: { - MyOrg: true, - }, - }) + describe('onOrganizationSelection', () => { + it('should goToOrganization page', () => { + component.onOrganizationSelection(selectedOrganization) + + fixture.detectChanges() + + expect(routerFacade.goToOrganization).toHaveBeenCalledWith( + selectedOrganization.name + ) }) }) }) diff --git a/apps/datahub/src/app/home/organisations-page/organisations-page.component.ts b/apps/datahub/src/app/home/organisations-page/organisations-page.component.ts index 3abb743666..23f0991d11 100644 --- a/apps/datahub/src/app/home/organisations-page/organisations-page.component.ts +++ b/apps/datahub/src/app/home/organisations-page/organisations-page.component.ts @@ -1,7 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { SearchService } from '@geonetwork-ui/feature/search' -import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { Organization } from '@geonetwork-ui/common/domain/model/record' +import { RouterFacade } from '@geonetwork-ui/feature/router' @Component({ selector: 'datahub-organisations-page', @@ -10,14 +9,9 @@ import { Organization } from '@geonetwork-ui/common/domain/model/record' changeDetection: ChangeDetectionStrategy.OnPush, }) export class OrganisationsPageComponent { - constructor( - private searchService: SearchService, - private orgsService: OrganizationsServiceInterface - ) {} + constructor(private routerFacade: RouterFacade) {} - searchByOrganisation(organisation: Organization) { - this.orgsService - .getFiltersForOrgs([organisation]) - .subscribe((filters) => this.searchService.setFilters(filters)) + onOrganizationSelection(organisation: Organization) { + this.routerFacade.goToOrganization(organisation.name) } } diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.html b/apps/datahub/src/app/home/search/search-filters/search-filters.component.html index be613dda88..419eb96534 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.html +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.html @@ -6,8 +6,8 @@ class="sm:col-span-4 grid grid-cols-1 sm:grid-cols-2 gap-7 sm:gap-4 sm:grid-rows-auto" [ngClass]=" isOpen - ? 'col-span-6 mb-7 sm:mb-0 sm:grid-cols-2' - : 'col-span-4 sm:col-span-3 lg:col-span-4 sm:grid-cols-3' + ? 'col-span-6 mb-7 sm:mb-0' + : 'col-span-4 sm:col-span-3 lg:col-span-4' " >
@@ -125,7 +125,7 @@

diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts index 14ae7a3c96..508e335189 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts @@ -110,7 +110,7 @@ export class SearchFiltersComponent implements OnInit { getClassForFilter(index: number) { return ( - (this.isOpen ? 'block' : 'hidden') + ' ' + (index < 3 ? 'sm:block' : '') + (this.isOpen ? 'block' : 'hidden') + ' ' + (index < 2 ? 'sm:block' : '') ) } } diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.css b/apps/datahub/src/app/organization/organization-details/organization-details.component.css new file mode 100644 index 0000000000..c0d50f2d1c --- /dev/null +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.css @@ -0,0 +1,15 @@ +.list-page-dot { + width: 6px; + height: 6px; + border-radius: 6px; + position: relative; +} + +.list-page-dot:after { + content: ''; + position: absolute; + left: -7px; + top: -7px; + width: 20px; + height: 20px; +} diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.html b/apps/datahub/src/app/organization/organization-details/organization-details.component.html new file mode 100644 index 0000000000..d08616ad65 --- /dev/null +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.html @@ -0,0 +1,157 @@ + + +
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+

+ organization.lastPublishedDatasets +

+ + + + +
+ +
+ +
+ +
+
+ +
+
+ + +
+ +
+
+ + + + +
+
+
+
+
+
+
+ + + + diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts b/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts new file mode 100644 index 0000000000..bec7bfd76b --- /dev/null +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts @@ -0,0 +1,295 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { SearchFacade } from '@geonetwork-ui/feature/search' +import { TranslateModule } from '@ngx-translate/core' +import { BehaviorSubject, of } from 'rxjs' +import { OrganizationDetailsComponent } from './organization-details.component' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { + DATASET_RECORDS, + ORGANISATIONS_FIXTURE, +} from '@geonetwork-ui/common/fixtures' +import { AsyncPipe, NgForOf, NgIf } from '@angular/common' +import { + ButtonComponent, + PreviousNextButtonsComponent, +} from '@geonetwork-ui/ui/inputs' +import { MatIconModule } from '@angular/material/icon' +import { + BlockListComponent, + CarouselComponent, + MaxLinesComponent, +} from '@geonetwork-ui/ui/layout' +import { LetDirective } from '@ngrx/component' +import { LinkCardComponent, UiElementsModule } from '@geonetwork-ui/ui/elements' +import { UiSearchModule } from '@geonetwork-ui/ui/search' +import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' +import { RouterLink } from '@angular/router' +import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { Organization } from '@geonetwork-ui/common/domain/model/record' +import { RouterTestingModule } from '@angular/router/testing' +import { By } from '@angular/platform-browser' +import { ROUTER_ROUTE_SEARCH } from '@geonetwork-ui/feature/router' + +let getHTMLElement: (dataTest: string) => HTMLElement | undefined + +const changeDetectorRefMock: Partial = { + markForCheck: jest.fn(), +} + +class OrganisationsServiceMock { + getFiltersForOrgs = jest.fn((orgs) => + of({ + orgs: orgs.reduce((prev, curr) => ({ ...prev, [curr.name]: true }), {}), + }) + ) + organisations$ = of(ORGANISATIONS_FIXTURE) +} + +const anOrganizationWithManyDatasets: Organization = ORGANISATIONS_FIXTURE[0] + +const oneDataset = [DATASET_RECORDS[0]] +const manyDatasets = DATASET_RECORDS.concat(DATASET_RECORDS[0]) + +const organizationIsLoading = new BehaviorSubject(false) +const totalPages = new BehaviorSubject(10) +const currentPage = new BehaviorSubject(0) +const results = new BehaviorSubject(manyDatasets) + +const desiredPageSize = 3 + +class SearchFacadeMock { + private pageSize = desiredPageSize + + setPageSize = jest.fn((pageSize: number) => (this.pageSize = pageSize)) + setFilters = jest.fn(() => new SearchFacadeMock()) + setSortBy = jest.fn(() => new SearchFacadeMock()) + results$ = results.asObservable() + isLoading$ = organizationIsLoading.asObservable() + totalPages$ = totalPages.asObservable() + isBeginningOfResults$ = of(currentPage.getValue() === 1) + isEndOfResults$ = of(totalPages.getValue() === currentPage.getValue()) + currentPage$ = currentPage.asObservable() + paginate = jest.fn(() => { + currentPage.next(currentPage.getValue() + 1) + return new SearchFacadeMock() + }) +} + +describe('OrganizationDetailsComponent', () => { + let component: OrganizationDetailsComponent + let fixture: ComponentFixture + let searchFacade: SearchFacade + let debugElement: DebugElement + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [], + schemas: [NO_ERRORS_SCHEMA], + imports: [ + AsyncPipe, + NgIf, + ButtonComponent, + MatIconModule, + TranslateModule, + CarouselComponent, + BlockListComponent, + LetDirective, + LinkCardComponent, + NgForOf, + PreviousNextButtonsComponent, + UiElementsModule, + UiSearchModule, + MaxLinesComponent, + UiDatavizModule, + RouterLink, + UiWidgetsModule, + TranslateModule.forRoot(), + RouterTestingModule, + ], + providers: [ + { + provide: OrganizationsServiceInterface, + useClass: OrganisationsServiceMock, + }, + { + provide: SearchFacade, + useClass: SearchFacadeMock, + }, + { + provide: ChangeDetectorRef, + useValue: changeDetectorRefMock, + }, + ], + }) + .overrideComponent(OrganizationDetailsComponent, { + set: { + changeDetection: ChangeDetectionStrategy.Default, + }, + }) + .compileComponents() + + searchFacade = TestBed.inject(SearchFacade) + + fixture = TestBed.createComponent(OrganizationDetailsComponent) + component = fixture.componentInstance + debugElement = fixture.debugElement + + getHTMLElement = (dataTest: string) => { + const debugEl = debugElement.query(By.css(`[data-test="${dataTest}"]`)) + return debugEl ? (debugEl.nativeElement as HTMLElement) : undefined + } + + component.organization = anOrganizationWithManyDatasets + + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('Left column', () => { + describe('Organization description', () => { + it('should contain the organization description', () => { + const organizationDescriptionHtml = getHTMLElement( + 'organizationDescription' + ) + + expect(organizationDescriptionHtml.textContent.trim()).toEqual( + anOrganizationWithManyDatasets.description?.trim() + ) + }) + }) + }) + + describe('Right column', () => { + describe('Organization dataset count', () => { + it('should have the right count of dataset', () => { + const organizationDatasetCount = getHTMLElement( + 'organizationDatasetCount' + ).querySelector('[data-test="figure"]') + + expect(organizationDatasetCount.innerHTML).toEqual( + anOrganizationWithManyDatasets.recordCount?.toString() + ) + }) + }) + + describe('Organization email', () => { + it('should have the email button', () => { + const organizationEmail = getHTMLElement('organizationEmail') + + expect(organizationEmail).toBeTruthy() + + expect(organizationEmail?.getAttribute('href')).toEqual( + `mailto:${anOrganizationWithManyDatasets.email}` + ) + }) + }) + }) + + describe('Last Published datasets', () => { + describe('Previous Next buttons', () => { + it('should not be displayed if organization is loading', () => { + organizationIsLoading.next(true) + fixture.detectChanges() + + const orgDetailsNavBtn = getHTMLElement('orgDetailsNavBtn') + + expect(orgDetailsNavBtn).toBeFalsy() + }) + + it('should not be displayed organization is loaded but has no pagination', () => { + organizationIsLoading.next(false) + totalPages.next(1) + fixture.detectChanges() + + const orgDetailsNavBtn = getHTMLElement('orgDetailsNavBtn') + + expect(orgDetailsNavBtn).toBeFalsy() + }) + + it('should be displayed if organization is loadded and have pagination', () => { + organizationIsLoading.next(false) + totalPages.next(10) + fixture.detectChanges() + + const orgDetailsNavBtn = getHTMLElement('orgDetailsNavBtn') + + expect(orgDetailsNavBtn).toBeTruthy() + }) + + it('should call paginate from the facade if button is clicked', () => { + const initialPageNumber = currentPage.getValue() + const nextPageNumber = initialPageNumber + 1 + + const orgDetailsNavBtn = getHTMLElement('orgDetailsNavBtn') + + const nextButton = orgDetailsNavBtn?.querySelector( + '[data-test="nextButton"]' + ) as HTMLElement + + ;(nextButton?.firstChild as HTMLElement).click() + fixture.detectChanges() + + expect(searchFacade.paginate).toHaveBeenCalledWith(nextPageNumber) + + const previousButton = orgDetailsNavBtn?.querySelector( + '[data-test="previousButton"]' + ) as HTMLElement + + ;(previousButton?.firstChild as HTMLElement).click() + fixture.detectChanges() + + expect(searchFacade.paginate).toHaveBeenCalledWith(initialPageNumber) + }) + + describe('Search all button', () => { + it('should send to the search page filtered on the correct organization', () => { + const orgDetailsSearchAllBtn = getHTMLElement( + 'orgDetailsSearchAllBtn' + ) + + expect(orgDetailsSearchAllBtn).toBeTruthy() + + expect(orgDetailsSearchAllBtn?.getAttribute('href')).toEqual( + `/${ROUTER_ROUTE_SEARCH}?publisher=${encodeURIComponent( + anOrganizationWithManyDatasets.name + )}` + ) + }) + }) + }) + + describe('Last published datasets', () => { + it('should display the datasets properly', () => { + const orgPageLasPubDat = getHTMLElement('orgPageLasPubDat') + + expect(orgPageLasPubDat).toBeTruthy() + expect(orgPageLasPubDat?.children.length).toEqual(desiredPageSize) + + results.next(oneDataset) + fixture.detectChanges() + + expect(orgPageLasPubDat?.children.length).toEqual(1) + }) + + it('should display the orgHasNodataset error component if the org has no dataset', () => { + results.next([]) + fixture.detectChanges() + + const orgHasNoDataset = getHTMLElement('lastPubliDatasets') + + console.log(orgHasNoDataset?.outerHTML) + + expect(orgHasNoDataset).toBeTruthy() + }) + }) + }) +}) diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.ts b/apps/datahub/src/app/organization/organization-details/organization-details.component.ts new file mode 100644 index 0000000000..2656219bee --- /dev/null +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.ts @@ -0,0 +1,193 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, +} from '@angular/core' +import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common' +import { + CatalogRecord, + Organization, +} from '@geonetwork-ui/common/domain/model/record' +import { + ButtonComponent, + PreviousNextButtonsComponent, +} from '@geonetwork-ui/ui/inputs' +import { MatIconModule } from '@angular/material/icon' +import { TranslateModule } from '@ngx-translate/core' +import { + BlockListComponent, + CarouselComponent, + MaxLinesComponent, +} from '@geonetwork-ui/ui/layout' +import { LetDirective } from '@ngrx/component' +import { + ErrorType, + LinkCardComponent, + UiElementsModule, +} from '@geonetwork-ui/ui/elements' +import { UiSearchModule } from '@geonetwork-ui/ui/search' +import { SearchFacade } from '@geonetwork-ui/feature/search' +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + Observable, + of, + Subscription, + switchMap, +} from 'rxjs' +import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' +import { RouterLink } from '@angular/router' +import { ROUTER_ROUTE_SEARCH } from '@geonetwork-ui/feature/router' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' + +@Component({ + selector: 'datahub-organization-details', + templateUrl: './organization-details.component.html', + styleUrls: ['./organization-details.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + AsyncPipe, + NgIf, + ButtonComponent, + MatIconModule, + TranslateModule, + CarouselComponent, + BlockListComponent, + LetDirective, + LinkCardComponent, + NgForOf, + PreviousNextButtonsComponent, + UiElementsModule, + UiSearchModule, + MaxLinesComponent, + UiDatavizModule, + RouterLink, + UiWidgetsModule, + NgClass, + ], +}) +export class OrganizationDetailsComponent + implements OnInit, AfterViewInit, OnDestroy, OnChanges +{ + protected readonly Error = Error + protected readonly ErrorType = ErrorType + protected readonly ROUTER_ROUTE_SEARCH = ROUTER_ROUTE_SEARCH + + protected get pages() { + return new Array(this.totalPages).fill(0).map((_, i) => i + 1) + } + + lastPublishedDatasets$: Observable = of([]) + + subscriptions$: Subscription = new Subscription() + + isSearchFacadeLoading = true + + totalPages = 0 + currentPage = 1 + isFirstPage = this.currentPage === 1 + isLastPage = false + + organizationHasChanged$ = new BehaviorSubject(undefined) + + @Input() organization?: Organization + @Input() paginationContainerClass = 'w-full bottom-0 top-auto' + + @ViewChild(BlockListComponent) list: BlockListComponent + + constructor( + private changeDetector: ChangeDetectorRef, + private searchFacade: SearchFacade, + private organizationsService: OrganizationsServiceInterface + ) {} + + ngOnInit(): void { + this.searchFacade.setPageSize(3) + + this.lastPublishedDatasets$ = this.organizationHasChanged$.pipe( + distinctUntilChanged(), + switchMap(() => { + return this.organizationsService + .getFiltersForOrgs([this.organization]) + .pipe( + switchMap((filters) => { + return this.searchFacade + .setFilters(filters) + .setSortBy(['desc', 'changeDate']).results$ + }) + ) + }) + ) + + this.manageSubscriptions() + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['organization']) { + this.organizationHasChanged$.next() + } + } + + ngAfterViewInit() { + // this is required to show the pagination correctly + this.changeDetector.detectChanges() + } + + ngOnDestroy(): void { + this.subscriptions$.unsubscribe() + } + + get hasPagination() { + return this.totalPages > 1 + } + + changeStepOrPage(direction: string) { + if (direction === 'next') { + this.searchFacade.paginate(this.currentPage + 1) + } else { + this.searchFacade.paginate(this.currentPage - 1) + } + } + + goToPage(page: number) { + this.searchFacade.paginate(page) + } + + private manageSubscriptions() { + this.subscriptions$.add( + combineLatest([ + this.searchFacade.isLoading$.pipe(distinctUntilChanged()), + this.searchFacade.totalPages$.pipe(distinctUntilChanged()), + this.searchFacade.isBeginningOfResults$.pipe(distinctUntilChanged()), + this.searchFacade.isEndOfResults$.pipe(distinctUntilChanged()), + this.searchFacade.currentPage$.pipe(distinctUntilChanged()), + ]).subscribe( + ([ + isSearchFacadeLoading, + totalPages, + isBeginningOfResults, + isEndOfResults, + currentPage, + ]) => { + this.isSearchFacadeLoading = isSearchFacadeLoading + this.totalPages = totalPages + this.isFirstPage = isBeginningOfResults + this.isLastPage = isEndOfResults + this.currentPage = currentPage + } + ) + ) + } + + protected readonly errorTypes = ErrorType +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.css b/apps/datahub/src/app/organization/organization-header/organization-header.component.css similarity index 100% rename from libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.css rename to apps/datahub/src/app/organization/organization-header/organization-header.component.css diff --git a/apps/datahub/src/app/organization/organization-header/organization-header.component.html b/apps/datahub/src/app/organization/organization-header/organization-header.component.html new file mode 100644 index 0000000000..42e90a94bd --- /dev/null +++ b/apps/datahub/src/app/organization/organization-header/organization-header.component.html @@ -0,0 +1,60 @@ +
+
+
+
+ + +
+
+ +
+
+ +
+ {{ organization.name }} +
+
+ folder +

+ {{ organization.recordCount }} +

+

+ organization.header.recordCount +

+ +

+ + {{ organization.website.href }} + open_in_new +
+
+
+
+
diff --git a/apps/datahub/src/app/organization/organization-header/organization-header.component.spec.ts b/apps/datahub/src/app/organization/organization-header/organization-header.component.spec.ts new file mode 100644 index 0000000000..2ca8215648 --- /dev/null +++ b/apps/datahub/src/app/organization/organization-header/organization-header.component.spec.ts @@ -0,0 +1,70 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { OrganizationHeaderComponent } from './organization-header.component' +import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' +import { UiCatalogModule } from '@geonetwork-ui/ui/catalog' +import { AsyncPipe, Location, NgIf } from '@angular/common' +import { MatIconModule } from '@angular/material/icon' +import { ORGANISATIONS_FIXTURE } from '@geonetwork-ui/common/fixtures' + +jest.mock('@geonetwork-ui/util/app-config', () => ({ + getThemeConfig: () => ({ + HEADER_BACKGROUND: 'red', + HEADER_FOREGROUND_COLOR: 'white', + }), + getGlobalConfig() { + return { + LANGUAGES: ['en', 'es'], + } + }, +})) + +const locationMock: Partial = { + back: jest.fn(), +} + +describe('OrganizationHeaderComponent', () => { + let component: OrganizationHeaderComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + OrganizationHeaderComponent, + UiInputsModule, + TranslateModule, + UiCatalogModule, + NgIf, + MatIconModule, + AsyncPipe, + TranslateModule.forRoot(), + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { + provide: Location, + useValue: locationMock, + }, + ], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(OrganizationHeaderComponent) + component = fixture.componentInstance + component.organization = ORGANISATIONS_FIXTURE[0] + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('#back', () => { + it('calls the back function of Location', () => { + component.back() + expect(locationMock.back).toHaveBeenCalled() + }) + }) +}) diff --git a/apps/datahub/src/app/organization/organization-header/organization-header.component.ts b/apps/datahub/src/app/organization/organization-header/organization-header.component.ts new file mode 100644 index 0000000000..8e62fd986d --- /dev/null +++ b/apps/datahub/src/app/organization/organization-header/organization-header.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { getGlobalConfig, getThemeConfig } from '@geonetwork-ui/util/app-config' +import { TranslateModule } from '@ngx-translate/core' +import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { UiCatalogModule } from '@geonetwork-ui/ui/catalog' +import { Organization } from '@geonetwork-ui/common/domain/model/record' +import { AsyncPipe, Location, NgIf } from '@angular/common' +import { MatIconModule } from '@angular/material/icon' +import { ErrorType, UiElementsModule } from '@geonetwork-ui/ui/elements' +import { Router } from '@angular/router' + +@Component({ + selector: 'datahub-organization-header', + templateUrl: './organization-header.component.html', + styleUrls: ['./organization-header.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + UiInputsModule, + TranslateModule, + UiCatalogModule, + NgIf, + MatIconModule, + AsyncPipe, + UiElementsModule, + ], +}) +export class OrganizationHeaderComponent { + @Input() organization?: Organization + + backgroundCss = + getThemeConfig().HEADER_BACKGROUND || + `center /cover url('assets/img/header_bg.webp')` + foregroundColor = getThemeConfig().HEADER_FOREGROUND_COLOR || '#ffffff' + showLanguageSwitcher = getGlobalConfig().LANGUAGES?.length > 0 + + constructor(private location: Location, private router: Router) {} + + back() { + this.organization + ? this.location.back() + : this.router.navigateByUrl('/organisations') + } + + protected readonly errorTypes = ErrorType +} diff --git a/libs/feature/search/src/lib/results-table/results-table.component.css b/apps/datahub/src/app/organization/organization-page/organization-page.component.css similarity index 100% rename from libs/feature/search/src/lib/results-table/results-table.component.css rename to apps/datahub/src/app/organization/organization-page/organization-page.component.css diff --git a/apps/datahub/src/app/organization/organization-page/organization-page.component.html b/apps/datahub/src/app/organization/organization-page/organization-page.component.html new file mode 100644 index 0000000000..026f5a90df --- /dev/null +++ b/apps/datahub/src/app/organization/organization-page/organization-page.component.html @@ -0,0 +1,12 @@ +
+ + +
diff --git a/apps/datahub/src/app/organization/organization-page/organization-page.component.spec.ts b/apps/datahub/src/app/organization/organization-page/organization-page.component.spec.ts new file mode 100644 index 0000000000..5068e8b7c4 --- /dev/null +++ b/apps/datahub/src/app/organization/organization-page/organization-page.component.spec.ts @@ -0,0 +1,82 @@ +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { OrganizationPageComponent } from './organization-page.component' +import { of } from 'rxjs' +import { ORGANISATIONS_FIXTURE } from '@geonetwork-ui/common/fixtures' +import { RouterFacade } from '@geonetwork-ui/feature/router' +import { Params } from '@angular/router' +import { TranslateModule } from '@ngx-translate/core' +import { EffectsModule } from '@ngrx/effects' +import { StoreModule } from '@ngrx/store' +import { RouterTestingModule } from '@angular/router/testing' + +const expectedOrganization = ORGANISATIONS_FIXTURE[0] + +class RouterFacadeMock { + pathParams$ = of({ name: ORGANISATIONS_FIXTURE[0].name } as Params) +} + +class OrganizationsServiceInterfaceMock { + organisations$ = of(ORGANISATIONS_FIXTURE) +} + +describe('OrganizationPageComponent', () => { + let component: OrganizationPageComponent + let fixture: ComponentFixture + let organizationsServiceInterface: OrganizationsServiceInterface + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + OrganizationPageComponent, + TranslateModule.forRoot({}), + RouterTestingModule, + EffectsModule.forRoot(), + StoreModule.forRoot({}), + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { + provide: RouterFacade, + useClass: RouterFacadeMock, + }, + { + provide: OrganizationsServiceInterface, + useClass: OrganizationsServiceInterfaceMock, + }, + ], + }) + .overrideComponent(OrganizationPageComponent, { + set: { + changeDetection: ChangeDetectionStrategy.Default, + imports: [], + schemas: [NO_ERRORS_SCHEMA], + }, + }) + .compileComponents() + + organizationsServiceInterface = TestBed.inject( + OrganizationsServiceInterface + ) + + fixture = TestBed.createComponent(OrganizationPageComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('#ngOnInit', () => { + beforeEach(() => { + component.ngOnInit() + }) + it('organization$', () => { + component.organization$.subscribe((org) => { + expect(org).toBe(expectedOrganization) + }) + }) + }) +}) diff --git a/apps/datahub/src/app/organization/organization-page/organization-page.component.ts b/apps/datahub/src/app/organization/organization-page/organization-page.component.ts new file mode 100644 index 0000000000..99cc91b16b --- /dev/null +++ b/apps/datahub/src/app/organization/organization-page/organization-page.component.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core' +import { RouterFacade } from '@geonetwork-ui/feature/router' +import { AsyncPipe, NgIf } from '@angular/common' +import { OrganizationHeaderComponent } from '../organization-header/organization-header.component' +import { OrganizationDetailsComponent } from '../organization-details/organization-details.component' +import { combineLatest, Observable, of, switchMap } from 'rxjs' +import { filter } from 'rxjs/operators' +import { Organization } from '@geonetwork-ui/common/domain/model/record' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { LetDirective } from '@ngrx/component' +import { FeatureSearchModule } from '@geonetwork-ui/feature/search' + +@Component({ + selector: 'datahub-organization-page', + templateUrl: './organization-page.component.html', + styleUrls: ['./organization-page.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + AsyncPipe, + OrganizationHeaderComponent, + OrganizationDetailsComponent, + LetDirective, + NgIf, + FeatureSearchModule, + ], +}) +export class OrganizationPageComponent implements OnInit { + organization$: Observable + + constructor( + private router: RouterFacade, + private orgService: OrganizationsServiceInterface + ) {} + + ngOnInit(): void { + this.organization$ = combineLatest([ + this.router.pathParams$, + this.orgService.organisations$, + ]).pipe( + filter(([pathParams, _]) => Object.keys(pathParams).length > 0), + switchMap(([pathParams, organizations]) => { + const organization = organizations.find( + (organization) => organization.name === pathParams['name'] + ) + return of(organization) + }) + ) + } +} diff --git a/apps/datahub/src/app/record/header-record/header-record.component.html b/apps/datahub/src/app/record/header-record/header-record.component.html index 7da801bc11..6046a273c7 100644 --- a/apps/datahub/src/app/record/header-record/header-record.component.html +++ b/apps/datahub/src/app/record/header-record/header-record.component.html @@ -13,7 +13,7 @@
- + >--> { await TestBed.configureTestingModule({ declarations: [RecordApisComponent], imports: [TranslateModule.forRoot()], + schemas: [NO_ERRORS_SCHEMA], providers: [ { provide: MdViewFacade, diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html index efc77e05c4..f33d710821 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html @@ -153,18 +153,15 @@
-
+
{ diff --git a/apps/datahub/src/app/router/datahub-router.service.ts b/apps/datahub/src/app/router/datahub-router.service.ts index 06c0c97030..de25fe46dc 100644 --- a/apps/datahub/src/app/router/datahub-router.service.ts +++ b/apps/datahub/src/app/router/datahub-router.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core' import { Router, Routes } from '@angular/router' import { ROUTER_ROUTE_DATASET, + ROUTER_ROUTE_ORGANIZATION, ROUTER_ROUTE_SEARCH, } from '@geonetwork-ui/feature/router' import { HomePageComponent } from '../home/home-page/home-page.component' @@ -12,8 +13,9 @@ import { RecordPageComponent } from '../record/record-page/record-page.component import { ROUTER_ROUTE_HOME, ROUTER_ROUTE_NEWS, - ROUTER_ROUTE_ORGANISATIONS, + ROUTER_ROUTE_ORGANIZATIONS, } from './constants' +import { OrganizationPageComponent } from '../organization/organization-page/organization-page.component' @Injectable({ providedIn: 'root', @@ -59,7 +61,7 @@ export class DatahubRouterService { }, }, { - path: ROUTER_ROUTE_ORGANISATIONS, + path: ROUTER_ROUTE_ORGANIZATIONS, component: OrganisationsPageComponent, data: { shouldDetach: true, @@ -71,6 +73,13 @@ export class DatahubRouterService { path: `${ROUTER_ROUTE_DATASET}/:metadataUuid`, component: RecordPageComponent, }, + { + path: `${ROUTER_ROUTE_ORGANIZATION}/:name`, + component: OrganizationPageComponent, + data: { + shouldDetach: true, + }, + }, { path: '**', redirectTo: '', pathMatch: 'full' }, ] } @@ -78,4 +87,8 @@ export class DatahubRouterService { getSearchRoute(): string { return `${ROUTER_ROUTE_HOME}/${ROUTER_ROUTE_SEARCH}` } + + getOrganizationPageRoute(): string { + return ROUTER_ROUTE_ORGANIZATION + } } diff --git a/apps/datahub/src/assets/img/favicon.ico b/apps/datahub/src/assets/img/favicon.ico new file mode 100644 index 0000000000..b809743444 Binary files /dev/null and b/apps/datahub/src/assets/img/favicon.ico differ diff --git a/apps/datahub/src/assets/img/logo-ign.png b/apps/datahub/src/assets/img/logo-ign.png new file mode 100644 index 0000000000..77e1bc0d5f Binary files /dev/null and b/apps/datahub/src/assets/img/logo-ign.png differ diff --git a/apps/datahub/src/assets/img/logo-ministere-ecologie.jpg b/apps/datahub/src/assets/img/logo-ministere-ecologie.jpg new file mode 100644 index 0000000000..b279d5c7f8 Binary files /dev/null and b/apps/datahub/src/assets/img/logo-ministere-ecologie.jpg differ diff --git a/apps/datahub/src/assets/img/logo-ministere-transformation.jpg b/apps/datahub/src/assets/img/logo-ministere-transformation.jpg new file mode 100644 index 0000000000..99406f884a Binary files /dev/null and b/apps/datahub/src/assets/img/logo-ministere-transformation.jpg differ diff --git a/apps/datahub/src/assets/img/logo-rf-cnig.jpg b/apps/datahub/src/assets/img/logo-rf-cnig.jpg new file mode 100644 index 0000000000..aeb2fc68cf Binary files /dev/null and b/apps/datahub/src/assets/img/logo-rf-cnig.jpg differ diff --git a/apps/datahub/src/index.html b/apps/datahub/src/index.html index 3b1d0309a6..7d5b326a99 100644 --- a/apps/datahub/src/index.html +++ b/apps/datahub/src/index.html @@ -2,7 +2,7 @@ - Datahub + Catalogue | cartes.gouv.fr @@ -19,6 +19,7 @@ --> + diff --git a/apps/datahub/src/styles.css b/apps/datahub/src/styles.css index fdc787a110..5a94b79c9a 100644 --- a/apps/datahub/src/styles.css +++ b/apps/datahub/src/styles.css @@ -60,7 +60,8 @@ gn-ui-button > button:hover.bg-secondary { background-color: rgba(0, 0, 145, 0.5) !important; } -[target='_blank']::after { +[target='_blank']:not(.fr-footer__bottom-link, .fr-footer__content-link, edu-item-link + > .fr-btn)::after { content: none; } @@ -72,6 +73,6 @@ h2.font-title { color: white; } -a { +a:not(.fr-footer__bottom-link, .fr-footer__content-link) { background-image: none !important; } diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 0908f40c8e..89322e4ee5 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -2,7 +2,7 @@ describe('dashboard', () => { let pageOne describe('pagination', () => { it('should display different results on click on arrow', () => { - cy.visit('/records/search') + cy.visit('/catalog/search') cy.get('gn-ui-results-table') .find('.table-row-cell') .first() @@ -13,7 +13,7 @@ describe('dashboard', () => { }) //TODO remove skip when dump contains more than 15 records it.skip('should display different results on click on specific page and change url', () => { - cy.visit('/records/search?_page=2') + cy.visit('/catalog/search?_page=2') cy.get('gn-ui-pagination-buttons').find('gn-ui-button').eq(1).click() cy.get('gn-ui-results-table') .find('.table-row-cell') @@ -36,8 +36,8 @@ describe('dashboard', () => { }) describe('sorting', () => { - it('should order the result list on click', () => { - cy.visit('/records/search') + it.only('should order the result list on click', () => { + cy.visit('/catalog/search') cy.get('gn-ui-results-table') .find('.table-row-cell') .eq(1) @@ -65,7 +65,7 @@ describe('dashboard', () => { describe('checkboxes', () => { it('should show the correct amount of selected records when they are selected', () => { - cy.visit('/records/search') + cy.visit('/catalog/search') cy.get('gn-ui-results-table') .find('.table-row-cell') .get('gn-ui-checkbox') @@ -75,7 +75,7 @@ describe('dashboard', () => { }) it('should show nothing when none are selected', () => { - cy.visit('/records/search') + cy.visit('/catalog/search') cy.get('gn-ui-results-table') .find('.table-row-cell') .get('gn-ui-checkbox') @@ -87,7 +87,7 @@ describe('dashboard', () => { }) it('should select all records when the "select all" checkbox is checked', () => { - cy.visit('/records/search') + cy.visit('/catalog/search') cy.get('gn-ui-results-table') .find('.table-row-cell') .get('gn-ui-checkbox') diff --git a/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts b/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts index a730895c2d..14ddf94cf9 100644 --- a/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/my-org.cy.ts @@ -5,8 +5,7 @@ describe('my-org', () => { method: 'GET', url: '/geonetwork/srv/api/userselections/0/101', }).as('dataGetFirst') - cy.visit(`/records/my-org`) - cy.get('md-editor-dashboard-menu').find('a').first().click() + cy.visit(`/catalog/my-org`) cy.wait('@dataGetFirst').its('response.statusCode').should('equal', 200) }) describe('my-org display', () => { @@ -31,8 +30,7 @@ describe('my-org', () => { }) }) it('should access the user list page and show my-org users', () => { - cy.visit(`/records/my-org`) - cy.get('md-editor-dashboard-menu').find('a').first().click() + cy.visit(`/catalog/my-org`) cy.get('[data-cy=link-to-users]').click() cy.url().should('include', '/users/my-org') cy.get('gn-ui-interactive-table .contents').should('have.length.above', 1) diff --git a/apps/metadata-editor/src/app/app.module.ts b/apps/metadata-editor/src/app/app.module.ts index ec73edecd0..db79237697 100644 --- a/apps/metadata-editor/src/app/app.module.ts +++ b/apps/metadata-editor/src/app/app.module.ts @@ -45,6 +45,7 @@ import { FeatureEditorModule } from '@geonetwork-ui/feature/editor' searchStateId: 'editor', searchRouteComponent: DashboardPageComponent, recordRouteComponent: null, + organizationRouteComponent: null, }), ...extModules, ], diff --git a/apps/metadata-editor/src/app/app.routes.ts b/apps/metadata-editor/src/app/app.routes.ts index b4f3b4e341..878df29ce9 100644 --- a/apps/metadata-editor/src/app/app.routes.ts +++ b/apps/metadata-editor/src/app/app.routes.ts @@ -3,17 +3,18 @@ import { DashboardPageComponent } from './dashboard/dashboard-page.component' import { SignInPageComponent } from './sign-in/sign-in-page.component' import { EditPageComponent } from './edit/edit-page.component' import { EditRecordResolver } from './edit-record.resolver' -import { MyOrgRecordsComponent } from './records/my-org-records/my-org-records.component' import { MyRecordsComponent } from './records/my-records/my-records.component' import { MyDraftComponent } from './records/my-draft/my-draft.component' import { MyLibraryComponent } from './records/my-library/my-library.component' import { SearchRecordsComponent } from './records/search-records/search-records-list.component' import { MyOrgUsersComponent } from './my-org-users/my-org-users.component' +import { MyOrgRecordsComponent } from './records/my-org-records/my-org-records.component' +import { NewRecordResolver } from './new-record.resolver' export const appRoutes: Route[] = [ - { path: '', component: DashboardPageComponent, pathMatch: 'prefix' }, + { path: '', redirectTo: 'catalog/search', pathMatch: 'prefix' }, { - path: 'records', + path: 'catalog', component: DashboardPageComponent, outlet: 'primary', children: [ @@ -22,12 +23,45 @@ export const appRoutes: Route[] = [ redirectTo: 'search', pathMatch: 'prefix', }, + { + path: 'discussion', + component: SearchRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'calendar', + component: SearchRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'contacts', + component: SearchRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'thesaurus', + component: SearchRecordsComponent, + pathMatch: 'prefix', + }, + { + path: 'search', + title: 'Search Records', + component: SearchRecordsComponent, + pathMatch: 'prefix', + }, { path: 'my-org', title: 'My Organisation', component: MyOrgRecordsComponent, - pathMatch: 'prefix', }, + ], + }, + { + path: 'my-space', + component: DashboardPageComponent, + outlet: 'primary', + title: 'My space', + children: [ { path: 'my-records', title: 'My Records', @@ -41,17 +75,11 @@ export const appRoutes: Route[] = [ pathMatch: 'prefix', }, { - path: 'my-library', - title: 'My Library', + path: 'templates', + title: 'Templates', component: MyLibraryComponent, pathMatch: 'prefix', }, - { - path: 'search', - title: 'Search Records', - component: SearchRecordsComponent, - pathMatch: 'prefix', - }, ], }, { @@ -68,7 +96,11 @@ export const appRoutes: Route[] = [ ], }, { path: 'sign-in', component: SignInPageComponent }, - { path: 'create', component: EditPageComponent }, + { + path: 'create', + component: EditPageComponent, + resolve: { record: NewRecordResolver }, + }, { path: 'edit/:uuid', component: EditPageComponent, diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html index c723ebc1a9..ed67fe8b72 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html @@ -1,50 +1,84 @@ -
- - home - dashboard.records.myOrg - + diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts index 823b8eb3af..2e04d1d6f7 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.spec.ts @@ -4,6 +4,12 @@ import { ActivatedRoute } from '@angular/router' import { TranslateModule } from '@ngx-translate/core' import { of } from 'rxjs' import { DashboardMenuComponent } from './dashboard-menu.component' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' + +class RecordsRepositoryMock { + getAllDrafts = jest.fn().mockReturnValue(of(DATASET_RECORDS)) +} describe('DashboardMenuComponent', () => { let component: DashboardMenuComponent @@ -17,6 +23,10 @@ describe('DashboardMenuComponent', () => { provide: ActivatedRoute, useValue: { params: of({ id: 1 }) }, }, + { + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents() diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts index e11b833cf8..4845040432 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts @@ -3,6 +3,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { MatIconModule } from '@angular/material/icon' import { RouterModule } from '@angular/router' import { TranslateModule } from '@ngx-translate/core' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { map } from 'rxjs/operators' +import { BadgeComponent } from '@geonetwork-ui/ui/inputs' @Component({ selector: 'md-editor-dashboard-menu', @@ -10,6 +13,18 @@ import { TranslateModule } from '@ngx-translate/core' styleUrls: ['./dashboard-menu.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, RouterModule, MatIconModule, TranslateModule], + imports: [ + CommonModule, + RouterModule, + MatIconModule, + TranslateModule, + BadgeComponent, + ], }) -export class DashboardMenuComponent {} +export class DashboardMenuComponent { + draftsCount$ = this.recordsRepository + .getAllDrafts() + .pipe(map((drafts) => drafts.length)) + + constructor(private recordsRepository: RecordsRepositoryInterface) {} +} diff --git a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html index 311cd19287..7487ff5bf0 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts index b036c7dd07..08fcdb3314 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts @@ -4,6 +4,12 @@ import { of } from 'rxjs' import { SidebarComponent } from './sidebar.component' import { ActivatedRoute } from '@angular/router' import { TranslateModule } from '@ngx-translate/core' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' + +class RecordsRepositoryMock { + getAllDrafts = jest.fn().mockReturnValue(of(DATASET_RECORDS)) +} describe('SidebarComponent', () => { let component: SidebarComponent @@ -17,6 +23,10 @@ describe('SidebarComponent', () => { provide: ActivatedRoute, useValue: { params: of({ id: 1 }) }, }, + { + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, + }, ], schemas: [NO_ERRORS_SCHEMA], }) diff --git a/apps/metadata-editor/src/app/edit-record.resolver.spec.ts b/apps/metadata-editor/src/app/edit-record.resolver.spec.ts index 15d76f32a2..f8de1c1e1c 100644 --- a/apps/metadata-editor/src/app/edit-record.resolver.spec.ts +++ b/apps/metadata-editor/src/app/edit-record.resolver.spec.ts @@ -4,16 +4,18 @@ import { HttpClientTestingModule } from '@angular/common/http/testing' import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { of, throwError } from 'rxjs' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' -import { EditorService } from '@geonetwork-ui/feature/editor' import { ActivatedRouteSnapshot, convertToParamMap } from '@angular/router' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { TranslateModule } from '@ngx-translate/core' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' class NotificationsServiceMock { showNotification = jest.fn() } -class EditorServiceMock { - loadRecordByUuid = jest.fn(() => of(DATASET_RECORDS[0])) +class RecordsRepositoryMock { + openRecordForEdition = jest.fn(() => + of([DATASET_RECORDS[0], 'blabla', false]) + ) } const activatedRoute = { @@ -22,20 +24,23 @@ const activatedRoute = { describe('EditRecordResolver', () => { let resolver: EditRecordResolver - let editorService: EditorService + let recordsRepository: RecordsRepositoryInterface let notificationsService: NotificationsService - let record: CatalogRecord + let resolvedData: [CatalogRecord, string, boolean] beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule, TranslateModule.forRoot()], providers: [ { provide: NotificationsService, useClass: NotificationsServiceMock }, - { provide: EditorService, useClass: EditorServiceMock }, + { + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, + }, ], }) resolver = TestBed.inject(EditRecordResolver) - editorService = TestBed.inject(EditorService) + recordsRepository = TestBed.inject(RecordsRepositoryInterface) notificationsService = TestBed.inject(NotificationsService) }) @@ -45,23 +50,27 @@ describe('EditRecordResolver', () => { describe('load record success', () => { beforeEach(() => { - record = undefined - resolver.resolve(activatedRoute, null).subscribe((r) => (record = r)) + resolvedData = undefined + resolver.resolve(activatedRoute).subscribe((r) => (resolvedData = r)) }) it('should load record by uuid', () => { - expect(record).toBe(DATASET_RECORDS[0]) + expect(resolvedData).toEqual([ + DATASET_RECORDS[0], + 'blabla', + false, + ]) }) }) describe('load record failure', () => { beforeEach(() => { - editorService.loadRecordByUuid = () => + recordsRepository.openRecordForEdition = () => throwError(() => new Error('oopsie')) - record = undefined - resolver.resolve(activatedRoute, null).subscribe((r) => (record = r)) + resolvedData = undefined + resolver.resolve(activatedRoute).subscribe((r) => (resolvedData = r)) }) it('should not emit anything', () => { - expect(record).toBeUndefined() + expect(resolvedData).toBeUndefined() }) it('should show error notification', () => { expect(notificationsService.showNotification).toHaveBeenCalledWith({ diff --git a/apps/metadata-editor/src/app/edit-record.resolver.ts b/apps/metadata-editor/src/app/edit-record.resolver.ts index 96cfb312db..337a1bf858 100644 --- a/apps/metadata-editor/src/app/edit-record.resolver.ts +++ b/apps/metadata-editor/src/app/edit-record.resolver.ts @@ -1,39 +1,42 @@ import { Injectable } from '@angular/core' -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router' +import { ActivatedRouteSnapshot } from '@angular/router' import { catchError, EMPTY, Observable } from 'rxjs' -import { EditorService } from '@geonetwork-ui/feature/editor' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { TranslateService } from '@ngx-translate/core' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @Injectable({ providedIn: 'root', }) export class EditRecordResolver { constructor( - private editorService: EditorService, + private recordsRepository: RecordsRepositoryInterface, private notificationsService: NotificationsService, private translateService: TranslateService ) {} resolve( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot - ): Observable { - return this.editorService.loadRecordByUuid(route.paramMap.get('uuid')).pipe( - catchError((error) => { - this.notificationsService.showNotification({ - type: 'error', - title: this.translateService.instant('editor.record.loadError.title'), - text: `${this.translateService.instant( - 'editor.record.loadError.body' - )} ${error.message}`, - closeMessage: this.translateService.instant( - 'editor.record.loadError.closeMessage' - ), + route: ActivatedRouteSnapshot + ): Observable<[CatalogRecord, string, boolean]> { + return this.recordsRepository + .openRecordForEdition(route.paramMap.get('uuid')) + .pipe( + catchError((error) => { + this.notificationsService.showNotification({ + type: 'error', + title: this.translateService.instant( + 'editor.record.loadError.title' + ), + text: `${this.translateService.instant( + 'editor.record.loadError.body' + )} ${error.message}`, + closeMessage: this.translateService.instant( + 'editor.record.loadError.closeMessage' + ), + }) + return EMPTY }) - return EMPTY - }) - ) + ) } } diff --git a/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.html b/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.html index 365dcea5af..aab1948240 100644 --- a/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.html +++ b/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.html @@ -11,7 +11,33 @@ undo -
Save status
+
+ + check_circle + editor.record.saveStatus.asDraftOnly + + + check_circle + editor.record.saveStatus.recordUpToDate + + + pending + editor.record.saveStatus.draftWithChangesPending + +
help diff --git a/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.spec.ts b/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.spec.ts index 77c3cb5b09..5c37c3f73f 100644 --- a/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.spec.ts +++ b/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.spec.ts @@ -2,6 +2,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { TopToolbarComponent } from './top-toolbar.component' import { Component } from '@angular/core' import { PublishButtonComponent } from '../publish-button/publish-button.component' +import { BehaviorSubject } from 'rxjs' +import { EditorFacade } from '@geonetwork-ui/feature/editor' +import { TranslateModule } from '@ngx-translate/core' + +class EditorFacadeMock { + changedSinceSave$ = new BehaviorSubject(false) + alreadySavedOnce$ = new BehaviorSubject(false) +} @Component({ selector: 'md-editor-publish-button', @@ -13,10 +21,17 @@ class MockPublishButtonComponent {} describe('TopToolbarComponent', () => { let component: TopToolbarComponent let fixture: ComponentFixture + let editorFacade: EditorFacadeMock beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TopToolbarComponent], + imports: [TopToolbarComponent, TranslateModule.forRoot()], + providers: [ + { + provide: EditorFacade, + useClass: EditorFacadeMock, + }, + ], }) .overrideComponent(TopToolbarComponent, { add: { @@ -30,10 +45,47 @@ describe('TopToolbarComponent', () => { fixture = TestBed.createComponent(TopToolbarComponent) component = fixture.componentInstance + editorFacade = TestBed.inject(EditorFacade) as any fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + + describe('save status', () => { + let saveStatus: string + beforeEach(() => { + component['saveStatus$'].subscribe((status) => { + saveStatus = status + }) + }) + describe('saved and not published', () => { + beforeEach(() => { + editorFacade.alreadySavedOnce$.next(false) + editorFacade.changedSinceSave$.next(true) + }) + it('sets the correct status', () => { + expect(saveStatus).toBe('draft_only') + }) + }) + describe('saved, published and up to date', () => { + beforeEach(() => { + editorFacade.alreadySavedOnce$.next(true) + editorFacade.changedSinceSave$.next(false) + }) + it('sets the correct status', () => { + expect(saveStatus).toBe('record_up_to_date') + }) + }) + describe('saved, published, pending changes', () => { + beforeEach(() => { + editorFacade.alreadySavedOnce$.next(true) + editorFacade.changedSinceSave$.next(true) + }) + it('sets the correct status', () => { + expect(saveStatus).toBe('draft_changes_pending') + }) + }) + }) }) diff --git a/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.ts b/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.ts index c7717d8342..36e6cba0e3 100644 --- a/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.ts +++ b/apps/metadata-editor/src/app/edit/components/top-toolbar/top-toolbar.component.ts @@ -3,6 +3,10 @@ import { CommonModule } from '@angular/common' import { PublishButtonComponent } from '../publish-button/publish-button.component' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { MatIconModule } from '@angular/material/icon' +import { EditorFacade } from '@geonetwork-ui/feature/editor' +import { combineLatest, Observable } from 'rxjs' +import { map } from 'rxjs/operators' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'md-editor-top-toolbar', @@ -12,9 +16,35 @@ import { MatIconModule } from '@angular/material/icon' PublishButtonComponent, ButtonComponent, MatIconModule, + TranslateModule, ], templateUrl: './top-toolbar.component.html', styleUrls: ['./top-toolbar.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TopToolbarComponent {} +export class TopToolbarComponent { + protected SaveStatus = [ + 'draft_only', // => when creating a record + 'record_up_to_date', // => when the record was just published (ie saved on the server) + 'draft_changes_pending', // => when the record was modified and not yet published + // these are not used since the draft is saved locally in a synchronous way + // TODO: use these states when the draft is saved on the server + // 'draft_saving', + // 'draft_saving_failed', + ] as const + + protected saveStatus$: Observable = + combineLatest([ + this.editorFacade.alreadySavedOnce$, + this.editorFacade.changedSinceSave$, + ]).pipe( + map(([alreadySavedOnce, changedSinceSave]) => { + if (!alreadySavedOnce) { + return 'draft_only' + } + return changedSinceSave ? 'draft_changes_pending' : 'record_up_to_date' + }) + ) + + constructor(private editorFacade: EditorFacade) {} +} diff --git a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts index 7b2671b56b..401d9541ce 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts @@ -1,25 +1,34 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { EditPageComponent } from './edit-page.component' -import { ActivatedRoute } from '@angular/router' +import { ActivatedRoute, Router } from '@angular/router' import { EditorFacade } from '@geonetwork-ui/feature/editor' import { NO_ERRORS_SCHEMA } from '@angular/core' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' -import { Subject } from 'rxjs' +import { BehaviorSubject, Subject } from 'rxjs' import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { TranslateModule } from '@ngx-translate/core' const getRoute = () => ({ snapshot: { data: { - record: DATASET_RECORDS[0], + record: [DATASET_RECORDS[0], 'blabla', false], + }, + routeConfig: { + path: '/edit/:uuid', }, }, }) +class RouterMock { + navigate = jest.fn() +} + class EditorFacadeMock { + record$ = new BehaviorSubject(DATASET_RECORDS[0]) openRecord = jest.fn() saveError$ = new Subject() saveSuccess$ = new Subject() + draftSaveSuccess$ = new Subject() } class NotificationsServiceMock { showNotification = jest.fn() @@ -48,6 +57,10 @@ describe('EditPageComponent', () => { provide: NotificationsService, useClass: NotificationsServiceMock, }, + { + provide: Router, + useClass: RouterMock, + }, ], }).compileComponents() @@ -55,42 +68,83 @@ describe('EditPageComponent', () => { notificationsService = TestBed.inject(NotificationsService) fixture = TestBed.createComponent(EditPageComponent) component = fixture.componentInstance - fixture.detectChanges() }) it('should create', () => { + fixture.detectChanges() expect(component).toBeTruthy() }) describe('initial state', () => { + beforeEach(() => { + fixture.detectChanges() + }) it('calls openRecord', () => { - expect(facade.openRecord).toHaveBeenCalledWith(DATASET_RECORDS[0]) + expect(facade.openRecord).toHaveBeenCalledWith( + DATASET_RECORDS[0], + 'blabla', + false + ) }) }) - describe('publish error', () => { - it('shows notification', () => { - ;(facade.saveError$ as any).next('oopsie') - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - title: 'editor.record.publishError.title', - text: 'editor.record.publishError.body oopsie', - closeMessage: 'editor.record.publishError.closeMessage', + describe('notifications', () => { + beforeEach(() => { + fixture.detectChanges() + }) + describe('publish error', () => { + it('shows notification', () => { + ;(facade.saveError$ as any).next('oopsie') + expect(notificationsService.showNotification).toHaveBeenCalledWith({ + type: 'error', + title: 'editor.record.publishError.title', + text: 'editor.record.publishError.body oopsie', + closeMessage: 'editor.record.publishError.closeMessage', + }) + }) + }) + + describe('publish success', () => { + it('shows notification', () => { + ;(facade.saveSuccess$ as any).next() + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'success', + title: 'editor.record.publishSuccess.title', + text: 'editor.record.publishSuccess.body', + }, + 2500 + ) }) }) }) - describe('publish success', () => { - it('shows notification', () => { - ;(facade.saveSuccess$ as any).next() - expect(notificationsService.showNotification).toHaveBeenCalledWith( - { - type: 'success', - title: 'editor.record.publishSuccess.title', - text: 'editor.record.publishSuccess.body', - }, - 2500 - ) + describe('new record', () => { + beforeEach(() => { + const activatedRoute = TestBed.inject(ActivatedRoute) + activatedRoute.snapshot.routeConfig.path = '/create' + fixture.detectChanges() + }) + it('navigate from /create to /edit/uuid on first change', () => { + const router = TestBed.inject(Router) + const navigateSpy = jest.spyOn(router, 'navigate') + ;(facade.draftSaveSuccess$ as any).next() + expect(navigateSpy).toHaveBeenCalledWith(['edit', 'my-dataset-001']) + }) + }) + + describe('unique identifier of the current record changes', () => { + beforeEach(() => { + fixture.detectChanges() + }) + it('navigates to /edit/newUuid', () => { + const router = TestBed.inject(Router) + const navigateSpy = jest.spyOn(router, 'navigate') + ;(facade.record$ as any).next({ + ...DATASET_RECORDS[0], + uniqueIdentifier: 'new-uuid', + }) + expect(navigateSpy).toHaveBeenCalledWith(['edit', 'new-uuid']) }) }) }) diff --git a/apps/metadata-editor/src/app/edit/edit-page.component.ts b/apps/metadata-editor/src/app/edit/edit-page.component.ts index 88373fc351..7327286a2c 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common' import { Component, OnDestroy, OnInit } from '@angular/core' -import { ActivatedRoute } from '@angular/router' +import { ActivatedRoute, Router } from '@angular/router' import { EditorFacade, RecordFormComponent, @@ -14,7 +14,7 @@ import { NotificationsService, } from '@geonetwork-ui/feature/notifications' import { TranslateService } from '@ngx-translate/core' -import { Subscription } from 'rxjs' +import { filter, Subscription, take } from 'rxjs' @Component({ selector: 'md-editor-edit', @@ -38,12 +38,18 @@ export class EditPageComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private facade: EditorFacade, private notificationsService: NotificationsService, - private translateService: TranslateService + private translateService: TranslateService, + private router: Router ) {} ngOnInit(): void { - const currentRecord = this.route.snapshot.data['record'] - this.facade.openRecord(currentRecord) + const [currentRecord, currentRecordSource, currentRecordAlreadySaved] = + this.route.snapshot.data['record'] + this.facade.openRecord( + currentRecord, + currentRecordSource, + currentRecordAlreadySaved + ) this.subscription.add( this.facade.saveError$.subscribe((error) => { @@ -78,6 +84,26 @@ export class EditPageComponent implements OnInit, OnDestroy { ) }) ) + + // if we're on the /create route, go to /edit/{uuid} on first change + if (this.route.snapshot.routeConfig?.path.includes('create')) { + this.facade.draftSaveSuccess$.pipe(take(1)).subscribe(() => { + this.router.navigate(['edit', currentRecord.uniqueIdentifier]) + }) + } + + // if the record unique identifier changes, navigate to /edit/newUuid + this.facade.record$ + .pipe( + filter( + (record) => + record?.uniqueIdentifier !== currentRecord.uniqueIdentifier + ), + take(1) + ) + .subscribe((savedRecord) => { + this.router.navigate(['edit', savedRecord.uniqueIdentifier]) + }) } ngOnDestroy() { diff --git a/apps/metadata-editor/src/app/new-record.resolver.spec.ts b/apps/metadata-editor/src/app/new-record.resolver.spec.ts new file mode 100644 index 0000000000..6f88be9b7a --- /dev/null +++ b/apps/metadata-editor/src/app/new-record.resolver.spec.ts @@ -0,0 +1,39 @@ +import { TestBed } from '@angular/core/testing' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { NewRecordResolver } from './new-record.resolver' + +describe('NewRecordResolver', () => { + let resolver: NewRecordResolver + let resolvedData: [CatalogRecord, string, boolean] + + beforeEach(() => { + TestBed.configureTestingModule({}) + resolver = TestBed.inject(NewRecordResolver) + }) + + it('should be created', () => { + expect(resolver).toBeTruthy() + }) + + describe('new record', () => { + beforeEach(() => { + resolvedData = undefined + resolver.resolve().subscribe((r) => (resolvedData = r)) + }) + it('creates a new empty record with a pregenerated id', () => { + expect(resolvedData).toMatchObject([ + { + abstract: '', + kind: 'dataset', + recordUpdated: expect.any(Date), + status: 'ongoing', + temporalExtents: [], + title: expect.stringMatching(/^My new record/), + uniqueIdentifier: expect.stringMatching(/^TEMP-ID-/), + }, + null, + false, + ]) + }) + }) +}) diff --git a/apps/metadata-editor/src/app/new-record.resolver.ts b/apps/metadata-editor/src/app/new-record.resolver.ts new file mode 100644 index 0000000000..ec206eeb99 --- /dev/null +++ b/apps/metadata-editor/src/app/new-record.resolver.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core' +import { Observable, of } from 'rxjs' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' + +@Injectable({ + providedIn: 'root', +}) +export class NewRecordResolver { + resolve(): Observable<[CatalogRecord, string, boolean]> { + return of([ + { + uniqueIdentifier: `TEMP-ID-${Date.now()}`, + title: `My new record (${new Date().toISOString()})`, + abstract: '', + ownerOrganization: {}, + contacts: [], + recordUpdated: new Date(), + updateFrequency: 'unknown', + languages: [], + topics: [], + keywords: [], + licenses: [], + legalConstraints: [], + securityConstraints: [], + otherConstraints: [], + overviews: [], + contactsForResource: [], + kind: 'dataset', + status: 'ongoing', + lineage: '', + distributions: [], + spatialExtents: [], + temporalExtents: [], + } as CatalogRecord, + null, + false, + ]) + } +} diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html index 48f670e775..de8be72269 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html @@ -1,2 +1,17 @@ - - +
+
+

+ dashboard.records.myDraft +

+
+ +
+ +
+
diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts index b6f7b1ba75..4c42919839 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts @@ -1,9 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { MyDraftComponent } from './my-draft.component' -import { SearchFacade } from '@geonetwork-ui/feature/search' import { Component, importProvidersFrom } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' import { RecordsListComponent } from '../records-list.component' +import { of } from 'rxjs' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @Component({ selector: 'md-editor-records-list', @@ -12,22 +14,21 @@ import { RecordsListComponent } from '../records-list.component' }) export class MockRecordsListComponent {} -class SearchFacadeMock { - resetSearch = jest.fn() +class RecordsRepositoryMock { + getAllDrafts = jest.fn().mockReturnValue(of(DATASET_RECORDS)) } describe('MyDraftComponent', () => { let component: MyDraftComponent let fixture: ComponentFixture - let searchFacade: SearchFacade beforeEach(() => { TestBed.configureTestingModule({ providers: [ importProvidersFrom(TranslateModule.forRoot()), { - provide: SearchFacade, - useClass: SearchFacadeMock, + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, }, ], }).overrideComponent(MyDraftComponent, { @@ -38,7 +39,6 @@ describe('MyDraftComponent', () => { imports: [MockRecordsListComponent], }, }) - searchFacade = TestBed.inject(SearchFacade) fixture = TestBed.createComponent(MyDraftComponent) component = fixture.componentInstance fixture.detectChanges() @@ -48,9 +48,9 @@ describe('MyDraftComponent', () => { expect(component).toBeTruthy() }) - describe('filters', () => { - it('clears filters on init', () => { - expect(searchFacade.resetSearch).toHaveBeenCalled() - }) + it('gets all drafts on init', () => { + expect( + TestBed.inject(RecordsRepositoryInterface).getAllDrafts + ).toHaveBeenCalled() }) }) diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts index 36c10fe8ae..cd9d312037 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts @@ -2,17 +2,44 @@ import { Component } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' import { RecordsListComponent } from '../records-list.component' -import { SearchFacade } from '@geonetwork-ui/feature/search' +import { ResultsTableContainerComponent } from '@geonetwork-ui/feature/search' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { MatIconModule } from '@angular/material/icon' +import { RecordsCountComponent } from '../records-count/records-count.component' +import { UiElementsModule } from '@geonetwork-ui/ui/elements' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { Router } from '@angular/router' +import { ResultsTableComponent } from '@geonetwork-ui/ui/search' +import { startWith } from 'rxjs' @Component({ selector: 'md-editor-my-my-draft', templateUrl: './my-draft.component.html', styleUrls: ['./my-draft.component.css'], standalone: true, - imports: [CommonModule, TranslateModule, RecordsListComponent], + imports: [ + CommonModule, + TranslateModule, + RecordsListComponent, + ButtonComponent, + MatIconModule, + RecordsCountComponent, + ResultsTableContainerComponent, + UiElementsModule, + ResultsTableComponent, + ], }) export class MyDraftComponent { - constructor(public searchFacade: SearchFacade) { - this.searchFacade.resetSearch() + records$ = this.recordsRepository.getAllDrafts().pipe(startWith([])) + hasDraft = () => true + + constructor( + private router: Router, + public recordsRepository: RecordsRepositoryInterface + ) {} + + editRecord(record: CatalogRecord) { + this.router.navigate(['/edit', record.uniqueIdentifier]) } } diff --git a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts index 6fee469372..eb6e675179 100644 --- a/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts +++ b/apps/metadata-editor/src/app/records/my-org-records/my-org-records.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy } from '@angular/core' +import { Component } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' import { RecordsListComponent } from '../records-list.component' @@ -7,7 +7,6 @@ import { SearchFacade } from '@geonetwork-ui/feature/search' import { Organization } from '@geonetwork-ui/common/domain/model/record' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { EditorRouterService } from '../../router.service' -import { UserModel } from '@geonetwork-ui/common/domain/model/user/user.model' import { take } from 'rxjs' @Component({ @@ -49,7 +48,6 @@ export class MyOrgRecordsComponent { this.router.getDatahubSearchRoute(), window.location.toString() ) - url.searchParams.append('publisher', this.orgName) return url.toString() } diff --git a/apps/metadata-editor/src/app/records/records-count/records-count.component.ts b/apps/metadata-editor/src/app/records/records-count/records-count.component.ts index 2b500daf90..65f5635c28 100644 --- a/apps/metadata-editor/src/app/records/records-count/records-count.component.ts +++ b/apps/metadata-editor/src/app/records/records-count/records-count.component.ts @@ -1,9 +1,4 @@ -import { - Component, - EventEmitter, - importProvidersFrom, - Output, -} from '@angular/core' +import { Component, EventEmitter, Output } from '@angular/core' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { diff --git a/apps/metadata-editor/src/app/records/records-list.component.html b/apps/metadata-editor/src/app/records/records-list.component.html index cb63129efb..7b214aa574 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -1,4 +1,4 @@ -
+
-
-

{{ title }}

+
+

+ {{ title }} +

+
+ +
@@ -43,10 +48,12 @@

{{ title }}

-
- + + >
() } @@ -38,6 +37,13 @@ export class PaginationButtonsComponent { @Output() newCurrentPageEvent = new EventEmitter() } +@Component({ + selector: 'md-editor-records-count', + template: '', + standalone: true, +}) +export class RecordsCountComponent {} + class SearchFacadeMock { results$ = new BehaviorSubject(results) currentPage$ = new BehaviorSubject(currentPage) @@ -82,8 +88,9 @@ describe('RecordsListComponent', () => { imports: [ CommonModule, MatIconModule, - RecordTableComponent, + ResultsTableContainerComponent, PaginationButtonsComponent, + RecordsCountComponent, ], }, }) @@ -102,7 +109,7 @@ describe('RecordsListComponent', () => { let table, pagination beforeEach(() => { table = fixture.debugElement.query( - By.directive(RecordTableComponent) + By.directive(ResultsTableContainerComponent) ).componentInstance pagination = fixture.debugElement.query( By.directive(PaginationButtonsComponent) diff --git a/apps/metadata-editor/src/app/records/records-list.component.ts b/apps/metadata-editor/src/app/records/records-list.component.ts index 2eee0525ed..a9f4b9801f 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.ts @@ -4,7 +4,7 @@ import { MatIconModule } from '@angular/material/icon' import { Router } from '@angular/router' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { - ResultsTableComponent, + ResultsTableContainerComponent, SearchFacade, SearchService, } from '@geonetwork-ui/feature/search' @@ -12,6 +12,7 @@ import { UiSearchModule } from '@geonetwork-ui/ui/search' import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { TranslateModule } from '@ngx-translate/core' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { RecordsCountComponent } from './records-count/records-count.component' const includes = [ 'uuid', @@ -36,8 +37,9 @@ const includes = [ UiSearchModule, UiElementsModule, TranslateModule, - ResultsTableComponent, + ResultsTableContainerComponent, UiInputsModule, + RecordsCountComponent, ], }) export class RecordsListComponent { diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html index 1379197fc3..3ad6cb07f9 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html +++ b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html @@ -40,10 +40,10 @@

- + >
diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts index 7d44779e66..600317c1ff 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts +++ b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts @@ -24,11 +24,11 @@ const totalPages = 25 @Component({ // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-results-table', + selector: 'gn-ui-results-table-container', template: '', standalone: true, }) -export class RecordTableComponent { +export class ResultsTableContainerComponent { @Output() recordClick = new EventEmitter() } @@ -103,7 +103,7 @@ describe('SearchRecordsComponent', () => { CommonModule, TranslateModule, MatIconModule, - RecordTableComponent, + ResultsTableContainerComponent, PaginationButtonsComponent, UiInputsModule, RecordsCountComponent, @@ -125,7 +125,7 @@ describe('SearchRecordsComponent', () => { let table, pagination beforeEach(() => { table = fixture.debugElement.query( - By.directive(RecordTableComponent) + By.directive(ResultsTableContainerComponent) ).componentInstance pagination = fixture.debugElement.query( By.directive(PaginationButtonsComponent) diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts index b2bdd73645..7bd7ee44b8 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts +++ b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts @@ -1,11 +1,10 @@ import { CommonModule } from '@angular/common' import { Component } from '@angular/core' import { - ResultsTableComponent, + ResultsTableContainerComponent, SearchFacade, SearchService, } from '@geonetwork-ui/feature/search' -import { RecordsListComponent } from '../records-list.component' import { TranslateModule } from '@ngx-translate/core' import { map } from 'rxjs/operators' import { Router } from '@angular/router' @@ -24,9 +23,8 @@ import { MatIconModule } from '@angular/material/icon' imports: [ CommonModule, TranslateModule, - RecordsListComponent, RecordsCountComponent, - ResultsTableComponent, + ResultsTableContainerComponent, UiElementsModule, UiInputsModule, MatIconModule, diff --git a/apps/metadata-editor/src/app/router.service.ts b/apps/metadata-editor/src/app/router.service.ts index 5a1b48121d..368f6fbc84 100644 --- a/apps/metadata-editor/src/app/router.service.ts +++ b/apps/metadata-editor/src/app/router.service.ts @@ -14,7 +14,7 @@ export class EditorRouterService { } getSearchRoute(): string { - return 'records/search' + return 'catalog/search' } getDatahubSearchRoute(): string { diff --git a/apps/metadata-editor/src/styles.css b/apps/metadata-editor/src/styles.css index 85c4ae6deb..29fec93421 100644 --- a/apps/metadata-editor/src/styles.css +++ b/apps/metadata-editor/src/styles.css @@ -16,10 +16,10 @@ body { @apply rounded px-9 py-3 flex gap-3 items-center hover:opacity-80 transition-opacity; } .btn-active { - @apply bg-blue-50 text-blue-600; + @apply bg-neutral-200 text-blue-600 font-bold; } .menu-title { - @apply text-xl px-9 py-3; + @apply text-2xl px-9 py-3; } .mat-mdc-button-base { diff --git a/apps/webcomponents/src/app/components/base.component.ts b/apps/webcomponents/src/app/components/base.component.ts index 5c8851ab01..422cfada74 100644 --- a/apps/webcomponents/src/app/components/base.component.ts +++ b/apps/webcomponents/src/app/components/base.component.ts @@ -116,9 +116,7 @@ export class BaseComponent implements OnChanges, OnInit { uuid: string, usages: LinkUsage[] ): Promise { - const record = await firstValueFrom( - this.recordsRepository.getByUniqueIdentifier(uuid) - ) + const record = await firstValueFrom(this.recordsRepository.getRecord(uuid)) if (record?.kind !== 'dataset') { return null } diff --git a/conf/default.toml b/conf/default.toml index 6cc8a843fc..134ab5e7e3 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -5,7 +5,7 @@ [global] # This URL (relative or absolute) must point to the API endpoint of a GeoNetwork4 instance -geonetwork4_api_url = "https://gpf-geonetwork-qua.priv.geopf.fr/geonetwork/srv/api" +geonetwork4_api_url = "https://data.geopf.fr/catalog" datahub_url = "/catalogue" # This should point to a proxy to avoid CORS errors on some requests (data preview, OGC capabilities etc.) # The actual URL will be appended after this path, e.g. : https://my.proxy/?url=http%3A%2F%2Fencoded.url%2Fows` @@ -65,7 +65,7 @@ header_foreground_color = '#212029' # fonts_stylesheet_url = "https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&family=Permanent+Marker&display=swap" # Use it to set custom location for the favicon; by default, the path `/favicon.ico` will be used -# favicon = "assets/favicon.ico" +favicon = "/assets/img/favicon.ico" ### SEARCH SETTINGS @@ -81,7 +81,7 @@ header_foreground_color = '#212029' # The advanced search filters available to the user can be customized with this setting. # The following fields can be used for filtering: 'publisher', 'format', 'publicationYear', 'standard', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType' # any other field will be ignored -advanced_filters = ['publisher', 'topic', 'publicationYear', 'format', 'documentStandard', 'inspireKeyword', 'isSpatial', 'license'] +advanced_filters = ['publisher', 'topic', 'resourceType', 'publicationYear', 'format', 'inspireKeyword', 'isSpatial', 'license'] # One or several search presets can be defined here; every search preset is composed of: # - a name (which can be a translation key) @@ -146,7 +146,7 @@ enabled = true # Optional; if true, the default basemap will not be added to the map. # Use [[map_layer]] sections to define your own custom layers (see below) -# do_not_use_default_basemap = false +do_not_use_default_basemap = true # One or several layers (as background or overlay) can be added to the map with the following properties: # - type (mandatory): Indicates the layer type. Possible values are 'xyz', 'wms', 'wfs', 'geojson'. @@ -171,6 +171,10 @@ enabled = true # "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [125.6, 10.1]}}] # } # """ +[[map_layer]] +type = 'wms' +url = 'https://data.geopf.fr/wms-r' +name = 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2' ### TRANSLATIONS diff --git a/jest.setup.ts b/jest.setup.ts index 983b1b49e6..4ecc30487a 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,4 +1,4 @@ -import { TextEncoder, TextDecoder } from 'util' +import { TextDecoder, TextEncoder } from 'util' // this is needed because jsdom does not include these as globals by default // see https://github.com/jsdom/jsdom/issues/2524 @@ -11,3 +11,35 @@ if (process.env.TEST_HIDE_CONSOLE) { console.warn = () => {} console.error = () => {} } + +// mock local storage (create a new one each time) +class LocalStorageRefStub { + store: Record = {} + mockLocalStorage = { + getItem: jest.fn((key: string): string => { + return key in this.store ? this.store[key] : null + }), + setItem: jest.fn((key: string, value: string) => { + this.mockLocalStorage[key] = `${value}` // we're also saving it here to be able to get it with {...localStorage} + this.store[key] = `${value}` + }), + removeItem: jest.fn((key: string) => { + delete this.mockLocalStorage[key] + delete this.store[key] + }), + clear: jest.fn(() => { + for (const key in this.store) { + delete this.mockLocalStorage[key] + } + this.store = {} + }), + } + public getLocalStorage() { + return this.mockLocalStorage + } +} +beforeEach(() => { + Object.defineProperty(window, 'localStorage', { + value: new LocalStorageRefStub().getLocalStorage(), + }) +}) diff --git a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml index 44095c03d9..cb39432b77 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml +++ b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+geo2france-plu.iso19139.xml @@ -150,6 +150,25 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè + + + + + 2024-05-24 + + + + + + + + + + 2024-05-30 + + + + diff --git a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+metawal.iso19115-3.xml b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+metawal.iso19115-3.xml index 7b89177e73..a4ac9733a8 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+metawal.iso19115-3.xml +++ b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset+metawal.iso19115-3.xml @@ -190,6 +190,25 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè + + + + + 2024-05-24 + + + + + + + + + + 2024-05-30 + + + + diff --git a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19115-3.xml b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19115-3.xml index 950f7d5cde..8e9f5ba9a3 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19115-3.xml +++ b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19115-3.xml @@ -8,6 +8,7 @@ xmlns:gcx="http://standards.iso.org/iso/19115/-3/gcx/1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:mmi="http://standards.iso.org/iso/19115/-3/mmi/1.0" + xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:mrd="http://standards.iso.org/iso/19115/-3/mrd/1.0" xmlns:mrl="http://standards.iso.org/iso/19115/-3/mrl/2.0"> @@ -437,6 +438,29 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè + + + + + + + 2024-05-24 + + + + + + + + + + 2024-05-30 + + + + + + diff --git a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19139.xml b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19139.xml index 5cd5e5a7e4..51ae0d83c6 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19139.xml +++ b/libs/api/metadata-converter/src/lib/fixtures/generic-dataset.iso19139.xml @@ -3,7 +3,8 @@ xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:gmx="http://www.isotc211.org/2005/gmx" xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:gts="http://www.isotc211.org/2005/gts"> + xmlns:gts="http://www.isotc211.org/2005/gts" + xmlns:gml="http://www.opengis.net/gml/3.2"> my-dataset-001 @@ -390,6 +391,29 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè + + + + + + + 2024-05-24 + + + + + + + + + + 2024-05-30 + + + + + + diff --git a/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts b/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts index 54994c3f99..f020b13392 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts @@ -195,7 +195,15 @@ As such, **it is not very interesting at all.**`, }, ], spatialExtents: [], - temporalExtents: [], + temporalExtents: [ + { + start: new Date('2024-05-24'), + end: null, + }, + { + start: new Date('2024-05-30'), + }, + ], updateFrequency: { updatedTimes: 3, per: 'month', diff --git a/libs/api/metadata-converter/src/lib/gn4/types/index.ts b/libs/api/metadata-converter/src/lib/gn4/types/index.ts index a579ebb10b..18157c49d2 100644 --- a/libs/api/metadata-converter/src/lib/gn4/types/index.ts +++ b/libs/api/metadata-converter/src/lib/gn4/types/index.ts @@ -1,3 +1,4 @@ export * from './elasticsearch.model' export * from './search.model' export * from './metadata.model' +export * from './keywords.model' diff --git a/libs/api/metadata-converter/src/lib/gn4/types/keywords.model.ts b/libs/api/metadata-converter/src/lib/gn4/types/keywords.model.ts new file mode 100644 index 0000000000..7dec4de69e --- /dev/null +++ b/libs/api/metadata-converter/src/lib/gn4/types/keywords.model.ts @@ -0,0 +1,31 @@ +export interface KeywordApiResponse { + values?: { + eng: string + } + definitions?: { + eng: string + } + coordEast?: string + coordWest?: string + coordSouth?: string + coordNorth?: string + thesaurusKey?: string + definition?: string + value?: string + uri?: string +} + +export interface ThesaurusApiResponse { + key?: string + dname?: string + description?: string[] | string + filename?: string + title?: string + multilingualTitles?: string[] + dublinCoreMultilinguals?: string[] + date?: string + url?: string + defaultNamespace?: string + type?: string + activated?: string +} diff --git a/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts b/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts index 1ce5344057..afa33a4e2f 100644 --- a/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +++ b/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts @@ -1,4 +1,7 @@ +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { XmlElement } from '@rgrove/parse-xml' import { Iso19139Converter } from '../iso19139' +import { renameElements } from '../xml-utils' import { readContacts, readDistributions, @@ -30,9 +33,6 @@ import { writeStatus, writeUniqueIdentifier, } from './write-parts' -import { XmlElement } from '@rgrove/parse-xml' -import { renameElements } from '../xml-utils' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' export class Iso191153Converter extends Iso19139Converter { constructor() { diff --git a/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts b/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts index 8ccfa3e18e..48eddf6980 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts @@ -4,6 +4,9 @@ import { DatasetRecord, ServiceRecord, } from '@geonetwork-ui/common/domain/model/record' +import { XmlElement } from '@rgrove/parse-xml' +import { BaseConverter } from '../base.converter' +import { isEqual } from '../convert-utils' import { createDocument, createElement, @@ -11,32 +14,6 @@ import { parseXmlString, xmlToString, } from '../xml-utils' -import { - writeAbstract, - writeContacts, - writeContactsForResource, - writeDistributions, - writeGraphicOverviews, - writeKeywords, - writeKind, - writeLegalConstraints, - writeLicenses, - writeLineage, - writeOnlineResources, - writeOtherConstraints, - writeOwnerOrganization, - writeRecordUpdated, - writeResourceCreated, - writeResourcePublished, - writeResourceUpdated, - writeSecurityConstraints, - writeSpatialRepresentation, - writeStatus, - writeTitle, - writeTopics, - writeUniqueIdentifier, - writeUpdateFrequency, -} from './write-parts' import { readAbstract, readContacts, @@ -59,13 +36,38 @@ import { readSecurityConstraints, readSpatialRepresentation, readStatus, + readTemporalExtents, readTitle, readUniqueIdentifier, readUpdateFrequency, } from './read-parts' -import { isEqual } from '../convert-utils' -import { BaseConverter } from '../base.converter' -import { XmlElement } from '@rgrove/parse-xml' +import { + writeAbstract, + writeContacts, + writeContactsForResource, + writeDistributions, + writeGraphicOverviews, + writeKeywords, + writeKind, + writeLegalConstraints, + writeLicenses, + writeLineage, + writeOnlineResources, + writeOtherConstraints, + writeOwnerOrganization, + writeRecordUpdated, + writeResourceCreated, + writeResourcePublished, + writeResourceUpdated, + writeSecurityConstraints, + writeSpatialRepresentation, + writeStatus, + writeTemporalExtents, + writeTitle, + writeTopics, + writeUniqueIdentifier, + writeUpdateFrequency, +} from './write-parts' export class Iso19139Converter extends BaseConverter { protected readers: Record< @@ -98,9 +100,9 @@ export class Iso19139Converter extends BaseConverter { lineage: readLineage, distributions: readDistributions, onlineResources: readOnlineResources, + temporalExtents: readTemporalExtents, // TODO spatialExtents: () => [], - temporalExtents: () => [], extras: () => undefined, landingPage: () => undefined, languages: () => [], @@ -136,9 +138,9 @@ export class Iso19139Converter extends BaseConverter { lineage: writeLineage, distributions: writeDistributions, onlineResources: writeOnlineResources, + temporalExtents: writeTemporalExtents, // TODO spatialExtents: () => undefined, - temporalExtents: () => undefined, extras: () => undefined, landingPage: () => undefined, languages: () => undefined, @@ -311,6 +313,8 @@ export class Iso19139Converter extends BaseConverter { fieldChanged('spatialRepresentation') && this.writers['spatialRepresentation'](record, rootEl) fieldChanged('overviews') && this.writers['overviews'](record, rootEl) + fieldChanged('temporalExtents') && + this.writers['temporalExtents'](record, rootEl) fieldChanged('distributions') && this.writers['distributions'](record, rootEl) fieldChanged('lineage') && this.writers['lineage'](record, rootEl) diff --git a/libs/api/metadata-converter/src/lib/iso19139/read-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19139/read-parts.spec.ts index 3a64318005..966d825ab8 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/read-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/read-parts.spec.ts @@ -1,17 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-ignore -import GEO2FRANCE_PLU_DATASET from '../fixtures/geo2france.iso19139.plu.xml' // @ts-ignore import GEOCAT_CH_DATASET from '../fixtures/geocat-ch.iso19139.dataset.xml' // @ts-ignore +import { XmlElement } from '@rgrove/parse-xml' import GEOCAT_CH_SERVICE from '../fixtures/geocat-ch.iso19139.service.xml' -import { - getUpdateFrequencyFromCustomPeriod, - readContacts, - readDistributions, - readOnlineResources, - readOwnerOrganization, -} from './read-parts' +import { pipe } from '../function-utils' import { appendChildren, findNestedElement, @@ -19,8 +13,15 @@ import { parseXmlString, removeChildrenByName, } from '../xml-utils' -import { pipe } from '../function-utils' -import { XmlElement } from '@rgrove/parse-xml' +import { + findIdentification, + getUpdateFrequencyFromCustomPeriod, + readContacts, + readDistributions, + readOnlineResources, + readOwnerOrganization, + readTemporalExtents, +} from './read-parts' describe('read parts', () => { let recordRootEl: XmlElement @@ -233,6 +234,151 @@ describe('read parts', () => { }) }) }) + describe('readTemporalExtents', () => { + describe('no temporal extent', () => { + it('returns an empty array', () => { + expect(readTemporalExtents(recordRootEl)).toEqual([]) + }) + }) + describe('instant temporal extent with known time position', () => { + beforeEach(() => { + const instantExtent = getRootElement( + parseXmlString(` + + + + + 2024-05-24 + + + +`) + ) + pipe( + findIdentification(), + findNestedElement('gmd:extent', 'gmd:EX_Extent'), + removeChildrenByName('gmd:temporalElement'), + appendChildren(() => instantExtent) + )(recordRootEl) + }) + it('returns an array of temporal extents with only the start attribute', () => { + expect(readTemporalExtents(recordRootEl)).toEqual([ + { + start: new Date('2024-05-24'), + }, + ]) + }) + }) + describe('instant temporal extent with unknown time position', () => { + beforeEach(() => { + const instantExtent = getRootElement( + parseXmlString(` + + + + + + + + +`) + ) + pipe( + findIdentification(), + findNestedElement('gmd:extent', 'gmd:EX_Extent'), + removeChildrenByName('gmd:temporalElement'), + appendChildren(() => instantExtent) + )(recordRootEl) + }) + it('returns an array of temporal extents with only the start attribute set to null', () => { + expect(readTemporalExtents(recordRootEl)).toEqual([ + { + start: null, + }, + ]) + }) + }) + describe('period temporal extent with known begin and end position', () => { + beforeEach(() => { + const periodExtent = getRootElement( + parseXmlString(` + + + + + 2024-05-24 + 2024-05-30 + + + +`) + ) + pipe( + findIdentification(), + findNestedElement('gmd:extent', 'gmd:EX_Extent'), + removeChildrenByName('gmd:temporalElement'), + appendChildren(() => periodExtent) + )(recordRootEl) + }) + it('returns an array of temporal extents with start and end attributes', () => { + expect(readTemporalExtents(recordRootEl)).toEqual([ + { + start: new Date('2024-05-24'), + end: new Date('2024-05-30'), + }, + ]) + }) + }) + describe('mixed temporal extents', () => { + beforeEach(() => { + const periodExtent = getRootElement( + parseXmlString(` + + + + + 2024-05-24 + + + + +`) + ) + const instantExtent = getRootElement( + parseXmlString(` + + + + + 2024-05-30 + + + +`) + ) + pipe( + findIdentification(), + findNestedElement('gmd:extent', 'gmd:EX_Extent'), + removeChildrenByName('gmd:temporalElement'), + appendChildren( + () => periodExtent, + () => instantExtent + ) + )(recordRootEl) + }) + it('returns an array of mixed temporal extents', () => { + expect(readTemporalExtents(recordRootEl)).toEqual([ + { + start: new Date('2024-05-24'), + end: null, + }, + { + start: new Date('2024-05-30'), + }, + ]) + }) + }) + }) }) describe('service record', () => { diff --git a/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts index dfb1cc3590..62f82dcd53 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts @@ -4,7 +4,6 @@ import { GraphicOverview, Individual, Keyword, - KeywordThesaurus, Organization, RecordKind, RecordStatus, @@ -14,18 +13,7 @@ import { UpdateFrequency, UpdateFrequencyCustom, } from '@geonetwork-ui/common/domain/model/record' -import { getStatusFromStatusCode } from './utils/status.mapper' -import { getUpdateFrequencyFromFrequencyCode } from './utils/update-frequency.mapper' -import { - findChildElement, - findChildrenElement, - findNestedElement, - findNestedElements, - findParent, - readAttribute, - readText, - XmlElement, -} from '../xml-utils' +import { matchMimeType, matchProtocol } from '../common/distribution.mapper' import { ChainableFunction, combine, @@ -37,10 +25,22 @@ import { mapArray, pipe, } from '../function-utils' -import { getRoleFromRoleCode } from './utils/role.mapper' -import { matchMimeType, matchProtocol } from '../common/distribution.mapper' -import { getKeywordTypeFromKeywordTypeCode } from './utils/keyword.mapper' +import { + XmlElement, + findChildElement, + findChildrenElement, + findNestedElement, + findNestedElements, + findParent, + readAttribute, + readText, +} from '../xml-utils' import { fullNameToParts } from './utils/individual-name' +import { getKeywordTypeFromKeywordTypeCode } from './utils/keyword.mapper' +import { getRoleFromRoleCode } from './utils/role.mapper' +import { getStatusFromStatusCode } from './utils/status.mapper' +import { getUpdateFrequencyFromFrequencyCode } from './utils/update-frequency.mapper' +import { ThesaurusModel } from '@geonetwork-ui/common/domain/model/thesaurus' export function extractCharacterString(): ChainableFunction< XmlElement, @@ -609,7 +609,7 @@ export function readContactsForResource(rootEl: XmlElement): Individual[] { } // from gmd:thesaurusName -export function readThesaurus(rootEl: XmlElement): KeywordThesaurus { +export function readThesaurus(rootEl: XmlElement): ThesaurusModel { if (!rootEl) return null const findIdentifier = findNestedElement( @@ -843,3 +843,55 @@ export function readOnlineResources( flattenArray() )(rootEl) } + +export function readTemporalExtents(rootEl: XmlElement) { + return pipe( + findIdentification(), + findNestedElements('gmd:extent', 'gmd:EX_Extent', 'gmd:temporalElement'), + mapArray( + combine( + findNestedElement( + 'gmd:EX_TemporalExtent', + 'gmd:extent', + 'gml:TimePeriod' + ), + findNestedElement( + 'gmd:EX_TemporalExtent', + 'gmd:extent', + 'gml:TimeInstant' + ) + ) + ), + mapArray(([periodEl, instantEl]) => { + if (periodEl) { + return pipe( + combine( + pipe( + findChildElement('gml:beginPosition', false), + readText(), + map((dateStr) => (dateStr ? new Date(dateStr) : null)) + ), + pipe( + findChildElement('gml:endPosition', false), + readText(), + map((dateStr) => (dateStr ? new Date(dateStr) : null)) + ) + ), + map(([start, end]) => ({ + start, + end, + })) + )(periodEl) + } else { + return pipe( + findChildElement('gml:timePosition', false), + readText(), + map((dateStr) => (dateStr ? new Date(dateStr) : null)), + map((date) => ({ + start: date, + })) + )(instantEl) + } + }) + )(rootEl) +} diff --git a/libs/api/metadata-converter/src/lib/iso19139/utils/keyword.mapper.ts b/libs/api/metadata-converter/src/lib/iso19139/utils/keyword.mapper.ts index 5ac0b688d7..5cb0a5314a 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/utils/keyword.mapper.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/utils/keyword.mapper.ts @@ -1,4 +1,4 @@ -import { KeywordType } from '@geonetwork-ui/common/domain/model/record' +import { KeywordType } from '@geonetwork-ui/common/domain/model/thesaurus' export function getKeywordTypeFromKeywordTypeCode( typeCode: string diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts index 1169c595b6..f5f89c6dcc 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts @@ -1,4 +1,6 @@ +import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' import { XmlElement } from '@rgrove/parse-xml' +import { GENERIC_DATASET_RECORD } from '../fixtures/generic.records' import { createElement, getRootElement, @@ -9,9 +11,8 @@ import { getISODuration, writeDistributions, writeKeywords, + writeTemporalExtents, } from './write-parts' -import { GENERIC_DATASET_RECORD } from '../fixtures/generic.records' -import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' describe('write parts', () => { let rootEl: XmlElement @@ -240,6 +241,78 @@ describe('write parts', () => { }) }) + describe('writeTemporalExtents', () => { + it('removes and writes several temporal extents', () => { + // add some temporal extents first + const sample = parseXmlString(` + + + + + + + + + + 2021-01-01 + 2021-01-31 + + + + + + + + +`) + rootEl = getRootElement(sample) + writeTemporalExtents( + { + ...datasetRecord, + temporalExtents: [ + { + start: new Date('2024-05-24'), + end: null, + }, + { + start: new Date('2024-05-30'), + }, + ], + }, + rootEl + ) + expect(rootAsString()).toEqual(` + + + + + + + + + 2024-05-24 + + + + + + + + + + 2024-05-30 + + + + + + + + +`) + }) + }) + describe('writeKeywords', () => { it('writes keywords grouped by thesaurus', () => { writeKeywords(datasetRecord, rootEl) diff --git a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts index b29dc6b599..02d69e1deb 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts @@ -6,7 +6,6 @@ import { DatasetServiceDistribution, Individual, Keyword, - KeywordThesaurus, RecordStatus, Role, ServiceEndpoint, @@ -15,7 +14,20 @@ import { UpdateFrequencyCode, UpdateFrequencyCustom, } from '@geonetwork-ui/common/domain/model/record' +import format from 'date-fns/format' +import { + ChainableFunction, + fallback, + filterArray, + getAtIndex, + map, + mapArray, + noop, + pipe, + tap, +} from '../function-utils' import { + XmlElement, addAttribute, appendChildren, createChild, @@ -30,22 +42,10 @@ import { removeChildren, removeChildrenByName, setTextContent, - XmlElement, } from '../xml-utils' -import { - ChainableFunction, - fallback, - filterArray, - getAtIndex, - map, - mapArray, - noop, - pipe, - tap, -} from '../function-utils' -import format from 'date-fns/format' import { readKind } from './read-parts' import { namePartsToFull } from './utils/individual-name' +import { ThesaurusModel } from '@geonetwork-ui/common/domain/model/thesaurus' export function writeCharacterString( text: string @@ -367,7 +367,7 @@ export function removeKeywords() { } // returns a element -export function createThesaurus(thesaurus: KeywordThesaurus) { +export function createThesaurus(thesaurus: ThesaurusModel) { return pipe( createElement('gmd:thesaurusName'), createChild('gmd:CI_Citation'), @@ -1126,3 +1126,61 @@ export function writeOnlineResources( appendChildren(...record.onlineResources.map(createOnlineResource)) )(rootEl) } + +export function writeTemporalExtents( + record: DatasetRecord, + rootEl: XmlElement +) { + pipe( + findOrCreateIdentification(), + findNestedChildOrCreate('gmd:extent', 'gmd:EX_Extent'), + removeChildrenByName('gmd:temporalElement'), + appendChildren( + ...record.temporalExtents.map((extent) => + pipe( + createElement('gmd:temporalElement'), + createChild('gmd:EX_TemporalExtent'), + appendChildren( + 'start' in extent && 'end' in extent + ? pipe( + createElement('gmd:extent'), + createChild('gml:TimePeriod'), + appendChildren( + pipe( + createElement('gml:beginPosition'), + pipe( + extent.start + ? setTextContent(format(extent.start, 'yyyy-MM-dd')) + : addAttribute('indeterminatePosition', 'unknown') + ) + ), + pipe( + createElement('gml:endPosition'), + pipe( + extent.end + ? setTextContent(format(extent.end, 'yyyy-MM-dd')) + : addAttribute('indeterminatePosition', 'unknown') + ) + ) + ) + ) + : pipe( + createElement('gmd:extent'), + createChild('gml:TimeInstant'), + appendChildren( + pipe( + createElement('gml:timePosition'), + pipe( + extent.start + ? setTextContent(format(extent.start, 'yyyy-MM-dd')) + : addAttribute('indeterminatePosition', 'unknown') + ) + ) + ) + ) + ) + ) + ) + ) + )(rootEl) +} diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts index c888d50870..b848e2e515 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -245,7 +245,7 @@ export class ElasticsearchService { const must = [] as Record[] const must_not = { ...this.queryFilterOnValues('resourceType', [ - 'service', + //'service', 'map', 'map/static', 'mapDigital', @@ -358,7 +358,7 @@ export class ElasticsearchService { ], must_not: { ...this.queryFilterOnValues('resourceType', [ - 'service', + //'service', 'map', 'map/static', 'mapDigital', diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index bbea77b453..3853a2e45f 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -1,6 +1,9 @@ import { Gn4Repository } from './gn4-repository' -import { SearchApiService } from '@geonetwork-ui/data-access/gn4' -import { lastValueFrom, of } from 'rxjs' +import { + RecordsApiService, + SearchApiService, +} from '@geonetwork-ui/data-access/gn4' +import { firstValueFrom, lastValueFrom, of, throwError } from 'rxjs' import { ElasticsearchService } from './elasticsearch' import { TestBed } from '@angular/core/testing' import { @@ -11,8 +14,14 @@ import { Aggregations, SearchResults, } from '@geonetwork-ui/common/domain/model/search' -import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { + DATASET_RECORD_SIMPLE, + DATASET_RECORD_SIMPLE_AS_XML, + DATASET_RECORDS, +} from '@geonetwork-ui/common/fixtures' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { map } from 'rxjs/operators' +import { HttpErrorResponse } from '@angular/common/http' class Gn4MetadataMapperMock { readRecords = jest.fn((records) => @@ -56,10 +65,32 @@ class SearchApiServiceMock { }) } +class RecordsApiServiceMock { + getRecordAs = jest.fn((uuid) => + of(` + + ${uuid} + +`).pipe(map((xml) => ({ body: xml }))) + ) + insert = jest.fn(() => + of({ + metadataInfos: { + 1234: [ + { + uuid: '1234-5678-9012', + }, + ], + }, + }) + ) +} + describe('Gn4Repository', () => { let repository: Gn4Repository let gn4Helper: ElasticsearchService let gn4SearchApi: SearchApiService + let gn4RecordsApi: RecordsApiService beforeEach(() => { TestBed.configureTestingModule({ @@ -73,6 +104,10 @@ describe('Gn4Repository', () => { provide: SearchApiService, useClass: SearchApiServiceMock, }, + { + provide: RecordsApiService, + useClass: RecordsApiServiceMock, + }, { provide: Gn4Converter, useClass: Gn4MetadataMapperMock, @@ -82,6 +117,7 @@ describe('Gn4Repository', () => { repository = TestBed.inject(Gn4Repository) gn4Helper = TestBed.inject(ElasticsearchService) gn4SearchApi = TestBed.inject(SearchApiService) + gn4RecordsApi = TestBed.inject(RecordsApiService) }) it('creates', () => { expect(repository).toBeTruthy() @@ -148,12 +184,10 @@ describe('Gn4Repository', () => { expect(count).toStrictEqual(1234) }) }) - describe('getByUniqueIdentifier', () => { + describe('getRecord', () => { let record: CatalogRecord beforeEach(async () => { - record = await lastValueFrom( - repository.getByUniqueIdentifier('1234-5678') - ) + record = await lastValueFrom(repository.getRecord('1234-5678')) }) it('builds a payload with the specified uuid', () => { expect(gn4Helper.getMetadataByIdPayload).toHaveBeenCalledWith('1234-5678') @@ -169,9 +203,7 @@ describe('Gn4Repository', () => { hits: [], }, }) - record = await lastValueFrom( - repository.getByUniqueIdentifier('1234-5678') - ) + record = await lastValueFrom(repository.getRecord('1234-5678')) }) it('returns null', () => { expect(record).toBe(null) @@ -234,4 +266,216 @@ describe('Gn4Repository', () => { expect(results.records).toStrictEqual(DATASET_RECORDS) }) }) + describe('openRecordForEdition', () => { + let record: CatalogRecord + let recordSource: string + let savedOnce: boolean + + describe('if the record is present in the backend', () => { + beforeEach(async () => { + ;[record, recordSource, savedOnce] = await lastValueFrom( + repository.openRecordForEdition('1234-5678') + ) + }) + it('calls the API to get the record as XML', () => { + expect(gn4RecordsApi.getRecordAs).toHaveBeenCalledWith( + '1234-5678', + undefined, + expect.anything(), + undefined, + undefined, + undefined, + expect.anything(), + expect.anything(), + undefined, + expect.anything() + ) + }) + it('parses the XML record into a native object', () => { + expect(record).toMatchObject({ uniqueIdentifier: '1234-5678' }) + }) + it('loads the source & tells the record is present in the backend', () => { + expect(recordSource).toMatch(//) + expect(savedOnce).toBe(true) + }) + }) + describe('if the record is present as draft but not in the backend', () => { + let recordSource: string + beforeEach(async () => { + recordSource = await firstValueFrom( + repository.saveRecordAsDraft({ + ...DATASET_RECORD_SIMPLE, + uniqueIdentifier: '1234-5678', + }) + ) + ;(gn4RecordsApi.getRecordAs as jest.Mock).mockReturnValueOnce( + throwError(() => new HttpErrorResponse({ status: 404 })) + ) + ;[record, recordSource, savedOnce] = await lastValueFrom( + repository.openRecordForEdition('1234-5678') + ) + }) + it('tells the record it has not been saved yet', () => { + expect(savedOnce).toBe(false) + }) + it('returns the record as serialized', () => { + expect(recordSource).toMatch(/ { + let recordSource: string + describe('with reference', () => { + beforeEach(async () => { + recordSource = await lastValueFrom( + repository.saveRecord( + DATASET_RECORD_SIMPLE, + DATASET_RECORD_SIMPLE_AS_XML + ) + ) + }) + it('uses a converter that matches the reference', () => { + const recordXml = (gn4RecordsApi.insert as jest.Mock).mock.calls[0][14] + expect(recordXml).toMatch(` + + + my-dataset-001`) + }) + it('calls the API to insert the record as XML', () => { + expect(gn4RecordsApi.insert).toHaveBeenCalledWith( + expect.anything(), + undefined, + undefined, + undefined, + expect.anything(), + undefined, + expect.anything(), + undefined, + undefined, + undefined, + expect.anything(), + undefined, + undefined, + undefined, + expect.stringMatching(` + + + my-dataset-001`) + ) + }) + it('returns the unique identifier of the record as it was saved', () => { + expect(recordSource).toEqual('1234-5678-9012') + }) + }) + describe('without reference', () => { + beforeEach(async () => { + await lastValueFrom(repository.saveRecord(DATASET_RECORDS[0])) + }) + it('uses the ISO19139 converter by default', () => { + const recordXml = (gn4RecordsApi.insert as jest.Mock).mock.calls[0][14] + expect(recordXml).toMatch(` + + ${DATASET_RECORD_SIMPLE.uniqueIdentifier} + `) + }) + }) + }) + describe('record draft', () => { + beforeEach(async () => { + // save a record, then a draft, then open the record again + await lastValueFrom( + repository.saveRecord( + DATASET_RECORD_SIMPLE, + DATASET_RECORD_SIMPLE_AS_XML + ) + ) + await lastValueFrom( + repository.saveRecordAsDraft( + { + ...DATASET_RECORD_SIMPLE, + title: 'The title has been modified', + }, + DATASET_RECORD_SIMPLE_AS_XML + ) + ) + }) + describe('#openRecordForEdition', () => { + it('loads the draft instead of the original one', async () => { + const [record] = await lastValueFrom( + repository.openRecordForEdition( + DATASET_RECORD_SIMPLE.uniqueIdentifier + ) + ) + expect(record).toStrictEqual({ + ...DATASET_RECORD_SIMPLE, + title: 'The title has been modified', + }) + }) + }) + describe('#clearRecordDraft', () => { + beforeEach(() => { + repository.clearRecordDraft(DATASET_RECORD_SIMPLE.uniqueIdentifier) + }) + it('removes the record draft', async () => { + const [record] = await lastValueFrom( + repository.openRecordForEdition( + DATASET_RECORD_SIMPLE.uniqueIdentifier + ) + ) + expect(record?.title).not.toBe('The title has been modified') + const hasDraft = repository.recordHasDraft( + DATASET_RECORD_SIMPLE.uniqueIdentifier + ) + + expect(hasDraft).toBe(false) + }) + }) + describe('#recordHasDraft', () => { + it('returns true when there is a draft', () => { + const hasDraft = repository.recordHasDraft( + DATASET_RECORD_SIMPLE.uniqueIdentifier + ) + expect(hasDraft).toBe(true) + }) + it('returns false otherwise', () => { + const hasDraft = repository.recordHasDraft('blargz') + expect(hasDraft).toBe(false) + }) + }) + }) + + describe('#getAllDrafts', () => { + beforeEach(async () => { + window.localStorage.clear() + // save 3 drafts + await firstValueFrom( + repository.saveRecordAsDraft({ + ...DATASET_RECORD_SIMPLE, + uniqueIdentifier: 'DRAFT-1', + }) + ) + await firstValueFrom( + repository.saveRecordAsDraft({ + ...DATASET_RECORD_SIMPLE, + uniqueIdentifier: 'DRAFT-2', + }) + ) + await firstValueFrom( + repository.saveRecordAsDraft({ + ...DATASET_RECORD_SIMPLE, + uniqueIdentifier: 'DRAFT-3', + }) + ) + }) + it('returns all drafts', async () => { + const drafts = await lastValueFrom(repository.getAllDrafts()) + expect(drafts.length).toBe(3) + expect(drafts.map((d) => d.uniqueIdentifier)).toEqual([ + 'DRAFT-1', + 'DRAFT-2', + 'DRAFT-3', + ]) + }) + }) }) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index a31a61aa81..fa17388957 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -1,7 +1,17 @@ import { Injectable } from '@angular/core' -import { SearchApiService } from '@geonetwork-ui/data-access/gn4' +import { + RecordsApiService, + SearchApiService, +} from '@geonetwork-ui/data-access/gn4' import { ElasticsearchService } from './elasticsearch' -import { Observable, of, switchMap } from 'rxjs' +import { + combineLatest, + from, + Observable, + of, + switchMap, + throwError, +} from 'rxjs' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { SearchParams, @@ -12,19 +22,23 @@ import { AggregationsParams, FieldFilters, } from '@geonetwork-ui/common/domain/model/search' -import { map } from 'rxjs/operators' +import { catchError, map, tap } from 'rxjs/operators' import { + findConverterForDocument, Gn4Converter, Gn4SearchResults, + Iso19139Converter, } from '@geonetwork-ui/api/metadata-converter' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { HttpErrorResponse } from '@angular/common/http' @Injectable() export class Gn4Repository implements RecordsRepositoryInterface { constructor( private gn4SearchApi: SearchApiService, private gn4SearchHelper: ElasticsearchService, - private gn4Mapper: Gn4Converter + private gn4Mapper: Gn4Converter, + private gn4RecordsApi: RecordsApiService ) {} search({ @@ -84,9 +98,7 @@ export class Gn4Repository implements RecordsRepositoryInterface { .pipe(map((results: Gn4SearchResults) => results.hits.total?.value || 0)) } - getByUniqueIdentifier( - uniqueIdentifier: string - ): Observable { + getRecord(uniqueIdentifier: string): Observable { return this.gn4SearchApi .search( 'bucket', @@ -165,4 +177,143 @@ export class Gn4Repository implements RecordsRepositoryInterface { ) ) } + + /** + * Returns null if the record is not found + */ + private loadRecordAsXml(uniqueIdentifier: string): Observable { + return this.gn4RecordsApi + .getRecordAs( + uniqueIdentifier, + undefined, + false, + undefined, + undefined, + undefined, + 'application/xml', + 'response', + undefined, + { httpHeaderAccept: 'text/xml,application/xml' as 'application/xml' } // this is to make sure that the response is parsed as text + ) + .pipe( + map((response) => response.body), + catchError((error: HttpErrorResponse) => + error.status === 404 ? of(null) : throwError(() => error) + ) + ) + } + + private getLocalStorageKeyForRecord(uniqueIdentifier: string) { + return `geonetwork-ui-draft-${uniqueIdentifier}` + } + + openRecordForEdition( + uniqueIdentifier: string + ): Observable<[CatalogRecord, string, boolean] | null> { + const draft$ = of( + window.localStorage.getItem( + this.getLocalStorageKeyForRecord(uniqueIdentifier) + ) + ) + const recordAsXml$ = this.loadRecordAsXml(uniqueIdentifier) + return combineLatest([draft$, recordAsXml$]).pipe( + switchMap(([draft, recordAsXml]) => { + const xml = draft ?? recordAsXml + const isSavedAlready = recordAsXml !== null + return findConverterForDocument(xml) + .readRecord(xml) + .then( + (record) => + [record, xml, isSavedAlready] as [CatalogRecord, string, boolean] + ) + }) + ) + } + + private serializeRecordToXml( + record: CatalogRecord, + referenceRecordSource?: string + ): Observable { + // if there's a reference record, use that standard; otherwise, use iso19139 + const converter = referenceRecordSource + ? findConverterForDocument(referenceRecordSource) + : new Iso19139Converter() + return from(converter.writeRecord(record, referenceRecordSource)) + } + + saveRecord( + record: CatalogRecord, + referenceRecordSource?: string + ): Observable { + return this.serializeRecordToXml(record, referenceRecordSource).pipe( + switchMap((recordXml) => + this.gn4RecordsApi + .insert( + 'METADATA', + undefined, + undefined, + undefined, + true, + undefined, + 'OVERWRITE', + undefined, + undefined, + undefined, + '_none_', + undefined, + undefined, + undefined, + recordXml + ) + .pipe( + map((response) => { + const metadataId = Object.keys(response.metadataInfos)[0] + return response.metadataInfos[metadataId][0].uuid + }) + ) + ) + ) + } + + saveRecordAsDraft( + record: CatalogRecord, + referenceRecordSource?: string + ): Observable { + return this.serializeRecordToXml(record, referenceRecordSource).pipe( + tap((recordXml) => + window.localStorage.setItem( + this.getLocalStorageKeyForRecord(record.uniqueIdentifier), + recordXml + ) + ) + ) + } + + clearRecordDraft(uniqueIdentifier: string): void { + window.localStorage.removeItem( + this.getLocalStorageKeyForRecord(uniqueIdentifier) + ) + } + + recordHasDraft(uniqueIdentifier: string): boolean { + return ( + window.localStorage.getItem( + this.getLocalStorageKeyForRecord(uniqueIdentifier) + ) !== null + ) + } + + // generated by copilot + getAllDrafts(): Observable { + const items = { ...window.localStorage } + const drafts = Object.keys(items) + .filter((key) => key.startsWith('geonetwork-ui-draft-')) + .map((key) => window.localStorage.getItem(key)) + .filter((draft) => draft !== null) + return from( + Promise.all( + drafts.map((draft) => findConverterForDocument(draft).readRecord(draft)) + ) + ) + } } diff --git a/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.spec.ts b/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.spec.ts index fe4c0ad104..dd77d0a52c 100644 --- a/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.spec.ts @@ -50,12 +50,14 @@ const sampleOrgA: Organization = { recordCount: 80, description: 'A description for Köniz Municipality', website: new URL('https://www.koeniz.ch/'), + email: 'reto.jau@koeniz.ch', } const sampleOrgB: Organization = { logoUrl: new URL('http://localhost/geonetwork/images/harvesting/bakom.png'), name: 'Office fédéral de la communication OFCOM', recordCount: 50, website: new URL('http://www.bakom.admin.ch/'), + email: 'christian.meier@bakom.admin.ch', } const sampleOrgC: Organization = { logoUrl: new URL( @@ -65,6 +67,7 @@ const sampleOrgC: Organization = { recordCount: 20, description: 'A description for ARE', website: new URL('http://www.are.admin.ch/'), + email: 'rolf.giezendanner@are.admin.ch', } class SearchApiServiceMock { diff --git a/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts b/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts index b6a8a39493..8ccaecb521 100644 --- a/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts +++ b/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts @@ -76,6 +76,7 @@ export class OrganizationsFromGroupsService return { name: group.label[lang3], ...(group.description && { description: group.description }), + ...(group.email && { email: group.email }), ...(group.logo && { logoUrl: getAsUrl(`${IMAGE_URL}${group.logo}`), }), diff --git a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts index 10fb3101b8..5f17e9e1d7 100644 --- a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.spec.ts @@ -25,21 +25,22 @@ const sampleOrgA: Organization = { name: 'ARE', recordCount: 5, website: new URL('http://www.are.admin.ch/'), + email: 'rolf.giezendanner@are.admin.ch', } const sampleOrgB: Organization = { logoUrl: new URL('http://localhost/geonetwork/images/harvesting/bakom.png'), name: 'BAKOM', recordCount: 2, website: new URL('http://www.bakom.admin.ch/'), + email: 'christian.meier@bakom.admin.ch', } const sampleOrgC: Organization = { - logoUrl: new URL( - 'http://localhost/geonetwork/images/harvesting/ifremer-org.png' - ), + logoUrl: new URL('http://localhost/geonetwork/images/harvesting/ifremer.png'), name: 'Ifremer', recordCount: 1, description: "Institut français de recherche pour l'exploitation de la mer", website: new URL('https://www.ifremer.fr/'), + email: 'ifremer.ifremer@ifremer.admin.fr', } let geonetworkVersion: string diff --git a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts index d218eac915..9ebf479652 100644 --- a/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts +++ b/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts @@ -230,10 +230,12 @@ export class OrganizationsFromMetadataService if (!group) return fullOrg return { ...fullOrg, + email: emails[0], ...(group.description && { description: group.description }), ...(group.logo && { logoUrl: getAsUrl(`${IMAGE_URL}${group.logo}`) }), ...(group.website && { website: getAsUrl(group.website) }), - } + ...(group.email && { email: group.email }), + } as Organization }) } diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.spec.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.spec.ts new file mode 100644 index 0000000000..2e42125ae5 --- /dev/null +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.spec.ts @@ -0,0 +1,88 @@ +import { Gn4PlatformMapper } from './gn4-platform.mapper' +import { + KeywordApiResponse, + ThesaurusApiResponse, +} from '@geonetwork-ui/api/metadata-converter' + +import { AvatarServiceInterface } from '../auth' + +const keywords: KeywordApiResponse[] = [ + { + values: { + eng: 'Addresses', + }, + definitions: { + eng: 'Location of properties based on address identifiers, usually by road name, house number, postal code.', + }, + thesaurusKey: 'external.theme.httpinspireeceuropaeutheme-theme', + definition: + 'Location of properties based on address identifiers, usually by road name, house number, postal code.', + value: 'Addresses', + uri: 'http://inspire.ec.europa.eu/theme/ad', + }, +] +const thesaurus: ThesaurusApiResponse[] = [ + { + key: 'external.theme.httpinspireeceuropaeutheme-theme', + dname: 'theme', + description: [], + filename: 'httpinspireeceuropaeutheme-theme.rdf', + title: 'GEMET - INSPIRE themes, version 1.0', + multilingualTitles: [], + dublinCoreMultilinguals: [], + date: '2008-06-01', + url: 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme', + defaultNamespace: 'http://inspire.ec.europa.eu/theme', + type: 'external', + activated: 'y', + }, + { + key: 'external.place.regions', + dname: 'place', + description: 'Generated from NaturalEarth datasets and SeaVox.', + filename: 'regions.rdf', + title: 'Continents, countries, sea regions of the world.', + multilingualTitles: [], + dublinCoreMultilinguals: [], + date: '2015-07-17', + url: 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.place.regions', + defaultNamespace: + 'http://geonetwork-opensource.org/thesaurus/naturalearth-and-seavox', + type: 'external', + activated: 'y', + }, +] +describe('Gn4PlatformMapper', () => { + let mapper: Gn4PlatformMapper + let avatarService: AvatarServiceInterface + + beforeEach(() => { + avatarService = { + getPlaceholder: jest.fn(), + getProfileIcon: jest.fn(), + getProfileIconUrl: jest.fn(), + } + mapper = new Gn4PlatformMapper(avatarService) + }) + + describe('keywordsFromApi', () => { + it('should return an array of Keyword objects', () => { + const lang3 = 'eng' + const result = mapper.keywordsFromApi(keywords, thesaurus, lang3) + const resultThesaurus = { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + type: 'theme', + } + expect(result).toBeInstanceOf(Array) + expect(result[0].label).toBe('Addresses') + expect(result[0].type).toBe('theme') + expect(JSON.stringify(result[0].thesaurus)).toBe( + JSON.stringify(resultThesaurus) + ) + }) + }) +}) diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts index db5a54dfa1..8b7c6d9240 100644 --- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts @@ -8,11 +8,16 @@ import { Injectable } from '@angular/core' import { AvatarServiceInterface } from '../auth' import { map } from 'rxjs/operators' import { Observable, of } from 'rxjs' -import { ThesaurusModel } from '@geonetwork-ui/common/domain/model/thesaurus/thesaurus.model' import { + Keyword, UserFeedback, UserFeedbackViewModel, } from '@geonetwork-ui/common/domain/model/record' +import { + KeywordApiResponse, + ThesaurusApiResponse, +} from '@geonetwork-ui/api/metadata-converter' +import { KeywordType } from '@geonetwork-ui/common/domain/model/thesaurus' @Injectable() export class Gn4PlatformMapper { @@ -40,7 +45,7 @@ export class Gn4PlatformMapper { const { enabled, emailAddresses, - organisation, + organization, kind, lastLoginDate, accountNonExpired, @@ -52,8 +57,12 @@ export class Gn4PlatformMapper { return { ...apiUser, id: id.toString() } as UserModel } - thesaurusFromApi(thesaurus: any[], lang3?: string): ThesaurusModel { - return thesaurus.map((keyword) => { + keywordsFromApi( + keywords: KeywordApiResponse[], + thesaurus: ThesaurusApiResponse[], + lang3?: string + ): Keyword[] { + return keywords.map((keyword): Keyword => { let key = keyword.uri // sometines GN can prefix an URI with an "all thesaurus" URI; only keep the last one if (key.indexOf('@@@') > -1) { @@ -65,10 +74,22 @@ export class Gn4PlatformMapper { lang3 && lang3 in keyword.definitions ? keyword.definitions[lang3] : keyword.definition + + const matchedThesaurus = thesaurus.find( + (thes) => keyword.thesaurusKey === thes.key + ) + return { key, label, description, + type: matchedThesaurus?.dname as KeywordType, + thesaurus: { + id: matchedThesaurus?.key, + name: matchedThesaurus?.title, + url: new URL(matchedThesaurus?.url), + type: matchedThesaurus?.dname as KeywordType, + }, } }) } diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts index 8e7e77cfcc..35255da4b7 100644 --- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts @@ -17,6 +17,11 @@ import { A_USER_FEEDBACK, SOME_USER_FEEDBACKS, } from '@geonetwork-ui/common/fixtures' +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing' +import { HttpClient } from '@angular/common/http' let geonetworkVersion: string @@ -84,7 +89,47 @@ class ToolsApiServiceMock { ) } +class HttpClientMock { + get = jest.fn(() => + of([ + [ + { + key: 'external.theme.httpinspireeceuropaeutheme-theme', + dname: 'theme', + description: [], + filename: 'httpinspireeceuropaeutheme-theme.rdf', + title: 'GEMET - INSPIRE themes, version 1.0', + multilingualTitles: [], + dublinCoreMultilinguals: [], + date: '2008-06-01', + url: 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme', + defaultNamespace: 'http://inspire.ec.europa.eu/theme', + type: 'external', + activated: 'y', + }, + { + key: 'external.place.regions', + dname: 'place', + description: 'Generated from NaturalEarth datasets and SeaVox.', + filename: 'regions.rdf', + title: 'Continents, countries, sea regions of the world.', + multilingualTitles: [], + dublinCoreMultilinguals: [], + date: '2015-07-17', + url: 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.place.regions', + defaultNamespace: + 'http://geonetwork-opensource.org/thesaurus/naturalearth-and-seavox', + type: 'external', + activated: 'y', + }, + ], + ]) + ) +} class RegistriesApiServiceMock { + configuration = { + basePath: 'https://demo.georchestra.org/geonetwork/srv/api', + } searchKeywords = jest.fn(() => of([ { @@ -178,13 +223,19 @@ describe('Gn4PlatformService', () => { provide: UserfeedbackApiService, useClass: UserfeedbackApiServiceMock, }, + { + provide: HttpClient, + useClass: HttpClientMock, + }, ], + imports: [HttpClientTestingModule], }) service = TestBed.inject(Gn4PlatformService) meApiService = TestBed.inject(MeApiService) toolsApiService = TestBed.inject(ToolsApiService) registriesApiService = TestBed.inject(RegistriesApiService) userFeedbackApiService = TestBed.inject(UserfeedbackApiService as any) + TestBed.inject(HttpTestingController) }) it('creates', () => { @@ -289,15 +340,15 @@ describe('Gn4PlatformService', () => { }) describe('if key is a URI', () => { beforeEach(() => { - jest.spyOn(service, 'getThesaurusByUri') + jest.spyOn(service, 'getKeywordsByUri') }) - it('calls getThesaurusByUri using the thesaurus base path', async () => { + it('calls getKeywordsByUri using the thesaurus base path', async () => { await lastValueFrom( service.translateKey( 'https://www.eionet.europa.eu/gemet/concept/15028?abc#123' ) ) - expect(service.getThesaurusByUri).toHaveBeenCalledWith( + expect(service.getKeywordsByUri).toHaveBeenCalledWith( 'https://www.eionet.europa.eu/gemet/concept/' ) }) @@ -319,9 +370,9 @@ describe('Gn4PlatformService', () => { }) }) }) - describe('#getThesaurusByUri', () => { + describe('#getKeywordsByUri', () => { it('calls api service ', async () => { - service.getThesaurusByUri('http://inspire.ec.europa.eu/theme/') + service.getKeywordsByUri('http://inspire.ec.europa.eu/theme/') expect(registriesApiService.searchKeywords).toHaveBeenCalledWith( null, 'fre', @@ -335,7 +386,7 @@ describe('Gn4PlatformService', () => { }) it('returns mapped thesaurus with translated values', async () => { const thesaurusDomain = await lastValueFrom( - service.getThesaurusByUri('http://inspire.ec.europa.eu/theme/') + service.getKeywordsByUri('http://inspire.ec.europa.eu/theme/') ) expect(thesaurusDomain).toEqual([ { @@ -343,12 +394,30 @@ describe('Gn4PlatformService', () => { 'Localisation des propriétés fondée sur les identifiants des adresses, habituellement le nom de la rue, le numéro de la maison et le code postal.', key: 'http://inspire.ec.europa.eu/theme/ad', label: 'Adresses', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', }, { description: "Modèles numériques pour l'altitude des surfaces terrestres, glaciaires et océaniques. Comprend l'altitude terrestre, la bathymétrie et la ligne de rivage.", key: 'http://inspire.ec.europa.eu/theme/el', label: 'Altitude', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', }, ]) }) @@ -356,18 +425,36 @@ describe('Gn4PlatformService', () => { it('uses default values', async () => { service['langService']['iso3'] = 'ger' const thesaurusDomain = await lastValueFrom( - service.getThesaurusByUri('http://inspire.ec.europa.eu/theme/') + service.getKeywordsByUri('http://inspire.ec.europa.eu/theme/') ) expect(thesaurusDomain).toEqual([ { description: 'localization of properties', key: 'http://inspire.ec.europa.eu/theme/ad', label: 'addresses', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', }, { description: 'digital terrain models', key: 'http://inspire.ec.europa.eu/theme/el', label: 'altitude', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', }, ]) }) diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts index a4be2ba0e0..3f1519c17c 100644 --- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core' -import { Observable, of, switchMap } from 'rxjs' +import { Observable, combineLatest, of, switchMap } from 'rxjs' import { catchError, map, shareReplay, tap } from 'rxjs/operators' import { MeApiService, RegistriesApiService, SiteApiService, + ThesaurusInfoApiModel, ToolsApiService, UserfeedbackApiService, UsersApiService, @@ -12,13 +13,18 @@ import { import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { UserModel } from '@geonetwork-ui/common/domain/model/user/user.model' import { + Keyword, Organization, UserFeedback, } from '@geonetwork-ui/common/domain/model/record' import { Gn4PlatformMapper } from './gn4-platform.mapper' import { ltr } from 'semver' -import { ThesaurusModel } from '@geonetwork-ui/common/domain/model/thesaurus/thesaurus.model' import { LangService } from '@geonetwork-ui/util/i18n' +import { HttpClient } from '@angular/common/http' +import { + KeywordApiResponse, + ThesaurusApiResponse, +} from '@geonetwork-ui/api/metadata-converter' const minApiVersion = '4.2.2' @@ -60,7 +66,7 @@ export class Gn4PlatformService implements PlatformServiceInterface { * A map of already loaded thesauri (groups of keywords); the key is a URI * @private */ - private thesauri: Record> = {} + private keywordsByThesauri: Record> = {} constructor( private siteApiService: SiteApiService, @@ -70,7 +76,8 @@ export class Gn4PlatformService implements PlatformServiceInterface { private toolsApiService: ToolsApiService, private registriesApiService: RegistriesApiService, private langService: LangService, - private userfeedbackApiService: UserfeedbackApiService + private userfeedbackApiService: UserfeedbackApiService, + private httpClient: HttpClient ) { this.me$ = this.meApi.getMe().pipe( switchMap((apiUser) => this.mapper.userFromMeApi(apiUser)), @@ -121,7 +128,7 @@ export class Gn4PlatformService implements PlatformServiceInterface { // the thesaurus URI is inferred by removing a part of the keyword URI // this is not exact science but it's OK, we'll still end up loading a bunch of keywords at once anyway const thesaurusUri = key.replace(/\/([^/]+)$/, '/') - return this.getThesaurusByUri(thesaurusUri).pipe( + return this.getKeywordsByUri(thesaurusUri).pipe( map((thesaurus) => { for (const item of thesaurus) { if (item.key === key) return item.label @@ -133,12 +140,49 @@ export class Gn4PlatformService implements PlatformServiceInterface { return this.keyTranslations$.pipe(map((translations) => translations[key])) } - getThesaurusByUri(uri: string): Observable { - if (this.thesauri[uri]) { - return this.thesauri[uri] + private allThesaurus$ = this.httpClient + .get( + `${this.registriesApiService.configuration.basePath}/thesaurus?_content_type=json` + ) + .pipe( + map((thesaurus) => { + // FIXME: find a better way to exclude place keywords + // thesaurus[0].filter((thes) => thes.dname !== 'place') + return thesaurus[0] as ThesaurusApiResponse[] + }), + shareReplay(1) + ) + + searchKeywords(query: string): Observable { + const keywords$: Observable = + this.registriesApiService.searchKeywords( + query, + this.langService.iso3, + 10, + 0, + null, + null, + null, + `*${query}*` + ) as Observable + + return combineLatest([keywords$, this.allThesaurus$]).pipe( + map(([keywords, thesaurus]) => { + return this.mapper.keywordsFromApi( + keywords, + thesaurus, + this.langService.iso3 + ) + }) + ) + } + + getKeywordsByUri(uri: string): Observable { + if (this.keywordsByThesauri[uri]) { + return this.keywordsByThesauri[uri] } - this.thesauri[uri] = this.registriesApiService - .searchKeywords( + const keywords$: Observable = + this.registriesApiService.searchKeywords( null, this.langService.iso3, 1000, @@ -147,18 +191,23 @@ export class Gn4PlatformService implements PlatformServiceInterface { null, null, `${uri}*` - ) - .pipe( - map((thesaurus) => - this.mapper.thesaurusFromApi( - thesaurus as any[], - this.langService.iso3 - ) - ), - shareReplay(1) - ) + ) as Observable + + this.keywordsByThesauri[uri] = combineLatest([ + keywords$, + this.allThesaurus$, + ]).pipe( + map(([keywords, thesaurus]) => { + return this.mapper.keywordsFromApi( + keywords, + thesaurus, + this.langService.iso3 + ) + }), + shareReplay(1) + ) - return this.thesauri[uri] + return this.keywordsByThesauri[uri] } getUserFeedbacks(uuid: string): Observable { diff --git a/libs/api/repository/src/test-setup.ts b/libs/api/repository/src/test-setup.ts index 90bc5062ea..4c58ab7724 100644 --- a/libs/api/repository/src/test-setup.ts +++ b/libs/api/repository/src/test-setup.ts @@ -1 +1,2 @@ import 'jest-preset-angular/setup-jest' +import '../../../../jest.setup' diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index eeb390358b..fb799d3643 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -2,6 +2,7 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker' import type { Individual } from './contact.model' import type { Organization } from './organization.model' import type { Geometry } from 'geojson' +import { KeywordType, ThesaurusModel } from '../thesaurus' type Uuid = string @@ -60,18 +61,12 @@ export type SpatialRepresentationType = | 'table' | 'point' -export type KeywordType = 'place' | 'temporal' | 'theme' | 'other' - -export type KeywordThesaurus = { - id: string - name?: string - url?: URL -} - export interface Keyword { + key?: string label: string + description?: string type: KeywordType - thesaurus?: KeywordThesaurus + thesaurus?: ThesaurusModel } // languages should be expressed using two-letters ISO 639-1 codes export type LanguageCode = string @@ -170,12 +165,12 @@ export interface DatasetSpatialExtent { } /** - * At least a start or an end date should be provided + * Period if both start and end are provided + * Instant if only start is provided */ export interface DatasetTemporalExtent { - start?: Date + start: Date end?: Date - description?: string } export interface DatasetRecord extends BaseRecord { diff --git a/libs/common/domain/src/lib/model/record/organization.model.ts b/libs/common/domain/src/lib/model/record/organization.model.ts index fa387137b0..8db5ed1a7b 100644 --- a/libs/common/domain/src/lib/model/record/organization.model.ts +++ b/libs/common/domain/src/lib/model/record/organization.model.ts @@ -1,5 +1,6 @@ export interface Organization { name: string + email?: string description?: string website?: URL logoUrl?: URL diff --git a/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts b/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts index 8b26fb5879..75ae29baff 100644 --- a/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts +++ b/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts @@ -1,7 +1,14 @@ -export interface ThesaurusItemModel { - key: string - label: string - description?: string -} +export type KeywordType = 'place' | 'temporal' | 'theme' | 'other' -export type ThesaurusModel = ThesaurusItemModel[] +export interface ThesaurusModel { + id: string + name?: string + url?: URL + thesaurusKey?: string + definition?: string + definitions?: object + uri?: string + value?: string + values?: object + type?: KeywordType +} diff --git a/libs/common/domain/src/lib/platform.service.interface.ts b/libs/common/domain/src/lib/platform.service.interface.ts index 545735a6f4..3a39f5a3b9 100644 --- a/libs/common/domain/src/lib/platform.service.interface.ts +++ b/libs/common/domain/src/lib/platform.service.interface.ts @@ -1,8 +1,7 @@ import type { Observable } from 'rxjs' import type { UserModel } from './model/user/user.model' import type { Organization } from './model/record/organization.model' -import type { ThesaurusModel } from './model/thesaurus/' -import { UserFeedback } from './model/record' +import { Keyword, UserFeedback } from './model/record' export abstract class PlatformServiceInterface { abstract getType(): string @@ -16,7 +15,8 @@ export abstract class PlatformServiceInterface { ): Observable abstract getOrganizations(): Observable abstract translateKey(key: string): Observable - abstract getThesaurusByUri(uri: string): Observable + abstract searchKeywords(query: string): Observable + abstract getKeywordsByUri(uri: string): Observable abstract getUserFeedbacks(recordUuid: string): Observable abstract postUserFeedbacks(recordUuid: UserFeedback): Observable } diff --git a/libs/common/domain/src/lib/repository/records-repository.interface.ts b/libs/common/domain/src/lib/repository/records-repository.interface.ts index 063873bc99..a8cc83aca2 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -11,12 +11,48 @@ import { CatalogRecord } from '../model/record' export abstract class RecordsRepositoryInterface { abstract search(params: SearchParams): Observable abstract getMatchesCount(filters: FieldFilters): Observable - abstract getByUniqueIdentifier( - uniqueIdentifier: string - ): Observable + abstract getRecord(uniqueIdentifier: string): Observable abstract aggregate(params: AggregationsParams): Observable abstract getSimilarRecords( similarTo: CatalogRecord ): Observable abstract fuzzySearch(query: string): Observable + + /** + * This emits once: + * - record object; if a draft exists, this will return it + * - serialized representation of the record as text + * - boolean indicating if the record has been saved at least once in a final version (i.e. not only as draft) + * @param uniqueIdentifier + * @returns Observable<[CatalogRecord, string, boolean] | null> + */ + abstract openRecordForEdition( + uniqueIdentifier: string + ): Observable<[CatalogRecord, string, boolean] | null> + + /** + * @param record + * @param referenceRecordSource + * @returns Observable Returns the unique identifier of the record as it was when saved + */ + abstract saveRecord( + record: CatalogRecord, + referenceRecordSource?: string + ): Observable + + /** + * @param record + * @param referenceRecordSource + * @returns Observable Returns the source of the record as it was serialized when saved + */ + abstract saveRecordAsDraft( + record: CatalogRecord, + referenceRecordSource?: string + ): Observable + + abstract clearRecordDraft(uniqueIdentifier: string): void + abstract recordHasDraft(uniqueIdentifier: string): boolean + + /** will return all pending drafts, both published and not published */ + abstract getAllDrafts(): Observable } diff --git a/libs/common/fixtures/src/lib/organisations.fixture.ts b/libs/common/fixtures/src/lib/organisations.fixture.ts index 96734f1aab..a2c5818b36 100644 --- a/libs/common/fixtures/src/lib/organisations.fixture.ts +++ b/libs/common/fixtures/src/lib/organisations.fixture.ts @@ -5,84 +5,112 @@ export const ORGANISATIONS_FIXTURE: Organization[] = deepFreeze([ { name: 'I Data Org', description: 'one org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo1.png'), recordCount: 12, }, { name: 'H Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo2.png'), recordCount: 15, }, { name: 'J Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo3.png'), recordCount: 6, }, { name: 'G Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo4.png'), recordCount: 8, }, { name: 'B Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo5.png'), recordCount: 2, }, { name: 'D Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo6.png'), recordCount: 17, }, { name: 'F Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo7.png'), recordCount: 14, }, { name: 'A Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo8.png'), recordCount: 3, }, { name: 'C Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo9.png'), recordCount: 9, }, { name: 'E Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo10.png'), recordCount: 1, }, { name: 'é Data Org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo10.png'), recordCount: 2, }, { name: 'wizard-org', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo11.png'), recordCount: 2, }, { name: "Université de l'Ingénierie", description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo12.png'), recordCount: 2, }, { name: 'ARS / Agence régionale de santé', description: 'another org for testing', + email: 'test@gmail.com', + website: new URL('https://my-geonetwork.org/'), logoUrl: new URL('https://my-geonetwork.org/logo12.png'), recordCount: 2, }, diff --git a/libs/common/fixtures/src/lib/records.fixtures.ts b/libs/common/fixtures/src/lib/records.fixtures.ts index c193335ee6..cc08efd8e3 100644 --- a/libs/common/fixtures/src/lib/records.fixtures.ts +++ b/libs/common/fixtures/src/lib/records.fixtures.ts @@ -233,6 +233,14 @@ Malgré l'attention portée à la création de ces données, il est rappelé que description: 'Téléchargement du fichier', mimeType: 'x-gis/x-shapefile', }, + { + type: 'service', + url: new URL('https://my-org.net/ogc'), + accessServiceProtocol: 'ogcFeatures', + name: 'ogcFeaturesSecondRecord', + description: 'This OGC service is the second part of the download', + identifierInService: 'my:featuretype', + }, ], lineage: `Document d’urbanisme numérisé conformément aux prescriptions nationales du CNIG par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne. Ce lot de données produit en 2019, a été numérisé à partir du PCI Vecteur de 2019 et contrôlé par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne.`, @@ -252,3 +260,206 @@ Ce lot de données produit en 2019, a été numérisé à partir du PCI Vecteur languages: ['fr', 'de'], }, ]) + +export const DATASET_RECORD_SIMPLE: DatasetRecord = { + uniqueIdentifier: 'my-dataset-001', + kind: 'dataset', + languages: [], + recordUpdated: new Date('2022-02-01T14:12:00.000Z'), + resourceCreated: new Date('2022-09-01T12:18:19.000Z'), + resourceUpdated: new Date('2022-12-04T14:12:00.000Z'), + status: 'ongoing', + title: 'A very interesting dataset (un jeu de données très intéressant)', + abstract: `This dataset has been established for testing purposes.`, + ownerOrganization: { name: 'MyOrganization' }, + contacts: [ + { + email: 'bob@org.net', + position: 'developer', + organization: { name: 'MyOrganization' }, + role: 'point_of_contact', + firstName: 'Bob', + lastName: 'TheGreat', + }, + ], + contactsForResource: [], + keywords: [], + topics: ['testData'], + licenses: [], + legalConstraints: [], + securityConstraints: [], + otherConstraints: [], + lineage: 'This record was edited manually to test the conversion processes', + spatialRepresentation: 'grid', + overviews: [], + spatialExtents: [], + temporalExtents: [], + distributions: [ + { + type: 'download', + url: new URL('http://my-org.net/download/1.zip'), + name: 'Direct download', + description: 'Dataset downloaded as a shapefile', + mimeType: 'x-gis/x-shapefile', + }, + ], + updateFrequency: { per: 'month', updatedTimes: 3 }, +} + +export const DATASET_RECORD_SIMPLE_AS_XML = ` + + + + + my-dataset-001 + + + + + + + dataset + + + + + + + pointOfContact + + + + + MyOrganization + + + + + + + bob@org.net + + + + + + + + + Bob TheGreat + + + developer + + + + + + + + + + + 2022-02-01T15:12:00 + + + revision + + + + + + + + + A very interesting dataset (un jeu de données très intéressant) + + + + + 2022-09-01T14:18:19 + + + creation + + + + + + + 2022-12-04T15:12:00 + + + revision + + + + + + + This dataset has been established for testing purposes. + + + testData + + + onGoing + + + + + P0Y0M10D + + + + + grid + + + + + + + + + + + x-gis/x-shapefile + + + + + + + + + + + http://my-org.net/download/1.zip + + + Dataset downloaded as a shapefile + + + Direct download + + + WWW:DOWNLOAD + + + + + + + + + + + + + + This record was edited manually to test the conversion processes + + + +` diff --git a/libs/data-access/gn4/src/openapi/api/records.api.service.ts b/libs/data-access/gn4/src/openapi/api/records.api.service.ts index 04cb7f290c..6884ce91a7 100644 --- a/libs/data-access/gn4/src/openapi/api/records.api.service.ts +++ b/libs/data-access/gn4/src/openapi/api/records.api.service.ts @@ -4586,8 +4586,8 @@ export class RecordsApiService { accept?: string, observe?: 'body', reportProgress?: boolean, - options?: { httpHeaderAccept?: 'application/json' | 'application/xml' } - ): Observable + options?: { httpHeaderAccept?: 'application/xml' | 'application/json' } + ): Observable public getRecordAs( metadataUuid: string, addSchemaLocation?: boolean, @@ -4598,8 +4598,8 @@ export class RecordsApiService { accept?: string, observe?: 'response', reportProgress?: boolean, - options?: { httpHeaderAccept?: 'application/json' | 'application/xml' } - ): Observable> + options?: { httpHeaderAccept?: 'application/xml' | 'application/json' } + ): Observable> public getRecordAs( metadataUuid: string, addSchemaLocation?: boolean, @@ -4610,8 +4610,8 @@ export class RecordsApiService { accept?: string, observe?: 'events', reportProgress?: boolean, - options?: { httpHeaderAccept?: 'application/json' | 'application/xml' } - ): Observable> + options?: { httpHeaderAccept?: 'application/xml' | 'application/json' } + ): Observable> public getRecordAs( metadataUuid: string, addSchemaLocation?: boolean, @@ -4622,7 +4622,7 @@ export class RecordsApiService { accept?: string, observe: any = 'body', reportProgress: boolean = false, - options?: { httpHeaderAccept?: 'application/json' | 'application/xml' } + options?: { httpHeaderAccept?: 'application/xml' | 'application/json' } ): Observable { if (metadataUuid === null || metadataUuid === undefined) { throw new Error( @@ -4677,8 +4677,8 @@ export class RecordsApiService { if (httpHeaderAcceptSelected === undefined) { // to determine the Accept header const httpHeaderAccepts: string[] = [ - 'application/json', 'application/xml', + 'application/json', ] httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts) @@ -4695,7 +4695,7 @@ export class RecordsApiService { responseType_ = 'text' } - return this.httpClient.get( + return this.httpClient.get( `${this.configuration.basePath}/records/${encodeURIComponent( String(metadataUuid) )}/formatters/xml`, diff --git a/libs/data-access/gn4/src/openapi/model/user.api.model.ts b/libs/data-access/gn4/src/openapi/model/user.api.model.ts index 1404ec9743..b9137375be 100644 --- a/libs/data-access/gn4/src/openapi/model/user.api.model.ts +++ b/libs/data-access/gn4/src/openapi/model/user.api.model.ts @@ -21,7 +21,7 @@ export interface UserApiModel { emailAddresses?: Set addresses?: Set primaryAddress?: AddressApiModel - organisation?: string + organization?: string kind?: string lastLoginDate?: string authorities?: Array diff --git a/libs/data-access/gn4/src/spec.yaml b/libs/data-access/gn4/src/spec.yaml index 93ce3a5b5c..ee451ac7a1 100644 --- a/libs/data-access/gn4/src/spec.yaml +++ b/libs/data-access/gn4/src/spec.yaml @@ -11114,16 +11114,15 @@ paths: default: description: default response content: - application/json: {} + application/xml: + schema: + type: string "200": description: Return the record. content: application/xml: schema: - type: object - application/json: - schema: - type: object + type: string "403": description: Operation not allowed. User needs to be able to view the resource. content: diff --git a/libs/feature/catalog/src/index.ts b/libs/feature/catalog/src/index.ts index efa22c7852..54cc255a57 100644 --- a/libs/feature/catalog/src/index.ts +++ b/libs/feature/catalog/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/feature-catalog.module' +export * from './lib/organization-url.token' export * from './lib/sources/sources.service' export * from './lib/sources/sources.model' export * from './lib/records/records.service' diff --git a/libs/feature/catalog/src/lib/feature-catalog.module.ts b/libs/feature/catalog/src/lib/feature-catalog.module.ts index c3f2fd8265..7c97c0d726 100644 --- a/libs/feature/catalog/src/lib/feature-catalog.module.ts +++ b/libs/feature/catalog/src/lib/feature-catalog.module.ts @@ -4,7 +4,6 @@ import { UiCatalogModule } from '@geonetwork-ui/ui/catalog' import { GroupsApiService, SearchApiService, - SiteApiService, } from '@geonetwork-ui/data-access/gn4' import { CommonModule } from '@angular/common' import { SourceLabelComponent } from './source-label/source-label.component' diff --git a/libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts b/libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts index d2c2624682..efff9f9afa 100644 --- a/libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts +++ b/libs/feature/catalog/src/lib/my-org/my-org.service.spec.ts @@ -69,7 +69,7 @@ describe('MyOrgService', () => { it('should update myOrgDataSubject when authService user$ emits a user', () => { const user: UserModel = { - organisation: 'Géo2France', + organization: 'Géo2France', id: '2', profile: 'profile', username: 'username', @@ -104,8 +104,8 @@ describe('MyOrgService', () => { it('should update myOrgDataSubject when authService allUsers$ emits users', () => { const users: UserApiModel[] = [ - { organisation: 'Géo2France' }, - { organisation: 'Géo2France' }, + { organization: 'Géo2France' }, + { organization: 'Géo2France' }, ] allUsersSubject.next(users) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.html b/libs/feature/catalog/src/lib/organisations/organisations.component.html index f832e2289e..3d253d16f3 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.html +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.html @@ -19,7 +19,7 @@ [showContent]="!!organisation.name" > diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts b/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts index 70e97b09b7..138e62fdeb 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts @@ -4,6 +4,7 @@ import { DebugElement, EventEmitter, Input, + NO_ERRORS_SCHEMA, Output, } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' @@ -27,8 +28,8 @@ class OrganisationsFilterMockComponent { template: '
', }) class OrganisationPreviewMockComponent { - @Input() organisation: Organization - @Output() clickedOrganisation = new EventEmitter() + @Input() organization: Organization + @Output() clickedOrganization = new EventEmitter() } @Component({ @@ -85,6 +86,7 @@ describe('OrganisationsComponent', () => { useClass: OrganisationsServiceMock, }, ], + schemas: [NO_ERRORS_SCHEMA], }) .overrideComponent(OrganisationsComponent, { set: { changeDetection: ChangeDetectionStrategy.Default }, @@ -118,10 +120,10 @@ describe('OrganisationsComponent', () => { .map((debugElement) => debugElement.componentInstance) }) it('should pass first organisation (sorted by name-asc) to first ui preview component', () => { - expect(orgPreviewComponents[0].organisation.name).toEqual('A Data Org') + expect(orgPreviewComponents[0].organization.name).toEqual('A Data Org') }) it('should pass 6th organisation (sorted by name-asc) on page to 6th ui preview component', () => { - expect(orgPreviewComponents[5].organisation.name).toEqual('E Data Org') + expect(orgPreviewComponents[5].organization.name).toEqual('E Data Org') }) }) describe('pass params to ui pagination component', () => { @@ -152,13 +154,13 @@ describe('OrganisationsComponent', () => { expect(paginationComponentDE.componentInstance.currentPage).toEqual(2) }) it('should pass first organisation of second page (sorted by name-asc) to first ui preview component', () => { - expect(orgPreviewComponents[0].organisation.name).toEqual( + expect(orgPreviewComponents[0].organization.name).toEqual( 'é Data Org' ) }) it('should pass last organisation of second page (sorted by name-asc) to last ui preview component', () => { expect( - orgPreviewComponents[orgPreviewComponents.length - 1].organisation + orgPreviewComponents[orgPreviewComponents.length - 1].organization .name ).toEqual('J Data Org') }) @@ -193,12 +195,12 @@ describe('OrganisationsComponent', () => { expect(organisations[0]).toEqual(ORGANISATIONS_FIXTURE[5]) }) it('should pass organisation with max recordCount to first preview component', () => { - expect(orgPreviewComponents[0].organisation).toEqual( + expect(orgPreviewComponents[0].organization).toEqual( ORGANISATIONS_FIXTURE[5] ) }) it('should pass organisation with 6th highest recordCount to 6th preview component', () => { - expect(orgPreviewComponents[5].organisation).toEqual( + expect(orgPreviewComponents[5].organization).toEqual( ORGANISATIONS_FIXTURE[3] ) }) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.ts b/libs/feature/catalog/src/lib/organisations/organisations.component.ts index 436345abe2..84194910c4 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.ts @@ -10,10 +10,10 @@ import { import { Organization } from '@geonetwork-ui/common/domain/model/record' import { BehaviorSubject, combineLatest, Observable } from 'rxjs' import { map, startWith, tap } from 'rxjs/operators' -import { ORGANIZATION_URL_TOKEN } from '../feature-catalog.module' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { SortByField } from '@geonetwork-ui/common/domain/model/search' import { createFuzzyFilter } from '@geonetwork-ui/util/shared' +import { ORGANIZATION_PAGE_URL_TOKEN } from '../organization-url.token' @Component({ selector: 'gn-ui-organisations', @@ -28,7 +28,7 @@ export class OrganisationsComponent { constructor( private organisationsService: OrganizationsServiceInterface, @Optional() - @Inject(ORGANIZATION_URL_TOKEN) + @Inject(ORGANIZATION_PAGE_URL_TOKEN) private urlTemplate: string ) {} diff --git a/libs/feature/catalog/src/lib/organization-url.token.ts b/libs/feature/catalog/src/lib/organization-url.token.ts new file mode 100644 index 0000000000..91deb6fdb8 --- /dev/null +++ b/libs/feature/catalog/src/lib/organization-url.token.ts @@ -0,0 +1,6 @@ +import { InjectionToken } from '@angular/core' + +// expects the replacement key ${name} +export const ORGANIZATION_PAGE_URL_TOKEN = new InjectionToken( + 'organization-page-url-token' +) diff --git a/libs/feature/dataviz/src/lib/feature-dataviz.module.ts b/libs/feature/dataviz/src/lib/feature-dataviz.module.ts index d414715c09..91c70f365b 100644 --- a/libs/feature/dataviz/src/lib/feature-dataviz.module.ts +++ b/libs/feature/dataviz/src/lib/feature-dataviz.module.ts @@ -12,7 +12,7 @@ import { import { TableViewComponent } from './table-view/table-view.component' import { ChartViewComponent } from './chart-view/chart-view.component' import { TranslateModule } from '@ngx-translate/core' -import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { PopupAlertComponent, UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' @NgModule({ @@ -26,6 +26,7 @@ import { UiInputsModule } from '@geonetwork-ui/ui/inputs' TranslateModule, ChartComponent, UiInputsModule, + PopupAlertComponent, ], declarations: [ GeoTableViewComponent, diff --git a/libs/feature/editor/src/lib/+state/editor.actions.ts b/libs/feature/editor/src/lib/+state/editor.actions.ts index c9886a46e2..f2c6b3f93d 100644 --- a/libs/feature/editor/src/lib/+state/editor.actions.ts +++ b/libs/feature/editor/src/lib/+state/editor.actions.ts @@ -4,7 +4,11 @@ import { SaveRecordError } from './editor.models' export const openRecord = createAction( '[Editor] Open record', - props<{ record: CatalogRecord }>() + props<{ + record: CatalogRecord + alreadySavedOnce: boolean + recordSource?: string | null + }>() ) export const updateRecordField = createAction( @@ -22,3 +26,5 @@ export const saveRecordFailure = createAction( '[Editor] Save record failure', props<{ error: SaveRecordError }>() ) + +export const draftSaveSuccess = createAction('[Editor] Draft save success') diff --git a/libs/feature/editor/src/lib/+state/editor.effects.spec.ts b/libs/feature/editor/src/lib/+state/editor.effects.spec.ts index f984d3cfb3..7ec73663fe 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.spec.ts @@ -1,17 +1,32 @@ import { TestBed } from '@angular/core/testing' import { provideMockActions } from '@ngrx/effects/testing' import { Action } from '@ngrx/store' -import { provideMockStore } from '@ngrx/store/testing' -import { hot } from 'jasmine-marbles' -import { Observable, of, throwError } from 'rxjs' +import { MockStore, provideMockStore } from '@ngrx/store/testing' +import { getTestScheduler, hot } from 'jasmine-marbles' +import { firstValueFrom, Observable, of, throwError } from 'rxjs' import * as EditorActions from './editor.actions' import { EditorEffects } from './editor.effects' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { EditorService } from '../services/editor.service' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { EditorPartialState } from './editor.reducer' class EditorServiceMock { - loadRecordByUuid = jest.fn(() => of(DATASET_RECORDS[0])) - saveRecord = jest.fn((record) => of(record)) + saveRecord = jest.fn((record) => of([record, 'blabla'])) + saveRecordAsDraft = jest.fn(() => of('blabla')) +} +class RecordsRepositoryMock { + recordHasDraft = jest.fn(() => true) +} + +const initialEditorState = { + record: DATASET_RECORDS[0], + recordSource: 'blabla', + saving: false, + saveError: null, + changedSinceSave: false, + alreadySavedOnce: true, + fieldsConfig: [], } describe('EditorEffects', () => { @@ -25,22 +40,19 @@ describe('EditorEffects', () => { providers: [ EditorEffects, provideMockActions(() => actions), - provideMockStore({ + provideMockStore({ initialState: { - editor: { - record: DATASET_RECORDS[0], - loading: false, - loadError: null, - saving: false, - saveError: null, - changedSinceSave: false, - }, + editor: initialEditorState, }, }), { provide: EditorService, useClass: EditorServiceMock, }, + { + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, + }, ], }) @@ -56,9 +68,34 @@ describe('EditorEffects', () => { }) const expected = hot('-(ab)|', { a: EditorActions.saveRecordSuccess(), - b: EditorActions.openRecord({ record: DATASET_RECORDS[0] }), + b: EditorActions.openRecord({ + record: DATASET_RECORDS[0], + alreadySavedOnce: true, + recordSource: 'blabla', + }), }) expect(effects.saveRecord$).toBeObservable(expected) + expect(service.saveRecord).toHaveBeenCalledWith( + DATASET_RECORDS[0], + [], + false + ) + }) + it('asks for a new unique identifier if the record was never saved', async () => { + const store = TestBed.inject(MockStore) + store.setState({ + editor: { + ...initialEditorState, + alreadySavedOnce: false, + }, + }) + actions = of(EditorActions.saveRecord()) + await firstValueFrom(effects.saveRecord$) + expect(service.saveRecord).toHaveBeenCalledWith( + DATASET_RECORDS[0], + [], + true + ) }) }) @@ -94,4 +131,69 @@ describe('EditorEffects', () => { expect(effects.markAsChanged$).toBeObservable(expected) }) }) + + describe('saveRecordDraft$', () => { + it('does not dispatch any action', () => { + actions = hot('-a-', { + a: EditorActions.updateRecordField({ + field: 'title', + value: 'Hello world', + }), + }) + expect(effects.saveRecordDraft$).toBeObservable(hot('---')) + expect(service.saveRecordAsDraft).not.toHaveBeenCalled() + }) + it('calls editorService.saveRecordAsDraft after 1000ms', () => { + getTestScheduler().run(() => { + actions = hot('a-a 1050ms -', { + a: EditorActions.updateRecordField({ + field: 'title', + value: 'Hello world', + }), + }) + expect(effects.saveRecordDraft$).toBeObservable( + hot('--- 999ms b', { + b: EditorActions.draftSaveSuccess(), + }) + ) + expect(service.saveRecordAsDraft).toHaveBeenCalledWith( + DATASET_RECORDS[0] + ) + }) + }) + }) + + describe('checkHasChangesOnOpen$', () => { + describe('if the record has a draft', () => { + it('dispatch markRecordAsChanged', () => { + actions = hot('-a-|', { + a: EditorActions.openRecord({ + record: DATASET_RECORDS[0], + alreadySavedOnce: true, + }), + }) + const expected = hot('-a-|', { + a: EditorActions.markRecordAsChanged(), + }) + expect(effects.checkHasChangesOnOpen$).toBeObservable(expected) + }) + }) + describe('if the record has no draft', () => { + beforeEach(() => { + ;( + TestBed.inject(RecordsRepositoryInterface).recordHasDraft as jest.Mock + ).mockImplementationOnce(() => false) + }) + it('dispatches nothing', () => { + actions = hot('-a-|', { + a: EditorActions.openRecord({ + record: DATASET_RECORDS[0], + alreadySavedOnce: true, + }), + }) + const expected = hot('---|') + expect(effects.checkHasChangesOnOpen$).toBeObservable(expected) + }) + }) + }) }) diff --git a/libs/feature/editor/src/lib/+state/editor.effects.ts b/libs/feature/editor/src/lib/+state/editor.effects.ts index 6ad336761f..7c28a2890f 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.ts @@ -1,16 +1,22 @@ import { inject, Injectable } from '@angular/core' import { Actions, createEffect, ofType } from '@ngrx/effects' -import { of, withLatestFrom } from 'rxjs' +import { debounceTime, filter, of, withLatestFrom } from 'rxjs' import { catchError, map, switchMap } from 'rxjs/operators' import * as EditorActions from './editor.actions' import { EditorService } from '../services/editor.service' import { Store } from '@ngrx/store' -import { selectRecord, selectRecordFieldsConfig } from './editor.selectors' +import { + selectRecord, + selectRecordAlreadySavedOnce, + selectRecordFieldsConfig, +} from './editor.selectors' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @Injectable() export class EditorEffects { private actions$ = inject(Actions) private editorService = inject(EditorService) + private recordsRepository = inject(RecordsRepositoryInterface) private store = inject(Store) saveRecord$ = createEffect(() => @@ -18,24 +24,31 @@ export class EditorEffects { ofType(EditorActions.saveRecord), withLatestFrom( this.store.select(selectRecord), - this.store.select(selectRecordFieldsConfig) + this.store.select(selectRecordFieldsConfig), + this.store.select(selectRecordAlreadySavedOnce) ), - switchMap(([, record, fieldsConfig]) => - this.editorService.saveRecord(record, fieldsConfig).pipe( - switchMap((newRecord) => - of( - EditorActions.saveRecordSuccess(), - EditorActions.openRecord({ record: newRecord }) - ) - ), - catchError((error) => - of( - EditorActions.saveRecordFailure({ - error: error.message, - }) + switchMap(([, record, fieldsConfig, alreadySavedOnce]) => + this.editorService + .saveRecord(record, fieldsConfig, !alreadySavedOnce) + .pipe( + switchMap(([record, recordSource]) => + of( + EditorActions.saveRecordSuccess(), + EditorActions.openRecord({ + record, + alreadySavedOnce: true, + recordSource, + }) + ) + ), + catchError((error) => + of( + EditorActions.saveRecordFailure({ + error: error.message, + }) + ) ) ) - ) ) ) ) @@ -46,4 +59,25 @@ export class EditorEffects { map(() => EditorActions.markRecordAsChanged()) ) ) + + saveRecordDraft$ = createEffect(() => + this.actions$.pipe( + ofType(EditorActions.updateRecordField), + debounceTime(1000), + withLatestFrom(this.store.select(selectRecord)), + switchMap(([, record]) => this.editorService.saveRecordAsDraft(record)), + map(() => EditorActions.draftSaveSuccess()) + ) + ) + + checkHasChangesOnOpen$ = createEffect(() => + this.actions$.pipe( + ofType(EditorActions.openRecord), + map(({ record }) => + this.recordsRepository.recordHasDraft(record.uniqueIdentifier) + ), + filter((hasDraft) => hasDraft), + map(() => EditorActions.markRecordAsChanged()) + ) + ) } diff --git a/libs/feature/editor/src/lib/+state/editor.facade.ts b/libs/feature/editor/src/lib/+state/editor.facade.ts index c5ad803ef3..d6d0992df9 100644 --- a/libs/feature/editor/src/lib/+state/editor.facade.ts +++ b/libs/feature/editor/src/lib/+state/editor.facade.ts @@ -3,7 +3,7 @@ import { select, Store } from '@ngrx/store' import * as EditorActions from './editor.actions' import * as EditorSelectors from './editor.selectors' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' -import { filter, Observable } from 'rxjs' +import { filter } from 'rxjs' import { Actions, ofType } from '@ngrx/effects' @Injectable() @@ -12,6 +12,10 @@ export class EditorFacade { private actions$ = inject(Actions) record$ = this.store.pipe(select(EditorSelectors.selectRecord)) + recordSource$ = this.store.pipe(select(EditorSelectors.selectRecordSource)) + alreadySavedOnce$ = this.store.pipe( + select(EditorSelectors.selectRecordAlreadySavedOnce) + ) saving$ = this.store.pipe(select(EditorSelectors.selectRecordSaving)) saveError$ = this.store.pipe( select(EditorSelectors.selectRecordSaveError), @@ -22,9 +26,16 @@ export class EditorFacade { select(EditorSelectors.selectRecordChangedSinceSave) ) recordFields$ = this.store.pipe(select(EditorSelectors.selectRecordFields)) + draftSaveSuccess$ = this.actions$.pipe(ofType(EditorActions.draftSaveSuccess)) - openRecord(record: CatalogRecord) { - this.store.dispatch(EditorActions.openRecord({ record })) + openRecord( + record: CatalogRecord, + recordSource: string, + alreadySavedOnce: boolean + ) { + this.store.dispatch( + EditorActions.openRecord({ record, recordSource, alreadySavedOnce }) + ) } saveRecord() { diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.spec.ts b/libs/feature/editor/src/lib/+state/editor.reducer.spec.ts index 875d025409..59b175e4b6 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.spec.ts @@ -1,25 +1,54 @@ import { Action } from '@ngrx/store' import * as EditorActions from './editor.actions' import { + editorReducer, EditorState, initialEditorState, - editorReducer, } from './editor.reducer' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' describe('Editor Reducer', () => { describe('valid Editor actions', () => { - it('openRecord', () => { + it('openRecord (with source)', () => { + const action = EditorActions.openRecord({ + record: DATASET_RECORDS[0], + recordSource: 'blabla', + alreadySavedOnce: false, + }) + const result: EditorState = editorReducer( + { + ...initialEditorState, + changedSinceSave: true, + recordSource: 'abcd', + alreadySavedOnce: true, + }, + action + ) + + expect(result.record).toBe(DATASET_RECORDS[0]) + expect(result.changedSinceSave).toBe(false) + expect(result.recordSource).toBe('blabla') + expect(result.alreadySavedOnce).toBe(false) + }) + it('openRecord (without source)', () => { const action = EditorActions.openRecord({ record: DATASET_RECORDS[0], + alreadySavedOnce: true, }) const result: EditorState = editorReducer( - { ...initialEditorState, changedSinceSave: true }, + { + ...initialEditorState, + changedSinceSave: true, + recordSource: 'blabla', + alreadySavedOnce: false, + }, action ) expect(result.record).toBe(DATASET_RECORDS[0]) expect(result.changedSinceSave).toBe(false) + expect(result.recordSource).toBe(null) + expect(result.alreadySavedOnce).toBe(true) }) it('saveRecord action', () => { const action = EditorActions.saveRecord() diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts index 7d207264c0..425e3d0c41 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts @@ -7,8 +7,18 @@ import { DEFAULT_FIELDS } from '../fields.config' export const EDITOR_FEATURE_KEY = 'editor' +/** + * @property record The record being edited + * @property recordSource Original representation of the record as text, used as a reference; null means the record hasn't be serialized yet + * @property saving + * @property saveError + * @property changedSinceSave + * @property fieldsConfig Configuration for the fields in the editor + */ export interface EditorState { record: CatalogRecord | null + recordSource: string | null + alreadySavedOnce: boolean saving: boolean saveError: SaveRecordError | null changedSinceSave: boolean @@ -21,6 +31,8 @@ export interface EditorPartialState { export const initialEditorState: EditorState = { record: null, + recordSource: null, + alreadySavedOnce: false, saving: false, saveError: null, changedSinceSave: false, @@ -29,11 +41,16 @@ export const initialEditorState: EditorState = { const reducer = createReducer( initialEditorState, - on(EditorActions.openRecord, (state, { record }) => ({ - ...state, - changedSinceSave: false, - record, - })), + on( + EditorActions.openRecord, + (state, { record, recordSource, alreadySavedOnce }) => ({ + ...state, + changedSinceSave: false, + recordSource: recordSource ?? null, + alreadySavedOnce, + record, + }) + ), on(EditorActions.saveRecord, (state) => ({ ...state, saving: true, diff --git a/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts b/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts index 847ac23f37..0d61773f97 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts @@ -11,6 +11,7 @@ describe('Editor Selectors', () => { editor: { ...initialEditorState, record: DATASET_RECORDS[0], + recordSource: 'blabla', saveError: 'something went wrong', saving: false, changedSinceSave: true, @@ -24,6 +25,11 @@ describe('Editor Selectors', () => { expect(result).toBe(DATASET_RECORDS[0]) }) + it('selectRecordSource() should return the source of the current record', () => { + const result = EditorSelectors.selectRecordSource(state) + expect(result).toBe('blabla') + }) + it('selectRecordSaving() should return the current "saving" state', () => { const result = EditorSelectors.selectRecordSaving(state) expect(result).toBe(false) @@ -39,43 +45,79 @@ describe('Editor Selectors', () => { expect(result).toBe(true) }) + it('selectRecordAlreadySavedOnce() should return the current "alreadySavedOnce" state', () => { + const result = EditorSelectors.selectRecordAlreadySavedOnce(state) + expect(result).toBe(false) + }) + it('selectRecordFieldsConfig() should return the current "fieldsConfig" state', () => { const result = EditorSelectors.selectRecordFieldsConfig(state) expect(result).toEqual(DEFAULT_FIELDS) }) - it('selectRecordFields() should return the config and value for each field', () => { - const result = EditorSelectors.selectRecordFields(state) - expect(result).toEqual([ - { + describe('selectRecordFields', () => { + it('should return the config and value for each field', () => { + const result = EditorSelectors.selectRecordFields(state) + expect(result).toEqual([ + { + config: DEFAULT_FIELDS[0], + value: DATASET_RECORDS[0].title, + }, + { + config: DEFAULT_FIELDS[1], + value: DATASET_RECORDS[0].abstract, + }, + { + config: DEFAULT_FIELDS[2], + value: DATASET_RECORDS[0].uniqueIdentifier, + }, + { + config: DEFAULT_FIELDS[3], + value: DATASET_RECORDS[0].recordUpdated, + }, + { + config: DEFAULT_FIELDS[4], + value: DATASET_RECORDS[0].licenses, + }, + { + config: DEFAULT_FIELDS[5], + value: DATASET_RECORDS[0].resourceUpdated, + }, + { + config: DEFAULT_FIELDS[6], + value: DATASET_RECORDS[0].updateFrequency, + }, + { + config: DEFAULT_FIELDS[7], + value: DATASET_RECORDS[0].temporalExtents, + }, + { + config: DEFAULT_FIELDS[8], + value: DATASET_RECORDS[0].keywords, + }, + ]) + }) + it('should not coerce falsy values to null', () => { + const result = EditorSelectors.selectRecordFields({ + ...state, + editor: { + ...state.editor, + record: { + ...DATASET_RECORDS[0], + abstract: '', + title: '', + }, + }, + }) + expect(result).toContainEqual({ config: DEFAULT_FIELDS[0], - value: DATASET_RECORDS[0].title, - }, - { + value: '', + }) + expect(result).toContainEqual({ config: DEFAULT_FIELDS[1], - value: DATASET_RECORDS[0].abstract, - }, - { - config: DEFAULT_FIELDS[2], - value: DATASET_RECORDS[0].uniqueIdentifier, - }, - { - config: DEFAULT_FIELDS[3], - value: DATASET_RECORDS[0].recordUpdated, - }, - { - config: DEFAULT_FIELDS[4], - value: DATASET_RECORDS[0].licenses, - }, - { - config: DEFAULT_FIELDS[5], - value: DATASET_RECORDS[0].resourceUpdated, - }, - { - config: DEFAULT_FIELDS[6], - value: DATASET_RECORDS[0].updateFrequency, - }, - ]) + value: '', + }) + }) }) }) }) diff --git a/libs/feature/editor/src/lib/+state/editor.selectors.ts b/libs/feature/editor/src/lib/+state/editor.selectors.ts index 2493207f4b..e9a601da80 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.ts @@ -9,6 +9,11 @@ export const selectRecord = createSelector( (state: EditorState) => state.record ) +export const selectRecordSource = createSelector( + selectEditorState, + (state: EditorState) => state.recordSource +) + export const selectRecordSaving = createSelector( selectEditorState, (state: EditorState) => state.saving @@ -24,6 +29,11 @@ export const selectRecordChangedSinceSave = createSelector( (state: EditorState) => state.changedSinceSave ) +export const selectRecordAlreadySavedOnce = createSelector( + selectEditorState, + (state: EditorState) => state.alreadySavedOnce +) + export const selectRecordFieldsConfig = createSelector( selectEditorState, (state: EditorState) => state.fieldsConfig @@ -34,6 +44,6 @@ export const selectRecordFields = createSelector( (state: EditorState) => state.fieldsConfig.map((fieldConfig) => ({ config: fieldConfig, - value: state.record?.[fieldConfig.model] || null, + value: state.record?.[fieldConfig.model] ?? null, })) ) diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.css similarity index 100% rename from libs/ui/elements/src/lib/max-lines/max-lines.component.css rename to libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.css diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.html new file mode 100644 index 0000000000..ce21d8e1b7 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.html @@ -0,0 +1,19 @@ +
+ +
+ {{ keyword.label }} +
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.spec.ts new file mode 100644 index 0000000000..a704b8549c --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { FormFieldKeywordsComponent } from './form-field-keywords.component' +import { + DropdownSelectorComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' +import { CommonModule } from '@angular/common' +import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { of } from 'rxjs' +import { FormControl } from '@angular/forms' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' + +class PlatformServiceInterfaceMock { + searchKeywords = jest.fn(() => + of([{ label: 'Address', thesaurus: { id: '1' } }]) + ) +} +describe('FormFieldKeywordsComponent', () => { + let component: FormFieldKeywordsComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + FormFieldKeywordsComponent, + DropdownSelectorComponent, + UiInputsModule, + CommonModule, + UiWidgetsModule, + ], + providers: [ + { + provide: PlatformServiceInterface, + useClass: PlatformServiceInterfaceMock, + }, + ], + }) + fixture = TestBed.createComponent(FormFieldKeywordsComponent) + component = fixture.componentInstance + component.control = new FormControl() + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.ts new file mode 100644 index 0000000000..b83745abba --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.ts @@ -0,0 +1,77 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { FormControl } from '@angular/forms' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { + AutocompleteComponent, + DropdownSelectorComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' +import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { map } from 'rxjs' +import { Keyword } from '@geonetwork-ui/common/domain/model/record' + +type AutocompleteItem = { title: string; value: Keyword } + +@Component({ + selector: 'gn-ui-form-field-keywords', + templateUrl: './form-field-keywords.component.html', + styleUrls: ['./form-field-keywords.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + DropdownSelectorComponent, + UiInputsModule, + CommonModule, + UiWidgetsModule, + AutocompleteComponent, + ], +}) +export class FormFieldKeywordsComponent { + @Input() control: FormControl + + displayWithFn = (item: AutocompleteItem) => { + return `${item.title} (${item.value.thesaurus?.name})` + } + + autoCompleteAction = (query: string) => { + return this.platformService.searchKeywords(query).pipe( + map((keywords) => + keywords.map((keyword) => { + return { title: keyword.label, value: keyword } + }) + ) + ) + } + + constructor(private platformService: PlatformServiceInterface) {} + + handleItemSelection(item: AutocompleteItem) { + this.addKeyword(item.value) + } + + addKeyword(keyword: Keyword) { + const addedKeywords = [...this.control.value, keyword] + + // remove duplicates from keyword + const filteredKeywords = addedKeywords.filter((value, index, self) => { + return ( + index === + self.findIndex( + (t) => + t?.label === value?.label && + t?.thesaurus?.id === value?.thesaurus?.id && + t?.type === value?.type + ) + ) + }) + + this.control.setValue(filteredKeywords) + } + + removeKeyword(index: number) { + const removeKeywords = this.control.value.filter((_, i) => i !== index) + + this.control.setValue(removeKeywords) + } +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.html deleted file mode 100644 index 0697777070..0000000000 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.html +++ /dev/null @@ -1 +0,0 @@ -

form-field-temporal-extent works!

diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.spec.ts deleted file mode 100644 index 6cdafe566d..0000000000 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' - -import { FormFieldTemporalExtentComponent } from './form-field-temporal-extent.component' - -describe('FormFieldTemporalExtentComponent', () => { - let component: FormFieldTemporalExtentComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FormFieldTemporalExtentComponent], - }).compileComponents() - - fixture = TestBed.createComponent(FormFieldTemporalExtentComponent) - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) -}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.ts deleted file mode 100644 index 21b9e6f05a..0000000000 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extent/form-field-temporal-extent.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' - -@Component({ - selector: 'gn-ui-form-field-temporal-extent', - templateUrl: './form-field-temporal-extent.component.html', - styleUrls: ['./form-field-temporal-extent.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, -}) -export class FormFieldTemporalExtentComponent {} diff --git a/libs/ui/widgets/src/lib/badge/badge.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.css similarity index 100% rename from libs/ui/widgets/src/lib/badge/badge.component.css rename to libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.css diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.html new file mode 100644 index 0000000000..46da1c5fa1 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.html @@ -0,0 +1,7 @@ +
+

editor.record.form.temporalExtents.date

+ +
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.spec.ts new file mode 100644 index 0000000000..9a49410c30 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { TranslateModule } from '@ngx-translate/core' +import { FormFieldTemporalExtentsDateComponent } from './form-field-temporal-extents-date.component' +import { FormControl } from '@angular/forms' + +describe('FormFieldTemporalExtentsDateComponent', () => { + let component: FormFieldTemporalExtentsDateComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + FormFieldTemporalExtentsDateComponent, + TranslateModule.forRoot(), + ], + }).compileComponents() + + fixture = TestBed.createComponent(FormFieldTemporalExtentsDateComponent) + component = fixture.componentInstance + const control = new FormControl() + control.setValue([ + { + start: new Date('2024-05-30'), + }, + ]) + component.control = control + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.ts new file mode 100644 index 0000000000..bcce4e1967 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-date/form-field-temporal-extents-date.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { FormControl } from '@angular/forms' +import { DatePickerComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-form-field-temporal-extents-date', + templateUrl: './form-field-temporal-extents-date.component.html', + styleUrls: ['./form-field-temporal-extents-date.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [DatePickerComponent, TranslateModule], +}) +export class FormFieldTemporalExtentsDateComponent { + @Input() control!: FormControl +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.html new file mode 100644 index 0000000000..32e804efbe --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.html @@ -0,0 +1,13 @@ +
+

editor.record.form.temporalExtents.range

+ +
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.spec.ts new file mode 100644 index 0000000000..f4ae34031b --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { TranslateModule } from '@ngx-translate/core' +import { FormFieldTemporalExtentsRangeComponent } from './form-field-temporal-extents-range.component' +import { FormControl } from '@angular/forms' + +describe('FormFieldTemporalExtentsRangeComponent', () => { + let component: FormFieldTemporalExtentsRangeComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + FormFieldTemporalExtentsRangeComponent, + TranslateModule.forRoot(), + ], + }).compileComponents() + + fixture = TestBed.createComponent(FormFieldTemporalExtentsRangeComponent) + component = fixture.componentInstance + const control = new FormControl() + control.setValue([ + { + start: new Date('2024-05-24'), + end: null, + }, + ]) + component.control = control + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.ts new file mode 100644 index 0000000000..f5a9a67268 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents-range/form-field-temporal-extents-range.component.ts @@ -0,0 +1,16 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { FormControl } from '@angular/forms' +import { DateRangePickerComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-form-field-temporal-extents-range', + templateUrl: './form-field-temporal-extents-range.component.html', + styleUrls: ['./form-field-temporal-extents-range.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [DateRangePickerComponent, TranslateModule], +}) +export class FormFieldTemporalExtentsRangeComponent { + @Input() control!: FormControl +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.html new file mode 100644 index 0000000000..787d2e3696 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.html @@ -0,0 +1,6 @@ + diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.spec.ts new file mode 100644 index 0000000000..3952e6d940 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { TranslateModule } from '@ngx-translate/core' +import { FormFieldTemporalExtentsComponent } from './form-field-temporal-extents.component' +import { FormControl } from '@angular/forms' + +describe('FormFieldTemporalExtentsComponent', () => { + let component: FormFieldTemporalExtentsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormFieldTemporalExtentsComponent, TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(FormFieldTemporalExtentsComponent) + component = fixture.componentInstance + const control = new FormControl() + control.setValue([ + { + start: new Date('2024-05-24'), + end: null, + }, + { + start: new Date('2024-05-30'), + }, + ]) + component.control = control + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.ts new file mode 100644 index 0000000000..b7e5398646 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.ts @@ -0,0 +1,130 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnDestroy, + OnInit, + Type, +} from '@angular/core' +import { FormArray, FormControl } from '@angular/forms' +import { SortableListComponent } from '@geonetwork-ui/ui/elements' +import { Subscription, combineLatest, map } from 'rxjs' +import { FormFieldTemporalExtentsDateComponent } from './form-field-temporal-extents-date/form-field-temporal-extents-date.component' +import { FormFieldTemporalExtentsRangeComponent } from './form-field-temporal-extents-range/form-field-temporal-extents-range.component' +import { TranslateService } from '@ngx-translate/core' +import { CommonModule } from '@angular/common' + +@Component({ + selector: 'gn-ui-form-field-temporal-extents', + templateUrl: './form-field-temporal-extents.component.html', + styleUrls: ['./form-field-temporal-extents.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, SortableListComponent], +}) +export class FormFieldTemporalExtentsComponent implements OnInit, OnDestroy { + @Input() control!: FormControl + + subscription: Subscription + + array: FormArray = new FormArray([]) + elements: Array<{ + component: Type + inputs: Record + }> + + addOptions$ = combineLatest([ + this.translateService + .get('editor.record.form.temporalExtents.addDate') + .pipe(map((buttonLabel) => ({ buttonLabel, eventName: 'date' }))), + this.translateService + .get('editor.record.form.temporalExtents.addRange') + .pipe(map((buttonLabel) => ({ buttonLabel, eventName: 'range' }))), + ]) + + constructor(private translateService: TranslateService) {} + + ngOnInit() { + this.resetValueFromInput(this.control.value) + + this.subscription = new Subscription() + + this.subscription.add( + this.control.valueChanges.subscribe((value) => { + this.resetValueFromInput(value) + }) + ) + + this.subscription.add( + this.array.valueChanges.subscribe((value) => { + this.control.setValue(value) + }) + ) + } + + onElementsChange(elements: any) { + this.array.clear({ emitEvent: false }) + elements.forEach((e: any, i: number) => + this.array.push(e.inputs.control, { + emitEvent: i === elements.length - 1, + }) + ) + } + + onAdd(eventName: string) { + switch (eventName) { + case 'date': { + const dateControl = new FormControl({ start: new Date() }) + this.array.push(dateControl) + break + } + case 'range': { + const rangeControl = new FormControl({ + start: new Date(), + end: new Date(), + }) + this.array.push(rangeControl) + break + } + } + } + + ngOnDestroy(): void { + this.subscription.unsubscribe() + } + + private resetValueFromInput(value) { + this.array.clear({ emitEvent: false }) + this.elements = [] + value.forEach((v: any) => { + if ('start' in v && 'end' in v) { + const rangeControl = new FormControl({ + start: v.start, + end: v.end, + }) + this.array.push(rangeControl, { emitEvent: false }) + this.elements = [ + ...this.elements, + { + component: FormFieldTemporalExtentsRangeComponent, + inputs: { + control: rangeControl, + }, + }, + ] + } else { + const dateControl = new FormControl({ start: v.start }) + this.array.push(dateControl, { emitEvent: false }) + this.elements = [ + ...this.elements, + { + component: FormFieldTemporalExtentsDateComponent, + inputs: { + control: dateControl, + }, + }, + ] + } + }) + } +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.spec.ts index b862ada4b4..34c77f3afc 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.spec.ts @@ -22,12 +22,21 @@ describe('FormFieldUpdateFrequencyComponent', () => { }) component.control = control fixture.detectChanges() + await component.ngOnInit() }) it('should create', () => { expect(component).toBeTruthy() }) + it('should offer a set of initial choices', () => { + expect(component['choices']).toHaveLength(10) + expect(component['choices']).toContainEqual({ + label: 'domain.record.updateFrequency.week', + value: 'week.3', + }) + }) + it('should parse the updatedTimes and per values', () => { component.onSelectFrequencyValue('day.1') expect(component.control.value).toEqual({ @@ -41,7 +50,7 @@ describe('FormFieldUpdateFrequencyComponent', () => { }) it('should add the custom frequency to the dropdown choices', () => { - expect(component.choices).toContainEqual({ + expect(component['choices']).toContainEqual({ value: 'week.3', label: 'domain.record.updateFrequency.week', }) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts index 81e045279f..5a4ead4855 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts @@ -5,12 +5,17 @@ import { OnInit, } from '@angular/core' import { FormControl } from '@angular/forms' -import { marker } from '@biesbjerg/ngx-translate-extract-marker' import { CheckToggleComponent, + DropdownChoice, DropdownSelectorComponent, } from '@geonetwork-ui/ui/inputs' import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { + UpdateFrequency, + UpdateFrequencyCustom, +} from '@geonetwork-ui/common/domain/model/record' +import { firstValueFrom } from 'rxjs' @Component({ selector: 'gn-ui-form-field-update-frequency', @@ -21,26 +26,32 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core' imports: [CheckToggleComponent, DropdownSelectorComponent, TranslateModule], }) export class FormFieldUpdateFrequencyComponent implements OnInit { - @Input() control: FormControl + @Input() control: FormControl + + protected choices: DropdownChoice[] = [] get planned() { - return this.control.value !== 'notPlanned' + return typeof this.control.value !== 'string' } constructor(private translateService: TranslateService) {} - ngOnInit() { - const updatedTimes = this.control.value?.updatedTimes - const per = this.control.value?.per + async ngOnInit() { + this.choices = await this.getInitialChoices() + if (typeof this.control.value === 'string') { + return + } + const updatedTimes = this.control.value.updatedTimes + const per = this.control.value.per + // the update frequency is not in the list; make it appear there if (updatedTimes && updatedTimes !== 1 && updatedTimes !== 2) { this.choices = [ { value: `${per}.${updatedTimes}`, - label: this.translateService.instant( - `domain.record.updateFrequency.${per}`, - { + label: await firstValueFrom( + this.translateService.get(`domain.record.updateFrequency.${per}`, { count: updatedTimes, - } + }) ), }, ...this.choices, @@ -56,88 +67,86 @@ export class FormFieldUpdateFrequencyComponent implements OnInit { } } - get selectedFrequency() { + get selectedFrequency(): string { + if (typeof this.control.value === 'string') return null const { updatedTimes, per } = this.control.value return `${per}.${updatedTimes}` } onSelectFrequencyValue(value: unknown) { const split = (value as string).split('.') - this.control.setValue({ updatedTimes: Number(split[1]), per: split[0] }) + this.control.setValue({ + updatedTimes: Number(split[1]), + per: split[0] as UpdateFrequencyCustom['per'], + }) } - choices = [ - { - value: 'day.1', - label: this.translateService.instant( - 'domain.record.updateFrequency.day', - { - count: 1, - } - ), - }, - { - value: 'day.2', - label: this.translateService.instant( - 'domain.record.updateFrequency.day', - { - count: 2, - } - ), - }, - { - value: 'week.1', - label: this.translateService.instant( - 'domain.record.updateFrequency.week', - { - count: 1, - } - ), - }, - { - value: 'week.2', - label: this.translateService.instant( - 'domain.record.updateFrequency.week', - { - count: 2, - } - ), - }, - { - value: 'month.1', - label: this.translateService.instant( - 'domain.record.updateFrequency.month', - { - count: 1, - } - ), - }, - { - value: 'month.2', - label: this.translateService.instant( - 'domain.record.updateFrequency.month', - { - count: 2, - } - ), - }, - { - value: 'year.1', - label: this.translateService.instant( - 'domain.record.updateFrequency.year', - { - count: 1, - } - ), - }, - { - value: 'year.2', - label: this.translateService.instant( - 'domain.record.updateFrequency.year', - { - count: 2, - } - ), - }, - ] + private async getInitialChoices() { + return [ + { + value: 'day.1', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.day', { + count: 1, + }) + ), + }, + { + value: 'day.2', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.day', { + count: 2, + }) + ), + }, + { + value: 'week.1', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.week', { + count: 1, + }) + ), + }, + { + value: 'week.2', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.week', { + count: 2, + }) + ), + }, + { + value: 'month.1', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.month', { + count: 1, + }) + ), + }, + { + value: 'month.2', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.month', { + count: 2, + }) + ), + }, + { + value: 'year.1', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.year', { + count: 1, + }) + ), + }, + { + value: 'year.2', + label: await firstValueFrom( + this.translateService.get('domain.record.updateFrequency.year', { + count: 2, + }) + ), + }, + ] + } } diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html index 861a3e0bbc..98f8503d27 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html @@ -61,37 +61,24 @@ [control]="formControl" > + + + - - - - - - - - - - - + + -
- {{ config.invalidHintKey | translate }} -
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.spec.ts index 8aeb70c9ed..aa81387dba 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.spec.ts @@ -2,17 +2,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { FormFieldWrapperComponent } from '@geonetwork-ui/ui/layout' import { TranslateModule } from '@ngx-translate/core' -import { FormFieldArrayComponent } from './form-field-array/form-field-array.component' -import { FormFieldFileComponent } from './form-field-file/form-field-file.component' import { FormFieldLicenseComponent } from './form-field-license/form-field-license.component' -import { FormFieldObjectComponent } from './form-field-object/form-field-object.component' import { FormFieldResourceUpdatedComponent } from './form-field-resource-updated/form-field-resource-updated.component' import { FormFieldRichComponent } from './form-field-rich/form-field-rich.component' import { FormFieldSimpleComponent } from './form-field-simple/form-field-simple.component' import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/form-field-spatial-extent.component' -import { FormFieldTemporalExtentComponent } from './form-field-temporal-extent/form-field-temporal-extent.component' import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component' import { FormFieldComponent } from './form-field.component' +import { FormFieldTemporalExtentsComponent } from './form-field-temporal-extents/form-field-temporal-extents.component' describe('FormFieldComponent', () => { let component: FormFieldComponent @@ -95,91 +92,55 @@ describe('FormFieldComponent', () => { expect(formField).toBeTruthy() }) }) - describe('simple field', () => { - let fieldWrapper + describe('temporal extents field', () => { let formField - beforeEach(async () => { - component.config.type = 'url' + beforeEach(() => { + component.model = 'temporalExtents' + component.value = [ + { + start: new Date('2024-05-24'), + end: null, + }, + { + start: new Date('2024-05-30'), + }, + ] fixture.detectChanges() - await fixture.whenStable() - fieldWrapper = fixture.debugElement.query( - By.directive(FormFieldWrapperComponent) - ).componentInstance formField = fixture.debugElement.query( - By.directive(FormFieldSimpleComponent) + By.directive(FormFieldTemporalExtentsComponent) ).componentInstance }) - it('creates a simple form field', () => { + it('creates a temporal extents form field', () => { expect(formField).toBeTruthy() - expect(formField.type).toEqual(component.config.type) - expect(formField.readonly).toEqual(component.config.locked) - expect(formField.invalid).toEqual(component.config.invalid) - }) - it('creates a form field wrapper', () => { - expect(fieldWrapper).toBeTruthy() }) }) - describe('simple field (invalid)', () => { + describe('simple field', () => { + let fieldWrapper let formField beforeEach(async () => { - component.config.type = 'number' - component.config.invalid = true - component.config.invalidHintKey = 'something.is.wrong' + component.model = 'uniqueIdentifier' fixture.detectChanges() await fixture.whenStable() - formField = fixture.debugElement.query( - By.directive(FormFieldSimpleComponent) + fieldWrapper = fixture.debugElement.query( + By.directive(FormFieldWrapperComponent) ).componentInstance - }) - it('shows a simple form field as invalid', () => { - expect(formField).toBeTruthy() - expect(formField.type).toEqual(component.config.type) - expect(formField.invalid).toEqual(true) - }) - it('shows the invalid hint key', () => { - const hint = fixture.debugElement.query(By.css('.field-invalid-hint')) - expect(hint.nativeElement.textContent).toContain( - component.config.invalidHintKey - ) - }) - }) - describe('simple field (invalid and locked)', () => { - let formField - beforeEach(async () => { - component.config.type = 'number' - component.config.locked = true - component.config.invalid = true - fixture.detectChanges() - await fixture.whenStable() formField = fixture.debugElement.query( By.directive(FormFieldSimpleComponent) ).componentInstance }) - it('shows a simple form field as locked (but not invalid)', () => { + it('creates a simple field field (unique identifier)', () => { expect(formField).toBeTruthy() - expect(formField.type).toEqual(component.config.type) + expect(formField.type).toEqual('text') expect(formField.readonly).toEqual(true) - expect(formField.invalid).toEqual(false) - }) - }) - describe('file field', () => { - let formField - beforeEach(() => { - component.config.type = 'file' - fixture.detectChanges() - formField = fixture.debugElement.query( - By.directive(FormFieldFileComponent) - ).componentInstance }) - it('creates a file form field', () => { - expect(formField).toBeTruthy() - expect(formField.readonly).toEqual(component.config.locked) + it('creates a form field wrapper', () => { + expect(fieldWrapper).toBeTruthy() }) }) describe('spatial extent field', () => { let formField beforeEach(() => { - component.config.type = 'spatial_extent' + component.model = 'spatialExtents' fixture.detectChanges() formField = fixture.debugElement.query( By.directive(FormFieldSpatialExtentComponent) @@ -189,43 +150,4 @@ describe('FormFieldComponent', () => { expect(formField).toBeTruthy() }) }) - describe('temporal extent field', () => { - let formField - beforeEach(() => { - component.config.type = 'temporal_extent' - fixture.detectChanges() - formField = fixture.debugElement.query( - By.directive(FormFieldTemporalExtentComponent) - ).componentInstance - }) - it('creates an array form field', () => { - expect(formField).toBeTruthy() - }) - }) - describe('array field', () => { - let formField - beforeEach(() => { - component.config.type = 'array' - fixture.detectChanges() - formField = fixture.debugElement.query( - By.directive(FormFieldArrayComponent) - ).componentInstance - }) - it('creates an array form field', () => { - expect(formField).toBeTruthy() - }) - }) - describe('object field', () => { - let formField - beforeEach(() => { - component.config.type = 'object' - fixture.detectChanges() - formField = fixture.debugElement.query( - By.directive(FormFieldObjectComponent) - ).componentInstance - }) - it('creates an object form field', () => { - expect(formField).toBeTruthy() - }) - }) }) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts index c60ff1a41b..5077db478e 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts @@ -14,17 +14,21 @@ import { EditableLabelDirective } from '@geonetwork-ui/ui/inputs' import { FormFieldWrapperComponent } from '@geonetwork-ui/ui/layout' import { TranslateModule } from '@ngx-translate/core' import { Observable } from 'rxjs' +import { + FormFieldLicenseComponent, + FormFieldResourceUpdatedComponent, + FormFieldTemporalExtentsComponent, +} from '.' import { FormFieldArrayComponent } from './form-field-array/form-field-array.component' import { FormFieldFileComponent } from './form-field-file/form-field-file.component' -import { FormFieldLicenseComponent } from './form-field-license/form-field-license.component' import { FormFieldObjectComponent } from './form-field-object/form-field-object.component' -import { FormFieldResourceUpdatedComponent } from './form-field-resource-updated/form-field-resource-updated.component' import { FormFieldRichComponent } from './form-field-rich/form-field-rich.component' import { FormFieldSimpleComponent } from './form-field-simple/form-field-simple.component' import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/form-field-spatial-extent.component' -import { FormFieldTemporalExtentComponent } from './form-field-temporal-extent/form-field-temporal-extent.component' import { FormFieldConfig } from './form-field.model' import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component' +import { CatalogRecordKeys } from '@geonetwork-ui/common/domain/model/record' +import { FormFieldKeywordsComponent } from './form-field-keywords/form-field-keywords.component' @Component({ selector: 'gn-ui-form-field', @@ -42,18 +46,19 @@ import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency FormFieldLicenseComponent, FormFieldResourceUpdatedComponent, FormFieldUpdateFrequencyComponent, + FormFieldTemporalExtentsComponent, FormFieldSimpleComponent, FormFieldRichComponent, FormFieldObjectComponent, FormFieldSpatialExtentComponent, - FormFieldTemporalExtentComponent, FormFieldFileComponent, FormFieldArrayComponent, + FormFieldKeywordsComponent, TranslateModule, ], }) export class FormFieldComponent { - @Input() model: string + @Input() model: CatalogRecordKeys @Input() config: FormFieldConfig @Input() set value(v: unknown) { this.formControl.setValue(v, { @@ -74,52 +79,6 @@ export class FormFieldComponent { this.titleInput.nativeElement.children[0].focus() } - get simpleType() { - return this.config.type as - | 'date' - | 'url' - | 'text' - | 'number' - | 'list' - | 'toggle' - } - - get isSimpleField() { - return ( - this.config.type === 'text' || - this.config.type === 'number' || - this.config.type === 'date' || - this.config.type === 'list' || - this.config.type === 'url' || - this.config.type === 'toggle' - ) - } - get isFileField() { - return this.config.type === 'file' - } - get isSpatialExtentField() { - return this.config.type === 'spatial_extent' - } - get isTemporalExtentField() { - return this.config.type === 'temporal_extent' - } - get isArrayField() { - return this.config.type === 'array' - } - get isObjectField() { - return this.config.type === 'object' - } - - get isFieldOk() { - return !this.config.locked && !this.config.invalid - } - get isFieldLocked() { - return this.config.locked - } - get isFieldInvalid() { - return !this.config.locked && this.config.invalid - } - get isTitle() { return this.model === 'title' } @@ -135,6 +94,21 @@ export class FormFieldComponent { get isUpdateFrequency() { return this.model === 'updateFrequency' } + get isTemporalExtents() { + return this.model === 'temporalExtents' + } + get isSpatialExtentField() { + return this.model === 'spatialExtents' + } + get isSimpleField() { + return this.model === 'uniqueIdentifier' || this.model === 'recordUpdated' + } + get isReadOnly() { + return this.model === 'uniqueIdentifier' || this.model === 'recordUpdated' + } + get isKeywords() { + return this.model === 'keywords' + } get withoutWrapper() { return this.model === 'title' || this.model === 'abstract' diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/index.ts b/libs/feature/editor/src/lib/components/record-form/form-field/index.ts index 7713fbf000..053dede8e5 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/index.ts @@ -1,9 +1,12 @@ +export * from './form-field-keywords/form-field-keywords.component' +export * from './form-field-license/form-field-license.component' +export * from './form-field-resource-updated/form-field-resource-updated.component' +export * from './form-field-temporal-extents/form-field-temporal-extents.component' export * from './form-field-simple/form-field-simple.component' export * from './form-field-file/form-field-file.component' export * from './form-field-rich/form-field-rich.component' export * from './form-field-object/form-field-object.component' export * from './form-field-array/form-field-array.component' export * from './form-field-spatial-extent/form-field-spatial-extent.component' -export * from './form-field-temporal-extent/form-field-temporal-extent.component' export * from './form-field.component' export * from './form-field.model' diff --git a/libs/feature/editor/src/lib/feature-editor.module.ts b/libs/feature/editor/src/lib/feature-editor.module.ts index ce78897685..b71b629033 100644 --- a/libs/feature/editor/src/lib/feature-editor.module.ts +++ b/libs/feature/editor/src/lib/feature-editor.module.ts @@ -15,6 +15,7 @@ import * as fromEditor from './+state/editor.reducer' import { WizardFieldComponent } from './components/wizard-field/wizard-field.component' import { WizardSummarizeComponent } from './components/wizard-summarize/wizard-summarize.component' import { WizardComponent } from './components/wizard/wizard.component' +import { Gn4PlatformService } from '@geonetwork-ui/api/repository' @NgModule({ declarations: [ @@ -39,7 +40,7 @@ import { WizardComponent } from './components/wizard/wizard.component' EffectsModule.forFeature([EditorEffects]), ], exports: [WizardComponent, WizardSummarizeComponent], - providers: [EditorFacade], + providers: [EditorFacade, Gn4PlatformService], }) export class FeatureEditorModule {} export * from './models/index' diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index 8c83feb68a..cda10b5196 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -54,4 +54,18 @@ export const DEFAULT_FIELDS: EditorFieldsConfig = [ type: 'text', }, }, + { + model: 'temporalExtents', + formFieldConfig: { + labelKey: marker('editor.record.form.temporalExtents'), + type: 'list', + }, + }, + { + model: 'keywords', + formFieldConfig: { + labelKey: marker('editor.record.form.keywords'), + type: 'list', + }, + }, ] diff --git a/libs/feature/editor/src/lib/services/editor.service.spec.ts b/libs/feature/editor/src/lib/services/editor.service.spec.ts index 7ac6c6c2cc..c58845c36b 100644 --- a/libs/feature/editor/src/lib/services/editor.service.spec.ts +++ b/libs/feature/editor/src/lib/services/editor.service.spec.ts @@ -7,20 +7,42 @@ import { import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { DEFAULT_FIELDS } from '../fields.config' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { firstValueFrom, of } from 'rxjs' const SAMPLE_RECORD: CatalogRecord = DATASET_RECORDS[0] +class RecordsRepositoryMock { + openRecordForEdition = jest.fn(() => + of([ + { ...SAMPLE_RECORD, recordUpdated: new Date() }, + 'blabla', + false, + ]) + ) + saveRecord = jest.fn(() => of('blabla')) + saveRecordAsDraft = jest.fn(() => of('blabla')) + clearRecordDraft = jest.fn() +} + describe('EditorService', () => { let service: EditorService let http: HttpTestingController + let repository: RecordsRepositoryInterface beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [], + providers: [ + { + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, + }, + ], }) service = TestBed.inject(EditorService) http = TestBed.inject(HttpTestingController) + repository = TestBed.inject(RecordsRepositoryInterface) }) afterEach(() => { @@ -31,49 +53,49 @@ describe('EditorService', () => { expect(service).toBeTruthy() }) - describe('loadRecordByUuid', () => { - let record: CatalogRecord - beforeEach(() => { - service.loadRecordByUuid('1234-5678').subscribe((v) => (record = v)) - http.expectOne( - (req) => req.url.indexOf('/records/1234-5678/formatters/xml') > -1 - ).flush(` - - - 1234-5678 - -`) + describe('saveRecord', () => { + let savedRecord: [CatalogRecord, string] + beforeEach(async () => { + savedRecord = await firstValueFrom( + service.saveRecord(SAMPLE_RECORD, DEFAULT_FIELDS) + ) }) - it('parses the XML record into a native object', () => { - expect(record).toMatchObject({ uniqueIdentifier: '1234-5678' }) + it('calls repository.saveRecord and repository.clearRecordDraft', () => { + const expected = { + ...SAMPLE_RECORD, + recordUpdated: expect.any(Date), + } + expect(repository.saveRecord).toHaveBeenCalledWith(expected) + expect(repository.clearRecordDraft).toHaveBeenCalledWith( + SAMPLE_RECORD.uniqueIdentifier + ) + expect(savedRecord).toEqual([expected, 'blabla']) }) - }) - - describe('saveRecord', () => { - describe('after a record was set as current', () => { - let savedRecord: CatalogRecord + it('applies field processes (update date in record)', () => { + const arg = (repository.saveRecord as jest.Mock).mock.calls[0][0] + expect(arg.recordUpdated).not.toEqual(SAMPLE_RECORD.recordUpdated) + }) + describe('if a new one has to be generated', () => { beforeEach(() => { - service - .saveRecord(SAMPLE_RECORD, DEFAULT_FIELDS) - .subscribe((v) => (savedRecord = v)) + service.saveRecord(SAMPLE_RECORD, DEFAULT_FIELDS, true).subscribe() }) - it('sends a record as XML to the API after applying field processes', () => { - const match = http.expectOne( - (req) => req.method === 'PUT' && req.url.indexOf('/records') > -1 - ) - match.flush('ok') - expect(match.request.body).toContain(` - - ${SAMPLE_RECORD.uniqueIdentifier} - `) - expect(savedRecord).toEqual({ + it('clears the unique identifier of the record', () => { + const expected = { ...SAMPLE_RECORD, recordUpdated: expect.any(Date), - }) - expect(savedRecord.recordUpdated).not.toEqual( - SAMPLE_RECORD.recordUpdated - ) + uniqueIdentifier: null, + } + expect(repository.saveRecord).toHaveBeenCalledWith(expected) }) }) }) + + describe('saveRecordAsDraft', () => { + beforeEach(() => { + service.saveRecordAsDraft(SAMPLE_RECORD).subscribe() + }) + it('calls saveRecordAsDraft', () => { + expect(repository.saveRecordAsDraft).toHaveBeenCalledWith(SAMPLE_RECORD) + }) + }) }) diff --git a/libs/feature/editor/src/lib/services/editor.service.ts b/libs/feature/editor/src/lib/services/editor.service.ts index f3f57481c5..9b7c005438 100644 --- a/libs/feature/editor/src/lib/services/editor.service.ts +++ b/libs/feature/editor/src/lib/services/editor.service.ts @@ -1,50 +1,23 @@ -import { Inject, Injectable, Optional } from '@angular/core' -import { - findConverterForDocument, - Iso19139Converter, -} from '@geonetwork-ui/api/metadata-converter' -import { Configuration } from '@geonetwork-ui/data-access/gn4' -import { from, Observable } from 'rxjs' -import { map, switchMap } from 'rxjs/operators' -import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { Observable, switchMap } from 'rxjs' +import { map, tap } from 'rxjs/operators' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { EditorFieldsConfig } from '../models/fields.model' import { evaluate } from '../expressions' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @Injectable({ providedIn: 'root', }) export class EditorService { - private apiUrl = `${this.apiConfiguration?.basePath || '/geonetwork/srv/api'}` + constructor(private recordsRepository: RecordsRepositoryInterface) {} - constructor( - private http: HttpClient, - @Optional() - @Inject(Configuration) - private apiConfiguration: Configuration - ) {} - - // TODO: use the catalog repository instead - loadRecordByUuid(uuid: string): Observable { - return this.http - .get(`${this.apiUrl}/records/${uuid}/formatters/xml`, { - responseType: 'text', - headers: { - Accept: 'application/xml', - }, - }) - .pipe( - switchMap((response) => - findConverterForDocument(response).readRecord(response.toString()) - ) - ) - } - - // returns the record as it was when saved + // returns the record as it was when saved, alongside its source saveRecord( record: CatalogRecord, - fieldsConfig: EditorFieldsConfig - ): Observable { + fieldsConfig: EditorFieldsConfig, + generateNewUniqueIdentifier = false + ): Observable<[CatalogRecord, string]> { const savedRecord = { ...record } // run onSave processes @@ -58,22 +31,28 @@ export class EditorService { } } - // TODO: use the catalog repository instead - // TODO: use converter based on the format of the record before change - return from(new Iso19139Converter().writeRecord(savedRecord)).pipe( - switchMap((recordXml) => - this.http.put( - `${this.apiUrl}/records?metadataType=METADATA&uuidProcessing=OVERWRITE&transformWith=_none_&publishToAll=on`, - recordXml, - { - headers: { - 'Content-Type': 'application/xml', - }, - withCredentials: true, - } - ) + // if we want a new unique identifier, clear the existing one + if (generateNewUniqueIdentifier) { + savedRecord.uniqueIdentifier = null + } + + return this.recordsRepository.saveRecord(savedRecord).pipe( + switchMap((uniqueIdentifier) => + this.recordsRepository.openRecordForEdition(uniqueIdentifier) ), - map(() => savedRecord) + tap(() => { + // if saving was successful, the original draft can be discarded + this.recordsRepository.clearRecordDraft(record.uniqueIdentifier) + }), + map(([record, recordSource]) => [record, recordSource]) ) } + + // emits and completes once saving is done + // note: onSave processes are not run for drafts + saveRecordAsDraft(record: CatalogRecord): Observable { + return this.recordsRepository + .saveRecordAsDraft(record) + .pipe(map(() => undefined)) + } } diff --git a/libs/feature/editor/src/lib/services/wizard.service.spec.ts b/libs/feature/editor/src/lib/services/wizard.service.spec.ts index 5d5063df4b..4d636de163 100644 --- a/libs/feature/editor/src/lib/services/wizard.service.spec.ts +++ b/libs/feature/editor/src/lib/services/wizard.service.spec.ts @@ -7,16 +7,6 @@ import { HttpClientTestingModule } from '@angular/common/http/testing' import { NO_ERRORS_SCHEMA } from '@angular/core' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' -const localStorageMock = () => { - let storage = {} - return { - getItem: (key) => (key in storage ? storage[key] : null), - setItem: (key, value) => (storage[key] = value || ''), - removeItem: (key) => delete storage[key], - clear: () => (storage = {}), - } -} - describe('WizardService', () => { let service: WizardService @@ -34,8 +24,6 @@ describe('WizardService', () => { }) beforeEach(() => { - Object.defineProperty(window, 'localStorage', { value: localStorageMock() }) - window.localStorage.setItem( 'datafeeder-state', '{"1":{"step":4,"values":[{"id":"title","value":"title"},{"id":"abstract","value":"dataset"},{"id":"tags","value":"[{\\"display\\":\\"Faeroe Islands\\",\\"value\\":\\"Faeroe Islands\\"}]"},{"id":"dropdown","value":"\\"25000\\""},{"id":"description","value":"description"}]},"10":{"step":4,"values":[{"id":"title","value":"title"},{"id":"abstract","value":"dataset"},{"id":"tags","value":"[{\\"display\\":\\"Davis Sea\\",\\"value\\":\\"Davis Sea\\"}]"},{"id":"dropdown","value":"\\"50000\\""},{"id":"description","value":"desctription"}]}}' diff --git a/libs/feature/map/src/lib/utils/map-utils.service.ts b/libs/feature/map/src/lib/utils/map-utils.service.ts index a99f1ee611..1f0feb2d06 100644 --- a/libs/feature/map/src/lib/utils/map-utils.service.ts +++ b/libs/feature/map/src/lib/utils/map-utils.service.ts @@ -26,7 +26,6 @@ import { MapContextLayerWmsModel, } from '../map-context/map-context.model' import Collection from 'ol/Collection' -import { defaults as defaultControls } from 'ol/control.js' import MapBrowserEvent from 'ol/MapBrowserEvent' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { ProxyService } from '@geonetwork-ui/util/shared' @@ -34,6 +33,7 @@ import { WmsEndpoint, WmtsEndpoint } from '@camptocamp/ogc-client' import { LONLAT_CRS_CODES } from '../constant/projections' import { fromEPSGCode, register } from 'ol/proj/proj4' import proj4 from 'proj4/dist/proj4' +import { defaults as defaultControls } from 'ol/control/defaults' const FEATURE_PROJECTION = 'EPSG:3857' const DATA_PROJECTION = 'EPSG:4326' diff --git a/libs/feature/record/src/lib/feature-record.module.ts b/libs/feature/record/src/lib/feature-record.module.ts index 2321022d6c..be19442097 100644 --- a/libs/feature/record/src/lib/feature-record.module.ts +++ b/libs/feature/record/src/lib/feature-record.module.ts @@ -1,4 +1,4 @@ -import { InjectionToken, NgModule } from '@angular/core' +import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { UiMapModule } from '@geonetwork-ui/ui/map' import { StoreModule } from '@ngrx/store' @@ -19,7 +19,7 @@ import { IgnApiDlComponent } from './ign-api-dl/ign-api-dl.component' import { IgnApiProduitComponent } from './ign-api-produit/ign-api-produit.component' import { MatTabsModule } from '@angular/material/tabs' import { MatIconModule } from '@angular/material/icon' -import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { PopupAlertComponent, UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { TranslateModule } from '@ngx-translate/core' import { ExternalViewerButtonComponent } from './external-viewer-button/external-viewer-button.component' import { FeatureCatalogModule } from '@geonetwork-ui/feature/catalog' @@ -56,6 +56,7 @@ import { DataViewShareComponent } from './data-view-share/data-view-share.compon TranslateModule, TableComponent, FeatureDatavizModule, + PopupAlertComponent, ], providers: [MdViewFacade], exports: [ diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index 15cc2ec364..8fe1da9369 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -76,7 +76,7 @@ export class IgnApiDlComponent implements OnInit { pageSize$ = new BehaviorSubject(this.initialPageSize) page$ = new BehaviorSubject('0') size$ = new BehaviorSubject(this.initialPageSize) - + // a passer en config url = 'https://data.geopf.fr/telechargement/capabilities?outputFormat=application/json' choices: any @@ -125,7 +125,6 @@ export class IgnApiDlComponent implements OnInit { } } outputUrl = url.toString() - console.log(outputUrl) } return outputUrl }) @@ -134,8 +133,7 @@ export class IgnApiDlComponent implements OnInit { listFilteredProduct$ = this.apiQueryUrl$.pipe( mergeMap((url) => { return this.getFilteredProduct$(url).pipe( - map((response) => response['entry']), - tap((el) => console.log(el)) + map((response) => response['entry']) ) }) ) @@ -203,11 +201,34 @@ export class IgnApiDlComponent implements OnInit { this.page$.next('0') } - async getFields() { - const [firstResponse] = await Promise.all([axios.get(this.url)]) - this.choices = firstResponse.data.entry.filter( + async getCapabilities() { + let page = 0 + let choicesTest = null + let [response] = await Promise.all([ + axios.get(this.url.concat(`&pageSize=200&page=${page}`)), + ]) + choicesTest = response.data.entry.filter( (element) => element['id'] == this.apiBaseUrl )[0] + + if (choicesTest) { + return choicesTest + } else { + while (choicesTest === undefined && response.data.pageCount > page) { + ;[response] = await Promise.all([ + axios.get(this.url.concat(`&pageSize=200&page=${page}`)), + ]) + choicesTest = response.data.entry.filter( + (element) => element['id'] == this.apiBaseUrl + )[0] + page += 1 + } + } + return choicesTest + } + async getFields() { + this.choices = await this.getCapabilities() + this.bucketPromisesZone = this.choices.zone.map((bucket) => ({ value: bucket.label, label: bucket.term, @@ -228,5 +249,7 @@ export class IgnApiDlComponent implements OnInit { })) this.bucketPromisesCrs.sort((a, b) => (a.label > b.label ? 1 : -1)) this.bucketPromisesCrs.unshift({ value: 'null', label: 'CRS' }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion } } diff --git a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts index 813939b6a4..ddfcd4763b 100644 --- a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts +++ b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts @@ -25,12 +25,9 @@ export class IgnApiProduitComponent implements OnInit { liste$: Observable ngOnInit(): void { - this.liste$ = this.http.get(this.link['id']).pipe( - map( - (response) => response['entry'] - // tap(el=> console.log(el)), - ) - ) + this.liste$ = this.http + .get(this.link['id']) + .pipe(map((response) => response['entry'])) } downloadListe(): void { @@ -44,7 +41,6 @@ export class IgnApiProduitComponent implements OnInit { } download(url): void { - console.log(url) this.http.get(url).subscribe() } } diff --git a/libs/feature/record/src/lib/map-view/map-view.component.spec.ts b/libs/feature/record/src/lib/map-view/map-view.component.spec.ts index 587b0d73e9..16cd5e363f 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.spec.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.spec.ts @@ -40,6 +40,15 @@ import { Interaction } from 'ol/interaction' import { DataService } from '@geonetwork-ui/feature/dataviz' import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' +const recordMapExtent = [-30, -60, 30, 60] + +const emptyMapContext = { + layers: [], + view: { + extent: recordMapExtent, + }, +} as MapContextModel + const mapConfigMock = { MAX_ZOOM: 10, MAX_EXTENT: [-418263.418776, 5251529.591305, 961272.067714, 6706890.609855], @@ -72,6 +81,7 @@ class MdViewFacadeMock { } class MapUtilsServiceMock { + createEmptyMap = jest.fn() getLayerExtent = jest.fn(function () { return new Promise((resolve, reject) => { this._resolve = resolve @@ -87,7 +97,7 @@ class MapUtilsServiceMock { }) }) prioritizePageScroll = jest.fn() - getRecordExtent = jest.fn(() => [-30, -60, 30, 60]) + getRecordExtent = jest.fn(() => recordMapExtent) _returnImmediately = true _resolve = null _reject = null @@ -559,8 +569,8 @@ describe('MapViewComponent', () => { tick(50) discardPeriodicTasks() })) - it('does not emit immediately a map context', () => { - expect(mapComponent.context).toBe(null) + it('emit an empty map context', () => { + expect(mapComponent.context).toEqual(emptyMapContext) }) it('shows a loading indicator', () => { expect( @@ -677,8 +687,8 @@ describe('MapViewComponent', () => { fixture.detectChanges() })) describe('while extent is not ready', () => { - it('does not emit a map context', () => { - expect(mapComponent.context).toBeFalsy() + it('emit a empty map context', () => { + expect(mapComponent.context).toEqual(emptyMapContext) }) }) describe('when extent is received', () => { @@ -726,7 +736,7 @@ describe('MapViewComponent', () => { }, ], view: { - extent: [-30, -60, 30, 60], + extent: recordMapExtent, }, }) }) @@ -754,7 +764,7 @@ describe('MapViewComponent', () => { type: 'wms', }, ], - view: { extent: [-30, -60, 30, 60] }, + view: { extent: recordMapExtent }, }) }) it('provides selected link to the external viewer component', () => { @@ -768,7 +778,7 @@ describe('MapViewComponent', () => { }) describe('selecting another layer, while extent is not ready', () => { beforeEach(fakeAsync(() => { - mapUtilsService._resolve([-30, -60, 30, 60]) + mapUtilsService._resolve(recordMapExtent) tick() dropdownComponent.selectValue.emit(0) tick() diff --git a/libs/feature/record/src/lib/map-view/map-view.component.ts b/libs/feature/record/src/lib/map-view/map-view.component.ts index 565d59eaca..87e40b8fd8 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.ts @@ -34,6 +34,7 @@ import { distinctUntilChanged, finalize, map, + startWith, switchMap, tap, } from 'rxjs/operators' @@ -122,6 +123,10 @@ export class MapViewComponent implements OnInit, OnDestroy { }) ) ), + startWith({ + layers: [], + view: {}, + } as MapContextModel), withLatestFrom(this.mdViewFacade.metadata$), map(([context, metadata]) => { if (context.view.extent) return context diff --git a/libs/feature/record/src/lib/state/mdview.effects.spec.ts b/libs/feature/record/src/lib/state/mdview.effects.spec.ts index 066c5abe60..a1f8a5e112 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.spec.ts @@ -28,7 +28,7 @@ const full = { class RecordsRepositoryMock { aggregate = jest.fn(() => of(SAMPLE_AGGREGATIONS_RESULTS)) search = jest.fn(() => of(SAMPLE_SEARCH_RESULTS)) - getByUniqueIdentifier = jest.fn(() => of(DATASET_RECORDS[0])) + getRecord = jest.fn(() => of(DATASET_RECORDS[0])) getSimilarRecords = jest.fn(() => of(DATASET_RECORDS)) } @@ -82,7 +82,7 @@ describe('MdViewEffects', () => { }) describe('when api success and at no record found', () => { beforeEach(() => { - repository.getByUniqueIdentifier = jest.fn(() => of(null)) + repository.getRecord = jest.fn(() => of(null)) }) it('dispatch loadFullSuccess', () => { actions = hot('-a-|', { @@ -97,9 +97,7 @@ describe('MdViewEffects', () => { describe('when api fails', () => { beforeEach(() => { - repository.getByUniqueIdentifier = jest.fn(() => - throwError(() => new Error('api')) - ) + repository.getRecord = jest.fn(() => throwError(() => new Error('api'))) }) it('dispatch loadFullFailure', () => { actions = hot('-a-|', { diff --git a/libs/feature/record/src/lib/state/mdview.effects.ts b/libs/feature/record/src/lib/state/mdview.effects.ts index ac98891858..cc97d7918c 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.ts @@ -20,9 +20,7 @@ export class MdViewEffects { loadFullMetadata$ = createEffect(() => this.actions$.pipe( ofType(MdViewActions.loadFullMetadata), - switchMap(({ uuid }) => - this.recordsRepository.getByUniqueIdentifier(uuid) - ), + switchMap(({ uuid }) => this.recordsRepository.getRecord(uuid)), map((record) => { if (record === null) { return MdViewActions.loadFullMetadataFailure({ notFound: true }) diff --git a/libs/feature/record/src/lib/state/mdview.facade.spec.ts b/libs/feature/record/src/lib/state/mdview.facade.spec.ts index 24957eea0c..dbfe53f427 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.spec.ts @@ -328,5 +328,45 @@ describe('MdViewFacade', () => { tick() expect(result).toEqual(values.a) })) + describe('When the user switches datasets and allLinks emits again', () => { + beforeEach(() => { + store.setState({ + [METADATA_VIEW_FEATURE_STATE_KEY]: { + ...initialMetadataViewState, + metadata: DATASET_RECORDS[1], + }, + }) + }) + it('should return only the last links from allLinks', fakeAsync(() => { + const values = { + a: [ + { + type: 'service', + url: new URL('https://my-org.net/ogc'), + accessServiceProtocol: 'ogcFeatures', + name: 'ogcFeaturesSecondRecord', + description: + 'This OGC service is the second part of the download', + identifierInService: 'my:featuretype', + }, + ], + } + jest.spyOn(facade.dataService, 'getItemsFromOgcApi').mockResolvedValue({ + id: '123', + type: 'Feature', + time: null, + properties: { + type: '', + title: '', + }, + links: [], + geometry: { type: 'MultiPolygon', coordinates: [] }, + }) + let result + facade.geoDataLinksWithGeometry$.subscribe((v) => (result = v)) + tick() + expect(result).toEqual(values.a) + })) + }) }) }) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 8472e60290..4c2d20709f 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -1,6 +1,14 @@ import { Injectable } from '@angular/core' import { select, Store } from '@ngrx/store' -import { defaultIfEmpty, filter, map, mergeMap, scan } from 'rxjs/operators' +import { + defaultIfEmpty, + filter, + map, + mergeMap, + scan, + switchMap, + toArray, +} from 'rxjs/operators' import * as MdViewActions from './mdview.actions' import * as MdViewSelectors from './mdview.selectors' import { LinkClassifierService, LinkUsage } from '@geonetwork-ui/util/shared' @@ -103,36 +111,35 @@ export class MdViewFacade { ) geoDataLinksWithGeometry$ = this.allLinks$.pipe( - mergeMap((links) => { - return from(links) - }), - mergeMap((link) => { - if (this.linkClassifier.hasUsage(link, LinkUsage.GEODATA)) { - if ( - link.type === 'service' && - link.accessServiceProtocol === 'ogcFeatures' - ) { - return from(this.dataService.getItemsFromOgcApi(link.url.href)).pipe( - map((collectionRecords: OgcApiRecord) => { - return collectionRecords && collectionRecords.geometry - ? link - : null - }), - defaultIfEmpty(null) - ) - } else { - return of(link) - } - } else { - return of(null) - } - }), - scan((acc, val) => { - if (val !== null && !acc.includes(val)) { - acc.push(val) - } - return acc - }, []) + switchMap((links) => + from(links).pipe( + mergeMap((link) => { + if (this.linkClassifier.hasUsage(link, LinkUsage.GEODATA)) { + if ( + link.type === 'service' && + link.accessServiceProtocol === 'ogcFeatures' + ) { + return from( + this.dataService.getItemsFromOgcApi(link.url.href) + ).pipe( + map((collectionRecords: OgcApiRecord) => { + return collectionRecords && collectionRecords.geometry + ? link + : null + }), + defaultIfEmpty(null) + ) + } else { + return of(link) + } + } else { + return of(null) + } + }), + toArray(), + map((links) => links.filter((link) => link !== null)) + ) + ) ) landingPageLinks$ = this.metadata$.pipe( diff --git a/libs/feature/router/src/lib/default/constants.ts b/libs/feature/router/src/lib/default/constants.ts index d446911cf9..3365b3eca5 100644 --- a/libs/feature/router/src/lib/default/constants.ts +++ b/libs/feature/router/src/lib/default/constants.ts @@ -2,6 +2,7 @@ export const ROUTER_STATE_KEY = 'router' export const ROUTER_ROUTE_SEARCH = 'search' export const ROUTER_ROUTE_DATASET = 'dataset' +export const ROUTER_ROUTE_ORGANIZATION = 'organization' export enum ROUTE_PARAMS { SORT = '_sort', diff --git a/libs/feature/router/src/lib/default/router.config.ts b/libs/feature/router/src/lib/default/router.config.ts index 3900710e7f..63227fa12f 100644 --- a/libs/feature/router/src/lib/default/router.config.ts +++ b/libs/feature/router/src/lib/default/router.config.ts @@ -4,6 +4,7 @@ export interface RouterConfigModel { searchStateId: string searchRouteComponent: Type recordRouteComponent: Type + organizationRouteComponent: Type } export const ROUTER_CONFIG = new InjectionToken( diff --git a/libs/feature/router/src/lib/default/router.service.spec.ts b/libs/feature/router/src/lib/default/router.service.spec.ts index bfae57c838..02e1736354 100644 --- a/libs/feature/router/src/lib/default/router.service.spec.ts +++ b/libs/feature/router/src/lib/default/router.service.spec.ts @@ -3,18 +3,24 @@ import { Router } from '@angular/router' import { RouterService } from './router.service' import { ROUTER_CONFIG } from './router.config' +import { ROUTER_ROUTE_ORGANIZATION } from './constants' const SearchRouteComponent = { name: 'searchRoute', } const RecordRouteComponent = { - name: 'recordhRoute', + name: 'recordRoute', +} + +const OrganizationRouteComponent = { + name: 'organizationRoute', } const routerConfigMock = { searchStateId: 'main', searchRouteComponent: SearchRouteComponent, recordRouteComponent: RecordRouteComponent, + organizationRouteComponent: OrganizationRouteComponent, } const RouterMock = { resetConfig: jest.fn(), @@ -37,10 +43,16 @@ const expectedRoutes = [ }, { component: { - name: 'recordhRoute', + name: 'recordRoute', }, path: 'dataset/:metadataUuid', }, + { + path: `${ROUTER_ROUTE_ORGANIZATION}/:name`, + component: { + name: 'organizationRoute', + }, + }, ] describe('RouterService', () => { let service: RouterService diff --git a/libs/feature/router/src/lib/default/router.service.ts b/libs/feature/router/src/lib/default/router.service.ts index 6020fcc566..19c1f41fb9 100644 --- a/libs/feature/router/src/lib/default/router.service.ts +++ b/libs/feature/router/src/lib/default/router.service.ts @@ -1,5 +1,9 @@ import { Inject, Injectable } from '@angular/core' -import { ROUTER_ROUTE_DATASET, ROUTER_ROUTE_SEARCH } from '.' +import { + ROUTER_ROUTE_DATASET, + ROUTER_ROUTE_ORGANIZATION, + ROUTER_ROUTE_SEARCH, +} from '.' import { Router, Routes } from '@angular/router' import { ROUTER_CONFIG, RouterConfigModel } from './router.config' @@ -30,10 +34,18 @@ export class RouterService { path: `${ROUTER_ROUTE_DATASET}/:metadataUuid`, component: this.routerConfig.recordRouteComponent, }, + { + path: `${ROUTER_ROUTE_ORGANIZATION}/:name`, + component: this.routerConfig.organizationRouteComponent, + }, ] } getSearchRoute(): string { return ROUTER_ROUTE_SEARCH } + + getOrganizationPageRoute(): string { + return ROUTER_ROUTE_ORGANIZATION + } } diff --git a/libs/feature/router/src/lib/default/state/router.facade.ts b/libs/feature/router/src/lib/default/state/router.facade.ts index a8e6ddfe40..c2f9dd2054 100644 --- a/libs/feature/router/src/lib/default/state/router.facade.ts +++ b/libs/feature/router/src/lib/default/state/router.facade.ts @@ -3,7 +3,7 @@ import { MdViewActions } from '@geonetwork-ui/feature/record' import { RouterService } from '../router.service' import { RouterReducerState } from '@ngrx/router-store' import { select, Store } from '@ngrx/store' -import { distinctUntilChanged, filter, map, take } from 'rxjs/operators' +import { distinctUntilChanged, filter, map, take, tap } from 'rxjs/operators' import { ROUTER_ROUTE_DATASET, ROUTER_ROUTE_SEARCH, @@ -51,6 +51,14 @@ export class RouterFacade { }) } + goToOrganization(organizationName: string) { + const path = `${this.routerService.getOrganizationPageRoute()}/${organizationName}` + this.go({ + path, + queryParamsHandling: '', + }) + } + updateSearch(query?: SearchRouteParams) { this.go({ path: this.routerService.getSearchRoute(), diff --git a/libs/feature/search/src/index.ts b/libs/feature/search/src/index.ts index 0234a5cedb..f6103754e3 100644 --- a/libs/feature/search/src/index.ts +++ b/libs/feature/search/src/index.ts @@ -21,4 +21,4 @@ export * from './lib/results-hits-number/results-hits.container.component' export * from './lib/results-layout/results-layout.component' export * from './lib/sort-by/sort-by.component' export * from './lib/state/container/search-state.container.directive' -export * from './lib/results-table/results-table.component' +export * from './lib/results-table/results-table-container.component' diff --git a/libs/feature/search/src/lib/feature-search.module.ts b/libs/feature/search/src/lib/feature-search.module.ts index 89083ba721..2787c4eb35 100644 --- a/libs/feature/search/src/lib/feature-search.module.ts +++ b/libs/feature/search/src/lib/feature-search.module.ts @@ -14,7 +14,7 @@ import { SearchEffects } from './state/effects' import { initialState, reducer, SEARCH_FEATURE_KEY } from './state/reducer' import { ResultsHitsContainerComponent } from './results-hits-number/results-hits.container.component' import { SearchStateContainerDirective } from './state/container/search-state.container.directive' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { AutocompleteComponent, UiInputsModule } from '@geonetwork-ui/ui/inputs' import { NgModule } from '@angular/core' import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { FavoriteStarComponent } from './favorites/favorite-star/favorite-star.component' @@ -51,6 +51,7 @@ import { Gn4Repository } from '@geonetwork-ui/api/repository' FacetsModule, MatIconModule, UiWidgetsModule, + AutocompleteComponent, ], exports: [ SortByComponent, diff --git a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts index 7cca8a5fee..429ee0c750 100644 --- a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts +++ b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts @@ -49,7 +49,11 @@ describe('FuzzySearchComponent', () => { useClass: RecordsRepositoryMock, }, ], - imports: [UiInputsModule, TranslateModule.forRoot()], + imports: [ + AutocompleteComponent, + UiInputsModule, + TranslateModule.forRoot(), + ], }).compileComponents() searchService = TestBed.inject(SearchService) diff --git a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts index 97a69fdb89..896673a8df 100644 --- a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts +++ b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts @@ -11,7 +11,7 @@ import { AutocompleteComponent, AutocompleteItem, } from '@geonetwork-ui/ui/inputs' -import { Observable, firstValueFrom } from 'rxjs' +import { firstValueFrom, Observable } from 'rxjs' import { map } from 'rxjs/operators' import { SearchFacade } from '../state/search.facade' import { SearchService } from '../utils/service/search.service' @@ -32,7 +32,7 @@ export class FuzzySearchComponent implements OnInit { @Output() inputSubmitted = new EventEmitter() searchInputValue$: Observable<{ title: string }> - displayWithFn: (record: CatalogRecord) => string = (record) => record?.title + displayWithFn: (record: CatalogRecord) => string = (record) => record.title autoCompleteAction = (query: string) => this.recordsRepository diff --git a/libs/feature/search/src/lib/results-list/results-list.container.component.html b/libs/feature/search/src/lib/results-list/results-list.container.component.html index 45ba6814aa..1e5116a0c5 100644 --- a/libs/feature/search/src/lib/results-list/results-list.container.component.html +++ b/libs/feature/search/src/lib/results-list/results-list.container.component.html @@ -55,5 +55,5 @@ - + diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.css b/libs/feature/search/src/lib/results-table/results-table-container.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.html b/libs/feature/search/src/lib/results-table/results-table-container.component.html new file mode 100644 index 0000000000..0853ac710e --- /dev/null +++ b/libs/feature/search/src/lib/results-table/results-table-container.component.html @@ -0,0 +1,9 @@ + diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.spec.ts b/libs/feature/search/src/lib/results-table/results-table-container.component.spec.ts new file mode 100644 index 0000000000..0c663abd04 --- /dev/null +++ b/libs/feature/search/src/lib/results-table/results-table-container.component.spec.ts @@ -0,0 +1,135 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { ResultsTableContainerComponent } from './results-table-container.component' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { By } from '@angular/platform-browser' +import { BehaviorSubject } from 'rxjs' +import { SearchFacade } from '../state/search.facade' +import { SearchService } from '../utils/service/search.service' +import { SelectionService } from '@geonetwork-ui/api/repository' +import { TranslateModule } from '@ngx-translate/core' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' + +class SearchFacadeMock { + results$ = new BehaviorSubject(DATASET_RECORDS) + resultsHits$ = new BehaviorSubject(1000) + setConfigRequestFields = jest.fn(() => this) + setSortBy = jest.fn(() => this) + sortBy$ = new BehaviorSubject(['asc', 'updateDate']) +} +class SearchServiceMock { + setPage = jest.fn() + setSortBy = jest.fn() +} +class SelectionServiceMock { + selectRecords = jest.fn() + deselectRecords = jest.fn() + clearSelection = jest.fn() + selectedRecordsIdentifiers$ = new BehaviorSubject([]) +} +class RecordsRepositoryMock { + recordHasDraft = jest.fn(() => false) +} + +describe('ResultsTableContainerComponent', () => { + let component: ResultsTableContainerComponent + let searchFacade: SearchFacadeMock + let searchService: SearchServiceMock + let selectionService: SelectionServiceMock + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + { + provide: SearchFacade, + useClass: SearchFacadeMock, + }, + { + provide: SearchService, + useClass: SearchServiceMock, + }, + { + provide: SelectionService, + useClass: SelectionServiceMock, + }, + { + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, + }, + ], + }).compileComponents() + + fixture = TestBed.createComponent(ResultsTableContainerComponent) + searchFacade = TestBed.inject(SearchFacade) as any + searchService = TestBed.inject(SearchService) as any + selectionService = TestBed.inject(SelectionService) as any + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('sorting', () => { + describe('#setSortBy', () => { + it('calls the facade to change sort order', () => { + component.handleSortByChange('title', 'asc') + expect(searchService.setSortBy).toHaveBeenCalledWith(['asc', 'title']) + }) + }) + }) + + describe('selection', () => { + beforeEach(() => { + searchFacade.results$.next([ + { + uniqueIdentifier: '1', + }, + { + uniqueIdentifier: '2', + }, + { + uniqueIdentifier: '3', + }, + ] as any) + }) + + describe('#handleRecordSelectedChange', () => { + it('should call selectRecords when checkbox is clicked', () => { + const record = { uniqueIdentifier: '1' } + component.handleRecordsSelectedChange([record as CatalogRecord], true) + expect(selectionService.selectRecords).toHaveBeenCalledWith([record]) + }) + }) + }) + + describe('clicking on a dataset', () => { + let clickedRecord: CatalogRecord + + beforeEach(() => { + clickedRecord = null + component.recordClick.subscribe((r) => (clickedRecord = r)) + }) + + it('emits a recordClick event', () => { + const tableRow = fixture.debugElement.queryAll( + By.css('.table-row-cell') + )[1].nativeElement as HTMLDivElement + tableRow.parentElement.click() + expect(clickedRecord).toEqual(DATASET_RECORDS[0]) + }) + }) + + describe('#hasDraft', () => { + it('calls the repository service', () => { + const record = DATASET_RECORDS[0] + component.hasDraft(record) + expect( + TestBed.inject(RecordsRepositoryInterface).recordHasDraft + ).toHaveBeenCalledWith('my-dataset-001') + }) + }) +}) diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.ts b/libs/feature/search/src/lib/results-table/results-table-container.component.ts new file mode 100644 index 0000000000..10c076e4c4 --- /dev/null +++ b/libs/feature/search/src/lib/results-table/results-table-container.component.ts @@ -0,0 +1,49 @@ +import { Component, EventEmitter, Output } from '@angular/core' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { SearchFacade } from '../state/search.facade' +import { SelectionService } from '@geonetwork-ui/api/repository' +import { SearchService } from '../utils/service/search.service' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { ResultsTableComponent } from '@geonetwork-ui/ui/search' +import { CommonModule } from '@angular/common' + +@Component({ + selector: 'gn-ui-results-table-container', + templateUrl: './results-table-container.component.html', + styleUrls: ['./results-table-container.component.css'], + standalone: true, + imports: [CommonModule, ResultsTableComponent], +}) +export class ResultsTableContainerComponent { + @Output() recordClick = new EventEmitter() + + records$ = this.searchFacade.results$ + selectedRecords$ = this.selectionService.selectedRecordsIdentifiers$ + sortBy$ = this.searchFacade.sortBy$ + + hasDraft = (record: CatalogRecord): boolean => + this.recordsRepository.recordHasDraft(record.uniqueIdentifier) + + constructor( + private searchFacade: SearchFacade, + private searchService: SearchService, + private selectionService: SelectionService, + private recordsRepository: RecordsRepositoryInterface + ) {} + + handleRecordClick(item: unknown) { + this.recordClick.emit(item as CatalogRecord) + } + + handleSortByChange(col: string, order: 'asc' | 'desc') { + this.searchService.setSortBy([order, col]) + } + + handleRecordsSelectedChange(records: CatalogRecord[], selected: boolean) { + if (!selected) { + this.selectionService.deselectRecords(records) + } else { + this.selectionService.selectRecords(records) + } + } +} diff --git a/libs/feature/search/src/lib/results-table/results-table.component.spec.ts b/libs/feature/search/src/lib/results-table/results-table.component.spec.ts deleted file mode 100644 index 038bbf8f08..0000000000 --- a/libs/feature/search/src/lib/results-table/results-table.component.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' -import { ResultsTableComponent } from './results-table.component' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' -import { By } from '@angular/platform-browser' -import { BehaviorSubject, firstValueFrom } from 'rxjs' -import { SearchFacade } from '../state/search.facade' -import { SearchService } from '../utils/service/search.service' -import { SelectionService } from '@geonetwork-ui/api/repository' -import { TranslateModule } from '@ngx-translate/core' - -class SearchFacadeMock { - results$ = new BehaviorSubject(DATASET_RECORDS) - resultsHits$ = new BehaviorSubject(1000) - setConfigRequestFields = jest.fn(() => this) - setSortBy = jest.fn(() => this) - sortBy$ = new BehaviorSubject(['asc', 'updateDate']) -} -class SearchServiceMock { - setPage = jest.fn() - setSortBy = jest.fn() -} -class SelectionServiceMock { - selectRecords = jest.fn() - deselectRecords = jest.fn() - clearSelection = jest.fn() - selectedRecordsIdentifiers$ = new BehaviorSubject([]) -} - -describe('ResultsTableComponent', () => { - let component: ResultsTableComponent - let searchFacade: SearchFacadeMock - let searchService: SearchServiceMock - let selectionService: SelectionServiceMock - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - providers: [ - { - provide: SearchFacade, - useClass: SearchFacadeMock, - }, - { - provide: SearchService, - useClass: SearchServiceMock, - }, - { - provide: SelectionService, - useClass: SelectionServiceMock, - }, - ], - }).compileComponents() - - fixture = TestBed.createComponent(ResultsTableComponent) - searchFacade = TestBed.inject(SearchFacade) as any - searchService = TestBed.inject(SearchService) as any - selectionService = TestBed.inject(SelectionService) as any - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - describe('get a list of formats and sorts them depending on priority', () => { - it('returns a list of unique formats', () => { - expect(component.getRecordFormats(DATASET_RECORDS[0])).toEqual([ - 'geojson', - 'shp', - 'pdf', - ]) - }) - }) - describe('get the badge color for given format', () => { - it('returns the color for its format', () => { - expect( - component.getBadgeColor( - component.getRecordFormats(DATASET_RECORDS[0])[0] - ) - ).toEqual('#1e5180') // geojson - }) - }) - - describe('sorting', () => { - describe('#setSortBy', () => { - it('calls the facade to change sort order', () => { - component.setSortBy('title', 'asc') - expect(searchService.setSortBy).toHaveBeenCalledWith(['asc', 'title']) - }) - }) - describe('#isSortedBy', () => { - it('returns null if not sorted by this column', async () => { - searchFacade.sortBy$.next(['desc', 'owner']) - const sort = await firstValueFrom(component.isSortedBy('title')) - expect(sort).toBe(null) - }) - it('returns the sort order if the current sortBy is for this column', async () => { - searchFacade.sortBy$.next(['desc', 'title']) - const sort = await firstValueFrom(component.isSortedBy('title')) - expect(sort).toBe('desc') - }) - it('returns true if the current sortBy is for this column (multiple sorts)', async () => { - searchFacade.sortBy$.next([ - ['asc', 'score'], - ['desc', 'title'], - ]) - expect(await firstValueFrom(component.isSortedBy('title'))).toBe('desc') - expect(await firstValueFrom(component.isSortedBy('score'))).toBe('asc') - expect(await firstValueFrom(component.isSortedBy('owner'))).toBe(null) - }) - }) - }) - - describe('selection', () => { - beforeEach(() => { - searchFacade.results$.next([ - { - uniqueIdentifier: '1', - }, - { - uniqueIdentifier: '2', - }, - { - uniqueIdentifier: '3', - }, - ] as any) - }) - - describe('#isChecked', () => { - it('should return true when the record is in the selectedRecords array', async () => { - selectionService.selectedRecordsIdentifiers$.next(['1', '2']) - const record = { uniqueIdentifier: '2' } as CatalogRecord - expect(await firstValueFrom(component.isChecked(record))).toBe(true) - }) - - it('should return false when the record is not in the selectedRecords array', async () => { - selectionService.selectedRecordsIdentifiers$.next(['1', '2', '3']) - const record = { uniqueIdentifier: '4' } as CatalogRecord - expect(await firstValueFrom(component.isChecked(record))).toBe(false) - }) - }) - - describe('#handleRecordSelectedChange', () => { - it('should call selectRecords when checkbox is clicked', () => { - const record = { uniqueIdentifier: '1' } - component.handleRecordSelectedChange(true, record as CatalogRecord) - expect(selectionService.selectRecords).toHaveBeenCalledWith([record]) - }) - }) - - describe('#isAllSelected', () => { - it('returns true if all records in the page are selected', async () => { - selectionService.selectedRecordsIdentifiers$.next([ - '1', - '2', - '3', - '4', - '5', - ]) - expect(await firstValueFrom(component.isAllSelected())).toBe(true) - }) - it('returns false otherwise', async () => { - selectionService.selectedRecordsIdentifiers$.next(['1']) - expect(await firstValueFrom(component.isAllSelected())).toBe(false) - }) - }) - - describe('#isSomeSelected', () => { - it('returns false if all records in the page are selected', async () => { - selectionService.selectedRecordsIdentifiers$.next([ - '1', - '2', - '3', - '4', - '5', - ]) - expect(await firstValueFrom(component.isSomeSelected())).toBe(false) - }) - it('returns true if one or more records in the page is selected', async () => { - selectionService.selectedRecordsIdentifiers$.next(['2', '3']) - expect(await firstValueFrom(component.isSomeSelected())).toBe(true) - }) - it('returns false if no record in the page is selected', async () => { - selectionService.selectedRecordsIdentifiers$.next(['4', '5']) - expect(await firstValueFrom(component.isSomeSelected())).toBe(false) - }) - }) - }) - - describe('clicking on a dataset', () => { - let clickedRecord: CatalogRecord - - beforeEach(() => { - clickedRecord = null - component.recordClick.subscribe((r) => (clickedRecord = r)) - }) - - it('emits a recordClick event', () => { - const tableRow = fixture.debugElement.queryAll( - By.css('.table-row-cell') - )[1].nativeElement as HTMLDivElement - tableRow.parentElement.click() - expect(clickedRecord).toEqual(DATASET_RECORDS[0]) - }) - }) -}) diff --git a/libs/feature/search/src/lib/results-table/results-table.component.ts b/libs/feature/search/src/lib/results-table/results-table.component.ts deleted file mode 100644 index be2f59b2e9..0000000000 --- a/libs/feature/search/src/lib/results-table/results-table.component.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Component, EventEmitter, Output } from '@angular/core' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' -import { - FileFormat, - getBadgeColor, - getFileFormat, - getFormatPriority, -} from '@geonetwork-ui/util/shared' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' -import { - InteractiveTableColumnComponent, - InteractiveTableComponent, -} from '@geonetwork-ui/ui/layout' -import { MatIconModule } from '@angular/material/icon' -import { TranslateModule } from '@ngx-translate/core' -import { SearchFacade } from '../state/search.facade' -import { SelectionService } from '@geonetwork-ui/api/repository' -import { combineLatest, firstValueFrom, Observable } from 'rxjs' -import { CommonModule } from '@angular/common' -import { map, take } from 'rxjs/operators' -import { FieldSort } from '@geonetwork-ui/common/domain/model/search' -import { SearchService } from '../utils/service/search.service' - -@Component({ - selector: 'gn-ui-results-table', - templateUrl: './results-table.component.html', - styleUrls: ['./results-table.component.css'], - standalone: true, - imports: [ - CommonModule, - UiInputsModule, - InteractiveTableComponent, - InteractiveTableColumnComponent, - MatIconModule, - TranslateModule, - ], -}) -export class ResultsTableComponent { - @Output() recordClick = new EventEmitter() - - records$ = this.searchFacade.results$ - selectedRecords$ = this.selectionService.selectedRecordsIdentifiers$ - - constructor( - private searchFacade: SearchFacade, - private searchService: SearchService, - private selectionService: SelectionService - ) {} - - dateToString(date: Date): string { - return date?.toLocaleDateString(undefined, { - year: 'numeric', - month: 'long', - day: 'numeric', - timeZone: 'UTC', - }) - } - - getStatus(isPublishedToAll: boolean | unknown) { - return isPublishedToAll ? 'published' : 'not published' - } - - getRecordFormats(record: CatalogRecord): FileFormat[] { - if (record.kind === 'service' || !('distributions' in record)) { - return [] - } - const formats = Array.from( - new Set( - record.distributions.map((distribution) => getFileFormat(distribution)) - ) - ).filter((format) => !!format) - formats.sort((a, b) => getFormatPriority(b) - getFormatPriority(a)) - return formats - } - - formatUserInfo(userInfo: string | unknown): string { - const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') - if (infos && infos.length === 4) { - return `${infos[2]} ${infos[1]}` - } - return undefined - } - - getBadgeColor(format: FileFormat): string { - return getBadgeColor(format) - } - - handleRecordClick(item: unknown) { - this.recordClick.emit(item as CatalogRecord) - } - - setSortBy(col: string, order: 'asc' | 'desc') { - this.searchService.setSortBy([order, col]) - } - - isSortedBy(col: string): Observable<'asc' | 'desc' | null> { - return this.searchFacade.sortBy$.pipe( - take(1), - map((sortOrder) => { - const sortArray = Array.isArray(sortOrder[0]) - ? (sortOrder as FieldSort[]) - : ([sortOrder] as FieldSort[]) - for (const sort of sortArray) { - if (sort[1] === col) { - return sort[0] - } - } - return null - }) - ) - } - - isChecked(record: CatalogRecord): Observable { - return this.selectedRecords$.pipe( - take(1), - map((selectedRecords) => { - return selectedRecords.includes(record.uniqueIdentifier) - }) - ) - } - - handleRecordSelectedChange(selected: boolean, record: CatalogRecord) { - if (!selected) { - this.selectionService.deselectRecords([record]) - } else { - this.selectionService.selectRecords([record]) - } - } - - async toggleSelectAll() { - const records = await firstValueFrom(this.records$) - if (await firstValueFrom(this.isAllSelected())) { - this.selectionService.deselectRecords(records) - } else { - this.selectionService.selectRecords(records) - } - } - - isAllSelected(): Observable { - return combineLatest([this.records$, this.selectedRecords$]).pipe( - take(1), - map(([records, selectedRecords]) => { - return records.every((record) => - selectedRecords.includes(record.uniqueIdentifier) - ) - }) - ) - } - - isSomeSelected(): Observable { - return combineLatest([this.records$, this.selectedRecords$]).pipe( - take(1), - map(([records, selectedRecords]) => { - const allSelected = records.every((record) => - selectedRecords.includes(record.uniqueIdentifier) - ) - const someSelected = records.some((record) => - selectedRecords.includes(record.uniqueIdentifier) - ) - return !allSelected && someSelected - }) - ) - } -} diff --git a/libs/feature/search/src/lib/state/search.facade.ts b/libs/feature/search/src/lib/state/search.facade.ts index 9e3046a73e..1f9388a2b1 100644 --- a/libs/feature/search/src/lib/state/search.facade.ts +++ b/libs/feature/search/src/lib/state/search.facade.ts @@ -37,6 +37,7 @@ import { getSearchResultsLoading, getSearchSortBy, getSpatialFilterEnabled, + isBeginningOfResults, isEndOfResults, totalPages, } from './selectors' @@ -58,6 +59,7 @@ export class SearchFacade { layout$: Observable sortBy$: Observable isLoading$: Observable + isBeginningOfResults$: Observable isEndOfResults$: Observable totalPages$: Observable currentPage$: Observable @@ -98,6 +100,9 @@ export class SearchFacade { this.isLoading$ = this.store.pipe(select(getSearchResultsLoading, searchId)) this.searchFilters$ = this.store.pipe(select(getSearchFilters, searchId)) this.resultsHits$ = this.store.pipe(select(getSearchResultsHits, searchId)) + this.isBeginningOfResults$ = this.store.pipe( + select(isBeginningOfResults, searchId) + ) this.isEndOfResults$ = this.store.pipe(select(isEndOfResults, searchId)) this.totalPages$ = this.store.pipe(select(totalPages, searchId)) this.currentPage$ = this.store.pipe(select(currentPage, searchId)) diff --git a/libs/feature/search/src/lib/state/selectors.spec.ts b/libs/feature/search/src/lib/state/selectors.spec.ts index d20bfb6156..70127f68c0 100644 --- a/libs/feature/search/src/lib/state/selectors.spec.ts +++ b/libs/feature/search/src/lib/state/selectors.spec.ts @@ -76,6 +76,38 @@ describe('Search Selectors', () => { }) }) + describe('isBeginningOfResults', () => { + it('should return true once at the beginning of results list', () => { + const beginningResult = fromSelectors.isBeginningOfResults.projector({ + ...initialStateSearch, + params: { + ...initialStateSearch.params, + currentPage: 0, + pageSize: 20, + }, + results: { + ...initialStateSearch.results, + count: 62, + }, + }) + expect(beginningResult).toEqual(true) + + const notBeginningResult = fromSelectors.isBeginningOfResults.projector({ + ...initialStateSearch, + params: { + ...initialStateSearch.params, + currentPage: 3, + pageSize: 20, + }, + results: { + ...initialStateSearch.results, + count: 62, + }, + }) + expect(notBeginningResult).toEqual(false) + }) + }) + describe('isEndOfResults', () => { it('should return true once at the end of results list', () => { const result = fromSelectors.isEndOfResults.projector({ diff --git a/libs/feature/search/src/lib/state/selectors.ts b/libs/feature/search/src/lib/state/selectors.ts index c8f85088c3..b2215ba3fc 100644 --- a/libs/feature/search/src/lib/state/selectors.ts +++ b/libs/feature/search/src/lib/state/selectors.ts @@ -50,6 +50,13 @@ export const getSearchResultsHits = createSelector( (state: SearchStateSearch) => state.results.count ) +export const isBeginningOfResults = createSelector( + getSearchStateSearch, + (state: SearchStateSearch) => { + return state.params.currentPage === 0 + } +) + export const isEndOfResults = createSelector( getSearchStateSearch, (state: SearchStateSearch) => { diff --git a/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.spec.ts b/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.spec.ts index 5c6a39f3d3..38fb3d77e8 100644 --- a/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.spec.ts +++ b/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.spec.ts @@ -3,21 +3,6 @@ import { LANGUAGE_STORAGE_KEY } from '@geonetwork-ui/util/i18n' import { TranslateService } from '@ngx-translate/core' import { LanguageSwitcherComponent } from './language-switcher.component' -export class LocalStorageRefStub { - store = {} - mockLocalStorage = { - getItem: (key: string): string => { - return key in this.store ? this.store[key] : null - }, - setItem: (key: string, value: string) => { - this.store[key] = `${value}` - }, - } - public getLocalStorage() { - return this.mockLocalStorage - } -} - class TranslateServiceMock { use = jest.fn() currentLang = 'en' @@ -43,10 +28,6 @@ describe('LanguageSwitcherComponent', () => { service = TestBed.inject(TranslateService) fixture = TestBed.createComponent(LanguageSwitcherComponent) component = fixture.componentInstance - - Object.defineProperty(window, 'localStorage', { - value: new LocalStorageRefStub().getLocalStorage(), - }) }) it('should create', () => { diff --git a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html index dedd016072..711122dffb 100644 --- a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html +++ b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.html @@ -1,14 +1,14 @@
@@ -18,22 +18,24 @@ class="shrink-0 mb-3 mt-5 font-title text-21 text-title group-hover:text-primary line-clamp-2 sm:mt-2 transition-colors" data-cy="organizationName" > - {{ organisation.name }}

- {{ organisation.description }} + {{ organization.description }}

folder_open {{ - organisation.recordCount + organization.recordCount }} - record.metadata.publications + record.metadata.publications
diff --git a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.spec.ts b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.spec.ts index c8b81ce2c7..36a5ef528f 100644 --- a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.spec.ts +++ b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.spec.ts @@ -35,7 +35,7 @@ describe('OrganisationPreviewComponent', () => { fixture = TestBed.createComponent(OrganisationPreviewComponent) component = fixture.componentInstance - component.organisation = organisationMock + component.organization = organisationMock fixture.detectChanges() }) diff --git a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.stories.ts b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.stories.ts index d0fbb769c7..2510384d2c 100644 --- a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.stories.ts +++ b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.stories.ts @@ -46,7 +46,7 @@ export default { export const Primary: StoryObj = { args: { - organisation: { + organization: { name: 'Agglo du Saint Quentinois', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', diff --git a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.ts b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.ts index 0cc798faab..c8eae909ce 100644 --- a/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.ts +++ b/libs/ui/catalog/src/lib/organisation-preview/organisation-preview.component.ts @@ -14,12 +14,12 @@ import { Organization } from '@geonetwork-ui/common/domain/model/record' changeDetection: ChangeDetectionStrategy.OnPush, }) export class OrganisationPreviewComponent { - @Input() organisation: Organization + @Input() organization: Organization @Input() organisationUrl: string @Output() clickedOrganisation = new EventEmitter() clickOrganisation(event: Event) { event.preventDefault() - this.clickedOrganisation.emit(this.organisation) + this.clickedOrganisation.emit(this.organization) } } diff --git a/libs/ui/catalog/src/lib/ui-catalog.module.ts b/libs/ui/catalog/src/lib/ui-catalog.module.ts index 990100a7f3..6a9a1fdfdd 100644 --- a/libs/ui/catalog/src/lib/ui-catalog.module.ts +++ b/libs/ui/catalog/src/lib/ui-catalog.module.ts @@ -9,6 +9,7 @@ import { OrganisationsFilterComponent } from './organisations-filter/organisatio import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { LanguageSwitcherComponent } from './language-switcher/language-switcher.component' import { OrganisationsResultComponent } from './organisations-result/organisations-result.component' +import { RouterLink } from '@angular/router' @NgModule({ declarations: [ @@ -24,6 +25,7 @@ import { OrganisationsResultComponent } from './organisations-result/organisatio UiElementsModule, UiInputsModule, MatIconModule, + RouterLink, ], exports: [ CatalogTitleComponent, diff --git a/libs/ui/dataviz/src/lib/figure/figure.component.html b/libs/ui/dataviz/src/lib/figure/figure.component.html index 7b06c7b37c..2324b2d5c1 100644 --- a/libs/ui/dataviz/src/lib/figure/figure.component.html +++ b/libs/ui/dataviz/src/lib/figure/figure.component.html @@ -1,7 +1,13 @@
- {{ figure }} + {{ + figure + }} {{ unit }}
diff --git a/libs/ui/dataviz/src/lib/figure/figure.component.spec.ts b/libs/ui/dataviz/src/lib/figure/figure.component.spec.ts index b587252eb9..e5029bb01e 100644 --- a/libs/ui/dataviz/src/lib/figure/figure.component.spec.ts +++ b/libs/ui/dataviz/src/lib/figure/figure.component.spec.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { FigureComponent } from './figure.component' +import { TranslateModule } from '@ngx-translate/core' describe('FigureComponent', () => { let component: FigureComponent @@ -11,6 +12,7 @@ describe('FigureComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [FigureComponent], + imports: [TranslateModule.forRoot({})], schemas: [NO_ERRORS_SCHEMA], }) .overrideComponent(FigureComponent, { @@ -66,7 +68,9 @@ describe('FigureComponent', () => { ) }) it('has a tooltip containing the information', () => { - const title = component.hoverTitle + const title = compiled.querySelector( + '[data-test="figureTitle"]' + )?.textContent expect(title).toContain(component.title) expect(title).toContain(component.unit) expect(title).toContain(component.figure) @@ -77,7 +81,9 @@ describe('FigureComponent', () => { component.unit = undefined }) it('does not have undefined in the tooltip', () => { - const title = component.hoverTitle + const title = compiled.querySelector( + '[data-test="figureTitle"]' + )?.textContent expect(title).toContain(component.title) expect(title).toContain(component.figure) expect(title).not.toContain('undefined') diff --git a/libs/ui/dataviz/src/lib/figure/figure.component.ts b/libs/ui/dataviz/src/lib/figure/figure.component.ts index 196240e313..46c4601d4a 100644 --- a/libs/ui/dataviz/src/lib/figure/figure.component.ts +++ b/libs/ui/dataviz/src/lib/figure/figure.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, Input } from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' @Component({ selector: 'gn-ui-figure', @@ -10,17 +10,13 @@ export class FigureComponent { @Input() icon!: string @Input() title!: string @Input() figure!: string | number - @Input() unit?: string + @Input() unit = '' @Input() color: 'primary' | 'secondary' = 'primary' - get hoverTitle() { - return `${this.figure.toString()} ${this.unit || ''} -${this.title}` - } - get textClass() { return this.color === 'primary' ? 'text-primary' : 'text-secondary' } + get bgClass() { return this.color === 'primary' ? 'bg-primary-white' : 'bg-secondary-white' } diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index e48900c513..578e08b8ed 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -3,22 +3,22 @@ export * from './lib/avatar/avatar.component' export * from './lib/content-ghost/content-ghost.component' export * from './lib/download-item/download-item.component' export * from './lib/downloads-list/downloads-list.component' +export * from './lib/error/error.component' export * from './lib/image-overlay-preview/image-overlay-preview.component' export * from './lib/link-card/link-card.component' export * from './lib/markdown-editor/markdown-editor.component' export * from './lib/markdown-parser/markdown-parser.component' -export * from './lib/max-lines/max-lines.component' export * from './lib/metadata-catalog/metadata-catalog.component' export * from './lib/metadata-contact/metadata-contact.component' export * from './lib/metadata-info/metadata-info.component' export * from './lib/metadata-quality-item/metadata-quality-item.component' export * from './lib/metadata-quality/metadata-quality.component' +export * from './lib/notification/notification.component' export * from './lib/pagination-buttons/pagination-buttons.component' export * from './lib/pagination/pagination.component' export * from './lib/record-api-form/record-api-form.component' export * from './lib/related-record-card/related-record-card.component' -export * from './lib/error/error.component' +export * from './lib/sortable-list/sortable-list.component' export * from './lib/thumbnail/thumbnail.component' export * from './lib/ui-elements.module' export * from './lib/user-preview/user-preview.component' -export * from './lib/notification/notification.component' diff --git a/libs/ui/elements/src/lib/error/error.component.html b/libs/ui/elements/src/lib/error/error.component.html index b145a4caf1..e06e2a1b2f 100644 --- a/libs/ui/elements/src/lib/error/error.component.html +++ b/libs/ui/elements/src/lib/error/error.component.html @@ -8,11 +8,11 @@
face question_mark + >question_mark + question_mark + >question_mark +
search.error.couldNotReachApi
@@ -32,6 +32,15 @@
search.error.receivedError
{{ error }}
+
+
+ computer + question_mark + +
+
search.error.organizationHasNoDataset
+
computer question_mark + >question_mark +
search.error.recordNotFound
{{ error }}
+
+
+ computer + question_mark + +
+
+ search.error.organizationNotFound +
+
{{ error }}
+
diff --git a/libs/ui/elements/src/lib/error/error.component.ts b/libs/ui/elements/src/lib/error/error.component.ts index cf0546eb62..a7b79425b0 100644 --- a/libs/ui/elements/src/lib/error/error.component.ts +++ b/libs/ui/elements/src/lib/error/error.component.ts @@ -5,6 +5,8 @@ export enum ErrorType { RECEIVED_ERROR, RECORD_NOT_FOUND, DATASET_HAS_NO_LINK, + ORGANIZATION_HAS_NO_DATASET, + ORGANIZATION_NOT_FOUND, } @Component({ diff --git a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html index 932c405b23..85fb920c14 100644 --- a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html +++ b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html @@ -1,5 +1,5 @@
diff --git a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts index f883b3c31b..c166ea575d 100644 --- a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts +++ b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, Input } from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' @Component({ @@ -8,5 +8,26 @@ import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' changeDetection: ChangeDetectionStrategy.OnPush, }) export class RelatedRecordCardComponent { + private readonly baseClasses: string + @Input() record: CatalogRecord + @Input() extraClass = '' + + constructor() { + this.baseClasses = [ + 'w-72', + 'h-96', + 'overflow-hidden', + 'rounded-lg', + 'bg-white', + 'cursor-pointer', + 'block', + 'hover:-translate-y-2 ', + 'duration-[180ms]', + ].join(' ') + } + + get classList() { + return `${this.baseClasses} ${this.extraClass}` + } } diff --git a/libs/ui/elements/src/lib/sortable-list/sortable-list.component.css b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.css new file mode 100644 index 0000000000..90ccd70082 --- /dev/null +++ b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.css @@ -0,0 +1,18 @@ +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.sortable-list.cdk-drop-list-dragging .sortable-box:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} diff --git a/libs/ui/elements/src/lib/sortable-list/sortable-list.component.html b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.html new file mode 100644 index 0000000000..f613d28401 --- /dev/null +++ b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.html @@ -0,0 +1,36 @@ +
+ + add +  {{ addOption.buttonLabel }} +
+
+ +
+ + drag_handle + +
+ +
+ + close + +
+
+
diff --git a/libs/ui/elements/src/lib/sortable-list/sortable-list.component.spec.ts b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.spec.ts new file mode 100644 index 0000000000..48718bd5ec --- /dev/null +++ b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { SortableListComponent } from './sortable-list.component' + +describe('SortableListComponent', () => { + let component: SortableListComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SortableListComponent], + }).compileComponents() + + fixture = TestBed.createComponent(SortableListComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/ui/elements/src/lib/sortable-list/sortable-list.component.stories.ts b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.stories.ts new file mode 100644 index 0000000000..7c33b7034e --- /dev/null +++ b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.stories.ts @@ -0,0 +1,51 @@ +import { importProvidersFrom } from '@angular/core' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { + DatePickerComponent, + DateRangePickerComponent, +} from '@geonetwork-ui/ui/inputs' +import { Meta, StoryObj, applicationConfig } from '@storybook/angular' +import { SortableListComponent } from './sortable-list.component' + +export default { + title: 'Elements/SortableListComponent', + component: SortableListComponent, + subcomponents: { DateRangePickerComponent }, + decorators: [ + applicationConfig({ + // FIXME: needed only for DateRangePickerComponent + providers: [importProvidersFrom(BrowserAnimationsModule)], + }), + ], +} as Meta + +export const TemporalExtents: StoryObj = { + args: { + addOptions: [ + { buttonLabel: 'Date déterminée', eventName: 'date' }, + { buttonLabel: 'Période de temps', eventName: 'range' }, + ], + elements: [ + { + component: DateRangePickerComponent, + inputs: { + startDate: new Date('1977-05-25'), + endDate: new Date('1977-05-26'), + }, + }, + { + component: DateRangePickerComponent, + inputs: { endDate: new Date('1978-05-25') }, + }, + { + component: DatePickerComponent, + inputs: { date: new Date('1979-05-25') }, + }, + { + component: DateRangePickerComponent, + inputs: { startDate: new Date('1977-06-25') }, + }, + { component: DateRangePickerComponent, inputs: {} }, + ], + }, +} diff --git a/libs/ui/elements/src/lib/sortable-list/sortable-list.component.ts b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.ts new file mode 100644 index 0000000000..de833ecced --- /dev/null +++ b/libs/ui/elements/src/lib/sortable-list/sortable-list.component.ts @@ -0,0 +1,56 @@ +import { + CdkDrag, + CdkDragDrop, + CdkDragHandle, + CdkDropList, + moveItemInArray, +} from '@angular/cdk/drag-drop' +import { NgComponentOutlet, NgFor } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + Type, +} from '@angular/core' +import { MatIconModule } from '@angular/material/icon' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' + +type DynamicElement = { + component: Type + inputs: Record +} + +@Component({ + selector: 'gn-ui-sortable-list', + templateUrl: 'sortable-list.component.html', + styleUrls: ['sortable-list.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + NgFor, + NgComponentOutlet, + CdkDropList, + CdkDrag, + CdkDragHandle, + MatIconModule, + ButtonComponent, + ], +}) +export class SortableListComponent { + @Input() elements: Array + @Input() addOptions: Array<{ buttonLabel: string; eventName: string }> + @Output() elementsChange = new EventEmitter>() + @Output() add = new EventEmitter() + + drop(event: CdkDragDrop) { + moveItemInArray(this.elements, event.previousIndex, event.currentIndex) + this.elementsChange.emit(this.elements) + } + + removeElement(index: number) { + this.elements = this.elements.filter((_, i) => i !== index) + this.elementsChange.emit(this.elements) + } +} diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index 28d194adc9..83c6e654ca 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -20,13 +20,12 @@ import { MetadataQualityItemComponent } from './metadata-quality-item/metadata-q import { ErrorComponent } from './error/error.component' import { PaginationComponent } from './pagination/pagination.component' import { ThumbnailComponent } from './thumbnail/thumbnail.component' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { BadgeComponent, UiInputsModule } from '@geonetwork-ui/ui/inputs' import { FormsModule } from '@angular/forms' import { AvatarComponent } from './avatar/avatar.component' import { UserPreviewComponent } from './user-preview/user-preview.component' import { GnUiLinkifyDirective } from './metadata-info/linkify.directive' import { PaginationButtonsComponent } from './pagination-buttons/pagination-buttons.component' -import { MaxLinesComponent } from './max-lines/max-lines.component' import { RecordApiFormComponent } from './record-api-form/record-api-form.component' import { MarkdownParserComponent } from './markdown-parser/markdown-parser.component' import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-overlay-preview.component' @@ -49,6 +48,7 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' MarkdownParserComponent, ThumbnailComponent, TimeSincePipe, + BadgeComponent, ], declarations: [ MetadataInfoComponent, @@ -67,7 +67,6 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' UserPreviewComponent, GnUiLinkifyDirective, PaginationButtonsComponent, - MaxLinesComponent, RecordApiFormComponent, UserFeedbackItemComponent, ImageOverlayPreviewComponent, @@ -89,7 +88,6 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' AvatarComponent, UserPreviewComponent, PaginationButtonsComponent, - MaxLinesComponent, RecordApiFormComponent, MarkdownParserComponent, UserFeedbackItemComponent, diff --git a/libs/ui/inputs/src/index.ts b/libs/ui/inputs/src/index.ts index 1baf8d6f45..06c277e821 100644 --- a/libs/ui/inputs/src/index.ts +++ b/libs/ui/inputs/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/autocomplete/autocomplete.component' +export * from './lib/badge/badge.component' export * from './lib/button/button.component' export * from './lib/check-toggle/check-toggle.component' export * from './lib/checkbox/checkbox.component' diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css index a03749aa95..cc2ca1991e 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css @@ -3,7 +3,6 @@ } .clear-btn { width: var(--input-height); - right: var(--input-height); height: 100%; } .search-btn { diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html index e542a23dfa..4e9e994bd0 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html @@ -8,23 +8,27 @@ [matAutocomplete]="auto" (keyup.enter)="handleEnter(searchInput.value)" /> - - +
+ + +
- {{ displayWithFn(suggestion) }} + {{ displayWithFnInternal(suggestion) }} diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts index 7c11ac9daa..138118521c 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts @@ -1,15 +1,13 @@ import { ChangeDetectionStrategy } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' -import { ReactiveFormsModule } from '@angular/forms' -import { MatAutocompleteModule } from '@angular/material/autocomplete' -import { MatIconModule } from '@angular/material/icon' import { By } from '@angular/platform-browser' -import { of, throwError } from 'rxjs' +import { of, Subscription, throwError } from 'rxjs' import { AutocompleteComponent, AutocompleteItem, } from './autocomplete.component' -import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { TranslateModule } from '@ngx-translate/core' describe('AutocompleteComponent', () => { let component: AutocompleteComponent @@ -18,12 +16,10 @@ describe('AutocompleteComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ - MatAutocompleteModule, - ReactiveFormsModule, - MatIconModule, - UiWidgetsModule, + AutocompleteComponent, + NoopAnimationsModule, + TranslateModule.forRoot(), ], - declarations: [AutocompleteComponent], }) .overrideComponent(AutocompleteComponent, { set: { changeDetection: ChangeDetectionStrategy.Default }, @@ -44,54 +40,78 @@ describe('AutocompleteComponent', () => { }) describe('suggestions', () => { - let emitted - let sub - beforeEach(() => { - fixture.detectChanges() - emitted = null - sub = component.suggestions$.subscribe((e) => (emitted = e)) - }) - afterEach(() => { - sub.unsubscribe() - }) - describe('when writing text over 2 chars', () => { + let emitted: unknown + let sub: Subscription + describe('with minCharacterCount above 0', () => { beforeEach(() => { - component.inputRef.nativeElement.value = 'bla' - component.inputRef.nativeElement.dispatchEvent(new InputEvent('input')) - }) - it('calls the action given as input after debounce', () => { - jest.runOnlyPendingTimers() - expect(component.action).toHaveBeenCalledWith('bla') - }) - it('emits suggestions', () => { - jest.runOnlyPendingTimers() - expect(emitted).toEqual(['aa', 'bb', 'cc']) - }) - it('does not show an error popup', () => { - const popup = fixture.debugElement.query(By.css('gn-ui-popup-alert')) - expect(popup).toBeFalsy() + fixture.detectChanges() + emitted = null + sub = component.suggestions$.subscribe((e) => (emitted = e)) + }) + afterEach(() => { + sub.unsubscribe() + }) + describe('when writing text over 2 chars', () => { + beforeEach(() => { + component.inputRef.nativeElement.value = 'bla' + component.inputRef.nativeElement.dispatchEvent( + new InputEvent('input') + ) + }) + it('calls the action given as input after debounce', () => { + jest.runOnlyPendingTimers() + expect(component.action).toHaveBeenCalledWith('bla') + }) + it('emits suggestions', () => { + jest.runOnlyPendingTimers() + expect(emitted).toEqual(['aa', 'bb', 'cc']) + }) + it('does not show an error popup', () => { + const popup = fixture.debugElement.query(By.css('gn-ui-popup-alert')) + expect(popup).toBeFalsy() + }) }) - }) - describe('when clicking a predefined button', () => { - beforeEach(() => { - component.updateInputValue({ title: 'cc' } as AutocompleteItem) + describe('when clicking a predefined button', () => { + beforeEach(() => { + component.updateInputValue({ title: 'cc' } as AutocompleteItem) + }) + it('calls the action with object given as input', () => { + expect(component.action).toHaveBeenCalledWith('cc') + }) }) - it('calls the action with object given as input', () => { - expect(component.action).toHaveBeenCalledWith('cc') + describe('when writing text with 2 chars or less', () => { + beforeEach(() => { + component.inputRef.nativeElement.value = 'bl' + component.inputRef.nativeElement.dispatchEvent( + new InputEvent('input') + ) + }) + it('does not call the action given as input after debounce', () => { + jest.runOnlyPendingTimers() + expect(component.action).not.toHaveBeenCalled() + }) + it('emit an empty suggestions list', () => { + jest.runOnlyPendingTimers() + expect(emitted).toEqual([]) + }) }) }) - describe('when writing text with 2 chars or less', () => { + describe('when minCharacterCount is 0', () => { beforeEach(() => { - component.inputRef.nativeElement.value = 'bl' - component.inputRef.nativeElement.dispatchEvent(new InputEvent('input')) + component.minCharacterCount = 0 + fixture.detectChanges() + emitted = null + sub = component.suggestions$.subscribe((e) => (emitted = e)) + component.inputRef.nativeElement.value = '' + component.inputRef.nativeElement.dispatchEvent(new InputEvent('focus')) }) - it('does not call the action given as input after debounce', () => { + it('calls action and shows suggestions on focus', () => { jest.runOnlyPendingTimers() - expect(component.action).not.toHaveBeenCalled() + expect(component.action).toHaveBeenCalled() }) - it('does not emit', () => { + it('emits suggestions', () => { jest.runOnlyPendingTimers() - expect(emitted).toEqual(null) + expect(emitted).toEqual(['aa', 'bb', 'cc']) }) }) }) @@ -164,9 +184,6 @@ describe('AutocompleteComponent', () => { it('sends a submitted value', () => { expect(anyEmitted).toEqual(['bla']) }) - it('closes the autocomplete panel', () => { - expect(component.triggerRef.closePanel).toHaveBeenCalled() - }) }) describe('with an empty text value', () => { beforeEach(() => { @@ -180,10 +197,35 @@ describe('AutocompleteComponent', () => { expect(anyEmitted).toEqual(['']) }) }) + describe('allowSubmit is false', () => { + let emitted + beforeEach(() => { + component.allowSubmit = false + fixture.detectChanges() + emitted = null + component.cancelEnter = false + component.inputSubmitted.subscribe((e) => (emitted = e)) + component.inputRef.nativeElement.value = 'blarg' + component.inputRef.nativeElement.dispatchEvent( + new KeyboardEvent('keyup', { + key: 'Enter', + bubbles: true, + }) + ) + }) + it('does not show a submit button', () => { + const button = fixture.debugElement.query( + By.css('[data-test=autocomplete-submit-btn]') + ) + expect(button).toBeFalsy() + }) + it('does not emit inputSubmitted on enter', () => { + expect(emitted).toBeNull() + }) + }) }) describe('@Input() value', () => { - let anyEmitted describe('when set', () => { beforeEach(() => { const simpleChanges: any = { @@ -192,8 +234,7 @@ describe('AutocompleteComponent', () => { currentValue: { title: 'hello' }, }, } - component.displayWithFn = (item) => item?.title - component.inputSubmitted.subscribe((event) => (anyEmitted = event)) + component.displayWithFn = (item) => item.title component.ngOnChanges(simpleChanges) }) it('set control value', () => { @@ -208,8 +249,7 @@ describe('AutocompleteComponent', () => { currentValue: { title: 'good bye' }, }, } - component.displayWithFn = (item) => item?.title - component.inputSubmitted.subscribe((event) => (anyEmitted = event)) + component.displayWithFn = (item) => item.title component.ngOnChanges(simpleChanges) }) it('set control value', () => { @@ -225,7 +265,7 @@ describe('AutocompleteComponent', () => { currentValue: { title: 'good bye' }, }, } - component.displayWithFn = (item) => item?.title + component.displayWithFn = (item) => item.title component.inputSubmitted.subscribe((event) => (anyEmitted = event)) component.ngOnChanges(simpleChanges) }) @@ -238,7 +278,6 @@ describe('AutocompleteComponent', () => { }) describe('when not set on init (firstChange == true)', () => { beforeEach(() => { - component.inputSubmitted.subscribe((event) => (anyEmitted = event)) const simpleChanges: any = { value: { firstChange: true, @@ -291,7 +330,7 @@ describe('AutocompleteComponent', () => { let suggestions beforeEach(() => { suggestions = null - component.action = jest.fn(() => throwError(new Error('blargz'))) + component.action = jest.fn(() => throwError(() => new Error('blargz'))) fixture.detectChanges() component.suggestions$.subscribe((value) => (suggestions = value)) diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.stories.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.stories.ts index af15224b7a..2f506b7ad8 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.stories.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.stories.ts @@ -1,14 +1,15 @@ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' -import { AutocompleteComponent } from './autocomplete.component' -import { of, throwError } from 'rxjs' -import { MatAutocompleteModule } from '@angular/material/autocomplete' -import { MatIconModule } from '@angular/material/icon' -import { ReactiveFormsModule } from '@angular/forms' +import { + AutocompleteComponent, + AutocompleteItem, +} from './autocomplete.component' +import { Observable, of, throwError } from 'rxjs' import { TranslateModule } from '@ngx-translate/core' import { TRANSLATE_DEFAULT_CONFIG, UtilI18nModule, } from '@geonetwork-ui/util/i18n' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' export default { title: 'Inputs/AutocompleteComponent', @@ -16,26 +17,35 @@ export default { decorators: [ moduleMetadata({ imports: [ + AutocompleteComponent, UtilI18nModule, TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - MatAutocompleteModule, - MatIconModule, - ReactiveFormsModule, + BrowserAnimationsModule, ], }), ], } as Meta type AutocompleteComponentWithActionResult = AutocompleteComponent & { - actionResult: string[] + actionResult: (value: string) => Observable actionThrowsError: boolean + value: AutocompleteItem +} + +const initialItems = ['Hello', 'world', 'from', 'storybook'] +function filterResults(value: string) { + return initialItems.filter((item) => { + return item.toLowerCase().includes(value?.toLowerCase()) + }) } export const Primary: StoryObj = { args: { placeholder: 'Full text search', - actionResult: ['Hello', 'world'], + minCharacterCount: 3, actionThrowsError: false, + clearOnSelection: false, + allowSubmit: true, }, argTypes: { itemSelected: { @@ -51,10 +61,34 @@ export const Primary: StoryObj = { render: (args) => ({ props: { ...args, - action: () => + action: (value: string) => args.actionThrowsError - ? throwError(new Error('Something went terribly wrong!')) - : of(args.actionResult), + ? throwError(() => new Error('Something went terribly wrong!')) + : of(filterResults(value)), }, }), } + +export const NoMinimumCharacterCount: StoryObj = + { + args: { + placeholder: + 'Click to show suggestions! selecting one should clear this field', + minCharacterCount: 0, + clearOnSelection: true, + }, + argTypes: { + itemSelected: { + action: 'itemSelected', + }, + inputSubmitted: { + action: 'inputSubmitted', + }, + }, + render: (args) => ({ + props: { + ...args, + action: (value: string) => of(filterResults(value)), + }, + }), + } diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts index 43730f25db..02761e908d 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts @@ -13,25 +13,29 @@ import { SimpleChanges, ViewChild, } from '@angular/core' -import { UntypedFormControl } from '@angular/forms' +import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms' import { MatAutocomplete, + MatAutocompleteModule, MatAutocompleteSelectedEvent, MatAutocompleteTrigger, } from '@angular/material/autocomplete' -import { merge, Observable, of, ReplaySubject, Subscription } from 'rxjs' +import { first, merge, Observable, of, ReplaySubject, Subscription } from 'rxjs' import { catchError, debounceTime, distinctUntilChanged, filter, finalize, - first, map, switchMap, take, tap, } from 'rxjs/operators' +import { MatIconModule } from '@angular/material/icon' +import { PopupAlertComponent } from '@geonetwork-ui/ui/widgets' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' export type AutocompleteItem = unknown @@ -40,6 +44,15 @@ export type AutocompleteItem = unknown templateUrl: './autocomplete.component.html', styleUrls: ['./autocomplete.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + MatIconModule, + PopupAlertComponent, + MatAutocompleteModule, + CommonModule, + TranslateModule, + ReactiveFormsModule, + ], }) export class AutocompleteComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges @@ -49,6 +62,8 @@ export class AutocompleteComponent @Input() value?: AutocompleteItem @Input() clearOnSelection = false @Input() autoFocus = false + @Input() minCharacterCount? = 3 + @Input() allowSubmit = true @Output() itemSelected = new EventEmitter() @Output() inputSubmitted = new EventEmitter() @Output() inputCleared = new EventEmitter() @@ -57,22 +72,28 @@ export class AutocompleteComponent @ViewChild('searchInput') inputRef: ElementRef searching: boolean - suggestions$: Observable control = new UntypedFormControl() - subscription = new Subscription() cancelEnter = true selectionSubject = new ReplaySubject(1) lastInputValue$ = new ReplaySubject(1) error: string | null = null + suggestions$: Observable + subscription = new Subscription() - @Input() displayWithFn: (AutocompleteItem) => string = (item) => item + @Input() displayWithFn: (item: AutocompleteItem) => string = (item) => + item.toString() + + displayWithFnInternal = (item?: AutocompleteItem) => { + if (item === null || item === undefined) return null + return this.displayWithFn(item) + } constructor(private cdRef: ChangeDetectorRef) {} ngOnChanges(changes: SimpleChanges): void { const { value } = changes if (value) { - const previousTextValue = this.displayWithFn(value.previousValue) - const currentTextValue = this.displayWithFn(value.currentValue) + const previousTextValue = this.displayWithFnInternal(value.previousValue) + const currentTextValue = this.displayWithFnInternal(value.currentValue) if (previousTextValue !== currentTextValue) { this.updateInputValue(value.currentValue) } @@ -80,20 +101,33 @@ export class AutocompleteComponent } ngOnInit(): void { - this.suggestions$ = merge( + const newValue$ = merge( + of(''), + this.inputCleared.pipe(map(() => '')), this.control.valueChanges.pipe( filter((value) => typeof value === 'string'), - filter((value: string) => value.length > 2), - debounceTime(400), distinctUntilChanged(), - tap(() => (this.searching = true)) - ), - this.control.valueChanges.pipe( - filter((value) => typeof value === 'object' && value.title), - map((item) => item.title) + debounceTime(400) ) + ) + + const externalValueChange$ = this.control.valueChanges.pipe( + filter((value) => typeof value === 'object' && value.title), + map((item) => item.title) + ) + + // this observable emits arrays of suggestions loaded using the given action + const suggestionsFromAction = merge( + newValue$.pipe( + filter((value: string) => value.length >= this.minCharacterCount) + ), + externalValueChange$ ).pipe( - switchMap((value) => (value ? this.action(value) : of([]))), + tap(() => { + this.searching = true + this.error = null + }), + switchMap((value) => this.action(value)), catchError((error: Error) => { this.error = error.message return of([]) @@ -101,11 +135,32 @@ export class AutocompleteComponent finalize(() => (this.searching = false)) ) - this.subscription = this.control.valueChanges.subscribe((any) => { - if (any !== '') { - this.cancelEnter = false - } - }) + this.suggestions$ = merge( + suggestionsFromAction, + // if a new value is under the min char count, clear suggestions + newValue$.pipe( + filter((value: string) => value.length < this.minCharacterCount), + map(() => []) + ) + ) + + // close the panel whenever suggestions are cleared + this.subscription.add( + this.suggestions$ + .pipe(filter((suggestions) => suggestions.length === 0)) + .subscribe(() => { + this.triggerRef?.closePanel() + }) + ) + + this.subscription.add( + this.control.valueChanges.subscribe((any) => { + if (any !== '') { + this.cancelEnter = false + } + }) + ) + this.control.valueChanges .pipe(filter((value) => typeof value === 'string')) .subscribe(this.lastInputValue$) @@ -120,7 +175,7 @@ export class AutocompleteComponent } ngOnDestroy(): void { - this.subscription.unsubscribe() + this.subscription?.unsubscribe() } updateInputValue(value: AutocompleteItem) { @@ -139,19 +194,16 @@ export class AutocompleteComponent .pipe(take(1)) .subscribe((selection) => selection && selection.option.deselect()) this.inputRef.nativeElement.focus() - this.triggerRef.closePanel() } handleEnter(any: string) { - if (!this.cancelEnter) { + if (!this.cancelEnter && this.allowSubmit) { this.inputSubmitted.emit(any) - this.triggerRef.closePanel() } } handleClickSearch() { this.inputSubmitted.emit(this.inputRef.nativeElement.value) - this.triggerRef.closePanel() } handleSelection(event: MatAutocompleteSelectedEvent) { diff --git a/libs/ui/inputs/src/lib/badge/badge.component.css b/libs/ui/inputs/src/lib/badge/badge.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/inputs/src/lib/badge/badge.component.html b/libs/ui/inputs/src/lib/badge/badge.component.html new file mode 100644 index 0000000000..dc6b927a4f --- /dev/null +++ b/libs/ui/inputs/src/lib/badge/badge.component.html @@ -0,0 +1,18 @@ +
+ + + close + +
diff --git a/libs/ui/widgets/src/lib/badge/badge.component.spec.ts b/libs/ui/inputs/src/lib/badge/badge.component.spec.ts similarity index 93% rename from libs/ui/widgets/src/lib/badge/badge.component.spec.ts rename to libs/ui/inputs/src/lib/badge/badge.component.spec.ts index 762877de4a..69d7f36eab 100644 --- a/libs/ui/widgets/src/lib/badge/badge.component.spec.ts +++ b/libs/ui/inputs/src/lib/badge/badge.component.spec.ts @@ -8,7 +8,7 @@ describe('BadgeComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [BadgeComponent], + imports: [BadgeComponent], }).compileComponents() }) diff --git a/libs/ui/inputs/src/lib/badge/badge.component.stories.ts b/libs/ui/inputs/src/lib/badge/badge.component.stories.ts new file mode 100644 index 0000000000..980e241e59 --- /dev/null +++ b/libs/ui/inputs/src/lib/badge/badge.component.stories.ts @@ -0,0 +1,49 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { BadgeComponent } from './badge.component' +import { MatIconModule } from '@angular/material/icon' + +export default { + title: 'Widgets/BadgeComponent', + component: BadgeComponent, + decorators: [ + moduleMetadata({ + imports: [MatIconModule], + }), + ], +} as Meta + +interface BadgeComponentContent extends Partial { + content: string +} + +export const Primary = ( + args: BadgeComponentContent & { removable: boolean } +) => ({ + props: args, + template: `
+ + {{ content }} + + + with an icon downloading + + + pest_control larger (with css) + + + different waves shape + + + different corners + + + different colors + +
`, +}) + +Primary.args = { + clickable: false, + content: 'My custom badge', + removable: false, +} diff --git a/libs/ui/inputs/src/lib/badge/badge.component.ts b/libs/ui/inputs/src/lib/badge/badge.component.ts new file mode 100644 index 0000000000..ff0abc4719 --- /dev/null +++ b/libs/ui/inputs/src/lib/badge/badge.component.ts @@ -0,0 +1,27 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { ButtonComponent } from '../button/button.component' + +@Component({ + selector: 'gn-ui-badge', + templateUrl: './badge.component.html', + styleUrls: ['./badge.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, ButtonComponent], +}) +export class BadgeComponent { + @Input() clickable? = false + @Input() removable? = false + @Output() badgeRemoveClicked = new EventEmitter() + + removeBadge() { + this.badgeRemoveClicked.emit() + } +} diff --git a/libs/ui/inputs/src/lib/date-picker/date-picker.component.html b/libs/ui/inputs/src/lib/date-picker/date-picker.component.html index e7f12888b0..50cae5bb1d 100644 --- a/libs/ui/inputs/src/lib/date-picker/date-picker.component.html +++ b/libs/ui/inputs/src/lib/date-picker/date-picker.component.html @@ -1,7 +1,8 @@
-
+
calendar_today -
+ diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts index 0b50d66a21..ddb4279cfe 100644 --- a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts @@ -1,5 +1,4 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MatDatepickerInputEvent } from '@angular/material/datepicker' import { DateRangePickerComponent } from './date-range-picker.component' describe('DateRangePickerComponent', () => { @@ -19,20 +18,4 @@ describe('DateRangePickerComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) - - it('should set start date on startDateSelected', () => { - const event = { - value: new Date('2023-01-01'), - } as MatDatepickerInputEvent - component.startDateSelected(event) - expect(component.startDate).toEqual(new Date('2023-01-01')) - }) - - it('should set end date on endDateSelected', () => { - const event = { - value: new Date('2023-01-31'), - } as MatDatepickerInputEvent - component.endDateSelected(event) - expect(component.endDate).toEqual(new Date('2023-01-31')) - }) }) diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.stories.ts b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.stories.ts index 107dea5289..86848313e4 100644 --- a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.stories.ts +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.stories.ts @@ -13,6 +13,25 @@ export default { ], } as Meta -export const Primary: StoryObj = { +export const StartEnd: StoryObj = { + args: { + startDate: new Date(), + endDate: new Date(), + }, +} + +export const Start: StoryObj = { + args: { + startDate: new Date(), + }, +} + +export const End: StoryObj = { + args: { + endDate: new Date(), + }, +} + +export const Neither: StoryObj = { args: {}, } diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts index d0032fb5ba..69e2f03250 100644 --- a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts @@ -1,27 +1,25 @@ -import { Component } from '@angular/core' -import { MatNativeDateModule } from '@angular/material/core' import { - MatDatepickerInputEvent, - MatDatepickerModule, -} from '@angular/material/datepicker' + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { MatNativeDateModule } from '@angular/material/core' +import { MatDatepickerModule } from '@angular/material/datepicker' import { MatIconModule } from '@angular/material/icon' @Component({ selector: 'gn-ui-date-range-picker', templateUrl: './date-range-picker.component.html', styleUrls: ['./date-range-picker.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [MatIconModule, MatNativeDateModule, MatDatepickerModule], }) export class DateRangePickerComponent { - startDate: Date - endDate: Date - - startDateSelected(event: MatDatepickerInputEvent) { - this.startDate = event.value - } - - endDateSelected(event: MatDatepickerInputEvent) { - this.endDate = event.value - } + @Input() startDate: Date + @Input() endDate: Date + @Output() startDateChange = new EventEmitter() + @Output() endDateChange = new EventEmitter() } diff --git a/libs/ui/inputs/src/lib/ui-inputs.module.ts b/libs/ui/inputs/src/lib/ui-inputs.module.ts index db239ac117..8f2e7b92bd 100644 --- a/libs/ui/inputs/src/lib/ui-inputs.module.ts +++ b/libs/ui/inputs/src/lib/ui-inputs.module.ts @@ -4,8 +4,8 @@ import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { TranslateModule } from '@ngx-translate/core' import { TagInputModule } from 'ngx-chips' import { NgxDropzoneModule } from 'ngx-dropzone' -import { AutocompleteComponent } from './autocomplete/autocomplete.component' import { ButtonComponent } from './button/button.component' +import { BadgeComponent } from './badge/badge.component' import { ChipsInputComponent } from './chips-input/chips-input.component' import { DragAndDropFileInputComponent } from './drag-and-drop-file-input/drag-and-drop-file-input.component' import { DropdownSelectorComponent } from './dropdown-selector/dropdown-selector.component' @@ -36,7 +36,6 @@ import { ImageInputComponent } from './image-input/image-input.component' @NgModule({ declarations: [ - AutocompleteComponent, TextInputComponent, DragAndDropFileInputComponent, ChipsInputComponent, @@ -73,10 +72,10 @@ import { ImageInputComponent } from './image-input/image-input.component' DropdownSelectorComponent, DateRangePickerComponent, CheckToggleComponent, + BadgeComponent, ], exports: [ DropdownSelectorComponent, - AutocompleteComponent, ButtonComponent, TextInputComponent, DragAndDropFileInputComponent, @@ -93,6 +92,7 @@ import { ImageInputComponent } from './image-input/image-input.component' DateRangePickerComponent, EditableLabelDirective, ImageInputComponent, + BadgeComponent, ], }) export class UiInputsModule {} diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts index 358c081b9f..ee72bfe6cb 100644 --- a/libs/ui/layout/src/index.ts +++ b/libs/ui/layout/src/index.ts @@ -3,6 +3,7 @@ export * from './lib/carousel/carousel.component' export * from './lib/expandable-panel-button/expandable-panel-button.component' export * from './lib/expandable-panel/expandable-panel.component' export * from './lib/form-field-wrapper/form-field-wrapper.component' +export * from './lib/max-lines/max-lines.component' export * from './lib/interactive-table/interactive-table-column/interactive-table-column.component' export * from './lib/interactive-table/interactive-table.component' export * from './lib/sticky-header/sticky-header.component' diff --git a/libs/ui/layout/src/lib/max-lines/max-lines.component.css b/libs/ui/layout/src/lib/max-lines/max-lines.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.html b/libs/ui/layout/src/lib/max-lines/max-lines.component.html similarity index 100% rename from libs/ui/elements/src/lib/max-lines/max-lines.component.html rename to libs/ui/layout/src/lib/max-lines/max-lines.component.html diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.spec.ts b/libs/ui/layout/src/lib/max-lines/max-lines.component.spec.ts similarity index 91% rename from libs/ui/elements/src/lib/max-lines/max-lines.component.spec.ts rename to libs/ui/layout/src/lib/max-lines/max-lines.component.spec.ts index dc28d6869c..075b5aae7c 100644 --- a/libs/ui/elements/src/lib/max-lines/max-lines.component.spec.ts +++ b/libs/ui/layout/src/lib/max-lines/max-lines.component.spec.ts @@ -4,6 +4,13 @@ import { MaxLinesComponent } from './max-lines.component' import { Component, importProvidersFrom } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' +// Mock implementation of ResizeObserver +class ResizeObserverMock { + observe = jest.fn() + unobserve = jest.fn() + disconnect = jest.fn() +} + @Component({ template: ` @@ -22,10 +29,12 @@ describe('MaxLinesComponent', () => { let maxLinesComponent: MaxLinesComponent beforeEach(() => { + ;(window as any).ResizeObserver = ResizeObserverMock + TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), MaxLinesComponent], providers: [importProvidersFrom(TranslateModule.forRoot())], - declarations: [MaxLinesComponent, TestHostComponent], + declarations: [TestHostComponent], }) fixture = TestBed.createComponent(TestHostComponent) hostComponent = fixture.componentInstance diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.stories.ts b/libs/ui/layout/src/lib/max-lines/max-lines.component.stories.ts similarity index 95% rename from libs/ui/elements/src/lib/max-lines/max-lines.component.stories.ts rename to libs/ui/layout/src/lib/max-lines/max-lines.component.stories.ts index 6da53e4f6f..1a811f97ab 100644 --- a/libs/ui/elements/src/lib/max-lines/max-lines.component.stories.ts +++ b/libs/ui/layout/src/lib/max-lines/max-lines.component.stories.ts @@ -14,12 +14,12 @@ import { import { importProvidersFrom } from '@angular/core' export default { - title: 'Elements/MaxLinesComponent', + title: 'Layout/MaxLinesComponent', component: MaxLinesComponent, decorators: [ moduleMetadata({ - declarations: [MaxLinesComponent], - imports: [TranslateModule], + declarations: [], + imports: [TranslateModule, MaxLinesComponent], }), applicationConfig({ providers: [ diff --git a/libs/ui/elements/src/lib/max-lines/max-lines.component.ts b/libs/ui/layout/src/lib/max-lines/max-lines.component.ts similarity index 93% rename from libs/ui/elements/src/lib/max-lines/max-lines.component.ts rename to libs/ui/layout/src/lib/max-lines/max-lines.component.ts index 1d47058644..702781f902 100644 --- a/libs/ui/elements/src/lib/max-lines/max-lines.component.ts +++ b/libs/ui/layout/src/lib/max-lines/max-lines.component.ts @@ -1,19 +1,23 @@ import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, - Input, ElementRef, - ChangeDetectionStrategy, - AfterViewInit, - ViewChild, + Input, OnDestroy, - ChangeDetectorRef, + ViewChild, } from '@angular/core' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-max-lines', templateUrl: './max-lines.component.html', styleUrls: ['./max-lines.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, TranslateModule], }) export class MaxLinesComponent implements AfterViewInit, OnDestroy { @Input() maxLines = 6 diff --git a/libs/ui/layout/src/lib/ui-layout.module.ts b/libs/ui/layout/src/lib/ui-layout.module.ts index 5d22fc9ce9..4cc50c09b6 100644 --- a/libs/ui/layout/src/lib/ui-layout.module.ts +++ b/libs/ui/layout/src/lib/ui-layout.module.ts @@ -6,7 +6,6 @@ import { StickyHeaderComponent } from './sticky-header/sticky-header.component' import { AnchorLinkDirective } from './anchor-link/anchor-link.directive' import { ExpandablePanelButtonComponent } from './expandable-panel-button/expandable-panel-button.component' import { MatIconModule } from '@angular/material/icon' -import { CarouselComponent } from './carousel/carousel.component' @NgModule({ imports: [CommonModule, MatIconModule, TranslateModule.forChild()], diff --git a/libs/ui/search/src/index.ts b/libs/ui/search/src/index.ts index 9d2763fafd..b35e869c35 100644 --- a/libs/ui/search/src/index.ts +++ b/libs/ui/search/src/index.ts @@ -17,3 +17,4 @@ export * from './lib/record-preview-title/record-preview-title.component' export * from './lib/record-metric/record-metric.component' export * from './lib/results-list-item/results-list-item.component' export * from './lib/results-hits-number/results-hits-number.component' +export * from './lib/results-table/results-table.component' diff --git a/libs/ui/search/src/lib/results-table/results-table.component.css b/libs/ui/search/src/lib/results-table/results-table.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/search/src/lib/results-table/results-table.component.html b/libs/ui/search/src/lib/results-table/results-table.component.html similarity index 80% rename from libs/feature/search/src/lib/results-table/results-table.component.html rename to libs/ui/search/src/lib/results-table/results-table.component.html index 8d157ef040..1b0e3d1225 100644 --- a/libs/feature/search/src/lib/results-table/results-table.component.html +++ b/libs/ui/search/src/lib/results-table/results-table.component.html @@ -1,13 +1,13 @@ record.metadata.title - {{ item.title }} +
+ {{ item.title }} + + dashboard.records.hasDraft + +
@@ -74,7 +85,7 @@ @@ -99,7 +110,7 @@ diff --git a/libs/ui/search/src/lib/results-table/results-table.component.spec.ts b/libs/ui/search/src/lib/results-table/results-table.component.spec.ts new file mode 100644 index 0000000000..d940a4a614 --- /dev/null +++ b/libs/ui/search/src/lib/results-table/results-table.component.spec.ts @@ -0,0 +1,152 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { ResultsTableComponent } from './results-table.component' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { By } from '@angular/platform-browser' +import { TranslateModule } from '@ngx-translate/core' + +describe('ResultsTableComponent', () => { + let component: ResultsTableComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(ResultsTableComponent) + component = fixture.componentInstance + component.records = DATASET_RECORDS + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('get a list of formats and sorts them depending on priority', () => { + it('returns a list of unique formats', () => { + expect(component.getRecordFormats(DATASET_RECORDS[0])).toEqual([ + 'geojson', + 'shp', + 'pdf', + ]) + }) + }) + describe('get the badge color for given format', () => { + it('returns the color for its format', () => { + expect( + component.getBadgeColor( + component.getRecordFormats(DATASET_RECORDS[0])[0] + ) + ).toEqual('#1e5180') // geojson + }) + }) + + describe('sorting', () => { + describe('#isSortedBy', () => { + it('returns null if not sorted by this column', () => { + component.sortOrder = ['desc', 'owner'] + const sort = component.isSortedBy('title') + expect(sort).toBe(null) + }) + it('returns the sort order if the current sortBy is for this column', () => { + component.sortOrder = ['desc', 'title'] + const sort = component.isSortedBy('title') + expect(sort).toBe('desc') + }) + it('returns true if the current sortBy is for this column (multiple sorts)', () => { + component.sortOrder = [ + ['asc', 'score'], + ['desc', 'title'], + ] + expect(component.isSortedBy('title')).toBe('desc') + expect(component.isSortedBy('score')).toBe('asc') + expect(component.isSortedBy('owner')).toBe(null) + }) + }) + }) + + describe('selection', () => { + beforeEach(() => { + component.records = [ + { + uniqueIdentifier: '1', + }, + { + uniqueIdentifier: '2', + }, + { + uniqueIdentifier: '3', + }, + ] as any + }) + + describe('#isChecked', () => { + it('should return true when the record is in the selectedRecords array', () => { + component.selectedRecordsIdentifiers = ['1', '2'] + const record = { uniqueIdentifier: '2' } as CatalogRecord + expect(component.isChecked(record)).toBe(true) + }) + + it('should return false when the record is not in the selectedRecords array', () => { + component.selectedRecordsIdentifiers = ['1', '2', '3'] + const record = { uniqueIdentifier: '4' } as CatalogRecord + expect(component.isChecked(record)).toBe(false) + }) + }) + + describe('#handleRecordSelectedChange', () => { + it('should call selectRecords when checkbox is clicked', () => { + const record = { uniqueIdentifier: '1' } + let emitted = null + component.recordsSelectedChange.subscribe((e) => (emitted = e)) + component.handleRecordSelectedChange(true, record as CatalogRecord) + expect(emitted).toEqual([[record], true]) + }) + }) + + describe('#isAllSelected', () => { + it('returns true if all records in the page are selected', () => { + component.selectedRecordsIdentifiers = ['1', '2', '3', '4', '5'] + expect(component.isAllSelected()).toBe(true) + }) + it('returns false otherwise', () => { + component.selectedRecordsIdentifiers = ['1'] + expect(component.isAllSelected()).toBe(false) + }) + }) + + describe('#isSomeSelected', () => { + it('returns false if all records in the page are selected', () => { + component.selectedRecordsIdentifiers = ['1', '2', '3', '4', '5'] + expect(component.isSomeSelected()).toBe(false) + }) + it('returns true if one or more records in the page is selected', () => { + component.selectedRecordsIdentifiers = ['2', '3'] + expect(component.isSomeSelected()).toBe(true) + }) + it('returns false if no record in the page is selected', () => { + component.selectedRecordsIdentifiers = ['4', '5'] + expect(component.isSomeSelected()).toBe(false) + }) + }) + }) + + describe('clicking on a dataset', () => { + let clickedRecord: CatalogRecord + + beforeEach(() => { + clickedRecord = null + component.recordClick.subscribe((r) => (clickedRecord = r)) + }) + + it('emits a recordClick event', () => { + const tableRow = fixture.debugElement.queryAll( + By.css('.table-row-cell') + )[1].nativeElement as HTMLDivElement + tableRow.parentElement.click() + expect(clickedRecord).toEqual(DATASET_RECORDS[0]) + }) + }) +}) diff --git a/libs/ui/search/src/lib/results-table/results-table.component.ts b/libs/ui/search/src/lib/results-table/results-table.component.ts new file mode 100644 index 0000000000..6752d70fdc --- /dev/null +++ b/libs/ui/search/src/lib/results-table/results-table.component.ts @@ -0,0 +1,138 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { + FileFormat, + getBadgeColor, + getFileFormat, + getFormatPriority, +} from '@geonetwork-ui/util/shared' +import { BadgeComponent, UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { + InteractiveTableColumnComponent, + InteractiveTableComponent, +} from '@geonetwork-ui/ui/layout' +import { MatIconModule } from '@angular/material/icon' +import { TranslateModule } from '@ngx-translate/core' +import { CommonModule } from '@angular/common' +import { + FieldSort, + SortByField, +} from '@geonetwork-ui/common/domain/model/search' + +@Component({ + selector: 'gn-ui-results-table', + templateUrl: './results-table.component.html', + styleUrls: ['./results-table.component.css'], + standalone: true, + imports: [ + CommonModule, + UiInputsModule, + InteractiveTableComponent, + InteractiveTableColumnComponent, + MatIconModule, + TranslateModule, + BadgeComponent, + ], +}) +export class ResultsTableComponent { + @Input() records: CatalogRecord[] = [] + @Input() selectedRecordsIdentifiers: string[] = [] + @Input() sortOrder: SortByField = null + @Input() recordHasDraft: (record: CatalogRecord) => boolean = () => false + + // emits the column (field) as well as the order + @Output() sortByChange = new EventEmitter<[string, 'asc' | 'desc']>() + @Output() recordClick = new EventEmitter() + @Output() recordsSelectedChange = new EventEmitter< + [CatalogRecord[], boolean] + >() + + dateToString(date: Date): string { + return date?.toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }) + } + + getStatus(isPublishedToAll: boolean | unknown) { + return isPublishedToAll ? 'published' : 'not published' + } + + getRecordFormats(record: CatalogRecord): FileFormat[] { + if (record.kind === 'service' || !('distributions' in record)) { + return [] + } + const formats = Array.from( + new Set( + record.distributions.map((distribution) => getFileFormat(distribution)) + ) + ).filter((format) => !!format) + formats.sort((a, b) => getFormatPriority(b) - getFormatPriority(a)) + return formats + } + + formatUserInfo(userInfo: string | unknown): string { + const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') + if (infos && infos.length === 4) { + return `${infos[2]} ${infos[1]}` + } + return undefined + } + + getBadgeColor(format: FileFormat): string { + return getBadgeColor(format) + } + + handleRecordClick(item: unknown) { + this.recordClick.emit(item as CatalogRecord) + } + + setSortBy(col: string, order: 'asc' | 'desc') { + this.sortByChange.emit([col, order]) + } + + isSortedBy(col: string): 'desc' | 'asc' | null { + if (!this.sortOrder) { + return null + } + const sortArray = Array.isArray(this.sortOrder[0]) + ? (this.sortOrder as FieldSort[]) + : ([this.sortOrder] as FieldSort[]) + for (const sort of sortArray) { + if (sort[1] === col) { + return sort[0] + } + } + return null + } + + isChecked(record: CatalogRecord): boolean { + return this.selectedRecordsIdentifiers.includes(record.uniqueIdentifier) + } + + handleRecordSelectedChange(selected: boolean, record: CatalogRecord) { + this.recordsSelectedChange.emit([[record], selected]) + } + + async toggleSelectAll() { + this.recordsSelectedChange.emit([this.records, !this.isAllSelected()]) + } + + isAllSelected(): boolean { + return this.records.every((record) => + this.selectedRecordsIdentifiers.includes(record.uniqueIdentifier) + ) + } + + isSomeSelected(): boolean { + const allSelected = this.records.every((record) => + this.selectedRecordsIdentifiers.includes(record.uniqueIdentifier) + ) + const someSelected = this.records.some((record) => + this.selectedRecordsIdentifiers.includes(record.uniqueIdentifier) + ) + return !allSelected && someSelected + } +} diff --git a/libs/ui/widgets/src/index.ts b/libs/ui/widgets/src/index.ts index a592ee2418..26b0aef91e 100644 --- a/libs/ui/widgets/src/index.ts +++ b/libs/ui/widgets/src/index.ts @@ -1,7 +1,6 @@ export * from './lib/ui-widgets.module' export * from './lib/progress-bar/progress-bar.component' export * from './lib/loading-mask/loading-mask.component' -export * from './lib/badge/badge.component' export * from './lib/color-scale/color-scale.component' export * from './lib/popup-alert/popup-alert.component' export * from './lib/spinning-loader/spinning-loader.component' diff --git a/libs/ui/widgets/src/lib/badge/badge.component.html b/libs/ui/widgets/src/lib/badge/badge.component.html deleted file mode 100644 index 8c439407a0..0000000000 --- a/libs/ui/widgets/src/lib/badge/badge.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
- -
diff --git a/libs/ui/widgets/src/lib/badge/badge.component.stories.ts b/libs/ui/widgets/src/lib/badge/badge.component.stories.ts deleted file mode 100644 index b8ad87d651..0000000000 --- a/libs/ui/widgets/src/lib/badge/badge.component.stories.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { componentWrapperDecorator, Meta, StoryObj } from '@storybook/angular' -import { BadgeComponent } from './badge.component' - -export default { - title: 'Widgets/BadgeComponent', - component: BadgeComponent, - decorators: [componentWrapperDecorator(BadgeComponent)], -} as Meta - -interface BadgeComponentContent extends Partial { - content: string -} - -export const Primary: StoryObj = { - args: { - clickable: true, - content: 'My custom badge', - }, - render: (args) => ({ - props: args, - template: `{{content}}`, - }), -} diff --git a/libs/ui/widgets/src/lib/badge/badge.component.ts b/libs/ui/widgets/src/lib/badge/badge.component.ts deleted file mode 100644 index 8d78ac1b34..0000000000 --- a/libs/ui/widgets/src/lib/badge/badge.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' - -@Component({ - selector: 'gn-ui-badge', - templateUrl: './badge.component.html', - styleUrls: ['./badge.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class BadgeComponent { - @Input() clickable? = false -} diff --git a/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.spec.ts b/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.spec.ts index e78385f0f1..ec3bc09709 100644 --- a/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.spec.ts +++ b/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.spec.ts @@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { PopupAlertComponent } from './popup-alert.component' import { Component, Input } from '@angular/core' import { By } from '@angular/platform-browser' -import { MatIconModule } from '@angular/material/icon' @Component({ template: '{{message}}', @@ -19,8 +18,8 @@ describe('PopupAlertComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [PopupAlertComponent, PopupAlertWrapperComponent], - imports: [MatIconModule], + declarations: [PopupAlertWrapperComponent], + imports: [PopupAlertComponent], }).compileComponents() }) diff --git a/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.stories.ts b/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.stories.ts index 500dd23f0c..fd39620cbf 100644 --- a/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.stories.ts +++ b/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.stories.ts @@ -1,19 +1,10 @@ -import { - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' +import { componentWrapperDecorator, Meta, StoryObj } from '@storybook/angular' import { PopupAlertComponent } from './popup-alert.component' -import { MatIconModule } from '@angular/material/icon' export default { title: 'Widgets/PopupAlertComponent', component: PopupAlertComponent, decorators: [ - moduleMetadata({ - imports: [MatIconModule], - }), componentWrapperDecorator( (story) => `
@@ -46,6 +37,6 @@ export const Primary: StoryObj = { }, render: (args) => ({ props: args, - template: `${content}`, + template: `${content}`, }), } diff --git a/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.ts b/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.ts index a77c831996..6c3e6c3345 100644 --- a/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.ts +++ b/libs/ui/widgets/src/lib/popup-alert/popup-alert.component.ts @@ -7,12 +7,16 @@ import { OnInit, ViewChild, } from '@angular/core' +import { CommonModule } from '@angular/common' +import { MatIconModule } from '@angular/material/icon' @Component({ selector: 'gn-ui-popup-alert', templateUrl: './popup-alert.component.html', styleUrls: ['./popup-alert.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, MatIconModule], }) export class PopupAlertComponent implements OnInit { @Input() icon: string diff --git a/libs/ui/widgets/src/lib/ui-widgets.module.ts b/libs/ui/widgets/src/lib/ui-widgets.module.ts index eda41bebf8..726025db7c 100644 --- a/libs/ui/widgets/src/lib/ui-widgets.module.ts +++ b/libs/ui/widgets/src/lib/ui-widgets.module.ts @@ -9,8 +9,6 @@ import { TagInputModule } from 'ngx-chips' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { LoadingMaskComponent } from './loading-mask/loading-mask.component' import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' -import { PopupAlertComponent } from './popup-alert/popup-alert.component' -import { BadgeComponent } from './badge/badge.component' import { MatIconModule } from '@angular/material/icon' import { SpinningLoaderComponent } from './spinning-loader/spinning-loader.component' import { CommonModule } from '@angular/common' @@ -21,8 +19,6 @@ import { CommonModule } from '@angular/common' ProgressBarComponent, StepBarComponent, LoadingMaskComponent, - PopupAlertComponent, - BadgeComponent, SpinningLoaderComponent, ], imports: [ @@ -40,8 +36,6 @@ import { CommonModule } from '@angular/common' ProgressBarComponent, StepBarComponent, LoadingMaskComponent, - PopupAlertComponent, - BadgeComponent, SpinningLoaderComponent, ], }) diff --git a/package-lock.json b/package-lock.json index 9a2021b77b..54f31af865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,26 @@ { "name": "geonetwork-ui", - "version": "2.3.0-dev", + "version": "2.4.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "geonetwork-ui", - "version": "2.3.0-dev", - "dependencies": { - "@angular/animations": "16.1.7", - "@angular/cdk": "16.0.4", - "@angular/common": "16.1.7", - "@angular/compiler": "16.1.7", - "@angular/core": "16.1.7", - "@angular/elements": "16.1.7", - "@angular/forms": "16.1.7", - "@angular/material": "16.0.4", - "@angular/material-moment-adapter": "16.0.4", - "@angular/platform-browser": "16.1.7", - "@angular/platform-browser-dynamic": "16.1.7", - "@angular/platform-server": "16.1.7", - "@angular/router": "16.1.7", + "version": "2.4.0-dev", + "dependencies": { + "@angular/animations": "^16.2", + "@angular/cdk": "^16.2", + "@angular/common": "^16.2", + "@angular/compiler": "^16.2", + "@angular/core": "^16.2", + "@angular/elements": "^16.2", + "@angular/forms": "^16.2", + "@angular/material": "^16.2", + "@angular/material-moment-adapter": "^16.2", + "@angular/platform-browser": "^16.2", + "@angular/platform-browser-dynamic": "^16.2", + "@angular/platform-server": "^16.2", + "@angular/router": "^16.2", "@bartholomej/ngx-translate-extract": "^8.0.2", "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@camptocamp/ogc-client": "1.1.1-dev.ad6d9ab", @@ -72,15 +72,15 @@ "zone.js": "0.13.0" }, "devDependencies": { - "@angular-devkit/build-angular": "16.2.12", - "@angular-devkit/core": "16.1.6", - "@angular-devkit/schematics": "16.1.6", - "@angular-eslint/eslint-plugin": "16.0.3", - "@angular-eslint/eslint-plugin-template": "16.0.3", - "@angular-eslint/template-parser": "16.0.3", - "@angular/cli": "~16.1.0", - "@angular/compiler-cli": "16.1.7", - "@angular/language-service": "16.1.7", + "@angular-devkit/build-angular": "^16.2", + "@angular-devkit/core": "^16.2", + "@angular-devkit/schematics": "^16.2", + "@angular-eslint/eslint-plugin": "^16.2", + "@angular-eslint/eslint-plugin-template": "^16.2", + "@angular-eslint/template-parser": "^16.2", + "@angular/cli": "^16.2", + "@angular/compiler-cli": "^16.2", + "@angular/language-service": "^16.2", "@nestjs/schematics": "10.0.1", "@nestjs/testing": "10.1.3", "@ngrx/component": "16.0.1", @@ -100,7 +100,7 @@ "@nx/webpack": "16.6.0", "@nx/workspace": "16.6.0", "@openapitools/openapi-generator-cli": "^2.6.0", - "@schematics/angular": "16.1.6", + "@schematics/angular": "^16.2", "@storybook/addon-essentials": "7.2.1", "@storybook/angular": "7.2.1", "@types/chroma-js": "^2.1.3", @@ -359,12 +359,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1601.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.8.tgz", - "integrity": "sha512-kOXVGwsQnZvtz2UZNefcEy64Jiwq0eSoQUeozvDXOaYRJABLjPKI2YaarvKC9/Z1SGLuje0o/eRJO4T8aRk9rQ==", + "version": "0.1602.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.14.tgz", + "integrity": "sha512-eSdONEV5dbtLNiOMBy9Ue9DdJ1ct6dH9RdZfYiedq6VZn0lejePAjY36MYVXgq2jTE+v/uIiaNy7caea5pt55A==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.8", + "@angular-devkit/core": "16.2.14", "rxjs": "7.8.1" }, "engines": { @@ -373,32 +373,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", - "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "node_modules/@angular-devkit/build-angular": { "version": "16.2.12", "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.2.12.tgz", @@ -997,13 +971,14 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.6.tgz", - "integrity": "sha512-3OjtrPWvsqVkMBwqPeE65ccCIw56FooNpVVAJ0XwhVQv5mA81pmbCzU7JsR6U449ZT7O4cQblzZMQvWvx74HCg==", + "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==", "dependencies": { "ajv": "8.12.0", "ajv-formats": "2.1.1", "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", "rxjs": "7.8.1", "source-map": "0.7.4" }, @@ -1022,13 +997,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.6.tgz", - "integrity": "sha512-KA8P78gaS76HMHGBOM8JHJXWLOxCIShYVB2Un/Cu6z3jVODvXq+ILZUc1Y0RsAce/vsl2wf8qpoh5Lku9KJHUQ==", + "version": "16.2.14", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.14.tgz", + "integrity": "sha512-B6LQKInCT8w5zx5Pbroext5eFFRTCJdTwHN8GhcVS8IeKCnkeqVTQLjB4lBUg7LEm8Y7UHXwzrVxmk+f+MBXhw==", "dependencies": { - "@angular-devkit/core": "16.1.6", + "@angular-devkit/core": "16.2.14", "jsonc-parser": "3.2.0", - "magic-string": "0.30.0", + "magic-string": "0.30.1", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -1038,20 +1013,31 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/magic-string": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", + "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.0.3.tgz", - "integrity": "sha512-8zwY6ustiPXBEF3+jELKVwGk6j2HJn7GHbqAhDFR02YiE27iRMSGTHIAWGs6ZI7F1JgfrIsOHrUgzC1x95K6rg==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.3.1.tgz", + "integrity": "sha512-m4WP1xwS9XLcC/3n6lIcG5HZoai/5eb5W3xm48GVcv//0qE2p7S96RSgKPgGHvif5pF8O9xAqEWs3gDEG45+7A==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.0.3.tgz", - "integrity": "sha512-1c+dFytcQDOA2wJ8/rtydMV6UYq1BgVfOcBXOr0WJxC9g8Cad9czcUOkW41WGrTp5kICMliV0ypH5eEaCM2WDQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-16.3.1.tgz", + "integrity": "sha512-kSc8ESfoy8TUSthbq0Lpq9e17I+3Smy4rHoNpKCFEGuJgPs0+OssZMxB6a5EawGbv2EKTPEtrxzFm1WsLR0U9Q==", "dev": true, "dependencies": { - "@angular-eslint/utils": "16.0.3", - "@typescript-eslint/utils": "5.59.7" + "@angular-eslint/utils": "16.3.1", + "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -1059,17 +1045,17 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.0.3.tgz", - "integrity": "sha512-OKTMWOjC7F5tdv7gm2tlmgyr/uVyS1RWJZn4X/6D6p0kOpiDXmajtbYHD5tzbshX2Ep62Nt+rg8+1XGHrU0ScA==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.3.1.tgz", + "integrity": "sha512-+RcFEWqNiRt3+5jXvmlIDlXtP9+vjdmgmVL6tt8yDbqdjBOewtyMu4pE4YaR4sFboyxgME9PbO2WrOyPXh6xjg==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.0.3", - "@angular-eslint/utils": "16.0.3", - "@typescript-eslint/type-utils": "5.59.7", - "@typescript-eslint/utils": "5.59.7", - "aria-query": "5.1.3", - "axobject-query": "3.1.1" + "@angular-eslint/bundled-angular-compiler": "16.3.1", + "@angular-eslint/utils": "16.3.1", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "aria-query": "5.3.0", + "axobject-query": "4.0.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -1077,12 +1063,12 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.0.3.tgz", - "integrity": "sha512-IAWdwp/S9QC3EMiVxSS0E3ABy9PSidN3PW0Ll2EtM3mzXMYlpZXmxqd+B1xV/xKWzhk1Mp04QX8hHfG6Vq+qaQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.3.1.tgz", + "integrity": "sha512-9+SxUtxB2iOnm0ldS2ow0stMxe02rB/TxeMIe8fxsLFHZdw8RQvs/p3HLvVHXzv6gUblMHebIb/ubUmwEVb2SA==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.0.3", + "@angular-eslint/bundled-angular-compiler": "16.3.1", "eslint-scope": "^7.0.0" }, "peerDependencies": { @@ -1091,13 +1077,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.0.3.tgz", - "integrity": "sha512-QsbUVHJLk+fE08/D4y3wOyGk1iX2LVSygw+uzilbaAXfjD5/c0Ei5FbVx2mMYPk+aOl4yrvGQW3dmetMiAR0MQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-16.3.1.tgz", + "integrity": "sha512-tEBcce0rG+DmcPO8jhRffUFDioGw3G4cUAE15XlRctY1J3QzOBH9HdUOTDt0mMjBgpWCzh0YVT1Moh2bPXU9Xg==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "16.0.3", - "@typescript-eslint/utils": "5.59.7" + "@angular-eslint/bundled-angular-compiler": "16.3.1", + "@typescript-eslint/utils": "5.62.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -1105,9 +1091,9 @@ } }, "node_modules/@angular/animations": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.1.7.tgz", - "integrity": "sha512-+fMLwUlHLNsHWzX2cnsr4sMyix0R5v/a5srQTQjl6BYhdyqFgT82h5F4P49yFu+nZr0jdsxF012wPJbDRR+1qQ==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz", + "integrity": "sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==", "dependencies": { "tslib": "^2.3.0" }, @@ -1115,13 +1101,13 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.7" + "@angular/core": "16.2.12" } }, "node_modules/@angular/cdk": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.0.4.tgz", - "integrity": "sha512-IIIACQJE6HOKiJo6ZnxZ1B8kSZmUcb7PZOllJhnerQzaJ2qL6tvwhPBTXlj5ekIh8j78VsQWswgr1ooFseYxhg==", + "version": "16.2.14", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.2.14.tgz", + "integrity": "sha512-n6PrGdiVeSTEmM/HEiwIyg6YQUUymZrb5afaNLGFRM5YL0Y8OBqd+XhCjb0OfD/AfgCUtedVEPwNqrfW8KzgGw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1135,15 +1121,15 @@ } }, "node_modules/@angular/cli": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.8.tgz", - "integrity": "sha512-amOIHMq8EvixhnI+do5Bcy6IZSFAJx0njhhLM4ltDuNUczH8VH0hNegZKxhb8K87AMO8jITFM+NLrzccyghsDQ==", + "version": "16.2.14", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.14.tgz", + "integrity": "sha512-0y71jtitigVolm4Rim1b8xPQ+B22cGp4Spef2Wunpqj67UowN6tsZaVuWBEQh4u5xauX8LAHKqsvy37ZPWCc4A==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1601.8", - "@angular-devkit/core": "16.1.8", - "@angular-devkit/schematics": "16.1.8", - "@schematics/angular": "16.1.8", + "@angular-devkit/architect": "0.1602.14", + "@angular-devkit/core": "16.2.14", + "@angular-devkit/schematics": "16.2.14", + "@schematics/angular": "16.2.14", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -1155,7 +1141,7 @@ "ora": "5.4.1", "pacote": "15.2.0", "resolve": "1.22.2", - "semver": "7.5.3", + "semver": "7.5.4", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -1168,103 +1154,10 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", - "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/schematics": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.8.tgz", - "integrity": "sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "16.1.8", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.0", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@schematics/angular": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.8.tgz", - "integrity": "sha512-gTHy1A/E9BCr0sj3VCr6eBYkgVkO96QWiZcFumedGnvstvp5wiCoIoJPLLfYaxVt1vt08xmnmS3OZ3r0qCLdpA==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "16.1.8", - "@angular-devkit/schematics": "16.1.8", - "jsonc-parser": "3.2.0" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@angular/common": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.7.tgz", - "integrity": "sha512-7WwYwtJjuJtUkutB+aMCvtV5zxa43T4x+kqT+kS4KnUmLv5KdrGPxcS+/7YUuKEELWp1SG032UTwGPX0DXxH4g==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.12.tgz", + "integrity": "sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==", "dependencies": { "tslib": "^2.3.0" }, @@ -1272,14 +1165,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.7", + "@angular/core": "16.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.7.tgz", - "integrity": "sha512-93nbMFPSpKNfUyuRvEQxPdYLU6g25oZ4Gp7ewzNLyDHIbTQv6FwsthHfgPigPJJUUyKak6Gr3koFsgk7Dl3LAA==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.2.12.tgz", + "integrity": "sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1287,7 +1180,7 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.7" + "@angular/core": "16.2.12" }, "peerDependenciesMeta": { "@angular/core": { @@ -1296,11 +1189,11 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.7.tgz", - "integrity": "sha512-6iuogfVrbCh6o4hWbNCClsLQdLtlXiaNc72LGz5LMXI0TOwKVlRXhbzhiQeLS0/nsYIdHFbgyr1aepI2wQA3mQ==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz", + "integrity": "sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==", "dependencies": { - "@babel/core": "7.22.5", + "@babel/core": "7.23.2", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^3.0.0", "convert-source-map": "^1.5.1", @@ -1318,30 +1211,30 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/compiler": "16.1.7", + "@angular/compiler": "16.2.12", "typescript": ">=4.9.3 <5.2" } }, "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1351,6 +1244,11 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1359,10 +1257,37 @@ "semver": "bin/semver.js" } }, + "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@angular/core": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.7.tgz", - "integrity": "sha512-Wl5BR9X1xnV7Z9v/MNVANhymuTKAuRv4etr4rRgaC5NbbJSuFM4y+mg4yVI4wmrYJo0gKRcV9+2mHaePr41fTg==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.2.12.tgz", + "integrity": "sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==", "dependencies": { "tslib": "^2.3.0" }, @@ -1375,9 +1300,9 @@ } }, "node_modules/@angular/elements": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-16.1.7.tgz", - "integrity": "sha512-lAFTVLmLnI/W7wNW4W9JMzptcxZF3btmk9Ybffw7QpqMIstYHkTCq9mqXvJQzywGAd6+NGQEZBRC1gYPcZD3Pw==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-16.2.12.tgz", + "integrity": "sha512-x5EPsoh6hikR52yWh4w9E+YEIIsHwPq88dlamTjDbqlShNfMFg9vRRTIGd0xzrSKc0yM/kkXNMzJUMcZAVdycQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1385,14 +1310,14 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/core": "16.1.7", + "@angular/core": "16.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/forms": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.1.7.tgz", - "integrity": "sha512-AZ6oCIAS2JwH7rJiTOj2uKl1eykiDP98y0trgQ/42+zzpOQZyZAjXrtdqHkVUXMc1PFf5NmYioz19Muj1p+Ttg==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.2.12.tgz", + "integrity": "sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1400,78 +1325,78 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.7", - "@angular/core": "16.1.7", - "@angular/platform-browser": "16.1.7", + "@angular/common": "16.2.12", + "@angular/core": "16.2.12", + "@angular/platform-browser": "16.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.1.7.tgz", - "integrity": "sha512-BBqT8ETBu1JtNZQS5Vs8e/Ru5UQKuNf2W4TGsWJVHFKdsjaghryG4NZQPXaYERDjU3k/64dZjcFNgzhP96LlZA==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-16.2.12.tgz", + "integrity": "sha512-sZwB+ZEjChx9EYcqPaS4OnhC/q5RcedZjIdM9mCxuU/MtseURRYRI/8Hnm1RHo9qyc5PmsQpg7p9Vp/5hXLUjw==", "dev": true, "engines": { "node": "^16.14.0 || >=18.10.0" } }, "node_modules/@angular/material": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.0.4.tgz", - "integrity": "sha512-N/jjaXf9wvQqajq+xbmt6MO9m0BpEOdAS4kiBZ0c53gSAFvpiv2YNCMXD/ok6kyoG5f5FDM+0tlE4LotKDmKxg==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/auto-init": "15.0.0-canary.90291f2e2.0", - "@material/banner": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/button": "15.0.0-canary.90291f2e2.0", - "@material/card": "15.0.0-canary.90291f2e2.0", - "@material/checkbox": "15.0.0-canary.90291f2e2.0", - "@material/chips": "15.0.0-canary.90291f2e2.0", - "@material/circular-progress": "15.0.0-canary.90291f2e2.0", - "@material/data-table": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dialog": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/drawer": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/fab": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/floating-label": "15.0.0-canary.90291f2e2.0", - "@material/form-field": "15.0.0-canary.90291f2e2.0", - "@material/icon-button": "15.0.0-canary.90291f2e2.0", - "@material/image-list": "15.0.0-canary.90291f2e2.0", - "@material/layout-grid": "15.0.0-canary.90291f2e2.0", - "@material/line-ripple": "15.0.0-canary.90291f2e2.0", - "@material/linear-progress": "15.0.0-canary.90291f2e2.0", - "@material/list": "15.0.0-canary.90291f2e2.0", - "@material/menu": "15.0.0-canary.90291f2e2.0", - "@material/menu-surface": "15.0.0-canary.90291f2e2.0", - "@material/notched-outline": "15.0.0-canary.90291f2e2.0", - "@material/radio": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/segmented-button": "15.0.0-canary.90291f2e2.0", - "@material/select": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/slider": "15.0.0-canary.90291f2e2.0", - "@material/snackbar": "15.0.0-canary.90291f2e2.0", - "@material/switch": "15.0.0-canary.90291f2e2.0", - "@material/tab": "15.0.0-canary.90291f2e2.0", - "@material/tab-bar": "15.0.0-canary.90291f2e2.0", - "@material/tab-indicator": "15.0.0-canary.90291f2e2.0", - "@material/tab-scroller": "15.0.0-canary.90291f2e2.0", - "@material/textfield": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tooltip": "15.0.0-canary.90291f2e2.0", - "@material/top-app-bar": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "16.2.14", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.2.14.tgz", + "integrity": "sha512-zQIxUb23elPfiIvddqkIDYqQhAHa9ZwMblfbv+ug8bxr4D0Dw360jIarxCgMjAcLj7Ccl3GBqZMUnVeM6cjthw==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/auto-init": "15.0.0-canary.bc9ae6c9c.0", + "@material/banner": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/button": "15.0.0-canary.bc9ae6c9c.0", + "@material/card": "15.0.0-canary.bc9ae6c9c.0", + "@material/checkbox": "15.0.0-canary.bc9ae6c9c.0", + "@material/chips": "15.0.0-canary.bc9ae6c9c.0", + "@material/circular-progress": "15.0.0-canary.bc9ae6c9c.0", + "@material/data-table": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dialog": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/drawer": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/fab": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", + "@material/form-field": "15.0.0-canary.bc9ae6c9c.0", + "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", + "@material/image-list": "15.0.0-canary.bc9ae6c9c.0", + "@material/layout-grid": "15.0.0-canary.bc9ae6c9c.0", + "@material/line-ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/linear-progress": "15.0.0-canary.bc9ae6c9c.0", + "@material/list": "15.0.0-canary.bc9ae6c9c.0", + "@material/menu": "15.0.0-canary.bc9ae6c9c.0", + "@material/menu-surface": "15.0.0-canary.bc9ae6c9c.0", + "@material/notched-outline": "15.0.0-canary.bc9ae6c9c.0", + "@material/radio": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/segmented-button": "15.0.0-canary.bc9ae6c9c.0", + "@material/select": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/slider": "15.0.0-canary.bc9ae6c9c.0", + "@material/snackbar": "15.0.0-canary.bc9ae6c9c.0", + "@material/switch": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab-bar": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab-indicator": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab-scroller": "15.0.0-canary.bc9ae6c9c.0", + "@material/textfield": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tooltip": "15.0.0-canary.bc9ae6c9c.0", + "@material/top-app-bar": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^16.0.0 || ^17.0.0", - "@angular/cdk": "16.0.4", + "@angular/cdk": "16.2.14", "@angular/common": "^16.0.0 || ^17.0.0", "@angular/core": "^16.0.0 || ^17.0.0", "@angular/forms": "^16.0.0 || ^17.0.0", @@ -1480,22 +1405,22 @@ } }, "node_modules/@angular/material-moment-adapter": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-16.0.4.tgz", - "integrity": "sha512-nqg6WoXL1WDPPGZpCO7/f2/mCPEEPOPw1zqcZbR0Owofou+axpiiokWcvv7TksTio0cim5ETHZsJcpD5xlHOAw==", + "version": "16.2.14", + "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-16.2.14.tgz", + "integrity": "sha512-LagTDXEq8XOVLy8CVswCbmq7v9bb84+VikEEN09tz831U/7PHjDZ3xRgpKtv7hXrh8cTZOg3UPQw5tZk0hwh3Q==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "^16.0.0 || ^17.0.0", - "@angular/material": "16.0.4", + "@angular/material": "16.2.14", "moment": "^2.18.1" } }, "node_modules/@angular/platform-browser": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.7.tgz", - "integrity": "sha512-AjdUUv5+v50cclHPsKVVdNRdCQZJMGNKmvxyLgeGj2hs61lGoJxBYcYqPre2PpM0SvezNJBreUvjwqM3ttOjng==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.2.12.tgz", + "integrity": "sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -1503,9 +1428,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.1.7", - "@angular/common": "16.1.7", - "@angular/core": "16.1.7" + "@angular/animations": "16.2.12", + "@angular/common": "16.2.12", + "@angular/core": "16.2.12" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1514,9 +1439,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.7.tgz", - "integrity": "sha512-xoT4wDl7Kurg2N5gcLNmkvqYx14xnYwa2Zm1ZIOM7kYMRXiAg1+XBzaxFXog0fCCs/lqUKUwaNn32YpLKwMNaw==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz", + "integrity": "sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1524,16 +1449,16 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.7", - "@angular/compiler": "16.1.7", - "@angular/core": "16.1.7", - "@angular/platform-browser": "16.1.7" + "@angular/common": "16.2.12", + "@angular/compiler": "16.2.12", + "@angular/core": "16.2.12", + "@angular/platform-browser": "16.2.12" } }, "node_modules/@angular/platform-server": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-16.1.7.tgz", - "integrity": "sha512-a1xGs9SAcoBZY9amRkMTVkOXbZIFIm88ONyaU3KBxQwv6qs5M4J7jQk9l+FoRZ+k/nBHwQrfxzo7MVYYY1BIxQ==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-16.2.12.tgz", + "integrity": "sha512-IKuDYk54ZOc/Ic39hY1nlPGfCNVGdxsV0FMcuuVrqKQ2pKjeDPkfKIDBZB5vje7P08JKIKgHmEhb3XKe2D7HoQ==", "dependencies": { "tslib": "^2.3.0", "xhr2": "^0.2.0" @@ -1542,17 +1467,17 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/animations": "16.1.7", - "@angular/common": "16.1.7", - "@angular/compiler": "16.1.7", - "@angular/core": "16.1.7", - "@angular/platform-browser": "16.1.7" + "@angular/animations": "16.2.12", + "@angular/common": "16.2.12", + "@angular/compiler": "16.2.12", + "@angular/core": "16.2.12", + "@angular/platform-browser": "16.2.12" } }, "node_modules/@angular/router": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.1.7.tgz", - "integrity": "sha512-nzjuAEAXLktA3puvSae54noAHEiuizNTvaOpuvQYHfvZF27QMW28XlC33+vDhckWjSD02K7Fb2+AELkOJhUM5Q==", + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.2.12.tgz", + "integrity": "sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA==", "dependencies": { "tslib": "^2.3.0" }, @@ -1560,9 +1485,9 @@ "node": "^16.14.0 || >=18.10.0" }, "peerDependencies": { - "@angular/common": "16.1.7", - "@angular/core": "16.1.7", - "@angular/platform-browser": "16.1.7", + "@angular/common": "16.2.12", + "@angular/core": "16.2.12", + "@angular/platform-browser": "16.2.12", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1584,21 +1509,21 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "engines": { "node": ">=6.9.0" } @@ -1677,21 +1602,18 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", - "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { @@ -1827,26 +1749,26 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -1855,6 +1777,17 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", @@ -1907,11 +1840,11 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.5" }, "engines": { "node": ">=6.9.0" @@ -1940,25 +1873,25 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "engines": { "node": ">=6.9.0" } @@ -1977,35 +1910,49 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", - "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.6", - "@babel/types": "^7.22.5" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.5", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -3539,19 +3486,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -3559,26 +3506,37 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" }, "engines": { @@ -5036,13 +4994,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -5057,9 +5015,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -5079,19 +5037,14 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", @@ -5122,754 +5075,754 @@ } }, "node_modules/@material/animation": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-tr1y4KYZ2Ml9lFU9b91r5jivDCbh0N3Zv6VFe0frphztlZO5Lqx7MCxsliQ7NwQjqpXg3MkD6ZusVNvnMyo+LA==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-leRf+BcZTfC/iSigLXnYgcHAGvFVQveoJT5+2PIRdyPI/bIG7hhciRgacHRsCKC0sGya81dDblLgdkjSUemYLw==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/auto-init": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-ZslBlje2LajaL5d7JCxUoWCKOBOsZYT33CamqPoDeY0Cjl77t3O+8B9YPHF8libytI8j9lrrDrTItQr53PHeHw==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-uxzDq7q3c0Bu1pAsMugc1Ik9ftQYQqZY+5e2ybNplT8gTImJhNt4M2mMiMHbMANk2l3UgICmUyRSomgPBWCPIA==", "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/banner": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-mVtGop9rBXRD6UYkMD7y+OJwd3MA73w7BJ/oJIKFij2q2fn/5hZba6vQ6d6YGUGv+iJPP/S/HaiMQuRE5yyoqA==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/button": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-SHeVoidCUFVhXANN6MNWxK9SZoTSgpIP8GZB7kAl52BywLxtV+FirTtLXkg/8RUkxZRyRWl7HvQ0ZFZa7QQAyA==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/button": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/base": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-DNelmohDScmGvWWS/J05dkIJb/dKOVkA6s0URgPrnTFKXNSavPsmwj7hWzYB5kusz3ZrXJBYBJsE6VqkRRXl0w==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-Fc3vGuOf+duGo22HTRP6dHdc+MUe0VqQfWOuKrn/wXKD62m0QQR2TqJd3rRhCumH557T5QUyheW943M3E+IGfg==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/button": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-m3YqCh33kcPGYehCKviDy7RxQIEM2m8Exo6AswPPsxd95jSN3rAeF+pXopoXW5QTOqyKHqHymTKTRYYvwvZHYg==", - "dependencies": { - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-3AQgwrPZCTWHDJvwgKq7Cj+BurQ4wTjDdGL+FEnIGUAjJDskwi1yzx5tW2Wf/NxIi7IoPFyOY3UB41jwMiOrnw==", + "dependencies": { + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/card": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-8Z8KQDmEIwt5IK0n+9C9Be9p4mWLKBXILbH+c6XcMCTemmUxH6cTTax1MwuAmqBGuIq3WE3g7qDpdzjFLTC2kw==", - "dependencies": { - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-nPlhiWvbLmooTnBmV5gmzB0eLWSgLKsSRBYAbIBmO76Okgz1y+fQNLag+lpm/TDaHVsn5fmQJH8e0zIg0rYsQA==", + "dependencies": { + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/checkbox": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-hCilHX0vLedMgeRSOskf+JjdfLIUvEg597LEkTJHnTtJkhwypvol8OwP3eqz3TyJ3qGimIi/sFPKdMBn1Uk4AQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-4tpNnO1L0IppoMF3oeQn8F17t2n0WHB0D7mdJK9rhrujen/fLbekkIC82APB3fdGtLGg3qeNqDqPsJm1YnmrwA==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/chips": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-TMtlzuadWP/YMRYg8mpqmaD9M9GzRL5ulHHgYO5F4kaZmI3L+3zvaPvUme/x5qwPkIJUO9S21NxxGAsp9X+ZJQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/checkbox": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-fqHKvE5bSWK0bXVkf57MWxZtytGqYBZvvHIOs4JI9HPHEhaJy4CpSw562BEtbm3yFxxALoQknvPW2KYzvADnmA==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/checkbox": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "safevalues": "^0.3.4", "tslib": "^2.1.0" } }, "node_modules/@material/circular-progress": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-znCz3cXC8rmC+1k1ZEeZNOhngm7O7kVG2PoANaE79NN9taDtCTyBGGeocJ4Kza3tb01vxJ2/tuQXC39GNFkHFg==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/progress-indicator": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-Lxe8BGAxQwCQqrLhrYrIP0Uok10h7aYS3RBXP41ph+5GmwJd5zdyE2t93qm2dyThvU6qKuXw9726Dtq/N+wvZQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/progress-indicator": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/data-table": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-9YU6KkOeKs2ARPXZdg7Cv6nPwLkEyBIN331ZB92apcbQpTMJMhR3uuW8SSw4p7aXCE6CJjREsCc0KuYAnFSa2A==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/checkbox": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/icon-button": "15.0.0-canary.90291f2e2.0", - "@material/linear-progress": "15.0.0-canary.90291f2e2.0", - "@material/list": "15.0.0-canary.90291f2e2.0", - "@material/menu": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/select": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-j/7qplT9+sUpfe4pyWhPbl01qJA+OoNAG3VMJruBBR461ZBKyTi7ssKH9yksFGZ8eCEPkOsk/+kDxsiZvRWkeQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/checkbox": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", + "@material/linear-progress": "15.0.0-canary.bc9ae6c9c.0", + "@material/list": "15.0.0-canary.bc9ae6c9c.0", + "@material/menu": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/select": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/density": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-z2L49vc5tbIGe7tUHwbmzoPvOugsTNVP24WWwBwtg9PRuK4Td5HIsMGYqSzSuwFJvDWQK9Ugvl37jGZSv4vxog==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-Zt3u07fXrBWLW06Tl5fgvjicxNQMkFdawLyNTzZ5TvbXfVkErILLePwwGaw8LNcvzqJP6ABLA8jiR+sKNoJQCg==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/dialog": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-n0o4MELpVqJHbJDPBYeXf3xeL9a8hbzHmfXYLDI1MUhDIr4xgSkckKdCRc2IFda/g7kxjAgcUTga9EFWqns2qA==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/button": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/icon-button": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-o+9a/fmwJ9+gY3Z/uhj/PMVJDq7it1NTWKJn2GwAKdB+fDkT4hb9qEdcxMPyvJJ5ups+XiKZo03+tZrD+38c1w==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/button": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/dom": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-90JKk/Ncnqn2dKopNxs1uruiiQZzgLTZQF3a8jxa/w3RQd3Ac9ET1KqmaJSfzXaxgebm+1RZfL9lL+ANEfLWwQ==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-ly78R7aoCJtundSUu0UROU+5pQD5Piae0Y1MkN6bs0724azeazX1KeXFeaf06JOXnlr5/41ol+fSUPowjoqnOg==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/drawer": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-7MJbjUxqpQG9J52xWGVKRhSI/0/7Uhf7l2P9VI2WFb5Fz0IeUupXlw2k1Ktb97nxSjMe9OazjtVUgzBNwOad/Q==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/list": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-PFL4cEFnt7VTxDsuspFVNhsFDYyumjU0VWfj3PWB7XudsEfQ3lo85D3HCEtTTbRsCainGN8bgYNDNafLBqiigw==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/list": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/elevation": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-4eYbCDc6IfgfguNmRc5GT4QMCfOEwj+K3BAraABcbpuCzEQ5nCClsVrPbRLfPnhWbQrFc2/eBglB8wsrNTjVBw==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-Ro+Pk8jFuap+T0B0shA3xI1hs2b89dNQ2EIPCNjNMp87emHKAzJfhKb7EZGIwv3+gFLlVaLyIVkb94I89KLsyg==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/fab": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-YJuJRdqe57DnJ4qyc04flknuGeN+7Nc9ciFZE6snPn84wD6J1khscb21yRARbALDki18kbfnJNrNbzHkYaEMZg==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-dvU0KWMRglwJEQwmQtFAmJcAjzg9VFF6Aqj78bJYu/DAIGFJ1VTTTSgoXM/XCm1YyQEZ7kZRvxBO37CH54rSDg==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/feature-targeting": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-mjUemPnMLXPooDcPHxxc2uhVUzm7X3NDsE0x0QJnsHDwuejakaRLghVcRDX3x1VmL/p52Eu5HrgW2FryFEiVhQ==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-wkDjVcoVEYYaJvun28IXdln/foLgPD7n9ZC9TY76GErGCwTq+HWpU6wBAAk+ePmpRFDayw4vI4wBlaWGxLtysQ==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/floating-label": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-dYtgNlXkO0H9Vn76oESZZg1KOa2XIOLhVxhV/qPYrhntET534i7TyajmVk54ncuSSoLPZrbrwrhhR2fUJWxZIg==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-bUWPtXzZITOD/2mkvLkEPO1ngDWmb74y0Kgbz6llHLOQBtycyJIpuoQJ1q2Ez0NM/tFLwPphhAgRqmL3YQ/Kzw==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/focus-ring": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-U7ERDgHi33ZRmqsiO6syFaWsCUGneltX2sYVtLpQnxME7pKFzi22GdUUIslhgHOFjSMBFF9av2Y79VFbyj9BaQ==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-cZHThVose3GvAlJzpJoBI1iqL6d1/Jj9hXrR+r8Mwtb1hBIUEG3hxfsRd4vGREuzROPlf0OgNf/V+YHoSwgR5w==", "dependencies": { - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0" + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0" } }, "node_modules/@material/form-field": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-KTj+EOobLcPUYy4nR+t0c2Cjvs7jCI4F1w8XuV0bbmSa6Sxh02tMKY2Xa7Lx55A/uUrsUfViMdP60OLzi7HgjQ==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-+JFXy5X44Gue1CbZZAQ6YejnI203lebYwL0i6k0ylDpWHEOdD5xkF2PyHR28r9/65Ebcbwbff6q7kI1SGoT7MA==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/icon-button": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-bpqiVPkf/LJEP7iIV5VL9Th0chCIQKTeOuw0mK8HmYucuvqq+k76oPsUcE7mvxRvuKyVh6KJ9fTHAkjse0y7cg==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-1a0MHgyIwOs4RzxrVljsqSizGYFlM1zY2AZaLDsgT4G3kzsplTx8HZQ022GpUCjAygW+WLvg4z1qAhQHvsbqlw==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/image-list": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-JobM4lWsf9grgbqrLuUtJ5fr8BkG02r9c2oFMl5++dtjtLdXWnUIWbiofna8CeqDFQCKXsCk4Jw8ydSKZvj/3A==", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-WKWmiYap2iu4QdqmeUSliLlN4O2Ueqa0OuVAYHn/TCzmQ2xmnhZ1pvDLbs6TplpOmlki7vFfe+aSt5SU9gwfOQ==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/layout-grid": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-f5DSHGf1ka6vHu+8VoTvcU9NkR8fdN2wHmPnITZHQXPVvR6SKoyDzdAf2gacmiTYy69ZFmmJeMcdfNnbcPPUJw==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-5GqmT6oTZhUGWIb+CLD0ZNyDyTiJsr/rm9oRIi3+vCujACwxFkON9tzBlZohdtFS16nuzUusthN6Jt9UrJcN6Q==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/line-ripple": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-cWpS7l/pFg6cUzL7Lx3ywF6RYF6ESYPkiGlDo9kFERv8lVA2/3m4NF4d9b4kC9h9OWx1b1CaUFRFGD07okgI+g==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-8S30WXEuUdgDdBulzUDlPXD6qMzwCX9SxYb5mGDYLwl199cpSGdXHtGgEcCjokvnpLhdZhcT1Dsxeo1g2Evh5Q==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/linear-progress": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-nK5RYn8NFZi/+fznLYoEdY6tSzXiJqOU0tX5by7hStURhP2g/RM5SQaJwyjEmHdorfCUIStgmKsN4rB5aMnxdw==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/progress-indicator": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-6EJpjrz6aoH2/gXLg9iMe0yF2C42hpQyZoHpmcgTLKeci85ktDvJIjwup8tnk8ULQyFiGiIrhXw2v2RSsiFjvQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/progress-indicator": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/list": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-h2c6G5aPH1bhgl2yBYAW4y86pl+yVl3YdqU0ixemQ5/2uB/t92imUbI+gKH5LzlbuJKenk3rZJ5eaV+t5zTS1A==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-TQ1ppqiCMQj/P7bGD4edbIIv4goczZUoiUAaPq/feb1dflvrFMzYqJ7tQRRCyBL8nRhJoI2x99tk8Q2RXvlGUQ==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/menu": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-yfpBN4Hg59pHMNplh2LIe8t+/qsfyP0iRAtJoCK90SBwX43kv65u22+3vEJmYzm3Ey/m3S3YRFXTFQRQnn9cmA==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/list": "15.0.0-canary.90291f2e2.0", - "@material/menu-surface": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-IlAh61xzrzxXs38QZlt74UYt8J431zGznSzDtB1Fqs6YFNd11QPKoiRXn1J2Qu/lUxbFV7i8NBKMCKtia0n6/Q==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/list": "15.0.0-canary.bc9ae6c9c.0", + "@material/menu-surface": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/menu-surface": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-AkRpOjFOJi2ROZPvFo1z8ik61eEyJEew8NuvlzCE4S3BX/RNFrYVh4W5ylo030S01ALCS5zhVOeekxa/4eokZA==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-dMtSPN+olTWE+08M5qe4ea1IZOhVryYqzK0Gyb2u1G75rSArUxCOB5rr6OC/ST3Mq3RS6zGuYo7srZt4534K9Q==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/notched-outline": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-4lpoIoFJ8cb++M1iQpZ+8iypUuTruzyBAkOvoaNjk7EftEV+aS3K6XntGNtlUZoB/fFho3mAUVjT19IHFWD03A==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/floating-label": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-WuurMg44xexkvLTBTnsO0A+qnzFjpcPdvgWBGstBepYozsvSF9zJGdb1x7Zv1MmqbpYh/Ohnuxtb/Y3jOh6irg==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/progress-indicator": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-trQLSstZIA0C64adW/HycycD+PtMfg6iZCIVuTNlZr7PR2Yc1EjuGyA7ts+iXBHZ0TxVshRbDYMwcDogP0rc1w==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-uOnsvqw5F2fkeTnTl4MrYzjI7KCLmmLyZaM0cgLNuLsWVlddQE+SGMl28tENx7DUK3HebWq0FxCP8f25LuDD+w==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@material/radio": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-Ypl4BZ9w1NdbiiEUV3Xw2pb97prMPGEE+5Lm719sVsaFmI4yCKgtsWNEbCbKixborh2ZDZWGCzgMyUQHf3a8xA==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-ehzOK+U1IxQN+OQjgD2lsnf1t7t7RAwQzeO6Czkiuid29ookYbQynWuLWk7NW8H8ohl7lnmfqTP1xSNkkL/F0g==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/ripple": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-8yCR/V+7SJ9om0cvAOULF/i5+gPQeT+cuPoCZJQRWq9IndfCmQPY3Zmy26reIT/zEyCebAvMG4/WtU4rc+jxyw==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-JfLW+g3GMVDv4cruQ19+HUxpKVdWCldFlIPw1UYezz2h3WTNDy05S3uP2zUdXzZ01C3dkBFviv4nqZ0GCT16MA==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/rtl": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-HkUhD8K03BxWVw21WDViWo01Chi22cZ1rmlsdCtggkxdVjtDhTbYm/3XvRnxt4RVpr6KaYQgRXI/52T5RtBUnw==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-SkKLNLFp5QtG7/JEFg9R92qq4MzTcZ5As6sWbH7rRg6ahTHoJEuqE+pOb9Vrtbj84k5gtX+vCYPvCILtSlr2uw==", "dependencies": { - "@material/theme": "15.0.0-canary.90291f2e2.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/segmented-button": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-u1DF+jbysX6wCqt7dDnHgEo2XhNrwkqHq6YsMOFVCoo54PHt3gpwhD89DONqQJKspkdvZuxYHzpqRtV0GIzYDQ==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/touch-target": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-YDwkCWP9l5mIZJ7pZJZ2hMDxfBlIGVJ+deNzr8O+Z7/xC5LGXbl4R5aPtUVHygvXAXxpf5096ZD+dSXzYzvWlw==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/touch-target": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/select": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-CCaftyi3eIJl2XqBfHbzj8W2jgTMBzSM2+q4WthA+7z0fYQI4JIHQVHO5YKQG5J9MR1VjYQ0Gy0GNotZLAcoOQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/floating-label": "15.0.0-canary.90291f2e2.0", - "@material/line-ripple": "15.0.0-canary.90291f2e2.0", - "@material/list": "15.0.0-canary.90291f2e2.0", - "@material/menu": "15.0.0-canary.90291f2e2.0", - "@material/menu-surface": "15.0.0-canary.90291f2e2.0", - "@material/notched-outline": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-unfOWVf7T0sixVG+3k3RTuATfzqvCF6QAzA6J9rlCh/Tq4HuIBNDdV4z19IVu4zwmgWYxY0iSvqWUvdJJYwakQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", + "@material/line-ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/list": "15.0.0-canary.bc9ae6c9c.0", + "@material/menu": "15.0.0-canary.bc9ae6c9c.0", + "@material/menu-surface": "15.0.0-canary.bc9ae6c9c.0", + "@material/notched-outline": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/shape": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-UEB168x8ovLvH4eoBtRnoCT9QvnxB/ZMpOKW1+C+xWisis6Wy9AX0wKT5T6wIpffYYCaBJuhI+ExX2134rAxJw==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-Dsvr771ZKC46ODzoixLdGwlLEQLfxfLrtnRojXABoZf5G3o9KtJU+J+5Ld5aa960OAsCzzANuaub4iR88b1guA==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/slider": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-krbdGHcROlvnZ0X7HT0d+PvJummczeShQeWeV/ZezXnQM7bQoy86qfwtX4ai1dIXYkF9qKTFlta2zZezTJyf5g==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-3AEu+7PwW4DSNLndue47dh2u7ga4hDJRYmuu7wnJCIWJBnLCkp6C92kNc4Rj5iQY2ftJio5aj1gqryluh5tlYg==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/snackbar": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-f1+HaSaAkALtQqEr6WMUqfwOsJr5nOUjP15GA+sTs9SD7yzwqMeWsVriBdWXVRe0zNgew6sfBM+cLjg2w4VAOQ==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/button": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/icon-button": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-TwwQSYxfGK6mc03/rdDamycND6o+1p61WNd7ElZv1F1CLxB4ihRjbCoH7Qo+oVDaP8CTpjeclka+24RLhQq0mA==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/button": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/icon-button": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/switch": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-94/+Zp2VjlLVJXY7u8VHPcJMHPRVNAwHydiGrKvnJ+6LfbLxAcILNBP9RVKqqqOWQeDxB4ApUl+0TV2Lj6mOzA==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-OjUjtT0kRz1ASAsOS+dNzwMwvsjmqy5edK57692qmrP6bL4GblFfBDoiNJ6t0AN4OaKcmL5Hy/xNrTdOZW7Qqw==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", "safevalues": "^0.3.4", "tslib": "^2.1.0" } }, "node_modules/@material/tab": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-zju6UP038Ddi3XAfliYy58A3OvkQ+zSlOdNOd5l82oMArLYEFi3t51QTjKVjV1wokr6ZQ3Chs4kcrgwVTElYtg==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/focus-ring": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/tab-indicator": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-s/L9otAwn/pZwVQZBRQJmPqYeNbjoEbzbjMpDQf/VBG/6dJ+aP03ilIBEkqo8NVnCoChqcdtVCoDNRtbU+yp6w==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/focus-ring": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab-indicator": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/tab-bar": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-dTC7oGZg6KuDK2OXO7luJWqshtNY2YgImwZbQ9a1vZZrIGMRHdu+ZtP6RVH2srFVlNIWjzcxfLgNrG+U027RdA==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/tab": "15.0.0-canary.90291f2e2.0", - "@material/tab-indicator": "15.0.0-canary.90291f2e2.0", - "@material/tab-scroller": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-Xmtq0wJGfu5k+zQeFeNsr4bUKv7L+feCmUp/gsapJ655LQKMXOUQZtSv9ZqWOfrCMy55hoF1CzGFV+oN3tyWWQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab-indicator": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab-scroller": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/tab-indicator": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-GXdFO6rO1crXcj+todijzZQVACW4EC72XwLAl6z69TKBgZrhwCoZ6RgzX6vIXSs+KoZ0eIyQLr+yQQx1JjDd4w==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-despCJYi1GrDDq7F2hvLQkObHnSLZPPDxnOzU16zJ6FNYvIdszgfzn2HgAZ6pl5hLOexQ8cla6cAqjTDuaJBhQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/tab-scroller": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-R6trOZpkfk54VV0w0NjMMDcZPQgbnARxCoHLrWeSzv5KOMoiDyWji7FFpLc4fynX/F2lNg8xHpEolpugNRW/1g==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/tab": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-QWHG/EWxirj4V9u2IHz+OSY9XCWrnNrPnNgEufxAJVUKV/A8ma1DYeFSQqxhX709R8wKGdycJksg0Flkl7Gq7w==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/tab": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/textfield": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-ipxPH8DRh9+cn4MOtAYvGsRLP5RJH/gB/BWh/BiJwjI38Djt4FK4LDHbx7fFo/C8hoj7UNs/BWaSLllyxuWKcg==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/density": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/floating-label": "15.0.0-canary.90291f2e2.0", - "@material/line-ripple": "15.0.0-canary.90291f2e2.0", - "@material/notched-outline": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-R3qRex9kCaZIAK8DuxPnVC42R0OaW7AB7fsFknDKeTeVQvRcbnV8E+iWSdqTiGdsi6QQHifX8idUrXw+O45zPw==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/density": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/floating-label": "15.0.0-canary.bc9ae6c9c.0", + "@material/line-ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/notched-outline": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/theme": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-rDSZ0bPoJothI8nRPQWB4Cyu7DTmc8qIuvFm3OOD4uI/2n+yIFqktS6X+6YF82LeKt4uMTZE+Ce/l51bb8UJGA==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-CpUwXGE0dbhxQ45Hu9r9wbJtO/MAlv5ER4tBHA9tp/K+SU+lDgurBE2touFMg5INmdfVNtdumxb0nPPLaNQcUg==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/tokens": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-ZTis8UeSRrm/4iQ6BujtcTf1J2bs2H+SAEnugtZSQiX8pyf90gQvylEoTuMPdUs1+YJ273cn04ipHdkq3OHaew==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-nbEuGj05txWz6ZMUanpM47SaAD7soyjKILR+XwDell9Zg3bGhsnexCNXPEz2fD+YgomS+jM5XmIcaJJHg/H93Q==", "dependencies": { - "@material/elevation": "15.0.0-canary.90291f2e2.0" + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0" } }, "node_modules/@material/tooltip": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-H3XsrctgRriNwt++NN+Zy6/JhyRznWo2pXiTFnOlaYwHOiGIFCNZR0A/0vf/3Kpf0GYhTfkJEFJMosUSZidSDg==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/button": "15.0.0-canary.90291f2e2.0", - "@material/dom": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/tokens": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-UzuXp0b9NuWuYLYpPguxrjbJnCmT/Cco8CkjI/6JajxaeA3o2XEBbQfRMTq8PTafuBjCHTc0b0mQY7rtxUp1Gg==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/button": "15.0.0-canary.bc9ae6c9c.0", + "@material/dom": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/tokens": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "safevalues": "^0.3.4", "tslib": "^2.1.0" } }, "node_modules/@material/top-app-bar": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-ZiJjK4WpIsE0MZTWokP9r4C9/oDqzUhKRn3ef2WCeJEIU3Vjg4t0xBTnST2vIrcBGw1s7WP1gfaxb3DSXSxzpw==", - "dependencies": { - "@material/animation": "15.0.0-canary.90291f2e2.0", - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/elevation": "15.0.0-canary.90291f2e2.0", - "@material/ripple": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/shape": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", - "@material/typography": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-vJWjsvqtdSD5+yQ/9vgoBtBSCvPJ5uF/DVssv8Hdhgs1PYaAcODUi77kdi0+sy/TaWyOsTkQixqmwnFS16zesA==", + "dependencies": { + "@material/animation": "15.0.0-canary.bc9ae6c9c.0", + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/elevation": "15.0.0-canary.bc9ae6c9c.0", + "@material/ripple": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/shape": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", + "@material/typography": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/touch-target": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-IpRFf4umZ4ZNxrP+qJkRY9syh7TFZmU4c7EbAlANAJ0/8rlkEo7WJiqa9P1p4nFaT4eMo4n5g+qRI0Dkb9zW5g==", - "dependencies": { - "@material/base": "15.0.0-canary.90291f2e2.0", - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/rtl": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-AqYh9fjt+tv4ZE0C6MeYHblS2H+XwLbDl2mtyrK0DOEnCVQk5/l5ImKDfhrUdFWHvS4a5nBM4AA+sa7KaroLoA==", + "dependencies": { + "@material/base": "15.0.0-canary.bc9ae6c9c.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/rtl": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, "node_modules/@material/typography": { - "version": "15.0.0-canary.90291f2e2.0", - "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.90291f2e2.0.tgz", - "integrity": "sha512-tv1HWkJYi5T0470k8vbBb+nefdPgsaIO04ocWMf7luvmfE+MZIaR13RxdupLJ4k5otrdydL3/wEaCNhQ+Ipnvw==", + "version": "15.0.0-canary.bc9ae6c9c.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.bc9ae6c9c.0.tgz", + "integrity": "sha512-CKsG1zyv34AKPNyZC8olER2OdPII64iR2SzQjpqh1UUvmIFiMPk23LvQ1OnC5aCB14pOXzmVgvJt31r9eNdZ6Q==", "dependencies": { - "@material/feature-targeting": "15.0.0-canary.90291f2e2.0", - "@material/theme": "15.0.0-canary.90291f2e2.0", + "@material/feature-targeting": "15.0.0-canary.bc9ae6c9c.0", + "@material/theme": "15.0.0-canary.bc9ae6c9c.0", "tslib": "^2.1.0" } }, @@ -7104,59 +7057,6 @@ } } }, - "node_modules/@nx/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@nx/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/@nx/eslint-plugin/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7188,28 +7088,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@nx/eslint-plugin/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nx/eslint-plugin/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@nx/eslint-plugin/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -9406,12 +9284,12 @@ } }, "node_modules/@schematics/angular": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.6.tgz", - "integrity": "sha512-BxghkeLfnMgV0D4DZDcbfPpox/Orw1ismSVGoQMIV/Daj2pqfSK+n97NAu0r0EsQyR5agPxOX9khVft+otODhg==", + "version": "16.2.14", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.14.tgz", + "integrity": "sha512-YqIv727l9Qze8/OL6H9mBHc2jVXzAGRNBYnxYWqWhLbfvuVbbldo6NNIIjgv6lrl2LJSdPAAMNOD5m/f6210ug==", "dependencies": { - "@angular-devkit/core": "16.1.6", - "@angular-devkit/schematics": "16.1.6", + "@angular-devkit/core": "16.2.14", + "@angular-devkit/schematics": "16.2.14", "jsonc-parser": "3.2.0" }, "engines": { @@ -12714,81 +12592,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@typescript-eslint/parser": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", @@ -12820,7 +12623,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" @@ -12834,12 +12636,12 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz", - "integrity": "sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.7", - "@typescript-eslint/utils": "5.59.7", + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -12859,65 +12661,10 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz", - "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz", - "integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==", - "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz", - "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==", - "dependencies": { - "@typescript-eslint/types": "5.59.7", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/types": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -12930,89 +12677,9 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.7.tgz", - "integrity": "sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.7", - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/typescript-estree": "5.59.7", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz", - "integrity": "sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==", - "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz", - "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz", - "integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==", - "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7", + "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -13032,13 +12699,19 @@ } } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz", - "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==", + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dependencies": { - "@typescript-eslint/types": "5.59.7", - "eslint-visitor-keys": "^3.3.0" + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -13046,6 +12719,9 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { @@ -13072,7 +12748,6 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" @@ -13839,9 +13514,9 @@ } }, "node_modules/@wessberg/ts-evaluator/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -14452,25 +14127,12 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dequal": "^2.0.3" } }, "node_modules/array-flatten": { @@ -14660,12 +14322,12 @@ } }, "node_modules/axobject-query": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", - "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/babel-core": { @@ -15306,11 +14968,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -15337,9 +14999,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -15355,10 +15017,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -15618,9 +15280,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001585", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz", - "integrity": "sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==", + "version": "1.0.30001615", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", + "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", "funding": [ { "type": "opencollective", @@ -17430,35 +17092,6 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, - "node_modules/deep-equal": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", - "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.1", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-freeze-strict": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz", @@ -17948,9 +17581,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.485", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.485.tgz", - "integrity": "sha512-1ndQ5IBNEnFirPwvyud69GHL+31FkE09gH/CJ6m3KCbkx3i0EVOrjwz4UNxRmN9H8OVHbC6vMRZGN1yCvjSs9w==" + "version": "1.4.754", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz", + "integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==" }, "node_modules/email-addresses": { "version": "5.0.0", @@ -18104,26 +17737,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-module-lexer": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", @@ -18206,9 +17819,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -19118,9 +18731,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -19634,15 +19247,6 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==" }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gauge": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", @@ -20151,15 +19755,6 @@ "node": ">= 0.4.0" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -21007,20 +20602,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -21069,37 +20650,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -21111,22 +20666,6 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -21177,21 +20716,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-deflate": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", @@ -21301,15 +20825,6 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "dev": true }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -21340,21 +20855,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -21400,43 +20900,6 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -21448,42 +20911,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-subset": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", "dev": true }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-typed-array": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", @@ -21516,28 +20949,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-what": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", @@ -21554,12 +20965,6 @@ "node": ">=8" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -24167,6 +23572,7 @@ "version": "0.30.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.13" }, @@ -25662,9 +25068,9 @@ "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nopt": { "version": "6.0.0", @@ -26270,24 +25676,6 @@ "node": ">= 10.12.0" } }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -28088,9 +27476,9 @@ } }, "node_modules/puppeteer-core/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "dev": true, "dependencies": { "async-limiter": "~1.0.0" @@ -28660,23 +28048,6 @@ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -29900,18 +29271,6 @@ "node": ">= 0.8" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/store2": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.2.tgz", @@ -31782,9 +31141,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz", + "integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==", "funding": [ { "type": "opencollective", @@ -31800,7 +31159,7 @@ } ], "dependencies": { - "escalade": "^3.1.1", + "escalade": "^3.1.2", "picocolors": "^1.0.0" }, "bin": { @@ -32959,37 +32318,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -33207,9 +32535,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index d278dabad6..60fcf534fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geonetwork-ui", - "version": "2.3.0-dev", + "version": "2.4.0-dev", "engines": { "node": ">=14.17.0" }, @@ -43,19 +43,19 @@ }, "private": true, "dependencies": { - "@angular/animations": "16.1.7", - "@angular/cdk": "16.0.4", - "@angular/common": "16.1.7", - "@angular/compiler": "16.1.7", - "@angular/core": "16.1.7", - "@angular/elements": "16.1.7", - "@angular/forms": "16.1.7", - "@angular/material": "16.0.4", - "@angular/material-moment-adapter": "16.0.4", - "@angular/platform-browser": "16.1.7", - "@angular/platform-browser-dynamic": "16.1.7", - "@angular/platform-server": "16.1.7", - "@angular/router": "16.1.7", + "@angular/animations": "^16.2", + "@angular/cdk": "^16.2", + "@angular/common": "^16.2", + "@angular/compiler": "^16.2", + "@angular/core": "^16.2", + "@angular/elements": "^16.2", + "@angular/forms": "^16.2", + "@angular/material": "^16.2", + "@angular/material-moment-adapter": "^16.2", + "@angular/platform-browser": "^16.2", + "@angular/platform-browser-dynamic": "^16.2", + "@angular/platform-server": "^16.2", + "@angular/router": "^16.2", "@bartholomej/ngx-translate-extract": "^8.0.2", "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@camptocamp/ogc-client": "1.1.1-dev.ad6d9ab", @@ -107,15 +107,15 @@ "zone.js": "0.13.0" }, "devDependencies": { - "@angular-devkit/build-angular": "16.2.12", - "@angular-devkit/core": "16.1.6", - "@angular-devkit/schematics": "16.1.6", - "@angular-eslint/eslint-plugin": "16.0.3", - "@angular-eslint/eslint-plugin-template": "16.0.3", - "@angular-eslint/template-parser": "16.0.3", - "@angular/cli": "~16.1.0", - "@angular/compiler-cli": "16.1.7", - "@angular/language-service": "16.1.7", + "@angular-devkit/build-angular": "^16.2", + "@angular-devkit/core": "^16.2", + "@angular-devkit/schematics": "^16.2", + "@angular-eslint/eslint-plugin": "^16.2", + "@angular-eslint/eslint-plugin-template": "^16.2", + "@angular-eslint/template-parser": "^16.2", + "@angular/cli": "^16.2", + "@angular/compiler-cli": "^16.2", + "@angular/language-service": "^16.2", "@nestjs/schematics": "10.0.1", "@nestjs/testing": "10.1.3", "@ngrx/component": "16.0.1", @@ -135,7 +135,7 @@ "@nx/webpack": "16.6.0", "@nx/workspace": "16.6.0", "@openapitools/openapi-generator-cli": "^2.6.0", - "@schematics/angular": "16.1.6", + "@schematics/angular": "^16.2", "@storybook/addon-essentials": "7.2.1", "@storybook/angular": "7.2.1", "@types/chroma-js": "^2.1.3", diff --git a/package/package.json b/package/package.json index d70a149f66..50a021937b 100644 --- a/package/package.json +++ b/package/package.json @@ -1,6 +1,6 @@ { "name": "geonetwork-ui", - "version": "2.3.0-dev", + "version": "2.4.0-dev", "engines": { "node": ">=14.17.0" }, diff --git a/support-services/docker-entrypoint-initdb.d/dump b/support-services/docker-entrypoint-initdb.d/dump index 8b4e5884f5..f1f88acdff 100644 Binary files a/support-services/docker-entrypoint-initdb.d/dump and b/support-services/docker-entrypoint-initdb.d/dump differ diff --git a/tailwind.base.css b/tailwind.base.css index 016e4d3f4f..9bce5e4729 100644 --- a/tailwind.base.css +++ b/tailwind.base.css @@ -15,6 +15,11 @@ width: 16px; height: 16px; } + .gn-ui-icon-medium { + font-size: 20px; + width: 20px; + height: 20px; + } /* LINK CLASSES */ .gn-ui-link { @@ -104,7 +109,27 @@ border border-white focus:ring-4 focus:ring-gray-300; } - /* TODO: add prefix */ + /* BADGE CLASS */ + .gn-ui-badge { + --rounded: var(--gn-ui-badge-rounded, 0.25em); + --padding: var(--gn-ui-badge-padding, 0.375em 0.75em); + --text-color: var(--gn-ui-badge-text-color, var(--color-gray-50)); + --background-color: var(--gn-ui-badge-background-color, black); + @apply inline-block opacity-70 p-[--padding] rounded-[--rounded] + font-medium text-[length:0.875em] leading-none text-[color:--text-color] bg-[color:--background-color]; + } + /* makes sure icons will not make the badges grow vertically; also make size proportional */ + .gn-ui-badge mat-icon.mat-icon { + margin-top: -0.325em; + margin-bottom: -0.325em; + flex-shrink: 0; + vertical-align: middle; + font-size: 1.4em; + width: 1em; + height: 1em; + } + + /* TODO: replace by gn-ui-badge class above */ .badge-btn { @apply flex items-center justify-center px-4 py-1 text-white rounded backdrop-blur; } diff --git a/tools/docker/apache/apache-ports.conf b/tools/docker/apache/apache-ports.conf index 9d36f04c09..032a1553e8 100644 --- a/tools/docker/apache/apache-ports.conf +++ b/tools/docker/apache/apache-ports.conf @@ -4,16 +4,6 @@ Listen 8080 Listen 443 - - Listen 443 - - -Listen 8080 - - - Listen 443 - - Listen 443 \ No newline at end of file diff --git a/tools/docker/apache/apache-security.conf b/tools/docker/apache/apache-security.conf index a3d339acb8..8f31b7eb33 100644 --- a/tools/docker/apache/apache-security.conf +++ b/tools/docker/apache/apache-security.conf @@ -1,7 +1,3 @@ -ServerTokens Prod -ServerSignature Off -TraceEnable Off - ServerTokens Prod ServerSignature Off TraceEnable Off \ No newline at end of file diff --git a/tools/docker/apache/apache-vhost.conf b/tools/docker/apache/apache-vhost.conf index 9aef3c671e..bd1aa05de1 100644 --- a/tools/docker/apache/apache-vhost.conf +++ b/tools/docker/apache/apache-vhost.conf @@ -1,24 +1,3 @@ - - ServerName localhost - - DocumentRoot /opt/catalogue - Alias /catalogue "/opt/catalogue" - - Options Indexes FollowSymLinks MultiViews - AllowOverride All - Require all granted - RewriteEngine On - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^ index.html [L] - - - # ErrorLog ${APACHE_LOG_DIR}/catalogue_error.log - # CustomLog ${APACHE_LOG_DIR}/catalogue_access.log combined - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - ServerName localhost diff --git a/translations/de.json b/translations/de.json index 5a5e80f3f9..0dc83bb483 100644 --- a/translations/de.json +++ b/translations/de.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{Datensätze} one{Datensatz} other{Datensätze}}", - "catalog.figures.organisations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", + "catalog.figures.organizations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", "chart.aggregation.average": "Durchschnitt", "chart.aggregation.count": "Anzahl", "chart.aggregation.max": "Maximum", @@ -17,15 +18,22 @@ "chart.type.line": "Liniendiagramm", "chart.type.lineSmooth": "Geglättes Liniendiagramm", "chart.type.pie": "Kreisdiagramm", + "dashboard.catalog.allRecords": "Metadatenkatalog", + "dashboard.catalog.calendar": "Kalender", + "dashboard.catalog.contacts": "Kontakte", + "dashboard.catalog.discussion": "Diskussion", + "dashboard.catalog.thesaurus": "Thesaurus", "dashboard.createRecord": "Neuer Eintrag", + "dashboard.labels.catalog": "Katalog", "dashboard.labels.mySpace": "Mein Bereich", "dashboard.records.all": "Katalog", + "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Meine Entwürfe", "dashboard.records.myLibrary": "Meine Bibliothek", - "dashboard.records.myOrg": "Meine Organisation", "dashboard.records.myRecords": "Meine Datensätze", "dashboard.records.publishedRecords": "{count, plural, =1{veröffentlichter Datensatz} other{veröffentlichte Datensätze}}", "dashboard.records.search": "Suche nach \"{searchText}\"", + "dashboard.records.templates": "Vorlagen", "dashboard.records.userDetail": "Name", "dashboard.records.userEmail": "E-Mail", "dashboard.records.username": "Benutzername", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Unbekannt oder nicht vorhanden", "editor.record.form.resourceUpdated": "", + "editor.record.form.temporalExtents": "", + "editor.record.form.temporalExtents.addDate": "", + "editor.record.form.temporalExtents.addRange": "", + "editor.record.form.temporalExtents.date": "", + "editor.record.form.temporalExtents.range": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "", "editor.record.publishSuccess.body": "", "editor.record.publishSuccess.title": "", + "editor.record.saveStatus.asDraftOnly": "", + "editor.record.saveStatus.draftWithChangesPending": "", + "editor.record.saveStatus.recordUpToDate": "", "editor.record.upToDate": "", "externalviewer.dataset.unnamed": "Datensatz aus dem Datahub", "facets.block.title.OrgForResource": "Organisation", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Veröffentlichungen 0 → 9", "organisations.sortBy.recordCountDesc": "Veröffentlichungen 9 → 0", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.details.publishedDataset": "{count, plural, =0{} one{} other{}}", + "organization.details.mailContact": "", + "organization.datasets": "", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Nächste Seite", "pagination.page": "Seite", "pagination.pageOf": "von", @@ -273,7 +295,7 @@ "record.metadata.preview": "Vorschau", "record.metadata.producer": "Datenproduzent", "record.metadata.publication": "Veröffentlichungsdatum", - "record.metadata.publications": "Veröffentlichungen", + "record.metadata.publications": "{count, plural, =0{Veröffentlichungsdatum} one{Veröffentlichungsdatum} other{Veröffentlichungen}}", "record.metadata.quality": "Metadatenqualität", "record.metadata.quality.contact.failed": "Kontakt nicht angegeben", "record.metadata.quality.contact.success": "Kontakt angegeben", @@ -334,8 +356,10 @@ "search.autocomplete.error": "Vorschläge konnten nicht abgerufen werden:", "search.error.couldNotReachApi": "Die API konnte nicht erreicht werden", "search.error.receivedError": "Ein Fehler ist aufgetreten", - "search.error.recordNotFound": "Der Datensatz mit der Kennung \"{ id }\" konnte nicht gefunden werden.", "search.error.recordHasnolink": "", + "search.error.recordNotFound": "Der Datensatz mit der Kennung \"{ id }\" konnte nicht gefunden werden.", + "search.error.organizationNotFound": "", + "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "Suche Datensätze ...", "search.field.sortBy": "Sortieren nach:", "search.filters.clear": "Zurücksetzen", diff --git a/translations/en.json b/translations/en.json index 90bbd05490..2e2fefda5e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "Log in", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organisations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", + "catalog.figures.organizations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", "chart.aggregation.average": "average", "chart.aggregation.count": "count", "chart.aggregation.max": "max", @@ -17,15 +18,22 @@ "chart.type.line": "line chart", "chart.type.lineSmooth": "smooth line chart", "chart.type.pie": "pie chart", + "dashboard.catalog.allRecords": "Metadata records", + "dashboard.catalog.calendar": "Calendar", + "dashboard.catalog.contacts": "Contacts", + "dashboard.catalog.discussion": "Discussion", + "dashboard.catalog.thesaurus": "Thesaurus", "dashboard.createRecord": "New record", + "dashboard.labels.catalog": "Catalog", "dashboard.labels.mySpace": "My space", "dashboard.records.all": "Metadata records", + "dashboard.records.hasDraft": "draft", "dashboard.records.myDraft": "My drafts", "dashboard.records.myLibrary": "My library", - "dashboard.records.myOrg": "Organization", "dashboard.records.myRecords": "My Records", "dashboard.records.publishedRecords": "{count, plural, =1{published record} other{published records}}", "dashboard.records.search": "Search for \"{searchText}\"", + "dashboard.records.templates": "Templates", "dashboard.records.userDetail": "Name", "dashboard.records.userEmail": "Email", "dashboard.records.username": "Username", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "Open Data Commons PDDL", "editor.record.form.license.unknown": "Unknown or absent", "editor.record.form.resourceUpdated": "Last update date", + "editor.record.form.temporalExtents": "Temporal extent", + "editor.record.form.temporalExtents.addDate": "Time instant", + "editor.record.form.temporalExtents.addRange": "Time period", + "editor.record.form.temporalExtents.date": "Date", + "editor.record.form.temporalExtents.range": "Date range", "editor.record.form.updateFrequency": "Update frequency", "editor.record.form.updateFrequency.planned": "The data should be updated regularly.", "editor.record.loadError.body": "The record could not be loaded:", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "Error publishing record", "editor.record.publishSuccess.body": "The record was successfully published!", "editor.record.publishSuccess.title": "Publish success", + "editor.record.saveStatus.asDraftOnly": "Saved as draft only - not published yet", + "editor.record.saveStatus.draftWithChangesPending": "Saved as draft - changes are pending", + "editor.record.saveStatus.recordUpToDate": "Record is up to date", "editor.record.upToDate": "This record is up to date", "externalviewer.dataset.unnamed": "Datahub layer", "facets.block.title.OrgForResource": "Organisation", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Publications 0 → 9", "organisations.sortBy.recordCountDesc": "Publications 9 → 0", + "organization.header.recordCount": "{count, plural, =0{data} one{data} other{datas}}", + "organization.details.publishedDataset": "{count, plural, =0{published dataset} one{published dataset} other{published datasets}}", + "organization.details.mailContact": "Contact by email", + "organization.datasets": "Datasets", + "organization.lastPublishedDatasets": "Last published datasets", + "organization.lastPublishedDatasets.searchAllButton": "Search all", "pagination.nextPage": "Next page", "pagination.page": "page", "pagination.pageOf": "of", @@ -274,7 +296,7 @@ "record.metadata.preview": "Preview", "record.metadata.producer": "Data producer", "record.metadata.publication": "Date of publication", - "record.metadata.publications": "publications", + "record.metadata.publications": "{count, plural, =0{publication} one{publication} other{publications}}", "record.metadata.quality": "Metadata Quality", "record.metadata.quality.contact.failed": "Contact is not specified", "record.metadata.quality.contact.success": "Contact is specified", @@ -335,8 +357,10 @@ "search.autocomplete.error": "Suggestions could not be fetched:", "search.error.couldNotReachApi": "The API could not be reached", "search.error.receivedError": "An error was received", - "search.error.recordNotFound": "The record with identifier \"{ id }\" could not be found.", "search.error.recordHasnolink": "This record currently has no link yet, please come back later.", + "search.error.recordNotFound": "The record with identifier \"{ id }\" could not be found.", + "search.error.organizationNotFound": "This organization could not be found.", + "search.error.organizationHasNoDataset": "This organization has no dataset yet.", "search.field.any.placeholder": "Search datasets ...", "search.field.sortBy": "Sort by:", "search.filters.clear": "Reset", diff --git a/translations/es.json b/translations/es.json index efbc8c4b50..0084cb65a2 100644 --- a/translations/es.json +++ b/translations/es.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de datos", - "catalog.figures.organisations": "organizaciones", + "catalog.figures.organizations": "organizaciones", "chart.aggregation.average": "promedio", "chart.aggregation.count": "conteo", "chart.aggregation.max": "máximo", @@ -17,15 +18,22 @@ "chart.type.line": "gráfico de líneas", "chart.type.lineSmooth": "gráfico de líneas suave", "chart.type.pie": "gráfico circular", + "dashboard.catalog.allRecords": "", + "dashboard.catalog.calendar": "", + "dashboard.catalog.contacts": "", + "dashboard.catalog.discussion": "", + "dashboard.catalog.thesaurus": "", "dashboard.createRecord": "", + "dashboard.labels.catalog": "Catálogo", "dashboard.labels.mySpace": "Mi espacio", "dashboard.records.all": "Catálogo", + "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Mis borradores", "dashboard.records.myLibrary": "Mi biblioteca", - "dashboard.records.myOrg": "Organización", "dashboard.records.myRecords": "Mis Registros", "dashboard.records.publishedRecords": "", "dashboard.records.search": "Buscar \"{searchText}\"", + "dashboard.records.templates": "", "dashboard.records.userDetail": "", "dashboard.records.userEmail": "", "dashboard.records.username": "", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", "editor.record.form.resourceUpdated": "", + "editor.record.form.temporalExtents": "", + "editor.record.form.temporalExtents.addDate": "", + "editor.record.form.temporalExtents.addRange": "", + "editor.record.form.temporalExtents.date": "", + "editor.record.form.temporalExtents.range": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "", "editor.record.publishSuccess.body": "", "editor.record.publishSuccess.title": "", + "editor.record.saveStatus.asDraftOnly": "", + "editor.record.saveStatus.draftWithChangesPending": "", + "editor.record.saveStatus.recordUpToDate": "", "editor.record.upToDate": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", + "organization.details.mailContact": "", + "organization.datasets": "", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", @@ -273,7 +295,7 @@ "record.metadata.preview": "", "record.metadata.producer": "", "record.metadata.publication": "", - "record.metadata.publications": "", + "record.metadata.publications": "{count, plural, =0{} one{} other{}}", "record.metadata.quality": "", "record.metadata.quality.contact.failed": "", "record.metadata.quality.contact.success": "", @@ -334,8 +356,10 @@ "search.autocomplete.error": "", "search.error.couldNotReachApi": "", "search.error.receivedError": "", - "search.error.recordNotFound": "", "search.error.recordHasnolink": "", + "search.error.recordNotFound": "", + "search.error.organizationNotFound": "", + "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", diff --git a/translations/fr.json b/translations/fr.json index eef5cc930b..da4af9f738 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "Se connecter", "catalog.figures.datasets": "{count, plural, =0{données} one{donnée} other{données}}", - "catalog.figures.organisations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", + "catalog.figures.organizations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", "chart.aggregation.average": "moyenne", "chart.aggregation.count": "nombre", "chart.aggregation.max": "maximum", @@ -17,15 +18,22 @@ "chart.type.line": "ligne", "chart.type.lineSmooth": "ligne lisse", "chart.type.pie": "camembert", + "dashboard.catalog.allRecords": "Fiches de métadonnée", + "dashboard.catalog.calendar": "Calendrier", + "dashboard.catalog.contacts": "Annuaire", + "dashboard.catalog.discussion": "Discussions", + "dashboard.catalog.thesaurus": "Thesaurus", "dashboard.createRecord": "Nouvel enregistrement", + "dashboard.labels.catalog": "Catalogue", "dashboard.labels.mySpace": "Mon espace", "dashboard.records.all": "Catalogue", + "dashboard.records.hasDraft": "brouillon", "dashboard.records.myDraft": "Mes brouillons", "dashboard.records.myLibrary": "Ma bibliothèque", - "dashboard.records.myOrg": "Mon organisation", "dashboard.records.myRecords": "Mes fiches publiées", "dashboard.records.publishedRecords": "{count, plural, =1{donnée publiée} other{données publiées}}", "dashboard.records.search": "Résultats pour \"{searchText}\"", + "dashboard.records.templates": "Modèles pré-remplis", "dashboard.records.userDetail": "Nom", "dashboard.records.userEmail": "Email", "dashboard.records.username": "Nom d'utilisateur", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Non-reconnue ou absente", "editor.record.form.resourceUpdated": "Date de dernière révision", + "editor.record.form.temporalExtents": "Étendue temporelle", + "editor.record.form.temporalExtents.addDate": "Date déterminée", + "editor.record.form.temporalExtents.addRange": "Période de temps", + "editor.record.form.temporalExtents.date": "Date concernée", + "editor.record.form.temporalExtents.range": "Période concernée", "editor.record.form.updateFrequency": "Fréquence de mise à jour", "editor.record.form.updateFrequency.planned": "Ces données doivent être mise à jour régulièrement.", "editor.record.loadError.body": "", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "", "editor.record.publishSuccess.body": "", "editor.record.publishSuccess.title": "", + "editor.record.saveStatus.asDraftOnly": "Brouillon enregistré - fiche non publiée", + "editor.record.saveStatus.draftWithChangesPending": "Brouillon enregistré - modifications en cours", + "editor.record.saveStatus.recordUpToDate": "La fiche publiée est à jour", "editor.record.upToDate": "", "externalviewer.dataset.unnamed": "Couche du datahub", "facets.block.title.OrgForResource": "Organisation", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "Nom Z → A", "organisations.sortBy.recordCountAsc": "Données 0 → 9", "organisations.sortBy.recordCountDesc": "Données 9 → 0", + "organization.header.recordCount": "{count, plural, =0{donnée} one{donnée} other{données}}", + "organization.details.publishedDataset": "{count, plural, =0{donnée publiée} one{donnée publiée} other{données publiées}}", + "organization.details.mailContact": "Contacter par mail", + "organization.datasets": "Données", + "organization.lastPublishedDatasets": "Dernières données publiées", + "organization.lastPublishedDatasets.searchAllButton": "Rechercher tous", "pagination.nextPage": "Page suivante", "pagination.page": "page", "pagination.pageOf": "sur", @@ -274,7 +296,7 @@ "record.metadata.preview": "Aperçu", "record.metadata.producer": "Producteur de la donnée", "record.metadata.publication": "Date de publication", - "record.metadata.publications": "données", + "record.metadata.publications": "{count, plural, =0{donnée} one{donnée} other{données}}", "record.metadata.quality": "Qualité des métadonnées", "record.metadata.quality.contact.failed": "Contact n'est pas renseigné", "record.metadata.quality.contact.success": "Contact est renseigné", @@ -335,8 +357,10 @@ "search.autocomplete.error": "Les suggestions ne peuvent pas être récupérées", "search.error.couldNotReachApi": "Problème de connexion à l'API", "search.error.receivedError": "Erreur retournée", - "search.error.recordNotFound": "Cette donnée n'a pu être trouvée.", "search.error.recordHasnolink": "Ce dataset n'a pas encore de lien, réessayez plus tard s'il vous plaît.", + "search.error.recordNotFound": "Cette donnée n'a pu être trouvée.", + "search.error.organizationNotFound": "L'organisation n'a pas pu être trouvée.", + "search.error.organizationHasNoDataset": "Cette organisation n'a pas encore de données.", "search.field.any.placeholder": "Rechercher une donnée...", "search.field.sortBy": "Trier par :", "search.filters.clear": "Réinitialiser", diff --git a/translations/it.json b/translations/it.json index 209c87ab2e..f5679702bd 100644 --- a/translations/it.json +++ b/translations/it.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organisations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", + "catalog.figures.organizations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", "chart.aggregation.average": "media", "chart.aggregation.count": "conteggio", "chart.aggregation.max": "massimo", @@ -17,15 +18,22 @@ "chart.type.line": "grafico a linee", "chart.type.lineSmooth": "grafico a linea liscia", "chart.type.pie": "grafico a torta", + "dashboard.catalog.allRecords": "", + "dashboard.catalog.calendar": "", + "dashboard.catalog.contacts": "", + "dashboard.catalog.discussion": "", + "dashboard.catalog.thesaurus": "", "dashboard.createRecord": "Crea un record", + "dashboard.labels.catalog": "Catalogo", "dashboard.labels.mySpace": "Il mio spazio", "dashboard.records.all": "Catalogo", + "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Le mie bozze", "dashboard.records.myLibrary": "La mia biblioteca", - "dashboard.records.myOrg": "La mia organizzazione", "dashboard.records.myRecords": "I miei dati", "dashboard.records.publishedRecords": "dati pubblicati", "dashboard.records.search": "Risultati per \"{searchText}\"", + "dashboard.records.templates": "", "dashboard.records.userDetail": "Nome", "dashboard.records.userEmail": "Email", "dashboard.records.username": "Nome utente", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Non riconosciuta o assente", "editor.record.form.resourceUpdated": "", + "editor.record.form.temporalExtents": "", + "editor.record.form.temporalExtents.addDate": "", + "editor.record.form.temporalExtents.addRange": "", + "editor.record.form.temporalExtents.date": "", + "editor.record.form.temporalExtents.range": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "", "editor.record.publishSuccess.body": "", "editor.record.publishSuccess.title": "", + "editor.record.saveStatus.asDraftOnly": "", + "editor.record.saveStatus.draftWithChangesPending": "", + "editor.record.saveStatus.recordUpToDate": "", "editor.record.upToDate": "", "externalviewer.dataset.unnamed": "Layer del datahub", "facets.block.title.OrgForResource": "Organizzazione", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "Nome Z → A", "organisations.sortBy.recordCountAsc": "Dati 0 → 9", "organisations.sortBy.recordCountDesc": "Dati 9 → 0", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", + "organization.details.mailContact": "", + "organization.datasets": "", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Pagina successiva", "pagination.page": "pagina", "pagination.pageOf": "di", @@ -273,7 +295,7 @@ "record.metadata.preview": "Anteprima", "record.metadata.producer": "Produttore dei dati", "record.metadata.publication": "Data di pubblicazione", - "record.metadata.publications": "pubblicazioni", + "record.metadata.publications": "{count, plural, =0{pubblicazione} one{pubblicazione} other{pubblicazioni}}", "record.metadata.quality": "Qualità dei metadati", "record.metadata.quality.contact.failed": "Il contatto non è specificato", "record.metadata.quality.contact.success": "Il contatto è specificato", @@ -334,8 +356,10 @@ "search.autocomplete.error": "Impossibile recuperare le suggerimenti", "search.error.couldNotReachApi": "Problema di connessione all'API", "search.error.receivedError": "Errore ricevuto", - "search.error.recordNotFound": "Impossibile trovare questo dato", "search.error.recordHasnolink": "", + "search.error.recordNotFound": "Impossibile trovare questo dato", + "search.error.organizationNotFound": "", + "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "Cerca un dato...", "search.field.sortBy": "Ordina per:", "search.filters.clear": "Ripristina", diff --git a/translations/nl.json b/translations/nl.json index 424abeffc5..283069f5a9 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "datasets", - "catalog.figures.organisations": "organisaties", + "catalog.figures.organizations": "organisaties", "chart.aggregation.average": "gemiddelde", "chart.aggregation.count": "aantal", "chart.aggregation.max": "max", @@ -17,15 +18,22 @@ "chart.type.line": "lijndiagram", "chart.type.lineSmooth": "glad lijndiagram", "chart.type.pie": "cirkeldiagram", + "dashboard.catalog.allRecords": "", + "dashboard.catalog.calendar": "", + "dashboard.catalog.contacts": "", + "dashboard.catalog.discussion": "", + "dashboard.catalog.thesaurus": "", "dashboard.createRecord": "", + "dashboard.labels.catalog": "Catalogus", "dashboard.labels.mySpace": "Mijn ruimte", "dashboard.records.all": "Catalogus", + "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Mijn concepten", "dashboard.records.myLibrary": "Mijn bibliotheek", - "dashboard.records.myOrg": "Organisatie", "dashboard.records.myRecords": "Mijn Records", "dashboard.records.publishedRecords": "", "dashboard.records.search": "Zoeken naar \"{searchText}\"", + "dashboard.records.templates": "", "dashboard.records.userDetail": "", "dashboard.records.userEmail": "", "dashboard.records.username": "", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", "editor.record.form.resourceUpdated": "", + "editor.record.form.temporalExtents": "", + "editor.record.form.temporalExtents.addDate": "", + "editor.record.form.temporalExtents.addRange": "", + "editor.record.form.temporalExtents.date": "", + "editor.record.form.temporalExtents.range": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "", "editor.record.publishSuccess.body": "", "editor.record.publishSuccess.title": "", + "editor.record.saveStatus.asDraftOnly": "", + "editor.record.saveStatus.draftWithChangesPending": "", + "editor.record.saveStatus.recordUpToDate": "", "editor.record.upToDate": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", + "organization.details.mailContact": "", + "organization.datasets": "", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", @@ -273,7 +295,7 @@ "record.metadata.preview": "", "record.metadata.producer": "", "record.metadata.publication": "", - "record.metadata.publications": "", + "record.metadata.publications": "{count, plural, =0{} one{} other{}}", "record.metadata.quality": "", "record.metadata.quality.contact.failed": "", "record.metadata.quality.contact.success": "", @@ -334,8 +356,10 @@ "search.autocomplete.error": "", "search.error.couldNotReachApi": "", "search.error.receivedError": "", - "search.error.recordNotFound": "", "search.error.recordHasnolink": "", + "search.error.recordNotFound": "", + "search.error.organizationNotFound": "", + "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", diff --git a/translations/pt.json b/translations/pt.json index 9ec6b6be38..0a6819fdad 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de dados", - "catalog.figures.organisations": "organizações", + "catalog.figures.organizations": "organizações", "chart.aggregation.average": "média", "chart.aggregation.count": "contagem", "chart.aggregation.max": "máximo", @@ -17,15 +18,22 @@ "chart.type.line": "gráfico de linha", "chart.type.lineSmooth": "gráfico de linha suave", "chart.type.pie": "gráfico de pizza", + "dashboard.catalog.allRecords": "", + "dashboard.catalog.calendar": "", + "dashboard.catalog.contacts": "", + "dashboard.catalog.discussion": "", + "dashboard.catalog.thesaurus": "", "dashboard.createRecord": "", + "dashboard.labels.catalog": "Catálogo", "dashboard.labels.mySpace": "Meu espaço", "dashboard.records.all": "Catálogo", + "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Meus rascunhos", "dashboard.records.myLibrary": "Minha biblioteca", - "dashboard.records.myOrg": "Organização", "dashboard.records.myRecords": "Meus Registros", "dashboard.records.publishedRecords": "", "dashboard.records.search": "Buscar por \"{searchText}\"", + "dashboard.records.templates": "", "dashboard.records.userDetail": "", "dashboard.records.userEmail": "", "dashboard.records.username": "", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", "editor.record.form.resourceUpdated": "", + "editor.record.form.temporalExtents": "", + "editor.record.form.temporalExtents.addDate": "", + "editor.record.form.temporalExtents.addRange": "", + "editor.record.form.temporalExtents.date": "", + "editor.record.form.temporalExtents.range": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "", "editor.record.publishSuccess.body": "", "editor.record.publishSuccess.title": "", + "editor.record.saveStatus.asDraftOnly": "", + "editor.record.saveStatus.draftWithChangesPending": "", + "editor.record.saveStatus.recordUpToDate": "", "editor.record.upToDate": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", + "organization.details.mailContact": "", + "organization.datasets": "", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", @@ -273,7 +295,7 @@ "record.metadata.preview": "", "record.metadata.producer": "", "record.metadata.publication": "", - "record.metadata.publications": "", + "record.metadata.publications": "{count, plural, =0{} one{} other{}}", "record.metadata.quality": "", "record.metadata.quality.contact.failed": "", "record.metadata.quality.contact.success": "", @@ -334,8 +356,10 @@ "search.autocomplete.error": "", "search.error.couldNotReachApi": "", "search.error.receivedError": "", - "search.error.recordNotFound": "", "search.error.recordHasnolink": "", + "search.error.recordNotFound": "", + "search.error.organizationNotFound": "", + "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", diff --git a/translations/sk.json b/translations/sk.json index b7a103afe4..4f45e3fea8 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -1,7 +1,8 @@ { + "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasety} one{dataset} other{datasety}}", - "catalog.figures.organisations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", + "catalog.figures.organizations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", "chart.aggregation.average": "priemer", "chart.aggregation.count": "počet", "chart.aggregation.max": "maximum", @@ -17,15 +18,22 @@ "chart.type.line": "čiarový graf", "chart.type.lineSmooth": "vyhladený čiarový graf", "chart.type.pie": "koláčový graf", + "dashboard.catalog.allRecords": "", + "dashboard.catalog.calendar": "", + "dashboard.catalog.contacts": "", + "dashboard.catalog.discussion": "", + "dashboard.catalog.thesaurus": "", "dashboard.createRecord": "", + "dashboard.labels.catalog": "Katalóg", "dashboard.labels.mySpace": "Môj priestor", "dashboard.records.all": "Katalóg", + "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Moje koncepty", "dashboard.records.myLibrary": "Moja knižnica", - "dashboard.records.myOrg": "Organizácia", "dashboard.records.myRecords": "Moje záznamy", "dashboard.records.publishedRecords": "{count, plural, =1{zverejnený záznam} other{zverejnených záznamov}}", "dashboard.records.search": "Hľadať \"{searchText}\"", + "dashboard.records.templates": "", "dashboard.records.userDetail": "Meno", "dashboard.records.userEmail": "Email", "dashboard.records.username": "Užívateľské meno", @@ -156,6 +164,11 @@ "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Neznáme alebo chýbajúce", "editor.record.form.resourceUpdated": "", + "editor.record.form.temporalExtents": "", + "editor.record.form.temporalExtents.addDate": "", + "editor.record.form.temporalExtents.addRange": "", + "editor.record.form.temporalExtents.date": "", + "editor.record.form.temporalExtents.range": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -167,6 +180,9 @@ "editor.record.publishError.title": "", "editor.record.publishSuccess.body": "", "editor.record.publishSuccess.title": "", + "editor.record.saveStatus.asDraftOnly": "", + "editor.record.saveStatus.draftWithChangesPending": "", + "editor.record.saveStatus.recordUpToDate": "", "editor.record.upToDate": "", "externalviewer.dataset.unnamed": "", "facets.block.title.OrgForResource": "Organizácia", @@ -236,6 +252,12 @@ "organisations.sortBy.nameDesc": "Názov Z → A", "organisations.sortBy.recordCountAsc": "Publikácie 0 → 9", "organisations.sortBy.recordCountDesc": "Publikácie 9 → 0", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", + "organization.details.mailContact": "", + "organization.datasets": "", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Ďalšia stránka", "pagination.page": "strana", "pagination.pageOf": "z", @@ -272,8 +294,8 @@ "record.metadata.owner": "Katalóg pôvodu", "record.metadata.preview": "Náhľad", "record.metadata.producer": "", - "record.metadata.publication": "", - "record.metadata.publications": "publikácie", + "record.metadata.publication": "publikácia", + "record.metadata.publications": "{count, plural, =0{publikácia} one{publikácia} other{publikácie}}", "record.metadata.quality": "Kvalita metadát", "record.metadata.quality.contact.failed": "Kontakt nie je uvedený", "record.metadata.quality.contact.success": "Kontakt je uvedený", @@ -334,8 +356,10 @@ "search.autocomplete.error": "Návrhy sa nepodarilo načítať:", "search.error.couldNotReachApi": "K rozhraniu API sa nepodarilo pripojiť", "search.error.receivedError": "Bola zaznamenaná chyba", - "search.error.recordNotFound": "Záznam s identifikátorom \"{ id }\" sa nepodarilo nájsť.", "search.error.recordHasnolink": "", + "search.error.recordNotFound": "Záznam s identifikátorom \"{ id }\" sa nepodarilo nájsť.", + "search.error.organizationNotFound": "", + "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "Hľadať datasety ...", "search.field.sortBy": "Zoradiť podľa:", "search.filters.clear": "Obnoviť",