diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4420beff93..72cb07f551 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -42,4 +42,4 @@ Please only check items relevant to your contribution. Thank you very much for y Please give credit to the sponsor of this work if possible. --> -**This work is sponsored by [Organization ABC](xx)**. + diff --git a/apps/datafeeder/src/app/app.module.ts b/apps/datafeeder/src/app/app.module.ts index 11a4cb3e45..7f0117eef5 100644 --- a/apps/datafeeder/src/app/app.module.ts +++ b/apps/datafeeder/src/app/app.module.ts @@ -1,7 +1,10 @@ import { BrowserModule } from '@angular/platform-browser' import { importProvidersFrom, NgModule } from '@angular/core' import { ApiModule, Configuration } from '@geonetwork-ui/data-access/datafeeder' -import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { + ProgressBarComponent, + UiWidgetsModule, +} from '@geonetwork-ui/ui/widgets' import { StoreModule } from '@ngrx/store' import { StoreDevtoolsModule } from '@ngrx/store-devtools' import { environment } from '../environments/environment' @@ -84,6 +87,7 @@ export function apiConfigurationFactory() { [DATAFEEDER_STATE_KEY]: reducer, }), !environment.production ? StoreDevtoolsModule.instrument() : [], + ProgressBarComponent, ], providers: [importProvidersFrom(FeatureAuthModule)], bootstrap: [AppComponent], diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 088b06fb4e..f62bf95973 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -331,10 +331,12 @@ describe('dataset pages', () => { .children('button') .should('have.length.gt', 1) }) - it('should display the map', () => { + it('should display the map and the legend', () => { cy.get('@previewSection') .find('gn-ui-map-container') .should('be.visible') + + cy.get('@previewSection').find('gn-ui-map-legend').should('be.visible') }) it('should display the table', () => { cy.get('@previewSection') @@ -529,12 +531,12 @@ describe('dataset pages', () => { }) it('should not display carousel dot button for 4 link cards', () => { cy.get('datahub-record-otherlinks') - .find('.carousel-step-dot') + .find('.pagination-dot') .should('exist') }) it('should not display carousel dot button for 2 API cards', () => { cy.get('datahub-record-apis') - .find('.carousel-step-dot') + .find('.pagination-dot') .should('not.exist') }) }) diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index 7a3bbdf750..aeee97225d 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -4,6 +4,7 @@ import { BrowserModule } from '@angular/platform-browser' import { Router, RouterModule } from '@angular/router' import { FeatureCatalogModule, + OrganisationsComponent, ORGANIZATION_PAGE_URL_TOKEN, ORGANIZATION_URL_TOKEN, } from '@geonetwork-ui/feature/catalog' @@ -29,19 +30,11 @@ import { RECORD_URL_TOKEN, } from '@geonetwork-ui/feature/search' import { - LinkCardComponent, THUMBNAIL_PLACEHOLDER, UiElementsModule, } from '@geonetwork-ui/ui/elements' -import { - PreviousNextButtonsComponent, - UiInputsModule, -} from '@geonetwork-ui/ui/inputs' -import { - BlockListComponent, - CarouselComponent, - UiLayoutModule, -} from '@geonetwork-ui/ui/layout' +import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { UiLayoutModule } from '@geonetwork-ui/ui/layout' import { UiSearchModule } from '@geonetwork-ui/ui/search' import { IgnApiDlComponent } from '@geonetwork-ui/feature/record' @@ -76,14 +69,16 @@ import { NewsPageComponent } from './home/news-page/news-page.component' import { OrganisationsPageComponent } from './home/organisations-page/organisations-page.component' import { SearchPageComponent } from './home/search/search-page/search-page.component' import { SearchFiltersComponent } from './home/search/search-filters/search-filters.component' -import { HeaderRecordComponent } from './record/header-record/header-record.component' import { NavigationBarComponent } from './record/navigation-bar/navigation-bar.component' import { RecordPageComponent } from './record/record-page/record-page.component' import { DatahubRouterService } from './router/datahub-router.service' import { NavigationMenuComponent } from './home/navigation-menu/navigation-menu.component' import { FormsModule } from '@angular/forms' import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' -import { LANGUAGES_LIST, UiCatalogModule } from '@geonetwork-ui/ui/catalog' +import { + LANGUAGES_LIST, + LanguageSwitcherComponent, +} from '@geonetwork-ui/ui/catalog' import { LOGIN_URL, METADATA_LANGUAGE, @@ -91,14 +86,8 @@ import { provideRepositoryUrl, } from '@geonetwork-ui/api/repository' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { RecordRelatedRecordsComponent } from './record/record-related-records/record-related-records.component' -import { RecordMetadataComponent } from './record/record-metadata/record-metadata.component' -import { RecordOtherlinksComponent } from './record/record-otherlinks/record-otherlinks.component' -import { RecordDownloadsComponent } from './record/record-downloads/record-downloads.component' -import { RecordApisComponent } from './record/record-apis/record-apis.component' 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 { @@ -107,16 +96,11 @@ import { MAP_VIEW_CONSTRAINTS, } from '@geonetwork-ui/ui/map' import { - matAccountBoxOutline, matAddOutline, - matCloseOutline, - matEditOutline, matExpandMoreOutline, - matLocationSearchingOutline, matMenuOutline, matMoreHorizOutline, matRemoveOutline, - matSendOutline, matStarOutline, } from '@ng-icons/material-icons/outline' import { NgIconsModule, provideNgIconsConfig } from '@ng-icons/core' @@ -134,8 +118,6 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] HomePageComponent, HomeHeaderComponent, HeaderBadgeButtonComponent, - HeaderRecordComponent, - RecordPageComponent, SearchFiltersComponent, NavigationBarComponent, NewsPageComponent, @@ -144,12 +126,6 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] LastCreatedComponent, KeyFiguresComponent, NavigationMenuComponent, - RecordRelatedRecordsComponent, - RecordUserFeedbacksComponent, - RecordMetadataComponent, - RecordOtherlinksComponent, - RecordDownloadsComponent, - RecordApisComponent, ], imports: [ BrowserModule, @@ -188,29 +164,21 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] UiDatavizModule, FormsModule, UiInputsModule, - UiCatalogModule, MatTabsModule, UiWidgetsModule, - LinkCardComponent, - CarouselComponent, - BlockListComponent, - PreviousNextButtonsComponent, RecordMetaComponent, LetDirective, // FIXME: these imports are required by non-standalone components and should be removed once all components have been made standalone NgIconsModule.withIcons({ matMenuOutline, matRemoveOutline, - matCloseOutline, matMoreHorizOutline, matAddOutline, matExpandMoreOutline, - matEditOutline, - matAccountBoxOutline, matStarOutline, - matLocationSearchingOutline, - matSendOutline, }), + OrganisationsComponent, + LanguageSwitcherComponent, MatButtonToggleModule, MatIconModule, DsfrHeaderModule, 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 index 6c27e7f32c..68d4b78084 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.html +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.html @@ -77,9 +77,7 @@
-
- -
+ 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 index 339dec1b85..4d43c8f2ac 100644 --- 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 @@ -1,9 +1,4 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - DebugElement, - NO_ERRORS_SCHEMA, -} from '@angular/core' +import { ChangeDetectionStrategy, DebugElement } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { SearchFacade } from '@geonetwork-ui/feature/search' import { TranslateModule } from '@ngx-translate/core' @@ -14,22 +9,6 @@ import { datasetRecordsFixture, someOrganizationsFixture, } from '@geonetwork-ui/common/fixtures' -import { AsyncPipe, NgForOf, NgIf } from '@angular/common' -import { - ButtonComponent, - PreviousNextButtonsComponent, -} from '@geonetwork-ui/ui/inputs' -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' @@ -37,10 +16,6 @@ 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({ @@ -58,7 +33,7 @@ const manyDatasets = datasetRecordsFixture().concat(datasetRecordsFixture()[0]) const organizationIsLoading = new BehaviorSubject(false) const totalPages = new BehaviorSubject(10) -const currentPage = new BehaviorSubject(0) +const currentPage = new BehaviorSubject(1) const results = new BehaviorSubject(manyDatasets) const desiredPageSize = 3 @@ -72,11 +47,9 @@ class 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) + paginate = jest.fn((newPage) => { + currentPage.next(newPage) return new SearchFacadeMock() }) } @@ -89,25 +62,8 @@ describe('OrganizationDetailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [], - schemas: [NO_ERRORS_SCHEMA], imports: [ - AsyncPipe, - NgIf, - ButtonComponent, - TranslateModule, - CarouselComponent, - BlockListComponent, - LetDirective, - LinkCardComponent, - NgForOf, - PreviousNextButtonsComponent, - UiElementsModule, - UiSearchModule, - MaxLinesComponent, - UiDatavizModule, - RouterLink, - UiWidgetsModule, + OrganizationDetailsComponent, TranslateModule.forRoot(), RouterTestingModule, ], @@ -120,10 +76,6 @@ describe('OrganizationDetailsComponent', () => { provide: SearchFacade, useClass: SearchFacadeMock, }, - { - provide: ChangeDetectorRef, - useValue: changeDetectorRefMock, - }, ], }) .overrideComponent(OrganizationDetailsComponent, { 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 index 620324cf4f..17245bf9f0 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.ts +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.ts @@ -4,27 +4,24 @@ import { Input, OnDestroy, OnInit, - ViewChild, } from '@angular/core' -import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common' +import { CommonModule } from '@angular/common' import { CatalogRecord, Organization, } from '@geonetwork-ui/common/domain/model/record' -import { - ButtonComponent, - PreviousNextButtonsComponent, -} from '@geonetwork-ui/ui/inputs' import { TranslateModule } from '@ngx-translate/core' import { - BlockListComponent, - CarouselComponent, MaxLinesComponent, + Paginable, + PaginationDotsComponent, + PreviousNextButtonsComponent, } from '@geonetwork-ui/ui/layout' import { LetDirective } from '@ngrx/component' import { + ErrorComponent, ErrorType, - LinkCardComponent, + RelatedRecordCardComponent, UiElementsModule, } from '@geonetwork-ui/ui/elements' import { UiSearchModule } from '@geonetwork-ui/ui/search' @@ -41,7 +38,10 @@ 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' +import { + SpinningLoaderComponent, + UiWidgetsModule, +} from '@geonetwork-ui/ui/widgets' import { startWith } from 'rxjs/operators' @Component({ @@ -51,15 +51,9 @@ import { startWith } from 'rxjs/operators' changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ - AsyncPipe, - NgIf, - ButtonComponent, + CommonModule, TranslateModule, - CarouselComponent, - BlockListComponent, LetDirective, - LinkCardComponent, - NgForOf, PreviousNextButtonsComponent, UiElementsModule, UiSearchModule, @@ -67,34 +61,28 @@ import { startWith } from 'rxjs/operators' UiDatavizModule, RouterLink, UiWidgetsModule, - NgClass, + ErrorComponent, + SpinningLoaderComponent, + RelatedRecordCardComponent, + PaginationDotsComponent, ], }) -export class OrganizationDetailsComponent implements OnInit, OnDestroy { +export class OrganizationDetailsComponent + implements OnInit, OnDestroy, Paginable +{ 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) - } - subscriptions$: Subscription = new Subscription() isSearchFacadeLoading = true - totalPages = 0 - currentPage = 1 - isFirstPage = this.currentPage === 1 - isLastPage = false - currentOrganization$ = new BehaviorSubject(null) @Input() set organization(value: Organization) { this.currentOrganization$.next(value) } @Input() paginationContainerClass = 'w-full bottom-0 top-auto' - @ViewChild(BlockListComponent) list: BlockListComponent - lastPublishedDatasets$: Observable = this.currentOrganization$.pipe( switchMap((organization) => @@ -124,17 +112,31 @@ export class OrganizationDetailsComponent implements OnInit, OnDestroy { } get hasPagination() { - return this.totalPages > 1 + return this.pagesCount > 1 } - changeStepOrPage(direction: string) { - if (direction === 'next') { - this.searchFacade.paginate(this.currentPage + 1) - } else { - this.searchFacade.paginate(this.currentPage - 1) - } - } + pagesCount_ = 0 + currentPage_ = 1 + // Paginable API + get currentPage() { + return this.currentPage_ + } + get pagesCount() { + return this.pagesCount_ + } + get isFirstPage() { + return this.currentPage === 1 + } + get isLastPage() { + return this.currentPage === this.pagesCount + } + goToPrevPage() { + this.searchFacade.paginate(this.currentPage - 1) + } + goToNextPage() { + this.searchFacade.paginate(this.currentPage + 1) + } goToPage(page: number) { this.searchFacade.paginate(page) } @@ -144,24 +146,12 @@ export class OrganizationDetailsComponent implements OnInit, OnDestroy { 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 - } - ) + ]).subscribe(([isSearchFacadeLoading, totalPages, currentPage]) => { + this.isSearchFacadeLoading = isSearchFacadeLoading + this.pagesCount_ = totalPages + this.currentPage_ = currentPage + }) ) } 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 index 7028299361..a43c64c1b1 100644 --- 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 @@ -3,7 +3,6 @@ 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 { someOrganizationsFixture } from '@geonetwork-ui/common/fixtures' @@ -33,7 +32,6 @@ describe('OrganizationHeaderComponent', () => { OrganizationHeaderComponent, UiInputsModule, TranslateModule, - UiCatalogModule, NgIf, AsyncPipe, TranslateModule.forRoot(), 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 index a59e7de46b..37b3216a59 100644 --- a/apps/datahub/src/app/organization/organization-header/organization-header.component.ts +++ b/apps/datahub/src/app/organization/organization-header/organization-header.component.ts @@ -1,8 +1,10 @@ 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 { + NavigationButtonComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' import { Organization } from '@geonetwork-ui/common/domain/model/record' import { AsyncPipe, Location, NgIf } from '@angular/common' import { ErrorType, UiElementsModule } from '@geonetwork-ui/ui/elements' @@ -16,6 +18,8 @@ import { matFolderOutline, matOpenInNewOutline, } from '@ng-icons/material-icons/outline' +import { LanguageSwitcherComponent } from '@geonetwork-ui/ui/catalog' +import { matArrowBack } from '@ng-icons/material-icons/baseline' @Component({ selector: 'datahub-organization-header', @@ -26,14 +30,15 @@ import { imports: [ UiInputsModule, TranslateModule, - UiCatalogModule, NgIf, AsyncPipe, UiElementsModule, NgIconComponent, + LanguageSwitcherComponent, + NavigationButtonComponent, ], providers: [ - provideIcons({ matFolderOutline, matOpenInNewOutline }), + provideIcons({ matFolderOutline, matOpenInNewOutline, matArrowBack }), provideNgIconsConfig({ size: '1.5em', }), diff --git a/apps/datahub/src/app/record/header-record/header-record.component.spec.ts b/apps/datahub/src/app/record/header-record/header-record.component.spec.ts index 8381d548d5..5d30a6cfcf 100644 --- a/apps/datahub/src/app/record/header-record/header-record.component.spec.ts +++ b/apps/datahub/src/app/record/header-record/header-record.component.spec.ts @@ -1,12 +1,12 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures' -import { MdViewFacade } from '@geonetwork-ui/feature/record' import { SearchService } from '@geonetwork-ui/feature/search' import { TranslateModule } from '@ngx-translate/core' -import { BehaviorSubject } from 'rxjs' import { HeaderRecordComponent } from './header-record.component' +import { MockBuilder, MockProvider } from 'ng-mocks' +import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' +import { MdViewFacade } from '@geonetwork-ui/feature/record' jest.mock('@geonetwork-ui/util/app-config', () => ({ getThemeConfig: () => ({ @@ -20,30 +20,20 @@ jest.mock('@geonetwork-ui/util/app-config', () => ({ }, })) -const searchServiceMock = { - updateFilters: jest.fn(), -} - -class MdViewFacadeMock { - mapApiLinks$ = new BehaviorSubject([]) - geoDataLinks$ = new BehaviorSubject([]) -} - describe('HeaderRecordComponent', () => { let component: HeaderRecordComponent let fixture: ComponentFixture + beforeEach(() => MockBuilder(HeaderRecordComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [HeaderRecordComponent], imports: [TranslateModule.forRoot()], - schemas: [NO_ERRORS_SCHEMA], providers: [ - { provide: SearchService, useValue: searchServiceMock }, - { - provide: MdViewFacade, - useClass: MdViewFacadeMock, - }, + MockProvider(MdViewFacade), + MockProvider(SearchService, { + updateFilters: jest.fn(), + }), ], }).compileComponents() }) @@ -53,7 +43,7 @@ describe('HeaderRecordComponent', () => { component = fixture.componentInstance component.metadata = { ...datasetRecordsFixture()[0], - } + } as DatasetRecord fixture.detectChanges() }) @@ -63,8 +53,9 @@ describe('HeaderRecordComponent', () => { describe('#back', () => { it('searchFilter updateSearch', () => { + const searchService = TestBed.inject(SearchService) component.back() - expect(searchServiceMock.updateFilters).toHaveBeenCalledWith({}) + expect(searchService.updateFilters).toHaveBeenCalledWith({}) }) }) }) diff --git a/apps/datahub/src/app/record/header-record/header-record.component.ts b/apps/datahub/src/app/record/header-record/header-record.component.ts index 9d92672674..dc0c3a481a 100644 --- a/apps/datahub/src/app/record/header-record/header-record.component.ts +++ b/apps/datahub/src/app/record/header-record/header-record.component.ts @@ -1,16 +1,39 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { SearchService } from '@geonetwork-ui/feature/search' +import { + FavoriteStarComponent, + SearchService, +} from '@geonetwork-ui/feature/search' import { getGlobalConfig, getThemeConfig } from '@geonetwork-ui/util/app-config' import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' import { combineLatest, map } from 'rxjs' -import { TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { + BadgeComponent, + NavigationButtonComponent, +} from '@geonetwork-ui/ui/inputs' +import { LanguageSwitcherComponent } from '@geonetwork-ui/ui/catalog' +import { CommonModule } from '@angular/common' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { matLocationSearchingOutline } from '@ng-icons/material-icons/outline' +import { matArrowBack } from '@ng-icons/material-icons/baseline' @Component({ selector: 'datahub-header-record', templateUrl: './header-record.component.html', styleUrls: ['./header-record.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + NavigationButtonComponent, + LanguageSwitcherComponent, + TranslateModule, + FavoriteStarComponent, + BadgeComponent, + NgIcon, + ], + viewProviders: [provideIcons({ matLocationSearchingOutline, matArrowBack })], }) export class HeaderRecordComponent { @Input() metadata: DatasetRecord diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.html b/apps/datahub/src/app/record/record-apis/record-apis.component.html index 8dc5fcb827..4836e9e2b6 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.html +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.html @@ -6,13 +6,15 @@ record.metadata.api

- + { let component: RecordApisComponent let fixture: ComponentFixture + beforeEach(() => MockBuilder(RecordApisComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [RecordApisComponent], imports: [TranslateModule.forRoot()], - schemas: [NO_ERRORS_SCHEMA], providers: [ { provide: MdViewFacade, diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.ts b/apps/datahub/src/app/record/record-apis/record-apis.component.ts index 72c1970d3e..de5461c86b 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.ts +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.ts @@ -7,13 +7,39 @@ import { } from '@angular/core' import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' -import { CarouselComponent } from '@geonetwork-ui/ui/layout' +import { + CarouselComponent, + PreviousNextButtonsComponent, +} from '@geonetwork-ui/ui/layout' +import { + ApiCardComponent, + RecordApiFormComponent, +} from '@geonetwork-ui/ui/elements' +import { CommonModule } from '@angular/common' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { matCloseOutline } from '@ng-icons/material-icons/outline' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'datahub-record-apis', templateUrl: './record-apis.component.html', styleUrls: ['./record-apis.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + CarouselComponent, + PreviousNextButtonsComponent, + ApiCardComponent, + RecordApiFormComponent, + NgIcon, + TranslateModule, + ], + viewProviders: [ + provideIcons({ + matCloseOutline, + }), + ], }) export class RecordApisComponent implements OnInit { @ViewChild(CarouselComponent) carousel: CarouselComponent @@ -35,22 +61,10 @@ export class RecordApisComponent implements OnInit { this.selectedApiLink = undefined } - get hasPagination() { - return this.carousel?.stepsCount > 1 - } - updateView() { this.changeDetector.detectChanges() } - get isFirstStep() { - return this.carousel?.isFirstStep - } - - get isLastStep() { - return this.carousel?.isLastStep - } - openRecordApiForm(link: DatasetServiceDistribution) { this.displayApiIgnForm = link.accessServiceProtocol === 'GPFDL' ? true : false @@ -67,12 +81,4 @@ export class RecordApisComponent implements OnInit { this.maxHeight = link === undefined ? '0px' : '700px' this.opacity = link === undefined ? 0 : 1 } - - changeStepOrPage(direction: string) { - if (direction === 'next') { - this.carousel?.slideToNext() - } else { - this.carousel?.slideToPrevious() - } - } } diff --git a/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts b/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts index faf9206080..4ac4a1fecd 100644 --- a/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts +++ b/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts @@ -6,11 +6,12 @@ import { } from '@angular/core/testing' import { BehaviorSubject, of, throwError } from 'rxjs' import { RecordDownloadsComponent } from './record-downloads.component' -import { Component, Input, NO_ERRORS_SCHEMA } from '@angular/core' import { By } from '@angular/platform-browser' import { DataService } from '@geonetwork-ui/feature/dataviz' import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { MockBuilder } from 'ng-mocks' +import { PopupAlertComponent } from '@geonetwork-ui/ui/widgets' // This is used to work around a very weird bug when comparing URL objects would fail // if the `searchParams` of the object wasn't accessed beforehand in some cases... @@ -71,34 +72,15 @@ class DataServiceMock { ) } -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-download-item', - template: '
', -}) -export class MockDownloadsListItemComponent { - @Input() link: DatasetOnlineResource -} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-popup-alert', - template: '
', -}) -export class MockPopupAlertComponent {} - -describe('DataDownloadsComponent', () => { +describe('RecordDownloadsComponent', () => { let component: RecordDownloadsComponent let fixture: ComponentFixture let facade + beforeEach(() => MockBuilder(RecordDownloadsComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - RecordDownloadsComponent, - MockDownloadsListItemComponent, - MockPopupAlertComponent, - ], providers: [ { provide: MdViewFacade, @@ -109,7 +91,6 @@ describe('DataDownloadsComponent', () => { useClass: DataServiceMock, }, ], - schemas: [NO_ERRORS_SCHEMA], }).compileComponents() facade = TestBed.inject(MdViewFacade) }) @@ -162,7 +143,7 @@ describe('DataDownloadsComponent', () => { // disable error handling in UI it.skip('shows an error', () => { const popup = fixture.debugElement.query( - By.directive(MockPopupAlertComponent) + By.directive(PopupAlertComponent) ) expect(popup).toBeTruthy() }) diff --git a/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts b/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts index d21343e025..e9293d7584 100644 --- a/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts +++ b/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts @@ -8,12 +8,16 @@ import { DatasetServiceDistribution, } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { CommonModule } from '@angular/common' +import { DownloadsListComponent } from '@geonetwork-ui/ui/elements' @Component({ selector: 'datahub-record-downloads', templateUrl: './record-downloads.component.html', styleUrls: ['./record-downloads.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, DownloadsListComponent], }) export class RecordDownloadsComponent { constructor(public facade: MdViewFacade, private dataService: DataService) {} diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts index d6363c9659..ddb562bcf2 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts @@ -1,30 +1,32 @@ -import { - Component, - EventEmitter, - Input, - NO_ERRORS_SCHEMA, - Output, -} from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { SourcesService } from '@geonetwork-ui/feature/catalog' -import { MapManagerService } from '@geonetwork-ui/feature/map' import { SearchService } from '@geonetwork-ui/feature/search' -import { ErrorComponent, ErrorType } from '@geonetwork-ui/ui/elements' +import { + ErrorComponent, + ErrorType, + ImageOverlayPreviewComponent, + MetadataCatalogComponent, + MetadataContactComponent, + MetadataInfoComponent, +} from '@geonetwork-ui/ui/elements' import { TranslateModule } from '@ngx-translate/core' import { BehaviorSubject, of } from 'rxjs' import { RecordMetadataComponent } from './record-metadata.component' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures' -import { MdViewFacade } from '@geonetwork-ui/feature/record' import { - CatalogRecord, - DatasetRecord, - DatasetServiceDistribution, - Individual, - Keyword, - Organization, -} from '@geonetwork-ui/common/domain/model/record' + DataViewComponent, + DataViewShareComponent, + MapViewComponent, + MdViewFacade, +} from '@geonetwork-ui/feature/record' +import { MockBuilder } from 'ng-mocks' +import { RecordDownloadsComponent } from '../record-downloads/record-downloads.component' +import { RecordOtherlinksComponent } from '../record-otherlinks/record-otherlinks.component' +import { RecordApisComponent } from '../record-apis/record-apis.component' +import { RecordRelatedRecordsComponent } from '../record-related-records/record-related-records.component' +import { MatTab, MatTabGroup } from '@angular/material/tabs' const SAMPLE_RECORD = { ...datasetRecordsFixture()[0], @@ -64,100 +66,6 @@ class OrganisationsServiceMock { ) } -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-map-view', - template: '
', -}) -export class MockDataMapComponent {} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-data-view', - template: '
', -}) -export class MockDataViewComponent {} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-data-view-share', - template: '
', -}) -export class MockDataViewShareComponent {} - -@Component({ - selector: 'datahub-record-downloads', - template: '
', -}) -export class MockDataDownloadsComponent {} - -@Component({ - selector: 'datahub-record-otherlinks', - template: '
', -}) -export class MockDataOtherlinksComponent {} - -@Component({ - selector: 'datahub-record-apis', - template: '
', -}) -export class MockDataApisComponent {} - -@Component({ - selector: 'datahub-record-related-records', - template: '
', -}) -export class MockRelatedComponent {} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-metadata-info', - template: '
', -}) -export class MockMetadataInfoComponent { - @Input() metadata: Partial - @Input() incomplete: boolean - @Output() keyword = new EventEmitter() -} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-metadata-contact', - template: '
', -}) -export class MockMetadataContactComponent { - @Input() metadata: Partial - @Output() organizationClick = new EventEmitter() - @Output() contactClick = new EventEmitter() -} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-metadata-catalog', - template: '
', -}) -export class MockMetadataCatalogComponent { - @Input() sourceLabel: string -} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-record-api-form', - template: '
', -}) -export class MockRecordApiFormComponent { - @Input() apiLink: DatasetServiceDistribution -} -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-image-overlay-preview', - template: '
', -}) -export class MockImgOverlayPreviewComponent { - @Input() imageUrl: string - @Output() isPlaceholderShown = new EventEmitter() -} - describe('RecordMetadataComponent', () => { let component: RecordMetadataComponent let fixture: ComponentFixture @@ -165,35 +73,16 @@ describe('RecordMetadataComponent', () => { let searchService: SearchService let sourcesService: SourcesService + beforeEach(() => MockBuilder(RecordMetadataComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - RecordMetadataComponent, - MockDataMapComponent, - MockDataViewComponent, - MockDataViewShareComponent, - MockDataDownloadsComponent, - MockDataOtherlinksComponent, - MockDataApisComponent, - MockRelatedComponent, - ErrorComponent, - MockMetadataInfoComponent, - MockMetadataCatalogComponent, - MockMetadataContactComponent, - MockRecordApiFormComponent, - MockImgOverlayPreviewComponent, - ], - schemas: [NO_ERRORS_SCHEMA], imports: [TranslateModule.forRoot()], providers: [ { provide: MdViewFacade, useClass: MdViewFacadeMock, }, - { - provide: MapManagerService, - useValue: {}, - }, { provide: SearchService, useClass: SearchServiceMock, @@ -224,21 +113,21 @@ describe('RecordMetadataComponent', () => { }) describe('about', () => { - let metadataInfo: MockMetadataInfoComponent - let metadataContact: MockMetadataContactComponent - let catalogComponent: MockMetadataCatalogComponent + let metadataInfo: MetadataInfoComponent + let metadataContact: MetadataContactComponent + let catalogComponent: MetadataCatalogComponent beforeEach(() => { facade.isPresent$.next(true) fixture.detectChanges() metadataInfo = fixture.debugElement.query( - By.directive(MockMetadataInfoComponent) + By.directive(MetadataInfoComponent) ).componentInstance metadataContact = fixture.debugElement.query( - By.directive(MockMetadataContactComponent) + By.directive(MetadataContactComponent) ).componentInstance catalogComponent = fixture.debugElement.query( - By.directive(MockMetadataCatalogComponent) + By.directive(MetadataCatalogComponent) ).componentInstance }) describe('if metadata present', () => { @@ -260,7 +149,7 @@ describe('RecordMetadataComponent', () => { facade.isPresent$.next(false) fixture.detectChanges() metadataInfo = fixture.debugElement.query( - By.directive(MockMetadataInfoComponent) + By.directive(MetadataInfoComponent) ).componentInstance }) it('shows a placeholder', () => { @@ -269,31 +158,29 @@ describe('RecordMetadataComponent', () => { }) it('does not display the metadata contact component', () => { expect( - fixture.debugElement.query(By.directive(MockMetadataContactComponent)) + fixture.debugElement.query(By.directive(MetadataContactComponent)) ).toBeFalsy() }) it('does not display the metadata catalog component', () => { expect( - fixture.debugElement.query(By.directive(MockMetadataCatalogComponent)) + fixture.debugElement.query(By.directive(MetadataCatalogComponent)) ).toBeFalsy() }) it('does not display the image overlay preview', () => { expect( - fixture.debugElement.query( - By.directive(MockImgOverlayPreviewComponent) - ) + fixture.debugElement.query(By.directive(ImageOverlayPreviewComponent)) ).toBeFalsy() }) }) describe('Image Overlay Preview', () => { describe('if metadata without overview', () => { - let imgOverlayPreview: MockImgOverlayPreviewComponent + let imgOverlayPreview: ImageOverlayPreviewComponent beforeEach(() => { facade.isPresent$.next(true) facade.metadata$.next({}) fixture.detectChanges() imgOverlayPreview = fixture.debugElement.query( - By.directive(MockImgOverlayPreviewComponent) + By.directive(ImageOverlayPreviewComponent) ).componentInstance }) it('should send undefined as imageUrl to imgOverlayPreview component', () => { @@ -302,12 +189,12 @@ describe('RecordMetadataComponent', () => { }) }) describe('if metadata with overview', () => { - let imgOverlayPreview: MockImgOverlayPreviewComponent + let imgOverlayPreview: ImageOverlayPreviewComponent beforeEach(() => { facade.isPresent$.next(true) fixture.detectChanges() imgOverlayPreview = fixture.debugElement.query( - By.directive(MockImgOverlayPreviewComponent) + By.directive(ImageOverlayPreviewComponent) ).componentInstance }) describe('and url defined', () => { @@ -347,18 +234,18 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { facade.dataLinks$.next(['link']) fixture.detectChanges() - mapTab = fixture.debugElement.queryAll(By.css('mat-tab'))[0] - tabGroup = fixture.debugElement.queryAll(By.css('mat-tab-group'))[0] + mapTab = fixture.debugElement.queryAll(By.directive(MatTab))[0] + tabGroup = fixture.debugElement.queryAll(By.directive(MatTabGroup))[0] }) it('renders preview, map tab is disabled', () => { - expect(mapTab.nativeNode.disabled).toBe(true) + expect(mapTab.componentInstance.disabled).toBe(true) }) it('renders preview, table tab is selected', () => { - expect(tabGroup.nativeNode.selectedIndex).toBe(1) + expect(tabGroup.componentInstance.selectedIndex).toBe(1) }) it('does not render map component', () => { expect( - fixture.debugElement.query(By.directive(MockDataMapComponent)) + fixture.debugElement.query(By.directive(MapViewComponent)) ).toBeFalsy() }) }) @@ -366,14 +253,14 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { facade.mapApiLinks$.next(['link']) fixture.detectChanges() - mapTab = fixture.debugElement.queryAll(By.css('mat-tab'))[0] + mapTab = fixture.debugElement.queryAll(By.directive(MatTab))[0] }) it('renders preview, map tab is enabled', () => { - expect(mapTab.nativeNode.disabled).toBe(false) + expect(mapTab.componentInstance.disabled).toBe(false) }) it('renders map component', () => { expect( - fixture.debugElement.query(By.directive(MockDataMapComponent)) + fixture.debugElement.query(By.directive(MapViewComponent)) ).toBeTruthy() }) }) @@ -382,14 +269,14 @@ describe('RecordMetadataComponent', () => { facade.geoDataLinksWithGeometry$.next(['link']) facade.geoDataLinks$.next(['link']) fixture.detectChanges() - mapTab = fixture.debugElement.queryAll(By.css('mat-tab'))[0] + mapTab = fixture.debugElement.queryAll(By.directive(MatTab))[0] }) it('renders preview, map tab is enabled', () => { - expect(mapTab.nativeNode.disabled).toBe(false) + expect(mapTab.componentInstance.disabled).toBe(false) }) it('renders map component', () => { expect( - fixture.debugElement.query(By.directive(MockDataMapComponent)) + fixture.debugElement.query(By.directive(MapViewComponent)) ).toBeTruthy() }) }) @@ -404,27 +291,27 @@ describe('RecordMetadataComponent', () => { facade.dataLinks$.next(null) facade.geoDataLinksWithGeometry$.next(null) fixture.detectChanges() - tableTab = fixture.debugElement.queryAll(By.css('mat-tab'))[1] - chartTab = fixture.debugElement.queryAll(By.css('mat-tab'))[2] - tabGroup = fixture.debugElement.queryAll(By.css('mat-tab-group'))[0] + tableTab = fixture.debugElement.queryAll(By.directive(MatTab))[1] + chartTab = fixture.debugElement.queryAll(By.directive(MatTab))[2] + tabGroup = fixture.debugElement.queryAll(By.directive(MatTabGroup))[0] }) it('renders preview, table tab is disabled', () => { - expect(tableTab.nativeNode.disabled).toBe(true) + expect(tableTab.componentInstance.disabled).toBe(true) }) it('renders preview, chart tab is disabled', () => { - expect(chartTab.nativeNode.disabled).toBe(true) + expect(chartTab.componentInstance.disabled).toBe(true) }) it('renders preview, map tab is selected', () => { - expect(tabGroup.nativeNode.selectedIndex).toBe(0) + expect(tabGroup.componentInstance.selectedIndex).toBe(0) }) it('does not render any data view component', () => { expect( - fixture.debugElement.query(By.directive(MockDataViewComponent)) + fixture.debugElement.query(By.directive(DataViewComponent)) ).toBeFalsy() }) it('does render the permalink component', () => { expect( - fixture.debugElement.query(By.directive(MockDataViewShareComponent)) + fixture.debugElement.query(By.directive(DataViewShareComponent)) ).toBeTruthy() }) }) @@ -432,24 +319,23 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { facade.dataLinks$.next(['link']) fixture.detectChanges() - tableTab = fixture.debugElement.queryAll(By.css('mat-tab'))[1] - chartTab = fixture.debugElement.queryAll(By.css('mat-tab'))[2] + tableTab = fixture.debugElement.queryAll(By.directive(MatTab))[1] + chartTab = fixture.debugElement.queryAll(By.directive(MatTab))[2] }) it('renders preview, table tab is enabled', () => { - expect(tableTab.nativeNode.disabled).toBe(false) + expect(tableTab.componentInstance.disabled).toBe(false) }) it('renders preview, chart tab is enabled', () => { - expect(chartTab.nativeNode.disabled).toBe(false) + expect(chartTab.componentInstance.disabled).toBe(false) }) it('renders two data view components (for table and chart tabs)', () => { expect( - fixture.debugElement.queryAll(By.directive(MockDataViewComponent)) - .length + fixture.debugElement.queryAll(By.directive(DataViewComponent)).length ).toEqual(2) }) it('does render the permalink component', () => { expect( - fixture.debugElement.query(By.directive(MockDataViewShareComponent)) + fixture.debugElement.query(By.directive(DataViewShareComponent)) ).toBeTruthy() }) describe('when selectedView$ is chart', () => { @@ -459,7 +345,7 @@ describe('RecordMetadataComponent', () => { }) it('renders the permalink component', () => { expect( - fixture.debugElement.query(By.directive(MockDataViewShareComponent)) + fixture.debugElement.query(By.directive(DataViewShareComponent)) ).toBeTruthy() }) }) @@ -468,19 +354,18 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { facade.geoDataLinks$.next(['link']) fixture.detectChanges() - tableTab = fixture.debugElement.queryAll(By.css('mat-tab'))[1] - chartTab = fixture.debugElement.queryAll(By.css('mat-tab'))[2] + tableTab = fixture.debugElement.queryAll(By.directive(MatTab))[1] + chartTab = fixture.debugElement.queryAll(By.directive(MatTab))[2] }) it('renders preview, table tab is enabled', () => { - expect(tableTab.nativeNode.disabled).toBe(false) + expect(tableTab.componentInstance.disabled).toBe(false) }) it('renders preview, chart tab is enabled', () => { - expect(chartTab.nativeNode.disabled).toBe(false) + expect(chartTab.componentInstance.disabled).toBe(false) }) it('renders two data view components (for table and chart tabs)', () => { expect( - fixture.debugElement.queryAll(By.directive(MockDataViewComponent)) - .length + fixture.debugElement.queryAll(By.directive(DataViewComponent)).length ).toEqual(2) }) }) @@ -491,7 +376,7 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { fixture.detectChanges() downloadsComponent = fixture.debugElement.query( - By.directive(MockDataDownloadsComponent) + By.directive(RecordDownloadsComponent) ) }) it('download component does not render', () => { @@ -503,7 +388,7 @@ describe('RecordMetadataComponent', () => { facade.downloadLinks$.next(['link']) fixture.detectChanges() downloadsComponent = fixture.debugElement.query( - By.directive(MockDataDownloadsComponent) + By.directive(RecordDownloadsComponent) ) }) it('download component renders', () => { @@ -517,7 +402,7 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { fixture.detectChanges() otherLinksComponent = fixture.debugElement.query( - By.directive(MockDataOtherlinksComponent) + By.directive(RecordOtherlinksComponent) ) }) it('otherlink component does not render', () => { @@ -529,7 +414,7 @@ describe('RecordMetadataComponent', () => { facade.otherLinks$.next(['link']) fixture.detectChanges() otherLinksComponent = fixture.debugElement.query( - By.directive(MockDataOtherlinksComponent) + By.directive(RecordOtherlinksComponent) ) }) it('otherlink component renders', () => { @@ -543,7 +428,7 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { fixture.detectChanges() apiComponent = fixture.debugElement.query( - By.directive(MockDataApisComponent) + By.directive(RecordApisComponent) ) }) it('API component does not render', () => { @@ -555,7 +440,7 @@ describe('RecordMetadataComponent', () => { facade.apiLinks$.next(['link']) fixture.detectChanges() apiComponent = fixture.debugElement.query( - By.directive(MockDataApisComponent) + By.directive(RecordApisComponent) ) }) it('API component renders', () => { @@ -571,7 +456,7 @@ describe('RecordMetadataComponent', () => { facade.related$.next([]) fixture.detectChanges() relatedComponent = fixture.debugElement.query( - By.directive(MockRelatedComponent) + By.directive(RecordRelatedRecordsComponent) ) }) it('Related component does not render', () => { @@ -583,7 +468,7 @@ describe('RecordMetadataComponent', () => { facade.related$.next([{ title: 'title' }]) fixture.detectChanges() relatedComponent = fixture.debugElement.query( - By.directive(MockRelatedComponent) + By.directive(RecordRelatedRecordsComponent) ) }) it('Related component renders', () => { diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts index 3d485c2c0f..90c0227e92 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -1,7 +1,15 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { SourcesService } from '@geonetwork-ui/feature/catalog' import { SearchService } from '@geonetwork-ui/feature/search' -import { ErrorType } from '@geonetwork-ui/ui/elements' +import { + ErrorComponent, + ErrorType, + ImageOverlayPreviewComponent, + MetadataCatalogComponent, + MetadataContactComponent, + MetadataInfoComponent, + MetadataQualityComponent, +} from '@geonetwork-ui/ui/elements' import { BehaviorSubject, combineLatest } from 'rxjs' import { filter, map, mergeMap, startWith } from 'rxjs/operators' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' @@ -9,13 +17,46 @@ import { Keyword, Organization, } from '@geonetwork-ui/common/domain/model/record' -import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { + DataViewComponent, + DataViewShareComponent, + MapViewComponent, + MdViewFacade, +} from '@geonetwork-ui/feature/record' +import { CommonModule } from '@angular/common' +import { MatTabsModule } from '@angular/material/tabs' +import { RecordUserFeedbacksComponent } from '../record-user-feedbacks/record-user-feedbacks.component' +import { RecordDownloadsComponent } from '../record-downloads/record-downloads.component' +import { RecordApisComponent } from '../record-apis/record-apis.component' +import { RecordOtherlinksComponent } from '../record-otherlinks/record-otherlinks.component' +import { RecordRelatedRecordsComponent } from '../record-related-records/record-related-records.component' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'datahub-record-metadata', templateUrl: './record-metadata.component.html', styleUrls: ['./record-metadata.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + ImageOverlayPreviewComponent, + MatTabsModule, + ErrorComponent, + RecordUserFeedbacksComponent, + RecordDownloadsComponent, + RecordApisComponent, + RecordOtherlinksComponent, + DataViewShareComponent, + MetadataInfoComponent, + MetadataContactComponent, + MetadataQualityComponent, + MetadataCatalogComponent, + RecordRelatedRecordsComponent, + DataViewComponent, + MapViewComponent, + TranslateModule, + ], }) export class RecordMetadataComponent { @Input() metadataQualityDisplay: boolean diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html index b97bc8a202..90ca03c0d7 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html @@ -6,10 +6,8 @@ record.metadata.links

diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts index bc4d48e632..34d39fa230 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { BehaviorSubject } from 'rxjs' import { RecordOtherlinksComponent } from './record-otherlinks.component' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { MockBuilder, MockProvider } from 'ng-mocks' class MdViewFacadeMock { otherLinks$ = new BehaviorSubject([]) @@ -10,10 +11,15 @@ describe('RecordOtherlinksComponent', () => { let component: RecordOtherlinksComponent let fixture: ComponentFixture + beforeEach(() => MockBuilder(RecordOtherlinksComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [RecordOtherlinksComponent], - providers: [{ provide: MdViewFacade, useClass: MdViewFacadeMock }], + providers: [ + MockProvider(MdViewFacade, { + otherLinks$: new BehaviorSubject([]), + }), + ], }).compileComponents() }) diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts index 97a39cf5a8..799d6dea65 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts @@ -6,47 +6,47 @@ import { ViewChild, } from '@angular/core' import { MdViewFacade } from '@geonetwork-ui/feature/record' -import { BlockListComponent, CarouselComponent } from '@geonetwork-ui/ui/layout' +import { + BlockListComponent, + CarouselComponent, + Paginable, + PreviousNextButtonsComponent, +} from '@geonetwork-ui/ui/layout' +import { CommonModule } from '@angular/common' +import { LinkCardComponent } from '@geonetwork-ui/ui/elements' +import { LetDirective } from '@ngrx/component' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'datahub-record-otherlinks', templateUrl: './record-otherlinks.component.html', styleUrls: ['./record-otherlinks.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + PreviousNextButtonsComponent, + BlockListComponent, + LinkCardComponent, + CarouselComponent, + LetDirective, + TranslateModule, + ], }) export class RecordOtherlinksComponent implements AfterViewInit { otherLinks$ = this.facade.otherLinks$ @ViewChild(CarouselComponent) carousel: CarouselComponent @ViewChild(BlockListComponent) list: BlockListComponent + get paginableElement(): Paginable { + return this.carousel || this.list + } constructor( public facade: MdViewFacade, private changeDetector: ChangeDetectorRef ) {} - get isFirstStepOrPage() { - return this.carousel?.isFirstStep ?? this.list?.isFirstPage ?? true - } - - get isLastStepOrPage() { - return this.carousel?.isLastStep ?? this.list?.isLastPage ?? false - } - - get hasPagination() { - return (this.carousel?.stepsCount || this.list?.pagesCount) > 1 - } - - changeStepOrPage(direction: string) { - if (direction === 'next') { - this.list?.nextPage() - this.carousel?.slideToNext() - } else { - this.carousel?.slideToPrevious() - this.list?.previousPage() - } - } - updateView() { this.changeDetector.detectChanges() } diff --git a/apps/datahub/src/app/record/record-page/record-page.component.spec.ts b/apps/datahub/src/app/record/record-page/record-page.component.spec.ts index a13f001ce8..723866b8fa 100644 --- a/apps/datahub/src/app/record/record-page/record-page.component.spec.ts +++ b/apps/datahub/src/app/record/record-page/record-page.component.spec.ts @@ -1,28 +1,18 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { MdViewFacade } from '@geonetwork-ui/feature/record' -import { of } from 'rxjs' import { RecordPageComponent } from './record-page.component' - -class MdViewFacadeMock { - metadata$ = of() -} +import { MockBuilder, MockProvider } from 'ng-mocks' describe('RecordPageComponent', () => { let component: RecordPageComponent let fixture: ComponentFixture + beforeEach(() => MockBuilder(RecordPageComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [RecordPageComponent], - schemas: [NO_ERRORS_SCHEMA], - providers: [ - { - provide: MdViewFacade, - useClass: MdViewFacadeMock, - }, - ], + providers: [MockProvider(MdViewFacade)], }).compileComponents() }) diff --git a/apps/datahub/src/app/record/record-page/record-page.component.ts b/apps/datahub/src/app/record/record-page/record-page.component.ts index dbe43824ce..08961234c9 100644 --- a/apps/datahub/src/app/record/record-page/record-page.component.ts +++ b/apps/datahub/src/app/record/record-page/record-page.component.ts @@ -1,15 +1,28 @@ import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core' -import { MdViewFacade } from '@geonetwork-ui/feature/record' import { - MetadataQualityConfig, + MdViewFacade, + RecordMetaComponent, +} from '@geonetwork-ui/feature/record' +import { getMetadataQualityConfig, + MetadataQualityConfig, } from '@geonetwork-ui/util/app-config' +import { RecordMetadataComponent } from '../record-metadata/record-metadata.component' +import { HeaderRecordComponent } from '../header-record/header-record.component' +import { CommonModule } from '@angular/common' @Component({ selector: 'datahub-record-page', templateUrl: './record-page.component.html', styleUrls: ['./record-page.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + RecordMetadataComponent, + HeaderRecordComponent, + RecordMetaComponent, + ], }) export class RecordPageComponent implements OnDestroy { metadataQualityDisplay: boolean diff --git a/apps/datahub/src/app/record/record-related-records/record-related-records.component.spec.ts b/apps/datahub/src/app/record/record-related-records/record-related-records.component.spec.ts index b5e95e7938..b51cfff4c3 100644 --- a/apps/datahub/src/app/record/record-related-records/record-related-records.component.spec.ts +++ b/apps/datahub/src/app/record/record-related-records/record-related-records.component.spec.ts @@ -1,16 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { RecordRelatedRecordsComponent } from './record-related-records.component' +import { MockBuilder } from 'ng-mocks' describe('RelatedRecordsComponent', () => { let component: RecordRelatedRecordsComponent let fixture: ComponentFixture - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [RecordRelatedRecordsComponent], - }).compileComponents() - }) + beforeEach(() => MockBuilder(RecordRelatedRecordsComponent)) beforeEach(() => { fixture = TestBed.createComponent(RecordRelatedRecordsComponent) diff --git a/apps/datahub/src/app/record/record-related-records/record-related-records.component.ts b/apps/datahub/src/app/record/record-related-records/record-related-records.component.ts index d82b51ff1f..d2802b425b 100644 --- a/apps/datahub/src/app/record/record-related-records/record-related-records.component.ts +++ b/apps/datahub/src/app/record/record-related-records/record-related-records.component.ts @@ -1,11 +1,16 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { RelatedRecordCardComponent } from '@geonetwork-ui/ui/elements' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'datahub-record-related-records', templateUrl: './record-related-records.component.html', styleUrls: ['./record-related-records.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, RelatedRecordCardComponent, TranslateModule], }) export class RecordRelatedRecordsComponent { @Input() records: CatalogRecord[] diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts index 048b1322fb..0fc6088674 100644 --- a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts +++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts @@ -8,11 +8,7 @@ import { RecordUserFeedbacksComponent } from './record-user-feedbacks.component' import { TranslateModule } from '@ngx-translate/core' import { MdViewFacade } from '@geonetwork-ui/feature/record' import { BehaviorSubject, of, Subject } from 'rxjs' -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - NO_ERRORS_SCHEMA, -} from '@angular/core' +import { ChangeDetectionStrategy } from '@angular/core' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { barbieUserFixture, @@ -24,8 +20,9 @@ import { UserFeedbackViewModel, } from '@geonetwork-ui/common/domain/model/record' import { Gn4PlatformMapper } from '@geonetwork-ui/api/repository' +import { MockBuilder, MockProvider } from 'ng-mocks' -describe('RelatedRecordsComponent', () => { +describe('RecordUserFeedbacksComponent', () => { const allUserFeedbacks = someUserFeedbacksFixture() let mockDestroy$: Subject @@ -48,10 +45,6 @@ describe('RelatedRecordsComponent', () => { }, } - const changeDetectorRefMock: Partial = { - markForCheck: jest.fn(), - } - const platformServiceInterfaceMock: Partial = { getUserFeedbacks: jest.fn(), getMe: jest.fn(() => new BehaviorSubject(activeUser)), @@ -60,30 +53,17 @@ describe('RelatedRecordsComponent', () => { let component: RecordUserFeedbacksComponent let fixture: ComponentFixture + beforeEach(() => MockBuilder(RecordUserFeedbacksComponent)) + beforeEach(async () => { mockDestroy$ = new Subject() await TestBed.configureTestingModule({ - declarations: [RecordUserFeedbacksComponent], imports: [TranslateModule.forRoot()], - schemas: [NO_ERRORS_SCHEMA], providers: [ - { - provide: MdViewFacade, - useValue: mdViewFacadeMock, - }, - { - provide: ChangeDetectorRef, - useValue: changeDetectorRefMock, - }, - { - provide: PlatformServiceInterface, - useValue: platformServiceInterfaceMock, - }, - { - provide: Gn4PlatformMapper, - useValue: gn4PlatformMapperMock, - }, + MockProvider(MdViewFacade, mdViewFacadeMock), + MockProvider(PlatformServiceInterface, platformServiceInterfaceMock), + MockProvider(Gn4PlatformMapper, gn4PlatformMapperMock), ], }) .overrideComponent(RecordUserFeedbacksComponent, { diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts index 9e7d92356f..033233ca64 100644 --- a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts +++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts @@ -14,11 +14,24 @@ import { } from '@geonetwork-ui/common/domain/model/record' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { UserModel } from '@geonetwork-ui/common/domain/model/user' -import { DropdownChoice } from '@geonetwork-ui/ui/inputs' +import { + ButtonComponent, + DropdownChoice, + DropdownSelectorComponent, + TextAreaComponent, +} from '@geonetwork-ui/ui/inputs' import { MdViewFacade } from '@geonetwork-ui/feature/record' -import { TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' import { AuthService, Gn4PlatformMapper } from '@geonetwork-ui/api/repository' -import { UserApiModel } from '@geonetwork-ui/data-access/gn4' +import { SpinningLoaderComponent } from '@geonetwork-ui/ui/widgets' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { + matAccountBoxOutline, + matEditOutline, + matSendOutline, +} from '@ng-icons/material-icons/outline' +import { CommonModule } from '@angular/common' +import { UserFeedbackItemComponent } from '@geonetwork-ui/ui/elements' type UserFeedbackSortingFunction = ( userFeedbackA: UserFeedback, @@ -30,6 +43,24 @@ type UserFeedbackSortingFunction = ( templateUrl: './record-user-feedbacks.component.html', styleUrls: ['./record-user-feedbacks.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + DropdownSelectorComponent, + SpinningLoaderComponent, + NgIcon, + ButtonComponent, + TextAreaComponent, + TranslateModule, + UserFeedbackItemComponent, + ], + viewProviders: [ + provideIcons({ + matEditOutline, + matSendOutline, + matAccountBoxOutline, + }), + ], }) export class RecordUserFeedbacksComponent implements OnInit, OnDestroy { @Input() organisationName$: Observable diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 5cee3c2b69..07e5185ad8 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -140,25 +140,11 @@ describe('dashboard (authenticated)', () => { it('should show nothing when none are selected', () => { cy.visit('/catalog/search') - cy.get('gn-ui-results-table') - .find('.table-row-cell') - .get('gn-ui-checkbox') - .each(($checkbox) => cy.wrap($checkbox).click()) cy.get('[data-cy=records-information]').should( 'not.have.descendants', '[data-test=selected-count]' ) }) - - it('should select all records when the "select all" checkbox is checked', () => { - cy.visit('/catalog/search') - cy.get('gn-ui-results-table') - .find('.table-row-cell') - .get('gn-ui-checkbox') - .first() - .click() - cy.get('[data-test=selected-count]').contains('15 selected') - }) }) describe('columns', () => { beforeEach(() => { @@ -225,7 +211,7 @@ describe('dashboard (authenticated)', () => { }) describe('my records', () => { it('should only display records I own', () => { - cy.get('md-editor-dashboard-menu').find('a').eq(5).click() + cy.get('md-editor-dashboard-menu').find('a').eq(3).click() cy.get('gn-ui-results-table') .find('[data-cy="table-row"]') .find('ng-icon') @@ -233,19 +219,18 @@ describe('dashboard (authenticated)', () => { .should('contain', 'admin admin') }) it('should display the correct amount of records', () => { - cy.get('md-editor-dashboard-menu').find('a').eq(5).click() + cy.get('md-editor-dashboard-menu').find('a').eq(3).click() cy.get('gn-ui-results-table') .find('[data-cy="table-row"]') .should('have.length', '10') }) it('should sort the records by title', () => { - cy.get('md-editor-dashboard-menu').find('a').eq(5).click() + cy.get('md-editor-dashboard-menu').find('a').eq(3).click() cy.get('gn-ui-results-table') .find('[data-cy="table-row"]') .first() .invoke('text') .then((firstRecord) => { - console.log(firstRecord) cy.get('gn-ui-results-table') .find('.table-header-cell') .eq(1) @@ -287,10 +272,6 @@ describe('dashboard (authenticated)', () => { cy.get('md-editor-dashboard-menu').find('a').eq(5).click() cy.get('gn-ui-autocomplete').should('have.value', '') }) - it('should hide the search input when navigating to my drafts', () => { - cy.get('md-editor-dashboard-menu').find('a').eq(6).click() - cy.get('gn-ui-autocomplete').should('not.exist') - }) }) describe('myRecords search input', () => { beforeEach(() => { @@ -308,19 +289,59 @@ describe('dashboard (authenticated)', () => { cy.get('md-editor-dashboard-menu').find('a').first().click() cy.get('gn-ui-autocomplete').should('have.value', '') }) + it('should allow to search in the entire catalog', () => { + cy.get('gn-ui-autocomplete').type('mat{enter}') + cy.get('gn-ui-interactive-table') + .find('[data-cy="table-row"]') + .should('have.length', '1') + cy.url().should('include', '/search?q=mat') + cy.url().should('not.include', 'owner') + }) }) }) describe('search filters', () => { + function selectUser(index = 0, openDropdown = true) { + if (openDropdown) { + cy.get('md-editor-search-filters').find('gn-ui-button').first().click() + } + cy.get('.cdk-overlay-container') + .find('input[type="checkbox"]') + .eq(index) + .check() + } + function selectDateRange() { + cy.get('mat-calendar-header').find('button').first().click() + cy.get('mat-multi-year-view').contains('button', '2024').click() + cy.get('mat-year-view').contains('button', 'AUG').click() + cy.get('mat-month-view').contains('button', '1').click() + cy.get('mat-month-view').contains('button', '30').click() + } + function closeDropDown() { + cy.get('body').click(0, 0) + } + function checkFilterByChangeDate() { + cy.get('gn-ui-interactive-table') + .find('[data-cy="table-row"]') + .should('have.length', '1') + cy.get('gn-ui-results-table') + .find('[data-cy="resultItemTitle"]') + .each(($resultItemTitle) => { + cy.wrap($resultItemTitle) + .invoke('text') + .should('eq', 'Accroches vélos MEL') + }) + } describe('allRecords search filter', () => { beforeEach(() => { cy.visit('/catalog/search') }) - it('should filter the record list by editor (Barbara Roberts)', () => { - cy.get('md-editor-search-filters').find('gn-ui-button').first().click() - cy.get('.cdk-overlay-container') - .find('input[type="checkbox"]') - .eq(1) - .check() + it('should contain filter component with two search filters', () => { + cy.get('md-editor-search-filters') + .find('gn-ui-button') + .should('have.length', 2) + }) + it('should filter the record list by user (Barbara Roberts)', () => { + selectUser(1) cy.get('gn-ui-interactive-table') .find('[data-cy="table-row"]') .should('have.length', '5') @@ -330,25 +351,207 @@ describe('dashboard (authenticated)', () => { cy.wrap($ownerInfo).invoke('text').should('eq', 'Barbara Roberts') }) }) + it('should filter the record list by last update (changeDate)', () => { + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + selectDateRange() + checkFilterByChangeDate() + }) + it('should display the expand icon for the date range dropdown correctly', () => { + cy.get('md-editor-search-filters') + .find('gn-ui-date-range-dropdown') + .find('ng-icon') + .should('have.attr', 'ng-reflect-name', 'matExpandMore') + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + cy.get('md-editor-search-filters') + .find('gn-ui-date-range-dropdown') + .find('ng-icon') + .should('have.attr', 'ng-reflect-name', 'matExpandLess') + }) }) describe('myRecords search filters', () => { beforeEach(() => { cy.visit('/my-space/my-records') }) - it('should contain filter component with no search filter for now', () => { + it('should contain filter component with one search filter', () => { cy.get('md-editor-search-filters') .find('gn-ui-button') - .should('not.exist') + .should('have.length', 1) + }) + it('should filter the record list by last update (changeDate)', () => { + cy.get('md-editor-search-filters').find('gn-ui-button').first().click() + selectDateRange() + checkFilterByChangeDate() + }) + }) + describe('allRecord search filters summary', () => { + beforeEach(() => { + cy.visit('/catalog/search') + }) + it('should not display anything without selected filters', () => { + cy.get('gn-ui-search-filters-summary-item').should('not.exist') + }) + describe('selecting users', () => { + beforeEach(() => { + selectUser(1) + }) + it('should display a label for badges of selected users', () => { + cy.get('gn-ui-search-filters-summary') + .find('[data-cy="filterSummaryLabel"]') + .invoke('text') + .should('eq', 'Modified by: ') + }) + it('should display the badge for a selected user', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 1) + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .invoke('text') + .should('eq', 'Barbara Roberts') + }) + it('should display a second badge for a second selected user', () => { + selectUser(0, false) + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 2) + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .eq(1) + .invoke('text') + .should('eq', 'admin admin') + }) + it('should remove one of two badges when a badge cross is clicked', () => { + selectUser(0, false) + closeDropDown() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .eq(0) + .find('ng-icon') + .click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 1) + }) + }) + describe('selecting date range', () => { + beforeEach(() => { + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + selectDateRange() + }) + it('should display a label for the date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('[data-cy="filterSummaryLabel"]') + .invoke('text') + .should('eq', 'Modified on: ') + }) + it('should display the badge for the selected date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .invoke('text') + .should('eq', '01.08.2024 - 30.08.2024') + }) + it('should remove the badge when the badge cross is clicked', () => { + closeDropDown() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .find('ng-icon') + .click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('not.exist') + }) + }) + describe('selecting multiple filters (users and date range)', () => { + beforeEach(() => { + selectUser(0) + closeDropDown() + cy.get('md-editor-search-filters').find('gn-ui-button').eq(1).click() + selectDateRange() + }) + it('should display both badges', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 2) + }) + it('should clear all filters when the clear button is clicked', () => { + cy.get('gn-ui-search-filters-summary').find('button').last().click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('have.length', 0) + cy.get('gn-ui-search-filters-summary-item').should('not.exist') + }) + }) + }) + describe('myRecords search filters summary', () => { + beforeEach(() => { + cy.visit('/my-space/my-records') + }) + it('should not display anything without selected filters', () => { + cy.get('gn-ui-search-filters-summary-item').should('not.exist') + }) + describe('selecting date range', () => { + beforeEach(() => { + cy.get('md-editor-search-filters').find('gn-ui-button').eq(0).click() + selectDateRange() + }) + it('should display a label for the date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('[data-cy="filterSummaryLabel"]') + .invoke('text') + .should('eq', 'Modified on: ') + }) + it('should display the badge for the selected date range', () => { + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .invoke('text') + .should('eq', '01.08.2024 - 30.08.2024') + }) + it('should remove the badge when the badge cross is clicked', () => { + closeDropDown() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .find('ng-icon') + .click() + cy.get('gn-ui-search-filters-summary') + .find('gn-ui-badge') + .should('not.exist') + }) }) }) }) + describe('Account settings access', () => { + it('should navigate to the account settings page', () => { + cy.visit('/catalog/search') + cy.get('md-editor-sidebar') + .find('gn-ui-button') + .first() + .find('a') + .invoke('removeAttr', 'target') + .click() + cy.url().should('include', '/admin.console') + }) + }) }) -describe('when the user is not logged in', () => { - beforeEach(() => { - cy.visit('/catalog/search') +describe('Logging in and out', () => { + describe('when the user is not logged in', () => { + beforeEach(() => { + cy.visit('/catalog/search') + }) + it('redirects to the login page', () => { + cy.url().should('include', '/catalog.signin?redirect=') + }) }) - it('redirects to the login page', () => { - cy.url().should('include', '/catalog.signin?redirect=') + describe('Logging out', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + cy.visit('/catalog/search') + }) + it('logs out the user', () => { + cy.get('gn-ui-avatar').should('be.visible') + cy.get('md-editor-sidebar').find('gn-ui-button').eq(1).click() + cy.url().should('include', '/catalog.signin?redirect=') + cy.get('gn-ui-avatar').should('not.exist') + }) }) }) diff --git a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts index 7d37930ba8..44c3c64479 100644 --- a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts @@ -123,25 +123,34 @@ describe('editor form', () => { it('shows the title', () => { cy.get('gn-ui-form-field') .first() - .find('input') + .find('textarea') .invoke('val') .should( 'eq', "Stations d'épuration selon la directive Eaux Résiduelles Urbaines (91/271/CEE) en Wallonie (Copy)" ) }) + it('shows very long titles entirely', () => { + cy.editor_wrapPreviousDraft() + cy.get('gn-ui-form-field').first().find('textarea').focus() + cy.focused().clear() + cy.get('gn-ui-form-field').first().find('textarea').focus() + cy.focused().type( + 'Metadata for E2E testing purpose. (this title is very long and should take several lines, so we can test the behavior of the title field when it is very long. just keep going until it hits 4 lines, now it should be long enough)' + ) + cy.get('gn-ui-form-field').first().invoke('height').should('eq', 156) + }) it('edits and saves the title', () => { cy.editor_wrapPreviousDraft() - cy.get('gn-ui-form-field').first().find('input').clear() - cy.get('gn-ui-form-field') - .first() - .find('input') - .type('Test record modified') + cy.get('gn-ui-form-field').first().find('textarea').focus() + cy.focused().clear() + cy.get('gn-ui-form-field').first().find('textarea').focus() + cy.focused().type('Test record modified') cy.editor_publishAndReload() cy.get('@saveStatus').should('eq', 'record_up_to_date') cy.get('gn-ui-form-field') .first() - .find('input') + .find('textarea') .invoke('val') .should('eq', 'Test record modified') }) @@ -218,11 +227,62 @@ describe('editor form', () => { .eq(1) .as('aboutSection') }) - describe('resource updated', () => { + describe('resource identifier', () => { + it('shows the resource identifier', () => { + cy.get('@aboutSection') + .find('gn-ui-form-field-simple') + .first() + .find('input') + .invoke('val') + .should('eq', 'UWWTD_WASTE_WATER_TREATMENT') + }) + it('edits and saves the resource identifiert', () => { + cy.editor_wrapPreviousDraft() + cy.get('gn-ui-form-field-simple').first().find('input').clear() + cy.get('gn-ui-form-field-simple') + .first() + .find('input') + .type('Test - resource identifier') + cy.editor_publishAndReload() + cy.get('@saveStatus').should('eq', 'record_up_to_date') + cy.get('gn-ui-form-field-simple') + .first() + .find('input') + .invoke('val') + .should('eq', 'Test - resource identifier') + }) + }) + describe('resource created', () => { beforeEach(() => { cy.get('@aboutSection') - .find('gn-ui-form-field-date-updated') + .find('gn-ui-form-field-date') .eq(0) + .as('resourceCreatedField') + }) + it('shows the resource creation date', () => { + cy.get('@resourceCreatedField') + .find('input') + .invoke('val') + .should('eq', '1/1/2005') + }) + it('edits and saves the resource creation date', () => { + cy.editor_wrapPreviousDraft() + cy.get('@resourceCreatedField') + .find('input') + .type('{selectall}{del}01/01/2019{enter}') + cy.editor_publishAndReload() + cy.get('@saveStatus').should('eq', 'record_up_to_date') + cy.get('@resourceCreatedField') + .find('input') + .invoke('val') + .should('eq', '1/1/2019') + }) + }) + describe('resource updated', () => { + beforeEach(() => { + cy.get('@aboutSection') + .find('gn-ui-form-field-date') + .eq(1) .as('resourceUpdatedField') }) it('shows the resource update date', () => { @@ -372,7 +432,7 @@ describe('editor form', () => { .should('have.length', 0) cy.get('gn-ui-form-field-spatial-extent') .find('gn-ui-autocomplete') - .click() + .type('a') cy.get('mat-option').eq(1).click() cy.editor_publishAndReload() cy.get('@saveStatus').should('eq', 'record_up_to_date') @@ -381,27 +441,6 @@ describe('editor form', () => { .should('have.length', 1) }) }) - describe('spatial scope', () => { - it('toggle between national and regional spatial coverage', () => { - cy.get('gn-ui-switch-toggle').should('exist') - - cy.get('gn-ui-switch-toggle').find('mat-button-toggle').eq(0).click() - cy.get('mat-button-toggle') - .eq(0) - .should('have.class', 'mat-button-toggle-checked') - cy.get('mat-button-toggle') - .eq(1) - .should('not.have.class', 'mat-button-toggle-checked') - - cy.get('gn-ui-switch-toggle').find('mat-button-toggle').eq(1).click() - cy.get('mat-button-toggle') - .eq(1) - .should('have.class', 'mat-button-toggle-checked') - cy.get('mat-button-toggle') - .eq(0) - .should('not.have.class', 'mat-button-toggle-checked') - }) - }) }) describe('distribution resources', () => { beforeEach(() => { @@ -569,7 +608,9 @@ describe('editor form', () => { }) it('should add a keyword', () => { cy.editor_wrapPreviousDraft() - cy.get('gn-ui-form-field-keywords').find('gn-ui-autocomplete').click() + cy.get('gn-ui-form-field-keywords') + .find('gn-ui-autocomplete') + .type('a') cy.get('mat-option').first().click() cy.editor_publishAndReload() cy.get('@saveStatus').should('eq', 'record_up_to_date') @@ -581,6 +622,17 @@ describe('editor form', () => { .find('span') .should('have.text', 'Addresses ') }) + it('should close the autocomplete and clear the input after selecting a keyword', () => { + cy.get('gn-ui-form-field-keywords') + .find('gn-ui-autocomplete') + .type('a') + cy.get('mat-option').first().click() + cy.get('mat-option').should('not.exist') + cy.get('gn-ui-form-field-keywords') + .find('gn-ui-autocomplete') + .find('input') + .should('have.value', '') + }) it('should delete a keyword', () => { cy.editor_wrapPreviousDraft() cy.get('gn-ui-form-field-keywords') diff --git a/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts b/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts index 41f73ae209..c4a9907dc9 100644 --- a/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/record-actions.cy.ts @@ -114,6 +114,52 @@ describe('record-actions', () => { .should('contain.text', 'Next') }) + it('the created record should not allow upload of resources and show info message as it was not saved yet', () => { + // first page + cy.get('gn-ui-form-field-overviews') + .find('gn-ui-image-input') + .find('input') + .should('be.disabled') + cy.get('gn-ui-form-field-overviews') + .children() + .find('[data-test="disabled-message"]') + .should( + 'contain.text', + ' This field will be enabled once the data has been published ' + ) + + // second page + cy.get('[data-test="previousNextPageButtons"]') + .children() + .eq(1) + .should('contain.text', 'Next') + .click() + cy.get('gn-ui-form-field-online-resources') + .find('gn-ui-switch-toggle') + .find('mat-button-toggle-group') + .find('button') + .should('be.disabled') + cy.get('gn-ui-file-input').find('input').should('be.disabled') + cy.get('gn-ui-form-field-online-resources') + .children() + .find('div') + .should( + 'contain.text', + ' This field will be enabled once the data has been published ' + ) + + cy.get('gn-ui-form-field-online-link-resources') + .find('input') + .should('be.disabled') + cy.get('gn-ui-form-field-online-link-resources') + .children() + .find('div') + .should( + 'contain.text', + ' This field will be enabled once the data has been published ' + ) + }) + it('back navigation should go to search after creating a record', () => { cy.go('back') cy.url().should('include', '/catalog/search') @@ -189,7 +235,7 @@ describe('record-actions', () => { cy.get('gn-ui-form-field') .first() - .find('input') + .find('textarea') .invoke('val') .should('eq', 'Accroches vélos MEL (Copy)') @@ -254,7 +300,7 @@ describe('record-actions', () => { cy.get('gn-ui-record-form') .find('gn-ui-form-field') .eq(0) - .find('input') + .find('textarea') .invoke('val') .should('contain', 'Copy') }) @@ -271,4 +317,67 @@ describe('record-actions', () => { }) }) }) + describe('drafting', () => { + let recordUuid: any + describe('if a user edits the record in the meantime', () => { + beforeEach(() => { + cy.visit('/edit/9e1ea778-d0ce-4b49-90b7-37bc0e448300') + cy.url().should('include', '/edit/') + cy.editor_readFormUniqueIdentifier().then((uuid) => { + recordUuid = uuid + cy.wrap(uuid).as('recordUuid') + }) + cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea').as( + 'abstractField' + ) + cy.get('@abstractField').clear() + cy.get('@abstractField').type('modified abstract') + cy.editor_findDraftInLocalStorage().then((value) => { + expect(value).to.contain('modified abstract') + }) + cy.editor_wrapFirstDraft() + cy.clearRecordDrafts() + cy.visit('/edit/9e1ea778-d0ce-4b49-90b7-37bc0e448300') + cy.editor_wrapPreviousDraft() + cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea').as( + 'abstractField' + ) + cy.get('@abstractField').clear() + cy.get('@abstractField').type('modified by someone else') + cy.editor_publishAndReload() + cy.window().then((win) => { + cy.get('@firstDraft').then((firstDraft) => { + return win.localStorage.setItem( + `geonetwork-ui-draft-${recordUuid}`, + firstDraft.toString() + ) + }) + }) + cy.visit('/edit/9e1ea778-d0ce-4b49-90b7-37bc0e448300') + }) + it('should show the warning banner and the warning menu when publishing', () => { + cy.get('[data-test="draft-alert"]').should('be.visible') + cy.get('md-editor-publish-button').click() + cy.get('[data-test="publish-warning"]').should('be.visible') + }) + }) + describe('if nobody edits the record in the meantime', () => { + beforeEach(() => { + cy.clearRecordDrafts() + cy.visit('/edit/9e1ea778-d0ce-4b49-90b7-37bc0e448300') + cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea').as( + 'abstractField' + ) + cy.get('@abstractField').clear() + cy.get('@abstractField').type('modified abstract') + cy.visit('/catalog/search') + }) + it('should not show any warning', () => { + cy.visit('/edit/9e1ea778-d0ce-4b49-90b7-37bc0e448300') + cy.get('[data-test="draft-alert"]').should('not.exist') + cy.get('md-editor-publish-button').click() + cy.get('[data-test="publish-warning"]').should('not.exist') + }) + }) + }) }) diff --git a/apps/metadata-editor/src/app/app.module.ts b/apps/metadata-editor/src/app/app.module.ts index 87354808bc..ce7a03c927 100644 --- a/apps/metadata-editor/src/app/app.module.ts +++ b/apps/metadata-editor/src/app/app.module.ts @@ -26,6 +26,8 @@ import { DashboardPageComponent } from './dashboard/dashboard-page.component' import { EditorRouterService } from './router.service' import { LOGIN_URL, + LOGOUT_URL, + SETTINGS_URL, provideGn4, provideRepositoryUrl, } from '@geonetwork-ui/api/repository' @@ -70,6 +72,14 @@ import { FeatureEditorModule } from '@geonetwork-ui/feature/editor' provide: LOGIN_URL, useFactory: () => getGlobalConfig().LOGIN_URL, }, + { + provide: LOGOUT_URL, + useFactory: () => getGlobalConfig().LOGOUT_URL, + }, + { + provide: SETTINGS_URL, + useFactory: () => getGlobalConfig().SETTINGS_URL, + }, ], bootstrap: [AppComponent], }) 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 021ad98957..debd4e9c70 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 @@ -10,7 +10,7 @@ dashboard.catalog.allRecords
- dashboard.catalog.calendar - + -->
-
+
+
+ +
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html index 75710a0f5e..3d4728505f 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.html @@ -1,13 +1,20 @@
- - +
+ + +
+
diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts index 487bdb8615..2b2d9a2fb5 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.spec.ts @@ -2,13 +2,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { SearchFiltersComponent } from './search-filters.component' import { MockBuilder } from 'ng-mocks' import { TranslateModule } from '@ngx-translate/core' +import { By } from '@angular/platform-browser' +import { SearchFiltersSummaryComponent } from '@geonetwork-ui/feature/search' describe('SearchFiltersComponent', () => { let component: SearchFiltersComponent let fixture: ComponentFixture beforeEach(() => { - return MockBuilder(SearchFiltersComponent) + return MockBuilder(SearchFiltersComponent).mock( + SearchFiltersSummaryComponent + ) }) beforeEach(async () => { @@ -40,5 +44,15 @@ describe('SearchFiltersComponent', () => { fixture.detectChanges() expect(component.searchConfig).toEqual([]) }) + + it('should pass searchFields to SearchFiltersSummaryComponent', () => { + const searchFields = ['user', 'publisherOrg', 'format', 'isSpatial'] + component.searchFields = searchFields + fixture.detectChanges() + const summaryComponent = fixture.debugElement.query( + By.directive(SearchFiltersSummaryComponent) + ).componentInstance + expect(summaryComponent.searchFields).toEqual(searchFields) + }) }) }) diff --git a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts index 68cc32e55e..3b64b8a646 100644 --- a/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts +++ b/apps/metadata-editor/src/app/dashboard/search-filters/search-filters.component.ts @@ -1,13 +1,16 @@ import { Component, Input, OnInit } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' -import { FeatureSearchModule } from '@geonetwork-ui/feature/search' import { NgIconComponent, provideIcons, provideNgIconsConfig, } from '@ng-icons/core' import { iconoirFilterList } from '@ng-icons/iconoir' +import { + FeatureSearchModule, + SearchFiltersSummaryComponent, +} from '@geonetwork-ui/feature/search' @Component({ selector: 'md-editor-search-filters', @@ -17,6 +20,7 @@ import { iconoirFilterList } from '@ng-icons/iconoir' TranslateModule, FeatureSearchModule, NgIconComponent, + SearchFiltersSummaryComponent, ], providers: [ provideIcons({ 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 0c81c789b8..d20f4982ee 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html @@ -20,19 +20,27 @@ > - +
+ + - Log out + 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 50982f3777..8743381d52 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 @@ -1,19 +1,27 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' +import { + AuthService, + AvatarServiceInterface, +} from '@geonetwork-ui/api/repository' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { TranslateModule } from '@ngx-translate/core' -import { MockBuilder, MockProviders } from 'ng-mocks' +import { MockBuilder, MockProvider, MockProviders } from 'ng-mocks' import { SidebarComponent } from './sidebar.component' describe('SidebarComponent', () => { let component: SidebarComponent let fixture: ComponentFixture + let service: AuthService beforeEach(() => { return MockBuilder(SidebarComponent) }) + afterEach(() => { + jest.resetAllMocks() + }) + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SidebarComponent, TranslateModule.forRoot()], @@ -23,9 +31,13 @@ describe('SidebarComponent', () => { AvatarServiceInterface, OrganizationsServiceInterface ), + MockProvider(AuthService, { + logoutUrl: 'http://logout.com/bla?', + }), ], }).compileComponents() + service = TestBed.inject(AuthService) fixture = TestBed.createComponent(SidebarComponent) component = fixture.componentInstance fixture.detectChanges() @@ -34,4 +46,21 @@ describe('SidebarComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + describe('logOut', () => { + it('should log out', async () => { + jest.spyOn(window, 'fetch').mockResolvedValue({ + ok: true, + } as Response) + + const originalUrl = window.origin + + await component.logOut() + + expect(window.fetch).toHaveBeenCalledWith(service.logoutUrl, { + method: 'GET', + }) + expect(window.location.href.slice(0, -1)).toBe(originalUrl) + }) + }) }) diff --git a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.ts b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.ts index 0aeb4afca0..c842bb1ef5 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.ts +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.ts @@ -3,7 +3,10 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' import { DashboardMenuComponent } from '../dashboard-menu/dashboard-menu.component' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' -import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' +import { + AuthService, + AvatarServiceInterface, +} from '@geonetwork-ui/api/repository' import { LetDirective } from '@ngrx/component' import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' @@ -28,10 +31,15 @@ export class SidebarComponent implements OnInit { public placeholder$ = this.avatarService.getPlaceholder() organisations$: Observable + get settingsUrl() { + return this.authService.settingsUrl + } + constructor( public platformService: PlatformServiceInterface, private avatarService: AvatarServiceInterface, - public organisationsService: OrganizationsServiceInterface + public organisationsService: OrganizationsServiceInterface, + private authService: AuthService ) {} ngOnInit(): void { @@ -41,4 +49,21 @@ export class SidebarComponent implements OnInit { (orgs, me) => orgs.filter((org) => org.name === me?.organisation) ) } + + logOut() { + const current_url = window.origin.toString() + fetch(this.authService.logoutUrl, { + method: 'GET', + }) + .then((response) => { + if (response.ok) { + window.location.href = current_url + } else { + console.error('Logout failed') + } + }) + .catch((error) => { + console.error('Error during logout request:', error) + }) + } } diff --git a/apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts b/apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts index 2ec7eafc53..9290157ac4 100644 --- a/apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts +++ b/apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts @@ -75,12 +75,16 @@ describe('DuplicateRecordResolver', () => { expect(resolvedData).toBeUndefined() }) it('should show error notification', () => { - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - title: 'editor.record.loadError.title', - text: 'editor.record.loadError.body oopsie', - closeMessage: 'editor.record.loadError.closeMessage', - }) + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'error', + title: 'editor.record.loadError.title', + text: 'editor.record.loadError.body oopsie', + closeMessage: 'editor.record.loadError.closeMessage', + }, + undefined, + expect.any(Error) + ) }) }) }) diff --git a/apps/metadata-editor/src/app/duplicate-record.resolver.ts b/apps/metadata-editor/src/app/duplicate-record.resolver.ts index 89df9a5699..78f95c4890 100644 --- a/apps/metadata-editor/src/app/duplicate-record.resolver.ts +++ b/apps/metadata-editor/src/app/duplicate-record.resolver.ts @@ -23,18 +23,22 @@ export class DuplicateRecordResolver { .openRecordForDuplication(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' - ), - }) + 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' + ), + }, + undefined, + error + ) return EMPTY }) ) 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 ffe79371a8..28bb067edb 100644 --- a/apps/metadata-editor/src/app/edit-record.resolver.spec.ts +++ b/apps/metadata-editor/src/app/edit-record.resolver.spec.ts @@ -75,12 +75,16 @@ describe('EditRecordResolver', () => { expect(resolvedData).toBeUndefined() }) it('should show error notification', () => { - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - title: 'editor.record.loadError.title', - text: 'editor.record.loadError.body oopsie', - closeMessage: 'editor.record.loadError.closeMessage', - }) + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'error', + title: 'editor.record.loadError.title', + text: 'editor.record.loadError.body oopsie', + closeMessage: 'editor.record.loadError.closeMessage', + }, + undefined, + expect.any(Error) + ) }) }) }) diff --git a/apps/metadata-editor/src/app/edit-record.resolver.ts b/apps/metadata-editor/src/app/edit-record.resolver.ts index 337a1bf858..fe4c9d7486 100644 --- a/apps/metadata-editor/src/app/edit-record.resolver.ts +++ b/apps/metadata-editor/src/app/edit-record.resolver.ts @@ -23,18 +23,22 @@ export class EditRecordResolver { .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' - ), - }) + 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' + ), + }, + undefined, + error + ) return EMPTY }) ) diff --git a/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.html b/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.html index f08b021ff7..ef09afaa7a 100644 --- a/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.html +++ b/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.html @@ -1,8 +1,10 @@ @@ -11,12 +13,49 @@ + +
+ editor.record.publish.confirmation.message +
+ {{ + 'editor.record.publish.confirmation.cancelText' | translate + }} + {{ + 'editor.record.publish.confirmation.confirmText' | translate + }} +
+
+
diff --git a/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.spec.ts b/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.spec.ts index f8e904a00f..8e093d8ee1 100644 --- a/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.spec.ts +++ b/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.spec.ts @@ -1,7 +1,12 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing' import { PublishButtonComponent } from './publish-button.component' import { EditorFacade } from '@geonetwork-ui/feature/editor' -import { BehaviorSubject, firstValueFrom, of } from 'rxjs' +import { BehaviorSubject, Subject, firstValueFrom, of } from 'rxjs' import { TranslateModule } from '@ngx-translate/core' import { HttpClientModule } from '@angular/common/http' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' @@ -10,6 +15,7 @@ import { RecordsApiService, } from '@geonetwork-ui/data-access/gn4' import { barbieUserFixture } from '@geonetwork-ui/common/fixtures' +import { OverlayRef } from '@angular/cdk/overlay' class EditorFacadeMock { changedSinceSave$ = new BehaviorSubject(false) @@ -18,9 +24,26 @@ class EditorFacadeMock { record$ = new BehaviorSubject({ ownerOrganization: { name: 'Group 1', id: 1 }, uniqueIdentifier: 304, + recordUpdated: new Date('2023-01-01'), + extras: { ownerInfo: '1|John|Doe' }, }) saveRecord = jest.fn() saveSuccess$ = new BehaviorSubject(true) + checkHasRecordChanged = jest.fn() + hasRecordChanged$ = new Subject() + isRecordNotYetSaved = jest.fn().mockReturnValue(false) + recordHasDraft = jest.fn().mockReturnValue(true) + getAllDrafts = jest + .fn() + .mockReturnValue( + of([{ uniqueIdentifier: 304, recordUpdated: new Date('2023-01-01') }]) + ) + getRecord = jest.fn().mockReturnValue( + of({ + recordUpdated: new Date('2023-02-01'), + extras: { ownerInfo: '1|John|Doe' }, + }) + ) } const user = barbieUserFixture() @@ -47,6 +70,7 @@ describe('PublishButtonComponent', () => { let fixture: ComponentFixture let facade: EditorFacadeMock let recordsApiService: RecordsApiService + let overlaySpy: any beforeEach(async () => { await TestBed.configureTestingModule({ @@ -79,6 +103,12 @@ describe('PublishButtonComponent', () => { facade = TestBed.inject(EditorFacade) as any fixture = TestBed.createComponent(PublishButtonComponent) component = fixture.componentInstance + overlaySpy = { + dispose: jest.fn(), + attach: jest.fn(), + backdropClick: jest.fn().mockReturnValue(of()), + } + component['overlayRef'] = overlaySpy fixture.detectChanges() }) @@ -127,4 +157,51 @@ describe('PublishButtonComponent', () => { ) }) }) + describe('#confirmPublish', () => { + it('should call saveRecord', () => { + const saveRecordSpy = jest.spyOn(component, 'saveRecord') + component.confirmPublish() + expect(saveRecordSpy).toHaveBeenCalled() + }) + }) + + describe('#cancelPublish', () => { + it('should set isActionMenuOpen to false', () => { + component.isActionMenuOpen = true + component.cancelPublish() + expect(component.isActionMenuOpen).toBe(false) + }) + }) + + describe('#verifyPublishConditions', () => { + it('should call openConfirmationMenu if hasRecordChanged emits with a date', () => { + const openConfirmationMenuSpy = jest.spyOn( + component, + 'openConfirmationMenu' + ) + const saveRecordSpy = jest.spyOn(component, 'saveRecord') + + component.verifyPublishConditions() + facade.hasRecordChanged$.next(null) + facade.hasRecordChanged$.next({ date: new Date(), user: 'John Doe' }) + + expect(openConfirmationMenuSpy).toHaveBeenCalled() + expect(saveRecordSpy).not.toHaveBeenCalled() + }) + + it('should call saveRecord if hasRecordChanged emits without a date', () => { + const openConfirmationMenuSpy = jest.spyOn( + component, + 'openConfirmationMenu' + ) + const saveRecordSpy = jest.spyOn(component, 'saveRecord') + + component.verifyPublishConditions() + facade.hasRecordChanged$.next(null) + facade.hasRecordChanged$.next({ date: undefined, user: undefined }) + + expect(saveRecordSpy).toHaveBeenCalled() + expect(openConfirmationMenuSpy).not.toHaveBeenCalled() + }) + }) }) diff --git a/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.ts b/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.ts index 8442bdc6ef..31d4eb09d3 100644 --- a/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.ts +++ b/apps/metadata-editor/src/app/edit/components/publish-button/publish-button.component.ts @@ -1,14 +1,28 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { + CdkConnectedOverlay, + CdkOverlayOrigin, + Overlay, + OverlayRef, +} from '@angular/cdk/overlay' +import { TemplatePortal } from '@angular/cdk/portal' import { CommonModule } from '@angular/common' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + OnDestroy, + TemplateRef, + ViewChild, + ViewContainerRef, +} from '@angular/core' +import { MatMenuTrigger } from '@angular/material/menu' import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' -import { EditorFacade } from '@geonetwork-ui/feature/editor' import { MatTooltipModule } from '@angular/material/tooltip' -import { TranslateModule } from '@ngx-translate/core' -import { combineLatest, Observable } from 'rxjs' -import { map, switchMap, take } from 'rxjs/operators' -import { RecordsApiService } from '@geonetwork-ui/data-access/gn4' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { RecordsApiService } from '@geonetwork-ui/data-access/gn4' +import { EditorFacade } from '@geonetwork-ui/feature/editor' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { NgIconComponent, provideIcons, @@ -16,9 +30,19 @@ import { } from '@ng-icons/core' import { iconoirCloudUpload } from '@ng-icons/iconoir' import { matCheckCircleOutline } from '@ng-icons/material-icons/outline' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { combineLatest, Observable, of, Subscription } from 'rxjs' +import { + catchError, + concatMap, + map, + skip, + switchMap, + take, + withLatestFrom, +} from 'rxjs/operators' export type RecordSaveStatus = 'saving' | 'upToDate' | 'hasChanges' - @Component({ selector: 'md-editor-publish-button', standalone: true, @@ -29,6 +53,8 @@ export type RecordSaveStatus = 'saving' | 'upToDate' | 'hasChanges' MatTooltipModule, TranslateModule, NgIconComponent, + CdkOverlayOrigin, + CdkConnectedOverlay, ], providers: [ provideIcons({ iconoirCloudUpload, matCheckCircleOutline }), @@ -40,7 +66,8 @@ export type RecordSaveStatus = 'saving' | 'upToDate' | 'hasChanges' styleUrls: ['./publish-button.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PublishButtonComponent { +export class PublishButtonComponent implements OnDestroy { + subscription = new Subscription() status$: Observable = combineLatest([ this.facade.changedSinceSave$, this.facade.saving$, @@ -58,12 +85,103 @@ export class PublishButtonComponent { record$ = this.facade.record$ + @ViewChild(MatMenuTrigger) trigger: MatMenuTrigger + + @ViewChild('actionMenuButton', { read: ElementRef }) + actionMenuButton!: ElementRef + @ViewChild('template') template!: TemplateRef + private overlayRef!: OverlayRef + + isActionMenuOpen = false + publishWarning = null + constructor( private facade: EditorFacade, private recordsApiService: RecordsApiService, - private platformService: PlatformServiceInterface + private platformService: PlatformServiceInterface, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private cdr: ChangeDetectorRef, + private translateService: TranslateService ) {} + ngOnDestroy() { + this.subscription.unsubscribe() + } + + confirmPublish() { + this.saveRecord() + this.closeMenu() + } + + cancelPublish() { + if (this.overlayRef) { + this.closeMenu() + } + } + + closeMenu() { + this.isActionMenuOpen = false + this.overlayRef.dispose() + this.cdr.markForCheck() + } + + openConfirmationMenu() { + this.isActionMenuOpen = true + const positionStrategy = this.overlay + .position() + .flexibleConnectedTo(this.actionMenuButton) + .withPositions([ + { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top', + }, + ]) + + this.overlayRef = this.overlay.create({ + hasBackdrop: true, + backdropClass: 'cdk-overlay-transparent-backdrop', + positionStrategy: positionStrategy, + scrollStrategy: this.overlay.scrollStrategies.reposition(), + }) + + const portal = new TemplatePortal(this.template, this.viewContainerRef) + + this.overlayRef.attach(portal) + + this.overlayRef.backdropClick().subscribe(() => { + this.cancelPublish() + }) + } + + verifyPublishConditions() { + this.facade.hasRecordChanged$ + .pipe( + skip(1), + take(1), + catchError(() => of({ user: undefined, date: undefined })) + ) + .subscribe((hasChanged) => { + if (hasChanged?.date) { + this.publishWarning = hasChanged + this.openConfirmationMenu() + } else { + this.saveRecord() + } + }) + + this.facade.record$ + .pipe( + take(1), + map((record) => { + this.facade.checkHasRecordChanged(record) + }) + ) + .subscribe() + } + saveRecord() { this.facade.saveRecord() this.facade.saveSuccess$ @@ -84,4 +202,14 @@ export class PublishButtonComponent { ) .subscribe() } + + formatDate(date: Date): string { + return date.toLocaleDateString(this.translateService.currentLang, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }) + } } 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 c191759ce0..8257d34748 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 @@ -1,5 +1,5 @@
- - + --> editor.record.saveStatus.draftWithChangesPending
- - + --> 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 afd16d6833..129b8bc541 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 @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { MatDialog, MatDialogModule } from '@angular/material/dialog' import { MatTooltipModule } from '@angular/material/tooltip' import { EditorFacade } from '@geonetwork-ui/feature/editor' diff --git a/apps/metadata-editor/src/app/edit/edit-page.component.html b/apps/metadata-editor/src/app/edit/edit-page.component.html index 3b08626cd7..01423fd7e3 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.html +++ b/apps/metadata-editor/src/app/edit/edit-page.component.html @@ -11,6 +11,20 @@
+
+
+ editor.record.form.draft.updateAlert +
+
{ describe('publish version error', () => { it('shows notification', () => { ;(facade.saveError$ as any).next(new PublicationVersionError('1.0.0')) - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - title: 'editor.record.publishVersionError.title', - text: 'editor.record.publishVersionError.body', - closeMessage: 'editor.record.publishVersionError.closeMessage', - }) + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'error', + title: 'editor.record.publishVersionError.title', + text: 'editor.record.publishVersionError.body', + closeMessage: 'editor.record.publishVersionError.closeMessage', + }, + undefined, + expect.any(PublicationVersionError) + ) }) }) describe('publish error', () => { it('shows notification', () => { ;(facade.saveError$ as any).next(new Error('oopsie')) - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - title: 'editor.record.publishError.title', - text: 'editor.record.publishError.body oopsie', - closeMessage: 'editor.record.publishError.closeMessage', - }) + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'error', + title: 'editor.record.publishError.title', + text: 'editor.record.publishError.body oopsie', + closeMessage: 'editor.record.publishError.closeMessage', + }, + undefined, + expect.any(Error) + ) }) }) @@ -204,4 +213,25 @@ describe('EditPageComponent', () => { expect(await firstValueFrom(component.isLastPage$)).toBe(false) }) }) + + describe('subscriptions', () => { + it('should add 5 subscriptions to component.subscription', () => { + const addSpy = jest.spyOn(component.subscription, 'add') + component.ngOnInit() + expect(addSpy).toHaveBeenCalledTimes(5) + }) + it('should add 6 subscriptions to component.subscription when on /create route', () => { + const activatedRoute = TestBed.inject(ActivatedRoute) + activatedRoute.snapshot.routeConfig.path = '/create' + fixture.detectChanges() + const addSpy = jest.spyOn(component.subscription, 'add') + component.ngOnInit() + expect(addSpy).toHaveBeenCalledTimes(6) + }) + it('unsubscribes', () => { + const unsubscribeSpy = jest.spyOn(component.subscription, 'unsubscribe') + component.ngOnDestroy() + expect(unsubscribeSpy).toHaveBeenCalled() + }) + }) }) 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 fe630e18ba..b2f46d20c5 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.ts @@ -21,10 +21,9 @@ import { import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { TranslateModule, TranslateService } from '@ngx-translate/core' import { combineLatest, filter, firstValueFrom, Subscription, take } from 'rxjs' -import { map } from 'rxjs/operators' +import { map, skip } from 'rxjs/operators' import { SidebarComponent } from '../dashboard/sidebar/sidebar.component' import { PageSelectorComponent } from './components/page-selector/page-selector.component' -import { PublishButtonComponent } from './components/publish-button/publish-button.component' import { TopToolbarComponent } from './components/top-toolbar/top-toolbar.component' marker('editor.record.form.bottomButtons.comeBackLater') @@ -41,7 +40,6 @@ marker('editor.record.form.bottomButtons.next') CommonModule, ButtonComponent, MatProgressSpinnerModule, - PublishButtonComponent, TopToolbarComponent, NotificationsContainerComponent, PageSelectorComponent, @@ -59,6 +57,7 @@ export class EditPageComponent implements OnInit, OnDestroy { isLastPage$ = combineLatest([this.currentPage$, this.pagesLength$]).pipe( map(([currentPage, pagesCount]) => currentPage >= pagesCount - 1) ) + hasRecordChanged$ = this.facade.hasRecordChanged$.pipe(skip(1)) @ViewChild('scrollContainer') scrollContainer: ElementRef @@ -83,32 +82,40 @@ export class EditPageComponent implements OnInit, OnDestroy { this.subscription.add( this.facade.saveError$.subscribe((error) => { if (error instanceof PublicationVersionError) { - this.notificationsService.showNotification({ - type: 'error', - title: this.translateService.instant( - 'editor.record.publishVersionError.title' - ), - text: this.translateService.instant( - 'editor.record.publishVersionError.body', - { currentVersion: error.detectedApiVersion } - ), - closeMessage: this.translateService.instant( - 'editor.record.publishVersionError.closeMessage' - ), - }) + this.notificationsService.showNotification( + { + type: 'error', + title: this.translateService.instant( + 'editor.record.publishVersionError.title' + ), + text: this.translateService.instant( + 'editor.record.publishVersionError.body', + { currentVersion: error.detectedApiVersion } + ), + closeMessage: this.translateService.instant( + 'editor.record.publishVersionError.closeMessage' + ), + }, + undefined, + error + ) } else { - this.notificationsService.showNotification({ - type: 'error', - title: this.translateService.instant( - 'editor.record.publishError.title' - ), - text: `${this.translateService.instant( - 'editor.record.publishError.body' - )} ${error.message}`, - closeMessage: this.translateService.instant( - 'editor.record.publishError.closeMessage' - ), - }) + this.notificationsService.showNotification( + { + type: 'error', + title: this.translateService.instant( + 'editor.record.publishError.title' + ), + text: `${this.translateService.instant( + 'editor.record.publishError.body' + )} ${error.message}`, + closeMessage: this.translateService.instant( + 'editor.record.publishError.closeMessage' + ), + }, + undefined, + error + ) } }) ) @@ -130,27 +137,43 @@ export class EditPageComponent implements OnInit, OnDestroy { }) ) + this.subscription.add( + this.facade.record$.subscribe((record) => { + this.facade.checkHasRecordChanged(record) + }) + ) + // 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], { - replaceUrl: true, + this.subscription.add( + this.facade.draftSaveSuccess$.pipe(take(1)).subscribe(() => { + this.router.navigate(['edit', currentRecord.uniqueIdentifier], { + replaceUrl: true, + }) }) - }) + ) } // 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]) + this.subscription.add( + this.facade.record$ + .pipe( + filter( + (record) => + record?.uniqueIdentifier !== currentRecord.uniqueIdentifier + ), + take(1) + ) + .subscribe((savedRecord) => { + this.router.navigate(['edit', savedRecord.uniqueIdentifier]) + }) + ) + + this.subscription.add( + this.facade.record$.subscribe((record) => { + this.facade.checkHasRecordChanged(record) }) + ) } ngOnDestroy() { @@ -182,4 +205,14 @@ export class EditPageComponent implements OnInit, OnDestroy { top: 0, }) } + + formatDate(date: Date): string { + return date.toLocaleDateString(this.translateService.currentLang, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }) + } } diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.html b/apps/metadata-editor/src/app/records/all-records/all-records.component.html index 2cef0f1d74..29ac1b8ca3 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.html +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.html @@ -1,6 +1,3 @@ -
- -
@@ -27,22 +24,6 @@

-
- dashboard.results.listMetadata -
-
- dashboard.results.listResources -
{ const searchFilters = new BehaviorSubject({ any: 'hello world', + owner: {}, }) let component: AllRecordsComponent @@ -34,6 +35,7 @@ describe('AllRecordsComponent', () => { let searchFacade: SearchFacade let platformService: PlatformServiceInterface let fieldsService: FieldsService + let searchService: SearchService beforeEach(() => { return MockBuilder(AllRecordsComponent) @@ -76,6 +78,7 @@ describe('AllRecordsComponent', () => { router = TestBed.inject(Router) searchFacade = TestBed.inject(SearchFacade) + searchService = TestBed.inject(SearchService) platformService = TestBed.inject(PlatformServiceInterface) fieldsService = TestBed.inject(FieldsService) @@ -103,6 +106,8 @@ describe('AllRecordsComponent', () => { searchFacade.setPageSize = jest.fn(() => this) searchFacade.setConfigRequestFields = jest.fn(() => this) + searchService.setFilters = jest.fn(() => this) + component = fixture.componentInstance fixture.detectChanges() @@ -119,6 +124,35 @@ describe('AllRecordsComponent', () => { }) }) + describe('when updating the search filters', () => { + beforeEach(() => { + searchFilters.next({ any: 'new search', owner: { 1: true } }) + }) + + it('updates the search text', async () => { + const searchText = await firstValueFrom(component.searchText$) + expect(searchText).toBe('new search') + }) + it('resets the owner filter', () => { + expect(searchService.setFilters).toHaveBeenCalledWith({ + any: 'new search', + }) + }) + }) + + describe('when destroying the component', () => { + beforeEach(() => { + component.ngOnDestroy() + }) + + it('resets the search filters', () => { + expect(searchFacade.updateFilters).toHaveBeenCalledWith({ any: '' }) + }) + it('unsubscribes from component subscription', () => { + expect(component.subscription.closed).toBe(true) + }) + }) + describe('when clicking createRecord', () => { beforeEach(() => { component.createRecord() diff --git a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts index d0a76bdf85..7a025b0ee5 100644 --- a/apps/metadata-editor/src/app/records/all-records/all-records.component.ts +++ b/apps/metadata-editor/src/app/records/all-records/all-records.component.ts @@ -3,32 +3,24 @@ import { ChangeDetectorRef, Component, ElementRef, + OnDestroy, + OnInit, TemplateRef, ViewChild, ViewContainerRef, } from '@angular/core' -import { - ResultsTableContainerComponent, - SearchFacade, - SearchService, -} from '@geonetwork-ui/feature/search' +import { SearchFacade, SearchService } from '@geonetwork-ui/feature/search' import { TranslateModule } from '@ngx-translate/core' import { Router } from '@angular/router' import { RecordsCountComponent } from '../records-count/records-count.component' -import { Observable } from 'rxjs' +import { Observable, Subscription } from 'rxjs' import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' -import { - CdkConnectedOverlay, - CdkOverlayOrigin, - Overlay, - OverlayRef, -} from '@angular/cdk/overlay' +import { CdkOverlayOrigin, Overlay, OverlayRef } from '@angular/cdk/overlay' import { TemplatePortal } from '@angular/cdk/portal' import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' import { RecordsListComponent } from '../records-list.component' -import { map } from 'rxjs/operators' -import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' +import { map, take } from 'rxjs/operators' import { SearchFiltersComponent } from '../../dashboard/search-filters/search-filters.component' import { NgIconComponent, @@ -50,14 +42,11 @@ import { CommonModule, TranslateModule, RecordsCountComponent, - ResultsTableContainerComponent, UiElementsModule, UiInputsModule, ImportRecordComponent, CdkOverlayOrigin, - CdkConnectedOverlay, RecordsListComponent, - SearchHeaderComponent, SearchFiltersComponent, NgIconComponent, ], @@ -72,16 +61,14 @@ import { }), ], }) -export class AllRecordsComponent { +export class AllRecordsComponent implements OnInit, OnDestroy { @ViewChild('importRecordButton', { read: ElementRef }) importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - searchFields = ['user'] - searchText$: Observable = - this.searchFacade.searchFilters$.pipe( - map((filters) => ('any' in filters ? (filters['any'] as string) : null)) - ) + searchFields = ['user', 'changeDate'] + searchText$: Observable + subscription: Subscription isImportMenuOpen = false @@ -94,6 +81,31 @@ export class AllRecordsComponent { private cdr: ChangeDetectorRef ) {} + ngOnInit() { + this.subscription = this.searchFacade.searchFilters$ + .pipe( + map((filters) => { + if ('owner' in filters) { + const { owner, ...rest } = filters + return rest + } + return filters + }), + take(1) + ) + .subscribe((filters) => { + this.searchService.setFilters(filters) + }) + this.searchText$ = this.searchFacade.searchFilters$.pipe( + map((filters) => ('any' in filters ? (filters['any'] as string) : null)) + ) + } + + ngOnDestroy() { + this.searchFacade.updateFilters({ any: '' }) + this.subscription.unsubscribe() + } + createRecord() { this.router.navigate(['/create']).catch((err) => console.error(err)) } 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 f65d41a994..fc400041ce 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,7 +1,6 @@ -
-

+

dashboard.records.myDraft

@@ -10,6 +9,7 @@

class="shadow-md shadow-gray-300 border-[1px] border-gray-200 overflow-hidden rounded bg-white grow mx-[32px] my-[16px] text-sm" > - -
- -

- dashboard.records.search -

-
- -
-
- -

+ +

dashboard.records.myRecords

- +

-
- dashboard.myRecords.publishedMetadatas -
-
- dashboard.myRecords.currentlyEdited -
{ owner: user.id, }) }) - - it('should map search filters to searchText$', (done) => { - component.searchText$.subscribe((text) => { - expect(text).toBe('hello world') - done() - }) - }) }) }) diff --git a/apps/metadata-editor/src/app/records/my-records/my-records.component.ts b/apps/metadata-editor/src/app/records/my-records/my-records.component.ts index f389fb265f..e255dff1d3 100644 --- a/apps/metadata-editor/src/app/records/my-records/my-records.component.ts +++ b/apps/metadata-editor/src/app/records/my-records/my-records.component.ts @@ -13,7 +13,7 @@ import { RecordsListComponent } from '../records-list.component' import { FeatureSearchModule, FieldsService, - ResultsTableContainerComponent, + FILTER_SUMMARY_IGNORE_LIST, SearchFacade, } from '@geonetwork-ui/feature/search' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' @@ -24,8 +24,6 @@ import { TemplatePortal } from '@angular/cdk/portal' import { RecordsCountComponent } from '../records-count/records-count.component' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { ImportRecordComponent } from '@geonetwork-ui/feature/editor' -import { SearchHeaderComponent } from '../../dashboard/search-header/search-header.component' -import { map, Observable } from 'rxjs' import { SearchFiltersComponent } from '../../dashboard/search-filters/search-filters.component' import { NgIconComponent, @@ -38,6 +36,8 @@ import { iconoirPagePlus, } from '@ng-icons/iconoir' +const FILTER_OWNER = 'owner' + @Component({ selector: 'md-editor-my-records', templateUrl: './my-records.component.html', @@ -47,13 +47,11 @@ import { CommonModule, TranslateModule, RecordsListComponent, - ResultsTableContainerComponent, UiElementsModule, RecordsCountComponent, ButtonComponent, ImportRecordComponent, FeatureSearchModule, - SearchHeaderComponent, SearchFiltersComponent, NgIconComponent, ], @@ -66,6 +64,7 @@ import { provideNgIconsConfig({ size: '1.5rem', }), + { provide: FILTER_SUMMARY_IGNORE_LIST, useValue: [FILTER_OWNER] }, ], }) export class MyRecordsComponent implements OnInit { @@ -73,8 +72,7 @@ export class MyRecordsComponent implements OnInit { private importRecordButton!: ElementRef @ViewChild('template') template!: TemplateRef private overlayRef!: OverlayRef - searchFields = [] - searchText$: Observable + searchFields = ['changeDate'] isImportMenuOpen = false @@ -93,15 +91,11 @@ export class MyRecordsComponent implements OnInit { this.platformService.getMe().subscribe((user) => { this.fieldsService - .buildFiltersFromFieldValues({ owner: user.id }) + .buildFiltersFromFieldValues({ [FILTER_OWNER]: user.id }) .subscribe((filters) => { this.searchFacade.updateFilters(filters) }) }) - - this.searchText$ = this.searchFacade.searchFilters$.pipe( - map((filters) => ('any' in filters ? (filters['any'] as string) : null)) - ) } createRecord() { 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 215e2e950f..b5be0a5672 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -7,10 +7,6 @@ >
- +
diff --git a/apps/metadata-editor/src/app/records/records-list.component.spec.ts b/apps/metadata-editor/src/app/records/records-list.component.spec.ts index 2b1621ed6f..f7e8f9a501 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.spec.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.spec.ts @@ -1,49 +1,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { SearchFacade, SearchService } from '@geonetwork-ui/feature/search' +import { + ResultsTableContainerComponent, + SearchFacade, + SearchService, +} from '@geonetwork-ui/feature/search' import { allSearchFields, RecordsListComponent } from './records-list.component' -import { Component, EventEmitter, Input, Output } from '@angular/core' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { By } from '@angular/platform-browser' import { Router } from '@angular/router' import { BehaviorSubject } from 'rxjs' -import { CommonModule } from '@angular/common' import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures' +import { MockBuilder } from 'ng-mocks' +import { PaginationButtonsComponent } from '@geonetwork-ui/ui/layout' const results = [{ md: true }] const currentPage = 5 const totalPages = 25 -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-results-table-container', - template: '', - standalone: true, -}) -export class ResultsTableContainerComponent { - @Output() recordClick = new EventEmitter() - @Output() duplicateRecord = new EventEmitter() -} - -@Component({ - // eslint-disable-next-line @angular-eslint/component-selector - selector: 'gn-ui-pagination-buttons', - template: '', - standalone: true, -}) -export class PaginationButtonsComponent { - @Input() currentPage = 1 - @Input() totalPages = 1 - @Input() hideButton = false - @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) @@ -68,6 +40,8 @@ describe('RecordsListComponent', () => { let searchService: SearchService let searchFacade: SearchFacade + beforeEach(() => MockBuilder(RecordsListComponent)) + beforeEach(() => { TestBed.configureTestingModule({ providers: [ @@ -84,15 +58,6 @@ describe('RecordsListComponent', () => { useClass: SearchServiceMock, }, ], - }).overrideComponent(RecordsListComponent, { - set: { - imports: [ - CommonModule, - ResultsTableContainerComponent, - PaginationButtonsComponent, - RecordsCountComponent, - ], - }, }) router = TestBed.inject(Router) searchService = TestBed.inject(SearchService) @@ -132,8 +97,7 @@ describe('RecordsListComponent', () => { }) it('displays pagination', () => { expect(pagination).toBeTruthy() - expect(pagination.currentPage).toEqual(currentPage) - expect(pagination.totalPages).toEqual(totalPages) + expect(pagination.listComponent).toBe(component) }) describe('when click on a record', () => { const uniqueIdentifier = 123 @@ -163,7 +127,7 @@ describe('RecordsListComponent', () => { }) describe('when click on pagination', () => { beforeEach(() => { - pagination.newCurrentPageEvent.emit(3) + component.goToPage(3) }) it('paginates', () => { expect(searchService.setPage).toHaveBeenCalledWith(3) 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 83b3b75ab7..958b943f88 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.ts @@ -11,7 +11,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' +import { Paginable, PaginationButtonsComponent } from '@geonetwork-ui/ui/layout' export const allSearchFields = [ 'uuid', @@ -36,10 +36,10 @@ export const allSearchFields = [ TranslateModule, ResultsTableContainerComponent, UiInputsModule, - RecordsCountComponent, + PaginationButtonsComponent, ], }) -export class RecordsListComponent implements OnInit { +export class RecordsListComponent implements OnInit, Paginable { constructor( private router: Router, public searchFacade: SearchFacade, @@ -49,10 +49,13 @@ export class RecordsListComponent implements OnInit { ngOnInit(): void { this.searchFacade.setConfigRequestFields(allSearchFields) this.searchFacade.setPageSize(15) - } - paginate(page: number) { - this.searchService.setPage(page) + this.searchFacade.currentPage$.subscribe((page) => { + this.currentPage_ = page + }) + this.searchFacade.totalPages$.subscribe((total) => { + this.totalPages_ = total + }) } editRecord(record: CatalogRecord) { @@ -62,4 +65,31 @@ export class RecordsListComponent implements OnInit { duplicateRecord(record: CatalogRecord) { this.router.navigate(['/duplicate', record.uniqueIdentifier]) } + + // these are 0 based + totalPages_: number + currentPage_: number + + // Paginable API + get isFirstPage() { + return this.currentPage_ === 1 + } + get isLastPage() { + return this.currentPage_ === this.totalPages_ + } + get pagesCount() { + return this.totalPages_ + } + get currentPage() { + return this.currentPage_ + } + goToPage(page: number) { + this.searchService.setPage(page) + } + goToNextPage() { + this.searchService.setPage(this.currentPage_ + 1) + } + goToPrevPage() { + this.searchService.setPage(this.currentPage_ - 1) + } } diff --git a/apps/metadata-editor/src/styles.css b/apps/metadata-editor/src/styles.css index 661c2e2930..aeb3e3a1f6 100644 --- a/apps/metadata-editor/src/styles.css +++ b/apps/metadata-editor/src/styles.css @@ -32,3 +32,7 @@ body { .mat-mdc-button-base { line-height: normal; } + +.input-as-button { + @apply border-2 border-gray-300 hover:border-main bg-transparent hover:text-main py-[11.5px] pl-[14px] pr-2; +} diff --git a/apps/search/src/app/app.module.ts b/apps/search/src/app/app.module.ts index 7b1d5d3b8b..a05b04fc82 100644 --- a/apps/search/src/app/app.module.ts +++ b/apps/search/src/app/app.module.ts @@ -2,7 +2,6 @@ import { HttpClientModule } from '@angular/common/http' import { NgModule } from '@angular/core' import { BrowserModule } from '@angular/platform-browser' import { FeatureCatalogModule } from '@geonetwork-ui/feature/catalog' -import { FeatureDatavizModule } from '@geonetwork-ui/feature/dataviz' import { FeatureMapModule } from '@geonetwork-ui/feature/map' import { UiLayoutModule } from '@geonetwork-ui/ui/layout' import { @@ -38,7 +37,6 @@ export const metaReducers: MetaReducer[] = !environment.production FeatureCatalogModule, UiLayoutModule, FeatureMapModule, - FeatureDatavizModule, StoreModule.forRoot({}, { metaReducers }), !environment.production ? StoreDevtoolsModule.instrument() : [], EffectsModule.forRoot(), diff --git a/apps/webcomponents/src/app/webcomponents.module.ts b/apps/webcomponents/src/app/webcomponents.module.ts index 7d2fa698fa..dc9db6efaa 100644 --- a/apps/webcomponents/src/app/webcomponents.module.ts +++ b/apps/webcomponents/src/app/webcomponents.module.ts @@ -33,7 +33,6 @@ import { MapStateContainerComponent, } from '@geonetwork-ui/feature/map' import { GnDatasetViewChartComponent } from './components/gn-dataset-view-chart/gn-dataset-view-chart.component' -import { FeatureDatavizModule } from '@geonetwork-ui/feature/dataviz' import { FeatureAuthModule } from '@geonetwork-ui/feature/auth' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { provideGn4 } from '@geonetwork-ui/api/repository' @@ -89,7 +88,6 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ useClass: EmbeddedTranslateLoader, }, }), - FeatureDatavizModule, FeatureAuthModule, BrowserAnimationsModule, MapStateContainerComponent, diff --git a/conf/default.toml b/conf/default.toml index 6b4872d9f4..624da410ce 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -23,6 +23,8 @@ proxy_path = "" # - ${lang2}, ${lang3}: indicates if and where the current language should be part of the login URL in language 2 or 3 letter code # Example to use the georchestra login page: # login_url = "/cas/login?service=${current_url}" +# logout_url = "/geonetwork/signout" +# settings_url = "/geonetwork/srv/\${lang3}/admin.console#/organization/users?userOrGroup=" # This optional URL should point to the static html page wc-embedder.html which allows to display a web component (like chart and table) via a permalink. # URLs can be indicated from the root of the same server starting with a "/" or as an external URL. Be conscious of potential CORS issues when using an external URL. # The default location in the dockerized datahub app for example is "/datahub/wc-embedder.html". @@ -158,6 +160,9 @@ do_not_use_default_basemap = true # Each layer is defined in its own [[map_layer]] section. # Example: # [[map_layer]] +# type = "maplibre-style" +# styleUrl = "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/gris.json" +# [[map_layer]] # type = "xyz" # url = "https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png" # [[map_layer]] diff --git a/docs/guide/configure.md b/docs/guide/configure.md index 61cce7aa89..5bc353bc34 100644 --- a/docs/guide/configure.md +++ b/docs/guide/configure.md @@ -231,6 +231,8 @@ The map section lets you customize how maps appear and behave across GeoNetwork- - `url` (mandatory for "xyz", "wms" and "wfs" types): Layer endpoint URL. - `name` (mandatory for "wms" and "wfs" types): indicates the layer name or feature type. - `data` (for "geojson" type only): inline GeoJSON data as string. + - `styleUrl` (mandatory for "maplibre-style" type only): Maplibre style URL. + - `accessToken` (optional for "maplibre-style" type only): credential to access the basemap styles service Layer order in the config is the same as in the map, the foreground layer being the last defined one. @@ -254,6 +256,11 @@ The map section lets you customize how maps appear and behave across GeoNetwork- "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [125.6, 10.1]}}] } """ + + [[map_layer]] + type = "maplibre-style" + styleUrl = "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/gris.json" + accessToken = "token_if_needed" # optional ``` - `external_viewer_url_template` (optional) diff --git a/docs/guide/dev-environment.md b/docs/guide/dev-environment.md index c7e4572fc2..b209263911 100644 --- a/docs/guide/dev-environment.md +++ b/docs/guide/dev-environment.md @@ -98,3 +98,11 @@ $ npm run storybook ``` For a guide on how to write Angular component stories, see: https://storybook.js.org/docs/angular/writing-stories/introduction + +### Users + +For development purposes, three users have been created : + +- username : admin, password : admin +- username: johndoe, password: p4ssworD\_ +- username: barbie, password: p4ssworD\_ diff --git a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts index 76fe1ecd7d..6cfa74924b 100644 --- a/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +++ b/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts @@ -55,6 +55,7 @@ export class DcatApConverter extends BaseConverter { spatialExtents: readSpatialExtents, keywords: readKeywords, topics: readTopics, + resourceIdentifier: () => undefined, recordUpdated: readRecordUpdated, recordCreated: readRecordCreated, resourceUpdated: readResourceUpdated, @@ -93,6 +94,7 @@ export class DcatApConverter extends BaseConverter { recordUpdated: () => undefined, recordCreated: () => undefined, recordPublished: () => undefined, + resourceIdentifier: () => undefined, resourceUpdated: () => undefined, resourceCreated: () => undefined, resourcePublished: () => undefined, 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 b48bdadf60..3110a1b7f7 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 @@ -45,13 +45,6 @@ A very interesting dataset (un jeu de données très intéressant) - - - - https://www.geoportail-urbanisme.gouv.fr/document/60036_PLU_20220329 - - - @@ -72,6 +65,13 @@ + + + + 2d974612-70b1-4662-a9f4-c43cbe453773 + + + 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 27a38de64f..e1f1c250a7 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 @@ -146,6 +146,13 @@ + + + + 2d974612-70b1-4662-a9f4-c43cbe453773 + + + 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 3ef61f6de0..af343e7be2 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 @@ -73,6 +73,13 @@ + + + + 2d974612-70b1-4662-a9f4-c43cbe453773 + + + 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 39bd90b360..024cdd93f6 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts @@ -70,6 +70,7 @@ export const GENERIC_DATASET_RECORD: DatasetRecord = { recordCreated: new Date('2021-11-15T09:00:00'), recordPublished: new Date('2022-01-01T10:00:00'), recordUpdated: new Date('2022-02-01T15:12:00'), + resourceIdentifier: '2d974612-70b1-4662-a9f4-c43cbe453773', resourceCreated: new Date('2022-09-01T14:18:19'), resourceUpdated: new Date('2022-12-04T15:12:00'), title: 'A very interesting dataset (un jeu de données très intéressant)', diff --git a/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts b/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts index 6b20e5a56f..156741f698 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts @@ -146,4 +146,6 @@ Ce lot de données produit en 2019, a été numérisé à partir du PCI Vecteur defaultLanguage: 'fr', otherLanguages: [], translations: {}, + resourceIdentifier: + 'https://www.geoportail-urbanisme.gouv.fr/document/60036_PLU_20220329', } diff --git a/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts b/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts index 72450d8f85..afb3cbc13b 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts @@ -68,6 +68,7 @@ export const GEOCAT_CH_DATASET_RECORD: DatasetRecord = { abstract: `Perimeter der Alpenkonvention in der Schweiz. Die Alpenkonvention ist ein völkerrechtlicher Vertrag zwischen den acht Alpenländern Deutschland, Frankreich, Italien, Liechtenstein, Monaco, Österreich, Schweiz, Slowenien sowie der Europäischen Union. Das Ziel des Übereinkommens ist der Schutz der Alpen durch eine sektorübergreifende, ganzheitliche und nachhaltige Politik.`, overviews: [], topics: ['planningCadastre', 'planningCadastre_Planning'], + resourceIdentifier: 'ch.are.alpenkonvention', keywords: [ { thesaurus: { diff --git a/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts b/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts index 546b13116f..c2d76808e4 100644 --- a/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +++ b/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts @@ -71,6 +71,7 @@ Toutes ces données sont reprises dans BDR.`, recordCreated: new Date('2019-04-02T12:34:35'), recordUpdated: new Date('2022-06-16T05:01:21'), resourceCreated: new Date('2002-01-01'), + resourceIdentifier: '2d974612-70b1-4662-a9f4-c43cbe453773', resourceUpdated: new Date('2022-06-16'), resourcePublished: new Date('2022-06-16'), onlineResources: [ @@ -644,6 +645,7 @@ export const METAWAL_SERVICE_RECORD: ServiceRecord = { }, recordCreated: new Date('2019-04-02T12:31:58'), recordUpdated: new Date('2022-02-09T11:31:06.766Z'), + resourceIdentifier: '6d2b6fdb-f1ea-4d48-8697-a0c05512f1dc', resourcePublished: new Date('2016-12-01'), securityConstraints: [], title: 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 4a7d1b474e..bf3f6e0e51 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts @@ -34,6 +34,7 @@ import { readOwnerOrganization, readRecordUpdated, readResourceCreated, + readResourceIdentifier, readResourcePublished, readResourceUpdated, readSecurityConstraints, @@ -61,6 +62,7 @@ import { writeOtherConstraints, writeRecordUpdated, writeResourceCreated, + writeResourceIdentifier, writeResourcePublished, writeResourceUpdated, writeSecurityConstraints, @@ -85,6 +87,7 @@ export class Iso19139Converter extends BaseConverter { recordUpdated: readRecordUpdated, recordCreated: () => undefined, // not supported in ISO19139 recordPublished: () => undefined, // not supported in ISO19139 + resourceIdentifier: readResourceIdentifier, resourceUpdated: readResourceUpdated, resourceCreated: readResourceCreated, resourcePublished: readResourcePublished, @@ -124,6 +127,7 @@ export class Iso19139Converter extends BaseConverter { recordUpdated: writeRecordUpdated, recordCreated: () => undefined, // not supported in ISO19139 recordPublished: () => undefined, // not supported in ISO19139 + resourceIdentifier: writeResourceIdentifier, resourceUpdated: writeResourceUpdated, resourceCreated: writeResourceCreated, resourcePublished: writeResourcePublished, @@ -232,6 +236,7 @@ export class Iso19139Converter extends BaseConverter { const onlineResources = this.readers['onlineResources'](rootEl, tr) const otherLanguages = this.readers['otherLanguages'](rootEl, tr) const defaultLanguage = this.readers['defaultLanguage'](rootEl, tr) + const resourceIdentifier = this.readers['resourceIdentifier'](rootEl, tr) if (kind === 'dataset') { const status = this.readers['status'](rootEl, tr) @@ -246,6 +251,7 @@ export class Iso19139Converter extends BaseConverter { return this.afterRecordRead({ uniqueIdentifier, + ...(resourceIdentifier && { resourceIdentifier }), kind, otherLanguages, defaultLanguage, @@ -280,6 +286,7 @@ export class Iso19139Converter extends BaseConverter { } else { return this.afterRecordRead({ uniqueIdentifier, + ...(resourceIdentifier && { resourceIdentifier }), kind, otherLanguages, defaultLanguage, @@ -370,6 +377,8 @@ export class Iso19139Converter extends BaseConverter { this.writers['otherConstraints'](record, rootEl) fieldChanged('onlineResources') && this.writers['onlineResources'](record, rootEl) + fieldChanged('resourceIdentifier') && + this.writers['resourceIdentifier'](record, rootEl) if (record.kind === 'dataset') { fieldChanged('status') && this.writers['status'](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 f33351c880..5580e7390b 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 @@ -19,6 +19,7 @@ import { readContacts, readOnlineResources, readOwnerOrganization, + readResourceIdentifier, readSpatialExtents, readTemporalExtents, } from './read-parts' 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 e1838d9043..4e88f74581 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts @@ -1124,3 +1124,17 @@ export function readDefaultLanguage(rootEl: XmlElement): LanguageCode { map((lang) => (lang ? LANG_3_TO_2_MAPPER[lang.toLowerCase()] : null)) )(rootEl) } + +export function readResourceIdentifier(rootEl: XmlElement): string { + return pipe( + findIdentification(), + findNestedElement( + 'gmd:citation', + 'gmd:CI_Citation', + 'gmd:identifier', + 'gmd:MD_Identifier', + 'gmd:code' + ), + extractCharacterString() + )(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 bf87b7ba18..b41c13e5c1 100644 --- a/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +++ b/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts @@ -1463,3 +1463,20 @@ export function writeDefaultLanguage( writeAttribute('codeListValue', lang3) )(rootEl) } + +export function writeResourceIdentifier( + record: DatasetRecord, + rootEl: XmlElement +) { + pipe( + findOrCreateIdentification(), + findNestedChildOrCreate('gmd:citation', 'gmd:CI_Citation'), + removeChildrenByName('gmd:identifier'), + record.resourceIdentifier + ? pipe( + createNestedChild('gmd:identifier', 'gmd:MD_Identifier', 'gmd:code'), + writeCharacterString(record.resourceIdentifier) + ) + : noop + )(rootEl) +} diff --git a/libs/api/repository/src/lib/gn4/auth/auth.service.spec.ts b/libs/api/repository/src/lib/gn4/auth/auth.service.spec.ts index f604788788..cc3fc4d381 100644 --- a/libs/api/repository/src/lib/gn4/auth/auth.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/auth/auth.service.spec.ts @@ -82,4 +82,23 @@ describe('AuthService', () => { ) }) }) + + describe('Logout', () => { + beforeEach(() => { + service = TestBed.inject(AuthService) + }) + it('should return the logout url', () => { + expect(service.logoutUrl).toEqual('/geonetwork/signout') + }) + }) + describe('Settings', () => { + beforeEach(() => { + service = TestBed.inject(AuthService) + }) + it('should return the logout url', () => { + expect(service.settingsUrl).toEqual( + '/geonetwork/srv/fre/admin.console#/organization/users?userOrGroup=' + ) + }) + }) }) diff --git a/libs/api/repository/src/lib/gn4/auth/auth.service.ts b/libs/api/repository/src/lib/gn4/auth/auth.service.ts index fae24500ba..da6d7fae3b 100644 --- a/libs/api/repository/src/lib/gn4/auth/auth.service.ts +++ b/libs/api/repository/src/lib/gn4/auth/auth.service.ts @@ -5,11 +5,20 @@ import { TranslateService } from '@ngx-translate/core' export const DEFAULT_GN4_LOGIN_URL = `/geonetwork/srv/\${lang3}/catalog.signin?redirect=\${current_url}` export const LOGIN_URL = new InjectionToken('loginUrl') +export const DEFAULT_GN4_LOGOUT_URL = `/geonetwork/signout` +export const LOGOUT_URL = new InjectionToken('logoutUrl') + +export const DEFAULT_GN4_SETTINGS_URL = `/geonetwork/srv/\${lang3}/admin.console#/organization/users?userOrGroup=` +export const SETTINGS_URL = new InjectionToken('settingsUrl') + @Injectable({ providedIn: 'root', }) export class AuthService { baseLoginUrl = this.baseLoginUrlToken || DEFAULT_GN4_LOGIN_URL + baseLogoutUrl = this.baseLogoutUrlToken || DEFAULT_GN4_LOGOUT_URL + baseSettingsUrl = this.baseSettingsUrlToken || DEFAULT_GN4_SETTINGS_URL + get loginUrl() { let baseUrl = this.baseLoginUrl const locationHasQueryParams = !!window.location.search @@ -25,10 +34,22 @@ export class AuthService { LANG_2_TO_3_MAPPER[this.translateService.currentLang] ) } + + get logoutUrl() { + return this.baseLogoutUrl + } + + get settingsUrl() { + return this.baseSettingsUrl.replace( + '${lang3}', + LANG_2_TO_3_MAPPER[this.translateService.currentLang] + ) + } + constructor( - @Optional() - @Inject(LOGIN_URL) - private baseLoginUrlToken: string, + @Optional() @Inject(LOGIN_URL) private baseLoginUrlToken: string, + @Optional() @Inject(LOGOUT_URL) private baseLogoutUrlToken: string, + @Optional() @Inject(SETTINGS_URL) private baseSettingsUrlToken: string, private translateService: TranslateService ) {} } diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.spec.ts b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.spec.ts new file mode 100644 index 0000000000..7108b689da --- /dev/null +++ b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.spec.ts @@ -0,0 +1,46 @@ +import { isDateRange, formatDate } from './date-range.utils' +import { FieldFilter } from '@geonetwork-ui/common/domain/model/search' + +describe('date-range.utils', () => { + describe('isDateRange', () => { + it('should return false if filter is null or undefined', () => { + expect(isDateRange(null)).toBe(false) + expect(isDateRange(undefined)).toBe(false) + }) + + it('should return false if filter is not an object', () => { + expect(isDateRange('string' as any)).toBe(false) + expect(isDateRange(123 as any)).toBe(false) + }) + + it('should return true if filter has start or end properties', () => { + const filterWithStart: FieldFilter = { start: '2023-01-01' } + const filterWithEnd: FieldFilter = { end: '2023-12-31' } + const filterWithBoth: FieldFilter = { + start: '2023-01-01', + end: '2023-12-31', + } + + expect(isDateRange(filterWithStart)).toBe(true) + expect(isDateRange(filterWithEnd)).toBe(true) + expect(isDateRange(filterWithBoth)).toBe(true) + }) + + it('should return false if filter does not have start or end properties', () => { + const filterWithoutDate: FieldFilter = { someOtherField: 'value' } + expect(isDateRange(filterWithoutDate)).toBe(false) + }) + }) + + describe('formatDate', () => { + it('should format date correctly', () => { + const date = new Date(2023, 11, 31) + expect(formatDate(date)).toBe('2023-12-31') + }) + + it('should handle single digit months and days', () => { + const date = new Date(2023, 3, 5) + expect(formatDate(date)).toBe('2023-04-05') + }) + }) +}) diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts new file mode 100644 index 0000000000..3e77e8c92f --- /dev/null +++ b/libs/api/repository/src/lib/gn4/elasticsearch/date-range.utils.ts @@ -0,0 +1,13 @@ +import { FieldFilter } from '@geonetwork-ui/common/domain/model/search' + +export function isDateRange(filter: FieldFilter): boolean { + if (!filter) return false + return typeof filter === 'object' && ('start' in filter || 'end' in filter) +} + +export function formatDate(date: Date): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts index 4519e45b1b..7ad69a3fae 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.spec.ts @@ -163,12 +163,83 @@ describe('ElasticsearchService', () => { }, }) }) - it('add any and other fields query_strings and limit search payload by ids', () => { + it('add any, other fields query_strings and date range and limit search payload by ids', () => { const query = service['buildPayloadQuery']( { Org: { world: true, }, + someDate: { + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }, + any: 'hello', + }, + {}, + ['record-1', 'record-2', 'record-3'] + ) + expect(query).toEqual({ + bool: { + filter: [ + { + terms: { + isTemplate: ['n'], + }, + }, + { + query_string: { + query: 'Org:("world")', + }, + }, + { + range: { + someDate: { + gte: '2020-01-01', + lte: '2020-12-31', + format: 'yyyy-MM-dd', + }, + }, + }, + { + ids: { + values: ['record-1', 'record-2', 'record-3'], + }, + }, + ], + should: [], + must: [ + { + query_string: { + default_operator: 'AND', + fields: [ + 'resourceTitleObject.langfre^5', + 'tag.langfre^4', + 'resourceAbstractObject.langfre^3', + 'lineageObject.langfre^2', + 'any.langfre', + 'uuid', + ], + query: 'hello', + }, + }, + ], + must_not: { + terms: { + resourceType: ['service', 'map', 'map/static', 'mapDigital'], + }, + }, + }, + }) + }) + it('handles date range object with start only, and limit search payload by ids', () => { + const query = service['buildPayloadQuery']( + { + Org: { + world: true, + }, + otherDate: { + start: new Date('2021-03-03'), + }, any: 'hello', }, {}, @@ -187,6 +258,14 @@ describe('ElasticsearchService', () => { query: 'Org:("world")', }, }, + { + range: { + otherDate: { + gte: '2021-03-03', + format: 'yyyy-MM-dd', + }, + }, + }, { ids: { values: ['record-1', 'record-2', 'record-3'], 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 b848e2e515..3ec113189b 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts @@ -11,7 +11,8 @@ import { AggregationsParams, FieldFilter, FieldFilters, - FilterAggregationParams, + FilterQuery, + FiltersAggregationParams, SortByField, } from '@geonetwork-ui/common/domain/model/search' import { METADATA_LANGUAGE } from '../../metadata-language' @@ -26,6 +27,9 @@ import { TermsAggregationResult, } from '@geonetwork-ui/api/metadata-converter' import { LangService } from '@geonetwork-ui/util/i18n' +import { formatDate, isDateRange } from './date-range.utils' + +export type DateRange = { start?: Date; end?: Date } @Injectable({ providedIn: 'root', @@ -213,7 +217,9 @@ export class ElasticsearchService { return this.metadataLang === 'current' } - private filtersToQueryString(filters: FieldFilters): string { + private filtersToQuery( + filters: FieldFilters | FiltersAggregationParams | string + ): FilterQuery { const makeQuery = (filter: FieldFilter): string => { if (typeof filter === 'string') { return filter @@ -227,13 +233,53 @@ export class ElasticsearchService { }) .join(' OR ') } - return Object.keys(filters) - .filter( - (fieldname) => - filters[fieldname] && JSON.stringify(filters[fieldname]) !== '{}' - ) - .map((fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`) - .join(' AND ') + const queryString = + typeof filters === 'string' + ? filters + : Object.keys(filters) + .filter((fieldname) => !isDateRange(filters[fieldname])) + .filter( + (fieldname) => + filters[fieldname] && + JSON.stringify(filters[fieldname]) !== '{}' + ) + .map( + (fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})` + ) + .join(' AND ') + const queryRange = Object.entries(filters) + .filter(([, value]) => isDateRange(value)) + .map(([searchField, dateRange]) => { + return { + searchField, + dateRange, + } as { + searchField: string + dateRange: DateRange + } + })[0] + const queryParts = [ + queryString && { + query_string: { + query: queryString, + }, + }, + queryRange && + queryRange.dateRange && { + range: { + [queryRange.searchField]: { + ...(queryRange.dateRange.start && { + gte: formatDate(queryRange.dateRange.start), + }), + ...(queryRange.dateRange.end && { + lte: formatDate(queryRange.dateRange.end), + }), + format: 'yyyy-MM-dd', + }, + }, + }, + ].filter(Boolean) + return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined } private buildPayloadQuery( @@ -266,13 +312,9 @@ export class ElasticsearchService { }, }) } - const queryFilters = this.filtersToQueryString(fieldSearchFilters) + const queryFilters = this.filtersToQuery(fieldSearchFilters) if (queryFilters) { - filter.push({ - query_string: { - query: queryFilters, - }, - }) + filter.push(...queryFilters) } if (uuids) { filter.push({ @@ -480,14 +522,7 @@ export class ElasticsearchService { const filter = aggregation.filters[curr] return { ...prev, - [curr]: { - query_string: { - query: - typeof filter === 'string' - ? filter - : this.filtersToQueryString(filter), - }, - }, + [curr]: this.filtersToQuery(filter)[0], } }, {}), } diff --git a/libs/api/repository/src/lib/gn4/elasticsearch/index.ts b/libs/api/repository/src/lib/gn4/elasticsearch/index.ts index 34bbef1fcd..87b40eae5f 100644 --- a/libs/api/repository/src/lib/gn4/elasticsearch/index.ts +++ b/libs/api/repository/src/lib/gn4/elasticsearch/index.ts @@ -1,2 +1,3 @@ export * from './elasticsearch.service' export * from './constant' +export * from './date-range.utils' 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 9160184bf4..25c33153ec 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -28,6 +28,7 @@ import { } from '@angular/common/http/testing' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { PublicationVersionError } from '@geonetwork-ui/common/domain/model/error' +import { TranslateService } from '@ngx-translate/core' class Gn4MetadataMapperMock { readRecords = jest.fn((records) => @@ -97,6 +98,17 @@ class PlatformServiceInterfaceMock { getApiVersion = jest.fn(() => of('4.2.5')) } +const SAMPLE_RECORD = { + ...datasetRecordsFixture()[0], + extras: { + ownerInfo: 'Owner|SomeDetails', + }, +} + +const translateServiceMock = { + currentLang: 'fr', +} + describe('Gn4Repository', () => { let repository: Gn4Repository let gn4Helper: ElasticsearchService @@ -130,6 +142,10 @@ describe('Gn4Repository', () => { provide: PlatformServiceInterface, useClass: PlatformServiceInterfaceMock, }, + { + provide: TranslateService, + useValue: translateServiceMock, + }, ], }) repository = TestBed.inject(Gn4Repository) @@ -749,4 +765,80 @@ describe('Gn4Repository', () => { expect(repository.isRecordNotYetSaved('1234-5678')).toBe(false) }) }) + describe('hasRecordChangedSinceDraft', () => { + it('should return an empty array if the record is unsaved', () => { + // Mock dependencies + repository.isRecordNotYetSaved = jest.fn().mockReturnValue(true) + repository.recordHasDraft = jest.fn().mockReturnValue(true) + + repository + .hasRecordChangedSinceDraft(SAMPLE_RECORD) + .subscribe((result) => { + expect(result).toEqual([]) + }) + }) + + it('should return an empty array if there is no draft', () => { + // Mock dependencies + repository.isRecordNotYetSaved = jest.fn().mockReturnValue(false) + repository.recordHasDraft = jest.fn().mockReturnValue(false) + + repository + .hasRecordChangedSinceDraft(SAMPLE_RECORD) + .subscribe((result) => { + expect(result).toEqual([]) + }) + }) + + it('should return updated date and owner info if the recent record is newer than the draft', () => { + const mockDrafts = [ + { + uniqueIdentifier: 'my-dataset-001', + recordUpdated: new Date('2023-01-01'), + }, + ] + const mockRecentRecord = { + uniqueIdentifier: 'my-dataset-001', + recordUpdated: new Date('2024-01-01'), + extras: { ownerInfo: 'Owner|SomeDetails' }, + } + + // Mock dependencies + repository.isRecordNotYetSaved = jest.fn().mockReturnValue(false) + repository.recordHasDraft = jest.fn().mockReturnValue(true) + repository.getAllDrafts = jest.fn().mockReturnValue(of(mockDrafts)) + repository.getRecord = jest.fn().mockReturnValue(of(mockRecentRecord)) + + repository + .hasRecordChangedSinceDraft(SAMPLE_RECORD) + .subscribe((result) => { + expect(result).toEqual([expect.any(String), 'Owner']) + }) + }) + + it('should return an empty array if the draft is more recent than the recent record', () => { + const mockDrafts = [ + { + uniqueIdentifier: 'my-dataset-001', + recordUpdated: new Date('2024-01-01'), + }, + ] + const mockRecentRecord = { + uniqueIdentifier: 'my-dataset-001', + recordUpdated: new Date('2023-01-01'), + } + + // Mock dependencies + repository.isRecordNotYetSaved = jest.fn().mockReturnValue(false) + repository.recordHasDraft = jest.fn().mockReturnValue(true) + repository.getAllDrafts = jest.fn().mockReturnValue(of(mockDrafts)) + repository.getRecord = jest.fn().mockReturnValue(of(mockRecentRecord)) + + repository + .hasRecordChangedSinceDraft(SAMPLE_RECORD) + .subscribe((result) => { + expect(result).toEqual([]) + }) + }) + }) }) diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 64068b7f02..c26d444852 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -31,6 +31,7 @@ import { import { combineLatest, exhaustMap, + forkJoin, from, Observable, of, @@ -365,6 +366,44 @@ export class Gn4Repository implements RecordsRepositoryInterface { return of(draftCount) } + hasRecordChangedSinceDraft(localRecord: CatalogRecord) { + return of({ + isUnsaved: this.isRecordNotYetSaved(localRecord.uniqueIdentifier), + hasDraft: this.recordHasDraft(localRecord.uniqueIdentifier), + }).pipe( + switchMap(({ isUnsaved, hasDraft }) => { + if (isUnsaved || !hasDraft) { + return of({ user: undefined, date: undefined }) + } + return forkJoin([ + this.getAllDrafts().pipe( + map((drafts) => { + const matchingRecord = drafts.find( + (draft) => + draft.uniqueIdentifier === localRecord.uniqueIdentifier + ) + return matchingRecord?.recordUpdated || null + }) + ), + this.getRecord(localRecord.uniqueIdentifier), + ]).pipe( + map(([draftRecordUpdated, recentRecord]) => { + if (recentRecord?.recordUpdated > draftRecordUpdated) { + const user = recentRecord.extras?.['ownerInfo'] + ?.toString() + ?.split('|') + return { + user: `${user[2]} ${user[1]}`, + date: recentRecord.recordUpdated, + } + } + return { user: undefined, date: undefined } + }) + ) + }) + ) + } + private getRecordAsXml(uniqueIdentifier: string): Observable { return this.gn4RecordsApi .getRecordAs( 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 f7abc8e0a6..6bfe311758 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -104,6 +104,7 @@ export interface BaseRecord { updateFrequency?: UpdateFrequency // information related to the resource (dataset, service) + resourceIdentifier?: string contactsForResource: Array resourceCreated?: Date resourcePublished?: Date diff --git a/libs/common/domain/src/lib/model/search/filter.model.ts b/libs/common/domain/src/lib/model/search/filter.model.ts index ddb4b683c5..b0043bbbb5 100644 --- a/libs/common/domain/src/lib/model/search/filter.model.ts +++ b/libs/common/domain/src/lib/model/search/filter.model.ts @@ -2,5 +2,21 @@ import { FieldName } from './field.model' export type FieldFilterByValues = Record export type FieldFilterByExpression = string | number -export type FieldFilter = FieldFilterByExpression | FieldFilterByValues +export type FieldFilterByRange = { + start?: Date + end?: Date +} + +export type FieldFilter = + | FieldFilterByExpression + | FieldFilterByValues + | FieldFilterByRange export type FieldFilters = Record + +export type QueryString = { + query_string: string +} +export type QueryRange = { + range: Record +} +export type FilterQuery = Array 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 b2640d069c..f04d0d5ed7 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -88,4 +88,7 @@ export abstract class RecordsRepositoryInterface { abstract getAllDrafts(): Observable abstract getDraftsCount(): Observable abstract draftsChanged$: Observable + abstract hasRecordChangedSinceDraft( + localRecord: CatalogRecord + ): Observable<{ user: string; date: Date }> } diff --git a/libs/common/fixtures/src/lib/editor/editor.fixtures.ts b/libs/common/fixtures/src/lib/editor/editor.fixtures.ts index e0c619a646..8d8ac394ef 100644 --- a/libs/common/fixtures/src/lib/editor/editor.fixtures.ts +++ b/libs/common/fixtures/src/lib/editor/editor.fixtures.ts @@ -25,6 +25,7 @@ export const editorSectionAboutFixture = () => ({ fields: [ editorFieldTitleFixture(), editorFieldAbstractFixture(), + editorFieldResourceCreatedFixture(), editorFieldResourceUpdatedFixture(), editorFieldRecordUpdatedFixture(), editorFieldUpdateFrequencyFixture(), @@ -71,6 +72,14 @@ export const editorFieldAbstractFixture = () => ({ }, }) +export const editorFieldResourceCreatedFixture = () => ({ + model: 'resourceCreated', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.resourceCreated', + }, +}) + export const editorFieldResourceUpdatedFixture = () => ({ model: 'resourceUpdated', hidden: false, @@ -144,6 +153,7 @@ export const editorFieldsFixture = () => [ editorFieldTitleFixture(), editorFieldAbstractFixture(), editorFieldResourceUpdatedFixture(), + editorFieldResourceCreatedFixture(), editorFieldRecordUpdatedFixture(), editorFieldUpdateFrequencyFixture(), editorFieldTemporalExtentsFixture(), diff --git a/libs/common/fixtures/src/lib/records.fixtures.ts b/libs/common/fixtures/src/lib/records.fixtures.ts index b5eee466aa..f4a59b1d73 100644 --- a/libs/common/fixtures/src/lib/records.fixtures.ts +++ b/libs/common/fixtures/src/lib/records.fixtures.ts @@ -1,6 +1,8 @@ import { CatalogRecord, DatasetRecord, + DatasetSpatialExtent, + Keyword, } from '@geonetwork-ui/common/domain/model/record' export const datasetRecordsFixture: () => CatalogRecord[] = () => [ @@ -464,3 +466,102 @@ export const simpleDatasetRecordAsXmlFixture = ` + +export const NATIONAL_KEYWORD = { + key: 'http://inspire.ec.europa.eu/metadata-codelist/SpatialScope/national', + label: 'National', + description: '', + type: 'theme', +} + +export const SAMPLE_PLACE_KEYWORDS: Keyword[] = [ + // these keywords come from a thesaurus available locally + { + key: 'uri1', + label: 'Berlin', + thesaurus: { + id: '1', + name: 'places', + }, + type: 'place', + bbox: [13.27, 52.63, 52.5, 13.14], + }, + { + key: 'uri2', + label: 'Hamburg', + thesaurus: { + id: '1', + name: 'places', + }, + type: 'place', + bbox: [10.5, 53.66, 53.53, 10], + }, + // this keyword is available locally but has no extent linked to it + { + key: 'uri3', + label: 'Munich', + thesaurus: { + id: '1', + name: 'places', + }, + type: 'place', + bbox: [11.64, 48.65, 48.51, 11.5], + }, + // this keyword comes from a thesaurus not available locally + { + label: 'Europe', + thesaurus: { + id: '2', + name: 'otherPlaces', + }, + type: 'place', + }, + // this keyword has no thesaurus + { + label: 'Narnia', + type: 'place', + }, +] + +// records coming from XML do not have a key or a bbox in them +export const SAMPLE_PLACE_KEYWORDS_FROM_XML = SAMPLE_PLACE_KEYWORDS.map( + ({ label, thesaurus, type }) => ({ + label, + type, + ...(thesaurus && { thesaurus }), + }) +) + +export const SAMPLE_SPATIAL_EXTENTS: DatasetSpatialExtent[] = [ + // these extents are linked to keywords known locally + { + description: 'uri1', + bbox: [13.5, 52.5, 14.5, 53.5], + }, + { + description: 'uri2', + bbox: [10, 53.5, 11, 53.4], + }, + { + description: 'uri4', + bbox: [11.5, 48.5, 11.5, 48.3], + }, + // this extent is linked to a keyword not available locally + { + description: 'URI-Paris', + bbox: [1, 2, 3, 4], + }, + // this extent is not linked to any keyword + { + bbox: [5, 6, 7, 8], + }, +] + +export const SAMPLE_RECORD = { + ...datasetRecordsFixture()[0], + spatialExtents: SAMPLE_SPATIAL_EXTENTS, + keywords: [ + ...datasetRecordsFixture()[0].keywords, + ...SAMPLE_PLACE_KEYWORDS_FROM_XML, + ], +} diff --git a/libs/feature/catalog/src/lib/feature-catalog.module.ts b/libs/feature/catalog/src/lib/feature-catalog.module.ts index 543275fb8d..7dfb323b01 100644 --- a/libs/feature/catalog/src/lib/feature-catalog.module.ts +++ b/libs/feature/catalog/src/lib/feature-catalog.module.ts @@ -1,8 +1,8 @@ import { InjectionToken, NgModule } from '@angular/core' import { SiteTitleComponent } from './site-title/site-title.component' import { + CatalogTitleComponent, OrganisationsFilterComponent, - UiCatalogModule, } from '@geonetwork-ui/ui/catalog' import { GroupsApiService, @@ -11,7 +11,6 @@ import { import { CommonModule } from '@angular/common' import { SourceLabelComponent } from './source-label/source-label.component' import { LangService, UtilI18nModule } from '@geonetwork-ui/util/i18n' -import { OrganisationsComponent } from './organisations/organisations.component' import { UiLayoutModule } from '@geonetwork-ui/ui/layout' import { TranslateModule, TranslateService } from '@ngx-translate/core' import { UiElementsModule } from '@geonetwork-ui/ui/elements' @@ -55,21 +54,17 @@ const organizationsServiceFactory = ( ) @NgModule({ - declarations: [ - SiteTitleComponent, - SourceLabelComponent, - OrganisationsComponent, - ], + declarations: [SiteTitleComponent, SourceLabelComponent], imports: [ - UiCatalogModule, UiLayoutModule, CommonModule, UtilI18nModule, TranslateModule.forChild(), UiElementsModule, OrganisationsFilterComponent, + CatalogTitleComponent, ], - exports: [SiteTitleComponent, SourceLabelComponent, OrganisationsComponent], + exports: [SiteTitleComponent, SourceLabelComponent], providers: [ { provide: OrganizationsServiceInterface, diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.html b/libs/feature/catalog/src/lib/organisations/organisations.component.html index 3d253d16f3..0e6e6c1c2b 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.html +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.html @@ -26,9 +26,5 @@

- +
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 be631b6959..ea9ed82c75 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.spec.ts @@ -1,55 +1,20 @@ import { ChangeDetectionStrategy, - Component, DebugElement, - EventEmitter, - Input, NO_ERRORS_SCHEMA, - Output, } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' -import { ContentGhostComponent } from '@geonetwork-ui/ui/elements' -import { Organization } from '@geonetwork-ui/common/domain/model/record' import { firstValueFrom, of } from 'rxjs' import { someOrganizationsFixture } from '@geonetwork-ui/common/fixtures' import { OrganisationsComponent } from './organisations.component' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' - -@Component({ - selector: 'gn-ui-organisations-filter', - template: '
', -}) -class OrganisationsFilterMockComponent { - @Output() sortBy = new EventEmitter() -} -@Component({ - selector: 'gn-ui-organisation-preview', - template: '
', -}) -class OrganisationPreviewMockComponent { - @Input() organization: Organization - @Output() clickedOrganization = new EventEmitter() -} - -@Component({ - selector: 'gn-ui-organisations-result', - template: '
', -}) -class OrganisationsResultMockComponent { - @Input() hits: number - @Input() total: number -} - -@Component({ - selector: 'gn-ui-pagination', - template: '
', -}) -class PaginationMockComponent { - @Input() currentPage: number - @Input() nPages: number - @Output() newCurrentPageEvent = new EventEmitter() -} +import { MockBuilder } from 'ng-mocks' +import { + OrganisationPreviewComponent, + OrganisationsFilterComponent, + OrganisationsResultComponent, +} from '@geonetwork-ui/ui/catalog' class OrganisationsServiceMock { organisations$ = of(someOrganizationsFixture()) @@ -70,16 +35,10 @@ describe('OrganisationsComponent', () => { let fixture: ComponentFixture let de: DebugElement + beforeEach(() => MockBuilder(OrganisationsComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - OrganisationsComponent, - OrganisationsFilterMockComponent, - OrganisationPreviewMockComponent, - PaginationMockComponent, - OrganisationsResultMockComponent, - ContentGhostComponent, - ], providers: [ { provide: OrganizationsServiceInterface, @@ -105,18 +64,13 @@ describe('OrganisationsComponent', () => { }) describe('on component init', () => { - let orgPreviewComponents: OrganisationPreviewMockComponent[] - let orgResultComponent: OrganisationsResultMockComponent - let paginationComponentDE: DebugElement - let setCurrentPageSpy + let orgPreviewComponents: OrganisationPreviewComponent[] + let orgResultComponent: OrganisationsResultComponent let setSortBySpy - beforeEach(() => { - paginationComponentDE = de.query(By.directive(PaginationMockComponent)) - }) describe('pass organisations to ui preview components', () => { beforeEach(() => { orgPreviewComponents = de - .queryAll(By.directive(OrganisationPreviewMockComponent)) + .queryAll(By.directive(OrganisationPreviewComponent)) .map((debugElement) => debugElement.componentInstance) }) it('should pass first organisation (sorted by name-asc) to first ui preview component', () => { @@ -128,30 +82,26 @@ describe('OrganisationsComponent', () => { }) describe('pass params to ui pagination component', () => { it('should init ui pagination component with currentPage = 1', () => { - expect(paginationComponentDE.componentInstance.currentPage).toEqual(1) + expect(component.currentPage).toEqual(1) }) it('should init ui pagination component with correct value for total nPages', () => { - expect(paginationComponentDE.componentInstance.nPages).toEqual( + expect(component.pagesCount).toEqual( Math.ceil(someOrganizationsFixture().length / ITEMS_ON_PAGE) ) }) - describe('navigate to second page (and trigger newCurrentPageEvent output)', () => { + describe('navigate to second page', () => { beforeEach(() => { - setCurrentPageSpy = jest.spyOn(component, 'setCurrentPage') - paginationComponentDE.triggerEventHandler('newCurrentPageEvent', 2) + component.goToPage(2) fixture.detectChanges() orgPreviewComponents = de - .queryAll(By.directive(OrganisationPreviewMockComponent)) + .queryAll(By.directive(OrganisationPreviewComponent)) .map((debugElement) => debugElement.componentInstance) }) afterEach(() => { jest.restoreAllMocks() }) - it('should call setcurrentPage() with correct value', () => { - expect(setCurrentPageSpy).toHaveBeenCalledWith(2) - }) it('should set currentPage in ui component to correct value', () => { - expect(paginationComponentDE.componentInstance.currentPage).toEqual(2) + expect(component.currentPage).toEqual(2) }) it('should pass first organisation of second page (sorted by name-asc) to first ui preview component', () => { expect(orgPreviewComponents[0].organization.name).toEqual( @@ -167,12 +117,12 @@ describe('OrganisationsComponent', () => { it('should not change currentPage when sorting results', () => { component['setSortBy'](['desc', 'recordCount']) fixture.detectChanges() - expect(paginationComponentDE.componentInstance.currentPage).toEqual(2) + expect(component.currentPage).toEqual(2) }) it('should set currentPage to 1 when filtering to display results', () => { component['setFilterBy']('Data') fixture.detectChanges() - expect(paginationComponentDE.componentInstance.currentPage).toEqual(1) + expect(component.currentPage).toEqual(1) }) }) }) @@ -180,11 +130,11 @@ describe('OrganisationsComponent', () => { beforeEach(() => { setSortBySpy = jest.spyOn(component, 'setSortBy') de.query( - By.directive(OrganisationsFilterMockComponent) + By.directive(OrganisationsFilterComponent) ).triggerEventHandler('sortBy', ['desc', 'recordCount']) fixture.detectChanges() orgPreviewComponents = de - .queryAll(By.directive(OrganisationPreviewMockComponent)) + .queryAll(By.directive(OrganisationPreviewComponent)) .map((debugElement) => debugElement.componentInstance) }) it('should call setSortBy', () => { @@ -209,7 +159,7 @@ describe('OrganisationsComponent', () => { describe('initial state', () => { beforeEach(() => { orgResultComponent = de.query( - By.directive(OrganisationsResultMockComponent) + By.directive(OrganisationsResultComponent) ).componentInstance }) it('should display number of organisations found to equal all', () => { @@ -226,7 +176,7 @@ describe('OrganisationsComponent', () => { describe('entering search terms', () => { beforeEach(() => { orgResultComponent = de.query( - By.directive(OrganisationsResultMockComponent) + By.directive(OrganisationsResultComponent) ).componentInstance }) it('should ignore case and display 11 matches for "Data", "DATA" or "data"', () => { @@ -247,7 +197,7 @@ describe('OrganisationsComponent', () => { orgSelected = [] component.orgSelect.subscribe((org) => orgSelected.push(org)) de.query( - By.directive(OrganisationPreviewMockComponent) + By.directive(OrganisationPreviewComponent) ).triggerEventHandler('clickedOrganisation', organisationMock) fixture.detectChanges() }) diff --git a/libs/feature/catalog/src/lib/organisations/organisations.component.ts b/libs/feature/catalog/src/lib/organisations/organisations.component.ts index 84194910c4..e7517f6f1e 100644 --- a/libs/feature/catalog/src/lib/organisations/organisations.component.ts +++ b/libs/feature/catalog/src/lib/organisations/organisations.component.ts @@ -14,14 +14,31 @@ import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/orga 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' +import { ContentGhostComponent } from '@geonetwork-ui/ui/elements' +import { CommonModule } from '@angular/common' +import { + OrganisationPreviewComponent, + OrganisationsFilterComponent, + OrganisationsResultComponent, +} from '@geonetwork-ui/ui/catalog' +import { Paginable, PaginationComponent } from '@geonetwork-ui/ui/layout' @Component({ selector: 'gn-ui-organisations', templateUrl: './organisations.component.html', styleUrls: ['./organisations.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + ContentGhostComponent, + OrganisationsFilterComponent, + OrganisationsResultComponent, + OrganisationPreviewComponent, + PaginationComponent, + ], }) -export class OrganisationsComponent { +export class OrganisationsComponent implements Paginable { @Input() itemsOnPage = 12 @Output() orgSelect = new EventEmitter() @@ -70,10 +87,6 @@ export class OrganisationsComponent { ) ) - protected setCurrentPage(page: number): void { - this.currentPage$.next(page) - } - protected setFilterBy(value: string): void { this.currentPage$.next(1) this.filterBy$.next(value) @@ -121,4 +134,27 @@ export class OrganisationsComponent { if (!this.urlTemplate) return null return this.urlTemplate.replace('${name}', organisation.name) } + + // Paginable API + get isFirstPage() { + return this.currentPage === 1 + } + get isLastPage() { + return this.currentPage === this.totalPages + } + get pagesCount() { + return this.totalPages + } + get currentPage() { + return this.currentPage$.value + } + goToPage(index: number) { + this.currentPage$.next(index) + } + goToNextPage() { + this.goToPage(this.currentPage + 1) + } + goToPrevPage() { + this.goToPage(this.currentPage - 1) + } } diff --git a/libs/feature/dataviz/src/index.ts b/libs/feature/dataviz/src/index.ts index 4999775b7a..8a1a74097c 100644 --- a/libs/feature/dataviz/src/index.ts +++ b/libs/feature/dataviz/src/index.ts @@ -1,4 +1,3 @@ -export * from './lib/feature-dataviz.module' export * from './lib/service/data.service' export * from './lib/chart-view/chart-view.component' export * from './lib/figure/figure-container/figure-container.component' diff --git a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.spec.ts b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.spec.ts index b675b3fe2f..f287c48322 100644 --- a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.spec.ts +++ b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.spec.ts @@ -6,14 +6,7 @@ import { tick, } from '@angular/core/testing' import { ChartViewComponent } from './chart-view.component' -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - NO_ERRORS_SCHEMA, - Output, -} from '@angular/core' +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' import { DataService } from '../service/data.service' import { firstValueFrom, of, throwError } from 'rxjs' @@ -21,28 +14,8 @@ import { By } from '@angular/platform-browser' import { aSetOfLinksFixture } from '@geonetwork-ui/common/fixtures' import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs' import { FetchError } from '@geonetwork-ui/data-fetcher' - -@Component({ - selector: 'gn-ui-chart', - template: '
', -}) -export class MockChartComponent { - @Input() data: object[] - @Input() labelProperty: string - @Input() valueProperty: string - @Input() secondaryValueProperty: string - @Input() type: string -} - -@Component({ - selector: 'gn-ui-dropdown-selector', - template: '
', -}) -export class MockDropdownSelectorComponent { - @Input() selected: any - @Input() choices: unknown[] - @Output() selectValue = new EventEmitter() -} +import { MockBuilder } from 'ng-mocks' +import { ChartComponent } from '@geonetwork-ui/ui/dataviz' const SAMPLE_DATA_ITEMS = [ { type: 'Feature', properties: { id: 1 } }, @@ -109,15 +82,12 @@ describe('ChartViewComponent', () => { let component: ChartViewComponent let fixture: ComponentFixture let dataService: DataService - let chartComponent: MockChartComponent + let chartComponent: ChartComponent + + beforeEach(() => MockBuilder(ChartViewComponent)) beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - ChartViewComponent, - MockDropdownSelectorComponent, - MockChartComponent, - ], imports: [TranslateModule.forRoot()], providers: [ { @@ -143,7 +113,7 @@ describe('ChartViewComponent', () => { component.link = aSetOfLinksFixture().dataCsv() flushMicrotasks() chartComponent = fixture.debugElement.query( - By.directive(MockChartComponent) + By.directive(ChartComponent) ).componentInstance fixture.detectChanges() })) diff --git a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts index c88ed76858..ede90dcb74 100644 --- a/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +++ b/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts @@ -12,7 +12,10 @@ import { FieldAggregation, getJsonDataItemsProxy, } from '@geonetwork-ui/data-fetcher' -import { DropdownChoice } from '@geonetwork-ui/ui/inputs' +import { + DropdownChoice, + DropdownSelectorComponent, +} from '@geonetwork-ui/ui/inputs' import { BehaviorSubject, combineLatest, EMPTY, Observable } from 'rxjs' import { catchError, @@ -26,7 +29,13 @@ import { import { DataService } from '../service/data.service' import { InputChartType } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' -import { TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { CommonModule } from '@angular/common' +import { ChartComponent } from '@geonetwork-ui/ui/dataviz' +import { + LoadingMaskComponent, + PopupAlertComponent, +} from '@geonetwork-ui/ui/widgets' marker('chart.type.bar') marker('chart.type.barHorizontal') @@ -45,6 +54,15 @@ marker('chart.aggregation.count') templateUrl: './chart-view.component.html', styleUrls: ['./chart-view.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + DropdownSelectorComponent, + TranslateModule, + ChartComponent, + LoadingMaskComponent, + PopupAlertComponent, + ], + standalone: true, }) export class ChartViewComponent { @Input() set link(value: DatasetOnlineResource) { diff --git a/libs/feature/dataviz/src/lib/feature-dataviz.module.ts b/libs/feature/dataviz/src/lib/feature-dataviz.module.ts deleted file mode 100644 index 6aa9e14c05..0000000000 --- a/libs/feature/dataviz/src/lib/feature-dataviz.module.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { FeatureMapModule } from '@geonetwork-ui/feature/map' -import { - FeatureDetailComponent, - MapContainerComponent, -} from '@geonetwork-ui/ui/map' -import { GeoTableViewComponent } from './geo-table-view/geo-table-view.component' -import { FigureContainerComponent } from './figure/figure-container/figure-container.component' -import { - ChartComponent, - TableComponent, - UiDatavizModule, -} from '@geonetwork-ui/ui/dataviz' -import { TableViewComponent } from './table-view/table-view.component' -import { ChartViewComponent } from './chart-view/chart-view.component' -import { TranslateModule } from '@ngx-translate/core' -import { PopupAlertComponent, UiWidgetsModule } from '@geonetwork-ui/ui/widgets' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' - -@NgModule({ - imports: [ - CommonModule, - FeatureMapModule, - UiDatavizModule, - TableComponent, - UiWidgetsModule, - TranslateModule, - ChartComponent, - UiInputsModule, - PopupAlertComponent, - FeatureDetailComponent, - MapContainerComponent, - ], - declarations: [ - GeoTableViewComponent, - FigureContainerComponent, - TableViewComponent, - ChartViewComponent, - ], - exports: [ - GeoTableViewComponent, - FigureContainerComponent, - TableViewComponent, - ChartViewComponent, - ], -}) -export class FeatureDatavizModule {} diff --git a/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.spec.ts b/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.spec.ts index 4ba7dd68db..25f26cc60c 100644 --- a/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.spec.ts +++ b/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.spec.ts @@ -1,25 +1,17 @@ -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { someFigureItemFixture, someHabFigureItemFixture, } from '../figure.fixtures' import { FigureContainerComponent } from './figure-container.component' +import { MockBuilder } from 'ng-mocks' +import { FigureService } from '../figure.service' describe('FigureContainerComponent', () => { let component: FigureContainerComponent let fixture: ComponentFixture - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [FigureContainerComponent], - schemas: [NO_ERRORS_SCHEMA], - }) - .overrideComponent(FigureContainerComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() - }) + beforeEach(() => MockBuilder(FigureContainerComponent).keep(FigureService)) beforeEach(() => { fixture = TestBed.createComponent(FigureContainerComponent) @@ -34,14 +26,14 @@ describe('FigureContainerComponent', () => { beforeEach(() => { component.dataset = someFigureItemFixture() component.expression = 'average|age' - component.ngOnChanges(null) + component.ngOnChanges() }) it('computes the average', () => { expect(component.figure).toEqual('26.67') }) it('with correct digits', () => { component.digits = 3 - component.ngOnChanges(null) + component.ngOnChanges() expect(component.figure).toEqual('26.667') }) }) @@ -49,7 +41,7 @@ describe('FigureContainerComponent', () => { beforeEach(() => { component.dataset = someHabFigureItemFixture() component.expression = 'sum|pop' - component.ngOnChanges(null) + component.ngOnChanges() }) it('computes the sum', () => { expect(component.figure).toEqual('159176260999') @@ -59,7 +51,7 @@ describe('FigureContainerComponent', () => { beforeEach(() => { component.dataset = someHabFigureItemFixture() component.expression = 'sumfds--fdfdspop' - component.ngOnChanges(null) + component.ngOnChanges() }) it('returns Nan', () => { expect(component.figure).toEqual('NaN') diff --git a/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.stories.ts b/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.stories.ts index 913ce743d2..08b9cef6ca 100644 --- a/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.stories.ts +++ b/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.stories.ts @@ -13,6 +13,8 @@ import { UiDatavizModule, } from '@geonetwork-ui/ui/dataviz' import { importProvidersFrom } from '@angular/core' +import { TRANSLATE_DEFAULT_CONFIG } from '@geonetwork-ui/util/i18n' +import { TranslateModule } from '@ngx-translate/core' export default { title: 'Dataviz/FigureContainerComponent', @@ -22,7 +24,10 @@ export default { imports: [UiDatavizModule], }), applicationConfig({ - providers: [importProvidersFrom(BrowserAnimationsModule)], + providers: [ + importProvidersFrom(BrowserAnimationsModule), + importProvidersFrom(TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG)), + ], }), componentWrapperDecorator( (story) => ` diff --git a/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.ts b/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.ts index 33f024df0a..078b4f9154 100644 --- a/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.ts +++ b/libs/feature/dataviz/src/lib/figure/figure-container/figure-container.component.ts @@ -4,7 +4,7 @@ import { Input, OnChanges, } from '@angular/core' -import { TableItemModel } from '@geonetwork-ui/ui/dataviz' +import { TableItemModel, UiDatavizModule } from '@geonetwork-ui/ui/dataviz' import { FigureService } from '../figure.service' @Component({ @@ -12,6 +12,8 @@ import { FigureService } from '../figure.service' templateUrl: './figure-container.component.html', styleUrls: ['./figure-container.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [UiDatavizModule], }) export class FigureContainerComponent implements OnChanges { @Input() dataset: TableItemModel[] diff --git a/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.spec.ts b/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.spec.ts index 46f46b9589..197d04ecb0 100644 --- a/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.spec.ts +++ b/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.spec.ts @@ -1,56 +1,14 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Input, - NO_ERRORS_SCHEMA, - Output, -} from '@angular/core' +import { ChangeDetectorRef } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { pointFeatureCollectionFixture } from '@geonetwork-ui/common/fixtures' import { GeoTableViewComponent } from './geo-table-view.component' -import { MapContext } from '@geospatial-sdk/core' -import { Subject } from 'rxjs' -import type { FeatureCollection } from 'geojson' - -@Component({ - selector: 'gn-ui-map-container', - template: '
', -}) -export class MockMapContainerComponent { - @Input() context: MapContext - @Output() featuresClick = new Subject() - openlayersMap = Promise.resolve({}) -} - -@Component({ - selector: 'gn-ui-table', - template: '
', -}) -export class MockTableComponent { - @Input() data: FeatureCollection - @Input() activeId: string - scrollToItem = jest.fn() -} +import { MockBuilder } from 'ng-mocks' describe('GeoTableViewComponent', () => { let component: GeoTableViewComponent let fixture: ComponentFixture - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ - GeoTableViewComponent, - MockMapContainerComponent, - MockTableComponent, - ], - schemas: [NO_ERRORS_SCHEMA], - }) - .overrideComponent(GeoTableViewComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() - }) + beforeEach(() => MockBuilder(GeoTableViewComponent)) beforeEach(() => { fixture = TestBed.createComponent(GeoTableViewComponent) diff --git a/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts b/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts index 0d47f5fd5c..360df24de4 100644 --- a/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +++ b/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts @@ -15,13 +15,18 @@ import { import type { Feature, FeatureCollection } from 'geojson' import { Subscription } from 'rxjs' import { MapContext } from '@geospatial-sdk/core' -import { MapContainerComponent } from '@geonetwork-ui/ui/map' +import { + FeatureDetailComponent, + MapContainerComponent, +} from '@geonetwork-ui/ui/map' @Component({ selector: 'gn-ui-geo-table-view', templateUrl: './geo-table-view.component.html', styleUrls: ['./geo-table-view.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TableComponent, MapContainerComponent, FeatureDetailComponent], + standalone: true, }) export class GeoTableViewComponent implements OnInit, OnDestroy { @Input() data: FeatureCollection = { type: 'FeatureCollection', features: [] } diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index 186218b08c..e90762d7b6 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -174,7 +174,7 @@ export class DataService { } async getDownloadUrlsFromOgcApi(url: string): Promise { - const endpoint = new OgcApiEndpoint(this.proxy.getProxiedUrl(url)) + const endpoint = new OgcApiEndpoint(url) return await endpoint.allCollections .then((collections) => { return endpoint.getCollectionInfo(collections[0].name) @@ -185,7 +185,7 @@ export class DataService { } async getItemsFromOgcApi(url: string): Promise { - const endpoint = new OgcApiEndpoint(this.proxy.getProxiedUrl(url)) + const endpoint = new OgcApiEndpoint(url) return await endpoint.featureCollections .then((collections) => { return collections.length diff --git a/libs/feature/dataviz/src/lib/table-view/table-view.component.spec.ts b/libs/feature/dataviz/src/lib/table-view/table-view.component.spec.ts index a9672f88ef..708e0aeb9b 100644 --- a/libs/feature/dataviz/src/lib/table-view/table-view.component.spec.ts +++ b/libs/feature/dataviz/src/lib/table-view/table-view.component.spec.ts @@ -8,18 +8,15 @@ import { } from '@angular/core/testing' import { TableViewComponent } from './table-view.component' import { of, throwError } from 'rxjs' -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core' -import { TranslateModule } from '@ngx-translate/core' +import { ChangeDetectionStrategy, importProvidersFrom } from '@angular/core' import { By } from '@angular/platform-browser' import { DataService } from '../service/data.service' import { aSetOfLinksFixture } from '@geonetwork-ui/common/fixtures' import { FetchError } from '@geonetwork-ui/data-fetcher' +import { MockBuilder } from 'ng-mocks' +import { TranslateModule } from '@ngx-translate/core' +import { LoadingMaskComponent } from '@geonetwork-ui/ui/widgets' +import { TableComponent } from '@geonetwork-ui/ui/dataviz' const SAMPLE_DATA_ITEMS = [ { type: 'Feature', properties: { id: 1 } }, @@ -38,50 +35,22 @@ class DataServiceMock { ) } -@Component({ - selector: 'gn-ui-table', - template: '
', -}) -export class MockTableComponent { - @Input() data: [] - @Input() activeId - @Output() selected = new EventEmitter() -} - -@Component({ - selector: 'gn-ui-loading-mask', - template: '
', -}) -export class MockLoadingMaskComponent { - @Input() message -} - -@Component({ - selector: 'gn-ui-popup-alert', - template: '
', -}) -export class MockPopupAlertComponent {} - describe('TableViewComponent', () => { let component: TableViewComponent let fixture: ComponentFixture let dataService: DataService + beforeEach(() => MockBuilder(TableViewComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - TableViewComponent, - MockTableComponent, - MockLoadingMaskComponent, - MockPopupAlertComponent, - ], providers: [ + importProvidersFrom(TranslateModule.forRoot()), { provide: DataService, useClass: DataServiceMock, }, ], - imports: [TranslateModule.forRoot()], }) .overrideComponent(TableViewComponent, { set: { changeDetection: ChangeDetectionStrategy.Default }, @@ -102,7 +71,7 @@ describe('TableViewComponent', () => { }) describe('initial state', () => { - let tableComponent: MockTableComponent + let tableComponent: TableComponent it('loads the data from the first available link', () => { expect(dataService.getDataset).toHaveBeenCalledWith( @@ -119,7 +88,7 @@ describe('TableViewComponent', () => { it('shows a loading indicator', () => { expect( - fixture.debugElement.query(By.directive(MockLoadingMaskComponent)) + fixture.debugElement.query(By.directive(LoadingMaskComponent)) ).toBeTruthy() }) }) @@ -130,7 +99,7 @@ describe('TableViewComponent', () => { fixture.detectChanges() flushMicrotasks() tableComponent = fixture.debugElement.query( - By.directive(MockTableComponent) + By.directive(TableComponent) ).componentInstance fixture.detectChanges() })) diff --git a/libs/feature/dataviz/src/lib/table-view/table-view.component.stories.ts b/libs/feature/dataviz/src/lib/table-view/table-view.component.stories.ts index f77ba78d1e..78feec5feb 100644 --- a/libs/feature/dataviz/src/lib/table-view/table-view.component.stories.ts +++ b/libs/feature/dataviz/src/lib/table-view/table-view.component.stories.ts @@ -11,16 +11,13 @@ import { } from '@storybook/angular' import { TableViewComponent } from './table-view.component' import { TableComponent, UiDatavizModule } from '@geonetwork-ui/ui/dataviz' -import { LoadingMaskComponent } from '@geonetwork-ui/ui/widgets' import { importProvidersFrom } from '@angular/core' -import { MatProgressSpinner } from '@angular/material/progress-spinner' export default { title: 'Smart/Dataviz/TableView', component: TableViewComponent, decorators: [ moduleMetadata({ - declarations: [LoadingMaskComponent, MatProgressSpinner], imports: [ TableComponent, TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), diff --git a/libs/feature/dataviz/src/lib/table-view/table-view.component.ts b/libs/feature/dataviz/src/lib/table-view/table-view.component.ts index 7b89e80cfa..cc483bf561 100644 --- a/libs/feature/dataviz/src/lib/table-view/table-view.component.ts +++ b/libs/feature/dataviz/src/lib/table-view/table-view.component.ts @@ -10,15 +10,28 @@ import { } from 'rxjs/operators' import { DataItem, FetchError } from '@geonetwork-ui/data-fetcher' import { DataService } from '../service/data.service' -import { TableItemModel } from '@geonetwork-ui/ui/dataviz' +import { TableComponent, TableItemModel } from '@geonetwork-ui/ui/dataviz' import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' -import { TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { + LoadingMaskComponent, + PopupAlertComponent, +} from '@geonetwork-ui/ui/widgets' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-table-view', templateUrl: './table-view.component.html', styleUrls: ['./table-view.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + TableComponent, + LoadingMaskComponent, + PopupAlertComponent, + TranslateModule, + ], + standalone: true, }) export class TableViewComponent { @Input() set link(value: DatasetOnlineResource) { diff --git a/libs/feature/editor/src/lib/+state/editor.actions.ts b/libs/feature/editor/src/lib/+state/editor.actions.ts index 85e02ce54e..4130e74d5c 100644 --- a/libs/feature/editor/src/lib/+state/editor.actions.ts +++ b/libs/feature/editor/src/lib/+state/editor.actions.ts @@ -41,3 +41,13 @@ export const setFieldVisibility = createAction( '[Editor] Set field visibility', props<{ field: EditorFieldIdentification; visible: boolean }>() ) + +export const hasRecordChangedSinceDraft = createAction( + '[Editor] Has Record Changed Since Draft', + props<{ record: CatalogRecord }>() +) + +export const hasRecordChangedSinceDraftSuccess = createAction( + '[Editor] Has Record Changed Since Draft Success', + props<{ changes: { user: string; date: Date } }>() +) 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 5ee8316314..124cf4cf26 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.spec.ts @@ -16,6 +16,7 @@ import { Gn4PlatformService } from '@geonetwork-ui/api/repository' class EditorServiceMock { saveRecord = jest.fn((record) => of([record, 'blabla'])) saveRecordAsDraft = jest.fn(() => of('blabla')) + hasRecordChangedSinceDraft = jest.fn((record) => of(['change1', 'change2'])) } class RecordsRepositoryMock { recordHasDraft = jest.fn(() => true) @@ -205,4 +206,19 @@ describe('EditorEffects', () => { }) }) }) + describe('hasRecordChangedSinceDraft$', () => { + it('dispatches hasRecordChangedSinceDraftSuccess on success', () => { + const record = datasetRecordsFixture()[0] + actions = hot('-a-|', { + a: EditorActions.hasRecordChangedSinceDraft({ record }), + }) + const expected = hot('-a-|', { + a: EditorActions.hasRecordChangedSinceDraftSuccess({ + changes: ['change1', 'change2'], + }), + }) + expect(effects.hasRecordChangedSinceDraft$).toBeObservable(expected) + expect(service.hasRecordChangedSinceDraft).toHaveBeenCalledWith(record) + }) + }) }) diff --git a/libs/feature/editor/src/lib/+state/editor.effects.ts b/libs/feature/editor/src/lib/+state/editor.effects.ts index cb678bf6df..4513b2e013 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.ts @@ -126,4 +126,19 @@ export class EditorEffects { map(() => EditorActions.markRecordAsChanged()) ) ) + + hasRecordChangedSinceDraft$ = createEffect(() => + this.actions$.pipe( + ofType(EditorActions.hasRecordChangedSinceDraft), + switchMap(({ record }) => + this.editorService + .hasRecordChangedSinceDraft(record) + .pipe( + map((changes) => + EditorActions.hasRecordChangedSinceDraftSuccess({ changes }) + ) + ) + ) + ) + ) } diff --git a/libs/feature/editor/src/lib/+state/editor.facade.spec.ts b/libs/feature/editor/src/lib/+state/editor.facade.spec.ts index 663cc1b13b..e4e355056d 100644 --- a/libs/feature/editor/src/lib/+state/editor.facade.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.facade.spec.ts @@ -78,5 +78,12 @@ describe('EditorFacade', () => { }) expect(spy).toHaveBeenCalledWith(action) }) + it('checkHasRecordChanged() should dispatch hasRecordChangedSinceDraft action', () => { + const spy = jest.spyOn(store, 'dispatch') + const record = datasetRecordsFixture()[0] + facade.checkHasRecordChanged(record) + const action = EditorActions.hasRecordChangedSinceDraft({ record }) + expect(spy).toHaveBeenCalledWith(action) + }) }) }) diff --git a/libs/feature/editor/src/lib/+state/editor.facade.ts b/libs/feature/editor/src/lib/+state/editor.facade.ts index 1b3f6dfb69..2c6e0400ee 100644 --- a/libs/feature/editor/src/lib/+state/editor.facade.ts +++ b/libs/feature/editor/src/lib/+state/editor.facade.ts @@ -32,6 +32,9 @@ export class EditorFacade { draftSaveSuccess$ = this.actions$.pipe(ofType(EditorActions.draftSaveSuccess)) currentPage$ = this.store.pipe(select(EditorSelectors.selectCurrentPage)) editorConfig$ = this.store.pipe(select(EditorSelectors.selectEditorConfig)) + hasRecordChanged$ = this.store.pipe( + select(EditorSelectors.selectHasRecordChanged) + ) openRecord( record: CatalogRecord, @@ -63,4 +66,8 @@ export class EditorFacade { setFieldVisibility(field: EditorFieldIdentification, visible: boolean) { this.store.dispatch(EditorActions.setFieldVisibility({ field, visible })) } + + checkHasRecordChanged(record: CatalogRecord) { + this.store.dispatch(EditorActions.hasRecordChangedSinceDraft({ record })) + } } 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 21585d40fc..af57bd953a 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.spec.ts @@ -113,6 +113,18 @@ describe('Editor Reducer', () => { expect(result.changedSinceSave).toBe(true) }) + it('hasRecordChangedSinceDraftSuccess action', () => { + const changes = ['change1', 'change2'] + const action = EditorActions.hasRecordChangedSinceDraftSuccess({ + changes, + }) + const result: EditorState = editorReducer( + { ...initialEditorState, hasRecordChanged: [] }, + action + ) + + expect(result.hasRecordChanged).toEqual(changes) + }) }) describe('unknown action', () => { diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts index 29848bb9cd..996d6f086a 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts @@ -24,6 +24,7 @@ export interface EditorState { changedSinceSave: boolean editorConfig: EditorConfig currentPage: number + hasRecordChanged: { user: string; date: Date } } export interface EditorPartialState { @@ -39,6 +40,7 @@ export const initialEditorState: EditorState = { changedSinceSave: false, editorConfig: DEFAULT_CONFIGURATION, currentPage: 0, + hasRecordChanged: null, } const reducer = createReducer( @@ -104,6 +106,10 @@ const reducer = createReducer( })), })), }, + })), + on(EditorActions.hasRecordChangedSinceDraftSuccess, (state, { changes }) => ({ + ...state, + hasRecordChanged: changes, })) ) 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 b82f8723f2..c17eca258e 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts @@ -21,6 +21,7 @@ describe('Editor Selectors', () => { saveError: 'something went wrong', saving: false, changedSinceSave: true, + hasRecordChanged: ['date', 'user'], }, } }) @@ -61,6 +62,11 @@ describe('Editor Selectors', () => { expect(result).toEqual(DEFAULT_CONFIGURATION) }) + it('selectHasRecordChanged() should return the current "hasRecordChanged" state', () => { + const result = EditorSelectors.selectHasRecordChanged(state) + expect(result).toEqual(['date', 'user']) + }) + describe('selectRecordFields', () => { it('should return the config and value for specified page', () => { const recordSections = EditorSelectors.selectRecordSections(state) diff --git a/libs/feature/editor/src/lib/+state/editor.selectors.ts b/libs/feature/editor/src/lib/+state/editor.selectors.ts index 886daa909a..528f5683b6 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.ts @@ -61,3 +61,8 @@ export const selectRecordSections = createSelector( })) as EditorSectionWithValues[] } ) + +export const selectHasRecordChanged = createSelector( + selectEditorState, + (state: EditorState) => state.hasRecordChanged +) diff --git a/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.html b/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.html index b7bf998906..260a1418b8 100644 --- a/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.html +++ b/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.html @@ -4,9 +4,10 @@ [displayWithFn]="displayWithFn" [action]="autoCompleteAction" (itemSelected)="handleItemSelection($event)" - [preventCompleteOnSelection]="true" - [minCharacterCount]="0" + [preventCompleteOnSelection]="false" + [minCharacterCount]="1" [allowSubmit]="false" + [clearOnSelection]="true" >
{ title: 'editor.record.importFromExternalFile.failure.title', text: `editor.record.importFromExternalFile.failure.body `, }), - 2500 + 2500, + mockError ) expect(component.isRecordImportInProgress).toBe(false) diff --git a/libs/feature/editor/src/lib/components/import-record/import-record.component.ts b/libs/feature/editor/src/lib/components/import-record/import-record.component.ts index 98f1193b7c..02e470ebb0 100644 --- a/libs/feature/editor/src/lib/components/import-record/import-record.component.ts +++ b/libs/feature/editor/src/lib/components/import-record/import-record.component.ts @@ -63,13 +63,13 @@ export class ImportRecordComponent { @Output() closeImportMenu = new EventEmitter() importMenuItems: ImportMenuItems[] = [ - { - label: this.translateService.instant('dashboard.importRecord.useModel'), - icon: 'iconoirLightBulbOn', - action: () => null, - dataTest: 'useAModelButton', - disabled: true, - }, + // { + // label: this.translateService.instant('dashboard.importRecord.useModel'), + // icon: 'iconoirLightBulbOn', + // action: () => null, + // dataTest: 'useAModelButton', + // disabled: true, + // }, { label: this.translateService.instant( 'dashboard.importRecord.importExternal' @@ -142,7 +142,8 @@ export class ImportRecordComponent { 'editor.record.importFromExternalFile.failure.body' )} ${error.message ?? ''}`, }, - 2500 + 2500, + error ) this.isRecordImportInProgress = false this.cdr.markForCheck() diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html index 4ee656c480..d7e845ff71 100644 --- a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html +++ b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html @@ -16,6 +16,7 @@

aria-labelledby="example-radio-group-label" class="flex flex-row gap-[8px]" [(ngModel)]="service.accessServiceProtocol" + [disabled]="disabled" > diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts index f75af603c8..9b4f6a187a 100644 --- a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts +++ b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts @@ -34,6 +34,7 @@ import { TranslateModule } from '@ngx-translate/core' export class OnlineServiceResourceInputComponent implements OnChanges { @Input() service: Omit @Input() protocolHint?: string + @Input() disabled? = false selectedProtocol: ServiceProtocol diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html index 822ed4b9fb..11801f5ae8 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html @@ -32,7 +32,7 @@
editor.record.form.field.contacts.noContact diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.css similarity index 100% rename from libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.css rename to libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.css diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.html similarity index 100% rename from libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.html rename to libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.html diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.spec.ts similarity index 60% rename from libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.spec.ts rename to libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.spec.ts index 0f61d940d0..fab46ef54a 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.spec.ts @@ -1,17 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { FormControl } from '@angular/forms' -import { FormFieldDateUpdatedComponent } from './form-field-date-updated.component' +import { FormFieldDateComponent } from './form-field-date.component' describe('FormFieldResourceUpdatedComponent', () => { - let component: FormFieldDateUpdatedComponent - let fixture: ComponentFixture + let component: FormFieldDateComponent + let fixture: ComponentFixture beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FormFieldDateUpdatedComponent], + imports: [FormFieldDateComponent], }).compileComponents() - fixture = TestBed.createComponent(FormFieldDateUpdatedComponent) + fixture = TestBed.createComponent(FormFieldDateComponent) component = fixture.componentInstance const control = new FormControl() control.setValue(new Date()) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.ts similarity index 64% rename from libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.ts rename to libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.ts index 9ad67ad7d7..eb974f8822 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date-updated/form-field-date-updated.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-date/form-field-date.component.ts @@ -8,14 +8,14 @@ import { import { DatePickerComponent } from '@geonetwork-ui/ui/inputs' @Component({ - selector: 'gn-ui-form-field-date-updated', - templateUrl: './form-field-date-updated.component.html', - styleUrls: ['./form-field-date-updated.component.css'], + selector: 'gn-ui-form-field-date', + templateUrl: './form-field-date.component.html', + styleUrls: ['./form-field-date.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [DatePickerComponent], }) -export class FormFieldDateUpdatedComponent { +export class FormFieldDateComponent { @Input() value: Date @Output() valueChange: EventEmitter = new EventEmitter() } diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-license/form-field-license.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-license/form-field-license.component.html index 833e5f015e..e7ad7f8d41 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-license/form-field-license.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-license/form-field-license.component.html @@ -5,6 +5,7 @@ [choices]="licenceOptions" [selected]="selectedLicence" (selectValue)="handleLicenceSelection($event)" + [extraBtnClass]="'input-as-button gn-ui-text-input'" >
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html index 7631601886..dd83fc7362 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.html @@ -1,44 +1,58 @@ - -
- - - - - +
+ +
+ + + + + - -
-
-

- editor.record.form.field.onlineResource.edit.title -

- + +
+
+

+ editor.record.form.field.onlineResource.edit.title +

+ +
+
+

+ editor.record.form.field.onlineResource.edit.description +

+ +
+ +
-
-

- editor.record.form.field.onlineResource.edit.description -

- -
- - +
+
+ editor.record.form.field.draft.only.disabled
- +
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.spec.ts index ce65f266c4..b789ddca52 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.spec.ts @@ -13,6 +13,7 @@ import { MatDialog, MatDialogRef } from '@angular/material/dialog' import { OnlineLinkResource } from '@geonetwork-ui/common/domain/model/record' import { ModalDialogComponent } from '@geonetwork-ui/ui/layout' import { ChangeDetectorRef } from '@angular/core' +import { EditorFacade } from '../../../../+state/editor.facade' let uploadSubject: Subject @@ -38,6 +39,10 @@ export class MatDialogMock { })) } +class EditorFacadeMock { + alreadySavedOnce$ = new BehaviorSubject(false) +} + describe('FormFieldOnlineLinkResourcesComponent', () => { let component: FormFieldOnlineLinkResourcesComponent let fixture: ComponentFixture @@ -65,6 +70,7 @@ describe('FormFieldOnlineLinkResourcesComponent', () => { detectChanges: jest.fn(), }), MockProvider(MatDialog, MatDialogMock, 'useClass'), + MockProvider(EditorFacade, EditorFacadeMock, 'useClass'), ], }).compileComponents() @@ -143,12 +149,16 @@ describe('FormFieldOnlineLinkResourcesComponent', () => { expect(component.uploadProgress).toBeUndefined() component.handleFileChange(file) uploadSubject.error(new Error('something went wrong')) - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - closeMessage: 'editor.record.onlineResourceError.closeMessage', - text: 'editor.record.onlineResourceError.body something went wrong', - title: 'editor.record.onlineResourceError.title', - }) + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'error', + closeMessage: 'editor.record.onlineResourceError.closeMessage', + text: 'editor.record.onlineResourceError.body something went wrong', + title: 'editor.record.onlineResourceError.title', + }, + undefined, + expect.any(Error) + ) }) }) describe('handleUploadCancel', () => { diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts index 3000324226..b9f5ad64b4 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-link-resources/form-field-online-link-resources.component.ts @@ -27,9 +27,10 @@ import { import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { TranslateModule, TranslateService } from '@ngx-translate/core' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' -import { Subscription } from 'rxjs' +import { map, Subscription } from 'rxjs' import { MatDialog } from '@angular/material/dialog' import { MAX_UPLOAD_SIZE_MB } from '../../../../fields.config' +import { EditorFacade } from '../../../../+state/editor.facade' @Component({ selector: 'gn-ui-form-field-online-link-resources', @@ -68,12 +69,17 @@ export class FormFieldOnlineLinkResourcesComponent { protected MAX_UPLOAD_SIZE_MB = MAX_UPLOAD_SIZE_MB + disabled$ = this.editorFacade.alreadySavedOnce$.pipe( + map((alreadySavedOnce) => !alreadySavedOnce) + ) + constructor( private notificationsService: NotificationsService, private translateService: TranslateService, private platformService: PlatformServiceInterface, private cd: ChangeDetectorRef, - private dialog: MatDialog + private dialog: MatDialog, + private editorFacade: EditorFacade ) {} handleFileChange(file: File) { @@ -137,18 +143,22 @@ export class FormFieldOnlineLinkResourcesComponent { private handleError(error: Error) { this.uploadProgress = undefined this.cd.detectChanges() - this.notificationsService.showNotification({ - type: 'error', - title: this.translateService.instant( - 'editor.record.onlineResourceError.title' - ), - text: `${this.translateService.instant( - 'editor.record.onlineResourceError.body' - )} ${error.message}`, - closeMessage: this.translateService.instant( - 'editor.record.onlineResourceError.closeMessage' - ), - }) + this.notificationsService.showNotification( + { + type: 'error', + title: this.translateService.instant( + 'editor.record.onlineResourceError.title' + ), + text: `${this.translateService.instant( + 'editor.record.onlineResourceError.body' + )} ${error.message}`, + closeMessage: this.translateService.instant( + 'editor.record.onlineResourceError.closeMessage' + ), + }, + undefined, + error + ) } private openEditDialog(resource: OnlineLinkResource, index: number) { diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html index d8c6a7367b..3d54511b29 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html @@ -1,72 +1,88 @@ - -
- -
- - - -
-
- - - - - - - -
-
-

- editor.record.form.field.onlineResource.edit.title -

- -
-
-

- editor.record.form.field.onlineResource.edit.description -

- -
- - - - +
+ +
+ +
+
- +
+ + + + + + + +
+
+

+ editor.record.form.field.onlineResource.edit.title +

+ +
+
+

+ editor.record.form.field.onlineResource.edit.description +

+ +
+ + + + + + +
+
+
+ editor.record.form.field.draft.only.disabled +
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts index 307a0d5a62..46d35577d5 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.spec.ts @@ -2,10 +2,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { TranslateModule } from '@ngx-translate/core' import { FormFieldOnlineResourcesComponent } from './form-field-online-resources.component' import { MockBuilder, MockProvider } from 'ng-mocks' -import { Subject } from 'rxjs' +import { BehaviorSubject, Subject } from 'rxjs' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { MatDialog, MatDialogRef } from '@angular/material/dialog' +import { EditorFacade } from '../../../../+state/editor.facade' let uploadSubject: Subject class PlatformServiceInterfaceMock { @@ -21,6 +22,9 @@ export class MatDialogMock { afterClosed: () => this._subject, })) } +class EditorFacadeMock { + alreadySavedOnce$ = new BehaviorSubject(false) +} describe('FormFieldOnlineResourcesComponent', () => { let component: FormFieldOnlineResourcesComponent @@ -42,6 +46,7 @@ describe('FormFieldOnlineResourcesComponent', () => { MockProvider(NotificationsService), MockProvider(MatDialogRef), MockProvider(MatDialog, MatDialogMock, 'useClass'), + MockProvider(EditorFacade, EditorFacadeMock, 'useClass'), ], }).compileComponents() diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts index 6079095018..d5ebd80cfd 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts @@ -32,10 +32,11 @@ import { SortableListComponent, } from '@geonetwork-ui/ui/layout' import { TranslateModule, TranslateService } from '@ngx-translate/core' -import { Subscription } from 'rxjs' +import { map, Subscription } from 'rxjs' import { MAX_UPLOAD_SIZE_MB } from '../../../../fields.config' import { OnlineResourceCardComponent } from '../../../online-resource-card/online-resource-card.component' import { OnlineServiceResourceInputComponent } from '../../../online-service-resource-input/online-service-resource-input.component' +import { EditorFacade } from '../../../../+state/editor.facade' type OnlineNotLinkResource = | DatasetDownloadDistribution @@ -100,12 +101,17 @@ export class FormFieldOnlineResourcesComponent { protected MAX_UPLOAD_SIZE_MB = MAX_UPLOAD_SIZE_MB + disabled$ = this.editorFacade.alreadySavedOnce$.pipe( + map((alreadySavedOnce) => !alreadySavedOnce) + ) + constructor( private notificationsService: NotificationsService, private translateService: TranslateService, private platformService: PlatformServiceInterface, private cd: ChangeDetectorRef, - private dialog: MatDialog + private dialog: MatDialog, + private editorFacade: EditorFacade ) {} onSelectedTypeChange(selectedType: unknown) { @@ -192,18 +198,22 @@ export class FormFieldOnlineResourcesComponent { private handleError(error: Error) { this.uploadProgress = undefined - this.notificationsService.showNotification({ - type: 'error', - title: this.translateService.instant( - 'editor.record.onlineResourceError.title' - ), - text: `${this.translateService.instant( - 'editor.record.onlineResourceError.body' - )} ${error.message}`, - closeMessage: this.translateService.instant( - 'editor.record.onlineResourceError.closeMessage' - ), - }) + this.notificationsService.showNotification( + { + type: 'error', + title: this.translateService.instant( + 'editor.record.onlineResourceError.title' + ), + text: `${this.translateService.instant( + 'editor.record.onlineResourceError.body' + )} ${error.message}`, + closeMessage: this.translateService.instant( + 'editor.record.onlineResourceError.closeMessage' + ), + }, + undefined, + error + ) } private openEditDialog(resource: OnlineNotLinkResource, index: number) { diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.html index 0e10c3e88c..6ed3d8e0eb 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.html @@ -1,11 +1,22 @@ - +
+ +
+ editor.record.form.field.draft.only.disabled +
+
diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.spec.ts index a18dab7012..e17615ac4e 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.spec.ts @@ -8,6 +8,7 @@ import { PlatformServiceInterface, RecordAttachment, } from '@geonetwork-ui/common/domain/platform.service.interface' +import { EditorFacade } from '../../../../+state/editor.facade' let uploadSubject: Subject @@ -26,6 +27,10 @@ class PlatformServiceInterfaceMock { getRecordAttachments = jest.fn(() => recordAttachments) } +class EditorFacadeMock { + alreadySavedOnce$ = new BehaviorSubject(false) +} + describe('FormFieldOverviewsComponent', () => { let component: FormFieldOverviewsComponent let fixture: ComponentFixture @@ -46,6 +51,7 @@ describe('FormFieldOverviewsComponent', () => { 'useClass' ), MockProvider(NotificationsService), + MockProvider(EditorFacade, EditorFacadeMock, 'useClass'), ], }).compileComponents() @@ -119,12 +125,16 @@ describe('FormFieldOverviewsComponent', () => { expect(component.uploadProgress).toBeUndefined() component.handleFileChange(file) uploadSubject.error(new Error('something went wrong')) - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - closeMessage: 'editor.record.resourceError.closeMessage', - text: 'editor.record.resourceError.body something went wrong', - title: 'editor.record.resourceError.title', - }) + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'error', + closeMessage: 'editor.record.resourceError.closeMessage', + text: 'editor.record.resourceError.body something went wrong', + title: 'editor.record.resourceError.title', + }, + undefined, + expect.any(Error) + ) }) }) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts index ae56250a87..6aca5c59ca 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts @@ -11,9 +11,10 @@ import { GraphicOverview } from '@geonetwork-ui/common/domain/model/record' import { ImageInputComponent } from '@geonetwork-ui/ui/inputs' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { NotificationsService } from '@geonetwork-ui/feature/notifications' -import { TranslateService } from '@ngx-translate/core' -import { Subscription } from 'rxjs' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { map, Subscription } from 'rxjs' import { MAX_UPLOAD_SIZE_MB } from '../../../../fields.config' +import { EditorFacade } from '../../../../+state/editor.facade' @Component({ selector: 'gn-ui-form-field-overviews', @@ -21,7 +22,7 @@ import { MAX_UPLOAD_SIZE_MB } from '../../../../fields.config' styleUrls: ['./form-field-overviews.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, ImageInputComponent], + imports: [CommonModule, ImageInputComponent, TranslateModule], }) export class FormFieldOverviewsComponent { @Input() metadataUuid: string @@ -29,6 +30,10 @@ export class FormFieldOverviewsComponent { @Output() valueChange: EventEmitter> = new EventEmitter() + disabled$ = this.editorFacade.alreadySavedOnce$.pipe( + map((alreadySavedOnce) => !alreadySavedOnce) + ) + uploadProgress = undefined uploadSubscription: Subscription = null @@ -47,7 +52,8 @@ export class FormFieldOverviewsComponent { private platformService: PlatformServiceInterface, private notificationsService: NotificationsService, private translateService: TranslateService, - private cd: ChangeDetectorRef + private cd: ChangeDetectorRef, + private editorFacade: EditorFacade ) {} handleFileChange(file: File) { @@ -109,15 +115,21 @@ export class FormFieldOverviewsComponent { private handleError = (error: Error) => { this.uploadProgress = undefined this.cd.markForCheck() - this.notificationsService.showNotification({ - type: 'error', - title: this.translateService.instant('editor.record.resourceError.title'), - text: `${this.translateService.instant( - 'editor.record.resourceError.body' - )} ${error.message}`, - closeMessage: this.translateService.instant( - 'editor.record.resourceError.closeMessage' - ), - }) + this.notificationsService.showNotification( + { + type: 'error', + title: this.translateService.instant( + 'editor.record.resourceError.title' + ), + text: `${this.translateService.instant( + 'editor.record.resourceError.body' + )} ${error.message}`, + closeMessage: this.translateService.instant( + 'editor.record.resourceError.closeMessage' + ), + }, + undefined, + error + ) } } diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.html index 587ff5eeb9..c9275759a4 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.html @@ -1,29 +1,8 @@ - diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.spec.ts index bcb0f8b7c7..68d785a46e 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.spec.ts @@ -1,7 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { FormFieldSimpleComponent } from './form-field-simple.component' -import { FormControl } from '@angular/forms' describe('FormFieldSimpleComponent', () => { let component: FormFieldSimpleComponent @@ -14,7 +13,6 @@ describe('FormFieldSimpleComponent', () => { fixture = TestBed.createComponent(FormFieldSimpleComponent) component = fixture.componentInstance - component.control = new FormControl() fixture.detectChanges() }) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.ts index e09b00ef0a..59a5cc0162 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-simple/form-field-simple.component.ts @@ -6,6 +6,7 @@ import { Input, Output, } from '@angular/core' +import { FormsModule } from '@angular/forms' @Component({ selector: 'gn-ui-form-field-simple', @@ -13,35 +14,14 @@ import { styleUrls: ['./form-field-simple.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule], + imports: [CommonModule, FormsModule], }) export class FormFieldSimpleComponent { - @Input() type: 'date' | 'url' | 'text' | 'number' | 'list' | 'toggle' + @Input() type: 'text' | 'number' @Input() readonly = false @Input() invalid = false @Input() placeholder = '' - @Input() options?: { label: string; value: unknown }[] @Input() value: unknown @Output() valueChange: EventEmitter = new EventEmitter() - - get inputType() { - switch (this.type) { - case 'url': - case 'text': - return 'text' - case 'date': - return 'datetime-local' - case 'number': - return 'number' - case 'toggle': - return 'checkbox' - default: - return '' - } - } - - get isSelect() { - return this.type === 'list' - } } diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.html index 7ae739977e..f290b66c9b 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.html @@ -1,9 +1,4 @@
- ({ - label, - type, - ...(thesaurus && { thesaurus }), - }) -) - -const SAMPLE_SPATIAL_EXTENTS: DatasetSpatialExtent[] = [ - // these extents are linked to keywords known locally - { - description: 'uri1', - bbox: [13.5, 52.5, 14.5, 53.5], - }, - { - description: 'uri2', - bbox: [10, 53.5, 11, 53.4], - }, - { - description: 'uri4', - bbox: [11.5, 48.5, 11.5, 48.3], - }, - // this extent is linked to a keyword not available locally - { - description: 'URI-Paris', - bbox: [1, 2, 3, 4], - }, - // this extent is not linked to any keyword - { - bbox: [5, 6, 7, 8], - }, -] - -const SAMPLE_RECORD = { - ...datasetRecordsFixture()[0], - spatialExtents: SAMPLE_SPATIAL_EXTENTS, - keywords: [ - ...datasetRecordsFixture()[0].keywords, - ...SAMPLE_PLACE_KEYWORDS_FROM_XML, - ], -} - class PlatformServiceInterfaceMock { // this simulates a search of a complete keyword with bbox, key... // only thesaurus 1 is known @@ -354,69 +261,6 @@ describe('FormFieldSpatialExtentComponent', () => { }) }) describe('spatial coverage', () => { - describe('switch toggle option is based on the keywords present in the record', () => { - it('should return true if the record has a national keyword', async () => { - const keywords = [ - ...SAMPLE_PLACE_KEYWORDS, - NATIONAL_KEYWORD, - ] as Keyword[] - editorFacade = TestBed.inject(EditorFacade) - editorFacade.record$ = from([ - { ...SAMPLE_RECORD, keywords } as CatalogRecord, - ]) - fixture = TestBed.createComponent(FormFieldSpatialExtentComponent) - component = fixture.componentInstance - fixture.detectChanges() - - const results = await firstValueFrom(component.switchToggleOptions$) - const nationalOption = results.filter( - (result) => result.label === 'National' - )[0] - - expect(nationalOption.checked).toBe(true) - }) - it('should return false if the record does not have a national keyword', async () => { - const keywords2 = [...SAMPLE_PLACE_KEYWORDS] as Keyword[] - editorFacade = TestBed.inject(EditorFacade) - editorFacade.record$ = from([ - { ...SAMPLE_RECORD, keywords: keywords2 } as CatalogRecord, - ]) - fixture = TestBed.createComponent(FormFieldSpatialExtentComponent) - component = fixture.componentInstance - fixture.detectChanges() - - const results = await firstValueFrom(component.switchToggleOptions$) - const nationalOption = results.filter( - (result) => result.label === 'National' - )[0] - - expect(nationalOption.checked).toBe(false) - }) - }) - describe('#onSpatialScopeChange', () => { - it('removes all existing spatial scope keywords and add the selected one', async () => { - const spatialScopes = [{ label: 'National' }, { label: 'Regional' }] - - const allKeywords = await firstValueFrom(component.allKeywords$) - const filteredKeywords = allKeywords.filter((keyword) => { - const spatialScopeLabels = spatialScopes.map((scope) => scope.label) - return !spatialScopeLabels.includes(keyword.label) - }) - - const selectedOption = { - label: 'National', - value: NATIONAL_KEYWORD, - checked: true, - } - await component.onSpatialScopeChange(selectedOption) - - expect(editorFacade.updateRecordField).toHaveBeenCalledWith( - 'keywords', - [...filteredKeywords, NATIONAL_KEYWORD] - ) - }) - }) - describe('#emitChanges', () => { const allKeywords = [ ...datasetRecordsFixture()[0].keywords, diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.ts index 3436410cb4..602d9ad214 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-extent/form-field-spatial-extent.component.ts @@ -6,15 +6,11 @@ import { } from '@geonetwork-ui/common/domain/model/record' import { GenericKeywordsComponent } from '../../../generic-keywords/generic-keywords.component' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' -import { firstValueFrom, map, Observable, shareReplay } from 'rxjs' +import { firstValueFrom, map, shareReplay } from 'rxjs' import { EditorFacade } from '../../../../+state/editor.facade' import { switchMap } from 'rxjs/operators' import { FormFieldMapContainerComponent } from '../form-field-map-container/form-field-map-container.component' import { TranslateService } from '@ngx-translate/core' -import { - SwitchToggleComponent, - SwitchToggleOption, -} from '@geonetwork-ui/ui/inputs' import { SPATIAL_SCOPES } from '../../../../fields.config' // This intermediary type will let us keep track of which keyword is bound to @@ -40,7 +36,6 @@ type KeywordWithExtent = Keyword & { CommonModule, GenericKeywordsComponent, FormFieldMapContainerComponent, - SwitchToggleComponent, ], }) export class FormFieldSpatialExtentComponent { @@ -52,21 +47,6 @@ export class FormFieldSpatialExtentComponent { map((record) => record?.keywords) ) - switchToggleOptions$: Observable = - this.allKeywords$.pipe( - map((keywords) => - SPATIAL_SCOPES.map((scope) => { - const isChecked = keywords.some( - (keyword) => keyword.label === scope.label - ) - return { - label: scope.label, - checked: isChecked, - } - }) - ) - ) - shownKeywords$ = this.editorFacade.record$.pipe( map((record) => record?.keywords.filter((k) => k.type === 'place')), // look for full keywords in the thesauri @@ -198,24 +178,4 @@ export class FormFieldSpatialExtentComponent { this.editorFacade.updateRecordField('keywords', allKeywords) this.editorFacade.updateRecordField('spatialExtents', spatialExtents) } - - async onSpatialScopeChange(selectedOption: SwitchToggleOption) { - // remove all existing spatial scope keywords - const allKeywords = await firstValueFrom(this.allKeywords$) - const filteredKeywords = allKeywords.filter((keyword) => { - const spatialScopeLabels = SPATIAL_SCOPES.map((scope) => scope.label) - return !spatialScopeLabels.includes(keyword.label) - }) - - const selectedOptionLabel = selectedOption.label - const selectedKeyword = SPATIAL_SCOPES.find( - (scopes) => scopes.label === selectedOptionLabel - ) - - // add the selected spatial scope keyword - this.editorFacade.updateRecordField('keywords', [ - ...filteredKeywords, - { ...selectedKeyword }, - ]) - } } diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.css similarity index 100% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.css rename to libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.css diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.html new file mode 100644 index 0000000000..ea2661e2c2 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.html @@ -0,0 +1,5 @@ + diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.spec.ts new file mode 100644 index 0000000000..04068df92a --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.spec.ts @@ -0,0 +1,103 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { FormFieldSpatialToggleComponent } from './form-field-spatial-toggle.component' +import { MockProvider } from 'ng-mocks' +import { EditorFacade } from '../../../../+state/editor.facade' +import { BehaviorSubject, firstValueFrom, from } from 'rxjs' +import { + datasetRecordsFixture, + NATIONAL_KEYWORD, + SAMPLE_PLACE_KEYWORDS, + SAMPLE_RECORD, +} from '@geonetwork-ui/common/fixtures' +import { + CatalogRecord, + Keyword, +} from '@geonetwork-ui/common/domain/model/record' +import { TranslateModule } from '@ngx-translate/core' + +describe('FormFieldSpatialToggleComponent', () => { + let component: FormFieldSpatialToggleComponent + let fixture: ComponentFixture + let editorFacade: EditorFacade + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FormFieldSpatialToggleComponent, TranslateModule.forRoot()], + providers: [ + MockProvider(EditorFacade, { + record$: new BehaviorSubject(SAMPLE_RECORD), + updateRecordField: jest.fn(), + }), + ], + }) + editorFacade = TestBed.inject(EditorFacade) + fixture = TestBed.createComponent(FormFieldSpatialToggleComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('switch toggle option is based on the keywords present in the record', () => { + it('should return true if the record has a national keyword', async () => { + const keywords = [...SAMPLE_PLACE_KEYWORDS, NATIONAL_KEYWORD] as Keyword[] + editorFacade = TestBed.inject(EditorFacade) + editorFacade.record$ = from([ + { ...SAMPLE_RECORD, keywords } as CatalogRecord, + ]) + fixture = TestBed.createComponent(FormFieldSpatialToggleComponent) + component = fixture.componentInstance + fixture.detectChanges() + + const results = await firstValueFrom(component.switchToggleOptions$) + const nationalOption = results.filter( + (result) => result.label === 'National' + )[0] + + expect(nationalOption.checked).toBe(true) + }) + it('should return false if the record does not have a national keyword', async () => { + const keywords2 = [...SAMPLE_PLACE_KEYWORDS] as Keyword[] + editorFacade = TestBed.inject(EditorFacade) + editorFacade.record$ = from([ + { ...SAMPLE_RECORD, keywords: keywords2 } as CatalogRecord, + ]) + fixture = TestBed.createComponent(FormFieldSpatialToggleComponent) + component = fixture.componentInstance + fixture.detectChanges() + + const results = await firstValueFrom(component.switchToggleOptions$) + const nationalOption = results.filter( + (result) => result.label === 'National' + )[0] + + expect(nationalOption.checked).toBe(false) + }) + }) + describe('#onSpatialScopeChange', () => { + it('removes all existing spatial scope keywords and add the selected one', async () => { + const spatialScopes = [{ label: 'National' }, { label: 'Regional' }] + + const allKeywords = await firstValueFrom(component.allKeywords$) + const filteredKeywords = allKeywords.filter((keyword) => { + const spatialScopeLabels = spatialScopes.map((scope) => scope.label) + return !spatialScopeLabels.includes(keyword.label) + }) + + const selectedOption = { + label: 'National', + value: NATIONAL_KEYWORD, + checked: true, + } + await component.onSpatialScopeChange(selectedOption) + + expect(editorFacade.updateRecordField).toHaveBeenCalledWith('keywords', [ + ...filteredKeywords, + NATIONAL_KEYWORD, + ]) + }) + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.ts new file mode 100644 index 0000000000..7687435a67 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-spatial-toggle/form-field-spatial-toggle.component.ts @@ -0,0 +1,60 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { CommonModule } from '@angular/common' +import { + SwitchToggleComponent, + SwitchToggleOption, +} from '@geonetwork-ui/ui/inputs' +import { EditorFacade } from '../../../../+state/editor.facade' +import { firstValueFrom, map, Observable } from 'rxjs' +import { SPATIAL_SCOPES } from '../../../../fields.config' + +@Component({ + selector: 'gn-ui-form-field-spatial-toggle', + standalone: true, + imports: [CommonModule, SwitchToggleComponent], + templateUrl: './form-field-spatial-toggle.component.html', + styleUrls: ['./form-field-spatial-toggle.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FormFieldSpatialToggleComponent { + allKeywords$ = this.editorFacade.record$.pipe( + map((record) => record?.keywords) + ) + + switchToggleOptions$: Observable = + this.allKeywords$.pipe( + map((keywords) => + SPATIAL_SCOPES.map((scope) => { + const isChecked = keywords.some( + (keyword) => keyword.label === scope.label + ) + return { + label: scope.label, + checked: isChecked, + } + }) + ) + ) + + constructor(private editorFacade: EditorFacade) {} + + async onSpatialScopeChange(selectedOption: SwitchToggleOption) { + // remove all existing spatial scope keywords + const allKeywords = await firstValueFrom(this.allKeywords$) + const filteredKeywords = allKeywords.filter((keyword) => { + const spatialScopeLabels = SPATIAL_SCOPES.map((scope) => scope.label) + return !spatialScopeLabels.includes(keyword.label) + }) + + const selectedOptionLabel = selectedOption.label + const selectedKeyword = SPATIAL_SCOPES.find( + (scopes) => scopes.label === selectedOptionLabel + ) + + // add the selected spatial scope keyword + this.editorFacade.updateRecordField('keywords', [ + ...filteredKeywords, + { ...selectedKeyword }, + ]) + } +} diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html index 9d784ff973..d7802bd204 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html @@ -11,6 +11,7 @@ [selected]="selectedFrequency" (selectValue)="onSelectFrequencyValue($event)" [disabled]="!planned" + [extraBtnClass]="'input-as-button gn-ui-text-input'" >
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 60344f8c67..5bb8ed772d 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 @@ -24,20 +24,24 @@
- -
+ class="grow font-title text-3xl font-normal overflow-hidden" + (change)="valueChange.emit($event.target.value)" + >{{ valueAsString }} +
edit + + + + + + - + > + + + 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 03a620834e..c108352ea7 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 @@ -4,7 +4,7 @@ import { TranslateModule } from '@ngx-translate/core' import { MockBuilder } from 'ng-mocks' import { FormFieldLicenseComponent } from './form-field-license/form-field-license.component' import { FormFieldOverviewsComponent } from './form-field-overviews/form-field-overviews.component' -import { FormFieldDateUpdatedComponent } from './form-field-date-updated/form-field-date-updated.component' +import { FormFieldDateComponent } from './form-field-date/form-field-date.component' import { FormFieldRichComponent } from './form-field-rich/form-field-rich.component' import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/form-field-spatial-extent.component' import { FormFieldTemporalExtentsComponent } from './form-field-temporal-extents/form-field-temporal-extents.component' @@ -63,13 +63,26 @@ describe('FormFieldComponent', () => { expect(formField).toBeTruthy() }) }) + describe('resource created field', () => { + let formField + beforeEach(() => { + component.model = 'resourceCreated' + fixture.detectChanges() + formField = fixture.debugElement.query( + By.directive(FormFieldDateComponent) + ).componentInstance + }) + it('creates a resource created form field', () => { + expect(formField).toBeTruthy() + }) + }) describe('resource updated field', () => { let formField beforeEach(() => { component.model = 'resourceUpdated' fixture.detectChanges() formField = fixture.debugElement.query( - By.directive(FormFieldDateUpdatedComponent) + By.directive(FormFieldDateComponent) ).componentInstance }) it('creates a resource updated form field', () => { 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 79167142ed..9d2d1b1311 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 @@ -4,9 +4,12 @@ import { Component, ElementRef, EventEmitter, + Injector, Input, Output, ViewChild, + afterNextRender, + inject, } from '@angular/core' import { MatTooltipModule } from '@angular/material/tooltip' import { @@ -23,7 +26,7 @@ import { EditableLabelDirective } from '@geonetwork-ui/ui/inputs' import { FormFieldWrapperComponent } from '@geonetwork-ui/ui/layout' import { TranslateModule } from '@ngx-translate/core' import { - FormFieldDateUpdatedComponent, + FormFieldDateComponent, FormFieldLicenseComponent, FormFieldTemporalExtentsComponent, } from '.' @@ -48,6 +51,8 @@ import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/for import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component' import { FormFieldConstraintsShortcutsComponent } from './form-field-constraints-shortcuts/form-field-constraints-shortcuts.component' import { FormFieldConstraintsComponent } from './form-field-constraints/form-field-constraints.component' +import { TextFieldModule } from '@angular/cdk/text-field' +import { FormFieldSpatialToggleComponent } from './form-field-spatial-toggle/form-field-spatial-toggle.component' @Component({ selector: 'gn-ui-form-field', @@ -62,7 +67,7 @@ import { FormFieldConstraintsComponent } from './form-field-constraints/form-fie MatTooltipModule, FormFieldWrapperComponent, FormFieldLicenseComponent, - FormFieldDateUpdatedComponent, + FormFieldDateComponent, FormFieldUpdateFrequencyComponent, FormFieldTemporalExtentsComponent, FormFieldSimpleComponent, @@ -80,6 +85,8 @@ import { FormFieldConstraintsComponent } from './form-field-constraints/form-fie FormFieldContactsComponent, FormFieldConstraintsComponent, FormFieldConstraintsShortcutsComponent, + FormFieldSpatialToggleComponent, + TextFieldModule, ], }) export class FormFieldComponent { @@ -101,7 +108,7 @@ export class FormFieldComponent { } focusTitleInput() { - this.titleInput.nativeElement.children[0].focus() + this.titleInput.nativeElement.focus() } get withoutWrapper() { 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 448238f460..83fc0352a2 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,6 +1,6 @@ export * from './form-field-keywords/form-field-keywords.component' export * from './form-field-license/form-field-license.component' -export * from './form-field-date-updated/form-field-date-updated.component' +export * from './form-field-date/form-field-date.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' diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index d64e1eb89f..f5e1b338a8 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -66,6 +66,21 @@ export const RECORD_KEYWORDS_FIELD: EditorField = { }, } +export const RECORD_RESOURCE_CREATED_FIELD: EditorField = { + model: 'resourceCreated', + formFieldConfig: { + labelKey: marker('editor.record.form.field.resourceCreated'), + }, + gridColumnSpan: 1, +} + +export const RESOURCE_IDENTIFIER_FIELD: EditorField = { + model: 'resourceIdentifier', + formFieldConfig: { + labelKey: marker('editor.record.form.field.resourceIdentifier'), + }, +} + export const RECORD_RESOURCE_UPDATED_FIELD: EditorField = { model: 'resourceUpdated', formFieldConfig: { @@ -133,6 +148,12 @@ export const RECORD_GRAPHICAL_OVERVIEW_FIELD: EditorField = { }, } +export const RECORD_SPATIAL_TOGGLE_FIELD: EditorField = { + componentName: 'form-field-spatial-toggle', + formFieldConfig: {}, + hidden: true, +} + export const RECORD_SPATIAL_EXTENTS_FIELD: EditorField = { model: 'spatialExtents', formFieldConfig: { @@ -176,6 +197,8 @@ export const ABOUT_SECTION: EditorSection = { hidden: false, fields: [ RECORD_UNIQUE_IDENTIFIER_FIELD, + RESOURCE_IDENTIFIER_FIELD, + RECORD_RESOURCE_CREATED_FIELD, RECORD_RESOURCE_UPDATED_FIELD, RECORD_UPDATED_FIELD, RECORD_UPDATE_FREQUENCY_FIELD, @@ -186,7 +209,7 @@ export const ABOUT_SECTION: EditorSection = { export const GEOGRAPHICAL_COVERAGE_SECTION: EditorSection = { labelKey: marker('editor.record.form.section.geographicalCoverage.label'), hidden: false, - fields: [RECORD_SPATIAL_EXTENTS_FIELD], + fields: [RECORD_SPATIAL_TOGGLE_FIELD, RECORD_SPATIAL_EXTENTS_FIELD], } export const ASSOCIATED_RESOURCES_SECTION: EditorSection = { diff --git a/libs/feature/editor/src/lib/models/editor-config.model.ts b/libs/feature/editor/src/lib/models/editor-config.model.ts index d4fcc83afe..b675bad2de 100644 --- a/libs/feature/editor/src/lib/models/editor-config.model.ts +++ b/libs/feature/editor/src/lib/models/editor-config.model.ts @@ -23,7 +23,9 @@ export type FieldModelSpecifier = | OnlineLinkResourceSpecifier | DatasetDistributionsSpecifier -export type FormFieldComponentName = 'form-field-constraints-shortcuts' +export type FormFieldComponentName = + | 'form-field-constraints-shortcuts' + | 'form-field-spatial-toggle' export interface EditorFieldIdentification { // name of the target field in the record; will not change the record directly if not defined diff --git a/libs/feature/editor/src/lib/services/editor.service.ts b/libs/feature/editor/src/lib/services/editor.service.ts index 64d58af9bf..9d6027ccaa 100644 --- a/libs/feature/editor/src/lib/services/editor.service.ts +++ b/libs/feature/editor/src/lib/services/editor.service.ts @@ -59,6 +59,7 @@ export class EditorService { record: CatalogRecord, recordSource: string ): Observable { + record.recordUpdated = new Date() return this.recordsRepository .saveRecordAsDraft(record, recordSource) .pipe(map(() => undefined)) @@ -70,4 +71,10 @@ export class EditorService { this.recordsRepository.clearRecordDraft(record.uniqueIdentifier) return this.recordsRepository.openRecordForEdition(record.uniqueIdentifier) } + + hasRecordChangedSinceDraft( + localRecord: CatalogRecord + ): Observable<{ user: string; date: Date }> { + return this.recordsRepository.hasRecordChangedSinceDraft(localRecord) + } } diff --git a/libs/feature/map/src/lib/layers-panel/layers-panel.component.ts b/libs/feature/map/src/lib/layers-panel/layers-panel.component.ts index 61e7dbf068..82774645f2 100644 --- a/libs/feature/map/src/lib/layers-panel/layers-panel.component.ts +++ b/libs/feature/map/src/lib/layers-panel/layers-panel.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { MapFacade } from '../+state/map.facade' import { firstValueFrom, map } from 'rxjs' import { MapContextLayer } from '@geospatial-sdk/core' -import { UiLayoutModule } from '@geonetwork-ui/ui/layout' +import { ExpandablePanelButtonComponent } from '@geonetwork-ui/ui/layout' import { MatTabsModule } from '@angular/material/tabs' import { AddLayerFromOgcApiComponent } from '../add-layer-from-ogc-api/add-layer-from-ogc-api.component' import { AddLayerFromWfsComponent } from '../add-layer-from-wfs/add-layer-from-wfs.component' @@ -29,7 +29,6 @@ import { matChevronRight } from '@ng-icons/material-icons/baseline' changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ - UiLayoutModule, MatTabsModule, AddLayerFromOgcApiComponent, AddLayerFromWfsComponent, @@ -39,6 +38,7 @@ import { matChevronRight } from '@ng-icons/material-icons/baseline' TranslateModule, CommonModule, NgIconComponent, + ExpandablePanelButtonComponent, ], providers: [ provideIcons({ diff --git a/libs/feature/notifications/src/lib/notifications.service.ts b/libs/feature/notifications/src/lib/notifications.service.ts index cbf38e52c2..b7281989ff 100644 --- a/libs/feature/notifications/src/lib/notifications.service.ts +++ b/libs/feature/notifications/src/lib/notifications.service.ts @@ -10,7 +10,12 @@ type NotificationWithIdentity = NotificationContent & { id: number } export class NotificationsService { notifications$ = new BehaviorSubject([]) - showNotification(content: NotificationContent, timeoutMs?: number) { + showNotification( + content: NotificationContent, + timeoutMs?: number, + error?: Error + ) { + error && console.error(error) const id = Math.floor(Math.random() * 1000000) this.notifications$.next([...this.notifications$.value, { ...content, id }]) if (typeof timeoutMs === 'undefined') return diff --git a/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.spec.ts b/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.spec.ts index 381b66ecbf..d90543561d 100644 --- a/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.spec.ts +++ b/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.spec.ts @@ -6,10 +6,10 @@ import { } from './data-view-permalink.component' import { BehaviorSubject, firstValueFrom } from 'rxjs' import { MdViewFacade } from '../state' -import { Component, Input } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' import { GN_UI_VERSION } from '../gn-ui-version.token' import { provideRepositoryUrl } from '@geonetwork-ui/api/repository' +import { MockBuilder } from 'ng-mocks' const chartConfig1 = { aggregation: 'sum', @@ -38,23 +38,15 @@ const baseUrl = 'https://example.com/wc-embedder' const gnUiVersion = 'v1.2.3' -@Component({ - selector: 'gn-ui-copy-text-button', - template: '
', -}) -export class MockCopyTextButtonComponent { - @Input() text: string - @Input() tooltipText: string - @Input() rows: number -} describe('DataViewPermalinkComponent', () => { let component: DataViewPermalinkComponent let fixture: ComponentFixture let facade + beforeEach(() => MockBuilder(DataViewPermalinkComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [DataViewPermalinkComponent, MockCopyTextButtonComponent], imports: [TranslateModule.forRoot()], providers: [ provideRepositoryUrl('http://gn-api.url/'), diff --git a/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.ts b/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.ts index ef55091071..c0036aa95d 100644 --- a/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.ts +++ b/libs/feature/record/src/lib/data-view-permalink/data-view-permalink.component.ts @@ -10,6 +10,9 @@ import { Configuration } from '@geonetwork-ui/data-access/gn4' import { BehaviorSubject, combineLatest, map } from 'rxjs' import { MdViewFacade } from '../state' import { GN_UI_VERSION } from '../gn-ui-version.token' +import { CopyTextButtonComponent } from '@geonetwork-ui/ui/inputs' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' export const WEB_COMPONENT_EMBEDDER_URL = new InjectionToken( 'webComponentEmbedderUrl' @@ -20,6 +23,8 @@ export const WEB_COMPONENT_EMBEDDER_URL = new InjectionToken( templateUrl: './data-view-permalink.component.html', styleUrls: ['./data-view-permalink.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, CopyTextButtonComponent, TranslateModule], }) export class DataViewPermalinkComponent { viewType$ = new BehaviorSubject('map') diff --git a/libs/feature/record/src/lib/data-view-share/data-view-share.component.spec.ts b/libs/feature/record/src/lib/data-view-share/data-view-share.component.spec.ts index cf87abd4f1..0dfac81abe 100644 --- a/libs/feature/record/src/lib/data-view-share/data-view-share.component.spec.ts +++ b/libs/feature/record/src/lib/data-view-share/data-view-share.component.spec.ts @@ -1,36 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { DataViewShareComponent } from './data-view-share.component' -import { Component, NO_ERRORS_SCHEMA } from '@angular/core' -import { WEB_COMPONENT_EMBEDDER_URL } from '../data-view-permalink/data-view-permalink.component' +import { + DataViewPermalinkComponent, + WEB_COMPONENT_EMBEDDER_URL, +} from '../data-view-permalink/data-view-permalink.component' import { By } from '@angular/platform-browser' - -@Component({ - selector: 'gn-ui-data-view-permalink', - template: '
', -}) -export class MockDataViewPermalinkComponent {} - -@Component({ - selector: 'gn-ui-data-view-web-component', - template: '
', -}) -export class MockDataViewWebComponentComponent {} +import { MockBuilder } from 'ng-mocks' +import { DataViewWebComponentComponent } from '../data-view-web-component/data-view-web-component.component' const baseUrl = 'https://example.com/wc-embedder' describe('DataViewShareComponent', () => { let component: DataViewShareComponent let fixture: ComponentFixture - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ - DataViewShareComponent, - MockDataViewPermalinkComponent, - MockDataViewWebComponentComponent, - ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents() - }) + beforeEach(() => MockBuilder(DataViewShareComponent)) describe('if no WEB_COMPONENT_EMBEDDER_URL is defined', () => { beforeEach(() => { @@ -51,14 +34,12 @@ describe('DataViewShareComponent', () => { }) it('does not render a data view permalink component', () => { expect( - fixture.debugElement.query(By.directive(MockDataViewPermalinkComponent)) + fixture.debugElement.query(By.directive(DataViewPermalinkComponent)) ).toBeFalsy() }) it('renders a data view web component component', () => { expect( - fixture.debugElement.query( - By.directive(MockDataViewWebComponentComponent) - ) + fixture.debugElement.query(By.directive(DataViewWebComponentComponent)) ).toBeTruthy() }) }) @@ -78,14 +59,12 @@ describe('DataViewShareComponent', () => { }) it('renders a data view permalink component', () => { expect( - fixture.debugElement.query(By.directive(MockDataViewPermalinkComponent)) + fixture.debugElement.query(By.directive(DataViewPermalinkComponent)) ).toBeTruthy() }) it('renders a data view web component component', () => { expect( - fixture.debugElement.query( - By.directive(MockDataViewWebComponentComponent) - ) + fixture.debugElement.query(By.directive(DataViewWebComponentComponent)) ).toBeTruthy() }) }) diff --git a/libs/feature/record/src/lib/data-view-share/data-view-share.component.ts b/libs/feature/record/src/lib/data-view-share/data-view-share.component.ts index 95085e9e0b..d9cca54f30 100644 --- a/libs/feature/record/src/lib/data-view-share/data-view-share.component.ts +++ b/libs/feature/record/src/lib/data-view-share/data-view-share.component.ts @@ -5,13 +5,28 @@ import { Input, Optional, } from '@angular/core' -import { WEB_COMPONENT_EMBEDDER_URL } from '../data-view-permalink/data-view-permalink.component' +import { + DataViewPermalinkComponent, + WEB_COMPONENT_EMBEDDER_URL, +} from '../data-view-permalink/data-view-permalink.component' +import { MatTabsModule } from '@angular/material/tabs' +import { CommonModule } from '@angular/common' +import { DataViewWebComponentComponent } from '../data-view-web-component/data-view-web-component.component' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-data-view-share', templateUrl: './data-view-share.component.html', styleUrls: ['./data-view-share.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + MatTabsModule, + DataViewPermalinkComponent, + DataViewWebComponentComponent, + TranslateModule, + ], + standalone: true, }) export class DataViewShareComponent { private _viewType: string diff --git a/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.spec.ts b/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.spec.ts index 7521751966..53f7280eb8 100644 --- a/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.spec.ts +++ b/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.spec.ts @@ -2,10 +2,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { DataViewWebComponentComponent } from './data-view-web-component.component' import { BehaviorSubject, firstValueFrom } from 'rxjs' import { MdViewFacade } from '../state' -import { TranslateModule } from '@ngx-translate/core' -import { Component, Input } from '@angular/core' import { GN_UI_VERSION } from '../gn-ui-version.token' import { provideRepositoryUrl } from '@geonetwork-ui/api/repository' +import { MockBuilder } from 'ng-mocks' const chartConfig1 = { aggregation: 'sum', @@ -32,28 +31,15 @@ class MdViewFacadeMock { metadata$ = new BehaviorSubject(metadata) } -@Component({ - selector: 'gn-ui-copy-text-button', - template: '
', -}) -export class MockCopyTextButtonComponent { - @Input() text: string - @Input() tooltipText: string - @Input() rows: number -} - describe('DataViewWebComponentComponent', () => { let component: DataViewWebComponentComponent let fixture: ComponentFixture let facade + beforeEach(() => MockBuilder(DataViewWebComponentComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - DataViewWebComponentComponent, - MockCopyTextButtonComponent, - ], - imports: [TranslateModule.forRoot()], providers: [ provideRepositoryUrl('http://gn-api.url/'), { diff --git a/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.ts b/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.ts index 98a9baa7b5..43d36b6fbc 100644 --- a/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.ts +++ b/libs/feature/record/src/lib/data-view-web-component/data-view-web-component.component.ts @@ -8,12 +8,17 @@ import { Configuration } from '@geonetwork-ui/data-access/gn4' import { MdViewFacade } from '../state' import { BehaviorSubject, combineLatest, map } from 'rxjs' import { GN_UI_VERSION } from '../gn-ui-version.token' +import { CopyTextButtonComponent } from '@geonetwork-ui/ui/inputs' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-data-view-web-component', templateUrl: './data-view-web-component.component.html', styleUrls: ['./data-view-web-component.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, CopyTextButtonComponent, TranslateModule], }) export class DataViewWebComponentComponent { viewType$ = new BehaviorSubject('map') diff --git a/libs/feature/record/src/lib/data-view/data-view.component.spec.ts b/libs/feature/record/src/lib/data-view/data-view.component.spec.ts index 8fcec52332..d66b0d6056 100644 --- a/libs/feature/record/src/lib/data-view/data-view.component.spec.ts +++ b/libs/feature/record/src/lib/data-view/data-view.component.spec.ts @@ -1,4 +1,3 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core' import { ComponentFixture, fakeAsync, @@ -6,16 +5,20 @@ import { TestBed, } from '@angular/core/testing' import { By } from '@angular/platform-browser' -import { BehaviorSubject, Subject } from 'rxjs' +import { Subject } from 'rxjs' import { MdViewFacade } from '../state' import { DataViewComponent } from './data-view.component' import { TranslateModule } from '@ngx-translate/core' -import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' -import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' import { someDataLinksFixture, someGeoDatalinksFixture, } from '@geonetwork-ui/common/fixtures' +import { MockBuilder } from 'ng-mocks' +import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs' +import { + ChartViewComponent, + TableViewComponent, +} from '@geonetwork-ui/feature/dataviz' class MdViewFacadeMock { dataLinks$ = new Subject() @@ -30,49 +33,18 @@ const chartConfigMock = { chartType: 'bar', } -@Component({ - selector: 'gn-ui-table-view', - template: '
', -}) -export class MockTableViewComponent { - @Input() link: DatasetOnlineResource -} - -@Component({ - selector: 'gn-ui-chart-view', - template: '
', -}) -export class MockChartViewComponent { - @Input() link: DatasetOnlineResource - @Output() chartConfig$ = new BehaviorSubject(null) -} - -@Component({ - selector: 'gn-ui-dropdown-selector', - template: '
', -}) -export class MockDropdownSelectorComponent { - @Input() choices: unknown[] - @Input() showTitle - @Output() selectValue = new EventEmitter() -} - describe('DataViewComponent', () => { let component: DataViewComponent let fixture: ComponentFixture let facade - let dropdownComponent: MockDropdownSelectorComponent - let tableViewComponent: MockTableViewComponent - let chartViewComponent: MockChartViewComponent + let dropdownComponent: DropdownSelectorComponent + let tableViewComponent: TableViewComponent + let chartViewComponent: ChartViewComponent + + beforeEach(() => MockBuilder(DataViewComponent)) beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - DataViewComponent, - MockTableViewComponent, - MockChartViewComponent, - MockDropdownSelectorComponent, - ], providers: [ { provide: MdViewFacade, @@ -103,10 +75,10 @@ describe('DataViewComponent', () => { fixture.detectChanges() dropdownComponent = fixture.debugElement.query( - By.directive(MockDropdownSelectorComponent) + By.directive(DropdownSelectorComponent) ).componentInstance tableViewComponent = fixture.debugElement.query( - By.directive(MockTableViewComponent) + By.directive(TableViewComponent) ).componentInstance })) describe('when component is rendered', () => { @@ -155,18 +127,18 @@ describe('DataViewComponent', () => { component.mode = 'chart' fixture.detectChanges() chartViewComponent = fixture.debugElement.query( - By.directive(MockChartViewComponent) + By.directive(ChartViewComponent) ).componentInstance chartViewComponent.chartConfig$.next(chartConfigMock) })) it('creates a chart view component to render data', () => { expect( - fixture.debugElement.query(By.directive(MockChartViewComponent)) + fixture.debugElement.query(By.directive(ChartViewComponent)) ).toBeTruthy() }) it('does not create a table view component to render data', () => { expect( - fixture.debugElement.query(By.directive(MockTableViewComponent)) + fixture.debugElement.query(By.directive(TableViewComponent)) ).toBeFalsy() }) it('calls setChartConfig', () => { diff --git a/libs/feature/record/src/lib/data-view/data-view.component.ts b/libs/feature/record/src/lib/data-view/data-view.component.ts index d8e735c9df..89c227a1ff 100644 --- a/libs/feature/record/src/lib/data-view/data-view.component.ts +++ b/libs/feature/record/src/lib/data-view/data-view.component.ts @@ -10,12 +10,27 @@ import { map, tap } from 'rxjs/operators' import { MdViewFacade } from '../state' import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' +import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs' +import { + ChartViewComponent, + TableViewComponent, +} from '@geonetwork-ui/feature/dataviz' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-data-view', templateUrl: './data-view.component.html', styleUrls: ['./data-view.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + DropdownSelectorComponent, + TableViewComponent, + TranslateModule, + ChartViewComponent, + ], }) export class DataViewComponent { @Input() mode: 'table' | 'chart' diff --git a/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.spec.ts b/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.spec.ts index b6f1ef46df..efa4b13be1 100644 --- a/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.spec.ts +++ b/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.spec.ts @@ -1,4 +1,3 @@ -import { Component, EventEmitter, Output } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { @@ -6,25 +5,21 @@ import { EXTERNAL_VIEWER_URL_TEMPLATE, ExternalViewerButtonComponent, } from './external-viewer-button.component' +import { MockBuilder } from 'ng-mocks' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { importProvidersFrom } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' -@Component({ - selector: 'gn-ui-button', - template: '
', -}) -export class MockButtonComponent { - @Output() buttonClick = new EventEmitter() -} - describe('ExternalViewerButtonComponent', () => { let component: ExternalViewerButtonComponent let fixture: ComponentFixture + beforeEach(() => MockBuilder(ExternalViewerButtonComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ExternalViewerButtonComponent, MockButtonComponent], - imports: [TranslateModule.forRoot()], providers: [ + importProvidersFrom(TranslateModule.forRoot()), { provide: EXTERNAL_VIEWER_URL_TEMPLATE, useValue: @@ -56,7 +51,7 @@ describe('ExternalViewerButtonComponent', () => { }) }) describe('with mapConfig and valid external links', () => { - let buttonComponent: MockButtonComponent + let buttonComponent: ButtonComponent let componentSpy let windowSpy const openMock = jest.fn().mockReturnThis() @@ -79,7 +74,7 @@ describe('ExternalViewerButtonComponent', () => { describe('click button', () => { beforeEach(() => { buttonComponent = fixture.debugElement.query( - By.directive(MockButtonComponent) + By.directive(ButtonComponent) ).componentInstance componentSpy = jest.spyOn(component, 'openInExternalViewer') windowSpy = jest @@ -127,7 +122,7 @@ describe('ExternalViewerButtonComponent', () => { describe('click button', () => { beforeEach(() => { buttonComponent = fixture.debugElement.query( - By.directive(MockButtonComponent) + By.directive(ButtonComponent) ).componentInstance componentSpy = jest.spyOn(component, 'openInExternalViewer') windowSpy = jest @@ -172,7 +167,7 @@ describe('ExternalViewerButtonComponent', () => { describe('click button', () => { beforeEach(() => { buttonComponent = fixture.debugElement.query( - By.directive(MockButtonComponent) + By.directive(ButtonComponent) ).componentInstance componentSpy = jest.spyOn(component, 'openInExternalViewer') windowSpy = jest diff --git a/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.ts b/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.ts index 326eac72de..ec68aee03b 100644 --- a/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.ts +++ b/libs/feature/record/src/lib/external-viewer-button/external-viewer-button.component.ts @@ -8,8 +8,12 @@ import { } from '@angular/core' import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' import { marker } from '@biesbjerg/ngx-translate-extract-marker' -import { TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' import { getFileFormat } from '@geonetwork-ui/util/shared' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { matOpenInNew } from '@ng-icons/material-icons/baseline' marker('externalviewer.dataset.unnamed') @@ -26,6 +30,9 @@ export const EXTERNAL_VIEWER_OPEN_NEW_TAB = new InjectionToken( templateUrl: './external-viewer-button.component.html', styleUrls: ['./external-viewer-button.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, ButtonComponent, NgIcon, TranslateModule], + viewProviders: [provideIcons({ matOpenInNew })], }) export class ExternalViewerButtonComponent { @Input() link: DatasetOnlineResource diff --git a/libs/feature/record/src/lib/feature-record.module.ts b/libs/feature/record/src/lib/feature-record.module.ts index 74354041bd..b0f580d939 100644 --- a/libs/feature/record/src/lib/feature-record.module.ts +++ b/libs/feature/record/src/lib/feature-record.module.ts @@ -1,52 +1,25 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { - FeatureDetailComponent, - MapContainerComponent, -} from '@geonetwork-ui/ui/map' import { StoreModule } from '@ngrx/store' import { EffectsModule } from '@ngrx/effects' import { UiLayoutModule } from '@geonetwork-ui/ui/layout' -import { - FeatureMapModule, - MapStateContainerComponent, -} from '@geonetwork-ui/feature/map' +import { FeatureMapModule } from '@geonetwork-ui/feature/map' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { UiElementsModule } from '@geonetwork-ui/ui/elements' import { MdViewFacade } from './state' import { MdViewEffects } from './state/mdview.effects' -import { MapViewComponent } from './map-view/map-view.component' -import { DataViewComponent } from './data-view/data-view.component' import { METADATA_VIEW_FEATURE_STATE_KEY, reducer, } from './state/mdview.reducer' -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 { PopupAlertComponent, UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { 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' import { TableComponent } from '@geonetwork-ui/ui/dataviz' -import { FeatureDatavizModule } from '@geonetwork-ui/feature/dataviz' -import { DataViewPermalinkComponent } from './data-view-permalink/data-view-permalink.component' -import { DataViewWebComponentComponent } from './data-view-web-component/data-view-web-component.component' -import { DataViewShareComponent } from './data-view-share/data-view-share.component' import { NgIconsModule, provideNgIconsConfig } from '@ng-icons/core' -import { matClose, matOpenInNew } from '@ng-icons/material-icons/baseline' @NgModule({ - declarations: [ - MapViewComponent, - DataViewComponent, - ExternalViewerButtonComponent, - DataViewPermalinkComponent, - DataViewWebComponentComponent, - DataViewShareComponent, - IgnApiDlComponent, - IgnApiProduitComponent, - ], imports: [ CommonModule, StoreModule.forFeature(METADATA_VIEW_FEATURE_STATE_KEY, reducer), @@ -60,16 +33,7 @@ import { matClose, matOpenInNew } from '@ng-icons/material-icons/baseline' UiWidgetsModule, TranslateModule, TableComponent, - FeatureDatavizModule, - PopupAlertComponent, - FeatureDetailComponent, - MapStateContainerComponent, - MapContainerComponent, - // FIXME: these imports are required by non-standalone components and should be removed once all components have been made standalone - NgIconsModule.withIcons({ - matClose, - matOpenInNew, - }), + NgIconsModule, ], providers: [ MdViewFacade, @@ -77,14 +41,5 @@ import { matClose, matOpenInNew } from '@ng-icons/material-icons/baseline' size: '1.5em', }), ], - exports: [ - MapViewComponent, - DataViewComponent, - DataViewPermalinkComponent, - DataViewWebComponentComponent, - DataViewShareComponent, - ExternalViewerButtonComponent, - IgnApiDlComponent, - ], }) -export class FeatureRecordModule {} +export class FeatureRecordModule { } diff --git a/libs/feature/record/src/lib/map-view/map-view.component.html b/libs/feature/record/src/lib/map-view/map-view.component.html index fb1da0662a..e85b3dee3c 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.html +++ b/libs/feature/record/src/lib/map-view/map-view.component.html @@ -25,14 +25,56 @@ class="top-[1em] right-[1em] p-3 bg-white absolute overflow-y-auto overflow-x-hidden max-h-72 w-56" [class.hidden]="!selection" > - +
+ +
+
+
Legend
+ + + +
+ +
+ + + Legend + + ({ + ...jest.requireActual('@geonetwork-ui/ui/map'), prioritizePageScroll: jest.fn(), })) @@ -121,44 +121,13 @@ class InteractionsMock extends Collection {} @Component({ selector: 'gn-ui-map-container', template: '
', + standalone: true, }) export class MockMapContainerComponent { @Input() context: MapContext openlayersMap = Promise.resolve(new OpenLayersMapMock()) } -@Component({ - selector: 'gn-ui-dropdown-selector', - template: '
', -}) -export class MockDropdownSelectorComponent { - @Input() choices: unknown[] - @Input() showTitle - @Output() selectValue = new EventEmitter() -} - -@Component({ - selector: 'gn-ui-external-viewer-button', - template: '
', -}) -export class MockExternalViewerButtonComponent { - @Input() link: DatasetOnlineResource -} - -@Component({ - selector: 'gn-ui-loading-mask', - template: '
', -}) -export class MockLoadingMaskComponent { - @Input() message -} - -@Component({ - selector: 'gn-ui-popup-alert', - template: '
', -}) -export class MockPopupAlertComponent {} - describe('MapViewComponent', () => { let component: MapViewComponent let fixture: ComponentFixture @@ -170,17 +139,15 @@ describe('MapViewComponent', () => { geoSdkCore.returnImmediately(true) }) + beforeEach(() => + MockBuilder(MapViewComponent).replace( + MapContainerComponent, + MockMapContainerComponent + ) + ) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - MapViewComponent, - MockMapContainerComponent, - MockDropdownSelectorComponent, - MockExternalViewerButtonComponent, - MockLoadingMaskComponent, - MockPopupAlertComponent, - ], - schemas: [NO_ERRORS_SCHEMA], providers: [ { provide: MdViewFacade, @@ -195,7 +162,7 @@ describe('MapViewComponent', () => { useClass: DataServiceMock, }, ], - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), MapLegendComponent], }).compileComponents() mdViewFacade = TestBed.inject(MdViewFacade) }) @@ -216,14 +183,14 @@ describe('MapViewComponent', () => { describe('map layers', () => { let dropdownComponent: DropdownSelectorComponent - let externalViewerButtonComponent: MockExternalViewerButtonComponent + let externalViewerButtonComponent: ExternalViewerButtonComponent beforeEach(() => { dropdownComponent = fixture.debugElement.query( - By.directive(MockDropdownSelectorComponent) + By.directive(DropdownSelectorComponent) ).componentInstance externalViewerButtonComponent = fixture.debugElement.query( - By.directive(MockExternalViewerButtonComponent) + By.directive(ExternalViewerButtonComponent) ).componentInstance }) @@ -516,7 +483,7 @@ describe('MapViewComponent', () => { }) it('shows a loading indicator', () => { expect( - fixture.debugElement.query(By.directive(MockLoadingMaskComponent)) + fixture.debugElement.query(By.directive(LoadingMaskComponent)) ).toBeTruthy() }) }) @@ -548,7 +515,7 @@ describe('MapViewComponent', () => { it('does not show a loading indicator', () => { fixture.detectChanges() expect( - fixture.debugElement.query(By.directive(MockLoadingMaskComponent)) + fixture.debugElement.query(By.directive(LoadingMaskComponent)) ).toBeFalsy() }) }) @@ -802,6 +769,25 @@ describe('MapViewComponent', () => { }) }) + describe('display legend', () => { + it('should render the MapLegendComponent', () => { + const legendComponent = fixture.debugElement.query( + By.directive(MapLegendComponent) + ) + expect(legendComponent).toBeTruthy() + }) + it('should handle legendStatusChange event', () => { + const legendComponent = fixture.debugElement.query( + By.directive(MapLegendComponent) + ).componentInstance + const legendStatusChangeSpy = jest.spyOn( + component, + 'onLegendStatusChange' + ) + legendComponent.legendStatusChange.emit(true) + expect(legendStatusChangeSpy).toHaveBeenCalledWith(true) + }) + }) describe('map view extent', () => { describe('if no record extent', () => { beforeEach(fakeAsync(() => { 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 953823db8f..bc7dfc2a49 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 @@ -22,6 +22,7 @@ import { distinctUntilChanged, finalize, map, + shareReplay, switchMap, tap, } from 'rxjs/operators' @@ -34,21 +35,64 @@ import { MapContextLayer, } from '@geospatial-sdk/core' import { + FeatureDetailComponent, MapContainerComponent, prioritizePageScroll, + MapLegendComponent, } from '@geonetwork-ui/ui/map' import { Feature } from 'geojson' +import { NgIconComponent, provideIcons } from '@ng-icons/core' +import { matClose } from '@ng-icons/material-icons/baseline' +import { CommonModule } from '@angular/common' +import { + ButtonComponent, + DropdownSelectorComponent, +} from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' +import { ExternalViewerButtonComponent } from '../external-viewer-button/external-viewer-button.component' +import { + LoadingMaskComponent, + PopupAlertComponent, +} from '@geonetwork-ui/ui/widgets' @Component({ selector: 'gn-ui-map-view', templateUrl: './map-view.component.html', styleUrls: ['./map-view.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + DropdownSelectorComponent, + MapContainerComponent, + FeatureDetailComponent, + PopupAlertComponent, + TranslateModule, + LoadingMaskComponent, + NgIconComponent, + ExternalViewerButtonComponent, + ButtonComponent, + MapLegendComponent, + ], + viewProviders: [provideIcons({ matClose })], }) export class MapViewComponent implements AfterViewInit { @ViewChild('mapContainer') mapContainer: MapContainerComponent selection: Feature + showLegend = true + legendExists = false + + toggleLegend() { + this.showLegend = !this.showLegend + } + + onLegendStatusChange(status: boolean) { + this.legendExists = status + if (!status) { + this.showLegend = false + } + } compatibleMapLinks$ = combineLatest([ this.mdViewFacade.mapApiLinks$, @@ -124,7 +168,8 @@ export class MapViewComponent implements AfterViewInit { ...context, view, } - }) + }), + shareReplay(1) ) constructor( diff --git a/libs/feature/router/src/lib/default/constants.ts b/libs/feature/router/src/lib/default/constants.ts index 3365b3eca5..bbb46353c2 100644 --- a/libs/feature/router/src/lib/default/constants.ts +++ b/libs/feature/router/src/lib/default/constants.ts @@ -1,3 +1,5 @@ +import { DateRange } from '@geonetwork-ui/api/repository' + export const ROUTER_STATE_KEY = 'router' export const ROUTER_ROUTE_SEARCH = 'search' @@ -9,4 +11,7 @@ export enum ROUTE_PARAMS { PUBLISHER = 'publisher', // FIXME: this shouldn't be here as it is a search field PAGE = '_page', } -export type SearchRouteParams = Record +export type SearchRouteParams = Record< + string, + string | string[] | number | DateRange +> diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts b/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts new file mode 100644 index 0000000000..ec3c3ab1b8 --- /dev/null +++ b/libs/feature/router/src/lib/default/state/query-params.utils.spec.ts @@ -0,0 +1,59 @@ +import { ROUTE_PARAMS } from '../constants' +import { expandQueryParams, flattenQueryParams } from './query-params.utils' + +describe('query params utilities', () => { + describe('flattenQueryParams', () => { + it('produces serialized query params from various route parameters', () => { + const params = flattenQueryParams({ + [ROUTE_PARAMS.SORT]: 'createDate', + publisher: ['john', 'barbie'], + updateDate: { + start: new Date('2010-03-10T14:50:12'), + end: new Date('2014-01-01'), + }, + changeDate: { + end: new Date('2008-08-14T14:50:12'), + }, + emptyParam: [], + }) + expect(params).toEqual({ + _sort: 'createDate', + publisher: ['john,barbie'], + updateDate: ['2010-03-10..2014-01-01'], + changeDate: ['..2008-08-14'], + emptyParam: [], + }) + }) + }) + describe('expandQueryParams', () => { + it('restores full route parameters from serialized query params in arrays', () => { + const params = expandQueryParams({ + _sort: 'createDate', + publisher: ['john,barbie'], + updateDate: ['2010-03-10..2014-01-01'], + changeDate: ['..2008-08-14'], + }) + expect(params).toEqual({ + [ROUTE_PARAMS.SORT]: 'createDate', + publisher: ['john', 'barbie'], + updateDate: { + start: new Date('2010-03-10T00:00:00'), + end: new Date('2014-01-01T00:00:00'), + }, + changeDate: { + end: new Date('2008-08-14T00:00:00'), + }, + }) + }) + it('restores full route parameter from a SINGLE serialized string query param', () => { + const params = expandQueryParams({ + changeDate: '..2008-08-14', + }) + expect(params).toEqual({ + changeDate: { + end: new Date('2008-08-14T00:00:00'), + }, + }) + }) + }) +}) diff --git a/libs/feature/router/src/lib/default/state/query-params.utils.ts b/libs/feature/router/src/lib/default/state/query-params.utils.ts new file mode 100644 index 0000000000..133d5dbc7a --- /dev/null +++ b/libs/feature/router/src/lib/default/state/query-params.utils.ts @@ -0,0 +1,61 @@ +import { + DateRange, + formatDate, + isDateRange, +} from '@geonetwork-ui/api/repository' +import { ROUTE_PARAMS, SearchRouteParams } from '../constants' + +export function flattenQueryParams( + params: SearchRouteParams +): Record { + const flattened = { ...params } + for (const key in params) { + if ( + Array.isArray(flattened[key]) && + (flattened[key] as string[]).length > 0 + ) { + flattened[key] = [(flattened[key] as string[]).join(',')] + } else if (isDateRange(flattened[key] as DateRange)) { + const start = (flattened[key] as DateRange).start + const end = (flattened[key] as DateRange).end + flattened[key] = [ + `${start ? formatDate(start) : ''}..${formatDate(end) || ''}`, + ] + } + } + + return flattened +} + +export function expandQueryParams( + params: Record +): SearchRouteParams { + const expanded = { ...params } + for (const key in params) { + const value: string = Array.isArray(expanded[key]) + ? expanded[key][0] + : (expanded[key] as string) + if (typeof value === 'string') { + if ( + Object.values(ROUTE_PARAMS).includes(key as ROUTE_PARAMS) && + key !== 'publisher' //FIXME: temporary workaround as publisher shouldn't be in ROUTE_PARAMS as it is a search field + ) { + //do nothing + } else if (isDateUrl(value)) { + const [start, end] = value.split('..') + expanded[key] = { + ...(start && { start: new Date(`${start}T00:00:00`) }), + ...(end && { end: new Date(`${end}T00:00:00`) }), + } + } else { + expanded[key] = value.split(',') + } + } + } + return expanded +} + +// this only matches if the separator ".." is present only once and no dots are present elsewhere +function isDateUrl(value: string) { + return value.match(/^[^.]*(\.\.)[^.]*$/) +} 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 1186f2fa18..a76e23114e 100644 --- a/libs/feature/router/src/lib/default/state/router.facade.ts +++ b/libs/feature/router/src/lib/default/state/router.facade.ts @@ -17,6 +17,7 @@ import { } from './router.actions' import { selectCurrentRoute, selectRouteParams } from './router.selectors' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { expandQueryParams, flattenQueryParams } from './query-params.utils' @Injectable() export class RouterFacade { @@ -27,7 +28,8 @@ export class RouterFacade { filter((route) => !!route), filter((route) => route.url[0]?.path.startsWith(ROUTER_ROUTE_SEARCH)), map((route) => route.queryParams), - distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)) + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), + map(expandQueryParams) ) constructor( @@ -62,7 +64,7 @@ export class RouterFacade { updateSearch(query?: SearchRouteParams) { this.go({ path: this.routerService.getSearchRoute(), - ...(query && { query }), + ...(query && { query: flattenQueryParams(query) }), queryParamsHandling: 'merge', }) } @@ -70,7 +72,7 @@ export class RouterFacade { setSearch(query?: SearchRouteParams) { this.go({ path: this.routerService.getSearchRoute(), - ...(query && { query }), + ...(query && { query: flattenQueryParams(query) }), }) } diff --git a/libs/feature/search/src/index.ts b/libs/feature/search/src/index.ts index f6103754e3..63ae62fd76 100644 --- a/libs/feature/search/src/index.ts +++ b/libs/feature/search/src/index.ts @@ -22,3 +22,5 @@ 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-container.component' +export * from './lib/search-filters-summary/search-filters-summary.component' +export * from './lib/search-filters-summary-item/search-filters-summary-item.component' diff --git a/libs/feature/search/src/lib/constants.ts b/libs/feature/search/src/lib/constants.ts index 28c3d59cb0..17075adfd6 100644 --- a/libs/feature/search/src/lib/constants.ts +++ b/libs/feature/search/src/lib/constants.ts @@ -19,9 +19,9 @@ export const FIELDS_SUMMARY: FieldName[] = [ 'userSavedCount', 'cl_topic', 'cl_maintenanceAndUpdateFrequency', - 'tag', 'MD_LegalConstraintsUseLimitationObject', 'qualityScore', + 'allKeywords', ] export const FIELDS_BRIEF: FieldName[] = [ diff --git a/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.spec.ts b/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.spec.ts index 9eca528262..d58ce87248 100644 --- a/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.spec.ts +++ b/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.spec.ts @@ -37,8 +37,11 @@ describe('FavoriteStarComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [FavoriteStarComponent, StarToggleComponent], - imports: [TranslateModule.forRoot()], + imports: [ + TranslateModule.forRoot(), + FavoriteStarComponent, + StarToggleComponent, + ], providers: [ { provide: PlatformServiceInterface, @@ -66,7 +69,7 @@ describe('FavoriteStarComponent', () => { favoritesService = TestBed.inject(FavoritesService) fixture = TestBed.createComponent(FavoriteStarComponent) component = fixture.componentInstance - component.displayCount = 'true' + component.displayCount = true fixture.detectChanges() starToggle = fixture.debugElement.query( By.directive(StarToggleComponent) diff --git a/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.ts b/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.ts index a6a9a1e8a1..d14ef66a4a 100644 --- a/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.ts +++ b/libs/feature/search/src/lib/favorites/favorite-star/favorite-star.component.ts @@ -16,12 +16,15 @@ import { Observable, Subscription } from 'rxjs' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { AuthService, FavoritesService } from '@geonetwork-ui/api/repository' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-favorite-star', templateUrl: './favorite-star.component.html', styleUrls: ['./favorite-star.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, StarToggleComponent], }) export class FavoriteStarComponent implements AfterViewInit, OnDestroy { @Input() displayCount? = true diff --git a/libs/feature/search/src/lib/feature-search.module.ts b/libs/feature/search/src/lib/feature-search.module.ts index 331634fea2..ef0a063a3e 100644 --- a/libs/feature/search/src/lib/feature-search.module.ts +++ b/libs/feature/search/src/lib/feature-search.module.ts @@ -14,14 +14,21 @@ 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 { AutocompleteComponent, UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { + AutocompleteComponent, + DateRangeDropdownComponent, + 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' +import { ErrorComponent, UiElementsModule } from '@geonetwork-ui/ui/elements' import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' -import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' +import { + SpinningLoaderComponent, + UiWidgetsModule, +} from '@geonetwork-ui/ui/widgets' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { Gn4Repository } from '@geonetwork-ui/api/repository' +import { FavoriteStarComponent } from './favorites/favorite-star/favorite-star.component' @NgModule({ declarations: [ @@ -32,7 +39,6 @@ import { Gn4Repository } from '@geonetwork-ui/api/repository' ResultsListContainerComponent, ResultsHitsContainerComponent, SearchStateContainerDirective, - FavoriteStarComponent, FilterDropdownComponent, ], imports: [ @@ -50,6 +56,10 @@ import { Gn4Repository } from '@geonetwork-ui/api/repository' FacetsModule, UiWidgetsModule, AutocompleteComponent, + SpinningLoaderComponent, + ErrorComponent, + FavoriteStarComponent, + DateRangeDropdownComponent, ], exports: [ SortByComponent, @@ -60,7 +70,6 @@ import { Gn4Repository } from '@geonetwork-ui/api/repository' ResultsHitsContainerComponent, FacetsModule, SearchStateContainerDirective, - FavoriteStarComponent, FilterDropdownComponent, ], providers: [ diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html index 9ff8ff4c1b..661f7fb47f 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html @@ -1,11 +1,21 @@ - - + [startDate]="(selectedDateRange$ | async)?.start" + [endDate]="(selectedDateRange$ | async)?.end" + (startDateChange)="onStartDateChange($event)" + (endDateChange)="onEndDateChange($event)" +> + + + + diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts index 293918e081..1172802872 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.spec.ts @@ -44,6 +44,7 @@ class FieldsServiceMock { ) ) ) + getFieldType = jest.fn(() => 'values') } @Component({ @@ -60,18 +61,32 @@ export class MockDropdownComponent { @Input() selected: unknown[] @Output() selectValues = new EventEmitter() } +@Component({ + selector: 'gn-ui-date-range-dropdown', + template: '
', +}) +export class MockDateRangeDropdownComponent { + @Input() title: string + @Output() startDateChange = new EventEmitter() + @Output() endDateChange = new EventEmitter() +} describe('FilterDropdownComponent', () => { let facade: SearchFacadeMock let component: FilterDropdownComponent let dropdown: MockDropdownComponent + let dateRangeDropdown: MockDateRangeDropdownComponent let searchService: SearchService let fieldsService: FieldsService let fixture: ComponentFixture beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [FilterDropdownComponent, MockDropdownComponent], + declarations: [ + FilterDropdownComponent, + MockDropdownComponent, + MockDateRangeDropdownComponent, + ], schemas: [NO_ERRORS_SCHEMA], providers: [ { @@ -102,6 +117,7 @@ describe('FilterDropdownComponent', () => { component = fixture.componentInstance component.fieldName = 'Org' + fixture.detectChanges() dropdown = fixture.debugElement.query( By.directive(MockDropdownComponent) ).componentInstance @@ -111,122 +127,183 @@ describe('FilterDropdownComponent', () => { expect(component).toBeTruthy() }) - it('provides selected values initially', () => { - fixture.detectChanges() - expect(dropdown.selected).toEqual([]) + describe('displays dropdown component based on fieldtype', () => { + it('displays dropdown-multiselect for fields of type values', () => { + expect( + fixture.debugElement.query(By.directive(MockDropdownComponent)) + ).toBeTruthy() + expect( + fixture.debugElement.query(By.directive(MockDateRangeDropdownComponent)) + ).toBeFalsy() + }) + it('displays daterange-dropdown for fields of type dateRange', () => { + component.fieldType = 'dateRange' + fixture.detectChanges() + expect( + fixture.debugElement.query(By.directive(MockDropdownComponent)) + ).toBeFalsy() + expect( + fixture.debugElement.query(By.directive(MockDateRangeDropdownComponent)) + ).toBeTruthy() + }) }) - describe('when selected values change', () => { - const values = ['org1', 'org2', 34] - beforeEach(fakeAsync(() => { - dropdown.selectValues.emit(values) - tick() - })) - it('converts values to filters', () => { - expect(fieldsService.buildFiltersFromFieldValues).toHaveBeenCalledWith({ - Org: values, - }) + describe('#dropdown-multiselect', () => { + it('provides selected values initially', () => { + fixture.detectChanges() + expect(dropdown.selected).toEqual([]) }) - it('calls updateSearch on the search service', () => { - expect(searchService.updateFilters).toHaveBeenCalledWith({ - 'converted from values': { + + describe('when selected values change', () => { + const values = ['org1', 'org2', 34] + beforeEach(fakeAsync(() => { + dropdown.selectValues.emit(values) + tick() + })) + it('converts values to filters', () => { + expect(fieldsService.buildFiltersFromFieldValues).toHaveBeenCalledWith({ Org: values, - }, + }) + }) + it('calls updateSearch on the search service', () => { + expect(searchService.updateFilters).toHaveBeenCalledWith({ + 'converted from values': { + Org: values, + }, + }) }) }) - }) - describe('available choices', () => { - describe('on init', () => { - beforeEach(() => { - component.ngOnInit() + describe('available choices', () => { + describe('on init', () => { + beforeEach(() => { + component.ngOnInit() + }) + it('reads available values', () => { + expect(fieldsService.getAvailableValues).toHaveBeenCalledWith('Org') + }) }) - it('reads available values', () => { - expect(fieldsService.getAvailableValues).toHaveBeenCalledWith('Org') + describe('when there are available values', () => { + const values = [ + { label: 'First Org (4)', value: 'First Org' }, + { label: 'Second Org (2)', value: 'Second Org' }, + { label: 'Third Org (1)', value: 'Third Org' }, + ] + beforeEach(() => { + fieldsService.getAvailableValues = () => of(values) + component.ngOnInit() + fixture.detectChanges() + }) + it('reads choices from the search response', () => { + expect(dropdown.choices).toEqual(values) + }) }) - }) - describe('when there are available values', () => { - const values = [ - { label: 'First Org (4)', value: 'First Org' }, - { label: 'Second Org (2)', value: 'Second Org' }, - { label: 'Third Org (1)', value: 'Third Org' }, - ] - beforeEach(() => { - fieldsService.getAvailableValues = () => of(values) - component.ngOnInit() - fixture.detectChanges() + describe('no available values', () => { + beforeEach(() => { + fieldsService.getAvailableValues = () => of([]) + component.ngOnInit() + fixture.detectChanges() + }) + it('uses an empty array', () => { + expect(dropdown.choices).toEqual([]) + }) }) - it('reads choices from the search response', () => { - expect(dropdown.choices).toEqual(values) + describe('available values are numerical', () => { + const values = [ + { label: '1 (4)', value: 1 }, + { label: '2 (2)', value: 2 }, + { label: '3 (1)', value: 3 }, + ] + beforeEach(() => { + fieldsService.getAvailableValues = () => of(values) + component.ngOnInit() + fixture.detectChanges() + }) + it('converts values to string', () => { + expect(dropdown.choices).toEqual([ + { label: '1 (4)', value: '1' }, + { label: '2 (2)', value: '2' }, + { label: '3 (1)', value: '3' }, + ]) + }) }) }) - describe('no available values', () => { + + describe('selected values', () => { + const filters = { + Org: 'bla', + } beforeEach(() => { - fieldsService.getAvailableValues = () => of([]) - component.ngOnInit() + facade.searchFilters$.next(filters) fixture.detectChanges() }) - it('uses an empty array', () => { - expect(dropdown.choices).toEqual([]) + it('converts filters to values', () => { + expect(fieldsService.readFieldValuesFromFilters).toHaveBeenCalledWith( + filters + ) + }) + it('shows selected values in the dropdown', () => { + expect(dropdown.selected).toEqual(['converted from filters', 'bla']) }) }) - describe('available values are numerical', () => { - const values = [ - { label: '1 (4)', value: 1 }, - { label: '2 (2)', value: 2 }, - { label: '3 (1)', value: 3 }, - ] + + describe('field is unsupported', () => { beforeEach(() => { - fieldsService.getAvailableValues = () => of(values) + fieldsService.getAvailableValues = () => + throwError(() => new Error('blah')) + fieldsService.readFieldValuesFromFilters = () => { + throw new Error('blah') + } + fieldsService.buildFiltersFromFieldValues = () => { + throw new Error('blah') + } component.ngOnInit() fixture.detectChanges() }) - it('converts values to string', () => { - expect(dropdown.choices).toEqual([ - { label: '1 (4)', value: '1' }, - { label: '2 (2)', value: '2' }, - { label: '3 (1)', value: '3' }, - ]) + it('still gives an array for choices', () => { + expect(dropdown.choices).toEqual([]) + }) + it('still gives an array for selected', () => { + expect(dropdown.selected).toEqual([]) }) }) }) - describe('selected values', () => { - const filters = { - Org: 'bla', - } + describe('#daterange-dropdown', () => { + const start = new Date('2021-01-01') + const end = new Date('2021-01-02') + beforeEach(() => { - facade.searchFilters$.next(filters) + component.fieldType = 'dateRange' + component.fieldName = 'someDateField' fixture.detectChanges() + dateRangeDropdown = fixture.debugElement.query( + By.directive(MockDateRangeDropdownComponent) + ).componentInstance }) - it('converts filters to values', () => { - expect(fieldsService.readFieldValuesFromFilters).toHaveBeenCalledWith( - filters - ) - }) - it('shows selected values in the dropdown', () => { - expect(dropdown.selected).toEqual(['converted from filters', 'bla']) + it('updates the start date', () => { + dateRangeDropdown.startDateChange.emit(start) + expect(component.dateRange).toEqual({ start }) }) - }) - - describe('field is unsupported', () => { - beforeEach(() => { - fieldsService.getAvailableValues = () => - throwError(() => new Error('blah')) - fieldsService.readFieldValuesFromFilters = () => { - throw new Error('blah') - } - fieldsService.buildFiltersFromFieldValues = () => { - throw new Error('blah') - } - component.ngOnInit() - fixture.detectChanges() + it('updates the end date', () => { + dateRangeDropdown.endDateChange.emit(end) + expect(component.dateRange).toEqual({ end }) }) - it('still gives an array for choices', () => { - expect(dropdown.choices).toEqual([]) + it('calls buildFiltersFromFieldValues with dates', () => { + dateRangeDropdown.startDateChange.emit(start) + dateRangeDropdown.endDateChange.emit(end) + expect(fieldsService.buildFiltersFromFieldValues).toHaveBeenCalledWith({ + someDateField: { start, end }, + }) }) - it('still gives an array for selected', () => { - expect(dropdown.selected).toEqual([]) + it('calls updateSearch on the search service', () => { + dateRangeDropdown.startDateChange.emit(start) + dateRangeDropdown.endDateChange.emit(end) + expect(searchService.updateFilters).toHaveBeenCalledWith({ + 'converted from values': { + someDateField: { start, end }, + }, + }) }) }) }) diff --git a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts index 8e36fb08b5..33f7d30adb 100644 --- a/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +++ b/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts @@ -10,7 +10,12 @@ import { catchError, filter, map, startWith } from 'rxjs/operators' import { SearchFacade } from '../state/search.facade' import { SearchService } from '../utils/service/search.service' import { FieldsService } from '../utils/service/fields.service' -import { FieldAvailableValue, FieldValue } from '../utils/service/fields' +import { + FieldAvailableValue, + FieldType, + FieldValue, +} from '../utils/service/fields' +import { DateRange } from '@geonetwork-ui/api/repository' @Component({ selector: 'gn-ui-filter-dropdown', @@ -22,6 +27,8 @@ export class FilterDropdownComponent implements OnInit { @Input() fieldName: string @Input() title: string + fieldType: FieldType + dateRange: DateRange choices$: Observable selected$ = this.searchFacade.searchFilters$.pipe( switchMap((filters) => @@ -33,6 +40,10 @@ export class FilterDropdownComponent implements OnInit { catchError(() => of([])) ) as Observable + selectedDateRange$ = this.selected$.pipe( + map((selectedDateRange) => selectedDateRange as DateRange) + ) as Observable + onSelectedValues(values: unknown[]) { this.fieldsService .buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] }) @@ -46,6 +57,7 @@ export class FilterDropdownComponent implements OnInit { ) {} ngOnInit() { + this.fieldType = this.fieldsService.getFieldType(this.fieldName) this.choices$ = this.fieldsService.getAvailableValues(this.fieldName).pipe( startWith([] as FieldAvailableValue[]), map((values) => @@ -57,4 +69,23 @@ export class FilterDropdownComponent implements OnInit { catchError(() => of([])) ) } + + onStartDateChange(start: Date) { + if (!start) return + this.dateRange = { ...this.dateRange, start } + } + + onEndDateChange(end: Date) { + if (!end) return + this.dateRange = { ...this.dateRange, end } + if (this.dateRange.start && this.dateRange.end) { + this.fieldsService + .buildFiltersFromFieldValues({ + [this.fieldName]: this.dateRange, + }) + .subscribe((filters) => { + return this.searchService.updateFilters(filters) + }) + } + } } 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 1e5116a0c5..5022ef8758 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 @@ -9,12 +9,7 @@ [recordUrlGetter]="recordUrlGetter" (mdSelect)="onMetadataSelection($event)" > - +
{ }) describe('when there are no more results', () => { beforeEach(() => { - searchFacade.isEndOfResults$.next(true) + searchFacade.currentPage$.next(5) fixture.detectChanges() }) it('show-more element is hidden', () => { diff --git a/libs/feature/search/src/lib/results-list/results-list.container.component.ts b/libs/feature/search/src/lib/results-list/results-list.container.component.ts index 992f71ac29..697a9f04d2 100644 --- a/libs/feature/search/src/lib/results-list/results-list.container.component.ts +++ b/libs/feature/search/src/lib/results-list/results-list.container.component.ts @@ -7,7 +7,7 @@ import { Optional, Output, } from '@angular/core' -import { Observable, tap } from 'rxjs' +import { combineLatest, Observable, tap } from 'rxjs' import { filter, map } from 'rxjs/operators' import { SearchFacade } from '../state/search.facade' import { SearchError } from '../state/reducer' @@ -39,6 +39,7 @@ export class ResultsListContainerComponent implements OnInit { errorCode$: Observable errorMessage$: Observable pipelineForQualityScoreActivated: Observable + allowShowMore$: Observable errorTypes = ErrorType recordUrlGetter = this.getRecordUrl.bind(this) @@ -81,6 +82,16 @@ export class ResultsListContainerComponent implements OnInit { filter((error) => error !== null), map((error) => error.message) ) + this.allowShowMore$ = combineLatest([ + this.facade.isLoading$, + this.facade.currentPage$, + this.facade.totalPages$, + ]).pipe( + map( + ([loading, currentPage, totalPages]) => + !loading && currentPage < totalPages + ) + ) } onMetadataSelection(metadata: CatalogRecord): void { 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 index 85e63b4d85..6504bea923 100644 --- 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 @@ -120,12 +120,16 @@ describe('ResultsTableContainerComponent', () => { throwError(() => 'oopsie') ) component.handleDeleteRecord(datasetRecordsFixture()[0]) - expect(notificationsService.showNotification).toHaveBeenCalledWith({ - type: 'error', - title: 'editor.record.deleteError.title', - text: 'editor.record.deleteError.body oopsie', - closeMessage: 'editor.record.deleteError.closeMessage', - }) + expect(notificationsService.showNotification).toHaveBeenCalledWith( + { + type: 'error', + title: 'editor.record.deleteError.title', + text: 'editor.record.deleteError.body oopsie', + closeMessage: 'editor.record.deleteError.closeMessage', + }, + undefined, + 'oopsie' + ) }) }) 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 index 43afbe95bc..b2f75f483b 100644 --- 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 @@ -79,18 +79,22 @@ export class ResultsTableContainerComponent implements OnDestroy { ) }, error: (error) => { - this.notificationsService.showNotification({ - type: 'error', - title: this.translateService.instant( - 'editor.record.deleteError.title' - ), - text: `${this.translateService.instant( - 'editor.record.deleteError.body' - )} ${error}`, - closeMessage: this.translateService.instant( - 'editor.record.deleteError.closeMessage' - ), - }) + this.notificationsService.showNotification( + { + type: 'error', + title: this.translateService.instant( + 'editor.record.deleteError.title' + ), + text: `${this.translateService.instant( + 'editor.record.deleteError.body' + )} ${error}`, + closeMessage: this.translateService.instant( + 'editor.record.deleteError.closeMessage' + ), + }, + undefined, + error + ) }, }) ) diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.css b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.css similarity index 100% rename from libs/ui/elements/src/lib/pagination/pagination.component.css rename to libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.css diff --git a/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.html b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.html new file mode 100644 index 0000000000..558703608b --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.html @@ -0,0 +1,20 @@ +
+ {{ + translatedLabel + }} + {{ fieldValue.label }} +
diff --git a/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.spec.ts b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.spec.ts new file mode 100644 index 0000000000..45bf0fdb39 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.spec.ts @@ -0,0 +1,155 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { SearchFiltersSummaryItemComponent } from './search-filters-summary-item.component' +import { BehaviorSubject, firstValueFrom, of } from 'rxjs' +import { MockBuilder, MockComponent, MockProvider } from 'ng-mocks' +import { SearchFacade } from '../state/search.facade' +import { SearchService } from '../utils/service/search.service' +import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' +import { BadgeComponent } from '@geonetwork-ui/ui/inputs' +import { CommonModule, DatePipe } from '@angular/common' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { FieldsService } from '../utils/service/fields.service' +import { FieldType } from '../utils/service/fields' + +const FIELD_VALUES_FROM_FILTERS_MOCK = { + organization: [], + format: [], + resourceType: [], + representationType: [], + publicationYear: [], + topic: [], + inspireKeyword: [], + keyword: [], + documentStandard: [], + isSpatial: [], + q: [], + license: [], + owner: [], + producerOrg: [], + publisherOrg: [], + user: ['admin|admin|admin|Administrator', 'barbie|Roberts|Barbara|UserAdmin'], + changeDate: { + start: new Date('2024-11-01T00:00:00.000Z'), + end: new Date('2024-11-29T00:00:00.000Z'), + }, +} +/* searchFilters$ is only used to trigger change detection. + ** its value is replaced by FIELD_VALUES_FROM_FILTERS_MOCK in stream + */ +class SearchFacadeMock { + searchFilters$ = new BehaviorSubject({}) +} +class SearchServiceMock { + setFilters = jest.fn() +} + +class TranslateServiceMock { + get = jest.fn(() => of('')) +} + +describe('SearchFiltersSummaryItemComponent', () => { + let component: SearchFiltersSummaryItemComponent + let fixture: ComponentFixture + let searchFacade: SearchFacade + let translateService: TranslateService + let fieldsService: FieldsService + + beforeEach(() => { + return MockBuilder(SearchFiltersSummaryItemComponent) + }) + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CommonModule, TranslateModule.forRoot()], + declarations: [ + SearchFiltersSummaryItemComponent, + MockComponent(BadgeComponent), + ], + providers: [ + MockProvider(SearchFacade, SearchFacadeMock, 'useClass'), + MockProvider(SearchService, SearchServiceMock, 'useClass'), + MockProvider(FieldsService), + MockProvider(DatePipe), + MockProvider(TranslateService, TranslateServiceMock, 'useClass'), + ], + }).compileComponents() + fixture = TestBed.createComponent(SearchFiltersSummaryItemComponent) + component = fixture.componentInstance + searchFacade = TestBed.inject(SearchFacade) + fieldsService = TestBed.inject(FieldsService) + translateService = TestBed.inject(TranslateService) + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should set fieldValues$ observable for empty filters', async () => { + const fieldValues = await firstValueFrom(component.fieldValues$) + expect(fieldValues).toEqual([]) + }) + + describe('fieldValues$', () => { + beforeEach(() => { + fieldsService.getFieldType = jest.fn( + (field: 'changeDate' | 'user') => + (field === 'changeDate' ? 'dateRange' : 'values') as FieldType + ) + fieldsService.readFieldValuesFromFilters = jest.fn(() => + of(FIELD_VALUES_FROM_FILTERS_MOCK) + ) + ;(searchFacade.searchFilters$ as BehaviorSubject).next({}) + }) + it('should set fieldValues$ observable for user values filters', async () => { + component.fieldName = 'user' + const fieldValues = await firstValueFrom(component.fieldValues$) + expect(fieldValues).toEqual([ + { + value: 'admin|admin|admin|Administrator', + label: 'admin admin', + }, + { + value: 'barbie|Roberts|Barbara|UserAdmin', + label: 'Barbara Roberts', + }, + ]) + }) + it('should set fieldValues$ observable for changeDate dateRange filters', async () => { + component.fieldName = 'changeDate' + fixture.detectChanges() + const fieldValues = await firstValueFrom(component.fieldValues$) + expect(fieldValues).toEqual([ + { + value: { + start: new Date('2024-11-01T00:00:00.000Z'), + end: new Date('2024-11-29T00:00:00.000Z'), + }, + label: '01.11.2024 - 29.11.2024', + }, + ]) + }) + }) + + describe('translateLabel', () => { + const fieldName = 'user' + const labelKey = `search.filters.summaryLabel.${fieldName}` + const fallbackKey = `search.filters.${fieldName}` + beforeEach(() => { + component.fieldName = fieldName + fixture.detectChanges() + translateService.get = jest.fn((key) => { + if (key === labelKey) { + return of(labelKey) // Simulate missing translation + } else if (key === fallbackKey) { + return of('Fallback Label') + } + return of('') + }) + }) + it('should translate label with fallback if necessary', () => { + component.translateLabel() + expect(component.translatedLabel).toBe('Fallback Label') + }) + }) +}) diff --git a/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts new file mode 100644 index 0000000000..024a0217cd --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts @@ -0,0 +1,118 @@ +import { Component, Input, OnInit } from '@angular/core' +import { CommonModule, DatePipe } from '@angular/common' +import { + catchError, + firstValueFrom, + map, + Observable, + of, + switchMap, +} from 'rxjs' +import { BadgeComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { DateRange } from '@geonetwork-ui/api/repository' +import { FieldType, FieldValue } from '../utils/service/fields' +import { SearchFacade } from '../state/search.facade' +import { SearchService } from '../utils/service/search.service' +import { FieldsService } from '../utils/service/fields.service' +import { formatUserInfo } from '@geonetwork-ui/util/shared' +import { marker } from '@biesbjerg/ngx-translate-extract-marker' + +marker('search.filters.summaryLabel.user') +marker('search.filters.summaryLabel.changeDate') + +interface DisplayedValue { + label: string + value: FieldValue | DateRange +} + +@Component({ + selector: 'gn-ui-search-filters-summary-item', + standalone: true, + imports: [CommonModule, TranslateModule, BadgeComponent], + templateUrl: './search-filters-summary-item.component.html', + styleUrls: ['./search-filters-summary-item.component.css'], + providers: [DatePipe], +}) +export class SearchFiltersSummaryItemComponent implements OnInit { + @Input() fieldName: string + fieldType: FieldType + translatedLabel: string + + fieldValues$ = this.searchFacade.searchFilters$.pipe( + switchMap((filters) => + this.fieldsService.readFieldValuesFromFilters(filters) + ), + map((fieldValues) => + Array.isArray(fieldValues[this.fieldName]) + ? (fieldValues[this.fieldName] as FieldValue[]) + : ([fieldValues[this.fieldName]] as FieldValue[]) + ), + map( + (fieldValues) => this.getReadableValues(fieldValues) as DisplayedValue[] + ), + catchError(() => of([])) + ) as Observable + + constructor( + private searchFacade: SearchFacade, + private searchService: SearchService, + private fieldsService: FieldsService, + private datePipe: DatePipe, + private translate: TranslateService + ) {} + + ngOnInit() { + this.fieldType = this.fieldsService.getFieldType(this.fieldName) + this.translateLabel() + } + + translateLabel() { + const labelKey = `search.filters.summaryLabel.${this.fieldName}` + const fallbackKey = `search.filters.${this.fieldName}` + this.translate.get(labelKey).subscribe((value: string) => { + if (value === labelKey) { + this.translate.get(fallbackKey).subscribe((fallbackValue: string) => { + this.translatedLabel = fallbackValue + }) + } else { + this.translatedLabel = value + } + }) + } + + getReadableValues(fieldValues: FieldValue[] | DateRange[]): DisplayedValue[] { + return fieldValues.map((value) => { + if (this.fieldType === 'dateRange') { + return { + value, + label: `${this.datePipe.transform( + value.start, + 'dd.MM.yyyy' + )} - ${this.datePipe.transform(value.end, 'dd.MM.yyyy')}`, + } + } else if (this.fieldName === 'user') { + return { value, label: formatUserInfo(value) } + } else { + return { value, label: value } + } + }) + } + + async removeFilterValue(fieldValue: FieldValue | DateRange) { + const currentFieldValues: DisplayedValue[] = await firstValueFrom( + this.fieldValues$ + ) + const updatedFieldValues = currentFieldValues + .filter( + (displayedValue: DisplayedValue) => displayedValue.value !== fieldValue + ) + .map((displayedValue: DisplayedValue) => displayedValue.value) + + this.fieldsService + .buildFiltersFromFieldValues({ + [this.fieldName]: updatedFieldValues as FieldValue[], + }) + .subscribe((filters) => this.searchService.updateFilters(filters)) + } +} diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.css b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html new file mode 100644 index 0000000000..289a9d9946 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.html @@ -0,0 +1,17 @@ +
+
+ +
+ +
diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.spec.ts b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.spec.ts new file mode 100644 index 0000000000..f5854b17c2 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.spec.ts @@ -0,0 +1,161 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { + FILTER_SUMMARY_IGNORE_LIST, + SearchFiltersSummaryComponent, +} from './search-filters-summary.component' +import { MockComponent, MockProvider } from 'ng-mocks' +import { SearchService } from '../utils/service/search.service' +import { SearchFacade } from '../state/search.facade' +import { BehaviorSubject, firstValueFrom } from 'rxjs' +import { TranslateModule } from '@ngx-translate/core' +import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' +import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' + +class SearchFacadeMock { + searchFilters$ = new BehaviorSubject({ + any: 'search should be ignored', + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': {}, + }) +} +class SearchServiceMock { + setFilters = jest.fn() +} + +describe('SearchFiltersSummaryComponent', () => { + let component: SearchFiltersSummaryComponent + let fixture: ComponentFixture + let searchFacade: SearchFacade + let searchService: SearchService + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ + SearchFiltersSummaryComponent, + MockComponent(SearchFiltersSummaryItemComponent), + ], + providers: [ + MockProvider(SearchFacade, SearchFacadeMock, 'useClass'), + MockProvider(SearchService, SearchServiceMock, 'useClass'), + ], + }) + }) + + describe('no injection token provided', () => { + beforeEach(async () => { + await TestBed.compileComponents() + fixture = TestBed.createComponent(SearchFiltersSummaryComponent) + searchFacade = TestBed.inject(SearchFacade) + searchService = TestBed.inject(SearchService) + component = fixture.componentInstance + fixture.detectChanges() + }) + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should set searchFilterActive$ observable to false for empty filters', async () => { + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeFalsy() + }) + + it('should set searchFilterActive$ observable to true for NON empty value filters', async () => { + const filters = { + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': { 'admin|admin|admin|Administrator': true }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeTruthy() + }) + + it('should set searchFilterActive$ observable to true for NON empty dateRange filters', async () => { + const filters = { + format: {}, + isSpatial: {}, + license: {}, + changeDate: { + start: new Date('2021-01-01'), + end: new Date('2021-01-02'), + }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeTruthy() + }) + + it('should clear filters', () => { + component.clearFilters() + expect(searchService.setFilters).toHaveBeenCalledWith({ + any: 'search should be ignored', + }) + }) + }) + + describe('FILTER_SUMMARY_IGNORE_LIST injection token provided', () => { + beforeEach(async () => { + TestBed.overrideProvider(FILTER_SUMMARY_IGNORE_LIST, { + useValue: ['owner'], + }) + await TestBed.compileComponents() + fixture = TestBed.createComponent(SearchFiltersSummaryComponent) + searchFacade = TestBed.inject(SearchFacade) + searchService = TestBed.inject(SearchService) + component = fixture.componentInstance + fixture.detectChanges() + }) + it('should ignore filters from FILTER_SUMMARY_IGNORE_LIST', async () => { + const filters = { + owner: { 1: true }, + isSpatial: {}, + license: {}, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeFalsy() + }) + it('should set searchFilterActive$ observable to true for NON empty value filters', async () => { + const filters = { + owner: { 1: true }, + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': { 'admin|admin|admin|Administrator': true }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + const isActive = await firstValueFrom(component.searchFilterActive$) + expect(isActive).toBeTruthy() + }) + it('should clear filters except with keys from FILTER_SUMMARY_IGNORE_LIST', () => { + const filters = { + owner: { 1: true }, + any: 'search should be ignored', + format: {}, + isSpatial: {}, + license: {}, + 'userInfo.keyword': { 'admin|admin|admin|Administrator': true }, + } + ;(searchFacade.searchFilters$ as BehaviorSubject).next( + filters + ) + component.clearFilters() + expect(searchService.setFilters).toHaveBeenCalledWith({ + owner: { 1: true }, + any: 'search should be ignored', + }) + }) + }) +}) diff --git a/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.ts b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.ts new file mode 100644 index 0000000000..5902567046 --- /dev/null +++ b/libs/feature/search/src/lib/search-filters-summary/search-filters-summary.component.ts @@ -0,0 +1,78 @@ +import { Component, Inject, Input, OnInit, Optional } from '@angular/core' +import { CommonModule } from '@angular/common' +import { first, map, Observable } from 'rxjs' +import { SearchFiltersSummaryItemComponent } from '../search-filters-summary-item/search-filters-summary-item.component' +import { TranslateModule } from '@ngx-translate/core' +import { SearchFacade } from '../state/search.facade' +import { SearchService } from '../utils/service/search.service' +import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' +import { InjectionToken } from '@angular/core' + +export const FILTER_SUMMARY_IGNORE_LIST = new InjectionToken( + 'FILTER_SUMMARY_IGNORE_LIST' +) +@Component({ + selector: 'gn-ui-search-filters-summary', + imports: [CommonModule, SearchFiltersSummaryItemComponent, TranslateModule], + templateUrl: './search-filters-summary.component.html', + styleUrls: ['./search-filters-summary.component.css'], + standalone: true, +}) +export class SearchFiltersSummaryComponent implements OnInit { + @Input() searchFields: string[] = [] + filterSummaryIgnoreList: string[] + + searchFilterActive$: Observable + + constructor( + private searchFacade: SearchFacade, + private searchService: SearchService, + @Optional() + @Inject(FILTER_SUMMARY_IGNORE_LIST) + filterSummaryIgnoreList: string[] + ) { + const defaultIgnoreList = ['any'] + this.filterSummaryIgnoreList = [ + ...defaultIgnoreList, + ...(filterSummaryIgnoreList ?? []), + ] + } + + ngOnInit(): void { + this.searchFilterActive$ = this.searchFacade.searchFilters$.pipe( + map((filters) => this.hasNonEmptyValues(filters)) + ) + } + + hasNonEmptyValues(filters: FieldFilters): boolean { + const filteredFilters = {} + for (const [key, value] of Object.entries(filters)) { + if (!this.filterSummaryIgnoreList.includes(key)) { + filteredFilters[key] = value + } + } + return Object.values(filteredFilters).some( + (value) => + value !== undefined && + (typeof value !== 'object' || + (typeof value === 'object' && Object.keys(value).length > 0)) + ) + } + + clearFilters() { + this.searchFacade.searchFilters$ + .pipe( + first(), + map((filters) => { + const newFilters = { ...filters } + Object.keys(newFilters).forEach((key) => { + if (!this.filterSummaryIgnoreList.includes(key)) { + delete newFilters[key] + } + }) + return newFilters + }) + ) + .subscribe((filters) => this.searchService.setFilters(filters)) + } +} diff --git a/libs/feature/search/src/lib/state/search.facade.ts b/libs/feature/search/src/lib/state/search.facade.ts index 44c9c34e1e..1c03fb0848 100644 --- a/libs/feature/search/src/lib/state/search.facade.ts +++ b/libs/feature/search/src/lib/state/search.facade.ts @@ -38,8 +38,6 @@ import { getSearchResultsLoading, getSearchSortBy, getSpatialFilterEnabled, - isBeginningOfResults, - isEndOfResults, totalPages, } from './selectors' import { FILTER_GEOMETRY } from '../filter-geometry.token' @@ -60,8 +58,6 @@ export class SearchFacade { layout$: Observable sortBy$: Observable isLoading$: Observable - isBeginningOfResults$: Observable - isEndOfResults$: Observable totalPages$: Observable currentPage$: Observable pageSize$: Observable @@ -101,10 +97,6 @@ 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)) this.pageSize$ = this.store.pipe(select(getPageSize, searchId)) diff --git a/libs/feature/search/src/lib/state/selectors.spec.ts b/libs/feature/search/src/lib/state/selectors.spec.ts index 4f610fc05d..210f3dd5ce 100644 --- a/libs/feature/search/src/lib/state/selectors.spec.ts +++ b/libs/feature/search/src/lib/state/selectors.spec.ts @@ -76,84 +76,6 @@ 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({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 0, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 62, - }, - }) - expect(result).toEqual(false) - - const endResult = fromSelectors.isEndOfResults.projector({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 3, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 62, - }, - }) - expect(endResult).toEqual(true) - - const exactEndOfResult = fromSelectors.isEndOfResults.projector({ - ...initialStateSearch, - params: { - ...initialStateSearch.params, - currentPage: 2, - pageSize: 20, - }, - results: { - ...initialStateSearch.results, - count: 60, - }, - }) - expect(exactEndOfResult).toEqual(true) - }) - }) - describe('totalPages', () => { it('returns correct page amount', () => { const result = fromSelectors.totalPages.projector({ diff --git a/libs/feature/search/src/lib/state/selectors.ts b/libs/feature/search/src/lib/state/selectors.ts index b2215ba3fc..338ee281cb 100644 --- a/libs/feature/search/src/lib/state/selectors.ts +++ b/libs/feature/search/src/lib/state/selectors.ts @@ -50,24 +50,6 @@ 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) => { - return ( - state.params.currentPage * state.params.pageSize + - state.params.pageSize >= - state.results.count - ) - } -) - export const totalPages = createSelector( getSearchStateSearch, (state: SearchStateSearch) => { diff --git a/libs/feature/search/src/lib/utils/service/fields.service.spec.ts b/libs/feature/search/src/lib/utils/service/fields.service.spec.ts index a973d029a8..fc9934c05b 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.spec.ts @@ -102,6 +102,7 @@ describe('FieldsService', () => { 'producerOrg', 'publisherOrg', 'user', + 'changeDate', ]) }) }) @@ -186,8 +187,17 @@ describe('FieldsService', () => { producerOrg: [], publisherOrg: [], user: [], + changeDate: [], }) }) }) + describe('#getFieldType', () => { + it('returns the field type', () => { + expect(service.getFieldType('organization')).toEqual('values') + expect(service.getFieldType('publicationYear')).toEqual('values') + expect(service.getFieldType('format')).toEqual('values') + expect(service.getFieldType('changeDate')).toEqual('dateRange') + }) + }) }) }) diff --git a/libs/feature/search/src/lib/utils/service/fields.service.ts b/libs/feature/search/src/lib/utils/service/fields.service.ts index 016b3162f4..d79ed22684 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -1,6 +1,7 @@ import { Injectable, Injector } from '@angular/core' import { AbstractSearchField, + DateRangeSearchField, FieldValue, FullTextSearchField, IsSpatialSearchField, @@ -16,9 +17,10 @@ import { forkJoin, Observable, of } from 'rxjs' import { map } from 'rxjs/operators' import { FieldFilters } from '@geonetwork-ui/common/domain/model/search' import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { DateRange } from '@geonetwork-ui/api/repository' // key is the field name -export type FieldValues = Record +export type FieldValues = Record marker('search.filters.format') marker('search.filters.inspireKeyword') @@ -35,6 +37,7 @@ marker('search.filters.contact') marker('search.filters.producerOrg') marker('search.filters.publisherOrg') marker('search.filters.user') +marker('search.filters.changeDate') @Injectable({ providedIn: 'root', @@ -87,6 +90,7 @@ export class FieldsService { 'key' ), user: new UserSearchField(this.injector), + changeDate: new DateRangeSearchField('changeDate', this.injector, 'desc'), } as Record get supportedFields() { @@ -101,13 +105,20 @@ export class FieldsService { return this.fields[fieldName].getAvailableValues() } - private getFiltersForValues(fieldName: string, values: FieldValue[]) { + private getFiltersForValues( + fieldName: string, + values: FieldValue[] | DateRange[] + ) { return this.fields[fieldName].getFiltersForValues(values) } private getValuesForFilters(fieldName: string, filters: FieldFilters) { return this.fields[fieldName].getValuesForFilter(filters) } + getFieldType(fieldName: string) { + return this.fields[fieldName].getType() + } + buildFiltersFromFieldValues( fieldValues: FieldValues ): Observable { @@ -119,7 +130,10 @@ export class FieldsService { const values = Array.isArray(fieldValues[fieldName]) ? fieldValues[fieldName] : [fieldValues[fieldName]] - return this.getFiltersForValues(fieldName, values as FieldValue[]) + return this.getFiltersForValues( + fieldName, + values as FieldValue[] | DateRange[] + ) }) return forkJoin(filtersByField$).pipe( map((filters) => diff --git a/libs/feature/search/src/lib/utils/service/fields.spec.ts b/libs/feature/search/src/lib/utils/service/fields.spec.ts index 39facadac2..4d8ba4138d 100644 --- a/libs/feature/search/src/lib/utils/service/fields.spec.ts +++ b/libs/feature/search/src/lib/utils/service/fields.spec.ts @@ -9,6 +9,7 @@ import { SimpleSearchField, MultilingualSearchField, UserSearchField, + DateRangeSearchField, } from './fields' import { TestBed } from '@angular/core/testing' import { Injector } from '@angular/core' @@ -274,10 +275,10 @@ describe('search fields implementations', () => { let filter beforeEach(async () => { filter = await lastValueFrom( - searchField.getFiltersForValues(['First value', 'Second value']) + searchField.getFiltersForValues(['First value', 'Second value', '']) ) }) - it('returns appropriate filters', () => { + it('returns appropriate filters (ignoring empty strings)', () => { expect(filter).toEqual({ myField: { 'First value': true, @@ -286,6 +287,17 @@ describe('search fields implementations', () => { }) }) }) + describe('#getFiltersForValues with empty value', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom(searchField.getFiltersForValues([''])) + }) + it('returns empty filter', () => { + expect(filter).toEqual({ + myField: {}, + }) + }) + }) describe('#getValuesForFilters', () => { let values describe('with several values', () => { @@ -330,6 +342,83 @@ describe('search fields implementations', () => { }) }) + describe('DateRangeSearchField (SimpleSearchField with date range)', () => { + beforeEach(() => { + searchField = new DateRangeSearchField('changeDate', injector, 'desc') + }) + describe('#getAvailableValues', () => { + let values + beforeEach(async () => { + values = await lastValueFrom(searchField.getAvailableValues()) + }) + it('returns an empty list of values for now', () => { + expect(values).toEqual([]) + }) + }) + describe('#getFiltersForValues', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom( + searchField.getFiltersForValues([ + { start: new Date('2020-01-01'), end: new Date('2020-12-31') }, + ]) + ) + }) + it('returns appropriate filters', () => { + expect(filter).toEqual({ + changeDate: { + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }, + }) + }) + }) + describe('#getFiltersForValues with empty value', () => { + let filter + beforeEach(async () => { + filter = await lastValueFrom(searchField.getFiltersForValues([''])) + }) + it('returns empty filter', () => { + expect(filter).toEqual({ + changeDate: {}, + }) + }) + }) + describe('#getValuesForFilters', () => { + let values + describe('with several values', () => { + beforeEach(async () => { + values = await lastValueFrom( + searchField.getValuesForFilter({ + changeDate: { + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }, + }) + ) + }) + it('returns filtered values', () => { + expect(values).toEqual({ + start: new Date('2020-01-01'), + end: new Date('2020-12-31'), + }) + }) + }) + describe('with a unique value', () => { + beforeEach(async () => { + values = await lastValueFrom( + searchField.getValuesForFilter({ + changeDate: { start: new Date('2020-01-01') }, + }) + ) + }) + it('returns the only value', () => { + expect(values).toEqual({ start: new Date('2020-01-01') }) + }) + }) + }) + }) + describe('TranslatedSearchField', () => { describe('sort by key', () => { beforeEach(() => { diff --git a/libs/feature/search/src/lib/utils/service/fields.ts b/libs/feature/search/src/lib/utils/service/fields.ts index 4a377ec969..1d8422ba03 100644 --- a/libs/feature/search/src/lib/utils/service/fields.ts +++ b/libs/feature/search/src/lib/utils/service/fields.ts @@ -14,10 +14,15 @@ import { TermBucket, } from '@geonetwork-ui/common/domain/model/search' import { + DateRange, ElasticsearchService, + isDateRange, METADATA_LANGUAGE, } from '@geonetwork-ui/api/repository' import { LangService } from '@geonetwork-ui/util/i18n' +import { formatUserInfo } from '@geonetwork-ui/util/shared' + +export type FieldType = 'values' | 'dateRange' export type FieldValue = string | number export interface FieldAvailableValue { @@ -26,9 +31,14 @@ export interface FieldAvailableValue { } export abstract class AbstractSearchField { - abstract getAvailableValues(): Observable - abstract getFiltersForValues(values: FieldValue[]): Observable - abstract getValuesForFilter(filters: FieldFilters): Observable + abstract getAvailableValues(): Observable + abstract getFiltersForValues( + values: FieldValue[] | DateRange[] + ): Observable + abstract getValuesForFilter( + filters: FieldFilters + ): Observable + abstract getType(): FieldType } export class SimpleSearchField implements AbstractSearchField { @@ -74,22 +84,47 @@ export class SimpleSearchField implements AbstractSearchField { }) ) } - getFiltersForValues(values: FieldValue[]): Observable { + getFiltersForValues( + values: FieldValue[] | DateRange[] + ): Observable { + // FieldValue[] + if (this.getType() === 'values') { + return of({ + [this.esFieldName]: (values as FieldValue[]).reduce((acc, val) => { + const value = val.toString() + if (value !== '') { + return { ...acc, [value]: true } + } + return acc + }, {}), + }) + } + // DateRange return of({ - [this.esFieldName]: values.reduce((acc, val) => { - return { ...acc, [val.toString()]: true } - }, {}), + [this.esFieldName]: values[0] !== '' ? values[0] : {}, }) } - getValuesForFilter(filters: FieldFilters): Observable { + getValuesForFilter( + filters: FieldFilters + ): Observable { const filter = filters[this.esFieldName] if (!filter) return of([]) - const values = - typeof filter === 'string' - ? [filter] - : Object.keys(filter).filter((v) => filter[v]) + // filter by expression + if (typeof filter === 'string') { + return of([filter]) + } + // filter by date range + if (isDateRange(filter)) { + return of(filter) + } + // filter by values + const values = Object.keys(filter).filter((v) => filter[v]) return of(values) } + + getType(): FieldType { + return 'values' + } } export class TranslatedSearchField extends SimpleSearchField { @@ -163,6 +198,9 @@ export class FullTextSearchField implements AbstractSearchField { getValuesForFilter(filters: FieldFilters): Observable { return of(filters.any ? [filters.any as FieldFilterByExpression] : []) } + getType(): FieldType { + return 'values' + } } marker('search.filters.isSpatial.yes') @@ -336,6 +374,10 @@ export class OrganizationSearchField implements AbstractSearchField { ) ) } + + getType(): FieldType { + return 'values' + } } export class OwnerSearchField extends SimpleSearchField { constructor(injector: Injector) { @@ -357,18 +399,29 @@ export class UserSearchField extends SimpleSearchField { map((values) => values.map((v) => ({ ...v, - label: this.formatUserInfo(v.label), + label: formatUserInfo(v.label, true), })) ) ) } +} - private formatUserInfo(userInfo: string | unknown): string { - const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') - const count = infos[3].split(' ')[1] - if (infos && infos.length === 4) { - return `${infos[2]} ${infos[1]} ${count}` - } - return undefined +export class DateRangeSearchField extends SimpleSearchField { + constructor( + protected esFieldName: string, + protected injector: Injector, + protected order: 'asc' | 'desc' = 'asc', + protected orderType: 'key' | 'count' = 'key' + ) { + super(esFieldName, injector, order, orderType) + } + + getAvailableValues(): Observable { + // TODO: return an array of dates to show which one are available in the date picker + return of([]) + } + + getType(): FieldType { + return 'dateRange' } } diff --git a/libs/ui/catalog/src/index.ts b/libs/ui/catalog/src/index.ts index 6e32ac7e87..ed6b54668a 100644 --- a/libs/ui/catalog/src/index.ts +++ b/libs/ui/catalog/src/index.ts @@ -1,4 +1,3 @@ -export * from './lib/ui-catalog.module' export * from './lib/catalog-title/catalog-title.component' export * from './lib/language-switcher/language-switcher.component' export * from './lib/organisation-preview/organisation-preview.component' diff --git a/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.spec.ts b/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.spec.ts index 113dc6414f..009ac7b953 100644 --- a/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.spec.ts +++ b/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.spec.ts @@ -1,16 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { CatalogTitleComponent } from './catalog-title.component' +import { MockBuilder } from 'ng-mocks' describe('CatalogTitleComponent', () => { let component: CatalogTitleComponent let fixture: ComponentFixture - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [CatalogTitleComponent], - }).compileComponents() - }) + beforeEach(() => MockBuilder(CatalogTitleComponent)) beforeEach(() => { fixture = TestBed.createComponent(CatalogTitleComponent) diff --git a/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.ts b/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.ts index 73726780d6..a567c779f8 100644 --- a/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.ts +++ b/libs/ui/catalog/src/lib/catalog-title/catalog-title.component.ts @@ -1,9 +1,12 @@ import { Component, Input } from '@angular/core' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-catalog-title', templateUrl: './catalog-title.component.html', styleUrls: ['./catalog-title.component.css'], + standalone: true, + imports: [CommonModule], }) export class CatalogTitleComponent { @Input() name: string 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 38fb3d77e8..a1458744e7 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 @@ -2,10 +2,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { LANGUAGE_STORAGE_KEY } from '@geonetwork-ui/util/i18n' import { TranslateService } from '@ngx-translate/core' import { LanguageSwitcherComponent } from './language-switcher.component' +import { MockBuilder } from 'ng-mocks' class TranslateServiceMock { use = jest.fn() currentLang = 'en' + get = jest.fn() } window.console.warn = jest.fn() @@ -15,9 +17,10 @@ describe('LanguageSwitcherComponent', () => { let fixture: ComponentFixture let service: TranslateService + beforeEach(() => MockBuilder(LanguageSwitcherComponent)) + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [LanguageSwitcherComponent], providers: [ { provide: TranslateService, diff --git a/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts b/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts index 0c103ac287..d28c36cbfb 100644 --- a/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +++ b/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts @@ -1,6 +1,7 @@ import { Component, Inject, InjectionToken, Optional } from '@angular/core' import { LANGUAGE_STORAGE_KEY } from '@geonetwork-ui/util/i18n' import { TranslateService } from '@ngx-translate/core' +import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs' export const LANGUAGES_LIST = new InjectionToken('languages-list') @@ -10,6 +11,8 @@ const DEFAULT_LANGUAGES = ['en', 'fr'] selector: 'gn-ui-language-switcher', templateUrl: './language-switcher.component.html', styleUrls: ['./language-switcher.component.css'], + imports: [DropdownSelectorComponent], + standalone: true, }) export class LanguageSwitcherComponent { languageChoices = (this.languagesList || DEFAULT_LANGUAGES).map( 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 048255d6b0..8883bc32d0 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 @@ -1,21 +1,12 @@ -import { Component, Input } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { OrganisationPreviewComponent } from './organisation-preview.component' - -@Component({ - selector: 'gn-ui-thumbnail', - template: '
', -}) -class RecordThumbnailMockComponent { - @Input() thumbnailUrl: string - @Input() fit: string -} +import { MockBuilder } from 'ng-mocks' const organisationMock = { name: 'my org', description: 'not much', - logoUrl: 'https://mygreatlogo.org', + logoUrl: new URL('https://mygreatlogo.org'), recordCount: 10, } @@ -23,14 +14,9 @@ describe('OrganisationPreviewComponent', () => { let component: OrganisationPreviewComponent let fixture: ComponentFixture - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ - OrganisationPreviewComponent, - RecordThumbnailMockComponent, - ], - }).compileComponents() + beforeEach(() => MockBuilder(OrganisationPreviewComponent)) + beforeEach(() => { fixture = TestBed.createComponent(OrganisationPreviewComponent) component = fixture.componentInstance component.organization = organisationMock 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 35702801fa..6a82644224 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 @@ -12,7 +12,6 @@ import { UtilI18nModule, } from '@geonetwork-ui/util/i18n' import { UtilSharedModule } from '@geonetwork-ui/util/shared' -import { ThumbnailComponent } from '@geonetwork-ui/ui/elements' import { OrganisationPreviewComponent } from './organisation-preview.component' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { importProvidersFrom } from '@angular/core' @@ -28,7 +27,6 @@ export default { ], }), moduleMetadata({ - declarations: [ThumbnailComponent], imports: [ UtilI18nModule, UtilSharedModule, 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 c8eae909ce..4bd51aef1c 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 @@ -6,12 +6,23 @@ import { Output, } from '@angular/core' import { Organization } from '@geonetwork-ui/common/domain/model/record' +import { ThumbnailComponent } from '@geonetwork-ui/ui/elements' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { tablerFolderOpen } from '@ng-icons/tabler-icons' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-organisation-preview', templateUrl: './organisation-preview.component.html', styleUrls: ['./organisation-preview.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ThumbnailComponent, NgIcon, TranslateModule], + viewProviders: [ + provideIcons({ + tablerFolderOpen, + }), + ], + standalone: true, }) export class OrganisationPreviewComponent { @Input() organization: Organization diff --git a/libs/ui/catalog/src/lib/organisations-filter/organisations-filter.component.stories.ts b/libs/ui/catalog/src/lib/organisations-filter/organisations-filter.component.stories.ts index 62adb35991..9dd32b8639 100644 --- a/libs/ui/catalog/src/lib/organisations-filter/organisations-filter.component.stories.ts +++ b/libs/ui/catalog/src/lib/organisations-filter/organisations-filter.component.stories.ts @@ -10,14 +10,12 @@ import { UtilI18nModule, } from '@geonetwork-ui/util/i18n' import { OrganisationsFilterComponent } from './organisations-filter.component' -import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs' export default { title: 'Catalog/OrganisationsFilterComponent', component: OrganisationsFilterComponent, decorators: [ moduleMetadata({ - declarations: [DropdownSelectorComponent], imports: [ UtilI18nModule, TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), diff --git a/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.spec.ts b/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.spec.ts index 4c9a6314a7..69629bc20d 100644 --- a/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.spec.ts +++ b/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.spec.ts @@ -1,15 +1,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { OrganisationsResultComponent } from './organisations-result.component' +import { MockBuilder } from 'ng-mocks' describe('OrganisationsResultComponent', () => { let component: OrganisationsResultComponent let fixture: ComponentFixture + beforeEach(() => MockBuilder(OrganisationsResultComponent)) + beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [OrganisationsResultComponent], - }) fixture = TestBed.createComponent(OrganisationsResultComponent) component = fixture.componentInstance fixture.detectChanges() diff --git a/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.ts b/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.ts index 3ed741624b..d98b4d1c90 100644 --- a/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.ts +++ b/libs/ui/catalog/src/lib/organisations-result/organisations-result.component.ts @@ -1,9 +1,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-organisations-result', templateUrl: './organisations-result.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TranslateModule], + standalone: true, }) export class OrganisationsResultComponent { @Input() hits: number diff --git a/libs/ui/catalog/src/lib/ui-catalog.module.ts b/libs/ui/catalog/src/lib/ui-catalog.module.ts deleted file mode 100644 index d5ed56289e..0000000000 --- a/libs/ui/catalog/src/lib/ui-catalog.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { CatalogTitleComponent } from './catalog-title/catalog-title.component' -import { OrganisationPreviewComponent } from './organisation-preview/organisation-preview.component' -import { TranslateModule } from '@ngx-translate/core' -import { UiElementsModule } from '@geonetwork-ui/ui/elements' -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' -import { NgIconsModule, provideNgIconsConfig } from '@ng-icons/core' -import { tablerFolderOpen } from '@ng-icons/tabler-icons' -@NgModule({ - declarations: [ - CatalogTitleComponent, - OrganisationPreviewComponent, - LanguageSwitcherComponent, - OrganisationsResultComponent, - ], - imports: [ - CommonModule, - TranslateModule.forChild(), - UiElementsModule, - UiInputsModule, - RouterLink, - // FIXME: these imports are required by non-standalone components and should be removed once all components have been made standalone - NgIconsModule.withIcons({ - tablerFolderOpen, - }), - ], - providers: [ - provideNgIconsConfig({ - size: '1.5em', - }), - ], - exports: [ - CatalogTitleComponent, - OrganisationPreviewComponent, - LanguageSwitcherComponent, - OrganisationsResultComponent, - ], -}) -export class UiCatalogModule {} 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 ac896b98ff..adaef407eb 100644 --- a/libs/ui/dataviz/src/lib/figure/figure.component.spec.ts +++ b/libs/ui/dataviz/src/lib/figure/figure.component.spec.ts @@ -50,7 +50,6 @@ describe('FigureComponent', () => { ) }) it('should render icon', () => { - console.log(compiled.querySelector('ng-icon')) const icon = compiled.querySelector('ng-icon') as HTMLElement expect(icon['name']).toContain('group') }) diff --git a/libs/ui/dataviz/src/lib/figure/figure.component.stories.ts b/libs/ui/dataviz/src/lib/figure/figure.component.stories.ts index 0f701413c2..6e805a5eb6 100644 --- a/libs/ui/dataviz/src/lib/figure/figure.component.stories.ts +++ b/libs/ui/dataviz/src/lib/figure/figure.component.stories.ts @@ -9,6 +9,8 @@ import { FigureComponent } from './figure.component' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { UiDatavizModule } from '../ui-dataviz.module' import { importProvidersFrom } from '@angular/core' +import { TranslateModule } from '@ngx-translate/core' +import { TRANSLATE_DEFAULT_CONFIG } from '@geonetwork-ui/util/i18n' export default { title: 'Dataviz/FigureComponent', @@ -18,7 +20,10 @@ export default { imports: [UiDatavizModule], }), applicationConfig({ - providers: [importProvidersFrom(BrowserAnimationsModule)], + providers: [ + importProvidersFrom(BrowserAnimationsModule), + importProvidersFrom(TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG)), + ], }), componentWrapperDecorator( (story) => ` diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 1e62e06968..7d8c2c6537 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -15,8 +15,6 @@ 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/thumbnail/thumbnail.component' diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.html b/libs/ui/elements/src/lib/api-card/api-card.component.html index ac15a06bf9..91110e5875 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.html +++ b/libs/ui/elements/src/lib/api-card/api-card.component.html @@ -39,7 +39,6 @@ 'py-2 px-4 rounded-r-md bg-gray-400 hover:bg-gray-600 focus:bg-gray-800 text-white': displayText }" - mat-raised-button [matTooltip]=" !currentlyActive ? ('record.metadata.api.form.openForm' | translate) diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts b/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts index 2da1df059b..0ae0e05a18 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts +++ b/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts @@ -9,8 +9,7 @@ describe('ApiCardComponent', () => { let openRecordApiFormEmit beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ApiCardComponent], - imports: [TranslateModule.forRoot()], + imports: [ApiCardComponent, TranslateModule.forRoot()], }).compileComponents() }) diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.stories.ts b/libs/ui/elements/src/lib/api-card/api-card.component.stories.ts index afcd18a5f5..61d1442b05 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.stories.ts +++ b/libs/ui/elements/src/lib/api-card/api-card.component.stories.ts @@ -12,14 +12,12 @@ import { } from '@storybook/angular' import { ApiCardComponent } from './api-card.component' import { MatTooltipModule } from '@angular/material/tooltip' -import { CopyTextButtonComponent } from '@geonetwork-ui/ui/libs/copy-text-button' export default { title: 'Elements/ApiCardComponent', component: ApiCardComponent, decorators: [ moduleMetadata({ - declarations: [CopyTextButtonComponent], imports: [ UtilI18nModule, TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.ts b/libs/ui/elements/src/lib/api-card/api-card.component.ts index abc1776fef..9ad9703ce4 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.ts +++ b/libs/ui/elements/src/lib/api-card/api-card.component.ts @@ -9,12 +9,31 @@ import { Output, SimpleChanges, } from '@angular/core' +import { CommonModule } from '@angular/common' +import { CopyTextButtonComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' +import { MatTooltipModule } from '@angular/material/tooltip' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { matMoreHoriz } from '@ng-icons/material-icons/baseline' @Component({ selector: 'gn-ui-api-card', templateUrl: './api-card.component.html', styleUrls: ['./api-card.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + CopyTextButtonComponent, + TranslateModule, + MatTooltipModule, + NgIcon, + ], + viewProviders: [ + provideIcons({ + matMoreHoriz, + }), + ], }) export class ApiCardComponent implements OnInit, OnChanges { @Input() link: DatasetServiceDistribution diff --git a/libs/ui/elements/src/lib/content-ghost/content-ghost.component.spec.ts b/libs/ui/elements/src/lib/content-ghost/content-ghost.component.spec.ts index ea3f4da05b..0d86654741 100644 --- a/libs/ui/elements/src/lib/content-ghost/content-ghost.component.spec.ts +++ b/libs/ui/elements/src/lib/content-ghost/content-ghost.component.spec.ts @@ -8,7 +8,7 @@ describe('ContentGhostComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ContentGhostComponent], + imports: [ContentGhostComponent], }).compileComponents() }) diff --git a/libs/ui/elements/src/lib/content-ghost/content-ghost.component.ts b/libs/ui/elements/src/lib/content-ghost/content-ghost.component.ts index 6421532701..c5375c773b 100644 --- a/libs/ui/elements/src/lib/content-ghost/content-ghost.component.ts +++ b/libs/ui/elements/src/lib/content-ghost/content-ghost.component.ts @@ -1,10 +1,13 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-content-ghost', templateUrl: './content-ghost.component.html', styleUrls: ['./content-ghost.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule], }) export class ContentGhostComponent { @Input() showContent: boolean diff --git a/libs/ui/elements/src/lib/download-item/download-item.component.spec.ts b/libs/ui/elements/src/lib/download-item/download-item.component.spec.ts index ab3d651181..cdb6ac203c 100644 --- a/libs/ui/elements/src/lib/download-item/download-item.component.spec.ts +++ b/libs/ui/elements/src/lib/download-item/download-item.component.spec.ts @@ -10,8 +10,7 @@ describe('DownloadsListItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [DownloadItemComponent], - imports: [TranslateModule.forRoot()], + imports: [DownloadItemComponent, TranslateModule.forRoot()], }) .overrideComponent(DownloadItemComponent, { set: { changeDetection: ChangeDetectionStrategy.Default }, diff --git a/libs/ui/elements/src/lib/download-item/download-item.component.ts b/libs/ui/elements/src/lib/download-item/download-item.component.ts index 9fdac82648..64768ffb6b 100644 --- a/libs/ui/elements/src/lib/download-item/download-item.component.ts +++ b/libs/ui/elements/src/lib/download-item/download-item.component.ts @@ -6,12 +6,23 @@ import { Output, } from '@angular/core' import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' +import { TranslateModule } from '@ngx-translate/core' +import { CommonModule } from '@angular/common' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { matCloudDownloadOutline } from '@ng-icons/material-icons/outline' @Component({ selector: 'gn-ui-download-item', templateUrl: './download-item.component.html', styleUrls: ['./download-item.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, TranslateModule, NgIcon], + standalone: true, + viewProviders: [ + provideIcons({ + matCloudDownloadOutline, + }), + ], }) export class DownloadItemComponent { @Input() link: DatasetOnlineResource diff --git a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts index 323c29eab9..d3e9da48d7 100644 --- a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts +++ b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts @@ -1,8 +1,6 @@ import { ChangeDetectionStrategy, - Component, DebugElement, - Input, NO_ERRORS_SCHEMA, } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' @@ -11,21 +9,8 @@ import { LinkClassifierService } from '@geonetwork-ui/util/shared' import { aSetOfLinksFixture } from '@geonetwork-ui/common/fixtures' import { TranslateModule } from '@ngx-translate/core' import { DownloadsListComponent } from './downloads-list.component' -import { - DatasetDownloadDistribution, - DatasetOnlineResource, -} from '@geonetwork-ui/common/domain/model/record' - -@Component({ - selector: 'gn-ui-download-item', - template: ``, -}) -class MockDownloadItemComponent { - @Input() link: DatasetOnlineResource - @Input() color: string - @Input() format: string - @Input() isFromWfs: boolean -} +import { DatasetDownloadDistribution } from '@geonetwork-ui/common/domain/model/record' +import { DownloadItemComponent } from '../download-item/download-item.component' describe('DownloadsListComponent', () => { let component: DownloadsListComponent @@ -34,13 +19,14 @@ describe('DownloadsListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [DownloadsListComponent, MockDownloadItemComponent], + imports: [TranslateModule.forRoot(), DownloadsListComponent], schemas: [NO_ERRORS_SCHEMA], providers: [LinkClassifierService], }) .overrideComponent(DownloadsListComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, + set: { + changeDetection: ChangeDetectionStrategy.Default, + }, }) .compileComponents() }) @@ -67,7 +53,7 @@ describe('DownloadsListComponent', () => { aSetOfLinksFixture().dataPdf(), ] fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) + items = de.queryAll(By.directive(DownloadItemComponent)) }) it('contains three links', () => { expect(items.length).toBe(3) @@ -93,7 +79,7 @@ describe('DownloadsListComponent', () => { beforeEach(() => { component.links = [aSetOfLinksFixture().unknownFormat()] fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) + items = de.queryAll(By.directive(DownloadItemComponent)) }) it('contains one link in "others" section', () => { expect(items.length).toBe(1) @@ -111,7 +97,7 @@ describe('DownloadsListComponent', () => { } as DatasetDownloadDistribution, ] fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) + items = de.queryAll(By.directive(DownloadItemComponent)) }) it('contains one link and mime type is ignored', () => { expect(items.length).toBe(1) @@ -124,7 +110,7 @@ describe('DownloadsListComponent', () => { beforeEach(() => { component.links = [aSetOfLinksFixture().geodataShpWithMimeType()] fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) + items = de.queryAll(By.directive(DownloadItemComponent)) }) it('contains color, isWfs & format', () => { expect(items.length).toBe(1) @@ -144,7 +130,7 @@ describe('DownloadsListComponent', () => { beforeEach(() => { component.links = [aSetOfLinksFixture().geodataWfsDownload()] fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) + items = de.queryAll(By.directive(DownloadItemComponent)) }) it('sets isFromWfs to true', () => { expect(items[0].componentInstance.isFromWfs).toEqual(true) diff --git a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts index 5a5aae0c9e..ca9f61cfec 100644 --- a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts +++ b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts @@ -1,8 +1,11 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' import { marker } from '@biesbjerg/ngx-translate-extract-marker' import { getBadgeColor, getFileFormat } from '@geonetwork-ui/util/shared' import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' +import { CommonModule } from '@angular/common' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { DownloadItemComponent } from '../download-item/download-item.component' marker('datahub.search.filter.all') marker('datahub.search.filter.others') @@ -15,6 +18,13 @@ type FilterFormat = typeof FILTER_FORMATS[number] templateUrl: './downloads-list.component.html', styleUrls: ['./downloads-list.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + ButtonComponent, + DownloadItemComponent, + TranslateModule, + ], }) export class DownloadsListComponent { constructor(private translateService: TranslateService) {} diff --git a/libs/ui/elements/src/lib/error/error.component.spec.ts b/libs/ui/elements/src/lib/error/error.component.spec.ts index e0bf10b72b..1499ea5460 100644 --- a/libs/ui/elements/src/lib/error/error.component.spec.ts +++ b/libs/ui/elements/src/lib/error/error.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { ErrorComponent, ErrorType } from './error.component' import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core' import { By } from '@angular/platform-browser' +import { TranslateModule } from '@ngx-translate/core' describe('ErrorComponent', () => { let component: ErrorComponent @@ -11,7 +12,7 @@ describe('ErrorComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ErrorComponent], + imports: [ErrorComponent, TranslateModule.forRoot()], schemas: [NO_ERRORS_SCHEMA], }).compileComponents() }) diff --git a/libs/ui/elements/src/lib/error/error.component.ts b/libs/ui/elements/src/lib/error/error.component.ts index a7b79425b0..6b0492d0e1 100644 --- a/libs/ui/elements/src/lib/error/error.component.ts +++ b/libs/ui/elements/src/lib/error/error.component.ts @@ -1,4 +1,13 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { + matFace, + matMoodBad, + matQuestionMark, +} from '@ng-icons/material-icons/baseline' +import { matComputerOutline } from '@ng-icons/material-icons/outline' +import { TranslateModule } from '@ngx-translate/core' export enum ErrorType { COULD_NOT_REACH_API, @@ -14,6 +23,16 @@ export enum ErrorType { templateUrl: './error.component.html', styleUrls: ['./error.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, NgIcon, TranslateModule], + viewProviders: [ + provideIcons({ + matFace, + matQuestionMark, + matMoodBad, + matComputerOutline, + }), + ], }) export class ErrorComponent { @Input() type!: ErrorType diff --git a/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html b/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html index 8a5ad0c93d..3afe7e675f 100644 --- a/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +++ b/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html @@ -4,9 +4,8 @@ >
{ beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ImageOverlayPreviewComponent], + imports: [ImageOverlayPreviewComponent], }) fixture = TestBed.createComponent(ImageOverlayPreviewComponent) component = fixture.componentInstance diff --git a/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts b/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts index c7f24db928..dacf6ad520 100644 --- a/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +++ b/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts @@ -1,10 +1,25 @@ import { Component, EventEmitter, Input, Output } from '@angular/core' import * as basicLightbox from 'basiclightbox' +import { ContentGhostComponent } from '../content-ghost/content-ghost.component' +import { ThumbnailComponent } from '../thumbnail/thumbnail.component' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { matZoomOutMap } from '@ng-icons/material-icons/baseline' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-image-overlay-preview', templateUrl: './image-overlay-preview.component.html', styleUrls: ['./image-overlay-preview.component.css'], + standalone: true, + imports: [ + CommonModule, + ContentGhostComponent, + ThumbnailComponent, + ButtonComponent, + NgIcon, + ], + viewProviders: [provideIcons({ matZoomOutMap })], }) export class ImageOverlayPreviewComponent { @Input() imageUrl: string diff --git a/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.spec.ts b/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.spec.ts index 975d690f34..6731bb9fb8 100644 --- a/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.spec.ts +++ b/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { MetadataCatalogComponent } from './metadata-catalog.component' +import { TranslateModule } from '@ngx-translate/core' describe('MetadataCatalogComponent', () => { let component: MetadataCatalogComponent @@ -9,7 +10,7 @@ describe('MetadataCatalogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [MetadataCatalogComponent], + imports: [MetadataCatalogComponent, TranslateModule.forRoot()], }).compileComponents() }) diff --git a/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.ts b/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.ts index b3e3ffdc34..6d8b2e6a81 100644 --- a/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.ts +++ b/libs/ui/elements/src/lib/metadata-catalog/metadata-catalog.component.ts @@ -1,10 +1,13 @@ -import { Component, ChangeDetectionStrategy, Input } from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-metadata-catalog', templateUrl: './metadata-catalog.component.html', styleUrls: ['./metadata-catalog.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [TranslateModule], }) export class MetadataCatalogComponent { @Input() sourceLabel: string diff --git a/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.spec.ts b/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.spec.ts index 29b27ac1de..e81d468ac6 100644 --- a/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.spec.ts +++ b/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.spec.ts @@ -2,6 +2,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { MetadataContactComponent } from './metadata-contact.component' +import { TranslateModule } from '@ngx-translate/core' describe('MetadataContactComponent', () => { let component: MetadataContactComponent @@ -9,7 +10,7 @@ describe('MetadataContactComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [MetadataContactComponent], + imports: [MetadataContactComponent, TranslateModule.forRoot()], schemas: [NO_ERRORS_SCHEMA], }).compileComponents() }) diff --git a/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts b/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts index 38ace97c9a..fbfbf96564 100644 --- a/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +++ b/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts @@ -10,12 +10,36 @@ import { Individual, Organization, } from '@geonetwork-ui/common/domain/model/record' +import { ThumbnailComponent } from '../thumbnail/thumbnail.component' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { + matMailOutline, + matOpenInNew, + matPersonOutline, +} from '@ng-icons/material-icons/baseline' +import { + matCallOutline, + matLocationOnOutline, +} from '@ng-icons/material-icons/outline' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-metadata-contact', templateUrl: './metadata-contact.component.html', styleUrls: ['./metadata-contact.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, ThumbnailComponent, NgIcon, TranslateModule], + viewProviders: [ + provideIcons({ + matOpenInNew, + matCallOutline, + matMailOutline, + matPersonOutline, + matLocationOnOutline, + }), + ], }) export class MetadataContactComponent { @Input() metadata: Partial diff --git a/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts b/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts index 0cc2edc61f..8f2cd4cab0 100644 --- a/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts +++ b/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts @@ -1,7 +1,8 @@ -import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { Component, DebugElement } from '@angular/core' import { By } from '@angular/platform-browser' import { GnUiLinkifyDirective } from './linkify.directive' +import { CommonModule } from '@angular/common' const testingUrls = [ ['First link http://bla.org no slash', 'http://bla.org'], @@ -81,6 +82,8 @@ const testWithHTML = {
{{ text }}
`, + standalone: true, + imports: [CommonModule, GnUiLinkifyDirective], }) class TestComponent { text = '' @@ -94,7 +97,7 @@ describe('GnUiLinkifyDirective', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [GnUiLinkifyDirective, TestComponent], + imports: [GnUiLinkifyDirective, TestComponent], }) fixture = TestBed.createComponent(TestComponent) diff --git a/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts b/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts index 2d55fd76ce..d6e4484bfd 100644 --- a/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts +++ b/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts @@ -5,7 +5,7 @@ export default { title: 'Elements/GnUiLinkifyDirective', decorators: [ moduleMetadata({ - declarations: [GnUiLinkifyDirective], + imports: [GnUiLinkifyDirective], }), ], } as Meta diff --git a/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts b/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts index a25a826f62..355b1ce521 100644 --- a/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts +++ b/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts @@ -1,8 +1,9 @@ /* eslint-disable @angular-eslint/directive-selector */ -import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core' +import { Directive, ElementRef, OnInit, Renderer2 } from '@angular/core' @Directive({ selector: '[gnUiLinkify]', + standalone: true, }) export class GnUiLinkifyDirective implements OnInit { constructor(private el: ElementRef, private renderer: Renderer2) {} diff --git a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.spec.ts b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.spec.ts index 655ce4f6ff..87f80cc0a8 100644 --- a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.spec.ts +++ b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.spec.ts @@ -1,7 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { TranslateModule } from '@ngx-translate/core' -import { ContentGhostComponent } from '../content-ghost/content-ghost.component' import { MetadataInfoComponent } from './metadata-info.component' import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures' import { TranslateTestingModule } from 'ngx-translate-testing' @@ -26,8 +25,8 @@ describe('MetadataInfoComponent', () => { }) .withDefaultLanguage('en') .withCompiler(new TranslateMessageFormatCompiler()), + MetadataInfoComponent, ], - declarations: [MetadataInfoComponent, ContentGhostComponent], }).compileComponents() }) diff --git a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.stories.ts b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.stories.ts index 9c9a2032f3..9846fc6863 100644 --- a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.stories.ts +++ b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.stories.ts @@ -11,7 +11,6 @@ import { } from '@storybook/angular' import { MetadataInfoComponent } from './metadata-info.component' import { UtilSharedModule } from '@geonetwork-ui/util/shared' -import { ContentGhostComponent } from '../content-ghost/content-ghost.component' import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures' import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record' @@ -20,7 +19,6 @@ export default { component: MetadataInfoComponent, decorators: [ moduleMetadata({ - declarations: [ContentGhostComponent], imports: [ UtilI18nModule, TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), diff --git a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts index 24a7dd3bd4..0fc4dbb6cc 100644 --- a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +++ b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts @@ -10,12 +10,49 @@ import { Keyword, } from '@geonetwork-ui/common/domain/model/record' import { getTemporalRangeUnion } from '@geonetwork-ui/util/shared' +import { MarkdownParserComponent } from '../markdown-parser/markdown-parser.component' +import { + ExpandablePanelComponent, + MaxLinesComponent, +} from '@geonetwork-ui/ui/layout' +import { TranslateModule } from '@ngx-translate/core' +import { + BadgeComponent, + CopyTextButtonComponent, +} from '@geonetwork-ui/ui/inputs' +import { ContentGhostComponent } from '../content-ghost/content-ghost.component' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { matOpenInNew } from '@ng-icons/material-icons/baseline' +import { matMailOutline } from '@ng-icons/material-icons/outline' +import { ThumbnailComponent } from '../thumbnail/thumbnail.component' +import { GnUiLinkifyDirective } from './linkify.directive' @Component({ selector: 'gn-ui-metadata-info', templateUrl: './metadata-info.component.html', styleUrls: ['./metadata-info.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + TranslateModule, + MarkdownParserComponent, + ExpandablePanelComponent, + BadgeComponent, + ContentGhostComponent, + ThumbnailComponent, + MaxLinesComponent, + CopyTextButtonComponent, + NgIcon, + GnUiLinkifyDirective, + ], + viewProviders: [ + provideIcons({ + matOpenInNew, + matMailOutline, + }), + ], }) export class MetadataInfoComponent { @Input() metadata: Partial diff --git a/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.spec.ts b/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.spec.ts index 8e7b1e4036..3d069262fb 100644 --- a/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.spec.ts +++ b/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.spec.ts @@ -1,10 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { UtilI18nModule } from '@geonetwork-ui/util/i18n' -import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { TranslateModule } from '@ngx-translate/core' import { MetadataQualityItemComponent } from './metadata-quality-item.component' import { By } from '@angular/platform-browser' -import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy } from '@angular/core' describe('MetadataQualityInfoComponent', () => { let component: MetadataQualityItemComponent @@ -12,14 +11,16 @@ describe('MetadataQualityInfoComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [MetadataQualityItemComponent], imports: [ - UtilSharedModule, - CommonModule, + MetadataQualityItemComponent, UtilI18nModule, TranslateModule.forRoot(), ], - }).compileComponents() + }) + .overrideComponent(MetadataQualityItemComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents() }) beforeEach(() => { @@ -36,8 +37,7 @@ describe('MetadataQualityInfoComponent', () => { component.value = true fixture.detectChanges() - const iconElement = fixture.nativeElement.querySelector('ng-icon') - expect(iconElement.name).toBe('matCheck') + expect(component.icon).toBe('matCheck') const textElement = fixture.debugElement.query(By.css('.text')) expect(textElement.nativeElement.innerHTML).toBe( @@ -50,8 +50,7 @@ describe('MetadataQualityInfoComponent', () => { component.value = false fixture.detectChanges() - const iconElement = fixture.nativeElement.querySelector('ng-icon') - expect(iconElement.name).toBe('matWarningAmber') + expect(component.icon).toBe('matWarningAmber') const textElement = fixture.debugElement.query(By.css('.text')) expect(textElement.nativeElement.innerHTML).toBe( @@ -64,8 +63,7 @@ describe('MetadataQualityInfoComponent', () => { component.value = true fixture.detectChanges() - const iconElement = fixture.nativeElement.querySelector('ng-icon') - expect(iconElement.name).toBe('matCheck') + expect(component.icon).toBe('matCheck') const textElement = fixture.debugElement.query(By.css('.text')) expect(textElement.nativeElement.innerHTML).toBe( @@ -78,8 +76,7 @@ describe('MetadataQualityInfoComponent', () => { component.value = false fixture.detectChanges() - const iconElement = fixture.nativeElement.querySelector('ng-icon') - expect(iconElement.name).toBe('matWarningAmber') + expect(component.icon).toBe('matWarningAmber') const textElement = fixture.debugElement.query(By.css('.text')) expect(textElement.nativeElement.innerHTML).toBe( diff --git a/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.ts b/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.ts index 95f1571abc..0cada97d7e 100644 --- a/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.ts +++ b/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.ts @@ -1,5 +1,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { TranslateModule } from '@ngx-translate/core' +import { matCheck, matWarningAmber } from '@ng-icons/material-icons/baseline' marker('record.metadata.quality.title.success') marker('record.metadata.quality.title.failed') @@ -27,6 +30,9 @@ export interface MetadataQualityItem { selector: 'gn-ui-metadata-quality-item', templateUrl: './metadata-quality-item.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [NgIcon, TranslateModule], + viewProviders: [provideIcons({ matCheck, matWarningAmber })], }) export class MetadataQualityItemComponent implements MetadataQualityItem { @Input() name: string diff --git a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts index 69d8dffd76..812d466ef0 100644 --- a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts +++ b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts @@ -8,10 +8,7 @@ import { } from '@geonetwork-ui/util/i18n' import { TranslateModule } from '@ngx-translate/core' import { MetadataQualityItemComponent } from '../metadata-quality-item/metadata-quality-item.component' -import { - PopoverComponent, - ProgressBarComponent, -} from '@geonetwork-ui/ui/widgets' +import { PopoverComponent } from '@geonetwork-ui/ui/widgets' import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { By } from '@angular/platform-browser' import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' @@ -35,12 +32,8 @@ describe('MetadataQualityComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], - declarations: [ - MetadataQualityComponent, - MetadataQualityItemComponent, - ProgressBarComponent, - ], imports: [ + MetadataQualityComponent, UtilSharedModule, CommonModule, UtilI18nModule, diff --git a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts index 5ebc30f32d..65ff37a4b8 100644 --- a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts +++ b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts @@ -7,11 +7,7 @@ import { UtilI18nModule, } from '@geonetwork-ui/util/i18n' import { TranslateModule } from '@ngx-translate/core' -import { MetadataQualityItemComponent } from '../metadata-quality-item/metadata-quality-item.component' -import { - PopoverComponent, - ProgressBarComponent, -} from '@geonetwork-ui/ui/widgets' +import { PopoverComponent } from '@geonetwork-ui/ui/widgets' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' export default { @@ -19,7 +15,6 @@ export default { component: MetadataQualityComponent, decorators: [ moduleMetadata({ - declarations: [ProgressBarComponent, MetadataQualityItemComponent], imports: [ CommonModule, UtilI18nModule, diff --git a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts index 6f604eb608..e73a807dc1 100644 --- a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts +++ b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts @@ -5,14 +5,29 @@ import { OnChanges, SimpleChanges, } from '@angular/core' -import { MetadataQualityItem } from '../metadata-quality-item/metadata-quality-item.component' +import { + MetadataQualityItem, + MetadataQualityItemComponent, +} from '../metadata-quality-item/metadata-quality-item.component' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { + PopoverComponent, + ProgressBarComponent, +} from '@geonetwork-ui/ui/widgets' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-metadata-quality', templateUrl: './metadata-quality.component.html', styleUrls: ['./metadata-quality.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + PopoverComponent, + ProgressBarComponent, + MetadataQualityItemComponent, + ], }) export class MetadataQualityComponent implements OnChanges { @Input() metadata: Partial diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html b/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html deleted file mode 100644 index a6dbd5bac5..0000000000 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
- - - - - - {{ page }} - - - {{ page }} - - - - - -
-
diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts b/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts deleted file mode 100644 index 70cc9ecee5..0000000000 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.stories.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { TranslateModule } from '@ngx-translate/core' -import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { PaginationButtonsComponent } from './pagination-buttons.component' -import { FormsModule } from '@angular/forms' -import { action } from '@storybook/addon-actions' - -export default { - title: 'Elements/PaginationButtonsComponent', - component: PaginationButtonsComponent, - decorators: [ - moduleMetadata({ - imports: [ - ButtonComponent, - UtilI18nModule, - FormsModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - ], - render: (args: PaginationButtonsComponent) => ({ - props: { - ...args, - newCurrentPageEvent: action('newCurrentPageEvent'), - }, - }), -} as Meta - -export const Primary: StoryObj = { - args: { - currentPage: 1, - totalPages: 10, - }, - parameters: { - layout: 'centered', - }, -} diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts b/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts deleted file mode 100644 index 38f5f8b768..0000000000 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - Component, - EventEmitter, - Input, - OnChanges, - Output, -} from '@angular/core' - -@Component({ - selector: 'gn-ui-pagination-buttons', - templateUrl: './pagination-buttons.component.html', - styleUrls: ['./pagination-buttons.component.css'], -}) -export class PaginationButtonsComponent implements OnChanges { - @Input() currentPage: number - @Input() totalPages: number - visiblePages: (number | '...')[] = [] - @Output() newCurrentPageEvent = new EventEmitter() - - ngOnChanges(): void { - this.calculateVisiblePages() - } - - calculateVisiblePages(): void { - const maxVisiblePages = 5 - const halfVisible = Math.floor(maxVisiblePages / 2) - const startPage = Math.max(this.currentPage - halfVisible, 1) - const endPage = Math.min(this.currentPage + halfVisible, this.totalPages) - - const visiblePages: (number | '...')[] = [] - if (startPage > 1) { - visiblePages.push(1) - if (startPage > 2) { - visiblePages.push('...') - } - } - for (let page = startPage; page <= endPage; page++) { - visiblePages.push(page) - } - if (endPage < this.totalPages) { - if (endPage < this.totalPages - 1) { - visiblePages.push('...') - } - visiblePages.push(this.totalPages) - } - - this.visiblePages = visiblePages - } - - changePage(page) { - this.setPage(page) - } - - nextPage() { - this.setPage(this.currentPage + 1) - } - - previousPage() { - this.setPage(this.currentPage - 1) - } - - setPage(newPage) { - if (!Number.isInteger(newPage)) return - this.currentPage = newPage - this.calculateVisiblePages() - this.newCurrentPageEvent.emit(this.currentPage) - } -} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts b/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts deleted file mode 100644 index ed7e9d5239..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { By } from '@angular/platform-browser' -import { PaginationComponent } from './pagination.component' - -describe('PaginationComponent', () => { - let component: PaginationComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [PaginationComponent], - schemas: [NO_ERRORS_SCHEMA], - imports: [], - }) - .overrideComponent(PaginationComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() - - fixture = TestBed.createComponent(PaginationComponent) - component = fixture.componentInstance - component.currentPage = 10 - component.nPages = 10 - component.hideButton = false - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - describe('next button', () => { - let btn - describe('by default', () => { - beforeEach(() => { - btn = fixture.debugElement.query(By.css('gn-ui-button[type=secondary]')) - }) - it('is displayed', () => { - expect(btn).toBeTruthy() - }) - }) - describe('if hidden', () => { - beforeEach(() => { - component.hideButton = true - fixture.detectChanges() - btn = fixture.debugElement.query(By.css('gn-ui-button[type=secondary]')) - }) - it('is displayed', () => { - expect(btn).toBeFalsy() - }) - }) - }) - - it('should navigation_next be disabled', () => { - const isDisabled = fixture.debugElement.query(By.css('#navigate_next')) - .nativeElement.disabled - expect(isDisabled).toBeTruthy() - }) - - it('should navigate_previous be enabled', () => { - const isDisabled = fixture.debugElement.query(By.css('#navigate_previous')) - .nativeElement.disabled - expect(isDisabled).toBeFalsy() - }) -}) diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts b/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts deleted file mode 100644 index 2879c62f0e..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.stories.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { TranslateModule } from '@ngx-translate/core' -import { - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' -import { - TRANSLATE_DEFAULT_CONFIG, - UtilI18nModule, -} from '@geonetwork-ui/util/i18n' -import { PaginationComponent } from './pagination.component' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' -import { FormsModule } from '@angular/forms' - -export default { - title: 'Elements/PaginationComponent', - component: PaginationComponent, - decorators: [ - moduleMetadata({ - imports: [ - ButtonComponent, - UtilI18nModule, - FormsModule, - TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), - ], - }), - componentWrapperDecorator( - (story) => `
${story}
` - ), - ], -} as Meta - -export const Primary: StoryObj = { - args: { - currentPage: 1, - nPages: 10, - }, -} diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.ts b/libs/ui/elements/src/lib/pagination/pagination.component.ts deleted file mode 100644 index d41bbbc333..0000000000 --- a/libs/ui/elements/src/lib/pagination/pagination.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - Output, - SimpleChanges, -} from '@angular/core' - -@Component({ - selector: 'gn-ui-pagination', - templateUrl: './pagination.component.html', - styleUrls: ['./pagination.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class PaginationComponent implements OnChanges { - @Input() currentPage = 1 - @Input() nPages = 1 - @Input() hideButton = false - @Output() newCurrentPageEvent = new EventEmitter() - - private applyPageBounds() { - // make sure this works with NaN inputs as well by adding `|| 1` - this.nPages = Math.max(1, this.nPages || 1) - this.currentPage = Math.max(1, Math.min(this.nPages, this.currentPage || 1)) - } - - ngOnChanges(changes: SimpleChanges) { - // make sure the inputs are valid - if ('currentPage' in changes || 'nPages' in changes) { - this.applyPageBounds() - } - } - - setPage(newPage) { - if (!Number.isInteger(newPage)) return - this.currentPage = newPage - this.applyPageBounds() - this.newCurrentPageEvent.emit(this.currentPage) - } - - nextPage() { - this.setPage(this.currentPage + 1) - } - - previousPage() { - this.setPage(this.currentPage - 1) - } -} diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts index 9dca9ca78f..834f165e55 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { RecordApiFormComponent } from './record-api-form.component' import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' import { firstValueFrom } from 'rxjs' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { TranslateModule } from '@ngx-translate/core' const mockDatasetServiceDistribution: DatasetServiceDistribution = { @@ -72,8 +71,7 @@ describe('RecordApiFormComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [RecordApiFormComponent], - imports: [UiInputsModule, TranslateModule.forRoot()], + imports: [RecordApiFormComponent, TranslateModule.forRoot()], }).compileComponents() fixture = TestBed.createComponent(RecordApiFormComponent) diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts index 5d7d598c10..c2d80e233b 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts @@ -6,6 +6,13 @@ import { } from '@geonetwork-ui/common/domain/model/record' import { mimeTypeToFormat } from '@geonetwork-ui/util/shared' import { BehaviorSubject, combineLatest, filter, map, switchMap } from 'rxjs' +import { + CopyTextButtonComponent, + DropdownSelectorComponent, + TextInputComponent, +} from '@geonetwork-ui/ui/inputs' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' const DEFAULT_PARAMS = { OFFSET: '', @@ -23,6 +30,14 @@ interface OutputFormats { templateUrl: './record-api-form.component.html', styleUrls: ['./record-api-form.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + CommonModule, + TextInputComponent, + DropdownSelectorComponent, + CopyTextButtonComponent, + TranslateModule, + ], }) export class RecordApiFormComponent { @Input() set apiLink(value: DatasetServiceDistribution) { 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 f8ae8b782d..b89c0fd134 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 @@ -17,7 +17,6 @@

-
+ diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts index 2b28278dde..e3a945d87a 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts +++ b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts @@ -56,7 +56,7 @@ describe('BlockListComponent', () => { }) describe('click on step', () => { beforeEach(() => { - component.goToPage(1) + component.goToPage(2) }) it('updates visibility', () => { const blocksVisibility = blockEls.map( @@ -73,7 +73,7 @@ describe('BlockListComponent', () => { ]) }) it('emits the selected page', () => { - expect(component['currentPage']).toEqual(1) + expect(component['currentPage']).toEqual(2) }) }) describe('custom page size', () => { @@ -103,7 +103,7 @@ describe('BlockListComponent', () => { beforeEach(() => { component.pageSize = 2 component.goToPage(2) - component.previousPage() + component.goToPrevPage() }) it('changes to previous page', () => { expect(component['currentPage']).toEqual(1) @@ -114,7 +114,7 @@ describe('BlockListComponent', () => { beforeEach(() => { component.pageSize = 2 component.goToPage(1) - component.nextPage() + component.goToNextPage() }) it('changes to next page', () => { expect(component['currentPage']).toEqual(2) @@ -129,7 +129,7 @@ describe('BlockListComponent', () => { expect(component.isFirstPage).toBe(true) }) it('returns false if the current page is not the first one', () => { - component.goToPage(1) + component.goToPage(2) expect(component.isFirstPage).toBe(false) }) }) @@ -139,11 +139,11 @@ describe('BlockListComponent', () => { component.pageSize = 3 }) it('returns true if the current page is the last one', () => { - component.goToPage(2) + component.goToPage(3) expect(component.isLastPage).toBe(true) }) it('returns false if the current page is not the last one', () => { - component.goToPage(1) + component.goToPage(2) expect(component.isLastPage).toBe(false) }) }) diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts index 31f06fdd74..7e921a6115 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts +++ b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/angular' -import { BlockListComponent } from './block-list.component' import { componentWrapperDecorator } from '@storybook/angular' +import { BlockListComponent } from './block-list.component' const meta: Meta = { component: BlockListComponent, diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.ts b/libs/ui/layout/src/lib/block-list/block-list.component.ts index 5a0937c761..8a89f27eea 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.ts +++ b/libs/ui/layout/src/lib/block-list/block-list.component.ts @@ -10,6 +10,8 @@ import { ViewChild, } from '@angular/core' import { CommonModule } from '@angular/common' +import { Paginable } from '../paginable.interface' +import { PaginationDotsComponent } from '../pagination-dots/pagination-dots.component' @Component({ selector: 'gn-ui-block-list', @@ -17,9 +19,9 @@ import { CommonModule } from '@angular/common' styleUrls: ['./block-list.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule], + imports: [CommonModule, PaginationDotsComponent], }) -export class BlockListComponent implements AfterViewInit { +export class BlockListComponent implements AfterViewInit, Paginable { @Input() pageSize = 5 @Input() containerClass = '' @Input() paginationContainerClass = 'w-full bottom-0 top-auto' @@ -30,20 +32,23 @@ export class BlockListComponent implements AfterViewInit { protected minHeight = 0 - protected currentPage = 0 + protected currentPage_ = 0 protected get pages() { return new Array(this.pagesCount).fill(0).map((_, i) => i) } get isFirstPage() { - return this.currentPage === 0 + return this.currentPage_ === 0 } get isLastPage() { - return this.currentPage === this.pagesCount - 1 + return this.currentPage_ === this.pagesCount - 1 } get pagesCount() { return this.blocks ? Math.ceil(this.blocks.length / this.pageSize) : 1 } + get currentPage() { + return this.currentPage_ + 1 // this is 1-based + } constructor(private changeDetector: ChangeDetectorRef) {} @@ -59,25 +64,29 @@ export class BlockListComponent implements AfterViewInit { protected refreshBlocksVisibility = () => { this.blocks.forEach((block, index) => { block.nativeElement.style.display = - index >= this.currentPage * this.pageSize && - index < (this.currentPage + 1) * this.pageSize + index >= this.currentPage_ * this.pageSize && + index < (this.currentPage_ + 1) * this.pageSize ? null : 'none' }) } - public goToPage(index: number) { - this.currentPage = Math.max(Math.min(index, this.pagesCount - 1), 0) + // pageIndex is 1-based + public goToPage(pageIndex: number) { + this.currentPage_ = Math.max( + Math.min(pageIndex - 1, this.pagesCount - 1), + 0 + ) this.changeDetector.detectChanges() this.refreshBlocksVisibility() } - public previousPage() { + public goToPrevPage() { if (this.isFirstPage) return this.goToPage(this.currentPage - 1) } - public nextPage() { + public goToNextPage() { if (this.isLastPage) return this.goToPage(this.currentPage + 1) } diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.css b/libs/ui/layout/src/lib/carousel/carousel.component.css index 33364704bc..09abc47b36 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.css +++ b/libs/ui/layout/src/lib/carousel/carousel.component.css @@ -6,19 +6,3 @@ position: relative; display: block; } - -.carousel-step-dot { - width: 6px; - height: 6px; - border-radius: 6px; - position: relative; -} - -.carousel-step-dot:after { - content: ''; - position: absolute; - left: -7px; - top: -7px; - width: 20px; - height: 20px; -} diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.html b/libs/ui/layout/src/lib/carousel/carousel.component.html index 0c2ae552d9..a56a62cf71 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.html +++ b/libs/ui/layout/src/lib/carousel/carousel.component.html @@ -3,15 +3,7 @@

-
- -
+ diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts index 7301cb0d1a..87c18a7f89 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts +++ b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts @@ -73,7 +73,7 @@ describe('CarouselComponent', () => { }) describe('click on step', () => { beforeEach(() => { - component.scrollToStep(2) + component.goToPage(3) }) it('calls #scrollTo', () => { expect(component.emblaApi.scrollTo).toHaveBeenCalledWith(2) @@ -88,30 +88,30 @@ describe('CarouselComponent', () => { it('emits the current step index', () => { const spy = jest.fn() component.currentStepChange.subscribe(spy) - component.scrollToStep(2) + component.goToPage(3) expect(spy).toHaveBeenCalledWith(2) expect(spy).toHaveBeenCalledTimes(1) }) }) - describe('isFirstStep', () => { + describe('isFirstPage', () => { it('returns true if the current step is the first one', () => { - expect(component.isFirstStep).toBe(true) + expect(component.isFirstPage).toBe(true) }) it('returns false if the current step is not the first one', () => { - component.scrollToStep(2) - expect(component.isFirstStep).toBe(false) + component.goToPage(3) + expect(component.isFirstPage).toBe(false) }) }) - describe('isLastStep', () => { + describe('isLastPage', () => { it('returns true if the current step is the last one', () => { - component.scrollToStep(3) - expect(component.isLastStep).toBe(true) + component.goToPage(4) + expect(component.isLastPage).toBe(true) }) it('returns false if the current step is not the last one', () => { - component.scrollToStep(1) - expect(component.isLastStep).toBe(false) + component.goToPage(2) + expect(component.isLastPage).toBe(false) }) }) }) diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.ts b/libs/ui/layout/src/lib/carousel/carousel.component.ts index abb751af2a..1e3b212ea7 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.ts +++ b/libs/ui/layout/src/lib/carousel/carousel.component.ts @@ -11,6 +11,8 @@ import { } from '@angular/core' import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel' import { CommonModule } from '@angular/common' +import { Paginable } from '../paginable.interface' +import { PaginationDotsComponent } from '../pagination-dots/pagination-dots.component' @Component({ selector: 'gn-ui-carousel', @@ -18,9 +20,9 @@ import { CommonModule } from '@angular/common' styleUrls: ['./carousel.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule], + imports: [CommonModule, PaginationDotsComponent], }) -export class CarouselComponent implements AfterViewInit { +export class CarouselComponent implements AfterViewInit, Paginable { @ViewChild('carouselOverflowContainer') carouselOverflowContainer: ElementRef @Input() containerClass = '' @@ -38,15 +40,30 @@ export class CarouselComponent implements AfterViewInit { this.changeDetector.detectChanges() } - get isFirstStep() { + // Paginable API + get isFirstPage() { return this.currentStep === 0 } - get isLastStep() { + get isLastPage() { return this.currentStep === this.steps.length - 1 } - get stepsCount() { + get currentPage() { + return this.currentStep + 1 // this is 1-based + } + get pagesCount() { return this.steps.length } + public goToPage(stepIndex: number) { + this.emblaApi.scrollTo(stepIndex - 1) // this is 0-based + } + public goToPrevPage() { + if (this.isFirstPage) return + this.emblaApi.scrollPrev() + } + public goToNextPage() { + if (this.isLastPage) return + this.emblaApi.scrollNext() + } constructor(private changeDetector: ChangeDetectorRef) {} @@ -63,18 +80,4 @@ export class CarouselComponent implements AfterViewInit { .on('reInit', this.refreshSteps) .on('select', this.refreshSteps) } - - public scrollToStep(stepIndex: number) { - this.emblaApi.scrollTo(stepIndex) - } - - public slideToPrevious() { - if (this.isFirstStep) return - this.emblaApi.scrollPrev() - } - - public slideToNext() { - if (this.isLastStep) return - this.emblaApi.scrollNext() - } } diff --git a/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.spec.ts b/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.spec.ts index dfca817ab8..a3d05c6a95 100644 --- a/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.spec.ts +++ b/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.spec.ts @@ -8,7 +8,7 @@ describe('ExpandablePanelButtonComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ExpandablePanelButtonComponent], + imports: [ExpandablePanelButtonComponent], }).compileComponents() }) diff --git a/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.ts b/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.ts index 340505e25c..1197fa4730 100644 --- a/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.ts +++ b/libs/ui/layout/src/lib/expandable-panel-button/expandable-panel-button.component.ts @@ -4,12 +4,18 @@ import { Input, TemplateRef, } from '@angular/core' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { matExpandLess, matExpandMore } from '@ng-icons/material-icons/baseline' @Component({ selector: 'gn-ui-expandable-panel-button', templateUrl: './expandable-panel-button.component.html', styleUrls: ['./expandable-panel-button.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, NgIcon], + viewProviders: [provideIcons({ matExpandMore, matExpandLess })], + standalone: true, }) export class ExpandablePanelButtonComponent { @Input() titleTemplate: TemplateRef diff --git a/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.spec.ts b/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.spec.ts index b4faee35df..806128b478 100644 --- a/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.spec.ts +++ b/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.spec.ts @@ -10,7 +10,7 @@ describe('ExpandablePanelComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ExpandablePanelComponent], + imports: [ExpandablePanelComponent], schemas: [NO_ERRORS_SCHEMA], }) .overrideComponent(ExpandablePanelComponent, { diff --git a/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.ts b/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.ts index 2f9e618b65..a9734fccdf 100644 --- a/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.ts +++ b/libs/ui/layout/src/lib/expandable-panel/expandable-panel.component.ts @@ -1,16 +1,22 @@ import { - Component, ChangeDetectionStrategy, + Component, + ElementRef, Input, ViewChild, - ElementRef, } from '@angular/core' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { matAdd, matRemove } from '@ng-icons/material-icons/baseline' @Component({ selector: 'gn-ui-expandable-panel', templateUrl: './expandable-panel.component.html', styleUrls: ['./expandable-panel.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, NgIcon], + viewProviders: [provideIcons({ matAdd, matRemove })], }) export class ExpandablePanelComponent { @Input() title: string diff --git a/libs/ui/layout/src/lib/interactive-table/interactive-table-column/interactive-table-column.component.ts b/libs/ui/layout/src/lib/interactive-table/interactive-table-column/interactive-table-column.component.ts index ddceb0984d..5b5633cbcb 100644 --- a/libs/ui/layout/src/lib/interactive-table/interactive-table-column/interactive-table-column.component.ts +++ b/libs/ui/layout/src/lib/interactive-table/interactive-table-column/interactive-table-column.component.ts @@ -22,6 +22,7 @@ export class InteractiveTableColumnComponent { @ContentChild('cell') cell: TemplateRef @Input() grow = false + @Input() width: string @Input() sortable = false @Input() activeSort: 'asc' | 'desc' | null = null @Output() sortChange = new EventEmitter<'asc' | 'desc'>() diff --git a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts index 589364dce2..68cdde2d35 100644 --- a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts +++ b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts @@ -36,7 +36,11 @@ export class InteractiveTableComponent { return { 'grid-template-columns': this.columns .map((column) => - column.grow ? `minmax(0px,1fr)` : `minmax(0px,max-content)` + column.width + ? column.width + : column.grow + ? `minmax(0px,1fr)` + : `minmax(0px,max-content)` ) .join(' '), } diff --git a/libs/ui/layout/src/lib/paginable.interface.ts b/libs/ui/layout/src/lib/paginable.interface.ts new file mode 100644 index 0000000000..a696dbfa40 --- /dev/null +++ b/libs/ui/layout/src/lib/paginable.interface.ts @@ -0,0 +1,14 @@ +/** + * This interface is used for components that want to offer pagination + * Note: pages indexes are 1-based!! so `isLastPage` means `currentPage === pagesCount` + * and `isFirstPage` means `currentPage === 1` + */ +export interface Paginable { + isFirstPage: boolean + isLastPage: boolean + pagesCount: number + currentPage: number + goToPage(index: number): void + goToNextPage(): void + goToPrevPage(): void +} diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.css b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html new file mode 100644 index 0000000000..237fff4344 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.html @@ -0,0 +1,51 @@ +
+
+ + + + + +
+ {{ page }} +
+
+ + ... + + + {{ page }} + +
+ + + +
+
diff --git a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts similarity index 73% rename from libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts rename to libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts index 78479f6ec7..dd63fd4793 100644 --- a/libs/ui/elements/src/lib/pagination-buttons/pagination-buttons.component.spec.ts +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.spec.ts @@ -1,34 +1,38 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { PaginationButtonsComponent } from './pagination-buttons.component' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 1 + pagesCount = 5 + isFirstPage = true + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} describe('PaginationButtonsComponent', () => { let component: PaginationButtonsComponent let fixture: ComponentFixture - const mockChangePage = (page) => { - component.setPage(page) - } - beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [PaginationButtonsComponent], + imports: [PaginationButtonsComponent], }).compileComponents() fixture = TestBed.createComponent(PaginationButtonsComponent) component = fixture.componentInstance - component.currentPage = 3 - component.totalPages = 10 - component.calculateVisiblePages() - component.changePage = mockChangePage + component.listComponent = new MockPaginable() fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + describe('when using next arrow', () => { beforeEach(() => { - component.currentPage = 2 const paginationButtons = fixture.nativeElement.querySelectorAll('gn-ui-button') paginationButtons.forEach((buttonElement) => { @@ -39,12 +43,11 @@ describe('PaginationButtonsComponent', () => { }) }) it('should access next page on click', () => { - expect(component.currentPage).toBe(3) + expect(component.listComponent.goToNextPage).toHaveBeenCalled() }) }) describe('when using previous arrow', () => { beforeEach(() => { - component.currentPage = 4 const paginationButtons = fixture.nativeElement.querySelectorAll('gn-ui-button') paginationButtons.forEach((buttonElement) => { @@ -54,13 +57,13 @@ describe('PaginationButtonsComponent', () => { } }) }) - it('is should access previous page', () => { - expect(component.currentPage).toBe(3) + it('should access previous page on click', () => { + expect(component.listComponent.goToPrevPage).toHaveBeenCalled() }) }) describe('when accessing first page', () => { beforeEach(() => { - component.currentPage = 1 + component.listComponent.isFirstPage = true fixture.detectChanges() }) it('is should disable the previous arrow', () => { @@ -77,7 +80,7 @@ describe('PaginationButtonsComponent', () => { }) describe('when accessing last page', () => { beforeEach(() => { - component.currentPage = 10 + component.listComponent.isLastPage = true fixture.detectChanges() }) it('is should disable the next arrow', () => { @@ -94,19 +97,12 @@ describe('PaginationButtonsComponent', () => { }) describe('when clicking on page button', () => { beforeEach(() => { - const paginationButtons = - fixture.nativeElement.querySelectorAll('gn-ui-button') - const pageBtnList = [] - paginationButtons.forEach((buttonElement) => { - const ngIcon = buttonElement.querySelector('ng-icon') - if (!ngIcon) { - pageBtnList.push(buttonElement) - } - }) - pageBtnList[1].dispatchEvent(new Event('buttonClick')) + const paginationButton = + fixture.nativeElement.querySelector('[data-test=page-2]') + paginationButton.dispatchEvent(new Event('buttonClick')) }) - it('is should access the requested page', () => { - expect(component.currentPage).toBe(2) + it('should access the requested page', () => { + expect(component.listComponent.goToPage).toHaveBeenCalledWith(2) }) }) }) diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts new file mode 100644 index 0000000000..b44214027c --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.stories.ts @@ -0,0 +1,21 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PaginationButtonsComponent } from './pagination-buttons.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PaginationButtonsComponent', + component: PaginationButtonsComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts new file mode 100644 index 0000000000..d696a1e89f --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-buttons/pagination-buttons.component.ts @@ -0,0 +1,50 @@ +import { Component, Input } from '@angular/core' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination-buttons', + templateUrl: './pagination-buttons.component.html', + styleUrls: ['./pagination-buttons.component.css'], + standalone: true, + imports: [CommonModule, ButtonComponent, NgIcon], + viewProviders: [ + provideIcons({ + iconoirNavArrowRight, + iconoirNavArrowLeft, + }), + ], +}) +export class PaginationButtonsComponent { + @Input() listComponent: Paginable + + get visiblePages(): (number | '...')[] { + const maxVisiblePages = 5 + const halfVisible = Math.floor(maxVisiblePages / 2) + const startPage = Math.max(this.listComponent.currentPage - halfVisible, 1) + const endPage = Math.min( + this.listComponent.currentPage + halfVisible, + this.listComponent.pagesCount + ) + + const allPages = new Array(this.listComponent.pagesCount) + .fill(0) + .map((_, i) => i + 1) // pages are 1-based + return allPages.reduce((pages, page) => { + if (page === 1 || page === this.listComponent.pagesCount) { + // first and last page + pages.push(page) + } else if (page >= startPage && page <= endPage) { + // pages around current one + pages.push(page) + } else if (pages[pages.length - 1] !== '...') { + // dots between pages + pages.push('...') + } + return pages + }, []) + } +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css new file mode 100644 index 0000000000..7263eb6669 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.css @@ -0,0 +1,16 @@ +.pagination-dot { + width: 6px; + height: 6px; + border-radius: 6px; + position: relative; + flex-shrink: 0; +} + +.pagination-dot:after { + content: ''; + position: absolute; + left: -7px; + top: -7px; + width: 20px; + height: 20px; +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html new file mode 100644 index 0000000000..923c8093f0 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.html @@ -0,0 +1,15 @@ +
+ +
diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts new file mode 100644 index 0000000000..1a4795d31f --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { PaginationDotsComponent } from './pagination-dots.component' +import { By } from '@angular/platform-browser' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 4 + pagesCount = 5 + isFirstPage = false + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} + +describe('PaginationDotsComponent', () => { + let component: PaginationDotsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaginationDotsComponent], + }).compileComponents() + + fixture = TestBed.createComponent(PaginationDotsComponent) + component = fixture.componentInstance + component.listComponent = new MockPaginable() + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('dots', () => { + let dots: HTMLElement[] + beforeEach(() => { + dots = fixture.debugElement + .queryAll(By.css('.pagination-dot')) + .map((dot) => dot.nativeElement) + }) + it('has 1 dot per page', () => { + expect(dots.length).toBe(component.listComponent.pagesCount) + }) + it('switches to a page on click', () => { + dots[2].click() + expect(component.listComponent.goToPage).toHaveBeenCalledWith(3) // page index is 1-based + }) + it('shows selected page as active', () => { + expect(dots[2].classList).not.toContain('bg-primary') + expect(dots[3].classList).toContain('bg-primary') + expect(dots[4].classList).not.toContain('bg-primary') + }) + }) +}) diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts new file mode 100644 index 0000000000..cecd216eee --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.stories.ts @@ -0,0 +1,30 @@ +import { + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { PaginationDotsComponent } from './pagination-dots.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PaginationDotsComponent', + component: PaginationDotsComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts new file mode 100644 index 0000000000..e989b0dc07 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination-dots/pagination-dots.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core' +import { provideIcons } from '@ng-icons/core' +import { CommonModule } from '@angular/common' +import { iconoirNavArrowLeft, iconoirNavArrowRight } from '@ng-icons/iconoir' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination-dots', + templateUrl: './pagination-dots.component.html', + styleUrls: ['./pagination-dots.component.css'], + standalone: true, + imports: [CommonModule], + viewProviders: [ + provideIcons({ + iconoirNavArrowRight, + iconoirNavArrowLeft, + }), + ], +}) +export class PaginationDotsComponent { + @Input() listComponent: Paginable + @Input() containerClass = '' + + // 1-based + get steps() { + return Array.from( + { length: this.listComponent.pagesCount }, + (_, i) => i + 1 + ) + } +} diff --git a/libs/ui/layout/src/lib/pagination/pagination.component.css b/libs/ui/layout/src/lib/pagination/pagination.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/elements/src/lib/pagination/pagination.component.html b/libs/ui/layout/src/lib/pagination/pagination.component.html similarity index 72% rename from libs/ui/elements/src/lib/pagination/pagination.component.html rename to libs/ui/layout/src/lib/pagination/pagination.component.html index 775f02154a..99d55d0a1e 100644 --- a/libs/ui/elements/src/lib/pagination/pagination.component.html +++ b/libs/ui/layout/src/lib/pagination/pagination.component.html @@ -1,9 +1,9 @@
pagination.pageOf {{ nPages }}pagination.pageOf + {{ listComponent.pagesCount }} { + let component: PaginationComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PaginationComponent, TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(PaginationComponent) + component = fixture.componentInstance + component.listComponent = new MockPaginable() + component.hideButton = false + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('next button', () => { + let btn: ButtonComponent + beforeEach(() => { + btn = fixture.debugElement.queryAll(By.directive(ButtonComponent))[0] + ?.componentInstance + }) + it('is displayed by default', () => { + expect(btn).toBeTruthy() + }) + it('is hidden if hideButton = true', () => { + component.hideButton = true + fixture.detectChanges() + expect( + fixture.debugElement.queryAll(By.directive(ButtonComponent)).length + ).toBe(2) + }) + it('is disabled if last page', () => { + component.listComponent.isLastPage = true + fixture.detectChanges() + expect(btn.disabled).toBe(true) + }) + it('goes to next page', () => { + btn.buttonClick.emit() + expect(component.listComponent.goToNextPage).toHaveBeenCalled() + }) + }) + + describe('prev and next buttons', () => { + let prevButton: ButtonComponent + let nextButton: ButtonComponent + beforeEach(() => { + prevButton = fixture.debugElement.queryAll( + By.directive(ButtonComponent) + )[1].componentInstance + nextButton = fixture.debugElement.queryAll( + By.directive(ButtonComponent) + )[2].componentInstance + }) + it('prev button disabled if first page', () => { + component.listComponent.isFirstPage = true + fixture.detectChanges() + expect(prevButton.disabled).toBe(true) + }) + it('prev button enabled if not first page', () => { + component.listComponent.isFirstPage = false + fixture.detectChanges() + expect(prevButton.disabled).toBe(false) + }) + it('calls goToPrevPage', () => { + prevButton.buttonClick.emit() + expect(component.listComponent.goToPrevPage).toHaveBeenCalled() + }) + it('next button disabled if last page', () => { + component.listComponent.isLastPage = true + fixture.detectChanges() + expect(nextButton.disabled).toBe(true) + }) + it('next button enabled if not last page', () => { + component.listComponent.isLastPage = false + fixture.detectChanges() + expect(nextButton.disabled).toBe(false) + }) + it('calls goToNextPage', () => { + nextButton.buttonClick.emit() + expect(component.listComponent.goToNextPage).toHaveBeenCalled() + }) + }) +}) diff --git a/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts b/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts new file mode 100644 index 0000000000..7f60821d94 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination/pagination.component.stories.ts @@ -0,0 +1,102 @@ +import { + applicationConfig, + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { PaginationComponent } from './pagination.component' +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + importProvidersFrom, +} from '@angular/core' +import { Paginable } from '../paginable.interface' +import { + TRANSLATE_DEFAULT_CONFIG, + UtilI18nModule, +} from '@geonetwork-ui/util/i18n' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-mock-list', + template: `current page: {{ currentPage }}
+   +
+
pages count: {{ pagesCount }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class MockListComponent implements Paginable { + currentPage = 1 + pagesCount = 8 + constructor(private changeDetector: ChangeDetectorRef) {} + get isFirstPage() { + return this.currentPage == 1 + } + get isLastPage() { + return this.currentPage == this.pagesCount + } + goToPage(index: number) { + this.currentPage = index + this.changeDetector.detectChanges() + } + goToPrevPage() { + if (this.isFirstPage) return + this.goToPage(this.currentPage - 1) + } + goToNextPage() { + if (this.isLastPage) return + this.goToPage(this.currentPage + 1) + } +} + +export default { + title: 'Layout/Pagination/PaginationComponent', + component: PaginationComponent, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + applicationConfig({ + providers: [ + importProvidersFrom(UtilI18nModule), + importProvidersFrom(TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG)), + ], + }), + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + hideButton: false, + }, + argTypes: { + hideButton: { + control: 'boolean', + }, + }, + render: (args) => ({ + props: args, + template: ` + +`, + }), +} diff --git a/libs/ui/layout/src/lib/pagination/pagination.component.ts b/libs/ui/layout/src/lib/pagination/pagination.component.ts new file mode 100644 index 0000000000..62bebe78d9 --- /dev/null +++ b/libs/ui/layout/src/lib/pagination/pagination.component.ts @@ -0,0 +1,45 @@ +import { Component, Input } from '@angular/core' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { NgIcon, provideIcons } from '@ng-icons/core' +import { FormsModule } from '@angular/forms' +import { + matChevronLeft, + matChevronRight, +} from '@ng-icons/material-icons/baseline' +import { CommonModule } from '@angular/common' +import { TranslateModule } from '@ngx-translate/core' +import { Paginable } from '../paginable.interface' + +@Component({ + selector: 'gn-ui-pagination', + templateUrl: './pagination.component.html', + styleUrls: ['./pagination.component.css'], + standalone: true, + imports: [ + CommonModule, + ButtonComponent, + NgIcon, + FormsModule, + TranslateModule, + ], + viewProviders: [ + provideIcons({ + matChevronLeft, + matChevronRight, + }), + ], +}) +export class PaginationComponent { + @Input() listComponent: Paginable + @Input() hideButton = false + + private applyPageBounds(page: number): number { + // make sure this works with NaN inputs as well by adding `|| 1` + return Math.max(1, Math.min(this.listComponent.pagesCount, page || 1)) + } + + setPage(newPage) { + if (!Number.isInteger(newPage)) return + this.listComponent.goToPage(this.applyPageBounds(newPage)) + } +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.css similarity index 100% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.css diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html similarity index 55% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html index ed38ca89ca..4ce91cf319 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html @@ -1,17 +1,17 @@
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts similarity index 80% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts index 95ac4c3eb5..588e4b5264 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts @@ -1,9 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' - import { PreviousNextButtonsComponent } from './previous-next-buttons.component' import { TranslateModule } from '@ngx-translate/core' import { By } from '@angular/platform-browser' import { DebugElement } from '@angular/core' +import { Paginable } from '../paginable.interface' + +class MockPaginable implements Paginable { + currentPage = 1 + pagesCount = 5 + isFirstPage = true + isLastPage = false + goToPage = jest.fn() + goToPrevPage = jest.fn() + goToNextPage = jest.fn() +} describe('PreviousNextButtonsComponent', () => { let component: PreviousNextButtonsComponent @@ -19,6 +29,7 @@ describe('PreviousNextButtonsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(PreviousNextButtonsComponent) component = fixture.componentInstance + component.listComponent = new MockPaginable() compiled = fixture.debugElement }) @@ -28,8 +39,8 @@ describe('PreviousNextButtonsComponent', () => { describe('onFirstElement', () => { beforeEach(() => { - component.isFirst = true - component.isLast = false + component.listComponent.isFirstPage = true + component.listComponent.isLastPage = false fixture.detectChanges() }) @@ -48,8 +59,8 @@ describe('PreviousNextButtonsComponent', () => { describe('onLastElement', () => { beforeEach(() => { - component.isFirst = false - component.isLast = true + component.listComponent.isFirstPage = false + component.listComponent.isLastPage = true fixture.detectChanges() }) diff --git a/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts new file mode 100644 index 0000000000..a65bafa6ac --- /dev/null +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts @@ -0,0 +1,26 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PreviousNextButtonsComponent } from './previous-next-buttons.component' +import { MockListComponent } from '../pagination/pagination.component.stories' + +export default { + title: 'Layout/Pagination/PreviousNextButtonsComponent', + component: PreviousNextButtonsComponent, + parameters: { + backgrounds: { + default: 'dark', + }, + }, + decorators: [ + moduleMetadata({ + imports: [MockListComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + render: () => ({ + template: ` + +`, + }), +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts similarity index 53% rename from libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts rename to libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts index 9e95996105..889428f6cf 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts +++ b/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts @@ -1,11 +1,4 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - Output, -} from '@angular/core' -import { ButtonComponent } from '../button/button.component' +import { Component, Input } from '@angular/core' import { NgIconComponent, provideIcons, @@ -15,12 +8,13 @@ import { matArrowBack, matArrowForward, } from '@ng-icons/material-icons/baseline' +import { Paginable } from '../paginable.interface' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' @Component({ selector: 'gn-ui-previous-next-buttons', templateUrl: './previous-next-buttons.component.html', styleUrls: ['./previous-next-buttons.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ButtonComponent, NgIconComponent], providers: [ @@ -31,16 +25,5 @@ import { ], }) export class PreviousNextButtonsComponent { - @Input() isFirst: boolean - @Input() isLast: boolean - - @Output() directionButtonClicked: EventEmitter = new EventEmitter() - - previousButtonClicked() { - this.directionButtonClicked.next('previous') - } - - nextButtonClicked() { - this.directionButtonClicked.next('next') - } + @Input() listComponent: Paginable } diff --git a/libs/ui/layout/src/lib/ui-layout.module.ts b/libs/ui/layout/src/lib/ui-layout.module.ts index 4eb12eaa58..bcaaebe25e 100644 --- a/libs/ui/layout/src/lib/ui-layout.module.ts +++ b/libs/ui/layout/src/lib/ui-layout.module.ts @@ -1,41 +1,18 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' -import { ExpandablePanelComponent } from './expandable-panel/expandable-panel.component' 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 { - NgIconComponent, - provideIcons, - provideNgIconsConfig, -} from '@ng-icons/core' -import { - matExpandMore, - matExpandLess, - matAdd, - matRemove, -} from '@ng-icons/material-icons/baseline' +import { NgIconComponent, provideNgIconsConfig } from '@ng-icons/core' @NgModule({ imports: [CommonModule, TranslateModule.forChild(), NgIconComponent], providers: [ - provideIcons({ matExpandMore, matExpandLess, matRemove, matAdd }), provideNgIconsConfig({ size: '0.9em', }), ], - declarations: [ - ExpandablePanelComponent, - StickyHeaderComponent, - AnchorLinkDirective, - ExpandablePanelButtonComponent, - ], - exports: [ - ExpandablePanelComponent, - StickyHeaderComponent, - AnchorLinkDirective, - ExpandablePanelButtonComponent, - ], + declarations: [StickyHeaderComponent, AnchorLinkDirective], + exports: [StickyHeaderComponent, AnchorLinkDirective], }) export class UiLayoutModule {} diff --git a/libs/ui/map/src/index.ts b/libs/ui/map/src/index.ts index fb10d8a265..921923dda7 100644 --- a/libs/ui/map/src/index.ts +++ b/libs/ui/map/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/components/map-container/map-container.component' export * from './lib/components/map-container/map-settings.token' export * from './lib/components/feature-detail/feature-detail.component' +export * from './lib/components/map-legend/map-legend.component' export * from './lib/map-utils' diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.css b/libs/ui/map/src/lib/components/map-legend/map-legend.component.css new file mode 100644 index 0000000000..91a65cd493 --- /dev/null +++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.css @@ -0,0 +1,5 @@ +.geosdk--legend-container { + overflow: auto; + white-space: normal; + word-wrap: break-word; +} diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.html b/libs/ui/map/src/lib/components/map-legend/map-legend.component.html new file mode 100644 index 0000000000..35ed987a84 --- /dev/null +++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.html @@ -0,0 +1 @@ +
diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.spec.ts b/libs/ui/map/src/lib/components/map-legend/map-legend.component.spec.ts new file mode 100644 index 0000000000..1c4cd1f511 --- /dev/null +++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.spec.ts @@ -0,0 +1,150 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MapLegendComponent } from './map-legend.component' +import { MapContext } from '@geospatial-sdk/core' +import { createLegendFromLayer } from '@geospatial-sdk/legend' + +jest.mock('@geospatial-sdk/legend', () => ({ + createLegendFromLayer: jest.fn(), +})) + +describe('MapLegendComponent', () => { + let component: MapLegendComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MapLegendComponent], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(MapLegendComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('Change of map-context', () => { + it('should create legend on first change', async () => { + const mockContext: MapContext = { + layers: [ + { + id: 'test-layer', + }, + ], + } as MapContext + + const mockLegendElement = document.createElement('div') + ;(createLegendFromLayer as jest.Mock).mockResolvedValue(mockLegendElement) + + const legendStatusChangeSpy = jest.spyOn( + component.legendStatusChange, + 'emit' + ) + + await component.ngOnChanges({ + context: { + currentValue: mockContext, + previousValue: null, + firstChange: true, + isFirstChange: () => true, + }, + }) + + expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0]) + expect(component.legendHTML).toBe(mockLegendElement) + expect(legendStatusChangeSpy).toHaveBeenCalledWith(true) + }) + + it('should create legend and emit status on subsequent context changes', async () => { + const mockContext: MapContext = { + layers: [ + { + id: 'test-layer', + }, + ], + } as MapContext + + const mockLegendElement = document.createElement('div') + ;(createLegendFromLayer as jest.Mock).mockResolvedValue(mockLegendElement) + + const legendStatusChangeSpy = jest.spyOn( + component.legendStatusChange, + 'emit' + ) + + await component.ngOnChanges({ + context: { + currentValue: mockContext, + previousValue: {}, + firstChange: false, + isFirstChange: () => false, + }, + }) + + expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0]) + expect(component.legendHTML).toBe(mockLegendElement) + expect(legendStatusChangeSpy).toHaveBeenCalledWith(true) + }) + + it('should emit nothing when no legend is created', async () => { + const mockContext: MapContext = { + layers: [ + { + id: 'test-layer', + }, + ], + } as MapContext + + ;(createLegendFromLayer as jest.Mock).mockResolvedValue(false) + + const legendStatusChangeSpy = jest.spyOn( + component.legendStatusChange, + 'emit' + ) + + await component.ngOnChanges({ + context: { + currentValue: mockContext, + previousValue: {}, + firstChange: false, + isFirstChange: () => false, + }, + }) + + expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0]) + expect(component.legendHTML).toBe(false) + expect(legendStatusChangeSpy).not.toHaveBeenCalled() + }) + + it('should handle multiple layers', async () => { + const mockContext: MapContext = { + layers: [{ id: 'layer-1' }, { id: 'layer-2' }], + } as MapContext + + const mockLegendElement = document.createElement('div') + ;(createLegendFromLayer as jest.Mock).mockResolvedValue(mockLegendElement) + + const legendStatusChangeSpy = jest.spyOn( + component.legendStatusChange, + 'emit' + ) + + await component.ngOnChanges({ + context: { + currentValue: mockContext, + previousValue: {}, + firstChange: false, + isFirstChange: () => false, + }, + }) + + expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0]) + expect(component.legendHTML).toBe(mockLegendElement) + expect(legendStatusChangeSpy).toHaveBeenCalledWith(true) + }) + }) +}) diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.ts b/libs/ui/map/src/lib/components/map-legend/map-legend.component.ts new file mode 100644 index 0000000000..7f635f10ef --- /dev/null +++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.ts @@ -0,0 +1,39 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + ViewEncapsulation, +} from '@angular/core' +import { MapContext } from '@geospatial-sdk/core' +import { createLegendFromLayer } from '@geospatial-sdk/legend' +import { NgIf } from '@angular/common' + +@Component({ + selector: 'gn-ui-map-legend', + templateUrl: './map-legend.component.html', + standalone: true, + styleUrls: ['./map-legend.component.css'], + encapsulation: ViewEncapsulation.None, + imports: [NgIf], +}) +export class MapLegendComponent implements OnChanges { + @Input() context: MapContext | null + @Output() legendStatusChange = new EventEmitter() + legendHTML: HTMLElement | false + + async ngOnChanges(changes: SimpleChanges) { + if ('context' in changes) { + const mapContext = changes['context'].currentValue + if (mapContext.layers && mapContext.layers.length > 0) { + const mapContextLayer = mapContext.layers[0] + this.legendHTML = await createLegendFromLayer(mapContextLayer) + if (this.legendHTML) { + this.legendStatusChange.emit(true) + } + } + } + } +} diff --git a/libs/ui/search/src/lib/record-preview-card/record-preview-card.component.stories.ts b/libs/ui/search/src/lib/record-preview-card/record-preview-card.component.stories.ts index 9c6f6cebe9..29be08ce46 100644 --- a/libs/ui/search/src/lib/record-preview-card/record-preview-card.component.stories.ts +++ b/libs/ui/search/src/lib/record-preview-card/record-preview-card.component.stories.ts @@ -5,7 +5,6 @@ import { StoryObj, } from '@storybook/angular' import { RecordPreviewCardComponent } from './record-preview-card.component' -import { ThumbnailComponent } from '@geonetwork-ui/ui/elements' import { importProvidersFrom } from '@angular/core' import { HttpClientModule } from '@angular/common/http' import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' @@ -19,7 +18,6 @@ export default { component: RecordPreviewCardComponent, decorators: [ moduleMetadata({ - declarations: [ThumbnailComponent], imports: [UiDatavizModule, UtilSharedModule], }), applicationConfig({ diff --git a/libs/ui/search/src/lib/record-preview-list/record-preview-list.component.stories.ts b/libs/ui/search/src/lib/record-preview-list/record-preview-list.component.stories.ts index 9d2fcf4432..c46c779f89 100644 --- a/libs/ui/search/src/lib/record-preview-list/record-preview-list.component.stories.ts +++ b/libs/ui/search/src/lib/record-preview-list/record-preview-list.component.stories.ts @@ -1,6 +1,5 @@ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' import { RecordPreviewListComponent } from './record-preview-list.component' -import { ThumbnailComponent } from '@geonetwork-ui/ui/elements' import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { TranslateModule } from '@ngx-translate/core' import { TRANSLATE_DEFAULT_CONFIG } from '@geonetwork-ui/util/i18n' @@ -13,7 +12,6 @@ export default { component: RecordPreviewListComponent, decorators: [ moduleMetadata({ - declarations: [ThumbnailComponent], imports: [ TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), UtilSharedModule, diff --git a/libs/ui/search/src/lib/record-preview-text/record-preview-text.component.stories.ts b/libs/ui/search/src/lib/record-preview-text/record-preview-text.component.stories.ts index 3af7c4b1f4..b4eb1cf2bf 100644 --- a/libs/ui/search/src/lib/record-preview-text/record-preview-text.component.stories.ts +++ b/libs/ui/search/src/lib/record-preview-text/record-preview-text.component.stories.ts @@ -5,7 +5,6 @@ import { StoryObj, } from '@storybook/angular' import { RecordPreviewTextComponent } from './record-preview-text.component' -import { ThumbnailComponent } from '@geonetwork-ui/ui/elements' import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { importProvidersFrom } from '@angular/core' import { RecordPreviewTitleComponent } from '../record-preview-title/record-preview-title.component' @@ -22,7 +21,6 @@ export default { component: RecordPreviewTextComponent, decorators: [ moduleMetadata({ - declarations: [ThumbnailComponent], imports: [ TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), UtilSharedModule, diff --git a/libs/ui/search/src/lib/record-preview-title/record-preview-title.component.stories.ts b/libs/ui/search/src/lib/record-preview-title/record-preview-title.component.stories.ts index 5ec91fe49f..e8cc5ccf55 100644 --- a/libs/ui/search/src/lib/record-preview-title/record-preview-title.component.stories.ts +++ b/libs/ui/search/src/lib/record-preview-title/record-preview-title.component.stories.ts @@ -1,6 +1,5 @@ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' import { RecordPreviewTitleComponent } from './record-preview-title.component' -import { ThumbnailComponent } from '@geonetwork-ui/ui/elements' import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { TranslateModule } from '@ngx-translate/core' import { TRANSLATE_DEFAULT_CONFIG } from '@geonetwork-ui/util/i18n' @@ -13,7 +12,6 @@ export default { component: RecordPreviewTitleComponent, decorators: [ moduleMetadata({ - declarations: [ThumbnailComponent], imports: [ TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), UtilSharedModule, diff --git a/libs/ui/search/src/lib/results-table/results-table.component.html b/libs/ui/search/src/lib/results-table/results-table.component.html index 595b2ac468..f24f827cf3 100644 --- a/libs/ui/search/src/lib/results-table/results-table.component.html +++ b/libs/ui/search/src/lib/results-table/results-table.component.html @@ -3,17 +3,7 @@ (itemClick)="handleRecordClick($event)" > - - - - - +
- {{ item.title }} + {{ + item.title + }} - + record.metadata.formats @@ -63,14 +55,14 @@ [title]="formats.join(', ')" > {{ formats[0] }} @@ -86,6 +78,7 @@ record.metadata.author - + {{ formatUserInfo(item.extras?.ownerInfo) }} @@ -102,7 +95,7 @@ - + record.metadata.status @@ -120,6 +113,7 @@ record.metadata.updatedOn -
+
{{ isUnsavedDraft(item) ? '-' : dateToString(item.recordUpdated) }}
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 index 572a593414..ac00566dc8 100644 --- 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 @@ -40,7 +40,7 @@ describe('ResultsTableComponent', () => { datasetRecordsFixture()[0] as CatalogRecord )[0] ) - ).toEqual('#1e5180') // geojson + ).toEqual('#b3cde8') // geojson }) }) @@ -106,32 +106,6 @@ describe('ResultsTableComponent', () => { 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', () => { 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 index edd2695388..fc2ed77557 100644 --- a/libs/ui/search/src/lib/results-table/results-table.component.ts +++ b/libs/ui/search/src/lib/results-table/results-table.component.ts @@ -22,6 +22,7 @@ import { } from '@geonetwork-ui/ui/layout' import { FileFormat, + formatUserInfo, getBadgeColor, getFileFormat, getFormatPriority, @@ -162,11 +163,7 @@ export class ResultsTableComponent { } formatUserInfo(userInfo: string | unknown): string { - const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') - if (infos && infos.length === 4) { - return `${infos[2]} ${infos[1]}` - } - return undefined + return formatUserInfo(userInfo) } getBadgeColor(format: FileFormat): string { @@ -212,24 +209,4 @@ export class ResultsTableComponent { 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/search/src/lib/ui-search.module.ts b/libs/ui/search/src/lib/ui-search.module.ts index d5d9fa9864..318ac81192 100644 --- a/libs/ui/search/src/lib/ui-search.module.ts +++ b/libs/ui/search/src/lib/ui-search.module.ts @@ -21,7 +21,10 @@ import { TagInputModule } from 'ngx-chips' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { ResultsListItemComponent } from './results-list-item/results-list-item.component' import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' -import { UiElementsModule } from '@geonetwork-ui/ui/elements' +import { + MetadataQualityComponent, + UiElementsModule, +} from '@geonetwork-ui/ui/elements' import { RecordPreviewFeedComponent } from './record-preview-feed/record-preview-feed.component' import { CommonModule } from '@angular/common' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' @@ -29,9 +32,9 @@ import { MatCheckboxModule } from '@angular/material/checkbox' import { InteractiveTableComponent } from '@geonetwork-ui/ui/layout' import { NgIconsModule, provideNgIconsConfig } from '@ng-icons/core' import { - matMapOutline, matCloudDownloadOutline, matHomeWorkOutline, + matMapOutline, } from '@ng-icons/material-icons/outline' import { matFace } from '@ng-icons/material-icons/baseline' @@ -71,6 +74,7 @@ import { matFace } from '@ng-icons/material-icons/baseline' matFace, matHomeWorkOutline, }), + MetadataQualityComponent, ], exports: [ RecordPreviewListComponent, diff --git a/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.spec.ts b/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.spec.ts index d88184511c..f15ffa77ed 100644 --- a/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.spec.ts +++ b/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.spec.ts @@ -8,8 +8,7 @@ describe('LoadingMaskComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [LoadingMaskComponent], - imports: [MatProgressSpinnerModule], + imports: [LoadingMaskComponent, MatProgressSpinnerModule], }).compileComponents() }) diff --git a/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.ts b/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.ts index b2bf90c916..43172e5797 100644 --- a/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.ts +++ b/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.ts @@ -1,15 +1,13 @@ -import { - Component, - OnInit, - ChangeDetectionStrategy, - Input, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' @Component({ selector: 'gn-ui-loading-mask', templateUrl: './loading-mask.component.html', styleUrls: ['./loading-mask.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [MatProgressSpinnerModule], + standalone: true, }) export class LoadingMaskComponent { @Input() message: string diff --git a/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.spec.ts b/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.spec.ts index c54f3ae446..df12f0dc43 100644 --- a/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.spec.ts +++ b/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.spec.ts @@ -8,7 +8,7 @@ describe('ProgressBarComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ProgressBarComponent], + imports: [ProgressBarComponent], }).compileComponents() }) diff --git a/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.ts b/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.ts index f4cf16c0f3..a35933a391 100644 --- a/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.ts +++ b/libs/ui/widgets/src/lib/progress-bar/progress-bar.component.ts @@ -10,6 +10,7 @@ interface ColorScheme { selector: 'gn-ui-progress-bar', templateUrl: './progress-bar.component.html', styleUrls: ['./progress-bar.component.css'], + standalone: true, }) export class ProgressBarComponent { @Input() value = 0 diff --git a/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.spec.ts b/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.spec.ts index e5ab2c852f..20f28bbb7b 100644 --- a/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.spec.ts +++ b/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.spec.ts @@ -8,7 +8,7 @@ describe('SpinningLoaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [SpinningLoaderComponent], + imports: [SpinningLoaderComponent], }).compileComponents() }) diff --git a/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.ts b/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.ts index 01361cb681..67fe142ae3 100644 --- a/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.ts +++ b/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.ts @@ -4,5 +4,6 @@ import { Component } from '@angular/core' selector: 'gn-ui-spinning-loader', templateUrl: './spinning-loader.component.html', styleUrls: ['./spinning-loader.component.css'], + standalone: true, }) export class SpinningLoaderComponent {} diff --git a/libs/ui/widgets/src/lib/ui-widgets.module.ts b/libs/ui/widgets/src/lib/ui-widgets.module.ts index 8ffd887ca8..26229d954f 100644 --- a/libs/ui/widgets/src/lib/ui-widgets.module.ts +++ b/libs/ui/widgets/src/lib/ui-widgets.module.ts @@ -3,23 +3,14 @@ import { UtilSharedModule } from '@geonetwork-ui/util/shared' import { TranslateModule } from '@ngx-translate/core' import { NgxDropzoneModule } from 'ngx-dropzone' import { ColorScaleComponent } from './color-scale/color-scale.component' -import { ProgressBarComponent } from './progress-bar/progress-bar.component' import { StepBarComponent } from './step-bar/step-bar.component' 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 { SpinningLoaderComponent } from './spinning-loader/spinning-loader.component' import { CommonModule } from '@angular/common' @NgModule({ - declarations: [ - ColorScaleComponent, - ProgressBarComponent, - StepBarComponent, - LoadingMaskComponent, - SpinningLoaderComponent, - ], + declarations: [ColorScaleComponent, StepBarComponent], imports: [ CommonModule, TranslateModule.forChild(), @@ -30,11 +21,6 @@ import { CommonModule } from '@angular/common' UtilSharedModule, MatProgressSpinnerModule, ], - exports: [ - ProgressBarComponent, - StepBarComponent, - LoadingMaskComponent, - SpinningLoaderComponent, - ], + exports: [StepBarComponent], }) export class UiWidgetsModule {} diff --git a/libs/util/app-config/src/lib/app-config.spec.ts b/libs/util/app-config/src/lib/app-config.spec.ts index e590d8a9ad..63c400e4b9 100644 --- a/libs/util/app-config/src/lib/app-config.spec.ts +++ b/libs/util/app-config/src/lib/app-config.spec.ts @@ -133,6 +133,7 @@ describe('app config utils', () => { PROXY_PATH: '/proxy/?url=', METADATA_LANGUAGE: 'fre', LOGIN_URL: '/cas/login?service=', + LOGOUT_URL: '/geonetwork/signout', WEB_COMPONENT_EMBEDDER_URL: '/datahub/wc-embedder.html', }) }) @@ -277,7 +278,11 @@ describe('app config utils', () => { [[map_layer]] type = "wfs" url = "https://www.geo2france.fr/geoserver/cr_hdf/ows" - name = "masque_hdf_ign_carto_latin1"` + name = "masque_hdf_ign_carto_latin1" + [[map_layer]] + type = "maplibre-style" + styleUrl = "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json" + accessToken = "any_token"` ) await loadAppConfig() }) @@ -302,6 +307,12 @@ describe('app config utils', () => { URL: 'https://www.geo2france.fr/geoserver/cr_hdf/ows', NAME: 'masque_hdf_ign_carto_latin1', }, + { + TYPE: 'maplibre-style', + STYLE_URL: + 'https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json', + ACCESS_TOKEN: 'any_token', + }, ], }) }) diff --git a/libs/util/app-config/src/lib/app-config.ts b/libs/util/app-config/src/lib/app-config.ts index 04ca6b87fd..6a459cd972 100644 --- a/libs/util/app-config/src/lib/app-config.ts +++ b/libs/util/app-config/src/lib/app-config.ts @@ -98,6 +98,7 @@ export function loadAppConfig() { 'proxy_path', 'metadata_language', 'login_url', + 'logout_url', 'web_component_embedder_url', 'languages', 'contact_email', @@ -124,6 +125,8 @@ export function loadAppConfig() { ).toLowerCase() : undefined, LOGIN_URL: parsedGlobalSection.login_url, + LOGOUT_URL: parsedGlobalSection.logout_url, + SETTINGS_URL: parsedGlobalSection.settings_url, WEB_COMPONENT_EMBEDDER_URL: parsedGlobalSection.web_component_embedder_url, LANGUAGES: parsedGlobalSection.languages, @@ -134,7 +137,7 @@ export function loadAppConfig() { parsed, 'map_layer', ['type'], - ['name', 'url', 'data'], + ['name', 'url', 'data', 'styleUrl', 'accessToken'], warnings, errors ) @@ -174,6 +177,8 @@ export function loadAppConfig() { URL: map_layer.url, NAME: map_layer.name, DATA: map_layer.data, + STYLE_URL: map_layer.styleUrl, + ACCESS_TOKEN: map_layer.accessToken, } as LayerConfig) ), } as MapConfig) diff --git a/libs/util/app-config/src/lib/fixtures.ts b/libs/util/app-config/src/lib/fixtures.ts index 2a7cec35d4..3126cb0492 100644 --- a/libs/util/app-config/src/lib/fixtures.ts +++ b/libs/util/app-config/src/lib/fixtures.ts @@ -6,6 +6,7 @@ geonetwork4_api_url = "/geonetwork/srv/api" proxy_path = "/proxy/?url=" metadata_language = "fre" login_url = "/cas/login?service=" +logout_url = "/geonetwork/signout" web_component_embedder_url = "/datahub/wc-embedder.html" [map] diff --git a/libs/util/app-config/src/lib/map-layers.ts b/libs/util/app-config/src/lib/map-layers.ts index 8f6e75ddc8..e5b42cf2f3 100644 --- a/libs/util/app-config/src/lib/map-layers.ts +++ b/libs/util/app-config/src/lib/map-layers.ts @@ -27,5 +27,11 @@ export function getMapContextLayerFromConfig( type: config.TYPE, ...(config.DATA ? { data: config.DATA } : { url: config.URL }), } + case 'maplibre-style': + return { + type: config.TYPE, + styleUrl: config.STYLE_URL, + accessToken: config.ACCESS_TOKEN, + } } } diff --git a/libs/util/app-config/src/lib/model.ts b/libs/util/app-config/src/lib/model.ts index 84682b6eee..42098acc61 100644 --- a/libs/util/app-config/src/lib/model.ts +++ b/libs/util/app-config/src/lib/model.ts @@ -6,16 +6,20 @@ export interface GlobalConfig { PROXY_PATH?: string METADATA_LANGUAGE?: string LOGIN_URL?: string + LOGOUT_URL?: string + SETTINGS_URL?: string WEB_COMPONENT_EMBEDDER_URL?: string LANGUAGES?: string[] CONTACT_EMAIL?: string } export interface LayerConfig { - TYPE: 'xyz' | 'wms' | 'wfs' | 'geojson' + TYPE: 'xyz' | 'wms' | 'wfs' | 'geojson' | 'maplibre-style' URL?: string NAME?: string DATA?: string + STYLE_URL?: string + ACCESS_TOKEN?: string } export interface MapConfig { diff --git a/libs/util/shared/src/lib/links/link-utils.spec.ts b/libs/util/shared/src/lib/links/link-utils.spec.ts index 91055ecf13..6a276cef8b 100644 --- a/libs/util/shared/src/lib/links/link-utils.spec.ts +++ b/libs/util/shared/src/lib/links/link-utils.spec.ts @@ -202,11 +202,11 @@ describe('link utils', () => { }) describe('#getBadgeColor for format', () => { - it('returns #1e5180', () => { - expect(getBadgeColor('json')).toEqual('#1e5180') + it('returns #b3cde8', () => { + expect(getBadgeColor('json')).toEqual('#b3cde8') }) - it('returns #559d7f', () => { - expect(getBadgeColor('csv')).toEqual('#559d7f') + it('returns #a6d6c0', () => { + expect(getBadgeColor('csv')).toEqual('#a6d6c0') }) }) describe('#sortPriority from formats object', () => { diff --git a/libs/util/shared/src/lib/links/link-utils.ts b/libs/util/shared/src/lib/links/link-utils.ts index a0c738b48d..5e1fa97999 100644 --- a/libs/util/shared/src/lib/links/link-utils.ts +++ b/libs/util/shared/src/lib/links/link-utils.ts @@ -7,7 +7,7 @@ export const FORMATS = { csv: { extensions: ['csv'], priority: 1, - color: '#559d7f', + color: '#a6d6c0', mimeTypes: ['text/csv', 'application/csv'], }, excel: { @@ -19,7 +19,7 @@ export const FORMATS = { 'openxmlformats-officedocument', ], priority: 2, - color: '#0f4395', + color: '#acc5e4', mimeTypes: [ 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', @@ -28,31 +28,31 @@ export const FORMATS = { geojson: { extensions: ['geojson'], priority: 3, - color: '#1e5180', + color: '#b3cde8', mimeTypes: ['application/geo+json', 'application/vnd.geo+json'], }, json: { extensions: ['json'], priority: 3, - color: '#1e5180', + color: '#b3cde8', mimeTypes: ['application/json'], }, shp: { extensions: ['shp', 'shape', 'zipped-shapefile'], priority: 4, - color: '#328556', + color: '#b2d8ba', mimeTypes: ['x-gis/x-shapefile'], }, gml: { extensions: ['gml'], priority: 5, - color: '#c92bce', + color: '#e3b3e5', mimeTypes: ['application/gml+xml', 'text/xml; subtype=gml'], }, kml: { extensions: ['kml', 'kmz'], priority: 6, - color: '#348009', + color: '#c1e6a0', mimeTypes: [ 'application/vnd.google-earth.kml+xml', 'application/vnd.google-earth.kmz', @@ -61,55 +61,55 @@ export const FORMATS = { gpkg: { extensions: ['gpkg', 'geopackage'], priority: 7, - color: '#ea79ba', + color: '#f7cce6', mimeTypes: ['application/geopackage+sqlite3'], }, zip: { extensions: ['zip', 'tar.gz'], priority: 8, - color: '#f2bb3a', + color: '#ffe7a3', mimeTypes: ['application/zip', 'application/x-zip'], }, pdf: { extensions: ['pdf'], priority: 9, - color: '#db544a', + color: '#f5b2a3', mimeTypes: ['application/pdf'], }, jpg: { extensions: ['jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp'], priority: 9, - color: '#673ab7', + color: '#d1c1e9', mimeTypes: ['image/jpg'], }, svg: { extensions: ['svg'], priority: 10, - color: '#d98294', + color: '#f3c1c9', mimeTypes: ['image/svg+xml'], }, dxf: { extensions: ['dxf'], priority: 11, - color: '#de630b', + color: '#f6ceac', mimeTypes: ['application/x-dxf', 'image/x-dxf'], }, html: { extensions: ['html', 'htm'], priority: 12, - color: '#f2bb3a', + color: '#FFF2CC', mimeTypes: ['text/html'], }, fgb: { extensions: ['fgb', 'flatgeobuf'], priority: 13, - color: '#f2bb3a', + color: '#ffe7a3', mimeTypes: ['application/flatgeobuf'], }, jsonfg: { extensions: ['jsonfg', 'jsonfgc'], priority: 14, - color: '#f2bb3a', + color: '#ffe7a3', mimeTypes: [ 'application/vnd.ogc.fg+json', 'application/vnd.ogc.fg+json;compatibility=geojson', diff --git a/libs/util/shared/src/lib/utils/format-fields.spec.ts b/libs/util/shared/src/lib/utils/format-fields.spec.ts new file mode 100644 index 0000000000..6420ccd4af --- /dev/null +++ b/libs/util/shared/src/lib/utils/format-fields.spec.ts @@ -0,0 +1,23 @@ +import { formatUserInfo } from './format-fields' + +describe('formatUserInfo', () => { + it('should format user info correctly', () => { + expect(formatUserInfo('barbie|Roberts|Barbara|UserAdmin (5)')).toEqual( + 'Barbara Roberts' + ) + }) + + it('should format user info correctly with count', () => { + expect( + formatUserInfo('barbie|Roberts|Barbara|UserAdmin (5)', true) + ).toEqual('Barbara Roberts (5)') + }) + + it('should return undefined if user info is empty', () => { + expect(formatUserInfo('')).toBeUndefined() + }) + + it('should return undefined if user info is not a string', () => { + expect(formatUserInfo(undefined)).toBeUndefined() + }) +}) diff --git a/libs/util/shared/src/lib/utils/format-fields.ts b/libs/util/shared/src/lib/utils/format-fields.ts new file mode 100644 index 0000000000..f3604251da --- /dev/null +++ b/libs/util/shared/src/lib/utils/format-fields.ts @@ -0,0 +1,11 @@ +export function formatUserInfo( + userInfo: string | unknown, + displayCount = false +): string { + const infos = (typeof userInfo === 'string' ? userInfo : '').split('|') + const count = displayCount ? ` ${infos[3].split(' ')[1]}` : '' + if (infos && infos.length === 4) { + return `${infos[2]} ${infos[1]}${count}` + } + return undefined +} diff --git a/libs/util/shared/src/lib/utils/index.ts b/libs/util/shared/src/lib/utils/index.ts index 301dce35e5..6ca3d5c95b 100644 --- a/libs/util/shared/src/lib/utils/index.ts +++ b/libs/util/shared/src/lib/utils/index.ts @@ -1,5 +1,6 @@ export * from './bytes-convert' export * from './event' +export * from './format-fields' export * from './fuzzy-filter' export * from './geojson' export * from './image-resize' diff --git a/package-lock.json b/package-lock.json index dd5c450aac..1df7052e98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,9 @@ "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@camptocamp/ogc-client": "1.1.1-dev.3e2d3cc", "@edugouvfr/ngx-dsfr": "^1.10.3", - "@geospatial-sdk/core": "0.0.5-dev.29", - "@geospatial-sdk/geocoding": "0.0.5-dev.29", + "@geospatial-sdk/core": "0.0.5-dev.31", + "@geospatial-sdk/geocoding": "0.0.5-dev.31", + "@geospatial-sdk/legend": "^0.0.5-dev.31", "@geospatial-sdk/openlayers": "0.0.5-dev.29", "@ltd/j-toml": "~1.35.2", "@messageformat/core": "^3.0.1", @@ -34,7 +35,7 @@ "@nestjs/config": "3.0.0", "@nestjs/core": "10.1.3", "@nestjs/mapped-types": "*", - "@nestjs/platform-express": "10.4.2", + "@nestjs/platform-express": "10.4.8", "@nestjs/swagger": "7.1.4", "@nestjs/typeorm": "10.0.0", "@ng-icons/core": "^25.6.1", @@ -55,7 +56,7 @@ "document-register-element": "^1.14.10", "duration-relativetimeformat": "^2.0.3", "embla-carousel": "^8.0.0-rc14", - "express": "^4.21.0", + "express": "^4.21.1", "geojson-validation": "^1.0.2", "marked": "^11.1.1", "moment": "^2.29.4", @@ -4522,17 +4523,22 @@ } }, "node_modules/@geospatial-sdk/core": { - "version": "0.0.5-dev.29", - "resolved": "https://registry.npmjs.org/@geospatial-sdk/core/-/core-0.0.5-dev.29.tgz", - "integrity": "sha512-urGQ0glk8fTqK4SkdKpuAGGh4TlFbjm+uW7FmyZ47HEvAOTcE0kvZPOsg/LtNY2VovyXVrnyavdOlUt9Rvqgcg==", + "version": "0.0.5-dev.31", + "resolved": "https://registry.npmjs.org/@geospatial-sdk/core/-/core-0.0.5-dev.31.tgz", + "integrity": "sha512-A3U7GuGgyhFnSneqpqXh80lwNJwQT8lLYBdlvKZnLo9usldI0BSSRFQo+iKgkJ1NxWMTdfpcIbefAVpDdILuqw==", "dependencies": { "proj4": "^2.9.2" } }, "node_modules/@geospatial-sdk/geocoding": { - "version": "0.0.5-dev.29", - "resolved": "https://registry.npmjs.org/@geospatial-sdk/geocoding/-/geocoding-0.0.5-dev.29.tgz", - "integrity": "sha512-WcxV6Ys6xN7AFLm4UOXkFspj/osCZ2f/OP3LiC63W/0MdwiAg3ajRldVX5pfDyPx2GTKv0ZYcxuFzi8P7ZLFaw==" + "version": "0.0.5-dev.31", + "resolved": "https://registry.npmjs.org/@geospatial-sdk/geocoding/-/geocoding-0.0.5-dev.31.tgz", + "integrity": "sha512-z6Hkb+fktKmyymDt7yS7xpCptjehZJR+BHzn0mNuawuAP2vW4+jRBj78+/jkdEvi4jKBLM+PXs3Tg1WoP2isbQ==" + }, + "node_modules/@geospatial-sdk/legend": { + "version": "0.0.5-dev.31", + "resolved": "https://registry.npmjs.org/@geospatial-sdk/legend/-/legend-0.0.5-dev.31.tgz", + "integrity": "sha512-pxy1bm6KSqkIH1KVSrBBz9fxxhzmv246bKQQDFXfkPmVaaRnNlpytq3QMQoTUb8XiOFP2p7gWR909wfmHfsNDQ==" }, "node_modules/@geospatial-sdk/openlayers": { "version": "0.0.5-dev.29", @@ -6296,13 +6302,13 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.2.tgz", - "integrity": "sha512-WQVfUyAgMZqDXWc+sIdfWZRl6+CLZhS/GB70ZiKbMNiOETbfBisQoZ1S95o+ztXZC527HnPxvwiF3GPjG/trmg==", + "version": "10.4.8", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.8.tgz", + "integrity": "sha512-bDz6wQD9LzGeK6uAAFv9l9AbrpyPwHStNObL8J95HBAXJesOblVlQMBAhdfci1YVMQUfOc36qq0IpRSa1II9Mg==", "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.21.0", + "express": "4.21.1", "multer": "1.4.4-lts.1", "tslib": "2.7.0" }, @@ -16663,9 +16669,9 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -16900,9 +16906,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -19346,16 +19352,16 @@ "dev": true }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -25768,9 +25774,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -35846,4 +35852,4 @@ "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 3bb2d63512..1077e61a34 100644 --- a/package.json +++ b/package.json @@ -59,9 +59,10 @@ "@bartholomej/ngx-translate-extract": "^8.0.2", "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@camptocamp/ogc-client": "1.1.1-dev.3e2d3cc", - "@geospatial-sdk/core": "0.0.5-dev.29", + "@geospatial-sdk/core": "0.0.5-dev.31", "@edugouvfr/ngx-dsfr": "^1.10.3", - "@geospatial-sdk/geocoding": "0.0.5-dev.29", + "@geospatial-sdk/geocoding": "0.0.5-dev.31", + "@geospatial-sdk/legend": "^0.0.5-dev.31", "@geospatial-sdk/openlayers": "0.0.5-dev.29", "@ltd/j-toml": "~1.35.2", "@messageformat/core": "^3.0.1", @@ -69,7 +70,7 @@ "@nestjs/config": "3.0.0", "@nestjs/core": "10.1.3", "@nestjs/mapped-types": "*", - "@nestjs/platform-express": "10.4.2", + "@nestjs/platform-express": "10.4.8", "@nestjs/swagger": "7.1.4", "@nestjs/typeorm": "10.0.0", "@ng-icons/core": "^25.6.1", @@ -90,7 +91,7 @@ "document-register-element": "^1.14.10", "duration-relativetimeformat": "^2.0.3", "embla-carousel": "^8.0.0-rc14", - "express": "^4.21.0", + "express": "^4.21.1", "geojson-validation": "^1.0.2", "marked": "^11.1.1", "moment": "^2.29.4", @@ -192,4 +193,4 @@ "rxjs": "^7.4.0" } } -} +} \ No newline at end of file diff --git a/tailwind.base.css b/tailwind.base.css index ad0ec12ad8..7638150011 100644 --- a/tailwind.base.css +++ b/tailwind.base.css @@ -143,12 +143,13 @@ .gn-ui-badge { --rounded: var(--gn-ui-badge-rounded, 0.25em); --padding: var(--gn-ui-badge-padding, 0.375em 0.75em); + --font-weight: var(--gn-ui-badge-font-weight, 500); --text-size: var(--gn-ui-badge-text-size, 0.875em); --text-color: var(--gn-ui-badge-text-color, var(--color-gray-50)); --background-color: var(--gn-ui-badge-background-color, black); --opacity: var(--gn-ui-badge-opacity, 0.7); @apply opacity-[--opacity] p-[--padding] rounded-[--rounded] - font-medium text-[length:--text-size] text-[color:--text-color] bg-[color:--background-color] flex justify-center items-center content-center; + font-[--font-weight] text-[length:--text-size] text-[color:--text-color] bg-[color:--background-color] flex justify-center items-center content-center; } /* makes sure icons will not make the badges grow vertically; also make size proportional */ .gn-ui-badge ng-icon { diff --git a/tools/e2e/commands.ts b/tools/e2e/commands.ts index 27024e63cb..d2a8acea66 100644 --- a/tools/e2e/commands.ts +++ b/tools/e2e/commands.ts @@ -18,6 +18,7 @@ declare namespace Cypress { clearRecordDrafts(): void editor_readFormUniqueIdentifier(): Chainable editor_wrapPreviousDraft(): void + editor_wrapFirstDraft(): void editor_publishAndReload(): void editor_findDraftInLocalStorage(): Chainable @@ -185,6 +186,18 @@ Cypress.Commands.add('editor_findDraftInLocalStorage', () => { }) }) +// this needs a recordUuid to have been wrapped +Cypress.Commands.add('editor_wrapFirstDraft', () => { + cy.get('@recordUuid').then((recordUuid) => { + cy.window() + .its('localStorage') + .invoke('getItem', `geonetwork-ui-draft-${recordUuid}`) + .then((previousDraft) => { + cy.wrap(previousDraft).as('firstDraft') + }) + }) +}) + // this needs a recordUuid to have been wrapped Cypress.Commands.add('editor_wrapPreviousDraft', () => { cy.get('@recordUuid').then((recordUuid) => { diff --git a/translations/de.json b/translations/de.json index d7f694a985..615833c769 100644 --- a/translations/de.json +++ b/translations/de.json @@ -20,19 +20,14 @@ "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.importRecord": "", "dashboard.importRecord.importExternal": "", "dashboard.importRecord.importExternalLabel": "", - "dashboard.importRecord.useModel": "", "dashboard.labels.catalog": "Katalog", "dashboard.labels.mySpace": "Mein Bereich", - "dashboard.myRecords.currentlyEdited": "", - "dashboard.myRecords.publishedMetadatas": "", "dashboard.records.all": "Metadatenkatalog", "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Meine Entwürfe", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "E-Mail", "dashboard.records.username": "Benutzername", "dashboard.records.users": "{count, plural, =1{Benutzer} other{Benutzer}}", - "dashboard.results.listMetadata": "Metadaten anzeigen", - "dashboard.results.listResources": "Ressourcen anzeigen", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "Dateiformat-Erkennung", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Sammeln von Datensatzinformationen", "datafeeder.analysisProgressBar.illustration.samplingData": "Datenauswahl", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "Die Bedingungen sind unbekannt.", "editor.record.form.constraint.otherConstraints": "", "editor.record.form.constraint.securityConstraints": "", + "editor.record.form.draft.updateAlert": "", "editor.record.form.field.abstract": "Kurzbeschreibung", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", + "editor.record.form.field.draft.only.disabled": "Dieses Feld wird aktiviert, sobald die Daten veröffentlicht wurden.", "editor.record.form.field.keywords": "Schlagwörter", "editor.record.form.field.legalConstraints": "Rechtliche Einschränkung", "editor.record.form.field.license": "Lizenz", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "Allgemeine Einschränkung", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "Datensatz zuletzt aktualisiert", + "editor.record.form.field.resourceCreated": "", + "editor.record.form.field.resourceIdentifier": "", "editor.record.form.field.resourceUpdated": "Letztes Aktualisierungsdatum", "editor.record.form.field.securityConstraints": "Sicherheitseinschränkung", "editor.record.form.field.spatialExtents": "", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "", "editor.record.placeKeywordWithoutLabel": "", "editor.record.publish": "Diesen Datensatz veröffentlichen", + "editor.record.publish.confirmation.cancelText": "", + "editor.record.publish.confirmation.confirmText": "", + "editor.record.publish.confirmation.message": "", "editor.record.publishError.body": "Der Datensatz konnte nicht veröffentlicht werden:", "editor.record.publishError.closeMessage": "Verstanden", "editor.record.publishError.title": "Fehler beim Veröffentlichen des Datensatzes", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "", "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "Dieser Datensatz ist auf dem neuesten Stand", + "editor.sidebar.logout": "", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "Datensatz aus dem Datahub", @@ -500,6 +501,7 @@ "search.error.recordNotFound": "Der Datensatz mit der Kennung \"{ id }\" konnte nicht gefunden werden.", "search.field.any.placeholder": "Suche Datensätze ...", "search.field.sortBy": "Sortieren nach:", + "search.filters.changeDate": "Letzte Aktualisierung", "search.filters.clear": "Zurücksetzen", "search.filters.contact": "Kontakte", "search.filters.format": "Formate", @@ -530,6 +532,8 @@ "search.filters.representationType": "Repräsentationstyp", "search.filters.resourceType": "Ressourcentyp", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "Geändert am: ", + "search.filters.summaryLabel.user": "Geändert von: ", "search.filters.title": "Ergebnisse filtern", "search.filters.topic": "Themen", "search.filters.useSpatialFilter": "Zuerst Datensätze im Interessenbereich anzeigen", diff --git a/translations/en.json b/translations/en.json index fda0abed93..8d7d77c87a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -20,19 +20,14 @@ "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.importRecord": "Import", "dashboard.importRecord.importExternal": "Import an external file", "dashboard.importRecord.importExternalLabel": "External file URL", - "dashboard.importRecord.useModel": "Use a model", "dashboard.labels.catalog": "Catalog", "dashboard.labels.mySpace": "My space", - "dashboard.myRecords.currentlyEdited": "Currently edited", - "dashboard.myRecords.publishedMetadatas": "Published metadatas", "dashboard.records.all": "Metadata records", "dashboard.records.hasDraft": "draft", "dashboard.records.myDraft": "My drafts", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "Email", "dashboard.records.username": "Username", "dashboard.records.users": "{count, plural, =1{user} other{users}}", - "dashboard.results.listMetadata": "Show metadata", - "dashboard.results.listResources": "Show resources", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "File format \n detection", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Gathering dataset \n information", "datafeeder.analysisProgressBar.illustration.samplingData": "Sampling \n data", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "The conditions are unknown.", "editor.record.form.constraint.otherConstraints": "Other constraints", "editor.record.form.constraint.securityConstraints": "Security constraints", + "editor.record.form.draft.updateAlert": "Since you created this draft, the record has been updated on { date } by { user }. Publishing your draft might erase their edits. To avoid this, you need to either cancel your changes or knowingly publish your own version.", "editor.record.form.field.abstract": "Abstract", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "Please provide at least one point of contact.", "editor.record.form.field.contactsForResource.noContact": "Please provide at least one point of contact responsible for the data.", + "editor.record.form.field.draft.only.disabled": "This field will be enabled once the data has been published", "editor.record.form.field.keywords": "Keywords", "editor.record.form.field.legalConstraints": "Legal constraint", "editor.record.form.field.license": "License", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "Other constraint", "editor.record.form.field.overviews": "Overviews", "editor.record.form.field.recordUpdated": "Record Updated", + "editor.record.form.field.resourceCreated": "Resource Created", + "editor.record.form.field.resourceIdentifier": "Identifier", "editor.record.form.field.resourceUpdated": "Resource Updated", "editor.record.form.field.securityConstraints": "Security constraint", "editor.record.form.field.spatialExtents": "Spatial extents", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "Error adding resource", "editor.record.placeKeywordWithoutLabel": "Unnamed location", "editor.record.publish": "Publish this record", + "editor.record.publish.confirmation.cancelText": "Cancel", + "editor.record.publish.confirmation.confirmText": "Publish", + "editor.record.publish.confirmation.message": "Since you created this draft, the record has been updated on { date } by { user }. Publishing your draft might erase their edits. Do you wish to proceed ?", "editor.record.publishError.body": "The record could not be published:", "editor.record.publishError.closeMessage": "Understood", "editor.record.publishError.title": "Error publishing record", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "There are no pending changes on this record", "editor.record.undo.tooltip.enabled": "Clicking this button will cancel the pending changes on this record.", "editor.record.upToDate": "This record is up to date", + "editor.sidebar.logout": "Log out", "editor.sidebar.menu.editor": "Editor", "editor.temporary.disabled": "Not implemented yet", "externalviewer.dataset.unnamed": "Datahub layer", @@ -502,6 +503,7 @@ "search.error.recordNotFound": "The record with identifier \"{ id }\" could not be found.", "search.field.any.placeholder": "Search datasets ...", "search.field.sortBy": "Sort by:", + "search.filters.changeDate": "Last updated", "search.filters.clear": "Reset", "search.filters.contact": "Contacts", "search.filters.format": "Formats", @@ -532,6 +534,8 @@ "search.filters.representationType": "Representation type", "search.filters.resourceType": "Resource type", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "Modified on: ", + "search.filters.summaryLabel.user": "Modified by: ", "search.filters.title": "Filter your results", "search.filters.topic": "Topics", "search.filters.useSpatialFilter": "Show records in the area of interest first", diff --git a/translations/es.json b/translations/es.json index fcc99e4f0f..1bed5d65f9 100644 --- a/translations/es.json +++ b/translations/es.json @@ -20,19 +20,14 @@ "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.importRecord": "", "dashboard.importRecord.importExternal": "", "dashboard.importRecord.importExternalLabel": "", - "dashboard.importRecord.useModel": "", "dashboard.labels.catalog": "Catálogo", "dashboard.labels.mySpace": "Mi espacio", - "dashboard.myRecords.currentlyEdited": "", - "dashboard.myRecords.publishedMetadatas": "", "dashboard.records.all": "Catálogo", "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Mis borradores", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "", "dashboard.records.username": "", "dashboard.records.users": "", - "dashboard.results.listMetadata": "", - "dashboard.results.listResources": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "", "datafeeder.analysisProgressBar.illustration.samplingData": "", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "", "editor.record.form.constraint.otherConstraints": "", "editor.record.form.constraint.securityConstraints": "", + "editor.record.form.draft.updateAlert": "", "editor.record.form.field.abstract": "", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", + "editor.record.form.field.draft.only.disabled": "", "editor.record.form.field.keywords": "", "editor.record.form.field.legalConstraints": "", "editor.record.form.field.license": "", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceCreated": "", + "editor.record.form.field.resourceIdentifier": "", "editor.record.form.field.resourceUpdated": "", "editor.record.form.field.securityConstraints": "", "editor.record.form.field.spatialExtents": "", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "", "editor.record.placeKeywordWithoutLabel": "", "editor.record.publish": "", + "editor.record.publish.confirmation.cancelText": "", + "editor.record.publish.confirmation.confirmText": "", + "editor.record.publish.confirmation.message": "", "editor.record.publishError.body": "", "editor.record.publishError.closeMessage": "", "editor.record.publishError.title": "", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "", "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", + "editor.sidebar.logout": "", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", @@ -500,6 +501,7 @@ "search.error.recordNotFound": "", "search.field.any.placeholder": "", "search.field.sortBy": "", + "search.filters.changeDate": "Última actualización", "search.filters.clear": "", "search.filters.contact": "", "search.filters.format": "", @@ -530,6 +532,8 @@ "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "", "search.filters.topic": "", "search.filters.useSpatialFilter": "", diff --git a/translations/fr.json b/translations/fr.json index 3a8b53e0fb..7da9a41ec0 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -20,19 +20,14 @@ "chart.type.lineSmooth": "ligne lisse", "chart.type.pie": "camembert", "dashboard.catalog.allRecords": "Fiches de métadonnées", - "dashboard.catalog.calendar": "Calendrier", "dashboard.catalog.contacts": "Annuaire", - "dashboard.catalog.discussion": "Discussions", "dashboard.catalog.thesaurus": "Thesaurus", "dashboard.createRecord": "Nouvel enregistrement", "dashboard.importRecord": "Importer", "dashboard.importRecord.importExternal": "Importer une fiche externe", "dashboard.importRecord.importExternalLabel": "URL de la fiche externe", - "dashboard.importRecord.useModel": "Utiliser un modèle", "dashboard.labels.catalog": "Catalogue", "dashboard.labels.mySpace": "Mon espace", - "dashboard.myRecords.currentlyEdited": "En cours d'édition", - "dashboard.myRecords.publishedMetadatas": "Fiches publiées", "dashboard.records.all": "Fiches de métadonnées", "dashboard.records.hasDraft": "brouillon", "dashboard.records.myDraft": "Mes brouillons", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "Email", "dashboard.records.username": "Nom d'utilisateur", "dashboard.records.users": "{count, plural, =1{utilisateur} other{utilisateurs}}", - "dashboard.results.listMetadata": "Afficher les métadonnées", - "dashboard.results.listResources": "Afficher les ressources", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "Détection du \n format de fichier", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Récupération des informations \n sur le jeu de données", "datafeeder.analysisProgressBar.illustration.samplingData": "Échantillonnage \n des données", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "Les conditions sont inconnues.", "editor.record.form.constraint.otherConstraints": "Autres contraintes", "editor.record.form.constraint.securityConstraints": "Contraintes de sécurité", + "editor.record.form.draft.updateAlert": "Depuis la création de ce brouillon, cette fiche a été modifiée le { date } par { user }. Publier votre version peut supprimer ses modifications. Pour éviter cela, vous pouvez annuler vos changements, ou publier votre version en connaissance de cause.", "editor.record.form.field.abstract": "Résumé", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "Veuillez renseigner au moins un point de contact.", "editor.record.form.field.contactsForResource.noContact": "Veuillez renseigner au moins un point de contact responsable de la donnée.", + "editor.record.form.field.draft.only.disabled": "Ce champ sera activé une fois les données publiées", "editor.record.form.field.keywords": "Mots-clés", "editor.record.form.field.legalConstraints": "Contrainte légale", "editor.record.form.field.license": "Licence", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "Contrainte générale", "editor.record.form.field.overviews": "Aperçus", "editor.record.form.field.recordUpdated": "Date de dernière révision", + "editor.record.form.field.resourceCreated": "", + "editor.record.form.field.resourceIdentifier": "", "editor.record.form.field.resourceUpdated": "Date de dernière révision", "editor.record.form.field.securityConstraints": "Contrainte de sécurité", "editor.record.form.field.spatialExtents": "Étendue spatiale", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "Erreur lors de l'ajout d'une ressource", "editor.record.placeKeywordWithoutLabel": "Localisation sans nom", "editor.record.publish": "Publier cette fiche", + "editor.record.publish.confirmation.cancelText": "Annuler", + "editor.record.publish.confirmation.confirmText": "Publier", + "editor.record.publish.confirmation.message": "Depuis la création de votre brouillon, cette fiche a été modifiée le { date } par { user }. Publier votre version pourrait supprimer ses modifications. Souhaitez-vous poursuivre ?", "editor.record.publishError.body": "La fiche n'a pas pu être publiée :", "editor.record.publishError.closeMessage": "Compris", "editor.record.publishError.title": "Erreur lors de la publication", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "Il n'y a pas de modifications en cours sur cette fiche", "editor.record.undo.tooltip.enabled": "Cliquez sur ce bouton pour annuler les modifications apportées à cette fiche", "editor.record.upToDate": "", + "editor.sidebar.logout": "Se déconnecter", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "Pas encore implémenté", "externalviewer.dataset.unnamed": "Couche du datahub", @@ -461,7 +462,7 @@ "record.metadata.temporalExtent.fromDateToDate": "Du { start } au { end }", "record.metadata.temporalExtent.sinceDate": "Depuis le { start }", "record.metadata.temporalExtent.untilDate": "Jusqu'au { end }", - "record.metadata.title": "Titre", + "record.metadata.title": "Intitulé", "record.metadata.topics": "Catégories", "record.metadata.type": "Donnée géographique", "record.metadata.uniqueId": "Identificateur de ressource unique", @@ -502,6 +503,7 @@ "search.error.recordNotFound": "Cette donnée n'a pu être trouvée.", "search.field.any.placeholder": "Rechercher une donnée...", "search.field.sortBy": "Trier par :", + "search.filters.changeDate": "Dernière mise à jour", "search.filters.clear": "Réinitialiser", "search.filters.contact": "Contacts", "search.filters.format": "Formats", @@ -532,6 +534,8 @@ "search.filters.representationType": "Type de représentation", "search.filters.resourceType": "Type de ressource", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "Modifiée le : ", + "search.filters.summaryLabel.user": "Modifiée par : ", "search.filters.title": "Affiner votre recherche", "search.filters.topic": "Thèmes", "search.filters.useSpatialFilter": "Mettre en avant les résultats sur la zone d'intérêt", diff --git a/translations/it.json b/translations/it.json index 6c0d71efba..19217bb533 100644 --- a/translations/it.json +++ b/translations/it.json @@ -20,19 +20,14 @@ "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.importRecord": "", "dashboard.importRecord.importExternal": "", "dashboard.importRecord.importExternalLabel": "", - "dashboard.importRecord.useModel": "", "dashboard.labels.catalog": "Catalogo", "dashboard.labels.mySpace": "Il mio spazio", - "dashboard.myRecords.currentlyEdited": "", - "dashboard.myRecords.publishedMetadatas": "", "dashboard.records.all": "Catalogo", "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Le mie bozze", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "Email", "dashboard.records.username": "Nome utente", "dashboard.records.users": "utenti", - "dashboard.results.listMetadata": "Elenco dei metadati", - "dashboard.results.listResources": "Elenco delle risorse", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "Rilevamento del formato dei file", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Recupero delle informazioni dal dataset", "datafeeder.analysisProgressBar.illustration.samplingData": "Campionatura dei dati", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "", "editor.record.form.constraint.otherConstraints": "", "editor.record.form.constraint.securityConstraints": "", + "editor.record.form.draft.updateAlert": "", "editor.record.form.field.abstract": "", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", + "editor.record.form.field.draft.only.disabled": "", "editor.record.form.field.keywords": "", "editor.record.form.field.legalConstraints": "", "editor.record.form.field.license": "Licenza", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceCreated": "", + "editor.record.form.field.resourceIdentifier": "", "editor.record.form.field.resourceUpdated": "", "editor.record.form.field.securityConstraints": "", "editor.record.form.field.spatialExtents": "", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "", "editor.record.placeKeywordWithoutLabel": "", "editor.record.publish": "", + "editor.record.publish.confirmation.cancelText": "", + "editor.record.publish.confirmation.confirmText": "", + "editor.record.publish.confirmation.message": "", "editor.record.publishError.body": "", "editor.record.publishError.closeMessage": "", "editor.record.publishError.title": "", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "", "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", + "editor.sidebar.logout": "", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "Layer del datahub", @@ -500,6 +501,7 @@ "search.error.recordNotFound": "Impossibile trovare questo dato", "search.field.any.placeholder": "Cerca un dato...", "search.field.sortBy": "Ordina per:", + "search.filters.changeDate": "", "search.filters.clear": "Ripristina", "search.filters.contact": "Contatti", "search.filters.format": "Formati", @@ -530,6 +532,8 @@ "search.filters.representationType": "Tipo di rappresentazione", "search.filters.resourceType": "Tipo di risorsa", "search.filters.standard": "Standard", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "Affina la sua ricerca", "search.filters.topic": "Argomenti", "search.filters.useSpatialFilter": "Evidenzia i risultati nell'area di interesse", diff --git a/translations/nl.json b/translations/nl.json index 99409151a3..8a459ff95a 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -20,19 +20,14 @@ "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.importRecord": "", "dashboard.importRecord.importExternal": "", "dashboard.importRecord.importExternalLabel": "", - "dashboard.importRecord.useModel": "", "dashboard.labels.catalog": "Catalogus", "dashboard.labels.mySpace": "Mijn ruimte", - "dashboard.myRecords.currentlyEdited": "", - "dashboard.myRecords.publishedMetadatas": "", "dashboard.records.all": "Catalogus", "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Mijn concepten", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "", "dashboard.records.username": "", "dashboard.records.users": "", - "dashboard.results.listMetadata": "", - "dashboard.results.listResources": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "", "datafeeder.analysisProgressBar.illustration.samplingData": "", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "", "editor.record.form.constraint.otherConstraints": "", "editor.record.form.constraint.securityConstraints": "", + "editor.record.form.draft.updateAlert": "", "editor.record.form.field.abstract": "", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", + "editor.record.form.field.draft.only.disabled": "", "editor.record.form.field.keywords": "", "editor.record.form.field.legalConstraints": "", "editor.record.form.field.license": "", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceCreated": "", + "editor.record.form.field.resourceIdentifier": "", "editor.record.form.field.resourceUpdated": "", "editor.record.form.field.securityConstraints": "", "editor.record.form.field.spatialExtents": "", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "", "editor.record.placeKeywordWithoutLabel": "", "editor.record.publish": "", + "editor.record.publish.confirmation.cancelText": "", + "editor.record.publish.confirmation.confirmText": "", + "editor.record.publish.confirmation.message": "", "editor.record.publishError.body": "", "editor.record.publishError.closeMessage": "", "editor.record.publishError.title": "", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "", "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", + "editor.sidebar.logout": "", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", @@ -500,6 +501,7 @@ "search.error.recordNotFound": "", "search.field.any.placeholder": "", "search.field.sortBy": "", + "search.filters.changeDate": "", "search.filters.clear": "", "search.filters.contact": "", "search.filters.format": "", @@ -530,6 +532,8 @@ "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "", "search.filters.topic": "", "search.filters.useSpatialFilter": "", diff --git a/translations/pt.json b/translations/pt.json index 2e15157d6e..544401e616 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -20,19 +20,14 @@ "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.importRecord": "", "dashboard.importRecord.importExternal": "", "dashboard.importRecord.importExternalLabel": "", - "dashboard.importRecord.useModel": "", "dashboard.labels.catalog": "Catálogo", "dashboard.labels.mySpace": "Meu espaço", - "dashboard.myRecords.currentlyEdited": "", - "dashboard.myRecords.publishedMetadatas": "", "dashboard.records.all": "Catálogo", "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Meus rascunhos", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "", "dashboard.records.username": "", "dashboard.records.users": "", - "dashboard.results.listMetadata": "", - "dashboard.results.listResources": "", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "", "datafeeder.analysisProgressBar.illustration.samplingData": "", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "", "editor.record.form.constraint.otherConstraints": "", "editor.record.form.constraint.securityConstraints": "", + "editor.record.form.draft.updateAlert": "", "editor.record.form.field.abstract": "", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", + "editor.record.form.field.draft.only.disabled": "", "editor.record.form.field.keywords": "", "editor.record.form.field.legalConstraints": "", "editor.record.form.field.license": "", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceCreated": "", + "editor.record.form.field.resourceIdentifier": "", "editor.record.form.field.resourceUpdated": "", "editor.record.form.field.securityConstraints": "", "editor.record.form.field.spatialExtents": "", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "", "editor.record.placeKeywordWithoutLabel": "", "editor.record.publish": "", + "editor.record.publish.confirmation.cancelText": "", + "editor.record.publish.confirmation.confirmText": "", + "editor.record.publish.confirmation.message": "", "editor.record.publishError.body": "", "editor.record.publishError.closeMessage": "", "editor.record.publishError.title": "", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "", "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", + "editor.sidebar.logout": "", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", @@ -500,6 +501,7 @@ "search.error.recordNotFound": "", "search.field.any.placeholder": "", "search.field.sortBy": "", + "search.filters.changeDate": "", "search.filters.clear": "", "search.filters.contact": "", "search.filters.format": "", @@ -530,6 +532,8 @@ "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "", "search.filters.topic": "", "search.filters.useSpatialFilter": "", diff --git a/translations/sk.json b/translations/sk.json index 64d84d7f89..5ecf1c88d0 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -20,19 +20,14 @@ "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.importRecord": "", "dashboard.importRecord.importExternal": "", "dashboard.importRecord.importExternalLabel": "", - "dashboard.importRecord.useModel": "", "dashboard.labels.catalog": "Katalóg", "dashboard.labels.mySpace": "Môj priestor", - "dashboard.myRecords.currentlyEdited": "", - "dashboard.myRecords.publishedMetadatas": "", "dashboard.records.all": "Katalóg", "dashboard.records.hasDraft": "", "dashboard.records.myDraft": "Moje koncepty", @@ -43,8 +38,6 @@ "dashboard.records.userEmail": "Email", "dashboard.records.username": "Užívateľské meno", "dashboard.records.users": "{count, plural, =1{užívateľ} other{užívatelia}}", - "dashboard.results.listMetadata": "Zobraziť metadata", - "dashboard.results.listResources": "Zobraziť zdroje", "datafeeder.analysisProgressBar.illustration.fileFormatDetection": "Detekcia formátu súboru", "datafeeder.analysisProgressBar.illustration.gatheringDatasetInformation": "Zbieranie informácií o datasete", "datafeeder.analysisProgressBar.illustration.samplingData": "Vzorkovanie dát", @@ -218,10 +211,12 @@ "editor.record.form.constraint.not.known": "", "editor.record.form.constraint.otherConstraints": "", "editor.record.form.constraint.securityConstraints": "", + "editor.record.form.draft.updateAlert": "", "editor.record.form.field.abstract": "", "editor.record.form.field.constraintsShortcuts": "", "editor.record.form.field.contacts.noContact": "", "editor.record.form.field.contactsForResource.noContact": "", + "editor.record.form.field.draft.only.disabled": "", "editor.record.form.field.keywords": "", "editor.record.form.field.legalConstraints": "", "editor.record.form.field.license": "Licencia", @@ -240,6 +235,8 @@ "editor.record.form.field.otherConstraints": "", "editor.record.form.field.overviews": "", "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceCreated": "", + "editor.record.form.field.resourceIdentifier": "", "editor.record.form.field.resourceUpdated": "", "editor.record.form.field.securityConstraints": "", "editor.record.form.field.spatialExtents": "", @@ -291,6 +288,9 @@ "editor.record.onlineResourceError.title": "", "editor.record.placeKeywordWithoutLabel": "", "editor.record.publish": "", + "editor.record.publish.confirmation.cancelText": "", + "editor.record.publish.confirmation.confirmText": "", + "editor.record.publish.confirmation.message": "", "editor.record.publishError.body": "", "editor.record.publishError.closeMessage": "", "editor.record.publishError.title": "", @@ -312,6 +312,7 @@ "editor.record.undo.tooltip.disabled": "", "editor.record.undo.tooltip.enabled": "", "editor.record.upToDate": "", + "editor.sidebar.logout": "", "editor.sidebar.menu.editor": "", "editor.temporary.disabled": "", "externalviewer.dataset.unnamed": "", @@ -500,6 +501,7 @@ "search.error.recordNotFound": "Záznam s identifikátorom \"{ id }\" sa nepodarilo nájsť.", "search.field.any.placeholder": "Hľadať datasety ...", "search.field.sortBy": "Zoradiť podľa:", + "search.filters.changeDate": "", "search.filters.clear": "Obnoviť", "search.filters.contact": "Kontakty", "search.filters.format": "Formáty", @@ -530,6 +532,8 @@ "search.filters.representationType": "Typ reprezentácie", "search.filters.resourceType": "Typ zdroja", "search.filters.standard": "Štandard", + "search.filters.summaryLabel.changeDate": "", + "search.filters.summaryLabel.user": "", "search.filters.title": "Filtrovanie výsledkov", "search.filters.topic": "Témy", "search.filters.useSpatialFilter": "Najskôr zobraziť záznamy v oblasti záujmu",