From 263ebb6b3d0a66d5637864b9b738a20e202498a6 Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Tue, 28 May 2024 09:09:17 +0200 Subject: [PATCH 001/378] feat(wc): add a web component for datasets figure --- .../gn-figure-datasets.component.css | 1 + .../gn-figure-datasets.component.html | 7 +++ .../gn-figure-datasets.component.ts | 39 +++++++++++++++++ .../gn-figure-datasets.sample.html | 43 +++++++++++++++++++ .../src/app/webcomponents.module.ts | 5 +++ apps/webcomponents/src/index.html | 3 ++ 6 files changed, 98 insertions(+) create mode 100644 apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.css create mode 100644 apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.html create mode 100644 apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts create mode 100644 apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.sample.html diff --git a/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.css b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.css new file mode 100644 index 0000000000..f1146d699b --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.css @@ -0,0 +1 @@ +@import '../../../styles.css'; diff --git a/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.html b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.html new file mode 100644 index 0000000000..2b3811e767 --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.html @@ -0,0 +1,7 @@ + diff --git a/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts new file mode 100644 index 0000000000..046aaeae89 --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts @@ -0,0 +1,39 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Injector, + ViewEncapsulation, +} from '@angular/core' +import { BaseComponent } from '../base.component' +import { RecordsService } from '@geonetwork-ui/feature/catalog' +import { startWith } from 'rxjs/operators' +import { Observable } from 'rxjs' +import { SearchFacade } from '@geonetwork-ui/feature/search' + +@Component({ + selector: 'wc-gn-figure-datasets', + templateUrl: './gn-figure-datasets.component.html', + styleUrls: ['./gn-figure-datasets.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.ShadowDom, + providers: [SearchFacade], +}) +export class GnFigureDatasetsComponent extends BaseComponent { + catalogRecords: RecordsService + recordsCount$: Observable + + constructor(injector: Injector, private changeDetector: ChangeDetectorRef) { + super(injector) + this.catalogRecords = injector.get(RecordsService) + this.recordsCount$ = this.catalogRecords.recordsCount$.pipe(startWith('-')) + } + + init(): void { + super.init() + } + + changes(): void { + super.changes() + } +} diff --git a/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.sample.html b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.sample.html new file mode 100644 index 0000000000..2ab65adf2a --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.sample.html @@ -0,0 +1,43 @@ + + + + + Web Component Demo - Figure datasets + + + + + + + + +
+

Figure - Datasets

+
+ + + +
+
+ + diff --git a/apps/webcomponents/src/app/webcomponents.module.ts b/apps/webcomponents/src/app/webcomponents.module.ts index 20d665a9b2..37862f3fff 100644 --- a/apps/webcomponents/src/app/webcomponents.module.ts +++ b/apps/webcomponents/src/app/webcomponents.module.ts @@ -34,6 +34,8 @@ 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' +import { GnFigureDatasetsComponent } from './components/gn-figure-datasets/gn-figure-datasets.component' +import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ [GnFacetsComponent, 'gn-facets'], @@ -43,6 +45,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ [GnDatasetViewTableComponent, 'gn-dataset-view-table'], [GnDatasetViewChartComponent, 'gn-dataset-view-chart'], [GnMapViewerComponent, 'gn-map-viewer'], + [GnFigureDatasetsComponent, 'gn-figure-datasets'], ] @NgModule({ @@ -57,6 +60,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ GnDatasetViewTableComponent, GnDatasetViewChartComponent, GnMapViewerComponent, + GnFigureDatasetsComponent, ], imports: [ CommonModule, @@ -64,6 +68,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ UiInputsModule, UiSearchModule, UiElementsModule, + UiDatavizModule, FeatureSearchModule, FeatureRecordModule, FeatureMapModule, diff --git a/apps/webcomponents/src/index.html b/apps/webcomponents/src/index.html index 2b86439b67..5368d639da 100644 --- a/apps/webcomponents/src/index.html +++ b/apps/webcomponents/src/index.html @@ -84,6 +84,9 @@

GeoNetwork demo

  • Map viewer
  • +
  • + Figures - Datasets +
  • From 2ec961846d1c11e5aa0e6edb702ffed486f84f1f Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Tue, 28 May 2024 09:10:18 +0200 Subject: [PATCH 002/378] fix(wc): don't initiate API obsevables in class declaration --- .../catalog/src/lib/records/records.service.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libs/feature/catalog/src/lib/records/records.service.ts b/libs/feature/catalog/src/lib/records/records.service.ts index 1d435aa47b..8c1fa8cd22 100644 --- a/libs/feature/catalog/src/lib/records/records.service.ts +++ b/libs/feature/catalog/src/lib/records/records.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Observable, of } from 'rxjs' +import { Observable, of, switchMap } from 'rxjs' import { catchError, shareReplay } from 'rxjs/operators' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @@ -7,12 +7,11 @@ import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/reposit providedIn: 'root', }) export class RecordsService { - recordsCount$: Observable = this.recordsRepository - .getMatchesCount({}) - .pipe( - shareReplay(1), - catchError(() => of(0)) - ) + recordsCount$: Observable = of(0).pipe( + switchMap(() => this.recordsRepository.getMatchesCount({})), + shareReplay(1), + catchError(() => of(0)) + ) constructor(private recordsRepository: RecordsRepositoryInterface) {} } From 1f253f9275631cdfe4785c5abc5a50f5a53571af Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Tue, 28 May 2024 09:13:39 +0200 Subject: [PATCH 003/378] doc(wc): improve add a new web component section --- apps/webcomponents/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/webcomponents/README.md b/apps/webcomponents/README.md index d842da3b72..215d6a5d1d 100644 --- a/apps/webcomponents/README.md +++ b/apps/webcomponents/README.md @@ -83,6 +83,20 @@ To export content as a Web Component you have to: } ``` +- Provide the dependencies which are not inject in root by default (eg `SearchFacade`, `SearchService`, etc.) + +```typescript +{ + providers: [SearchFacade] +} +``` + +- import gnui styles from the component css file + +```css +@import '../../../styles.css'; +``` + - add your component in application module `webcomponents.module.ts` `declarations` list. - register your component as a custom element in the `CUSTOM_ELEMENTS` array in application module `webcomponents.ts`, the custom element identifier (i.e Web Component tag name) _must_ be the same as the component folder name From 943c8845290c09abd7a79e9908e53f74482a083a Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Tue, 11 Jun 2024 15:48:22 +0200 Subject: [PATCH 004/378] feat: implement csv in datafeeder --- apps/datafeeder/src/app/app-routing.module.ts | 6 + apps/datafeeder/src/app/app.module.ts | 2 + .../upload-data-rules.component.ts | 8 +- .../upload-data/upload-data.component.ts | 7 +- .../analysis-progress.page.spec.ts | 12 +- .../analysis-progress.page.ts | 16 +- .../dataset-validation-csv-page.css | 4 + .../dataset-validation-csv-page.html | 113 ++++++++++ .../dataset-validation-csv-page.spec.ts | 117 +++++++++++ .../dataset-validation-csv-page.ts | 196 ++++++++++++++++++ .../forms-page/forms-page.component.spec.ts | 18 ++ .../pages/forms-page/forms-page.component.ts | 20 +- .../success-publish-page.component.html | 13 +- .../success-publish-page.component.spec.ts | 3 +- .../success-publish-page.component.ts | 22 +- .../summarize-page.component.ts | 6 + .../upload-data-page/upload-data.page.html | 2 +- .../app/router/upload-progress.guard.spec.ts | 3 +- .../src/app/router/upload-progress.guard.ts | 18 +- .../model/datasetMetadata.api.model.ts | 4 + .../model/datasetUploadStatus.api.model.ts | 2 + support-services/docker-compose.yml | 15 ++ translations/de.json | 9 + translations/en.json | 12 ++ translations/es.json | 9 + translations/fr.json | 14 +- translations/it.json | 9 + translations/nl.json | 9 + translations/pt.json | 9 + translations/sk.json | 9 + 30 files changed, 660 insertions(+), 27 deletions(-) create mode 100644 apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.css create mode 100644 apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.html create mode 100644 apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.spec.ts create mode 100644 apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.ts diff --git a/apps/datafeeder/src/app/app-routing.module.ts b/apps/datafeeder/src/app/app-routing.module.ts index ca106c5939..a62670161e 100644 --- a/apps/datafeeder/src/app/app-routing.module.ts +++ b/apps/datafeeder/src/app/app-routing.module.ts @@ -11,6 +11,7 @@ import { PublicationLockGuard } from './router/publication-lock.guard' import { PublicationStatusGuard } from './router/publication-status.guard' import { UploadProgressGuard } from './router/upload-progress.guard' import { UploadStatusGuard } from './router/upload-status.guard' +import { DatasetValidationCsvPageComponent } from './presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page' const routes: Routes = [ { path: '', component: UploadDataPageComponent }, @@ -24,6 +25,11 @@ const routes: Routes = [ component: DatasetValidationPageComponent, canActivate: [UploadStatusGuard, PublicationLockGuard], }, + { + path: ':id/validation-csv', + component: DatasetValidationCsvPageComponent, + canActivate: [UploadStatusGuard, PublicationLockGuard], + }, { path: ':id/step/:stepId', component: FormsPageComponent, diff --git a/apps/datafeeder/src/app/app.module.ts b/apps/datafeeder/src/app/app.module.ts index 21237b8d8d..71dfe67482 100644 --- a/apps/datafeeder/src/app/app.module.ts +++ b/apps/datafeeder/src/app/app.module.ts @@ -38,6 +38,7 @@ import { DATAFEEDER_STATE_KEY, reducer } from './store/datafeeder.reducer' import { FeatureAuthModule } from '@geonetwork-ui/feature/auth' import { MatIconModule } from '@angular/material/icon' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { DatasetValidationCsvPageComponent } from './presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page' export function apiConfigurationFactory() { return new Configuration({ @@ -54,6 +55,7 @@ export function apiConfigurationFactory() { UploadDataRulesComponent, AnalysisProgressPageComponent, DatasetValidationPageComponent, + DatasetValidationCsvPageComponent, DataImportValidationMapPanelComponent, UploadDataErrorDialogComponent, UploadDataBackgroundComponent, diff --git a/apps/datafeeder/src/app/presentation/components/upload-data-rules/upload-data-rules.component.ts b/apps/datafeeder/src/app/presentation/components/upload-data-rules/upload-data-rules.component.ts index d93b6eab9d..1a84c3f7e1 100644 --- a/apps/datafeeder/src/app/presentation/components/upload-data-rules/upload-data-rules.component.ts +++ b/apps/datafeeder/src/app/presentation/components/upload-data-rules/upload-data-rules.component.ts @@ -7,5 +7,11 @@ import { Component, Input } from '@angular/core' }) export class UploadDataRulesComponent { @Input() maxFileSizeMb: number - @Input() acceptedFileFormats = ['SHP', 'GeoJSON', 'GeoPackage', 'Spatialite'] + @Input() acceptedFileFormats = [ + 'SHP', + 'GeoJSON', + 'GeoPackage', + 'Spatialite', + 'CSV', + ] } diff --git a/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts b/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts index c317e5188a..c139e3125d 100644 --- a/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts +++ b/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts @@ -25,7 +25,12 @@ export class UploadDataComponent { haveRights = false uploading = false // Edge use uncommon 'application/x-zip-compressed' mime type - acceptedMimeType = ['.zip', 'application/zip', 'application/x-zip-compressed'] + acceptedMimeType = [ + '.zip', + 'application/zip', + 'application/x-zip-compressed', + 'text/csv', + ] @Input() maxFileSizeMb: number @Output() errors$ = new EventEmitter() diff --git a/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.spec.ts b/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.spec.ts index 27c302f570..d6bca490d5 100644 --- a/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.spec.ts +++ b/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.spec.ts @@ -1,13 +1,5 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' -import { - ComponentFixture, - discardPeriodicTasks, - fakeAsync, - flush, - flushMicrotasks, - TestBed, - tick, -} from '@angular/core/testing' +import { ComponentFixture, TestBed } from '@angular/core/testing' import { ActivatedRoute, Router } from '@angular/router' import { AnalysisStatusEnumApiModel, @@ -25,7 +17,7 @@ const jobMock: UploadJobStatusApiModel = { jobId: JOB_ID, status: AnalysisStatusEnumApiModel.Done, progress: 1, - datasets: [{}], + datasets: [{ format: 'SHAPEFILE' }], } const jobMockNoDS: UploadJobStatusApiModel = { jobId: JOB_ID, diff --git a/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.ts b/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.ts index 0a04106dbc..69583a65db 100644 --- a/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.ts +++ b/apps/datafeeder/src/app/presentation/pages/analysis-progress-page/analysis-progress.page.ts @@ -9,6 +9,7 @@ import { import { EMPTY, firstValueFrom, Observable, timer } from 'rxjs' import { DatafeederFacade } from '../../../store/datafeeder.facade' import { expand, switchMap } from 'rxjs/operators' +import { UploadProgressGuard } from '../../../router/upload-progress.guard' const { Pending, Analyzing, Done } = AnalysisStatusEnumApiModel @@ -51,9 +52,16 @@ export class AnalysisProgressPageComponent implements OnInit { onJobFinish(job: UploadJobStatusApiModel) { const done = job.status === Done && job.datasets?.length > 0 - this.router.navigate([done ? 'validation' : '/'], { - relativeTo: this.activatedRoute, - queryParams: done ? {} : { error: 'analysis' }, - }) + this.router.navigate( + [ + done + ? UploadProgressGuard.getRedirectPage(job.datasets[0].format) + : '/', + ], + { + relativeTo: this.activatedRoute, + queryParams: done ? {} : { error: 'analysis' }, + } + ) } } diff --git a/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.css b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.css new file mode 100644 index 0000000000..2453e29160 --- /dev/null +++ b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.css @@ -0,0 +1,4 @@ +:host { + display: flex; + flex: 1; +} diff --git a/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.html b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.html new file mode 100644 index 0000000000..3410b7932e --- /dev/null +++ b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.html @@ -0,0 +1,113 @@ +
    +
    +
    + datafeeder.datasetValidation.title +
    +
    + +
    +
    + datafeeder.datasetValidation.datasetInformation +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + datafeeder.datasetValidationCsv.lineNumbers +
    +
    + + + + + + +
    + {{value}} +
    +
    +
    +
    +
    +

    + + + datafeeder.datasetValidation.submitButton + + +
    +
    +
    diff --git a/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.spec.ts b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.spec.ts new file mode 100644 index 0000000000..1f5e001256 --- /dev/null +++ b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.spec.ts @@ -0,0 +1,117 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { ActivatedRoute, Router } from '@angular/router' +import { + AnalysisStatusEnumApiModel, + UploadJobStatusApiModel, +} from '@geonetwork-ui/data-access/datafeeder' +import { of } from 'rxjs' +import { WizardService } from '@geonetwork-ui/feature/editor' +import { DatafeederFacade } from '../../../store/datafeeder.facade' +import { DatasetValidationCsvPageComponent } from './dataset-validation-csv-page' +import { UtilI18nModule } from '@geonetwork-ui/util/i18n' +import { TranslateModule } from '@ngx-translate/core' + +const jobMock: UploadJobStatusApiModel = { + jobId: '1234', + status: AnalysisStatusEnumApiModel.Done, + progress: 100, + datasets: [ + { + name: 'f_name', + featureCount: 36, + format: 'CSV', + options: { + quoteChar: '"', + csv: 'IlllYXIiLCJNYWtlIiwiTW9kZWwiLCJMZW5ndGgiCiIxOTk3IiwiRm9yZCIsIkUzNTAiLCIyLjM1IgoiMjAwMCIsIk1lcmN1cnkiLCJDb3VnYXIiLCIyLjM4Ig==', + }, + }, + ], +} + +const facadeMock = { + upload$: of(jobMock), +} + +const wizardServiceMock = { + getConfigurationStepNumber: jest.fn(() => 6), + initialize: jest.fn(), + getWizardFieldData: jest.fn(() => null), +} + +const activatedRouteMock = { + params: of({ id: 1 }), +} + +const routerMock = { + navigate: jest.fn(), +} + +describe('DatasetValidationCsvPageComponent', () => { + let component: DatasetValidationCsvPageComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DatasetValidationCsvPageComponent], + imports: [UtilI18nModule, TranslateModule.forRoot()], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { + provide: WizardService, + useValue: wizardServiceMock, + }, + { + provide: DatafeederFacade, + useValue: facadeMock, + }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: Router, useValue: routerMock }, + ], + }).compileComponents() + }) + + it('should create', () => { + createComponent() + expect(component).toBeTruthy() + }) + + describe('Job DONE', () => { + beforeEach(() => { + createComponent() + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should contain the correct csvData', () => { + expect(component.csvData).toEqual([ + ['Year', 'Make', 'Model', 'Length'], + ['1997', 'Ford', 'E350', '2.35'], + ['2000', 'Mercury', 'Cougar', '2.38'], + ]) + }) + }) + + describe('Job ERROR', () => { + beforeEach(() => { + jobMock.status = AnalysisStatusEnumApiModel.Error + createComponent() + }) + + it('route to validation page', () => { + expect(routerMock.navigate).toHaveBeenCalledWith(['/'], { + relativeTo: activatedRouteMock, + queryParams: { error: 'analysis' }, + }) + }) + }) + + function createComponent() { + fixture = TestBed.createComponent(DatasetValidationCsvPageComponent) + component = fixture.componentInstance + fixture.detectChanges() + } +}) diff --git a/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.ts b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.ts new file mode 100644 index 0000000000..618378ea10 --- /dev/null +++ b/apps/datafeeder/src/app/presentation/pages/dataset-validation-csv-page/dataset-validation-csv-page.ts @@ -0,0 +1,196 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { + AnalysisStatusEnumApiModel, + DatasetUploadStatusApiModel, + UploadJobStatusApiModel, +} from '@geonetwork-ui/data-access/datafeeder' +import { WizardService } from '@geonetwork-ui/feature/editor' +import { Subscription } from 'rxjs' +import { take } from 'rxjs/operators' +import { config as wizardConfig } from '../../../configs/wizard.config' +import { DatafeederFacade } from '../../../store/datafeeder.facade' +import * as Papa from 'papaparse' +import { DropdownChoice } from '@geonetwork-ui/ui/inputs' + +marker('datafeeder.validation.csv.delimiter.comma') +marker('datafeeder.validation.csv.delimiter.semicolon') +marker('datafeeder.validation.csv.quote.none') +marker('datafeeder.validation.csv.quote.simple') +marker('datafeeder.validation.csv.quote.double') + +@Component({ + selector: 'gn-ui-dataset-validation-csv-page', + templateUrl: './dataset-validation-csv-page.html', + styleUrls: ['./dataset-validation-csv-page.css'], +}) +export class DatasetValidationCsvPageComponent implements OnInit, OnDestroy { + dataset: DatasetUploadStatusApiModel + + encoding: string + nativeName: string + + numOfEntities = 0 + numberOfSteps: number + + csvData: [] + csvDelimiter: string + delimiterChoices: DropdownChoice[] = [ + { label: 'datafeeder.validation.csv.delimiter.comma', value: ',' }, + { label: 'datafeeder.validation.csv.delimiter.semicolon', value: ';' }, + ] + quoteChar: string + quoteCharChoices: DropdownChoice[] = [ + { label: 'datafeeder.validation.csv.quote.none', value: '' }, + { label: 'datafeeder.validation.csv.quote.simple', value: "'" }, + { label: 'datafeeder.validation.csv.quote.double', value: '"' }, + ] + latLngChoices: DropdownChoice[] = [] + latField: string + lngField: string + latLngValid: boolean + + private csv: string + private routeParamsSub: Subscription + rootId: number + constructor( + private activatedRoute: ActivatedRoute, + private router: Router, + private wizard: WizardService, + private facade: DatafeederFacade + ) {} + + ngOnInit(): void { + this.routeParamsSub = this.activatedRoute.params.subscribe(({ id }) => { + this.rootId = id + this.wizard.initialize(id, wizardConfig) + this.numberOfSteps = this.wizard.getConfigurationStepNumber() + 1 + + this.facade.upload$ + .pipe(take(1)) + .subscribe((job: UploadJobStatusApiModel) => { + if (job.status === AnalysisStatusEnumApiModel.Error) { + this.router.navigate(['/'], { + relativeTo: this.activatedRoute, + queryParams: { error: 'analysis' }, + }) + return + } + + this.dataset = job.datasets[0] + const options = this.dataset.options + this.csv = new TextDecoder().decode(this.base64ToBytes(options.csv)) + this.csvDelimiter = + this.wizard.getWizardFieldData('csvDelimiter') || options.delimiter + this.quoteChar = + this.wizard.getWizardFieldData('quoteChar') || options.quoteChar + + this.latField = this.wizard.getWizardFieldData('latField') + + this.lngField = this.wizard.getWizardFieldData('lngField') + + this.numOfEntities = this.dataset.featureCount + this.nativeName = this.dataset.name + this.updateArray() + }) + }) + } + + submitValidation() { + if (!this.isValid()) { + return + } + const fields = [ + 'csvDelimiter', + 'quoteChar', + 'nativeName', + 'crs', + 'latField', + 'lngField', + ] + fields.forEach((f) => this.wizard.setWizardFieldData(f, this[f])) + this.router.navigate(['/', this.rootId, 'step', 1]) + } + + updateArray(): void { + const parseResult = Papa.parse(this.csv, { + delimiter: this.csvDelimiter, + quoteChar: this.quoteChar, + }) + this.csvDelimiter ??= parseResult.meta.delimiter + this.quoteChar ??= parseResult.meta.quoteChar + + this.latLngChoices = [ + { value: '', label: 'datafeeder.validation.csv.quote.none' }, + ...parseResult.data[0].map((o) => ({ + label: o, + value: o, + })), + ] + // remove empty rows + this.csvData = parseResult.data.filter( + (row) => row.length > 1 || (row.length == 1 && row[0]) + ) + + this.latLngValid = + (!this.latField && !this.lngField) || + (this.latField && + this.lngField && + this.checkLatLongValues() && + this.latField !== this.lngField) + } + + checkLatLongValues(): boolean { + let latIndex = -1 + let lngIndex = -1 + return this.csvData.every((row: [string], index) => { + if (index == 0) { + latIndex = row.indexOf(this.latField) + lngIndex = row.indexOf(this.lngField) + return true + } + const lat = Number(row[latIndex]) + const lng = Number(row[lngIndex]) + return ( + !isNaN(lng) && + lng >= -180 && + lng <= 180 && + !isNaN(lat) && + lat >= -90 && + lat <= 90 + ) + }) + } + + isValid(): boolean { + return this.latLngValid + } + + ngOnDestroy() { + this.routeParamsSub.unsubscribe() + } + + selectDelimiter($event: string) { + this.csvDelimiter = $event + this.updateArray() + } + selectQuoteChar($event: string) { + this.quoteChar = $event + this.updateArray() + } + + selectLatLng($event: string, lat: boolean) { + if (lat) { + this.latField = $event + } else { + this.lngField = $event + } + this.updateArray() + } + + base64ToBytes(base64) { + const binString = atob(base64) + return Uint8Array.from(binString, (m) => m.codePointAt(0)) + } +} diff --git a/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.spec.ts b/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.spec.ts index adb4740a78..04197376c9 100644 --- a/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.spec.ts +++ b/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.spec.ts @@ -3,13 +3,31 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { RouterTestingModule } from '@angular/router/testing' import { FormsPageComponent } from './forms-page.component' +import { DatafeederFacade } from '../../../store/datafeeder.facade' +import { of } from 'rxjs' +import { PublishStatusEnumApiModel } from '@geonetwork-ui/data-access/datafeeder' +const uploadStateStatusMock = { + jobId: '123', + progress: 1, + status: PublishStatusEnumApiModel.Pending, +} +const facadeMock = { + upload$: (() => of(uploadStateStatusMock))(), + setUpload: jest.fn(), +} describe('FormsPageComponent', () => { let component: FormsPageComponent let fixture: ComponentFixture beforeEach(async () => { await TestBed.configureTestingModule({ + providers: [ + { + provide: DatafeederFacade, + useValue: facadeMock, + }, + ], declarations: [FormsPageComponent], imports: [RouterTestingModule], schemas: [NO_ERRORS_SCHEMA], diff --git a/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.ts b/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.ts index 99d5460025..83ac7824c6 100644 --- a/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.ts +++ b/apps/datafeeder/src/app/presentation/pages/forms-page/forms-page.component.ts @@ -3,6 +3,10 @@ import { ActivatedRoute, Router } from '@angular/router' import { marker } from '@biesbjerg/ngx-translate-extract-marker' import { Subscription } from 'rxjs' import { config as wizardConfig } from '../../../configs/wizard.config' +import { take } from 'rxjs/operators' +import { UploadJobStatusApiModel } from '@geonetwork-ui/data-access/datafeeder' +import { DatafeederFacade } from '../../../store/datafeeder.facade' +import { UploadProgressGuard } from '../../../router/upload-progress.guard' marker('datafeeder.wizard.emptyRequiredValuesMessage') @@ -21,16 +25,28 @@ export class FormsPageComponent implements OnInit, OnDestroy { wizardConfig = wizardConfig private routeParamsSub: Subscription + private redirectPage: string constructor( private activatedRoute: ActivatedRoute, private router: Router, - private cd: ChangeDetectorRef + private cd: ChangeDetectorRef, + private facade: DatafeederFacade ) {} ngOnInit(): void { this.routeParamsSub = this.activatedRoute.params.subscribe(({ id }) => { this.id = id + this.facade.upload$ + .pipe(take(1)) + .subscribe((job: UploadJobStatusApiModel) => { + const jobFormat = job.datasets[0].format + const thirdStep = this.wizardConfig.configuration[2] + if (jobFormat === 'CSV' && thirdStep.length > 1) { + thirdStep.pop() + } + this.redirectPage = UploadProgressGuard.getRedirectPage(jobFormat) + }) }) this.cd.detectChanges() } @@ -38,7 +54,7 @@ export class FormsPageComponent implements OnInit, OnDestroy { handleStepChanges(step: number) { let route if (this.currentStep === 1 && step === 1) { - route = ['/', this.id, 'validation'] + route = ['/', this.id, this.redirectPage] } else if (this.currentStep === 4 && step === 4) { route = ['/', this.id, 'confirm'] } else { diff --git a/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.html b/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.html index b44d895517..70506a99d9 100644 --- a/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.html +++ b/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.html @@ -14,6 +14,7 @@
    datafeeder.publishSuccess.mapViewer + datafeeder.publishSuccess.ogcFeature
    { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [SuccessPublishPageComponent], - imports: [UiInputsModule], + imports: [UiInputsModule, HttpClientModule], schemas: [NO_ERRORS_SCHEMA], providers: [ { diff --git a/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.ts b/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.ts index 4b10195f6b..cc17cbfe7e 100644 --- a/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.ts +++ b/apps/datafeeder/src/app/presentation/pages/success-publish-page/success-publish-page.component.ts @@ -10,6 +10,7 @@ import { TranslateService } from '@ngx-translate/core' import { Subscription } from 'rxjs' import { take } from 'rxjs/operators' import { DatafeederFacade } from '../../../store/datafeeder.facade' +import { HttpClient } from '@angular/common/http' interface DatasetModel extends DatasetPublishingStatusApiModel { _links: any @@ -31,11 +32,13 @@ export class SuccessPublishPageComponent implements OnInit, OnDestroy { subscription: Subscription gnLink: string gsLink: string + ogcLink: string constructor( private facade: DatafeederFacade, private router: Router, - private translateService: TranslateService + private translateService: TranslateService, + private http: HttpClient ) {} ngOnInit(): void { @@ -46,11 +49,22 @@ export class SuccessPublishPageComponent implements OnInit, OnDestroy { .pipe(take(1)) .subscribe((job: JobStatusModel) => { const links = job.datasets[0]._links - this.gsLink = links.preview.href + this.gsLink = links.preview?.href + this.http + .get(this.gsLink, { observe: 'response', responseType: 'text' }) + .subscribe((data) => { + if ( + data.headers.get('content-type') === 'text/xml;charset=utf-8' && + data.body?.includes('NullPointerException') + ) { + this.gsLink = '' + } + }) this.gnLink = links.describedBy[1].href.replace( '/eng/', `/${LANG_2_TO_3_MAPPER[this.translateService.currentLang]}/` ) + this.ogcLink = links.hosts?.href }) ) } @@ -63,6 +77,10 @@ export class SuccessPublishPageComponent implements OnInit, OnDestroy { window.open(this.gsLink, '_blank') } + openOgcLink() { + window.open(this.ogcLink, '_blank') + } + backToHome() { this.router.navigate(['/']) } diff --git a/apps/datafeeder/src/app/presentation/pages/summarize-page/summarize-page.component.ts b/apps/datafeeder/src/app/presentation/pages/summarize-page/summarize-page.component.ts index 5ee07ee527..3d083540a5 100644 --- a/apps/datafeeder/src/app/presentation/pages/summarize-page/summarize-page.component.ts +++ b/apps/datafeeder/src/app/presentation/pages/summarize-page/summarize-page.component.ts @@ -57,6 +57,12 @@ export class SummarizePageComponent implements OnInit, OnDestroy { creationDate: new Date(parseInt(dataset.datepicker, 10)).toISOString(), scale: parseInt(dataset.dropdown, 10), creationProcessDescription: dataset.description, + options: { + delimiter: dataset.csvDelimiter, + quoteChar: dataset.quoteChar, + latField: dataset.latField, + lngField: dataset.lngField, + }, }, } } diff --git a/apps/datafeeder/src/app/presentation/pages/upload-data-page/upload-data.page.html b/apps/datafeeder/src/app/presentation/pages/upload-data-page/upload-data.page.html index 386499add3..0ba2fe23e1 100644 --- a/apps/datafeeder/src/app/presentation/pages/upload-data-page/upload-data.page.html +++ b/apps/datafeeder/src/app/presentation/pages/upload-data-page/upload-data.page.html @@ -24,7 +24,7 @@ diff --git a/apps/datafeeder/src/app/router/upload-progress.guard.spec.ts b/apps/datafeeder/src/app/router/upload-progress.guard.spec.ts index 05878d052f..19ceafecb5 100644 --- a/apps/datafeeder/src/app/router/upload-progress.guard.spec.ts +++ b/apps/datafeeder/src/app/router/upload-progress.guard.spec.ts @@ -2,8 +2,8 @@ import { HttpClientTestingModule } from '@angular/common/http/testing' import { getTestBed, TestBed } from '@angular/core/testing' import { Router } from '@angular/router' import { - FileUploadApiService, AnalysisStatusEnumApiModel, + FileUploadApiService, } from '@geonetwork-ui/data-access/datafeeder' import { of, throwError } from 'rxjs' import { UploadProgressGuard } from './upload-progress.guard' @@ -12,6 +12,7 @@ const uploadApiStatusMock = { jobId: '123', progress: 1, status: AnalysisStatusEnumApiModel.Done, + datasets: [{ format: 'SHAPEFILE' }], } const fileUploadApiServiceMock = { diff --git a/apps/datafeeder/src/app/router/upload-progress.guard.ts b/apps/datafeeder/src/app/router/upload-progress.guard.ts index ff52055cb8..dcbd48d044 100644 --- a/apps/datafeeder/src/app/router/upload-progress.guard.ts +++ b/apps/datafeeder/src/app/router/upload-progress.guard.ts @@ -5,8 +5,8 @@ import { RouterStateSnapshot, } from '@angular/router' import { - FileUploadApiService, AnalysisStatusEnumApiModel, + FileUploadApiService, } from '@geonetwork-ui/data-access/datafeeder' import { Observable, of } from 'rxjs' import { catchError, filter, map } from 'rxjs/operators' @@ -27,7 +27,10 @@ export class UploadProgressGuard { filter((job) => !!job), map((job) => { if (job.status === AnalysisStatusEnumApiModel.Done) { - this.router.navigate([state.url, `validation`]) + this.router.navigate([ + state.url, + UploadProgressGuard.getRedirectPage(job.datasets[0].format), + ]) return false } if (job.status === AnalysisStatusEnumApiModel.Error) { @@ -42,4 +45,15 @@ export class UploadProgressGuard { }) ) } + + static getRedirectPage(format: string) { + switch (format) { + case 'CSV': + return 'validation-csv' + case 'SHAPEFILE': + return 'validation' + default: + return '/' + } + } } diff --git a/libs/data-access/datafeeder/src/openapi/model/datasetMetadata.api.model.ts b/libs/data-access/datafeeder/src/openapi/model/datasetMetadata.api.model.ts index 3c3e4f5bfc..1f228a2292 100644 --- a/libs/data-access/datafeeder/src/openapi/model/datasetMetadata.api.model.ts +++ b/libs/data-access/datafeeder/src/openapi/model/datasetMetadata.api.model.ts @@ -38,4 +38,8 @@ export interface DatasetMetadataApiModel { * textual description of dataset lineage */ creationProcessDescription?: string + /** + * Optional, additional options for the dataset + */ + options: any } diff --git a/libs/data-access/datafeeder/src/openapi/model/datasetUploadStatus.api.model.ts b/libs/data-access/datafeeder/src/openapi/model/datasetUploadStatus.api.model.ts index ebbd77a49c..a1b3e931db 100644 --- a/libs/data-access/datafeeder/src/openapi/model/datasetUploadStatus.api.model.ts +++ b/libs/data-access/datafeeder/src/openapi/model/datasetUploadStatus.api.model.ts @@ -34,4 +34,6 @@ export interface DatasetUploadStatusApiModel { * detected charset */ encoding?: string + format?: 'CSV' | 'SHAPEFILE' + options?: any } diff --git a/support-services/docker-compose.yml b/support-services/docker-compose.yml index 8ee3b01ff6..91c4f4ff0d 100644 --- a/support-services/docker-compose.yml +++ b/support-services/docker-compose.yml @@ -153,3 +153,18 @@ services: condition: service_healthy ports: - '8081:8080' + + datahub: + image: ghcr.io/camptocamp/mel-dataplatform/datahub:latest + healthcheck: + test: + [ + 'CMD-SHELL', + 'curl -s -f http://localhost:80/datahub/ >/dev/null || exit 1', + ] + interval: 30s + timeout: 10s + retries: 10 + restart: always + ports: + - '8082:80' diff --git a/translations/de.json b/translations/de.json index 822d224de6..c875aebde7 100644 --- a/translations/de.json +++ b/translations/de.json @@ -98,6 +98,15 @@ "datafeeder.upload.title": "Laden Sie Ihren Datensatz hoch", "datafeeder.upload.uploadButton": "Hochladen", "datafeeder.validation.encoding": "Codierung", + "datafeeder.validation.csv.delimiter": "", + "datafeeder.validation.csv.delimiter.comma": "", + "datafeeder.validation.csv.delimiter.semicolon": "", + "datafeeder.validation.csv.lat.field": "", + "datafeeder.validation.csv.lng.field": "", + "datafeeder.validation.csv.quote.double": "", + "datafeeder.validation.csv.quote.none": "", + "datafeeder.validation.csv.quote.simple": "", + "datafeeder.validation.csv.quoteChar": "", "datafeeder.validation.extent.title": "Hier ist der Datensatzumfang", "datafeeder.validation.extent.title.unknown": "Das Projektionssystem ist unbekannt", "datafeeder.validation.projection": "Raumbezugssystem:", diff --git a/translations/en.json b/translations/en.json index 53e231e70e..87ae5ec9d9 100644 --- a/translations/en.json +++ b/translations/en.json @@ -39,6 +39,8 @@ "datafeeder.analysisProgressBar.subtitle": "The analysis may take several minutes, please wait.", "datafeeder.analysisProgressBar.title": "Analyze in progress", "datafeeder.datasetValidation.datasetInformation": "The provided dataset contains {number} entities", + "datafeeder.datasetValidationCsv.lineNumbers": "Sample of the first 5 lines* of the dataset:", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "*The table must display the first 5 lines (excluding the header)
    If this is not the case, check that the file is correctly formatted", "datafeeder.datasetValidation.submitButton": "OK, my data are correct", "datafeeder.datasetValidation.title": "Make sure your data are correct", "datafeeder.datasetValidation.unknown": " - ", @@ -69,6 +71,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "Metadata record", "datafeeder.publishSuccess.illustration.title": "Done, all is good!", "datafeeder.publishSuccess.mapViewer": "Map viewer", + "datafeeder.publishSuccess.ogcFeature": "OGC API", "datafeeder.publishSuccess.subtitle": "View your data in:", "datafeeder.publishSuccess.title": "Congratulation! \n Your dataset has been published", "datafeeder.publishSuccess.uploadAnotherData": "Upload another dataset", @@ -97,6 +100,15 @@ "datafeeder.upload.maxFileSize": "Maximum file size is {size} MB", "datafeeder.upload.title": "Upload your dataset", "datafeeder.upload.uploadButton": "Upload", + "datafeeder.validation.csv.delimiter": "Delimiter", + "datafeeder.validation.csv.delimiter.comma": "Comma", + "datafeeder.validation.csv.delimiter.semicolon": "Semicolon", + "datafeeder.validation.csv.lat.field": "Latitude column", + "datafeeder.validation.csv.lng.field": "Longitude column", + "datafeeder.validation.csv.quote.double": "Double quote", + "datafeeder.validation.csv.quote.none": "None", + "datafeeder.validation.csv.quote.simple": "Simple quote", + "datafeeder.validation.csv.quoteChar": "Quote separator", "datafeeder.validation.encoding": "encoding", "datafeeder.validation.extent.title": "Here is the dataset extent", "datafeeder.validation.extent.title.unknown": "The projection system is unknown", diff --git a/translations/es.json b/translations/es.json index 384aa3cba3..ad5d38064f 100644 --- a/translations/es.json +++ b/translations/es.json @@ -97,6 +97,15 @@ "datafeeder.upload.maxFileSize": "", "datafeeder.upload.title": "", "datafeeder.upload.uploadButton": "", + "datafeeder.validation.csv.delimiter": "", + "datafeeder.validation.csv.delimiter.comma": "Coma", + "datafeeder.validation.csv.delimiter.semicolon": "Punto y coma", + "datafeeder.validation.csv.lat.field": "", + "datafeeder.validation.csv.lng.field": "", + "datafeeder.validation.csv.quote.double": "Comillas dobles", + "datafeeder.validation.csv.quote.none": "Ninguno", + "datafeeder.validation.csv.quote.simple": "Comillas simples", + "datafeeder.validation.csv.quoteChar": "", "datafeeder.validation.encoding": "", "datafeeder.validation.extent.title": "", "datafeeder.validation.extent.title.unknown": "", diff --git a/translations/fr.json b/translations/fr.json index 8dc865f38f..a77add03bb 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -39,6 +39,8 @@ "datafeeder.analysisProgressBar.subtitle": "L'analyse peut prendre plusieurs minutes, merci d'attendre.", "datafeeder.analysisProgressBar.title": "Analyse en cours", "datafeeder.datasetValidation.datasetInformation": "Le jeu de données fourni contient {number} entités", + "datafeeder.datasetValidationCsv.lineNumbers": "Résumé des 5 premières lignes* du CSV :", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "*Le tableau doit afficher les 5 premières lignes (hors en-tête)
    Si ce n'est pas le cas, vérifier que le fichier est bien formatté", "datafeeder.datasetValidation.submitButton": "OK, mes données sont correctes", "datafeeder.datasetValidation.title": "Vérifiez que vos données sont correctes", "datafeeder.datasetValidation.unknown": " - ", @@ -68,7 +70,8 @@ "datafeeder.publish.upload": "Upload maintenant", "datafeeder.publishSuccess.geonetworkRecord": "Fiche de métadonnée", "datafeeder.publishSuccess.illustration.title": "Terminé, tout s'est bien passé !", - "datafeeder.publishSuccess.mapViewer": "Visualisateur", + "datafeeder.publishSuccess.mapViewer": "Visualiseur", + "datafeeder.publishSuccess.ogcFeature": "OGC API", "datafeeder.publishSuccess.subtitle": "Visualisez vos données :", "datafeeder.publishSuccess.title": "Félicitations! \n Vos données ont été publiées", "datafeeder.publishSuccess.uploadAnotherData": "Importer une autre donnée", @@ -97,6 +100,15 @@ "datafeeder.upload.maxFileSize": "La taille maximale est {size} Mo", "datafeeder.upload.title": "Importez vos données", "datafeeder.upload.uploadButton": "Transférer", + "datafeeder.validation.csv.delimiter": "Séparateur de colonne", + "datafeeder.validation.csv.delimiter.comma": "Virgule", + "datafeeder.validation.csv.delimiter.semicolon": "Point-virgule", + "datafeeder.validation.csv.lat.field": "Colonne latitude", + "datafeeder.validation.csv.lng.field": "Colonne longitude", + "datafeeder.validation.csv.quote.double": "Double guillemets", + "datafeeder.validation.csv.quote.none": "Aucun", + "datafeeder.validation.csv.quote.simple": "Simple guillemet", + "datafeeder.validation.csv.quoteChar": "Séparateur de texte", "datafeeder.validation.encoding": "encodage", "datafeeder.validation.extent.title": "Voici l'emprise du jeu de données", "datafeeder.validation.extent.title.unknown": "Le système de projection est inconnu", diff --git a/translations/it.json b/translations/it.json index 34ec4c3674..23ff72facf 100644 --- a/translations/it.json +++ b/translations/it.json @@ -97,6 +97,15 @@ "datafeeder.upload.maxFileSize": "Dimensione massima: {size} MB", "datafeeder.upload.title": "Importa i suoi dati", "datafeeder.upload.uploadButton": "Carica", + "datafeeder.validation.csv.delimiter": "", + "datafeeder.validation.csv.delimiter.comma": "", + "datafeeder.validation.csv.delimiter.semicolon": "", + "datafeeder.validation.csv.lat.field": "", + "datafeeder.validation.csv.lng.field": "", + "datafeeder.validation.csv.quote.double": "", + "datafeeder.validation.csv.quote.none": "", + "datafeeder.validation.csv.quote.simple": "", + "datafeeder.validation.csv.quoteChar": "", "datafeeder.validation.encoding": "Codifica", "datafeeder.validation.extent.title": "Ecco l'estensione del dataset", "datafeeder.validation.extent.title.unknown": "Sistema di proiezione sconosciuto", diff --git a/translations/nl.json b/translations/nl.json index 98078f8718..ec8deaf45c 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -97,6 +97,15 @@ "datafeeder.upload.maxFileSize": "", "datafeeder.upload.title": "", "datafeeder.upload.uploadButton": "", + "datafeeder.validation.csv.delimiter": "", + "datafeeder.validation.csv.delimiter.comma": "", + "datafeeder.validation.csv.delimiter.semicolon": "", + "datafeeder.validation.csv.lat.field": "", + "datafeeder.validation.csv.lng.field": "", + "datafeeder.validation.csv.quote.double": "", + "datafeeder.validation.csv.quote.none": "", + "datafeeder.validation.csv.quote.simple": "", + "datafeeder.validation.csv.quoteChar": "", "datafeeder.validation.encoding": "", "datafeeder.validation.extent.title": "", "datafeeder.validation.extent.title.unknown": "", diff --git a/translations/pt.json b/translations/pt.json index 46a680eb6e..988a702520 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -97,6 +97,15 @@ "datafeeder.upload.maxFileSize": "", "datafeeder.upload.title": "", "datafeeder.upload.uploadButton": "", + "datafeeder.validation.csv.delimiter": "", + "datafeeder.validation.csv.delimiter.comma": "", + "datafeeder.validation.csv.delimiter.semicolon": "", + "datafeeder.validation.csv.lat.field": "", + "datafeeder.validation.csv.lng.field": "", + "datafeeder.validation.csv.quote.double": "", + "datafeeder.validation.csv.quote.none": "", + "datafeeder.validation.csv.quote.simple": "", + "datafeeder.validation.csv.quoteChar": "", "datafeeder.validation.encoding": "", "datafeeder.validation.extent.title": "", "datafeeder.validation.extent.title.unknown": "", diff --git a/translations/sk.json b/translations/sk.json index 02131776ed..493d2616c6 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -97,6 +97,15 @@ "datafeeder.upload.maxFileSize": "Maximálna veľkosť súboru je {size} MB", "datafeeder.upload.title": "Nahrajte svoj dataset", "datafeeder.upload.uploadButton": "Nahrať", + "datafeeder.validation.csv.delimiter": "", + "datafeeder.validation.csv.delimiter.comma": "", + "datafeeder.validation.csv.delimiter.semicolon": "", + "datafeeder.validation.csv.lat.field": "", + "datafeeder.validation.csv.lng.field": "", + "datafeeder.validation.csv.quote.double": "", + "datafeeder.validation.csv.quote.none": "", + "datafeeder.validation.csv.quote.simple": "", + "datafeeder.validation.csv.quoteChar": "", "datafeeder.validation.encoding": "kódovanie", "datafeeder.validation.extent.title": "Tu je rozsah datasetu", "datafeeder.validation.extent.title.unknown": "Systém projekcie je neznámy", From 7b687307acd7212a848b36e570f694fdc0369f97 Mon Sep 17 00:00:00 2001 From: cde-barros Date: Fri, 21 Jun 2024 15:17:17 +0200 Subject: [PATCH 005/378] correction apache duplication merge --- tools/docker/apache/apache-ports.conf | 10 ---------- tools/docker/apache/apache-security.conf | 4 ---- tools/docker/apache/apache-vhost.conf | 21 --------------------- 3 files changed, 35 deletions(-) diff --git a/tools/docker/apache/apache-ports.conf b/tools/docker/apache/apache-ports.conf index 9d36f04c09..032a1553e8 100644 --- a/tools/docker/apache/apache-ports.conf +++ b/tools/docker/apache/apache-ports.conf @@ -4,16 +4,6 @@ Listen 8080 Listen 443 - - Listen 443 - - -Listen 8080 - - - Listen 443 - - Listen 443 \ No newline at end of file diff --git a/tools/docker/apache/apache-security.conf b/tools/docker/apache/apache-security.conf index a3d339acb8..8f31b7eb33 100644 --- a/tools/docker/apache/apache-security.conf +++ b/tools/docker/apache/apache-security.conf @@ -1,7 +1,3 @@ -ServerTokens Prod -ServerSignature Off -TraceEnable Off - ServerTokens Prod ServerSignature Off TraceEnable Off \ No newline at end of file diff --git a/tools/docker/apache/apache-vhost.conf b/tools/docker/apache/apache-vhost.conf index 9aef3c671e..bd1aa05de1 100644 --- a/tools/docker/apache/apache-vhost.conf +++ b/tools/docker/apache/apache-vhost.conf @@ -1,24 +1,3 @@ - - ServerName localhost - - DocumentRoot /opt/catalogue - Alias /catalogue "/opt/catalogue" - - Options Indexes FollowSymLinks MultiViews - AllowOverride All - Require all granted - RewriteEngine On - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^ index.html [L] - - - # ErrorLog ${APACHE_LOG_DIR}/catalogue_error.log - # CustomLog ${APACHE_LOG_DIR}/catalogue_access.log combined - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - - ServerName localhost From 68c3ace9821e09be58daf060359226972a1b67ee Mon Sep 17 00:00:00 2001 From: Ronan Date: Mon, 24 Jun 2024 14:36:37 +0200 Subject: [PATCH 006/378] =?UTF-8?q?fix(formapi):=20Mise=20=C3=A0=20jour=20?= =?UTF-8?q?des=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../record-apis/record-apis.component.spec.ts | 2 +- .../ign-api-dl/ign-api-dl.component.spec.ts | 173 ++++++++++++------ 2 files changed, 120 insertions(+), 55 deletions(-) diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts b/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts index 28c7318b45..7f10f3653c 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts @@ -48,7 +48,7 @@ describe('RecordApisComponent', () => { expect(component.selectedApiLink).toEqual(serviceDistributionMock) }) it('should update maxHeight for transition', () => { - expect(component.maxHeight).toEqual('500px') + expect(component.maxHeight).toEqual('700px') }) it('should update opacity for transition', () => { expect(component.opacity).toEqual(1) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts index d21e700497..3f17fc6b86 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts @@ -1,30 +1,36 @@ import { TestBed, ComponentFixture } 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 { Choice, UiInputsModule } from '@geonetwork-ui/ui/inputs' import { TranslateModule } from '@ngx-translate/core' +import { IgnApiDlComponent } from './ign-api-dl.component' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import exp from 'constants' const mockDatasetServiceDistribution: DatasetServiceDistribution = { url: new URL('https://api.example.com/data'), type: 'service', - accessServiceProtocol: 'ogcFeatures', + accessServiceProtocol: 'GPFDL', } -describe('RecordApFormComponent', () => { - let component: RecordApiFormComponent - let fixture: ComponentFixture +describe('IgnApiDlComponent', () => { + let component: IgnApiDlComponent + let fixture: ComponentFixture beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [RecordApiFormComponent], - imports: [UiInputsModule, TranslateModule.forRoot()], + declarations: [IgnApiDlComponent], + imports: [ + UiInputsModule, + HttpClientTestingModule, + TranslateModule.forRoot(), + ], }).compileComponents() - fixture = TestBed.createComponent(RecordApiFormComponent) + fixture = TestBed.createComponent(IgnApiDlComponent) component = fixture.componentInstance component.apiLink = mockDatasetServiceDistribution - fixture.detectChanges() }) it('should create', () => { @@ -33,58 +39,117 @@ describe('RecordApFormComponent', () => { describe('When panel is opened', () => { it('should set the links and initial values correctly', async () => { expect(component.apiBaseUrl).toBe('https://api.example.com/data') - expect(component.offset$.getValue()).toBe('') - expect(component.limit$.getValue()).toBe('') - expect(component.format$.getValue()).toBe('json') + expect(component.format$.getValue()).toBe('') + expect(component.zone$.getValue()).toBe('') + expect(component.crs$.getValue()).toBe('') const url = await firstValueFrom(component.apiQueryUrl$) - expect(url).toBe('https://api.example.com/data?f=json') + expect(url).toBe('https://api.example.com/data?pageSize=200&page=0') + //expect(component.apiLink.accessServiceProtocol).toBe('GPFDL') }) }) describe('When URL params are changed', () => { - it('should update query URL correctly when setting offset, limit, and format', async () => { - const mockOffset = '10' - const mockLimit = '20' - const mockFormat = 'json' - component.setOffset(mockOffset) - component.setLimit(mockLimit) - component.setFormat(mockFormat) - const url = await firstValueFrom(component.apiQueryUrl$) - expect(url).toBe( - `https://api.example.com/data?offset=${mockOffset}&limit=${mockLimit}&f=${mockFormat}` - ) + describe('When Format changed', () => { + const bucketFormat: Choice[] = [ + { value: 'SHP', label: 'SHP' }, + { value: 'json', label: 'json' }, + { value: 'SQL', label: 'SQL' }, + ] + const mockFormat = 'SHP' + const mockBadFormat = 'notAFormat' + it('should be a correct format', async () => { + jest.spyOn(component, 'resetPage') + component.bucketPromisesFormat = bucketFormat + component.setFormat(mockFormat) + expect(component.format$.getValue()).toBe(mockFormat) + expect(component.resetPage).toHaveBeenCalled() + }) + + it('should not be a correct format', async () => { + component.bucketPromisesFormat = bucketFormat + component.setFormat(mockFormat) + component.setFormat(mockBadFormat) + expect(component.format$.getValue()).toBe(mockFormat) + }) }) - it('should remove the param in url if value is null', async () => { - const mockOffset = null - const mockLimit = '20' - const mockFormat = 'json' - component.setOffset(mockOffset) - component.setLimit(mockLimit) - component.setFormat(mockFormat) - const url = await firstValueFrom(component.apiQueryUrl$) - expect(url).toBe( - `https://api.example.com/data?limit=${mockLimit}&f=${mockFormat}` - ) + describe('When CRS changed', () => { + const bucketCRS: Choice[] = [ + { value: 'CRS12', label: 'CRS1' }, + { value: 'CRS2', label: 'CRS2' }, + { value: 'CRS3', label: 'CRS3' }, + ] + const mockCRS = 'CRS12' + const mockBadCRS = 'notACRS' + it('should be a correct CRS', async () => { + jest.spyOn(component, 'resetPage') + component.bucketPromisesCrs = bucketCRS + component.setCrs(mockCRS) + expect(component.crs$.getValue()).toBe(mockCRS) + expect(component.resetPage).toHaveBeenCalled() + }) + + it('should not be a correct CRS', async () => { + component.bucketPromisesCrs = bucketCRS + component.setCrs(mockCRS) + component.setCrs(mockBadCRS) + expect(component.crs$.getValue()).toBe(mockCRS) + }) }) - it('should remove the param in url if value is zero', async () => { - const mockOffset = '10' - const mockLimit = '0' - const mockFormat = 'json' - component.setOffset(mockOffset) - component.setLimit(mockLimit) - component.setFormat(mockFormat) - const url = await firstValueFrom(component.apiQueryUrl$) - expect(url).toBe( - `https://api.example.com/data?offset=${mockOffset}&f=${mockFormat}` - ) + describe('When Zone changed', () => { + const bucketZone: Choice[] = [ + { value: 'D01', label: 'D01' }, + { value: 'D02', label: 'D02' }, + { value: 'D03', label: 'D03' }, + ] + const mockZone = 'D03' + const mockBadZone = 'notAZone' + it('should be a correct Zone', async () => { + jest.spyOn(component, 'resetPage') + component.bucketPromisesZone = bucketZone + component.setZone(mockZone) + expect(component.zone$.getValue()).toBe(mockZone) + expect(component.resetPage).toHaveBeenCalled() + }) + + it('should not be a correct Zone', async () => { + component.bucketPromisesZone = bucketZone + component.setZone(mockZone) + component.setZone(mockBadZone) + expect(component.zone$.getValue()).toBe(mockZone) + }) + }) + + describe('When EditionDate changed', () => { + const mockEditionDate = '2022-04-30' + const mockBadEditionDate = '88-88-88' + it('hould be a correct edition date', () => { + jest.spyOn(component, 'resetPage') + component.setEditionDate(mockEditionDate) + expect(component.editionDate$.getValue()).toBe(mockEditionDate) + expect(component.resetPage).toHaveBeenCalled() + }) + it('should not be a correct edition date', () => { + component.setEditionDate(mockEditionDate) + component.setEditionDate(mockBadEditionDate) + expect(component.editionDate$.getValue()).toBe(mockEditionDate) + }) + }) + + describe('When Url is reset', () => { + it('Should reset zone, format, crs, page and size value', () => { + component.resetUrl() + expect(component.zone$.getValue()).toBe('null') + expect(component.format$.getValue()).toBe('null') + expect(component.crs$.getValue()).toBe('null') + expect(component.page$.getValue()).toBe('0') + expect(component.size$.getValue()).toBe(component.initialPageSize) + }) }) - }) - describe('#resetUrl', () => { - it('should reset URL to default parameters', () => { - component.resetUrl() - expect(component.offset$.getValue()).toBe('') - expect(component.limit$.getValue()).toBe('') - expect(component.format$.getValue()).toBe('json') + describe('When page is reset', () => { + it('Should reset page value', () => { + component.resetPage() + expect(component.page$.getValue()).toBe('0') + }) }) }) }) From a185ee991b30008ad5d74f4cc5fbf3c6a1ef7985 Mon Sep 17 00:00:00 2001 From: Ronan Date: Mon, 24 Jun 2024 14:39:18 +0200 Subject: [PATCH 007/378] =?UTF-8?q?fix(formapi):=20Mise=20en=20place=20de?= =?UTF-8?q?=20conditions=20pour=20les=20diff=C3=A9rents=20set=20des=20tris?= =?UTF-8?q?=20du=20formulaire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/ign-api-dl/ign-api-dl.component.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index c6e6ebbc83..15cc2ec364 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -156,23 +156,33 @@ export class IgnApiDlComponent implements OnInit { } setEditionDate(value: string) { - this.editionDate$.next(value) - this.resetPage() + if (value.match(/[0-9]{4}-[0-1][0-9]-[0-3][0-9]/)) { + this.editionDate$.next(value) + this.resetPage() + } } setZone(value: string) { - this.zone$.next(value) - this.resetPage() + if (this.bucketPromisesZone.map((choice) => choice.value).includes(value)) { + this.zone$.next(value) + this.resetPage() + } } setCrs(value: string) { - this.crs$.next(value) - this.resetPage() + if (this.bucketPromisesCrs.map((choice) => choice.value).includes(value)) { + this.crs$.next(value) + this.resetPage() + } } setFormat(value: string) { - this.format$.next(value) - this.resetPage() + if ( + this.bucketPromisesFormat.map((choice) => choice.value).includes(value) + ) { + this.format$.next(value) + this.resetPage() + } } resetUrl() { From 224207ec7ab273c7d993bba4a57036a72ac09641 Mon Sep 17 00:00:00 2001 From: cde-barros Date: Mon, 24 Jun 2024 15:39:00 +0200 Subject: [PATCH 008/378] (TO UNDO - dropdown filter search metadata quality --- .../search-filters.component.html | 2 +- .../ign-api-produit.component.html | 20 +++++++++------- .../ign-api-produit.component.ts | 23 +++++++++++-------- .../record/src/lib/state/mdview.facade.ts | 9 +++++++- .../src/lib/api-card/api-card.component.html | 7 +++--- .../src/lib/api-card/api-card.component.ts | 4 ++-- .../dropdown-selector.component.ts | 2 +- .../src/lib/links/link-classifier.service.ts | 2 +- translations/en.json | 2 +- translations/fr.json | 2 +- 10 files changed, 44 insertions(+), 29 deletions(-) diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.html b/apps/datahub/src/app/home/search/search-filters/search-filters.component.html index 5b94900251..419eb96534 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.html +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.html @@ -125,7 +125,7 @@

    diff --git a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.html b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.html index a9d1c6e125..c413764ffa 100644 --- a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.html +++ b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.html @@ -23,11 +23,15 @@ >datahub.search.filter.generatedByWfs
    - - - - + + + diff --git a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts index 5650cc2d8d..813939b6a4 100644 --- a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts +++ b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts @@ -26,21 +26,24 @@ export class IgnApiProduitComponent implements OnInit { ngOnInit(): void { this.liste$ = this.http.get(this.link['id']).pipe( - - map((response) => response['entry'], - // tap(el=> console.log(el)), + map( + (response) => response['entry'] + // tap(el=> console.log(el)), ) ) - } + } - downloadListe():void{ - this.http.get(this.link['id']).pipe( - map((response) => response['entry']), - mergeMap((response) => response) - ).subscribe(reponse=>this.download(reponse['id'])) + downloadListe(): void { + this.http + .get(this.link['id']) + .pipe( + map((response) => response['entry']), + mergeMap((response) => response) + ) + .subscribe((reponse) => this.download(reponse['id'])) } - download(url):void{ + download(url): void { console.log(url) this.http.get(url).subscribe() } diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 11edd89d1f..d35087ea2f 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -69,7 +69,14 @@ export class MdViewFacade { apiLinks$ = this.allLinks$.pipe( map((links) => - links.filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)).sort((dd1, dd2) => {return (dd2 as DatasetServiceDistribution).accessServiceProtocol == 'GPFDL'? 1 : 0}) + links + .filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)) + .sort((dd1, dd2) => { + return (dd2 as DatasetServiceDistribution).accessServiceProtocol == + 'GPFDL' + ? 1 + : 0 + }) ) ) 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 80bdb7667f..70875f5e35 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 @@ -11,7 +11,7 @@
    record.metadata.api.gpfdl + record.metadata.api.gpfdl = + @Output() openRecordApiForm: EventEmitter = new EventEmitter() ngOnInit() { this.displayApiFormButton = this.link.accessServiceProtocol === 'ogcFeatures' || this.link.accessServiceProtocol === 'wfs' || - this.link.accessServiceProtocol === 'GPFDL' + this.link.accessServiceProtocol === 'GPFDL' } ngOnChanges(changes: SimpleChanges) { diff --git a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts index 3c48313b7b..127b67741a 100644 --- a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts +++ b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts @@ -94,7 +94,7 @@ export class DropdownSelectorComponent implements OnInit { if (!this.choices || this.choices.length === 0) { this.choices = [] } - console.log("un nv test",this.focusFirstItem()) + console.log('un nv test', this.focusFirstItem()) } isSelected(choice: DropdownChoice) { diff --git a/libs/util/shared/src/lib/links/link-classifier.service.ts b/libs/util/shared/src/lib/links/link-classifier.service.ts index 03b3348244..c33604fffb 100644 --- a/libs/util/shared/src/lib/links/link-classifier.service.ts +++ b/libs/util/shared/src/lib/links/link-classifier.service.ts @@ -25,7 +25,7 @@ export class LinkClassifierService { case 'wms': case 'wmts': return [LinkUsage.API, LinkUsage.MAP_API] - + case 'ogcFeatures': return [LinkUsage.API, LinkUsage.DOWNLOAD, LinkUsage.GEODATA] case 'GPFDL': diff --git a/translations/en.json b/translations/en.json index f78a70c843..2e2fefda5e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -411,4 +411,4 @@ "wfs.unreachable.cors": "The service could not be reached because of CORS limitations", "wfs.unreachable.http": "The service returned an HTTP error", "wfs.unreachable.unknown": "The service could not be reached" -} \ No newline at end of file +} diff --git a/translations/fr.json b/translations/fr.json index 4ff8f0e814..da4af9f738 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -411,4 +411,4 @@ "wfs.unreachable.cors": "Le service n'est pas accessible en raison de limitations CORS", "wfs.unreachable.http": "Le service a retourné une erreur HTTP", "wfs.unreachable.unknown": "Le service n'est pas accessible" -} \ No newline at end of file +} From e17d7a7b811b21ea72b12d0b75bbe494b2b4e976 Mon Sep 17 00:00:00 2001 From: cde-barros Date: Mon, 24 Jun 2024 17:33:21 +0200 Subject: [PATCH 009/378] ajout filtre type de ressource et modification url api --- conf/default.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/default.toml b/conf/default.toml index 74830980a0..b3595b7f34 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -5,7 +5,7 @@ [global] # This URL (relative or absolute) must point to the API endpoint of a GeoNetwork4 instance -geonetwork4_api_url = "https://gpf-geonetwork-qua.priv.geopf.fr/geonetwork/srv/api" +geonetwork4_api_url = "https://data-qua.priv.geopf.fr/catalog" datahub_url = "/catalogue" # This should point to a proxy to avoid CORS errors on some requests (data preview, OGC capabilities etc.) # The actual URL will be appended after this path, e.g. : https://my.proxy/?url=http%3A%2F%2Fencoded.url%2Fows` @@ -81,7 +81,7 @@ favicon = "/assets/img/favicon.ico" # The advanced search filters available to the user can be customized with this setting. # The following fields can be used for filtering: 'publisher', 'format', 'publicationYear', 'standard', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType' # any other field will be ignored -advanced_filters = ['publisher', 'topic', 'publicationYear', 'format', 'inspireKeyword', 'isSpatial', 'license'] +advanced_filters = ['publisher', 'topic', 'resourceType', 'publicationYear', 'format', 'inspireKeyword', 'isSpatial', 'license'] # One or several search presets can be defined here; every search preset is composed of: # - a name (which can be a translation key) From a04287a7160a03aac7af8431169438a8fa723756 Mon Sep 17 00:00:00 2001 From: cde-barros Date: Mon, 24 Jun 2024 17:46:31 +0200 Subject: [PATCH 010/378] correction attribution carte --- conf/default.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/conf/default.toml b/conf/default.toml index b3595b7f34..b30314a055 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -175,7 +175,6 @@ do_not_use_default_basemap = true type = 'wms' url = 'https://data.geopf.fr/wms-r' name = 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2' -attributions = 'Géoservices © IGN-France' ### TRANSLATIONS From 569b07c207745626295d0662694cfa8be3aa614c Mon Sep 17 00:00:00 2001 From: cde-barros Date: Mon, 24 Jun 2024 18:09:02 +0200 Subject: [PATCH 011/378] build nx skip cache --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7d808ab3d2..20628eaa05 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -70,7 +70,7 @@ jobs: - name: Npm install run: npm install - name: npx build - run: npx nx build datahub --base-href=/catalogue/ + run: npx nx build datahub --base-href=/catalogue/ --skip-nx-cache # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action From 5676322e4eb0e4878bc3bd0799a4c17f6bf8c886 Mon Sep 17 00:00:00 2001 From: Ronan Date: Tue, 25 Jun 2024 11:31:28 +0200 Subject: [PATCH 012/378] =?UTF-8?q?fix(formapi):=20R=C3=A9solution=20de=20?= =?UTF-8?q?la=20compatibilit=C3=A9=20des=20navigateurs=20mettant=20les=20d?= =?UTF-8?q?onn=C3=A9es=20a=20t=C3=A9l=C3=A9charger=20en=20premier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/feature/record/src/lib/state/mdview.facade.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 7aebf76282..8472e60290 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -61,7 +61,14 @@ export class MdViewFacade { apiLinks$ = this.allLinks$.pipe( map((links) => - links.filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)).sort((dd1, dd2) => {return (dd2 as DatasetServiceDistribution).accessServiceProtocol == 'GPFDL'? 1 : 0}) + links + .filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)) + .sort((dd1, dd2) => { + return (dd2 as DatasetServiceDistribution).accessServiceProtocol === + 'GPFDL' + ? 1 + : -1 + }) ) ) From 264578b2c60b35b3f94cf6818453dece9e7f9d72 Mon Sep 17 00:00:00 2001 From: Ronan Date: Tue, 25 Jun 2024 11:44:20 +0200 Subject: [PATCH 013/378] =?UTF-8?q?fix(formapi):=20Ajustement=20de=20la=20?= =?UTF-8?q?r=C3=A9initialisation=20de=20la=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index c6e6ebbc83..8936842b57 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -191,6 +191,8 @@ export class IgnApiDlComponent implements OnInit { } resetPage(): void { this.page$.next('0') + this.pageSize$.next(this.initialPageSize) + this.size$.next(this.initialPageSize) } async getFields() { From 8eb8fd3c9083cef2d149d781817ae60210dbad9b Mon Sep 17 00:00:00 2001 From: Ronan Date: Tue, 25 Jun 2024 11:44:20 +0200 Subject: [PATCH 014/378] =?UTF-8?q?fix(formapi):=20Ajustement=20de=20la=20?= =?UTF-8?q?r=C3=A9initialisation=20de=20la=20page=20#37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index c6e6ebbc83..8936842b57 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -191,6 +191,8 @@ export class IgnApiDlComponent implements OnInit { } resetPage(): void { this.page$.next('0') + this.pageSize$.next(this.initialPageSize) + this.size$.next(this.initialPageSize) } async getFields() { From c13546085bcc99a87f0f5b70001d6a30b2c71a60 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Tue, 25 Jun 2024 16:12:43 +0200 Subject: [PATCH 015/378] fix(ME): Exclude place thesarus keywords --- .../gn4/platform/gn4-platform.service.spec.ts | 89 +++++++++++++++++++ .../lib/gn4/platform/gn4-platform.service.ts | 41 +++++---- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts index 35255da4b7..98c400e780 100644 --- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts @@ -370,6 +370,95 @@ describe('Gn4PlatformService', () => { }) }) }) + describe('#searchKeywords', () => { + beforeEach(() => { + jest.spyOn(service, 'searchKeywords') + }) + it('calls api service with qeury', () => { + service.searchKeywords('road').subscribe() + expect(registriesApiService.searchKeywords).toHaveBeenCalledWith( + 'road', + 'fre', + 10, + 0, + null, + ['external.theme.httpinspireeceuropaeutheme-theme'], + null, + '*road*' + ) + }) + it('returns mapped thesaurus with translated values', async () => { + const keywords = await lastValueFrom(service.searchKeywords('road')) + expect(keywords).toEqual([ + { + description: + 'Localisation des propriétés fondée sur les identifiants des adresses, habituellement le nom de la rue, le numéro de la maison et le code postal.', + key: 'http://inspire.ec.europa.eu/theme/ad', + label: 'Adresses', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', + }, + { + description: + "Modèles numériques pour l'altitude des surfaces terrestres, glaciaires et océaniques. Comprend l'altitude terrestre, la bathymétrie et la ligne de rivage.", + key: 'http://inspire.ec.europa.eu/theme/el', + label: 'Altitude', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', + }, + ]) + }) + describe('if translations are unavailable', () => { + it('uses default values', async () => { + service['langService']['iso3'] = 'ger' + const keywords = await lastValueFrom(service.searchKeywords('road')) + expect(keywords).toEqual([ + { + description: 'localization of properties', + key: 'http://inspire.ec.europa.eu/theme/ad', + label: 'addresses', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', + }, + { + description: 'digital terrain models', + key: 'http://inspire.ec.europa.eu/theme/el', + label: 'altitude', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', + }, + ]) + }) + }) + }) describe('#getKeywordsByUri', () => { it('calls api service ', async () => { service.getKeywordsByUri('http://inspire.ec.europa.eu/theme/') diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts index 3f1519c17c..f1e1aaa7e8 100644 --- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts @@ -1,11 +1,10 @@ import { Injectable } from '@angular/core' import { Observable, combineLatest, of, switchMap } from 'rxjs' -import { catchError, map, shareReplay, tap } from 'rxjs/operators' +import { catchError, concatMap, map, shareReplay, tap } from 'rxjs/operators' import { MeApiService, RegistriesApiService, SiteApiService, - ThesaurusInfoApiModel, ToolsApiService, UserfeedbackApiService, UsersApiService, @@ -146,25 +145,35 @@ export class Gn4PlatformService implements PlatformServiceInterface { ) .pipe( map((thesaurus) => { - // FIXME: find a better way to exclude place keywords - // thesaurus[0].filter((thes) => thes.dname !== 'place') - return thesaurus[0] as ThesaurusApiResponse[] + const thesauriWithoutPlace = thesaurus[0].filter( + (thes) => thes.dname !== 'place' + ) + return thesauriWithoutPlace as ThesaurusApiResponse[] }), shareReplay(1) ) searchKeywords(query: string): Observable { - const keywords$: Observable = - this.registriesApiService.searchKeywords( - query, - this.langService.iso3, - 10, - 0, - null, - null, - null, - `*${query}*` - ) as Observable + const keywords$: Observable = this.allThesaurus$.pipe( + concatMap((thesaurus) => { + return this.registriesApiService + .searchKeywords( + query, + this.langService.iso3, + 10, + 0, + null, + thesaurus.map((thes) => thes.key), + null, + `*${query}*` + ) + .pipe( + map((keywords) => { + return keywords as KeywordApiResponse[] + }) + ) + }) + ) return combineLatest([keywords$, this.allThesaurus$]).pipe( map(([keywords, thesaurus]) => { From 2019bb990b81cf0aa846b7be144b0a81d4e67121 Mon Sep 17 00:00:00 2001 From: mmohad Date: Tue, 25 Jun 2024 17:25:57 +0200 Subject: [PATCH 016/378] conf(default): change for prod --- conf/default.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/default.toml b/conf/default.toml index b30314a055..0c0ea3631b 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -5,7 +5,7 @@ [global] # This URL (relative or absolute) must point to the API endpoint of a GeoNetwork4 instance -geonetwork4_api_url = "https://data-qua.priv.geopf.fr/catalog" +geonetwork4_api_url = "https://data.geopf.fr/catalog"" datahub_url = "/catalogue" # This should point to a proxy to avoid CORS errors on some requests (data preview, OGC capabilities etc.) # The actual URL will be appended after this path, e.g. : https://my.proxy/?url=http%3A%2F%2Fencoded.url%2Fows` From 3d3ee70b0997dc7ec1744b836cdc52b06eb5ca23 Mon Sep 17 00:00:00 2001 From: mmohad Date: Wed, 26 Jun 2024 09:37:03 +0200 Subject: [PATCH 017/378] fix(conf): bad write --- conf/default.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/default.toml b/conf/default.toml index 0c0ea3631b..134ab5e7e3 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -5,7 +5,7 @@ [global] # This URL (relative or absolute) must point to the API endpoint of a GeoNetwork4 instance -geonetwork4_api_url = "https://data.geopf.fr/catalog"" +geonetwork4_api_url = "https://data.geopf.fr/catalog" datahub_url = "/catalogue" # This should point to a proxy to avoid CORS errors on some requests (data preview, OGC capabilities etc.) # The actual URL will be appended after this path, e.g. : https://my.proxy/?url=http%3A%2F%2Fencoded.url%2Fows` From bea1f363c691b5181d4e015bc5180c70b154d6c5 Mon Sep 17 00:00:00 2001 From: Florian Necas Date: Wed, 26 Jun 2024 10:18:48 +0200 Subject: [PATCH 018/378] feat: last review change requested --- .../upload-data/upload-data.component.ts | 1 + support-services/docker-compose.yml | 15 --------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts b/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts index c139e3125d..bd750af4a9 100644 --- a/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts +++ b/apps/datafeeder/src/app/presentation/components/upload-data/upload-data.component.ts @@ -30,6 +30,7 @@ export class UploadDataComponent { 'application/zip', 'application/x-zip-compressed', 'text/csv', + 'application/csv', ] @Input() maxFileSizeMb: number diff --git a/support-services/docker-compose.yml b/support-services/docker-compose.yml index 91c4f4ff0d..8ee3b01ff6 100644 --- a/support-services/docker-compose.yml +++ b/support-services/docker-compose.yml @@ -153,18 +153,3 @@ services: condition: service_healthy ports: - '8081:8080' - - datahub: - image: ghcr.io/camptocamp/mel-dataplatform/datahub:latest - healthcheck: - test: - [ - 'CMD-SHELL', - 'curl -s -f http://localhost:80/datahub/ >/dev/null || exit 1', - ] - interval: 30s - timeout: 10s - retries: 10 - restart: always - ports: - - '8082:80' From 03828b5a8698cf6b3a2ee5a3824cc335de63d986 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 26 Jun 2024 11:05:22 +0200 Subject: [PATCH 019/378] fix(mdview): catch OGC API error silently --- .../src/lib/state/mdview.facade.spec.ts | 62 +++++++++++-------- .../record/src/lib/state/mdview.facade.ts | 8 ++- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/libs/feature/record/src/lib/state/mdview.facade.spec.ts b/libs/feature/record/src/lib/state/mdview.facade.spec.ts index dbfe53f427..f0bba8948b 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.spec.ts @@ -239,6 +239,30 @@ describe('MdViewFacade', () => { }) describe('geoDataLinksWithGeometry$', () => { + const links = [ + { + type: 'download', + url: new URL('http://my-org.net/download/2.geojson'), + mimeType: 'application/geo+json', + name: 'Direct download', + }, + { + type: 'service', + url: new URL('https://my-org.net/wfs'), + accessServiceProtocol: 'wfs', + name: 'my:featuretype', // FIXME: same as identifier otherwise it will be lost in iso... + description: 'This WFS service offers direct download capability', + identifierInService: 'my:featuretype', + }, + { + type: 'service', + url: new URL('https://my-org.net/ogc'), + accessServiceProtocol: 'ogcFeatures', + name: 'my:featuretype', + description: 'This OGC service offers direct download capability', + identifierInService: 'my:featuretype', + }, + ] beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected) @@ -251,32 +275,6 @@ describe('MdViewFacade', () => { }) }) it('should return OGC links that have geometry', fakeAsync(() => { - const values = { - a: [ - { - type: 'download', - url: new URL('http://my-org.net/download/2.geojson'), - mimeType: 'application/geo+json', - name: 'Direct download', - }, - { - type: 'service', - url: new URL('https://my-org.net/wfs'), - accessServiceProtocol: 'wfs', - name: 'my:featuretype', // FIXME: same as identifier otherwise it will be lost in iso... - description: 'This WFS service offers direct download capability', - identifierInService: 'my:featuretype', - }, - { - type: 'service', - url: new URL('https://my-org.net/ogc'), - accessServiceProtocol: 'ogcFeatures', - name: 'my:featuretype', - description: 'This OGC service offers direct download capability', - identifierInService: 'my:featuretype', - }, - ], - } jest.spyOn(facade.dataService, 'getItemsFromOgcApi').mockResolvedValue({ id: '123', type: 'Feature', @@ -291,7 +289,17 @@ describe('MdViewFacade', () => { let result facade.geoDataLinksWithGeometry$.subscribe((v) => (result = v)) tick() - expect(result).toEqual(values.a) + expect(result).toEqual(links) + })) + it('should return links that have geometry if OGC API does not respond', fakeAsync(() => { + jest + .spyOn(facade.dataService, 'getItemsFromOgcApi') + .mockRejectedValue(new Error('An error occurred')) + let result + facade.geoDataLinksWithGeometry$.subscribe((v) => (result = v)) + tick() + const linksWithoutOgcApi = links.slice(0, -1) + expect(result).toEqual(linksWithoutOgcApi) })) it('should not return OGC links that do not have geometry', fakeAsync(() => { const values = { diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index c979cce049..605b623bf6 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core' import { select, Store } from '@ngrx/store' import { + catchError, defaultIfEmpty, filter, map, mergeMap, - scan, switchMap, toArray, } from 'rxjs/operators' @@ -119,7 +119,11 @@ export class MdViewFacade { ? link : null }), - defaultIfEmpty(null) + defaultIfEmpty(null), + catchError((e) => { + console.error(e) + return of(null) + }) ) } else { return of(link) From 7c07b48aa7992609b0dff259c81a7131ea20de1f Mon Sep 17 00:00:00 2001 From: mmohad Date: Wed, 26 Jun 2024 13:16:47 +0200 Subject: [PATCH 020/378] fix(filters): fix pagination #41 --- .../lib/ign-api-dl/ign-api-dl.component.ts | 41 +++++++++++++++---- .../ign-api-produit.component.ts | 10 ++--- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index c6e6ebbc83..1bdcebe98b 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -76,7 +76,7 @@ export class IgnApiDlComponent implements OnInit { pageSize$ = new BehaviorSubject(this.initialPageSize) page$ = new BehaviorSubject('0') size$ = new BehaviorSubject(this.initialPageSize) - + // a passer en config url = 'https://data.geopf.fr/telechargement/capabilities?outputFormat=application/json' choices: any @@ -125,7 +125,6 @@ export class IgnApiDlComponent implements OnInit { } } outputUrl = url.toString() - console.log(outputUrl) } return outputUrl }) @@ -134,8 +133,7 @@ export class IgnApiDlComponent implements OnInit { listFilteredProduct$ = this.apiQueryUrl$.pipe( mergeMap((url) => { return this.getFilteredProduct$(url).pipe( - map((response) => response['entry']), - tap((el) => console.log(el)) + map((response) => response['entry']) ) }) ) @@ -193,11 +191,38 @@ export class IgnApiDlComponent implements OnInit { this.page$.next('0') } - async getFields() { - const [firstResponse] = await Promise.all([axios.get(this.url)]) - this.choices = firstResponse.data.entry.filter( + async getCapabilities() { + let page = 0 + let choicesTest = null + let [response] = await Promise.all([ + axios.get(this.url.concat(`&pageSize=200&page=${page}`)), + ]) + choicesTest = response.data.entry.filter( (element) => element['id'] == this.apiBaseUrl )[0] + console.log(choicesTest) + + if (choicesTest) { + return choicesTest + } else { + console.log('avant while') + + while (choicesTest === undefined || response.data.pageCount > page) { + console.log('hello') + ;[response] = await Promise.all([ + axios.get(this.url.concat(`&pageSize=200&page=${page}`)), + ]) + choicesTest = response.data.entry.filter( + (element) => element['id'] == this.apiBaseUrl + )[0] + page += 1 + } + } + return choicesTest + } + async getFields() { + this.choices = await this.getCapabilities() + this.bucketPromisesZone = this.choices.zone.map((bucket) => ({ value: bucket.label, label: bucket.term, @@ -218,5 +243,7 @@ export class IgnApiDlComponent implements OnInit { })) this.bucketPromisesCrs.sort((a, b) => (a.label > b.label ? 1 : -1)) this.bucketPromisesCrs.unshift({ value: 'null', label: 'CRS' }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion } } diff --git a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts index 813939b6a4..ddfcd4763b 100644 --- a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts +++ b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts @@ -25,12 +25,9 @@ export class IgnApiProduitComponent implements OnInit { liste$: Observable ngOnInit(): void { - this.liste$ = this.http.get(this.link['id']).pipe( - map( - (response) => response['entry'] - // tap(el=> console.log(el)), - ) - ) + this.liste$ = this.http + .get(this.link['id']) + .pipe(map((response) => response['entry'])) } downloadListe(): void { @@ -44,7 +41,6 @@ export class IgnApiProduitComponent implements OnInit { } download(url): void { - console.log(url) this.http.get(url).subscribe() } } From f098c15fb5f24c6cf1823a60a971c2da87ca4794 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Wed, 26 Jun 2024 13:22:21 +0200 Subject: [PATCH 021/378] feat(platform-service): Add argument keywordTypes to searchKeywords method in order to be able to specify the thesauri to be searched within. Add translations for keywords label, update translations. --- .../gn4/platform/gn4-platform.service.spec.ts | 62 ++++++++++++++++++- .../lib/gn4/platform/gn4-platform.service.ts | 46 +++++++------- .../src/lib/platform.service.interface.ts | 6 +- .../form-field-keywords.component.ts | 14 +++-- translations/de.json | 11 ++-- translations/en.json | 11 ++-- translations/es.json | 11 ++-- translations/fr.json | 11 ++-- translations/it.json | 11 ++-- translations/nl.json | 11 ++-- translations/pt.json | 11 ++-- translations/sk.json | 11 ++-- 12 files changed, 136 insertions(+), 80 deletions(-) diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts index 98c400e780..088bb54a26 100644 --- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts @@ -375,7 +375,7 @@ describe('Gn4PlatformService', () => { jest.spyOn(service, 'searchKeywords') }) it('calls api service with qeury', () => { - service.searchKeywords('road').subscribe() + service.searchKeywords('road', ['theme']).subscribe() expect(registriesApiService.searchKeywords).toHaveBeenCalledWith( 'road', 'fre', @@ -388,7 +388,9 @@ describe('Gn4PlatformService', () => { ) }) it('returns mapped thesaurus with translated values', async () => { - const keywords = await lastValueFrom(service.searchKeywords('road')) + const keywords = await lastValueFrom( + service.searchKeywords('road', ['theme']) + ) expect(keywords).toEqual([ { description: @@ -425,7 +427,9 @@ describe('Gn4PlatformService', () => { describe('if translations are unavailable', () => { it('uses default values', async () => { service['langService']['iso3'] = 'ger' - const keywords = await lastValueFrom(service.searchKeywords('road')) + const keywords = await lastValueFrom( + service.searchKeywords('road', ['theme']) + ) expect(keywords).toEqual([ { description: 'localization of properties', @@ -458,6 +462,58 @@ describe('Gn4PlatformService', () => { ]) }) }) + describe('if keywordType is empty Array', () => { + it('calls api service with empty array and returns keywords from all thesauri', async () => { + service.searchKeywords('road', ['theme']).subscribe() + const keywords = await lastValueFrom( + service.searchKeywords('road', ['theme']) + ) + + expect(registriesApiService.searchKeywords).toHaveBeenCalledWith( + 'road', + 'fre', + 10, + 0, + null, + ['external.theme.httpinspireeceuropaeutheme-theme'], + null, + '*road*' + ) + + expect(keywords).toEqual([ + { + description: + 'Localisation des propriétés fondée sur les identifiants des adresses, habituellement le nom de la rue, le numéro de la maison et le code postal.', + key: 'http://inspire.ec.europa.eu/theme/ad', + label: 'Adresses', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', + }, + { + description: + "Modèles numériques pour l'altitude des surfaces terrestres, glaciaires et océaniques. Comprend l'altitude terrestre, la bathymétrie et la ligne de rivage.", + key: 'http://inspire.ec.europa.eu/theme/el', + label: 'Altitude', + thesaurus: { + id: 'external.theme.httpinspireeceuropaeutheme-theme', + name: 'GEMET - INSPIRE themes, version 1.0', + type: 'theme', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.httpinspireeceuropaeutheme-theme' + ), + }, + type: 'theme', + }, + ]) + }) + }) }) describe('#getKeywordsByUri', () => { it('calls api service ', async () => { diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts index f1e1aaa7e8..3583787a47 100644 --- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core' import { Observable, combineLatest, of, switchMap } from 'rxjs' -import { catchError, concatMap, map, shareReplay, tap } from 'rxjs/operators' +import { catchError, map, shareReplay, tap } from 'rxjs/operators' import { MeApiService, RegistriesApiService, @@ -24,6 +24,7 @@ import { KeywordApiResponse, ThesaurusApiResponse, } from '@geonetwork-ui/api/metadata-converter' +import { KeywordType } from '@geonetwork-ui/common/domain/model/thesaurus' const minApiVersion = '4.2.2' @@ -145,33 +146,34 @@ export class Gn4PlatformService implements PlatformServiceInterface { ) .pipe( map((thesaurus) => { - const thesauriWithoutPlace = thesaurus[0].filter( - (thes) => thes.dname !== 'place' - ) - return thesauriWithoutPlace as ThesaurusApiResponse[] + return thesaurus[0] as ThesaurusApiResponse[] }), shareReplay(1) ) - searchKeywords(query: string): Observable { + searchKeywords( + query: string, + keywordTypes: KeywordType[] + ): Observable { const keywords$: Observable = this.allThesaurus$.pipe( - concatMap((thesaurus) => { - return this.registriesApiService - .searchKeywords( - query, - this.langService.iso3, - 10, - 0, - null, - thesaurus.map((thes) => thes.key), - null, - `*${query}*` - ) - .pipe( - map((keywords) => { - return keywords as KeywordApiResponse[] - }) + switchMap((thesaurus) => { + const selectedThesauri = [] + keywordTypes.map((keywordType) => { + selectedThesauri.push( + ...thesaurus.filter((thes) => thes.dname === keywordType) ) + }) + + return this.registriesApiService.searchKeywords( + query, + this.langService.iso3, + 10, + 0, + null, + selectedThesauri.map((thes) => thes.key), + null, + `*${query}*` + ) as Observable }) ) diff --git a/libs/common/domain/src/lib/platform.service.interface.ts b/libs/common/domain/src/lib/platform.service.interface.ts index 3a39f5a3b9..552ec7df1c 100644 --- a/libs/common/domain/src/lib/platform.service.interface.ts +++ b/libs/common/domain/src/lib/platform.service.interface.ts @@ -2,6 +2,7 @@ import type { Observable } from 'rxjs' import type { UserModel } from './model/user/user.model' import type { Organization } from './model/record/organization.model' import { Keyword, UserFeedback } from './model/record' +import { KeywordType } from './model/thesaurus' export abstract class PlatformServiceInterface { abstract getType(): string @@ -15,7 +16,10 @@ export abstract class PlatformServiceInterface { ): Observable abstract getOrganizations(): Observable abstract translateKey(key: string): Observable - abstract searchKeywords(query: string): Observable + abstract searchKeywords( + query: string, + keywordTypes: KeywordType[] + ): Observable abstract getKeywordsByUri(uri: string): Observable abstract getUserFeedbacks(recordUuid: string): Observable abstract postUserFeedbacks(recordUuid: UserFeedback): Observable diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.ts index b83745abba..c912468415 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-keywords/form-field-keywords.component.ts @@ -35,13 +35,15 @@ export class FormFieldKeywordsComponent { } autoCompleteAction = (query: string) => { - return this.platformService.searchKeywords(query).pipe( - map((keywords) => - keywords.map((keyword) => { - return { title: keyword.label, value: keyword } - }) + return this.platformService + .searchKeywords(query, ['temporal', 'theme', 'other']) + .pipe( + map((keywords) => + keywords.map((keyword) => { + return { title: keyword.label, value: keyword } + }) + ) ) - ) } constructor(private platformService: PlatformServiceInterface) {} diff --git a/translations/de.json b/translations/de.json index 0dc83bb483..51e549a2a4 100644 --- a/translations/de.json +++ b/translations/de.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{Datensätze} one{Datensatz} other{Datensätze}}", - "catalog.figures.organizations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", + "catalog.figures.organisations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", "chart.aggregation.average": "Durchschnitt", "chart.aggregation.count": "Anzahl", "chart.aggregation.max": "Maximum", @@ -153,6 +153,7 @@ "downloads.format.unknown": "unbekannt", "downloads.wfs.featuretype.not.found": "Der Layer wurde nicht gefunden", "dropFile": "Datei ablegen", + "editor.record.form.keywords": "Schlagwörter", "editor.record.form.license": "Lizenz", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Veröffentlichungen 0 → 9", "organisations.sortBy.recordCountDesc": "Veröffentlichungen 9 → 0", - "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.details.publishedDataset": "{count, plural, =0{} one{} other{}}", "organization.details.mailContact": "", - "organization.datasets": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", "organization.lastPublishedDatasets": "", "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Nächste Seite", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "Relevanz", "search.autocomplete.error": "Vorschläge konnten nicht abgerufen werden:", "search.error.couldNotReachApi": "Die API konnte nicht erreicht werden", + "search.error.organizationHasNoDataset": "", + "search.error.organizationNotFound": "", "search.error.receivedError": "Ein Fehler ist aufgetreten", "search.error.recordHasnolink": "", "search.error.recordNotFound": "Der Datensatz mit der Kennung \"{ id }\" konnte nicht gefunden werden.", - "search.error.organizationNotFound": "", - "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "Suche Datensätze ...", "search.field.sortBy": "Sortieren nach:", "search.filters.clear": "Zurücksetzen", diff --git a/translations/en.json b/translations/en.json index b0fda49554..d0ca52ce6b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "Log in", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organizations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", + "catalog.figures.organisations": "", "chart.aggregation.average": "average", "chart.aggregation.count": "count", "chart.aggregation.max": "max", @@ -153,6 +153,7 @@ "downloads.format.unknown": "unknown", "downloads.wfs.featuretype.not.found": "The layer was not found", "dropFile": "drop file", + "editor.record.form.keywords": "Keywords", "editor.record.form.license": "License", "editor.record.form.license.cc-by": "Creative Commons CC-BY", "editor.record.form.license.cc-by-sa": "Creative Commons CC-BY-SA", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Publications 0 → 9", "organisations.sortBy.recordCountDesc": "Publications 9 → 0", - "organization.header.recordCount": "{count, plural, =0{data} one{data} other{datas}}", - "organization.details.publishedDataset": "{count, plural, =0{published dataset} one{published dataset} other{published datasets}}", "organization.details.mailContact": "Contact by email", - "organization.datasets": "Datasets", + "organization.header.recordCount": "{count, plural, =0{data} one{data} other{datas}}", "organization.lastPublishedDatasets": "Last published datasets", "organization.lastPublishedDatasets.searchAllButton": "Search all", "pagination.nextPage": "Next page", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "Relevancy", "search.autocomplete.error": "Suggestions could not be fetched:", "search.error.couldNotReachApi": "The API could not be reached", + "search.error.organizationHasNoDataset": "This organization has no dataset yet.", + "search.error.organizationNotFound": "This organization could not be found.", "search.error.receivedError": "An error was received", "search.error.recordHasnolink": "This record currently has no link yet, please come back later.", "search.error.recordNotFound": "The record with identifier \"{ id }\" could not be found.", - "search.error.organizationNotFound": "This organization could not be found.", - "search.error.organizationHasNoDataset": "This organization has no dataset yet.", "search.field.any.placeholder": "Search datasets ...", "search.field.sortBy": "Sort by:", "search.filters.clear": "Reset", diff --git a/translations/es.json b/translations/es.json index 0084cb65a2..741c743cb9 100644 --- a/translations/es.json +++ b/translations/es.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de datos", - "catalog.figures.organizations": "organizaciones", + "catalog.figures.organisations": "", "chart.aggregation.average": "promedio", "chart.aggregation.count": "conteo", "chart.aggregation.max": "máximo", @@ -153,6 +153,7 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.keywords": "", "editor.record.form.license": "", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", - "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", "organization.details.mailContact": "", - "organization.datasets": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", "organization.lastPublishedDatasets": "", "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "", "search.autocomplete.error": "", "search.error.couldNotReachApi": "", + "search.error.organizationHasNoDataset": "", + "search.error.organizationNotFound": "", "search.error.receivedError": "", "search.error.recordHasnolink": "", "search.error.recordNotFound": "", - "search.error.organizationNotFound": "", - "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", diff --git a/translations/fr.json b/translations/fr.json index 9f0dfa1bd9..6c2b7e5f79 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "Se connecter", "catalog.figures.datasets": "{count, plural, =0{données} one{donnée} other{données}}", - "catalog.figures.organizations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", + "catalog.figures.organisations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", "chart.aggregation.average": "moyenne", "chart.aggregation.count": "nombre", "chart.aggregation.max": "maximum", @@ -153,6 +153,7 @@ "downloads.format.unknown": "inconnu", "downloads.wfs.featuretype.not.found": "La couche n'a pas été retrouvée", "dropFile": "Faites glisser votre fichier", + "editor.record.form.keywords": "Mots-clés", "editor.record.form.license": "Licence", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "Nom Z → A", "organisations.sortBy.recordCountAsc": "Données 0 → 9", "organisations.sortBy.recordCountDesc": "Données 9 → 0", - "organization.header.recordCount": "{count, plural, =0{donnée} one{donnée} other{données}}", - "organization.details.publishedDataset": "{count, plural, =0{donnée publiée} one{donnée publiée} other{données publiées}}", "organization.details.mailContact": "Contacter par mail", - "organization.datasets": "Données", + "organization.header.recordCount": "{count, plural, =0{donnée} one{donnée} other{données}}", "organization.lastPublishedDatasets": "Dernières données publiées", "organization.lastPublishedDatasets.searchAllButton": "Rechercher tous", "pagination.nextPage": "Page suivante", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "Pertinence", "search.autocomplete.error": "Les suggestions ne peuvent pas être récupérées", "search.error.couldNotReachApi": "Problème de connexion à l'API", + "search.error.organizationHasNoDataset": "Cette organisation n'a pas encore de données.", + "search.error.organizationNotFound": "L'organisation n'a pas pu être trouvée.", "search.error.receivedError": "Erreur retournée", "search.error.recordHasnolink": "Ce dataset n'a pas encore de lien, réessayez plus tard s'il vous plaît.", "search.error.recordNotFound": "Cette donnée n'a pu être trouvée.", - "search.error.organizationNotFound": "L'organisation n'a pas pu être trouvée.", - "search.error.organizationHasNoDataset": "Cette organisation n'a pas encore de données.", "search.field.any.placeholder": "Rechercher une donnée...", "search.field.sortBy": "Trier par :", "search.filters.clear": "Réinitialiser", diff --git a/translations/it.json b/translations/it.json index f5679702bd..b126c422e2 100644 --- a/translations/it.json +++ b/translations/it.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organizations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", + "catalog.figures.organisations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", "chart.aggregation.average": "media", "chart.aggregation.count": "conteggio", "chart.aggregation.max": "massimo", @@ -153,6 +153,7 @@ "downloads.format.unknown": "sconosciuto", "downloads.wfs.featuretype.not.found": "Il layer non è stato trovato", "dropFile": "Trascina il suo file", + "editor.record.form.keywords": "", "editor.record.form.license": "Licenza", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "Nome Z → A", "organisations.sortBy.recordCountAsc": "Dati 0 → 9", "organisations.sortBy.recordCountDesc": "Dati 9 → 0", - "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", "organization.details.mailContact": "", - "organization.datasets": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", "organization.lastPublishedDatasets": "", "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Pagina successiva", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "Rilevanza", "search.autocomplete.error": "Impossibile recuperare le suggerimenti", "search.error.couldNotReachApi": "Problema di connessione all'API", + "search.error.organizationHasNoDataset": "", + "search.error.organizationNotFound": "", "search.error.receivedError": "Errore ricevuto", "search.error.recordHasnolink": "", "search.error.recordNotFound": "Impossibile trovare questo dato", - "search.error.organizationNotFound": "", - "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "Cerca un dato...", "search.field.sortBy": "Ordina per:", "search.filters.clear": "Ripristina", diff --git a/translations/nl.json b/translations/nl.json index 283069f5a9..0236f0c0c5 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "datasets", - "catalog.figures.organizations": "organisaties", + "catalog.figures.organisations": "organisaties", "chart.aggregation.average": "gemiddelde", "chart.aggregation.count": "aantal", "chart.aggregation.max": "max", @@ -153,6 +153,7 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.keywords": "", "editor.record.form.license": "", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", - "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", "organization.details.mailContact": "", - "organization.datasets": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", "organization.lastPublishedDatasets": "", "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "", "search.autocomplete.error": "", "search.error.couldNotReachApi": "", + "search.error.organizationHasNoDataset": "", + "search.error.organizationNotFound": "", "search.error.receivedError": "", "search.error.recordHasnolink": "", "search.error.recordNotFound": "", - "search.error.organizationNotFound": "", - "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", diff --git a/translations/pt.json b/translations/pt.json index 0a6819fdad..dd1c147f16 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de dados", - "catalog.figures.organizations": "organizações", + "catalog.figures.organisations": "organizações", "chart.aggregation.average": "média", "chart.aggregation.count": "contagem", "chart.aggregation.max": "máximo", @@ -153,6 +153,7 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.keywords": "", "editor.record.form.license": "", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", - "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", "organization.details.mailContact": "", - "organization.datasets": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", "organization.lastPublishedDatasets": "", "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "", "search.autocomplete.error": "", "search.error.couldNotReachApi": "", + "search.error.organizationHasNoDataset": "", + "search.error.organizationNotFound": "", "search.error.receivedError": "", "search.error.recordHasnolink": "", "search.error.recordNotFound": "", - "search.error.organizationNotFound": "", - "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "", "search.field.sortBy": "", "search.filters.clear": "", diff --git a/translations/sk.json b/translations/sk.json index 4f45e3fea8..8a76204f8c 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasety} one{dataset} other{datasety}}", - "catalog.figures.organizations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", + "catalog.figures.organisations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", "chart.aggregation.average": "priemer", "chart.aggregation.count": "počet", "chart.aggregation.max": "maximum", @@ -153,6 +153,7 @@ "downloads.format.unknown": "neznámy", "downloads.wfs.featuretype.not.found": "Vrstva nebola nájdená", "dropFile": "nahrať súbor", + "editor.record.form.keywords": "", "editor.record.form.license": "Licencia", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -252,10 +253,8 @@ "organisations.sortBy.nameDesc": "Názov Z → A", "organisations.sortBy.recordCountAsc": "Publikácie 0 → 9", "organisations.sortBy.recordCountDesc": "Publikácie 9 → 0", - "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.details.publishedDataset": "{count, plural, =0{} one{} other{{}}", "organization.details.mailContact": "", - "organization.datasets": "", + "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", "organization.lastPublishedDatasets": "", "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Ďalšia stránka", @@ -355,11 +354,11 @@ "results.sortBy.relevancy": "Relevancia", "search.autocomplete.error": "Návrhy sa nepodarilo načítať:", "search.error.couldNotReachApi": "K rozhraniu API sa nepodarilo pripojiť", + "search.error.organizationHasNoDataset": "", + "search.error.organizationNotFound": "", "search.error.receivedError": "Bola zaznamenaná chyba", "search.error.recordHasnolink": "", "search.error.recordNotFound": "Záznam s identifikátorom \"{ id }\" sa nepodarilo nájsť.", - "search.error.organizationNotFound": "", - "search.error.organizationHasNoDataset": "", "search.field.any.placeholder": "Hľadať datasety ...", "search.field.sortBy": "Zoradiť podľa:", "search.filters.clear": "Obnoviť", From a824251263a7f1c18660efa511d526eeb76b33af Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 26 Jun 2024 17:43:50 +0200 Subject: [PATCH 022/378] fix(dh): Fixed some organization translation typos --- .../navigation-menu/navigation-menu.component.spec.ts | 2 +- .../app/home/navigation-menu/navigation-menu.component.ts | 4 ++-- .../home/news-page/key-figures/key-figures.component.ts | 2 +- .../organization-details.component.html | 4 ++-- translations/de.json | 8 ++++---- translations/en.json | 8 ++++---- translations/es.json | 8 ++++---- translations/fr.json | 8 ++++---- translations/it.json | 8 ++++---- translations/nl.json | 8 ++++---- translations/pt.json | 8 ++++---- translations/sk.json | 8 ++++---- 12 files changed, 38 insertions(+), 38 deletions(-) diff --git a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts index 20e073e7ff..fef9b933ad 100644 --- a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts +++ b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.spec.ts @@ -78,7 +78,7 @@ describe('NavigationMenuComponent', () => { }) it('displays activeLabel for organisations', async () => { const activeLabel = (await readFirst(component.activeLink$)).label - expect(activeLabel).toEqual('datahub.header.organisations') + expect(activeLabel).toEqual('datahub.header.organizations') }) }) describe('navigate to a route with missing label', () => { diff --git a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts index 862c2744cc..0768891642 100644 --- a/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts +++ b/apps/datahub/src/app/home/navigation-menu/navigation-menu.component.ts @@ -13,7 +13,7 @@ import { getThemeConfig } from '@geonetwork-ui/util/app-config' marker('datahub.header.news') marker('datahub.header.datasets') -marker('datahub.header.organisations') +marker('datahub.header.organizations') @Component({ selector: 'datahub-navigation-menu', @@ -34,7 +34,7 @@ export class NavigationMenuComponent { }, { link: `${ROUTER_ROUTE_ORGANIZATIONS}`, - label: 'datahub.header.organisations', + label: 'datahub.header.organizations', }, ] diff --git a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts index 30880371a7..37fdfb234e 100644 --- a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts +++ b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts @@ -7,7 +7,7 @@ import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/orga import { marker } from '@biesbjerg/ngx-translate-extract-marker' marker('catalog.figures.datasets') -marker('catalog.figures.organisations') +marker('catalog.figures.organizations') @Component({ selector: 'datahub-key-figures', 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 d08616ad65..4147dd8a79 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 @@ -65,7 +65,7 @@ class="font-title text-[28px] font-medium text-title text-center sm:text-left" translate > - organization.lastPublishedDatasets + organization.details.lastPublishedDatasets

    - organization.lastPublishedDatasets.searchAllButton + organization.details.lastPublishedDatasets.searchAllButton
    diff --git a/translations/de.json b/translations/de.json index 51e549a2a4..4a0d13dcae 100644 --- a/translations/de.json +++ b/translations/de.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{Datensätze} one{Datensatz} other{Datensätze}}", - "catalog.figures.organisations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", + "catalog.figures.organizations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", "chart.aggregation.average": "Durchschnitt", "chart.aggregation.count": "Anzahl", "chart.aggregation.max": "Maximum", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "Die neuesten", "datahub.header.myfavorites": "Meine Favoriten", "datahub.header.news": "Startseite", - "datahub.header.organisations": "Organisationen", + "datahub.header.organizations": "Organisationen", "datahub.header.popularRecords": "Die beliebtesten", "datahub.header.title.html": "
    Entdecken Sie offene
    Daten meiner Organisation
    ", "datahub.news.contact.contactus": "Kontaktieren Sie uns", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Veröffentlichungen 0 → 9", "organisations.sortBy.recordCountDesc": "Veröffentlichungen 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Nächste Seite", "pagination.page": "Seite", "pagination.pageOf": "von", diff --git a/translations/en.json b/translations/en.json index d0ca52ce6b..74c24bb2c6 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "Log in", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "{count, plural, =0{organizations} one{organization} other{organizations}}", "chart.aggregation.average": "average", "chart.aggregation.count": "count", "chart.aggregation.max": "max", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "The latest", "datahub.header.myfavorites": "My favorites", "datahub.header.news": "Home", - "datahub.header.organisations": "Organisations", + "datahub.header.organizations": "Organizations", "datahub.header.popularRecords": "The most popular", "datahub.header.title.html": "
    Discover open
    data from my Organization
    ", "datahub.news.contact.contactus": "Contact us", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Publications 0 → 9", "organisations.sortBy.recordCountDesc": "Publications 9 → 0", + "organization.details.lastPublishedDatasets": "Last published datasets", + "organization.details.lastPublishedDatasets.searchAllButton": "Search all", "organization.details.mailContact": "Contact by email", "organization.header.recordCount": "{count, plural, =0{data} one{data} other{datas}}", - "organization.lastPublishedDatasets": "Last published datasets", - "organization.lastPublishedDatasets.searchAllButton": "Search all", "pagination.nextPage": "Next page", "pagination.page": "page", "pagination.pageOf": "of", diff --git a/translations/es.json b/translations/es.json index 741c743cb9..779f27b4f6 100644 --- a/translations/es.json +++ b/translations/es.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de datos", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "organizaciones", "chart.aggregation.average": "promedio", "chart.aggregation.count": "conteo", "chart.aggregation.max": "máximo", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "", "datahub.header.myfavorites": "", "datahub.header.news": "", - "datahub.header.organisations": "", + "datahub.header.organizations": "", "datahub.header.popularRecords": "", "datahub.header.title.html": "", "datahub.news.contact.contactus": "", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/fr.json b/translations/fr.json index 6c2b7e5f79..5f927b5575 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "Se connecter", "catalog.figures.datasets": "{count, plural, =0{données} one{donnée} other{données}}", - "catalog.figures.organisations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", + "catalog.figures.organizations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", "chart.aggregation.average": "moyenne", "chart.aggregation.count": "nombre", "chart.aggregation.max": "maximum", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "Les plus récentes", "datahub.header.myfavorites": "Mes favoris", "datahub.header.news": "Accueil", - "datahub.header.organisations": "Organisations", + "datahub.header.organizations": "Organisations", "datahub.header.popularRecords": "Les plus appréciées", "datahub.header.title.html": "
    Toutes les données
    publiques de mon organisation
    ", "datahub.news.contact.contactus": "Contactez-nous", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "Nom Z → A", "organisations.sortBy.recordCountAsc": "Données 0 → 9", "organisations.sortBy.recordCountDesc": "Données 9 → 0", + "organization.details.lastPublishedDatasets": "Dernières données publiées", + "organization.details.lastPublishedDatasets.searchAllButton": "Rechercher tous", "organization.details.mailContact": "Contacter par mail", "organization.header.recordCount": "{count, plural, =0{donnée} one{donnée} other{données}}", - "organization.lastPublishedDatasets": "Dernières données publiées", - "organization.lastPublishedDatasets.searchAllButton": "Rechercher tous", "pagination.nextPage": "Page suivante", "pagination.page": "page", "pagination.pageOf": "sur", diff --git a/translations/it.json b/translations/it.json index b126c422e2..2480390ab8 100644 --- a/translations/it.json +++ b/translations/it.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organisations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", + "catalog.figures.organizations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", "chart.aggregation.average": "media", "chart.aggregation.count": "conteggio", "chart.aggregation.max": "massimo", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "Ultimi", "datahub.header.myfavorites": "Miei preferiti", "datahub.header.news": "Home", - "datahub.header.organisations": "Organizzazioni", + "datahub.header.organizations": "Organizzazioni", "datahub.header.popularRecords": "Più popolari", "datahub.header.title.html": "
    Tutti i dati
    pubblici della mia organizzazione
    ", "datahub.news.contact.contactus": "Contattateci", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "Nome Z → A", "organisations.sortBy.recordCountAsc": "Dati 0 → 9", "organisations.sortBy.recordCountDesc": "Dati 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Pagina successiva", "pagination.page": "pagina", "pagination.pageOf": "di", diff --git a/translations/nl.json b/translations/nl.json index 0236f0c0c5..1bce9d226e 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "datasets", - "catalog.figures.organisations": "organisaties", + "catalog.figures.organizations": "organisaties", "chart.aggregation.average": "gemiddelde", "chart.aggregation.count": "aantal", "chart.aggregation.max": "max", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "", "datahub.header.myfavorites": "", "datahub.header.news": "", - "datahub.header.organisations": "", + "datahub.header.organizations": "", "datahub.header.popularRecords": "", "datahub.header.title.html": "", "datahub.news.contact.contactus": "", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/pt.json b/translations/pt.json index dd1c147f16..daaeb899ac 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de dados", - "catalog.figures.organisations": "organizações", + "catalog.figures.organizations": "organizações", "chart.aggregation.average": "média", "chart.aggregation.count": "contagem", "chart.aggregation.max": "máximo", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "", "datahub.header.myfavorites": "", "datahub.header.news": "", - "datahub.header.organisations": "", + "datahub.header.organizations": "", "datahub.header.popularRecords": "", "datahub.header.title.html": "", "datahub.news.contact.contactus": "", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/sk.json b/translations/sk.json index 8a76204f8c..d1a23894fd 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasety} one{dataset} other{datasety}}", - "catalog.figures.organisations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", + "catalog.figures.organizations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", "chart.aggregation.average": "priemer", "chart.aggregation.count": "počet", "chart.aggregation.max": "maximum", @@ -117,7 +117,7 @@ "datahub.header.lastRecords": "Najnovšie", "datahub.header.myfavorites": "Moje obľúbené", "datahub.header.news": "Domov", - "datahub.header.organisations": "Organizácie", + "datahub.header.organizations": "Organizácie", "datahub.header.popularRecords": "Najpopulárnejšie", "datahub.header.title.html": "
    Objavte otvorené
    dáta z mojej organizácie
    ", "datahub.news.contact.contactus": "Kontaktujte nás", @@ -253,10 +253,10 @@ "organisations.sortBy.nameDesc": "Názov Z → A", "organisations.sortBy.recordCountAsc": "Publikácie 0 → 9", "organisations.sortBy.recordCountDesc": "Publikácie 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Ďalšia stránka", "pagination.page": "strana", "pagination.pageOf": "z", From 4f1441a519115a4b8f4e6055c6b44a7ed6bd37e5 Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Wed, 26 Jun 2024 18:12:35 +0200 Subject: [PATCH 023/378] refactor: recordsCount$ should not catch the error --- .../key-figures/key-figures.component.spec.ts | 15 +++++++++++++-- .../key-figures/key-figures.component.ts | 8 ++++++-- .../gn-figure-datasets.component.ts | 9 ++++++--- .../src/lib/records/records.service.spec.ts | 14 +------------- .../catalog/src/lib/records/records.service.ts | 7 +++---- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts index a91879ff46..bf900a05ad 100644 --- a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts +++ b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { KeyFiguresComponent } from './key-figures.component' -import { of } from 'rxjs' +import { BehaviorSubject, of } from 'rxjs' import { RecordsService } from '@geonetwork-ui/feature/catalog' import { TranslateModule } from '@ngx-translate/core' import { NO_ERRORS_SCHEMA } from '@angular/core' @@ -8,8 +8,9 @@ import { RouterTestingModule } from '@angular/router/testing' import { By } from '@angular/platform-browser' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +const recordsCount$ = new BehaviorSubject(1234) class RecordsServiceMock { - recordsCount$ = of(1234) + recordsCount$ = recordsCount$ } class OrganisationsServiceMock { @@ -58,6 +59,16 @@ describe('KeyFiguresComponent', () => { it('emits the records count', () => { expect(values[1]).toBe(1234) }) + describe('when the request does not behave as expected', () => { + beforeEach(() => { + recordsCount$.error('blargz') + }) + it('emits -', () => { + let count + component.recordsCount$.subscribe((v) => (count = v)) + expect(count).toBe('-') + }) + }) }) describe('orgsCount$', () => { diff --git a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts index bee90f8cfe..c4643d73a4 100644 --- a/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts +++ b/apps/datahub/src/app/home/news-page/key-figures/key-figures.component.ts @@ -1,10 +1,11 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { startWith } from 'rxjs/operators' +import { catchError, startWith } from 'rxjs/operators' import { RecordsService } from '@geonetwork-ui/feature/catalog' import { ROUTER_ROUTE_SEARCH } from '@geonetwork-ui/feature/router' import { ROUTER_ROUTE_ORGANISATIONS } from '../../../router/constants' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { of } from 'rxjs' marker('catalog.figures.datasets') marker('catalog.figures.organisations') @@ -16,7 +17,10 @@ marker('catalog.figures.organisations') changeDetection: ChangeDetectionStrategy.OnPush, }) export class KeyFiguresComponent { - recordsCount$ = this.catalogRecords.recordsCount$.pipe(startWith('-')) + recordsCount$ = this.catalogRecords.recordsCount$.pipe( + startWith('-'), + catchError(() => of('-')) + ) orgsCount$ = this.catalogOrgs.organisationsCount$.pipe(startWith('-')) ROUTE_SEARCH = `/${ROUTER_ROUTE_SEARCH}` ROUTE_ORGANISATIONS = `/${ROUTER_ROUTE_ORGANISATIONS}` diff --git a/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts index 046aaeae89..aaeaf65cb7 100644 --- a/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts +++ b/apps/webcomponents/src/app/components/gn-figure-datasets/gn-figure-datasets.component.ts @@ -7,8 +7,8 @@ import { } from '@angular/core' import { BaseComponent } from '../base.component' import { RecordsService } from '@geonetwork-ui/feature/catalog' -import { startWith } from 'rxjs/operators' -import { Observable } from 'rxjs' +import { catchError, startWith } from 'rxjs/operators' +import { Observable, of } from 'rxjs' import { SearchFacade } from '@geonetwork-ui/feature/search' @Component({ @@ -26,7 +26,10 @@ export class GnFigureDatasetsComponent extends BaseComponent { constructor(injector: Injector, private changeDetector: ChangeDetectorRef) { super(injector) this.catalogRecords = injector.get(RecordsService) - this.recordsCount$ = this.catalogRecords.recordsCount$.pipe(startWith('-')) + this.recordsCount$ = this.catalogRecords.recordsCount$.pipe( + startWith('-'), + catchError(() => of('-')) + ) } init(): void { diff --git a/libs/feature/catalog/src/lib/records/records.service.spec.ts b/libs/feature/catalog/src/lib/records/records.service.spec.ts index be91cb1cb2..fff21c5376 100644 --- a/libs/feature/catalog/src/lib/records/records.service.spec.ts +++ b/libs/feature/catalog/src/lib/records/records.service.spec.ts @@ -1,5 +1,5 @@ import { RecordsService } from './records.service' -import { of, throwError } from 'rxjs' +import { of } from 'rxjs' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' class RecordsRepositoryMock { @@ -33,17 +33,5 @@ describe('RecordsService', () => { expect(repository.getMatchesCount).toHaveBeenCalledTimes(1) }) }) - - describe('when the request does not behave as expected', () => { - beforeEach(() => { - repository.getMatchesCount = () => throwError(() => 'blargz') - service = new RecordsService(repository) // create a new service to enable the changed repository behaviour - }) - it('emits 0', () => { - let count - service.recordsCount$.subscribe((v) => (count = v)) - expect(count).toBe(0) - }) - }) }) }) diff --git a/libs/feature/catalog/src/lib/records/records.service.ts b/libs/feature/catalog/src/lib/records/records.service.ts index 8c1fa8cd22..2d332f1155 100644 --- a/libs/feature/catalog/src/lib/records/records.service.ts +++ b/libs/feature/catalog/src/lib/records/records.service.ts @@ -1,16 +1,15 @@ import { Injectable } from '@angular/core' import { Observable, of, switchMap } from 'rxjs' -import { catchError, shareReplay } from 'rxjs/operators' +import { shareReplay } from 'rxjs/operators' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @Injectable({ providedIn: 'root', }) export class RecordsService { - recordsCount$: Observable = of(0).pipe( + recordsCount$: Observable = of(true).pipe( switchMap(() => this.recordsRepository.getMatchesCount({})), - shareReplay(1), - catchError(() => of(0)) + shareReplay(1) ) constructor(private recordsRepository: RecordsRepositoryInterface) {} From 85e33e30490afafe5e9a1c562b1b488e4712ba49 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 28 Jun 2024 14:37:53 +0200 Subject: [PATCH 024/378] feat: make map view component work --- .../gn-dataset-view-map.component.css | 6 +++ .../gn-dataset-view-map.component.ts | 22 ++++++-- .../gn-dataset-view-map.sample.html | 53 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.sample.html diff --git a/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.css b/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.css index f1146d699b..6ac7bba4dd 100644 --- a/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.css +++ b/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.css @@ -1 +1,7 @@ @import '../../../styles.css'; +@import 'ol/ol.css'; + +:host { + display: block; + height: 500px; +} diff --git a/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.ts b/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.ts index 7f7c3907b4..7c5494e54b 100644 --- a/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.ts +++ b/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.component.ts @@ -1,13 +1,17 @@ import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, Injector, + Input, OnInit, ViewEncapsulation, } from '@angular/core' import { MdViewFacade } from '@geonetwork-ui/feature/record' import { SearchFacade } from '@geonetwork-ui/feature/search' import { BaseComponent } from '../base.component' +import { LinkUsage } from '@geonetwork-ui/util/shared' +import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' @Component({ selector: 'wc-gn-dataset-view-map', @@ -18,10 +22,22 @@ import { BaseComponent } from '../base.component' providers: [SearchFacade], }) export class GnDatasetViewMapComponent extends BaseComponent implements OnInit { - constructor(injector: Injector, private mdViewFacade: MdViewFacade) { + constructor( + injector: Injector, + private mdViewFacade: MdViewFacade, + private changeDetector: ChangeDetectorRef + ) { super(injector) } - ngOnInit(): void { - this.mdViewFacade.loadFull('ee965118-2416-4d48-b07e-bbc696f002c2') + @Input() datasetId: string + link: DatasetDistribution + async init() { + super.init() + this.mdViewFacade.loadFull(this.datasetId) + this.link = await this.getRecordLink(this.datasetId, [ + LinkUsage.MAP_API, + LinkUsage.GEODATA, + ]) + this.changeDetector.detectChanges() } } diff --git a/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.sample.html b/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.sample.html new file mode 100644 index 0000000000..f46c32478d --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-dataset-view-map/gn-dataset-view-map.sample.html @@ -0,0 +1,53 @@ + + + + + Web Component Demo + + + + + + + + +
    +
    +

    + Visualize Map dataset from a Metadata Record with + gn-dataset-view-map web component +

    +
    + + + +
    + + From 47aee91b9294459f43eee5bc7da12a9285f11b4f Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 28 Jun 2024 14:39:19 +0200 Subject: [PATCH 025/378] fea: include tables and maps into embedding and share components --- .../data-view-permalink.component.spec.ts | 101 ++++++++++----- .../data-view-permalink.component.ts | 55 ++++++--- .../data-view-share.component.html | 8 +- .../data-view-share.component.ts | 2 + .../data-view-web-component.component.spec.ts | 115 +++++++++++++----- .../data-view-web-component.component.ts | 71 +++++++++-- 6 files changed, 256 insertions(+), 96 deletions(-) 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 ac42247bc6..f93ecb8541 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 @@ -4,7 +4,7 @@ import { DataViewPermalinkComponent, WEB_COMPONENT_EMBEDDER_URL, } from './data-view-permalink.component' -import { BehaviorSubject, firstValueFrom } from 'rxjs' +import { BehaviorSubject, firstValueFrom, lastValueFrom, takeLast } from 'rxjs' import { MdViewFacade } from '../state' import { Component, Input } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' @@ -75,6 +75,7 @@ describe('DataViewPermalinkComponent', () => { facade = TestBed.inject(MdViewFacade) fixture = TestBed.createComponent(DataViewPermalinkComponent) component = fixture.componentInstance + component.tabIndex$.next(2) fixture.detectChanges() }) @@ -82,39 +83,79 @@ describe('DataViewPermalinkComponent', () => { expect(component).toBeTruthy() }) - describe('init permalinkUrl$', () => { - it('should generate URL based on configs', async () => { - const url = await firstValueFrom(component.permalinkUrl$) - expect(url).toBe( - `https://example.com/wc-embedder?v=${gnUiVersion}&e=gn-dataset-view-chart&a=api-url%3D${encodeURIComponent( - component.config.basePath - )}&a=dataset-id%3D${ - metadata.uniqueIdentifier - }&a=primary-color%3D%230f4395&a=secondary-color%3D%238bc832&a=main-color%3D%23555&a=background-color%3D%23fdfbff&a=aggregation%3D${ - chartConfig1.aggregation - }&a=x-property%3D${chartConfig1.xProperty}&a=y-property%3D${ - chartConfig1.yProperty - }&a=chart-type%3D${chartConfig1.chartType}` - ) + describe('Chart view', () => { + describe('init permalinkUrl$', () => { + it('should generate URL based on configs', async () => { + const url = await firstValueFrom(component.permalinkUrl$) + expect(url).toBe( + `https://example.com/wc-embedder?v=${gnUiVersion}&e=gn-dataset-view-chart&a=aggregation%3D${ + chartConfig1.aggregation + }&a=x-property%3D${chartConfig1.xProperty}&a=y-property%3D${ + chartConfig1.yProperty + }&a=chart-type%3D${ + chartConfig1.chartType + }&a=api-url%3D${encodeURIComponent( + component.config.basePath + )}&a=dataset-id%3D${ + metadata.uniqueIdentifier + }&a=primary-color%3D%230f4395&a=secondary-color%3D%238bc832&a=main-color%3D%23555&a=background-color%3D%23fdfbff` + ) + }) + }) + describe('update permalinkUrl$', () => { + beforeEach(() => { + facade.chartConfig$.next(chartConfig2) + }) + it('should update URL based on configs', async () => { + const url = await firstValueFrom(component.permalinkUrl$) + expect(url).toBe( + `https://example.com/wc-embedder?v=${gnUiVersion}&e=gn-dataset-view-chart&a=aggregation%3D${ + chartConfig2.aggregation + }&a=x-property%3D${chartConfig2.xProperty}&a=y-property%3D${ + chartConfig2.yProperty + }&a=chart-type%3D${ + chartConfig2.chartType + }&a=api-url%3D${encodeURIComponent( + component.config.basePath + )}&a=dataset-id%3D${ + metadata.uniqueIdentifier + }&a=primary-color%3D%230f4395&a=secondary-color%3D%238bc832&a=main-color%3D%23555&a=background-color%3D%23fdfbff` + ) + }) + }) + }) + describe('Map view', () => { + beforeEach(() => { + component.tabIndex$.next(0) + }) + describe('init permalinkUrl$', () => { + it('should generate URL based on configs', async () => { + const url = await firstValueFrom(component.permalinkUrl$) + expect(url).toBe( + `https://example.com/wc-embedder?v=${gnUiVersion}&e=gn-dataset-view-map&a=api-url%3D${encodeURIComponent( + component.config.basePath + )}&a=dataset-id%3D${ + metadata.uniqueIdentifier + }&a=primary-color%3D%230f4395&a=secondary-color%3D%238bc832&a=main-color%3D%23555&a=background-color%3D%23fdfbff` + ) + }) }) }) - describe('update permalinkUrl$', () => { + describe('Table view', () => { beforeEach(() => { - facade.chartConfig$.next(chartConfig2) + component.tabIndex$.next(1) }) - it('should update URL based on configs', async () => { - const url = await firstValueFrom(component.permalinkUrl$) - expect(url).toBe( - `https://example.com/wc-embedder?v=${gnUiVersion}&e=gn-dataset-view-chart&a=api-url%3D${encodeURIComponent( - component.config.basePath - )}&a=dataset-id%3D${ - metadata.uniqueIdentifier - }&a=primary-color%3D%230f4395&a=secondary-color%3D%238bc832&a=main-color%3D%23555&a=background-color%3D%23fdfbff&a=aggregation%3D${ - chartConfig2.aggregation - }&a=x-property%3D${chartConfig2.xProperty}&a=y-property%3D${ - chartConfig2.yProperty - }&a=chart-type%3D${chartConfig2.chartType}` - ) + describe('init permalinkUrl$', () => { + it('should generate URL based on configs', async () => { + const url = await firstValueFrom(component.permalinkUrl$) + expect(url).toBe( + `https://example.com/wc-embedder?v=${gnUiVersion}&e=gn-dataset-view-table&a=api-url%3D${encodeURIComponent( + component.config.basePath + )}&a=dataset-id%3D${ + metadata.uniqueIdentifier + }&a=primary-color%3D%230f4395&a=secondary-color%3D%238bc832&a=main-color%3D%23555&a=background-color%3D%23fdfbff` + ) + }) }) }) }) 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 1bff396f40..f430cc8de7 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 @@ -3,10 +3,11 @@ import { Component, Inject, InjectionToken, + Input, Optional, } from '@angular/core' import { Configuration } from '@geonetwork-ui/data-access/gn4' -import { combineLatest, map } from 'rxjs' +import { BehaviorSubject, combineLatest, map } from 'rxjs' import { MdViewFacade } from '../state' import { GN_UI_VERSION } from '../gn-ui-version.token' @@ -21,29 +22,45 @@ export const WEB_COMPONENT_EMBEDDER_URL = new InjectionToken( changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataViewPermalinkComponent { + tabIndex$ = new BehaviorSubject(0) + @Input() + set tabIndex(value: number) { + this.tabIndex$.next(value) + } + permalinkUrl$ = combineLatest([ + this.tabIndex$, this.facade.chartConfig$, this.facade.metadata$, ]).pipe( - map(([config, metadata]) => { - if (config) { - const { aggregation, xProperty, yProperty, chartType } = config - const url = new URL(`${this.wcEmbedderBaseUrl}`, window.location.origin) - url.searchParams.set('v', `${this.version}`) - url.searchParams.append('e', `gn-dataset-view-chart`) - url.searchParams.append('a', `api-url=${this.config.basePath}`) - url.searchParams.append('a', `dataset-id=${metadata.uniqueIdentifier}`) - url.searchParams.append('a', `primary-color=#0f4395`) - url.searchParams.append('a', `secondary-color=#8bc832`) - url.searchParams.append('a', `main-color=#555`) - url.searchParams.append('a', `background-color=#fdfbff`) - url.searchParams.append('a', `aggregation=${aggregation}`) - url.searchParams.append('a', `x-property=${xProperty}`) - url.searchParams.append('a', `y-property=${yProperty}`) - url.searchParams.append('a', `chart-type=${chartType}`) - return url.toString() + map(([tabIndex, config, metadata]) => { + const url = new URL(`${this.wcEmbedderBaseUrl}`, window.location.origin) + url.searchParams.set('v', `${this.version}`) + if (tabIndex === 2) { + if (config) { + const { aggregation, xProperty, yProperty, chartType } = config + url.searchParams.append('e', `gn-dataset-view-chart`) + url.searchParams.append('a', `aggregation=${aggregation}`) + url.searchParams.append('a', `x-property=${xProperty}`) + url.searchParams.append('a', `y-property=${yProperty}`) + url.searchParams.append('a', `chart-type=${chartType}`) + } else { + return '' + } + } else if (tabIndex === 1) { + // table + url.searchParams.append('e', `gn-dataset-view-table`) + } else { + // map + url.searchParams.append('e', `gn-dataset-view-map`) } - return '' + url.searchParams.append('a', `api-url=${this.config.basePath}`) + url.searchParams.append('a', `dataset-id=${metadata.uniqueIdentifier}`) + url.searchParams.append('a', `primary-color=#0f4395`) + url.searchParams.append('a', `secondary-color=#8bc832`) + url.searchParams.append('a', `main-color=#555`) + url.searchParams.append('a', `background-color=#fdfbff`) + return url.toString() }) ) diff --git a/libs/feature/record/src/lib/data-view-share/data-view-share.component.html b/libs/feature/record/src/lib/data-view-share/data-view-share.component.html index 4af8536077..f9d5a39fe3 100644 --- a/libs/feature/record/src/lib/data-view-share/data-view-share.component.html +++ b/libs/feature/record/src/lib/data-view-share/data-view-share.component.html @@ -10,7 +10,9 @@ share.tab.permalink - + @@ -24,7 +26,9 @@ >share.tab.webComponent - +
    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 2480fd2e67..7be9f4527c 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 @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Inject, + Input, Optional, } from '@angular/core' import { WEB_COMPONENT_EMBEDDER_URL } from '../data-view-permalink/data-view-permalink.component' @@ -13,6 +14,7 @@ import { WEB_COMPONENT_EMBEDDER_URL } from '../data-view-permalink/data-view-per changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataViewShareComponent { + @Input() tabIndex: number constructor( @Optional() @Inject(WEB_COMPONENT_EMBEDDER_URL) 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 b274b4299b..38221b2293 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 @@ -69,6 +69,7 @@ describe('DataViewWebComponentComponent', () => { facade = TestBed.inject(MdViewFacade) fixture = TestBed.createComponent(DataViewWebComponentComponent) component = fixture.componentInstance + component.tabIndex$.next(2) fixture.detectChanges() }) @@ -76,51 +77,99 @@ describe('DataViewWebComponentComponent', () => { expect(component).toBeTruthy() }) - describe('init webComponentHtml$', () => { - it('should generate HTML based on configs', async () => { - const html = await firstValueFrom(component.webComponentHtml$) - expect(html).toBe( - ` -` - ) + describe('Chart view', () => { + describe('init webComponentHtml$', () => { + it('should generate HTML based on configs', async () => { + const html = await firstValueFrom(component.webComponentHtml$) + expect(html).toBe( + ` + ` + ) + }) + }) + describe('update webComponentHtml$', () => { + beforeEach(() => { + facade.chartConfig$.next(chartConfig2) + }) + it('should update HTML based on configs', async () => { + const html = await firstValueFrom(component.webComponentHtml$) + expect(html).toBe( + ` + ` + ) + }) }) }) - describe('update webComponentHtml$', () => { + describe('Map view', () => { beforeEach(() => { - facade.chartConfig$.next(chartConfig2) + component.tabIndex$.next(0) }) - it('should update HTML based on configs', async () => { - const html = await firstValueFrom(component.webComponentHtml$) - expect(html).toBe( - ` - { + it('should generate HTML based on configs', async () => { + const html = await firstValueFrom(component.webComponentHtml$) + expect(html).toBe( + ` +` - ) +>` + ) + }) + }) + }) + describe('Table view', () => { + beforeEach(() => { + component.tabIndex$.next(1) + }) + describe('init webComponentHtml$', () => { + it('should generate HTML based on configs', async () => { + const html = await firstValueFrom(component.webComponentHtml$) + expect(html).toBe( + ` + ` + ) + }) }) }) }) 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 37b84acfd2..a2164d2562 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 @@ -1,7 +1,12 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + Inject, + Input, +} from '@angular/core' import { Configuration } from '@geonetwork-ui/data-access/gn4' import { MdViewFacade } from '../state' -import { combineLatest, map } from 'rxjs' +import { BehaviorSubject, combineLatest, map } from 'rxjs' import { GN_UI_VERSION } from '../gn-ui-version.token' @Component({ @@ -11,35 +16,77 @@ import { GN_UI_VERSION } from '../gn-ui-version.token' changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataViewWebComponentComponent { + tabIndex$ = new BehaviorSubject(0) + @Input() + set tabIndex(value: number) { + this.tabIndex$.next(value) + } webComponentHtml$ = combineLatest( + this.tabIndex$, this.facade.chartConfig$, this.facade.metadata$ ).pipe( - map(([config, metadata]) => { - if (config) { - const { aggregation, xProperty, yProperty, chartType } = config + map(([tabIndex, config, metadata]) => { + if (tabIndex === 2) { + if (config) { + const { aggregation, xProperty, yProperty, chartType } = config + return ` + ` + } + return '' + } else if (tabIndex === 1) { return ` -` + } else { + return ` +` +>` } - return '' }) ) From 2ee4c7da3e9c97992018f7d88d9ef5926d6759b7 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 28 Jun 2024 14:39:54 +0200 Subject: [PATCH 026/378] feat: add new webcomponents to modules --- apps/webcomponents/src/app/webcomponents.module.ts | 5 ++++- apps/webcomponents/src/index.html | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/webcomponents/src/app/webcomponents.module.ts b/apps/webcomponents/src/app/webcomponents.module.ts index 37862f3fff..1515a39e71 100644 --- a/apps/webcomponents/src/app/webcomponents.module.ts +++ b/apps/webcomponents/src/app/webcomponents.module.ts @@ -36,6 +36,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { provideGn4 } from '@geonetwork-ui/api/repository' import { GnFigureDatasetsComponent } from './components/gn-figure-datasets/gn-figure-datasets.component' import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' +import { GnDatasetViewMapComponent } from './components/gn-dataset-view-map/gn-dataset-view-map.component' const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ [GnFacetsComponent, 'gn-facets'], @@ -46,6 +47,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ [GnDatasetViewChartComponent, 'gn-dataset-view-chart'], [GnMapViewerComponent, 'gn-map-viewer'], [GnFigureDatasetsComponent, 'gn-figure-datasets'], + [GnDatasetViewMapComponent, 'gn-dataset-view-map'], ] @NgModule({ @@ -61,6 +63,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ GnDatasetViewChartComponent, GnMapViewerComponent, GnFigureDatasetsComponent, + GnDatasetViewMapComponent, ], imports: [ CommonModule, @@ -100,7 +103,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - // bootstrap: [AppComponent], + bootstrap: [AppComponent], }) export class WebcomponentsModule { constructor(private injector: Injector) { diff --git a/apps/webcomponents/src/index.html b/apps/webcomponents/src/index.html index 5368d639da..11553c7c9a 100644 --- a/apps/webcomponents/src/index.html +++ b/apps/webcomponents/src/index.html @@ -81,6 +81,9 @@

    GeoNetwork demo

    >Preview dataset as chart +
  • + Preview dataset as map +
  • Map viewer
  • From 173163d8a09af70b3c2260b0564a2dadf5200c2d Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 28 Jun 2024 14:40:51 +0200 Subject: [PATCH 027/378] feat: make data view share be displayed all the time --- .../record/record-metadata/record-metadata.component.html | 2 +- .../record-metadata/record-metadata.component.spec.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html index efc77e05c4..ae74bdfc8a 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html @@ -110,7 +110,7 @@
    { fixture.debugElement.query(By.directive(MockDataViewComponent)) ).toBeFalsy() }) - it('does not render the permalink component', () => { + it('does render the permalink component', () => { expect( fixture.debugElement.query(By.directive(MockDataViewShareComponent)) - ).toBeFalsy() + ).toBeTruthy() }) }) describe('when a DATA link present', () => { @@ -446,10 +446,10 @@ describe('RecordMetadataComponent', () => { .length ).toEqual(2) }) - it('does not render the permalink component', () => { + it('does render the permalink component', () => { expect( fixture.debugElement.query(By.directive(MockDataViewShareComponent)) - ).toBeFalsy() + ).toBeTruthy() }) describe('when selectedTabIndex$ is 2 (chart tab)', () => { beforeEach(() => { From bb2314aa2ad68b56d1addef2c67ff42a7f4c6007 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 28 Jun 2024 14:41:04 +0200 Subject: [PATCH 028/378] feat: e2e testing --- apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 0e9813850e..92a928df64 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -1,4 +1,5 @@ import 'cypress-real-events' +import { tile } from 'ol/loadingstrategy' import path from 'path' beforeEach(() => { @@ -355,6 +356,9 @@ describe('dataset pages', () => { }) cy.screenshot({ capture: 'fullPage' }) }) + it('should display the sharing options', () => { + cy.get('gn-ui-data-view-permalink').should('be.visible') + }) }) describe('features', () => { it('MAP : should open a popup on layer click', () => { From 0225bdfee175a2b8266b551003b741b9703b98d4 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 28 Jun 2024 15:01:24 +0200 Subject: [PATCH 029/378] fix: fix e2e tests --- apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 92a928df64..f4860f419d 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -294,7 +294,7 @@ describe('dataset pages', () => { }) }) - describe('PREVIEW SECTION : display & functions', () => { + describe.only('PREVIEW SECTION : display & functions', () => { beforeEach(() => { cy.get('datahub-record-metadata') .find('[id="preview"]') @@ -357,7 +357,7 @@ describe('dataset pages', () => { cy.screenshot({ capture: 'fullPage' }) }) it('should display the sharing options', () => { - cy.get('gn-ui-data-view-permalink').should('be.visible') + cy.get('gn-ui-data-view-share').should('be.visible') }) }) describe('features', () => { From 7823fc1200cc1f6f9397a0d756b4d4297f067e93 Mon Sep 17 00:00:00 2001 From: Ronan Date: Mon, 1 Jul 2024 14:38:11 +0200 Subject: [PATCH 030/378] =?UTF-8?q?test(formapi):=20Am=C3=A9lioration=20de?= =?UTF-8?q?s=20tests=20pour=20qu'ils=20soient=20plus=20pr=C3=A9cis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/lib/ign-api-dl/ign-api-dl.component.spec.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts index 3f17fc6b86..16490bb897 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.spec.ts @@ -66,9 +66,8 @@ describe('IgnApiDlComponent', () => { it('should not be a correct format', async () => { component.bucketPromisesFormat = bucketFormat - component.setFormat(mockFormat) component.setFormat(mockBadFormat) - expect(component.format$.getValue()).toBe(mockFormat) + expect(component.format$.getValue()).toBe('') }) }) describe('When CRS changed', () => { @@ -89,9 +88,8 @@ describe('IgnApiDlComponent', () => { it('should not be a correct CRS', async () => { component.bucketPromisesCrs = bucketCRS - component.setCrs(mockCRS) component.setCrs(mockBadCRS) - expect(component.crs$.getValue()).toBe(mockCRS) + expect(component.crs$.getValue()).toBe('') }) }) describe('When Zone changed', () => { @@ -112,9 +110,8 @@ describe('IgnApiDlComponent', () => { it('should not be a correct Zone', async () => { component.bucketPromisesZone = bucketZone - component.setZone(mockZone) component.setZone(mockBadZone) - expect(component.zone$.getValue()).toBe(mockZone) + expect(component.zone$.getValue()).toBe('') }) }) @@ -128,9 +125,8 @@ describe('IgnApiDlComponent', () => { expect(component.resetPage).toHaveBeenCalled() }) it('should not be a correct edition date', () => { - component.setEditionDate(mockEditionDate) component.setEditionDate(mockBadEditionDate) - expect(component.editionDate$.getValue()).toBe(mockEditionDate) + expect(component.editionDate$.getValue()).toBe('') }) }) From 571b4e10351e4a350df93bf8a2d8bb1fca333f63 Mon Sep 17 00:00:00 2001 From: mmohad Date: Mon, 1 Jul 2024 14:44:23 +0200 Subject: [PATCH 031/378] format --- .../lib/ign-api-dl/ign-api-dl.component.html | 1 - .../ign-api-produit.component.html | 20 +++++++++------- .../ign-api-produit.component.ts | 23 +++++++++++-------- .../record/src/lib/state/mdview.facade.ts | 9 +++++++- .../src/lib/api-card/api-card.component.html | 7 +++--- .../src/lib/api-card/api-card.component.ts | 4 ++-- .../dropdown-selector.component.ts | 2 +- .../src/lib/links/link-classifier.service.ts | 2 +- package-lock.json | 2 +- package.json | 2 +- translations/en.json | 2 +- translations/fr.json | 2 +- 12 files changed, 45 insertions(+), 31 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html index 9828be4a75..c8a5607751 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html @@ -94,7 +94,6 @@
    - - - - + + + diff --git a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts index 5650cc2d8d..813939b6a4 100644 --- a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts +++ b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts @@ -26,21 +26,24 @@ export class IgnApiProduitComponent implements OnInit { ngOnInit(): void { this.liste$ = this.http.get(this.link['id']).pipe( - - map((response) => response['entry'], - // tap(el=> console.log(el)), + map( + (response) => response['entry'] + // tap(el=> console.log(el)), ) ) - } + } - downloadListe():void{ - this.http.get(this.link['id']).pipe( - map((response) => response['entry']), - mergeMap((response) => response) - ).subscribe(reponse=>this.download(reponse['id'])) + downloadListe(): void { + this.http + .get(this.link['id']) + .pipe( + map((response) => response['entry']), + mergeMap((response) => response) + ) + .subscribe((reponse) => this.download(reponse['id'])) } - download(url):void{ + download(url): void { console.log(url) this.http.get(url).subscribe() } diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 7aebf76282..69f483b804 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -61,7 +61,14 @@ export class MdViewFacade { apiLinks$ = this.allLinks$.pipe( map((links) => - links.filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)).sort((dd1, dd2) => {return (dd2 as DatasetServiceDistribution).accessServiceProtocol == 'GPFDL'? 1 : 0}) + links + .filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)) + .sort((dd1, dd2) => { + return (dd2 as DatasetServiceDistribution).accessServiceProtocol == + 'GPFDL' + ? 1 + : 0 + }) ) ) 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 80bdb7667f..70875f5e35 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 @@ -11,7 +11,7 @@
    record.metadata.api.gpfdl + record.metadata.api.gpfdl = + @Output() openRecordApiForm: EventEmitter = new EventEmitter() ngOnInit() { this.displayApiFormButton = this.link.accessServiceProtocol === 'ogcFeatures' || this.link.accessServiceProtocol === 'wfs' || - this.link.accessServiceProtocol === 'GPFDL' + this.link.accessServiceProtocol === 'GPFDL' } ngOnChanges(changes: SimpleChanges) { diff --git a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts index 3c48313b7b..127b67741a 100644 --- a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts +++ b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts @@ -94,7 +94,7 @@ export class DropdownSelectorComponent implements OnInit { if (!this.choices || this.choices.length === 0) { this.choices = [] } - console.log("un nv test",this.focusFirstItem()) + console.log('un nv test', this.focusFirstItem()) } isSelected(choice: DropdownChoice) { diff --git a/libs/util/shared/src/lib/links/link-classifier.service.ts b/libs/util/shared/src/lib/links/link-classifier.service.ts index 03b3348244..c33604fffb 100644 --- a/libs/util/shared/src/lib/links/link-classifier.service.ts +++ b/libs/util/shared/src/lib/links/link-classifier.service.ts @@ -25,7 +25,7 @@ export class LinkClassifierService { case 'wms': case 'wmts': return [LinkUsage.API, LinkUsage.MAP_API] - + case 'ogcFeatures': return [LinkUsage.API, LinkUsage.DOWNLOAD, LinkUsage.GEODATA] case 'GPFDL': diff --git a/package-lock.json b/package-lock.json index 3dce7dff4a..9a2021b77b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33431,4 +33431,4 @@ "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index e1365620f4..d278dabad6 100644 --- a/package.json +++ b/package.json @@ -183,4 +183,4 @@ "rxjs": "^7.4.0" } } -} \ No newline at end of file +} diff --git a/translations/en.json b/translations/en.json index f58ef254ff..90bbd05490 100644 --- a/translations/en.json +++ b/translations/en.json @@ -387,4 +387,4 @@ "wfs.unreachable.cors": "The service could not be reached because of CORS limitations", "wfs.unreachable.http": "The service returned an HTTP error", "wfs.unreachable.unknown": "The service could not be reached" -} \ No newline at end of file +} diff --git a/translations/fr.json b/translations/fr.json index 840df4b0cb..eef5cc930b 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -387,4 +387,4 @@ "wfs.unreachable.cors": "Le service n'est pas accessible en raison de limitations CORS", "wfs.unreachable.http": "Le service a retourné une erreur HTTP", "wfs.unreachable.unknown": "Le service n'est pas accessible" -} \ No newline at end of file +} From fd25b702a410850b371d6c79ef57f26979881bbf Mon Sep 17 00:00:00 2001 From: mmohad Date: Mon, 1 Jul 2024 14:47:46 +0200 Subject: [PATCH 032/378] format --- .../lib/ign-api-dl/ign-api-dl.component.html | 1 - .../ign-api-produit.component.html | 20 +++++++++------- .../ign-api-produit.component.ts | 23 +++++++++++-------- .../record/src/lib/state/mdview.facade.ts | 9 +++++++- .../src/lib/api-card/api-card.component.html | 7 +++--- .../src/lib/api-card/api-card.component.ts | 4 ++-- .../dropdown-selector.component.ts | 2 +- .../src/lib/links/link-classifier.service.ts | 2 +- package-lock.json | 2 +- package.json | 2 +- translations/en.json | 2 +- translations/fr.json | 2 +- 12 files changed, 45 insertions(+), 31 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html index 9828be4a75..c8a5607751 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html @@ -94,7 +94,6 @@
    - -
    - - + + + diff --git a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts index 5650cc2d8d..813939b6a4 100644 --- a/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts +++ b/libs/feature/record/src/lib/ign-api-produit/ign-api-produit.component.ts @@ -26,21 +26,24 @@ export class IgnApiProduitComponent implements OnInit { ngOnInit(): void { this.liste$ = this.http.get(this.link['id']).pipe( - - map((response) => response['entry'], - // tap(el=> console.log(el)), + map( + (response) => response['entry'] + // tap(el=> console.log(el)), ) ) - } + } - downloadListe():void{ - this.http.get(this.link['id']).pipe( - map((response) => response['entry']), - mergeMap((response) => response) - ).subscribe(reponse=>this.download(reponse['id'])) + downloadListe(): void { + this.http + .get(this.link['id']) + .pipe( + map((response) => response['entry']), + mergeMap((response) => response) + ) + .subscribe((reponse) => this.download(reponse['id'])) } - download(url):void{ + download(url): void { console.log(url) this.http.get(url).subscribe() } diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 7aebf76282..69f483b804 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -61,7 +61,14 @@ export class MdViewFacade { apiLinks$ = this.allLinks$.pipe( map((links) => - links.filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)).sort((dd1, dd2) => {return (dd2 as DatasetServiceDistribution).accessServiceProtocol == 'GPFDL'? 1 : 0}) + links + .filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API)) + .sort((dd1, dd2) => { + return (dd2 as DatasetServiceDistribution).accessServiceProtocol == + 'GPFDL' + ? 1 + : 0 + }) ) ) 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 80bdb7667f..70875f5e35 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 @@ -11,7 +11,7 @@
    record.metadata.api.gpfdl + record.metadata.api.gpfdl = + @Output() openRecordApiForm: EventEmitter = new EventEmitter() ngOnInit() { this.displayApiFormButton = this.link.accessServiceProtocol === 'ogcFeatures' || this.link.accessServiceProtocol === 'wfs' || - this.link.accessServiceProtocol === 'GPFDL' + this.link.accessServiceProtocol === 'GPFDL' } ngOnChanges(changes: SimpleChanges) { diff --git a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts index 3c48313b7b..127b67741a 100644 --- a/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts +++ b/libs/ui/inputs/src/lib/dropdown-selector/dropdown-selector.component.ts @@ -94,7 +94,7 @@ export class DropdownSelectorComponent implements OnInit { if (!this.choices || this.choices.length === 0) { this.choices = [] } - console.log("un nv test",this.focusFirstItem()) + console.log('un nv test', this.focusFirstItem()) } isSelected(choice: DropdownChoice) { diff --git a/libs/util/shared/src/lib/links/link-classifier.service.ts b/libs/util/shared/src/lib/links/link-classifier.service.ts index 03b3348244..c33604fffb 100644 --- a/libs/util/shared/src/lib/links/link-classifier.service.ts +++ b/libs/util/shared/src/lib/links/link-classifier.service.ts @@ -25,7 +25,7 @@ export class LinkClassifierService { case 'wms': case 'wmts': return [LinkUsage.API, LinkUsage.MAP_API] - + case 'ogcFeatures': return [LinkUsage.API, LinkUsage.DOWNLOAD, LinkUsage.GEODATA] case 'GPFDL': diff --git a/package-lock.json b/package-lock.json index 3dce7dff4a..9a2021b77b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33431,4 +33431,4 @@ "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index e1365620f4..d278dabad6 100644 --- a/package.json +++ b/package.json @@ -183,4 +183,4 @@ "rxjs": "^7.4.0" } } -} \ No newline at end of file +} diff --git a/translations/en.json b/translations/en.json index f58ef254ff..90bbd05490 100644 --- a/translations/en.json +++ b/translations/en.json @@ -387,4 +387,4 @@ "wfs.unreachable.cors": "The service could not be reached because of CORS limitations", "wfs.unreachable.http": "The service returned an HTTP error", "wfs.unreachable.unknown": "The service could not be reached" -} \ No newline at end of file +} diff --git a/translations/fr.json b/translations/fr.json index 840df4b0cb..eef5cc930b 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -387,4 +387,4 @@ "wfs.unreachable.cors": "Le service n'est pas accessible en raison de limitations CORS", "wfs.unreachable.http": "Le service a retourné une erreur HTTP", "wfs.unreachable.unknown": "Le service n'est pas accessible" -} \ No newline at end of file +} From 498ed25d9220d1f4626e168fca006b73ca5f5fca Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Tue, 2 Jul 2024 09:51:46 +0200 Subject: [PATCH 033/378] feat: make changes from review --- apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts | 2 +- .../record-metadata.component.html | 2 +- .../record-metadata/record-metadata.component.ts | 15 +++++++++++++-- .../data-view-permalink.component.ts | 14 +++++++------- .../data-view-share.component.html | 4 ++-- .../data-view-share/data-view-share.component.ts | 11 ++++++++++- .../data-view-web-component.component.ts | 14 +++++++------- 7 files changed, 41 insertions(+), 21 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index f4860f419d..4db1f53251 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -294,7 +294,7 @@ describe('dataset pages', () => { }) }) - describe.only('PREVIEW SECTION : display & functions', () => { + describe('PREVIEW SECTION : display & functions', () => { beforeEach(() => { cy.get('datahub-record-metadata') .find('[id="preview"]') diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html index ae74bdfc8a..8cd37d1680 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html @@ -110,7 +110,7 @@
    { @@ -108,7 +108,18 @@ export class RecordMetadataComponent { ) {} onTabIndexChange(index: number): void { - this.selectedTabIndex$.next(index) + let view + switch (index) { + case 0: + view = 'map' + break + case 1: + view = 'table' + break + default: + view = 'chart' + } + this.selectedView$.next(view) setTimeout(() => { window.dispatchEvent(new Event('resize')) }, 0) 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 f430cc8de7..ef55091071 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 @@ -22,21 +22,21 @@ export const WEB_COMPONENT_EMBEDDER_URL = new InjectionToken( changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataViewPermalinkComponent { - tabIndex$ = new BehaviorSubject(0) + viewType$ = new BehaviorSubject('map') @Input() - set tabIndex(value: number) { - this.tabIndex$.next(value) + set viewType(value: string) { + this.viewType$.next(value) } permalinkUrl$ = combineLatest([ - this.tabIndex$, + this.viewType$, this.facade.chartConfig$, this.facade.metadata$, ]).pipe( - map(([tabIndex, config, metadata]) => { + map(([viewType, config, metadata]) => { const url = new URL(`${this.wcEmbedderBaseUrl}`, window.location.origin) url.searchParams.set('v', `${this.version}`) - if (tabIndex === 2) { + if (viewType === 'chart') { if (config) { const { aggregation, xProperty, yProperty, chartType } = config url.searchParams.append('e', `gn-dataset-view-chart`) @@ -47,7 +47,7 @@ export class DataViewPermalinkComponent { } else { return '' } - } else if (tabIndex === 1) { + } else if (viewType === 'table') { // table url.searchParams.append('e', `gn-dataset-view-table`) } else { diff --git a/libs/feature/record/src/lib/data-view-share/data-view-share.component.html b/libs/feature/record/src/lib/data-view-share/data-view-share.component.html index f9d5a39fe3..8fd544693c 100644 --- a/libs/feature/record/src/lib/data-view-share/data-view-share.component.html +++ b/libs/feature/record/src/lib/data-view-share/data-view-share.component.html @@ -11,7 +11,7 @@ share.tab.permalink @@ -27,7 +27,7 @@ > 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 7be9f4527c..95085e9e0b 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 @@ -14,7 +14,16 @@ import { WEB_COMPONENT_EMBEDDER_URL } from '../data-view-permalink/data-view-per changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataViewShareComponent { - @Input() tabIndex: number + private _viewType: string + + @Input() + set viewType(value: string) { + this._viewType = value + } + + get viewType(): string { + return this._viewType + } constructor( @Optional() @Inject(WEB_COMPONENT_EMBEDDER_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 a2164d2562..98a9baa7b5 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 @@ -16,18 +16,18 @@ import { GN_UI_VERSION } from '../gn-ui-version.token' changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataViewWebComponentComponent { - tabIndex$ = new BehaviorSubject(0) + viewType$ = new BehaviorSubject('map') @Input() - set tabIndex(value: number) { - this.tabIndex$.next(value) + set viewType(value: string) { + this.viewType$.next(value) } webComponentHtml$ = combineLatest( - this.tabIndex$, + this.viewType$, this.facade.chartConfig$, this.facade.metadata$ ).pipe( - map(([tabIndex, config, metadata]) => { - if (tabIndex === 2) { + map(([viewType, config, metadata]) => { + if (viewType === 'chart') { if (config) { const { aggregation, xProperty, yProperty, chartType } = config return ` From 51db788f03e3ee4fe59a6ac82481ca57f4e91412 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Tue, 2 Jul 2024 10:01:03 +0200 Subject: [PATCH 034/378] fix: fix unit tests --- .../record-metadata/record-metadata.component.spec.ts | 4 ++-- .../data-view-permalink.component.spec.ts | 6 +++--- .../data-view-web-component.component.spec.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) 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 589d2a4aa4..0b23b693b5 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 @@ -451,9 +451,9 @@ describe('RecordMetadataComponent', () => { fixture.debugElement.query(By.directive(MockDataViewShareComponent)) ).toBeTruthy() }) - describe('when selectedTabIndex$ is 2 (chart tab)', () => { + describe('when selectedView$ is chart', () => { beforeEach(() => { - component.selectedTabIndex$.next(2) + component.selectedView$.next('chart') fixture.detectChanges() }) it('renders the permalink component', () => { 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 f93ecb8541..848ee4bac4 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 @@ -75,7 +75,7 @@ describe('DataViewPermalinkComponent', () => { facade = TestBed.inject(MdViewFacade) fixture = TestBed.createComponent(DataViewPermalinkComponent) component = fixture.componentInstance - component.tabIndex$.next(2) + component.viewType$.next('chart') fixture.detectChanges() }) @@ -126,7 +126,7 @@ describe('DataViewPermalinkComponent', () => { }) describe('Map view', () => { beforeEach(() => { - component.tabIndex$.next(0) + component.viewType$.next('map') }) describe('init permalinkUrl$', () => { it('should generate URL based on configs', async () => { @@ -143,7 +143,7 @@ describe('DataViewPermalinkComponent', () => { }) describe('Table view', () => { beforeEach(() => { - component.tabIndex$.next(1) + component.viewType$.next('table') }) describe('init permalinkUrl$', () => { it('should generate URL based on configs', async () => { 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 38221b2293..7521751966 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 @@ -69,7 +69,7 @@ describe('DataViewWebComponentComponent', () => { facade = TestBed.inject(MdViewFacade) fixture = TestBed.createComponent(DataViewWebComponentComponent) component = fixture.componentInstance - component.tabIndex$.next(2) + component.viewType$.next('chart') fixture.detectChanges() }) @@ -128,7 +128,7 @@ describe('DataViewWebComponentComponent', () => { }) describe('Map view', () => { beforeEach(() => { - component.tabIndex$.next(0) + component.viewType$.next('map') }) describe('init webComponentHtml$', () => { it('should generate HTML based on configs', async () => { @@ -151,7 +151,7 @@ describe('DataViewWebComponentComponent', () => { }) describe('Table view', () => { beforeEach(() => { - component.tabIndex$.next(1) + component.viewType$.next('table') }) describe('init webComponentHtml$', () => { it('should generate HTML based on configs', async () => { From 44d762f79ded3323bc8fdeddaf6b70efff75e99b Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Thu, 27 Jun 2024 09:52:29 +0200 Subject: [PATCH 035/378] feat(UI-inputs): Create switch toggle component --- .../switch-toggle/switch-toggle.component.css | 0 .../switch-toggle.component.html | 15 ++++++++ .../switch-toggle.component.spec.ts | 21 +++++++++++ .../switch-toggle/switch-toggle.component.ts | 37 +++++++++++++++++++ .../switch-toggle/switch-toggle.stories.ts | 28 ++++++++++++++ libs/ui/inputs/src/lib/ui-inputs.module.ts | 4 ++ 6 files changed, 105 insertions(+) create mode 100644 libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.css create mode 100644 libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.html create mode 100644 libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts create mode 100644 libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts create mode 100644 libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.css b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.html b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.html new file mode 100644 index 0000000000..7d8b0a343e --- /dev/null +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.html @@ -0,0 +1,15 @@ + + {{ option.label }} + diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts new file mode 100644 index 0000000000..0b21360790 --- /dev/null +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SwitchToggleComponent } from './switch-toggle.component'; + +describe('SwitchToggleComponent', () => { + let component: SwitchToggleComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [SwitchToggleComponent] + }); + fixture = TestBed.createComponent(SwitchToggleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts new file mode 100644 index 0000000000..81dc52fb09 --- /dev/null +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts @@ -0,0 +1,37 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' + +export type SwitchToggleOption = { + label: string + value: string + checked: boolean +} + +@Component({ + selector: 'gn-ui-switch-toggle', + templateUrl: './switch-toggle.component.html', + styleUrls: ['./switch-toggle.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SwitchToggleComponent { + @Input() options: SwitchToggleOption[] + @Input() ariaLabel? = '' + @Input() extraClasses? = '' + @Output() selectedValue = new EventEmitter() + + onChange(selectedOption: SwitchToggleOption) { + this.options.forEach((option) => { + if (option.value === selectedOption.value) { + option.checked = true + } else { + option.checked = false + } + }) + this.selectedValue.emit(selectedOption) + } +} diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts new file mode 100644 index 0000000000..fddcdbb12b --- /dev/null +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts @@ -0,0 +1,28 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { SwitchToggleComponent } from './switch-toggle.component' +import { MatButtonToggleModule } from '@angular/material/button-toggle' + +export default { + title: 'Inputs/SwitchToggle', + component: SwitchToggleComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [MatButtonToggleModule], + }), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + options: [ + { label: 'city', value: 'city', checked: false }, + { label: 'municipality', value: 'municipality', checked: false }, + { label: 'state', value: 'state', checked: false }, + ], + extraClasses: 'grow', + }, + render: (args) => ({ + props: { ...args }, + }), +} diff --git a/libs/ui/inputs/src/lib/ui-inputs.module.ts b/libs/ui/inputs/src/lib/ui-inputs.module.ts index 8f2e7b92bd..7f224e91b5 100644 --- a/libs/ui/inputs/src/lib/ui-inputs.module.ts +++ b/libs/ui/inputs/src/lib/ui-inputs.module.ts @@ -33,6 +33,8 @@ import { MatDatepickerModule } from '@angular/material/datepicker' import { MatNativeDateModule } from '@angular/material/core' import { EditableLabelDirective } from './editable-label/editable-label.directive' import { ImageInputComponent } from './image-input/image-input.component' +import { SwitchToggleComponent } from './switch-toggle/switch-toggle.component' +import { MatButtonToggleModule } from '@angular/material/button-toggle' @NgModule({ declarations: [ @@ -46,6 +48,7 @@ import { ImageInputComponent } from './image-input/image-input.component' CopyTextButtonComponent, CheckboxComponent, SearchInputComponent, + SwitchToggleComponent, ], imports: [ CommonModule, @@ -73,6 +76,7 @@ import { ImageInputComponent } from './image-input/image-input.component' DateRangePickerComponent, CheckToggleComponent, BadgeComponent, + MatButtonToggleModule, ], exports: [ DropdownSelectorComponent, From 729eca6e76670d7ec096729eb7624b37ba985001 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Tue, 2 Jul 2024 09:17:52 +0200 Subject: [PATCH 036/378] chore(i18n): update translations --- libs/feature/editor/src/lib/fields.config.ts | 8 +-- translations/de.json | 75 +++++++++++--------- translations/en.json | 12 ++-- translations/es.json | 13 +++- translations/fr.json | 16 +++-- translations/it.json | 13 +++- translations/nl.json | 13 +++- translations/pt.json | 13 +++- translations/sk.json | 13 +++- 9 files changed, 113 insertions(+), 63 deletions(-) diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index cda10b5196..6591a3090c 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -5,21 +5,21 @@ export const DEFAULT_FIELDS: EditorFieldsConfig = [ { model: 'title', formFieldConfig: { - labelKey: 'Metadata title', + labelKey: marker('editor.record.form.metadata.title'), type: 'text', }, }, { model: 'abstract', formFieldConfig: { - labelKey: 'Abstract', + labelKey: marker('editor.record.form.abstract'), type: 'rich', }, }, { model: 'uniqueIdentifier', formFieldConfig: { - labelKey: 'Unique identifier', + labelKey: marker('editor.record.form.unique.identifier'), type: 'text', locked: true, }, @@ -27,7 +27,7 @@ export const DEFAULT_FIELDS: EditorFieldsConfig = [ { model: 'recordUpdated', formFieldConfig: { - labelKey: 'Record Updated', + labelKey: marker('editor.record.form.record.updated'), type: 'text', locked: true, }, diff --git a/translations/de.json b/translations/de.json index 2db3fe2365..9f4d68aa20 100644 --- a/translations/de.json +++ b/translations/de.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{Datensätze} one{Datensatz} other{Datensätze}}", - "catalog.figures.organizations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", + "catalog.figures.organisations": "", "chart.aggregation.average": "Durchschnitt", "chart.aggregation.count": "Anzahl", "chart.aggregation.max": "Maximum", @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "OK, meine Daten sind korrekt", "datafeeder.datasetValidation.title": "Stellen Sie sicher, dass Ihre Daten korrekt sind", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "Wie würden Sie Ihren Datensatz beschreiben?", "datafeeder.form.datepicker": "Wissen Sie, wann der Datensatz erstellt wurde?", "datafeeder.form.description": "Beschreiben Sie abschließend den Prozess, der zur Erstellung des Datensatzes verwendet wurde", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "Metadatensatz", "datafeeder.publishSuccess.illustration.title": "Erledigt, alles ist gut!", "datafeeder.publishSuccess.mapViewer": "Kartenviewer", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "Zeigen Sie Ihre Daten an in:", "datafeeder.publishSuccess.title": "Herzlichen Glückwunsch! \n Ihr Datensatz wurde veröffentlicht", "datafeeder.publishSuccess.uploadAnotherData": "Ein weiteren Datensatz hochladen", @@ -104,7 +107,6 @@ "datafeeder.upload.maxFileSize": "Maximale Dateigröße beträgt {size} MB", "datafeeder.upload.title": "Laden Sie Ihren Datensatz hoch", "datafeeder.upload.uploadButton": "Hochladen", - "datafeeder.validation.encoding": "Codierung", "datafeeder.validation.csv.delimiter": "", "datafeeder.validation.csv.delimiter.comma": "", "datafeeder.validation.csv.delimiter.semicolon": "", @@ -114,6 +116,7 @@ "datafeeder.validation.csv.quote.none": "", "datafeeder.validation.csv.quote.simple": "", "datafeeder.validation.csv.quoteChar": "", + "datafeeder.validation.encoding": "Codierung", "datafeeder.validation.extent.title": "Hier ist der Datensatzumfang", "datafeeder.validation.extent.title.unknown": "Das Projektionssystem ist unbekannt", "datafeeder.validation.projection": "Raumbezugssystem:", @@ -162,38 +165,42 @@ "downloads.format.unknown": "unbekannt", "downloads.wfs.featuretype.not.found": "Der Layer wurde nicht gefunden", "dropFile": "Datei ablegen", - "editor.record.form.keywords": "Schlagwörter", + "editor.record.form.abstract": "Kurzbeschreibung", + "editor.record.form.keywords": "Schlüsselwörter", "editor.record.form.license": "Lizenz", - "editor.record.form.license.cc-by": "", - "editor.record.form.license.cc-by-sa": "", - "editor.record.form.license.cc-zero": "", - "editor.record.form.license.etalab": "", - "editor.record.form.license.etalab-v2": "", - "editor.record.form.license.odbl": "", - "editor.record.form.license.odc-by": "", - "editor.record.form.license.pddl": "", + "editor.record.form.license.cc-by": "Creative Commons CC-BY", + "editor.record.form.license.cc-by-sa": "Creative Commons CC-BY-SA", + "editor.record.form.license.cc-zero": "Creative Commons CC-0", + "editor.record.form.license.etalab": "Offene Lizenz (Etalab)", + "editor.record.form.license.etalab-v2": "Offene Lizenz v2.0 (Etalab)", + "editor.record.form.license.odbl": "Open Data Commons ODbL", + "editor.record.form.license.odc-by": "Open Data Commons ODC-By", + "editor.record.form.license.pddl": "Open Data Commons PDDL", "editor.record.form.license.unknown": "Unbekannt oder nicht vorhanden", - "editor.record.form.resourceUpdated": "", - "editor.record.form.temporalExtents": "", - "editor.record.form.temporalExtents.addDate": "", - "editor.record.form.temporalExtents.addRange": "", - "editor.record.form.temporalExtents.date": "", - "editor.record.form.temporalExtents.range": "", - "editor.record.form.updateFrequency": "", - "editor.record.form.updateFrequency.planned": "", - "editor.record.loadError.body": "", - "editor.record.loadError.closeMessage": "", - "editor.record.loadError.title": "", - "editor.record.publish": "", - "editor.record.publishError.body": "", - "editor.record.publishError.closeMessage": "", - "editor.record.publishError.title": "", - "editor.record.publishSuccess.body": "", - "editor.record.publishSuccess.title": "", - "editor.record.saveStatus.asDraftOnly": "", - "editor.record.saveStatus.draftWithChangesPending": "", - "editor.record.saveStatus.recordUpToDate": "", - "editor.record.upToDate": "", + "editor.record.form.metadata.title": "Metadaten-Titel", + "editor.record.form.record.updated": "Datensatz zuletzt aktualisiert", + "editor.record.form.resourceUpdated": "Letztes Aktualisierungsdatum", + "editor.record.form.temporalExtents": "Zeitlicher Umfang", + "editor.record.form.temporalExtents.addDate": "Zeitpunkt", + "editor.record.form.temporalExtents.addRange": "Zeitraum", + "editor.record.form.temporalExtents.date": "Datum", + "editor.record.form.temporalExtents.range": "Datumsbereich", + "editor.record.form.unique.identifier": "Eindeutige Kennung (ID)", + "editor.record.form.updateFrequency": "Aktualisierungshäufigkeit", + "editor.record.form.updateFrequency.planned": "Die Daten sollten regelmäßig aktualisiert werden.", + "editor.record.loadError.body": "Der Datensatz konnte nicht geladen werden:", + "editor.record.loadError.closeMessage": "Verstanden", + "editor.record.loadError.title": "Fehler beim Laden des Datensatzes", + "editor.record.publish": "Diesen Datensatz veröffentlichen", + "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", + "editor.record.publishSuccess.body": "Der Datensatz wurde erfolgreich veröffentlicht!", + "editor.record.publishSuccess.title": "Veröffentlichung erfolgreich", + "editor.record.saveStatus.asDraftOnly": "Nur als Entwurf gespeichert - noch nicht veröffentlicht", + "editor.record.saveStatus.draftWithChangesPending": "Als Entwurf gespeichert - Änderungen stehen aus", + "editor.record.saveStatus.recordUpToDate": "Datensatz ist auf dem neuesten Stand", + "editor.record.upToDate": "Dieser Datensatz ist auf dem neuesten Stand", "externalviewer.dataset.unnamed": "Datensatz aus dem Datahub", "facets.block.title.OrgForResource": "Organisation", "facets.block.title.availableInServices": "Verfügbar für", @@ -262,10 +269,10 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Veröffentlichungen 0 → 9", "organisations.sortBy.recordCountDesc": "Veröffentlichungen 9 → 0", - "organization.details.lastPublishedDatasets": "", - "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Nächste Seite", "pagination.page": "Seite", "pagination.pageOf": "von", diff --git a/translations/en.json b/translations/en.json index 041390c69f..e02b5eb542 100644 --- a/translations/en.json +++ b/translations/en.json @@ -46,11 +46,11 @@ "datafeeder.analysisProgressBar.subtitle": "The analysis may take several minutes, please wait.", "datafeeder.analysisProgressBar.title": "Analyze in progress", "datafeeder.datasetValidation.datasetInformation": "The provided dataset contains {number} entities", - "datafeeder.datasetValidationCsv.lineNumbers": "Sample of the first 5 lines* of the dataset:", - "datafeeder.datasetValidationCsv.explicitLineNumbers": "*The table must display the first 5 lines (excluding the header)
    If this is not the case, check that the file is correctly formatted", "datafeeder.datasetValidation.submitButton": "OK, my data are correct", "datafeeder.datasetValidation.title": "Make sure your data are correct", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "*The table must display the first 5 lines (excluding the header)
    If this is not the case, check that the file is correctly formatted", + "datafeeder.datasetValidationCsv.lineNumbers": "Sample of the first 5 lines* of the dataset:", "datafeeder.form.abstract": "How would you describe your dataset?", "datafeeder.form.datepicker": "Do you know when the dataset was created?", "datafeeder.form.description": "Finally, please describe the process that was used to create the dataset", @@ -165,6 +165,7 @@ "downloads.format.unknown": "unknown", "downloads.wfs.featuretype.not.found": "The layer was not found", "dropFile": "drop file", + "editor.record.form.abstract": "Abstract", "editor.record.form.keywords": "Keywords", "editor.record.form.license": "License", "editor.record.form.license.cc-by": "Creative Commons CC-BY", @@ -176,12 +177,15 @@ "editor.record.form.license.odc-by": "Open Data Commons ODC-By", "editor.record.form.license.pddl": "Open Data Commons PDDL", "editor.record.form.license.unknown": "Unknown or absent", + "editor.record.form.metadata.title": "Metadata title", + "editor.record.form.record.updated": "Record updated", "editor.record.form.resourceUpdated": "Last update date", "editor.record.form.temporalExtents": "Temporal extent", "editor.record.form.temporalExtents.addDate": "Time instant", "editor.record.form.temporalExtents.addRange": "Time period", "editor.record.form.temporalExtents.date": "Date", "editor.record.form.temporalExtents.range": "Date range", + "editor.record.form.unique.identifier": "Unique identifier", "editor.record.form.updateFrequency": "Update frequency", "editor.record.form.updateFrequency.planned": "The data should be updated regularly.", "editor.record.loadError.body": "The record could not be loaded:", @@ -265,10 +269,10 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Publications 0 → 9", "organisations.sortBy.recordCountDesc": "Publications 9 → 0", - "organization.details.lastPublishedDatasets": "Last published datasets", - "organization.details.lastPublishedDatasets.searchAllButton": "Search all", "organization.details.mailContact": "Contact by email", "organization.header.recordCount": "{count, plural, =0{data} one{data} other{datas}}", + "organization.lastPublishedDatasets": "Last published datasets", + "organization.lastPublishedDatasets.searchAllButton": "Search all", "pagination.nextPage": "Next page", "pagination.page": "page", "pagination.pageOf": "of", diff --git a/translations/es.json b/translations/es.json index c4b7d6ef74..cbd02da733 100644 --- a/translations/es.json +++ b/translations/es.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de datos", - "catalog.figures.organizations": "organizaciones", + "catalog.figures.organisations": "", "chart.aggregation.average": "promedio", "chart.aggregation.count": "conteo", "chart.aggregation.max": "máximo", @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "", "datafeeder.datasetValidation.title": "", "datafeeder.datasetValidation.unknown": "", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "", "datafeeder.form.datepicker": "", "datafeeder.form.description": "", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "", "datafeeder.publishSuccess.illustration.title": "", "datafeeder.publishSuccess.mapViewer": "", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "", "datafeeder.publishSuccess.title": "", "datafeeder.publishSuccess.uploadAnotherData": "", @@ -162,6 +165,7 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "", "editor.record.form.license.cc-by": "", @@ -173,12 +177,15 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", + "editor.record.form.metadata.title": "", + "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", "editor.record.form.temporalExtents": "", "editor.record.form.temporalExtents.addDate": "", "editor.record.form.temporalExtents.addRange": "", "editor.record.form.temporalExtents.date": "", "editor.record.form.temporalExtents.range": "", + "editor.record.form.unique.identifier": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -262,10 +269,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", - "organization.details.lastPublishedDatasets": "", - "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/fr.json b/translations/fr.json index 9e15942f5c..e71b65ed72 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "Se connecter", "catalog.figures.datasets": "{count, plural, =0{données} one{donnée} other{données}}", - "catalog.figures.organizations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", + "catalog.figures.organisations": "", "chart.aggregation.average": "moyenne", "chart.aggregation.count": "nombre", "chart.aggregation.max": "maximum", @@ -46,11 +46,11 @@ "datafeeder.analysisProgressBar.subtitle": "L'analyse peut prendre plusieurs minutes, merci d'attendre.", "datafeeder.analysisProgressBar.title": "Analyse en cours", "datafeeder.datasetValidation.datasetInformation": "Le jeu de données fourni contient {number} entités", - "datafeeder.datasetValidationCsv.lineNumbers": "Résumé des 5 premières lignes* du CSV :", - "datafeeder.datasetValidationCsv.explicitLineNumbers": "*Le tableau doit afficher les 5 premières lignes (hors en-tête)
    Si ce n'est pas le cas, vérifier que le fichier est bien formatté", "datafeeder.datasetValidation.submitButton": "OK, mes données sont correctes", "datafeeder.datasetValidation.title": "Vérifiez que vos données sont correctes", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "*Le tableau doit afficher les 5 premières lignes (hors en-tête)
    Si ce n'est pas le cas, vérifier que le fichier est bien formatté", + "datafeeder.datasetValidationCsv.lineNumbers": "Résumé des 5 premières lignes* du CSV :", "datafeeder.form.abstract": "Comment décrire votre jeu de données ?", "datafeeder.form.datepicker": "Savez-vous quand la donnée a été créée ?", "datafeeder.form.description": "Enfin, décrivez le processus utilisé pour créer la donnée", @@ -165,7 +165,8 @@ "downloads.format.unknown": "inconnu", "downloads.wfs.featuretype.not.found": "La couche n'a pas été retrouvée", "dropFile": "Faites glisser votre fichier", - "editor.record.form.keywords": "Mots-clés", + "editor.record.form.abstract": "", + "editor.record.form.keywords": "", "editor.record.form.license": "Licence", "editor.record.form.license.cc-by": "", "editor.record.form.license.cc-by-sa": "", @@ -176,12 +177,15 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Non-reconnue ou absente", + "editor.record.form.metadata.title": "", + "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "Date de dernière révision", "editor.record.form.temporalExtents": "Étendue temporelle", "editor.record.form.temporalExtents.addDate": "Date déterminée", "editor.record.form.temporalExtents.addRange": "Période de temps", "editor.record.form.temporalExtents.date": "Date concernée", "editor.record.form.temporalExtents.range": "Période concernée", + "editor.record.form.unique.identifier": "", "editor.record.form.updateFrequency": "Fréquence de mise à jour", "editor.record.form.updateFrequency.planned": "Ces données doivent être mise à jour régulièrement.", "editor.record.loadError.body": "", @@ -265,10 +269,10 @@ "organisations.sortBy.nameDesc": "Nom Z → A", "organisations.sortBy.recordCountAsc": "Données 0 → 9", "organisations.sortBy.recordCountDesc": "Données 9 → 0", - "organization.details.lastPublishedDatasets": "Dernières données publiées", - "organization.details.lastPublishedDatasets.searchAllButton": "Rechercher tous", "organization.details.mailContact": "Contacter par mail", "organization.header.recordCount": "{count, plural, =0{donnée} one{donnée} other{données}}", + "organization.lastPublishedDatasets": "Dernières données publiées", + "organization.lastPublishedDatasets.searchAllButton": "Rechercher tous", "pagination.nextPage": "Page suivante", "pagination.page": "page", "pagination.pageOf": "sur", diff --git a/translations/it.json b/translations/it.json index 02621f0449..6403acfbd0 100644 --- a/translations/it.json +++ b/translations/it.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organizations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", + "catalog.figures.organisations": "", "chart.aggregation.average": "media", "chart.aggregation.count": "conteggio", "chart.aggregation.max": "massimo", @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "OK, i miei dati sono corretti", "datafeeder.datasetValidation.title": "Controllare che i dati siano corretti", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "Come descrivere il suo dataset?", "datafeeder.form.datepicker": "Sa quando è stato creato il suo dataset ?", "datafeeder.form.description": "Infine, descrivere il processo utilizzato per creare il dataset", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "Scheda di metadati", "datafeeder.publishSuccess.illustration.title": "Completato, tutto è andato bene!", "datafeeder.publishSuccess.mapViewer": "Visualizzatore", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "Visualizzare i dati:", "datafeeder.publishSuccess.title": "Congratulazioni! \n I suoi dati sono stati pubblicati", "datafeeder.publishSuccess.uploadAnotherData": "Caricare un altro dato", @@ -162,6 +165,7 @@ "downloads.format.unknown": "sconosciuto", "downloads.wfs.featuretype.not.found": "Il layer non è stato trovato", "dropFile": "Trascina il suo file", + "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "Licenza", "editor.record.form.license.cc-by": "", @@ -173,12 +177,15 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Non riconosciuta o assente", + "editor.record.form.metadata.title": "", + "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", "editor.record.form.temporalExtents": "", "editor.record.form.temporalExtents.addDate": "", "editor.record.form.temporalExtents.addRange": "", "editor.record.form.temporalExtents.date": "", "editor.record.form.temporalExtents.range": "", + "editor.record.form.unique.identifier": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -262,10 +269,10 @@ "organisations.sortBy.nameDesc": "Nome Z → A", "organisations.sortBy.recordCountAsc": "Dati 0 → 9", "organisations.sortBy.recordCountDesc": "Dati 9 → 0", - "organization.details.lastPublishedDatasets": "", - "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Pagina successiva", "pagination.page": "pagina", "pagination.pageOf": "di", diff --git a/translations/nl.json b/translations/nl.json index 6e489176c7..a1b8b59b86 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "datasets", - "catalog.figures.organizations": "organisaties", + "catalog.figures.organisations": "", "chart.aggregation.average": "gemiddelde", "chart.aggregation.count": "aantal", "chart.aggregation.max": "max", @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "", "datafeeder.datasetValidation.title": "", "datafeeder.datasetValidation.unknown": "", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "", "datafeeder.form.datepicker": "", "datafeeder.form.description": "", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "", "datafeeder.publishSuccess.illustration.title": "", "datafeeder.publishSuccess.mapViewer": "", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "", "datafeeder.publishSuccess.title": "", "datafeeder.publishSuccess.uploadAnotherData": "", @@ -162,6 +165,7 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "", "editor.record.form.license.cc-by": "", @@ -173,12 +177,15 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", + "editor.record.form.metadata.title": "", + "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", "editor.record.form.temporalExtents": "", "editor.record.form.temporalExtents.addDate": "", "editor.record.form.temporalExtents.addRange": "", "editor.record.form.temporalExtents.date": "", "editor.record.form.temporalExtents.range": "", + "editor.record.form.unique.identifier": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -262,10 +269,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", - "organization.details.lastPublishedDatasets": "", - "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/pt.json b/translations/pt.json index 92ab9940c4..3ffac6989d 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de dados", - "catalog.figures.organizations": "organizações", + "catalog.figures.organisations": "", "chart.aggregation.average": "média", "chart.aggregation.count": "contagem", "chart.aggregation.max": "máximo", @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "", "datafeeder.datasetValidation.title": "", "datafeeder.datasetValidation.unknown": "", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "", "datafeeder.form.datepicker": "", "datafeeder.form.description": "", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "", "datafeeder.publishSuccess.illustration.title": "", "datafeeder.publishSuccess.mapViewer": "", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "", "datafeeder.publishSuccess.title": "", "datafeeder.publishSuccess.uploadAnotherData": "", @@ -162,6 +165,7 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "", "editor.record.form.license.cc-by": "", @@ -173,12 +177,15 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", + "editor.record.form.metadata.title": "", + "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", "editor.record.form.temporalExtents": "", "editor.record.form.temporalExtents.addDate": "", "editor.record.form.temporalExtents.addRange": "", "editor.record.form.temporalExtents.date": "", "editor.record.form.temporalExtents.range": "", + "editor.record.form.unique.identifier": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -262,10 +269,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", - "organization.details.lastPublishedDatasets": "", - "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/sk.json b/translations/sk.json index 41fb225ad6..9c792569b7 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasety} one{dataset} other{datasety}}", - "catalog.figures.organizations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", + "catalog.figures.organisations": "", "chart.aggregation.average": "priemer", "chart.aggregation.count": "počet", "chart.aggregation.max": "maximum", @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "OK, moje dáta sú správne", "datafeeder.datasetValidation.title": "Uistite sa, že dáta sú správne", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "Ako by ste opísali Váš dataset?", "datafeeder.form.datepicker": "Viete, kedy bol dataset vytvorený?", "datafeeder.form.description": "Nakoniec popíšte proces, ktorý bol použitý na vytvorenie datasetu", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "Metadátový záznam", "datafeeder.publishSuccess.illustration.title": "Hotovo, všetko je v poriadku!", "datafeeder.publishSuccess.mapViewer": "Prehliadač máp", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "Pozrite si svoje dáta v:", "datafeeder.publishSuccess.title": "Gratulujeme! Váš dataset bol publikovaný", "datafeeder.publishSuccess.uploadAnotherData": "Nahrajte ďalší dataset", @@ -162,6 +165,7 @@ "downloads.format.unknown": "neznámy", "downloads.wfs.featuretype.not.found": "Vrstva nebola nájdená", "dropFile": "nahrať súbor", + "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "Licencia", "editor.record.form.license.cc-by": "", @@ -173,12 +177,15 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Neznáme alebo chýbajúce", + "editor.record.form.metadata.title": "", + "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", "editor.record.form.temporalExtents": "", "editor.record.form.temporalExtents.addDate": "", "editor.record.form.temporalExtents.addRange": "", "editor.record.form.temporalExtents.date": "", "editor.record.form.temporalExtents.range": "", + "editor.record.form.unique.identifier": "", "editor.record.form.updateFrequency": "", "editor.record.form.updateFrequency.planned": "", "editor.record.loadError.body": "", @@ -262,10 +269,10 @@ "organisations.sortBy.nameDesc": "Názov Z → A", "organisations.sortBy.recordCountAsc": "Publikácie 0 → 9", "organisations.sortBy.recordCountDesc": "Publikácie 9 → 0", - "organization.details.lastPublishedDatasets": "", - "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", + "organization.lastPublishedDatasets": "", + "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Ďalšia stránka", "pagination.page": "strana", "pagination.pageOf": "z", From e8e3b525c6ba3c763457054bcad7fdf0894a2c7b Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Tue, 2 Jul 2024 10:01:45 +0200 Subject: [PATCH 037/378] feat(inputs): style switch toggle component --- .../switch-toggle/switch-toggle.component.css | 31 +++++++++++++++++++ .../switch-toggle.component.spec.ts | 28 +++++++++-------- .../switch-toggle/switch-toggle.stories.ts | 2 +- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.css b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.css index e69de29bb2..0683b61eef 100644 --- a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.css +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.css @@ -0,0 +1,31 @@ +:host { + --mat-standard-button-toggle-height: 32px; +} + +.mat-button-toggle-group-appearance-standard { + background-color: var(--color-gray-200); + padding: 4px; + display: flex; + gap: 4px; + border-radius: 8px; +} + +.mat-button-toggle-appearance-standard { + color: var(--color-main); + background-color: var(--color-gray-200); + border-radius: 4px; + border-left: none; +} + +.mat-button-toggle-appearance-standard.mat-button-toggle-checked { + background-color: var(--color-main); + color: var(--color-primary-white); +} + +button.mat-button-toggle-button.mat-focus-indicator.mat-button-toggle-label-content { + line-height: 32px; +} + +.mat-button-toggle-label-content { + line-height: 32px; +} diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts index 0b21360790..d2488596dd 100644 --- a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts @@ -1,21 +1,23 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing' -import { SwitchToggleComponent } from './switch-toggle.component'; +import { SwitchToggleComponent } from './switch-toggle.component' +import { MatButtonToggleModule } from '@angular/material/button-toggle' describe('SwitchToggleComponent', () => { - let component: SwitchToggleComponent; - let fixture: ComponentFixture; + let component: SwitchToggleComponent + let fixture: ComponentFixture beforeEach(() => { TestBed.configureTestingModule({ - declarations: [SwitchToggleComponent] - }); - fixture = TestBed.createComponent(SwitchToggleComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + declarations: [SwitchToggleComponent], + imports: [MatButtonToggleModule], + }) + fixture = TestBed.createComponent(SwitchToggleComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) it('should create', () => { - expect(component).toBeTruthy(); - }); -}); + expect(component).toBeTruthy() + }) +}) diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts index fddcdbb12b..0a7082d251 100644 --- a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts @@ -16,7 +16,7 @@ export default { export const Primary: StoryObj = { args: { options: [ - { label: 'city', value: 'city', checked: false }, + { label: 'city', value: 'city', checked: true }, { label: 'municipality', value: 'municipality', checked: false }, { label: 'state', value: 'state', checked: false }, ], From 580718f2200b0aae86b80768d80af207c69f4ed4 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Tue, 2 Jul 2024 10:16:25 +0200 Subject: [PATCH 038/378] fix(i18n): translations --- translations/de.json | 6 +++--- translations/en.json | 4 ++-- translations/es.json | 6 +++--- translations/fr.json | 6 +++--- translations/it.json | 6 +++--- translations/nl.json | 6 +++--- translations/pt.json | 6 +++--- translations/sk.json | 6 +++--- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/translations/de.json b/translations/de.json index 9f4d68aa20..36605a29f6 100644 --- a/translations/de.json +++ b/translations/de.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{Datensätze} one{Datensatz} other{Datensätze}}", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}", "chart.aggregation.average": "Durchschnitt", "chart.aggregation.count": "Anzahl", "chart.aggregation.max": "Maximum", @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Veröffentlichungen 0 → 9", "organisations.sortBy.recordCountDesc": "Veröffentlichungen 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Nächste Seite", "pagination.page": "Seite", "pagination.pageOf": "von", diff --git a/translations/en.json b/translations/en.json index e02b5eb542..7d9306f5fc 100644 --- a/translations/en.json +++ b/translations/en.json @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "Name Z → A", "organisations.sortBy.recordCountAsc": "Publications 0 → 9", "organisations.sortBy.recordCountDesc": "Publications 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "Contact by email", "organization.header.recordCount": "{count, plural, =0{data} one{data} other{datas}}", - "organization.lastPublishedDatasets": "Last published datasets", - "organization.lastPublishedDatasets.searchAllButton": "Search all", "pagination.nextPage": "Next page", "pagination.page": "page", "pagination.pageOf": "of", diff --git a/translations/es.json b/translations/es.json index cbd02da733..ed5cde17d4 100644 --- a/translations/es.json +++ b/translations/es.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de datos", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "organizaciones", "chart.aggregation.average": "promedio", "chart.aggregation.count": "conteo", "chart.aggregation.max": "máximo", @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/fr.json b/translations/fr.json index e71b65ed72..c7c9859661 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "Se connecter", "catalog.figures.datasets": "{count, plural, =0{données} one{donnée} other{données}}", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "{count, plural, =0{organisations} one{organisation} other{organisations}}", "chart.aggregation.average": "moyenne", "chart.aggregation.count": "nombre", "chart.aggregation.max": "maximum", @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "Nom Z → A", "organisations.sortBy.recordCountAsc": "Données 0 → 9", "organisations.sortBy.recordCountDesc": "Données 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "Contacter par mail", "organization.header.recordCount": "{count, plural, =0{donnée} one{donnée} other{données}}", - "organization.lastPublishedDatasets": "Dernières données publiées", - "organization.lastPublishedDatasets.searchAllButton": "Rechercher tous", "pagination.nextPage": "Page suivante", "pagination.page": "page", "pagination.pageOf": "sur", diff --git a/translations/it.json b/translations/it.json index 6403acfbd0..85b1d6e0be 100644 --- a/translations/it.json +++ b/translations/it.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}", "chart.aggregation.average": "media", "chart.aggregation.count": "conteggio", "chart.aggregation.max": "massimo", @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "Nome Z → A", "organisations.sortBy.recordCountAsc": "Dati 0 → 9", "organisations.sortBy.recordCountDesc": "Dati 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Pagina successiva", "pagination.page": "pagina", "pagination.pageOf": "di", diff --git a/translations/nl.json b/translations/nl.json index a1b8b59b86..3601d0fdb2 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "datasets", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "organisaties", "chart.aggregation.average": "gemiddelde", "chart.aggregation.count": "aantal", "chart.aggregation.max": "max", @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/pt.json b/translations/pt.json index 3ffac6989d..4f814a97ff 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "conjuntos de dados", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "organizações", "chart.aggregation.average": "média", "chart.aggregation.count": "contagem", "chart.aggregation.max": "máximo", @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "", "organisations.sortBy.recordCountAsc": "", "organisations.sortBy.recordCountDesc": "", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "", "pagination.page": "", "pagination.pageOf": "", diff --git a/translations/sk.json b/translations/sk.json index 9c792569b7..484e8c69cf 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -2,7 +2,7 @@ "Add Layer As": "", "button.login": "", "catalog.figures.datasets": "{count, plural, =0{datasety} one{dataset} other{datasety}}", - "catalog.figures.organisations": "", + "catalog.figures.organizations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}", "chart.aggregation.average": "priemer", "chart.aggregation.count": "počet", "chart.aggregation.max": "maximum", @@ -269,10 +269,10 @@ "organisations.sortBy.nameDesc": "Názov Z → A", "organisations.sortBy.recordCountAsc": "Publikácie 0 → 9", "organisations.sortBy.recordCountDesc": "Publikácie 9 → 0", + "organization.details.lastPublishedDatasets": "", + "organization.details.lastPublishedDatasets.searchAllButton": "", "organization.details.mailContact": "", "organization.header.recordCount": "{count, plural, =0{} one{} other{}}", - "organization.lastPublishedDatasets": "", - "organization.lastPublishedDatasets.searchAllButton": "", "pagination.nextPage": "Ďalšia stránka", "pagination.page": "strana", "pagination.pageOf": "z", From 2a336bf27d5622fcf4144e815e3bd3ffe671bcd7 Mon Sep 17 00:00:00 2001 From: mmohad Date: Wed, 3 Jul 2024 14:58:51 +0200 Subject: [PATCH 039/378] Fix(ApiForm): boucle infini --- .../record/src/lib/ign-api-dl/ign-api-dl.component.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index 1bdcebe98b..126ba2f05b 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -200,15 +200,11 @@ export class IgnApiDlComponent implements OnInit { choicesTest = response.data.entry.filter( (element) => element['id'] == this.apiBaseUrl )[0] - console.log(choicesTest) if (choicesTest) { return choicesTest } else { - console.log('avant while') - - while (choicesTest === undefined || response.data.pageCount > page) { - console.log('hello') + while (choicesTest === undefined && response.data.pageCount > page) { ;[response] = await Promise.all([ axios.get(this.url.concat(`&pageSize=200&page=${page}`)), ]) From 1c432eb75314f1767d848acdf14f4bc5c8f02e95 Mon Sep 17 00:00:00 2001 From: Ronan Date: Thu, 4 Jul 2024 10:57:33 +0200 Subject: [PATCH 040/378] =?UTF-8?q?feat(formapi):=20Ajout=20d'un=20syst?= =?UTF-8?q?=C3=A8me=20de=20page=20dans=20le=20formulaire=20#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/lib/ign-api-dl/ign-api-dl.component.html | 14 ++++++++++---- .../src/lib/ign-api-dl/ign-api-dl.component.ts | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html index 9828be4a75..9604718a27 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.html @@ -93,12 +93,18 @@ + diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index c6e6ebbc83..f560ef55c4 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -189,6 +189,13 @@ export class IgnApiDlComponent implements OnInit { this.size$.next(String(size)) this.page$.next(String(page)) } + + lessResult(): void { + const page = Number(this.page$.value) - 1 + const size = (page + 1) * Number(this.initialPageSize) + this.size$.next(String(size)) + this.page$.next(String(page)) + } resetPage(): void { this.page$.next('0') } From 065b5046f100e884ddfb9492b229c77e2a40a91e Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 4 Jul 2024 11:14:03 +0200 Subject: [PATCH 041/378] feat: rework org filters --- .../search-filters/search-filters.component.ts | 4 +++- .../src/lib/utils/service/fields.service.ts | 17 +++++++++++++++-- translations/de.json | 9 +++++++-- translations/en.json | 8 +++++--- translations/es.json | 5 +++++ translations/fr.json | 8 +++++--- translations/it.json | 7 ++++++- translations/nl.json | 5 +++++ translations/pt.json | 5 +++++ translations/sk.json | 7 ++++++- 10 files changed, 62 insertions(+), 13 deletions(-) diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts index 508e335189..f3e144f1ac 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts @@ -53,12 +53,14 @@ export class SearchFiltersComponent implements OnInit { this.platformService.getMe().subscribe((user) => (this.userId = user?.id)) this.searchConfig = ( getOptionalSearchConfig().ADVANCED_FILTERS || [ - 'publisher', + 'organization', 'format', 'publicationYear', 'topic', 'isSpatial', 'license', + 'producer', + 'publisher', ] ) .filter((adv_filter) => { 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 1096380a15..3fe888a417 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -25,19 +25,21 @@ marker('search.filters.keyword') marker('search.filters.isSpatial') marker('search.filters.license') marker('search.filters.publicationYear') -marker('search.filters.publisher') +marker('search.filters.organization') marker('search.filters.representationType') marker('search.filters.resourceType') marker('search.filters.standard') marker('search.filters.topic') marker('search.filters.contact') +marker('search.filters.producer') +marker('search.filters.publisher') @Injectable({ providedIn: 'root', }) export class FieldsService { protected fields = { - publisher: new OrganizationSearchField(this.injector), + organization: new OrganizationSearchField(this.injector), format: new SimpleSearchField('format', this.injector, 'asc'), resourceType: new TranslatedSearchField( 'resourceType', @@ -70,6 +72,17 @@ export class FieldsService { q: new FullTextSearchField(), license: new LicenseSearchField(this.injector), owner: new OwnerSearchField(this.injector), + producer: new MultilingualSearchField( + 'originatorOrgForResourceObject', + this.injector, + 'desc', + 'count' + ), + publisher: new MultilingualSearchField( + 'distributorOrgForResourceObject', + this.injector, + 'asc' + ), } as Record get supportedFields() { diff --git a/translations/de.json b/translations/de.json index 2db3fe2365..49ef680e97 100644 --- a/translations/de.json +++ b/translations/de.json @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "OK, meine Daten sind korrekt", "datafeeder.datasetValidation.title": "Stellen Sie sicher, dass Ihre Daten korrekt sind", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "Wie würden Sie Ihren Datensatz beschreiben?", "datafeeder.form.datepicker": "Wissen Sie, wann der Datensatz erstellt wurde?", "datafeeder.form.description": "Beschreiben Sie abschließend den Prozess, der zur Erstellung des Datensatzes verwendet wurde", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "Metadatensatz", "datafeeder.publishSuccess.illustration.title": "Erledigt, alles ist gut!", "datafeeder.publishSuccess.mapViewer": "Kartenviewer", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "Zeigen Sie Ihre Daten an in:", "datafeeder.publishSuccess.title": "Herzlichen Glückwunsch! \n Ihr Datensatz wurde veröffentlicht", "datafeeder.publishSuccess.uploadAnotherData": "Ein weiteren Datensatz hochladen", @@ -104,7 +107,6 @@ "datafeeder.upload.maxFileSize": "Maximale Dateigröße beträgt {size} MB", "datafeeder.upload.title": "Laden Sie Ihren Datensatz hoch", "datafeeder.upload.uploadButton": "Hochladen", - "datafeeder.validation.encoding": "Codierung", "datafeeder.validation.csv.delimiter": "", "datafeeder.validation.csv.delimiter.comma": "", "datafeeder.validation.csv.delimiter.semicolon": "", @@ -114,6 +116,7 @@ "datafeeder.validation.csv.quote.none": "", "datafeeder.validation.csv.quote.simple": "", "datafeeder.validation.csv.quoteChar": "", + "datafeeder.validation.encoding": "Codierung", "datafeeder.validation.extent.title": "Hier ist der Datensatzumfang", "datafeeder.validation.extent.title.unknown": "Das Projektionssystem ist unbekannt", "datafeeder.validation.projection": "Raumbezugssystem:", @@ -392,9 +395,11 @@ "search.filters.minimize": "Minimieren", "search.filters.myRecords": "Nur meine Datensätze anzeigen", "search.filters.myRecordsHelp": "Wenn dies aktiviert ist, werden nur von mir erstellte Datensätze angezeigt; Datensätze, die von anderen erstellt wurden, werden nicht angezeigt.", + "search.filters.organization": "", "search.filters.otherRecords": "Datensätze von einer anderen Person anzeigen", + "search.filters.producer": "", "search.filters.publicationYear": "Veröffentlichungsjahr", - "search.filters.publisher": "Organisationen", + "search.filters.publisher": "", "search.filters.representationType": "Repräsentationstyp", "search.filters.resourceType": "Ressourcentyp", "search.filters.standard": "Standard", diff --git a/translations/en.json b/translations/en.json index 041390c69f..6497695d09 100644 --- a/translations/en.json +++ b/translations/en.json @@ -46,11 +46,11 @@ "datafeeder.analysisProgressBar.subtitle": "The analysis may take several minutes, please wait.", "datafeeder.analysisProgressBar.title": "Analyze in progress", "datafeeder.datasetValidation.datasetInformation": "The provided dataset contains {number} entities", - "datafeeder.datasetValidationCsv.lineNumbers": "Sample of the first 5 lines* of the dataset:", - "datafeeder.datasetValidationCsv.explicitLineNumbers": "*The table must display the first 5 lines (excluding the header)
    If this is not the case, check that the file is correctly formatted", "datafeeder.datasetValidation.submitButton": "OK, my data are correct", "datafeeder.datasetValidation.title": "Make sure your data are correct", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "*The table must display the first 5 lines (excluding the header)
    If this is not the case, check that the file is correctly formatted", + "datafeeder.datasetValidationCsv.lineNumbers": "Sample of the first 5 lines* of the dataset:", "datafeeder.form.abstract": "How would you describe your dataset?", "datafeeder.form.datepicker": "Do you know when the dataset was created?", "datafeeder.form.description": "Finally, please describe the process that was used to create the dataset", @@ -395,9 +395,11 @@ "search.filters.minimize": "Minimize", "search.filters.myRecords": "Show only my records", "search.filters.myRecordsHelp": "When this is enabled, records only created by myself are shown; records created by others will not show up.", + "search.filters.organization": "Organization", "search.filters.otherRecords": "Showing records from another person", + "search.filters.producer": "Producer", "search.filters.publicationYear": "Publication year", - "search.filters.publisher": "Organizations", + "search.filters.publisher": "Publisher", "search.filters.representationType": "Representation type", "search.filters.resourceType": "Resource type", "search.filters.standard": "Standard", diff --git a/translations/es.json b/translations/es.json index c4b7d6ef74..474298aaca 100644 --- a/translations/es.json +++ b/translations/es.json @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "", "datafeeder.datasetValidation.title": "", "datafeeder.datasetValidation.unknown": "", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "", "datafeeder.form.datepicker": "", "datafeeder.form.description": "", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "", "datafeeder.publishSuccess.illustration.title": "", "datafeeder.publishSuccess.mapViewer": "", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "", "datafeeder.publishSuccess.title": "", "datafeeder.publishSuccess.uploadAnotherData": "", @@ -392,7 +395,9 @@ "search.filters.minimize": "", "search.filters.myRecords": "", "search.filters.myRecordsHelp": "", + "search.filters.organization": "", "search.filters.otherRecords": "", + "search.filters.producer": "", "search.filters.publicationYear": "", "search.filters.publisher": "", "search.filters.representationType": "", diff --git a/translations/fr.json b/translations/fr.json index 9e15942f5c..4eaa3e9676 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -46,11 +46,11 @@ "datafeeder.analysisProgressBar.subtitle": "L'analyse peut prendre plusieurs minutes, merci d'attendre.", "datafeeder.analysisProgressBar.title": "Analyse en cours", "datafeeder.datasetValidation.datasetInformation": "Le jeu de données fourni contient {number} entités", - "datafeeder.datasetValidationCsv.lineNumbers": "Résumé des 5 premières lignes* du CSV :", - "datafeeder.datasetValidationCsv.explicitLineNumbers": "*Le tableau doit afficher les 5 premières lignes (hors en-tête)
    Si ce n'est pas le cas, vérifier que le fichier est bien formatté", "datafeeder.datasetValidation.submitButton": "OK, mes données sont correctes", "datafeeder.datasetValidation.title": "Vérifiez que vos données sont correctes", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "*Le tableau doit afficher les 5 premières lignes (hors en-tête)
    Si ce n'est pas le cas, vérifier que le fichier est bien formatté", + "datafeeder.datasetValidationCsv.lineNumbers": "Résumé des 5 premières lignes* du CSV :", "datafeeder.form.abstract": "Comment décrire votre jeu de données ?", "datafeeder.form.datepicker": "Savez-vous quand la donnée a été créée ?", "datafeeder.form.description": "Enfin, décrivez le processus utilisé pour créer la donnée", @@ -395,9 +395,11 @@ "search.filters.minimize": "Réduire", "search.filters.myRecords": "Voir mes données", "search.filters.myRecordsHelp": "Quand activé, n'affiche que les données créées avec mon utilisateur. Les données créées par les autres utilisateurs ne sont pas affichées.", + "search.filters.organization": "Organisation", "search.filters.otherRecords": "Affichage des données d'un autre utilisateur", + "search.filters.producer": "Producteur", "search.filters.publicationYear": "Année de publication", - "search.filters.publisher": "Organisations", + "search.filters.publisher": "Distributeur", "search.filters.representationType": "Type de représentation", "search.filters.resourceType": "Type de ressource", "search.filters.standard": "Standard", diff --git a/translations/it.json b/translations/it.json index 02621f0449..03b9737af9 100644 --- a/translations/it.json +++ b/translations/it.json @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "OK, i miei dati sono corretti", "datafeeder.datasetValidation.title": "Controllare che i dati siano corretti", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "Come descrivere il suo dataset?", "datafeeder.form.datepicker": "Sa quando è stato creato il suo dataset ?", "datafeeder.form.description": "Infine, descrivere il processo utilizzato per creare il dataset", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "Scheda di metadati", "datafeeder.publishSuccess.illustration.title": "Completato, tutto è andato bene!", "datafeeder.publishSuccess.mapViewer": "Visualizzatore", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "Visualizzare i dati:", "datafeeder.publishSuccess.title": "Congratulazioni! \n I suoi dati sono stati pubblicati", "datafeeder.publishSuccess.uploadAnotherData": "Caricare un altro dato", @@ -392,9 +395,11 @@ "search.filters.minimize": "Riduci", "search.filters.myRecords": "Visualizza i miei dati", "search.filters.myRecordsHelp": "Quando attivato, mostra solo i dati creati con il mio utente. I dati creati da altri utenti non sono visualizzati.", + "search.filters.organization": "", "search.filters.otherRecords": "Visualizzazione dei dati di un altro utente", + "search.filters.producer": "", "search.filters.publicationYear": "Anno di pubblicazione", - "search.filters.publisher": "Organizzazioni", + "search.filters.publisher": "", "search.filters.representationType": "Tipo di rappresentazione", "search.filters.resourceType": "Tipo di risorsa", "search.filters.standard": "Standard", diff --git a/translations/nl.json b/translations/nl.json index 6e489176c7..5fe6272582 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "", "datafeeder.datasetValidation.title": "", "datafeeder.datasetValidation.unknown": "", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "", "datafeeder.form.datepicker": "", "datafeeder.form.description": "", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "", "datafeeder.publishSuccess.illustration.title": "", "datafeeder.publishSuccess.mapViewer": "", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "", "datafeeder.publishSuccess.title": "", "datafeeder.publishSuccess.uploadAnotherData": "", @@ -392,7 +395,9 @@ "search.filters.minimize": "", "search.filters.myRecords": "", "search.filters.myRecordsHelp": "", + "search.filters.organization": "", "search.filters.otherRecords": "", + "search.filters.producer": "", "search.filters.publicationYear": "", "search.filters.publisher": "", "search.filters.representationType": "", diff --git a/translations/pt.json b/translations/pt.json index 92ab9940c4..2465a82ba4 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "", "datafeeder.datasetValidation.title": "", "datafeeder.datasetValidation.unknown": "", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "", "datafeeder.form.datepicker": "", "datafeeder.form.description": "", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "", "datafeeder.publishSuccess.illustration.title": "", "datafeeder.publishSuccess.mapViewer": "", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "", "datafeeder.publishSuccess.title": "", "datafeeder.publishSuccess.uploadAnotherData": "", @@ -392,7 +395,9 @@ "search.filters.minimize": "", "search.filters.myRecords": "", "search.filters.myRecordsHelp": "", + "search.filters.organization": "", "search.filters.otherRecords": "", + "search.filters.producer": "", "search.filters.publicationYear": "", "search.filters.publisher": "", "search.filters.representationType": "", diff --git a/translations/sk.json b/translations/sk.json index 41fb225ad6..125dd26f1c 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -49,6 +49,8 @@ "datafeeder.datasetValidation.submitButton": "OK, moje dáta sú správne", "datafeeder.datasetValidation.title": "Uistite sa, že dáta sú správne", "datafeeder.datasetValidation.unknown": " - ", + "datafeeder.datasetValidationCsv.explicitLineNumbers": "", + "datafeeder.datasetValidationCsv.lineNumbers": "", "datafeeder.form.abstract": "Ako by ste opísali Váš dataset?", "datafeeder.form.datepicker": "Viete, kedy bol dataset vytvorený?", "datafeeder.form.description": "Nakoniec popíšte proces, ktorý bol použitý na vytvorenie datasetu", @@ -76,6 +78,7 @@ "datafeeder.publishSuccess.geonetworkRecord": "Metadátový záznam", "datafeeder.publishSuccess.illustration.title": "Hotovo, všetko je v poriadku!", "datafeeder.publishSuccess.mapViewer": "Prehliadač máp", + "datafeeder.publishSuccess.ogcFeature": "", "datafeeder.publishSuccess.subtitle": "Pozrite si svoje dáta v:", "datafeeder.publishSuccess.title": "Gratulujeme! Váš dataset bol publikovaný", "datafeeder.publishSuccess.uploadAnotherData": "Nahrajte ďalší dataset", @@ -392,9 +395,11 @@ "search.filters.minimize": "Zmenšiť", "search.filters.myRecords": "Zobraziť len moje záznamy", "search.filters.myRecordsHelp": "Keď je táto možnosť zapnutá, zobrazia sa len záznamy vytvorené mnou; záznamy vytvorené inými sa nezobrazia.", + "search.filters.organization": "", "search.filters.otherRecords": "Zobrazenie záznamov od inej osoby", + "search.filters.producer": "", "search.filters.publicationYear": "Rok zverejnenia", - "search.filters.publisher": "Organizácie", + "search.filters.publisher": "", "search.filters.representationType": "Typ reprezentácie", "search.filters.resourceType": "Typ zdroja", "search.filters.standard": "Štandard", From 6fcd63c67f08d9552902876b52b49df2cb081287 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 4 Jul 2024 11:38:37 +0200 Subject: [PATCH 042/378] feat: adapt ut --- .../src/lib/utils/service/fields.service.spec.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 b51ac1167a..3a71e24620 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 @@ -86,7 +86,7 @@ describe('FieldsService', () => { describe('#supportedFields', () => { it('returns a list of fields', () => { expect(service.supportedFields).toEqual([ - 'publisher', + 'organization', 'format', 'resourceType', 'representationType', @@ -99,13 +99,15 @@ describe('FieldsService', () => { 'q', 'license', 'owner', + 'producer', + 'publisher', ]) }) }) describe('#getAvailableValues', () => { let values beforeEach(async () => { - values = await lastValueFrom(service.getAvailableValues('publisher')) + values = await lastValueFrom(service.getAvailableValues('organization')) }) it('gets the values from the orgs service', () => { expect(values).toEqual([{ label: 'orgA (10)', value: 'orgA' }]) @@ -121,7 +123,7 @@ describe('FieldsService', () => { beforeEach(async () => { filters = await lastValueFrom( service.buildFiltersFromFieldValues({ - publisher: ['aa', 'bb'], + organization: ['aa', 'bb'], format: ['cc', 'dd'], publicationYear: '2022', q: 'any', @@ -174,12 +176,14 @@ describe('FieldsService', () => { isSpatial: [], license: [], publicationYear: [], - publisher: ['orgB'], + organization: ['orgB'], q: [], representationType: [], resourceType: [], topic: [], owner: [], + producer: [], + publisher: [], }) }) }) From f2a7369ce1846e0068a4fc69e80da252a8699f34 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 4 Jul 2024 11:50:35 +0200 Subject: [PATCH 043/378] fix: edit e2e to work with new filter name --- apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts | 2 +- docs/reference/search-fields.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 0e9813850e..ef55ee7586 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -246,7 +246,7 @@ describe('dataset pages', () => { }) it('should go to dataset search page when clicking on org name and filter by org', () => { cy.get('[data-cy="organization-name"]').eq(1).click() - cy.url().should('include', '/search?publisher=') + cy.url().should('include', '/search?organization=') }) it('should go to dataset search page when clicking on keyword and filter by keyword', () => { cy.get('gn-ui-expandable-panel').eq(2).click() diff --git a/docs/reference/search-fields.md b/docs/reference/search-fields.md index 0625fe8192..614f8495e9 100644 --- a/docs/reference/search-fields.md +++ b/docs/reference/search-fields.md @@ -11,7 +11,7 @@ GeoNetwork-UI has built-in logic for several search fields, each of them relying These fields are used in the following context: - when building a URL or permalink from several search criteria; these fields will appear as query parameters in the URL, for instance: - `/search?publisher=MyOrg&format=csv&format=excel` + `/search?organization=MyOrg&format=csv&format=excel` - when specifying advanced filters [in a configuration file](../guide/configure.md#search) ## Fields From 7a9ca716b355988a8a8aa71f17d20ad035004780 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 4 Jul 2024 12:40:03 +0200 Subject: [PATCH 044/378] feat: fix e2e tests --- apps/datahub-e2e/src/e2e/datasets.cy.ts | 6 ++++-- apps/datahub-e2e/src/fixtures/config-with-all-filters.toml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasets.cy.ts b/apps/datahub-e2e/src/e2e/datasets.cy.ts index cb4c40e487..c19ff052f7 100644 --- a/apps/datahub-e2e/src/e2e/datasets.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasets.cy.ts @@ -173,7 +173,7 @@ describe('datasets', () => { .click() }) it('should display all filters', () => { - cy.get('@filters').filter(':visible').should('have.length', 10) + cy.get('@filters').filter(':visible').should('have.length', 12) cy.get('@filters') .children() .then(($dropdowns) => @@ -182,7 +182,7 @@ describe('datasets', () => { .map((dropdown) => dropdown.getAttribute('data-cy-field')) ) .should('eql', [ - 'publisher', + 'organization', 'format', 'publicationYear', 'topic', @@ -192,6 +192,8 @@ describe('datasets', () => { 'keyword', 'resourceType', 'representationType', + 'producer', + 'publisher', ]) cy.screenshot({ capture: 'viewport' }) }) diff --git a/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml b/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml index 2fa889d3f5..70c1b400c4 100644 --- a/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml +++ b/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml @@ -9,4 +9,4 @@ main_color = "#212029" # All-purpose text color background_color = "#fdfbff" [search] -advanced_filters = ['publisher', 'format', 'publicationYear', 'topic', 'isSpatial', 'license', 'inspireKeyword', 'keyword', 'resourceType', 'representationType'] +advanced_filters = ['organization', 'format', 'publicationYear', 'topic', 'isSpatial', 'license', 'inspireKeyword', 'keyword', 'resourceType', 'representationType', 'producer', 'publisher'] From e958c6093cfa06e8d891d453cd7ba7cf0a33b22c Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 4 Jul 2024 15:32:03 +0200 Subject: [PATCH 045/378] fix(max-lines): fix import for standalone --- apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts | 10 ++++++++++ libs/ui/elements/src/lib/ui-elements.module.ts | 3 ++- .../layout/src/lib/max-lines/max-lines.component.html | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 0e9813850e..a45659c6f0 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -140,6 +140,16 @@ describe('dataset pages', () => { expect(text).not.to.equal('') }) }) + it('should display the read more button and expand description', () => { + cy.visit('/dataset/01491630-78ce-49f3-b479-4b30dabc4c69') + cy.get('datahub-record-metadata') + .find('[id="about"]') + .find('gn-ui-max-lines') + .as('maxLines') + cy.get('@maxLines').find('.ease-out').should('exist') + cy.get('[data-cy=readMoreButton]').click() + cy.get('@maxLines').find('.ease-in').should('exist') + }) it('should display the thumbnail image and magnify', () => { cy.get('datahub-record-metadata') .find('[id="about"]') diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index 83c6e654ca..274cbc06fe 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -10,7 +10,7 @@ import { DownloadItemComponent } from './download-item/download-item.component' import { DownloadsListComponent } from './downloads-list/downloads-list.component' import { ApiCardComponent } from './api-card/api-card.component' import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' -import { UiLayoutModule } from '@geonetwork-ui/ui/layout' +import { MaxLinesComponent, UiLayoutModule } from '@geonetwork-ui/ui/layout' import { TranslateModule } from '@ngx-translate/core' import { RelatedRecordCardComponent } from './related-record-card/related-record-card.component' import { MetadataContactComponent } from './metadata-contact/metadata-contact.component' @@ -49,6 +49,7 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe' ThumbnailComponent, TimeSincePipe, BadgeComponent, + MaxLinesComponent, ], declarations: [ MetadataInfoComponent, diff --git a/libs/ui/layout/src/lib/max-lines/max-lines.component.html b/libs/ui/layout/src/lib/max-lines/max-lines.component.html index 2cdf3f4bbf..f6e70920b9 100644 --- a/libs/ui/layout/src/lib/max-lines/max-lines.component.html +++ b/libs/ui/layout/src/lib/max-lines/max-lines.component.html @@ -14,6 +14,7 @@ *ngIf="showToggleButton" (click)="toggleDisplay()" class="text-secondary cursor-pointer pt-2.5" + data-cy="readMoreButton" > {{ (isExpanded ? 'ui.readLess' : 'ui.readMore') | translate }}
    From 51881b1084a07b6c29c38d903dcd8f41b77ef8cd Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 4 Jul 2024 15:32:56 +0200 Subject: [PATCH 046/378] fix(header-record): reduce line-clamp to 3 --- .../src/app/record/header-record/header-record.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/datahub/src/app/record/header-record/header-record.component.html b/apps/datahub/src/app/record/header-record/header-record.component.html index 57c5637e7f..468272bcc0 100644 --- a/apps/datahub/src/app/record/header-record/header-record.component.html +++ b/apps/datahub/src/app/record/header-record/header-record.component.html @@ -32,7 +32,7 @@
    {{ metadata.title }} From b9d69c9c42d56362e7a2f5433b62324d0b135d13 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 4 Jul 2024 15:44:40 +0200 Subject: [PATCH 047/378] feat: sort options correctly --- .../feature/search/src/lib/utils/service/fields.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 3fe888a417..ad4a8b6cea 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -75,13 +75,14 @@ export class FieldsService { producer: new MultilingualSearchField( 'originatorOrgForResourceObject', this.injector, - 'desc', - 'count' + 'asc', + 'key' ), publisher: new MultilingualSearchField( 'distributorOrgForResourceObject', this.injector, - 'asc' + 'asc', + 'key' ), } as Record From 40e71af6ace00b6c73cd7166a1fafd9cfe6fcc1c Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 5 Jul 2024 15:09:58 +0200 Subject: [PATCH 048/378] feat: add -Org at the end of the new filters --- .../datahub-e2e/src/fixtures/config-with-all-filters.toml | 2 +- .../search/src/lib/utils/service/fields.service.ts | 8 ++++---- translations/de.json | 4 ++-- translations/en.json | 4 ++-- translations/es.json | 4 ++-- translations/fr.json | 4 ++-- translations/it.json | 4 ++-- translations/nl.json | 4 ++-- translations/pt.json | 4 ++-- translations/sk.json | 4 ++-- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml b/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml index 70c1b400c4..c3f09682ae 100644 --- a/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml +++ b/apps/datahub-e2e/src/fixtures/config-with-all-filters.toml @@ -9,4 +9,4 @@ main_color = "#212029" # All-purpose text color background_color = "#fdfbff" [search] -advanced_filters = ['organization', 'format', 'publicationYear', 'topic', 'isSpatial', 'license', 'inspireKeyword', 'keyword', 'resourceType', 'representationType', 'producer', 'publisher'] +advanced_filters = ['organization', 'format', 'publicationYear', 'topic', 'isSpatial', 'license', 'inspireKeyword', 'keyword', 'resourceType', 'representationType', 'producerOrg', 'publisherOrg'] 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 ad4a8b6cea..76a1850932 100644 --- a/libs/feature/search/src/lib/utils/service/fields.service.ts +++ b/libs/feature/search/src/lib/utils/service/fields.service.ts @@ -31,8 +31,8 @@ marker('search.filters.resourceType') marker('search.filters.standard') marker('search.filters.topic') marker('search.filters.contact') -marker('search.filters.producer') -marker('search.filters.publisher') +marker('search.filters.producerOrg') +marker('search.filters.publisherOrg') @Injectable({ providedIn: 'root', @@ -72,13 +72,13 @@ export class FieldsService { q: new FullTextSearchField(), license: new LicenseSearchField(this.injector), owner: new OwnerSearchField(this.injector), - producer: new MultilingualSearchField( + producerOrg: new MultilingualSearchField( 'originatorOrgForResourceObject', this.injector, 'asc', 'key' ), - publisher: new MultilingualSearchField( + publisherOrg: new MultilingualSearchField( 'distributorOrgForResourceObject', this.injector, 'asc', diff --git a/translations/de.json b/translations/de.json index 49ef680e97..55feccb7c4 100644 --- a/translations/de.json +++ b/translations/de.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "Wenn dies aktiviert ist, werden nur von mir erstellte Datensätze angezeigt; Datensätze, die von anderen erstellt wurden, werden nicht angezeigt.", "search.filters.organization": "", "search.filters.otherRecords": "Datensätze von einer anderen Person anzeigen", - "search.filters.producer": "", + "search.filters.producerOrg": "", "search.filters.publicationYear": "Veröffentlichungsjahr", - "search.filters.publisher": "", + "search.filters.publisherOrg": "", "search.filters.representationType": "Repräsentationstyp", "search.filters.resourceType": "Ressourcentyp", "search.filters.standard": "Standard", diff --git a/translations/en.json b/translations/en.json index 6497695d09..1f3e28e966 100644 --- a/translations/en.json +++ b/translations/en.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "When this is enabled, records only created by myself are shown; records created by others will not show up.", "search.filters.organization": "Organization", "search.filters.otherRecords": "Showing records from another person", - "search.filters.producer": "Producer", + "search.filters.producerOrg": "Producer", "search.filters.publicationYear": "Publication year", - "search.filters.publisher": "Publisher", + "search.filters.publisherOrg": "Publisher", "search.filters.representationType": "Representation type", "search.filters.resourceType": "Resource type", "search.filters.standard": "Standard", diff --git a/translations/es.json b/translations/es.json index 474298aaca..8973368432 100644 --- a/translations/es.json +++ b/translations/es.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "", "search.filters.organization": "", "search.filters.otherRecords": "", - "search.filters.producer": "", + "search.filters.producerOrg": "", "search.filters.publicationYear": "", - "search.filters.publisher": "", + "search.filters.publisherOrg": "", "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", diff --git a/translations/fr.json b/translations/fr.json index 4eaa3e9676..fb4d6e7462 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "Quand activé, n'affiche que les données créées avec mon utilisateur. Les données créées par les autres utilisateurs ne sont pas affichées.", "search.filters.organization": "Organisation", "search.filters.otherRecords": "Affichage des données d'un autre utilisateur", - "search.filters.producer": "Producteur", + "search.filters.producerOrg": "Producteur", "search.filters.publicationYear": "Année de publication", - "search.filters.publisher": "Distributeur", + "search.filters.publisherOrg": "Distributeur", "search.filters.representationType": "Type de représentation", "search.filters.resourceType": "Type de ressource", "search.filters.standard": "Standard", diff --git a/translations/it.json b/translations/it.json index 03b9737af9..e7fff9fb95 100644 --- a/translations/it.json +++ b/translations/it.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "Quando attivato, mostra solo i dati creati con il mio utente. I dati creati da altri utenti non sono visualizzati.", "search.filters.organization": "", "search.filters.otherRecords": "Visualizzazione dei dati di un altro utente", - "search.filters.producer": "", + "search.filters.producerOrg": "", "search.filters.publicationYear": "Anno di pubblicazione", - "search.filters.publisher": "", + "search.filters.publisherOrg": "", "search.filters.representationType": "Tipo di rappresentazione", "search.filters.resourceType": "Tipo di risorsa", "search.filters.standard": "Standard", diff --git a/translations/nl.json b/translations/nl.json index 5fe6272582..1086aa0994 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "", "search.filters.organization": "", "search.filters.otherRecords": "", - "search.filters.producer": "", + "search.filters.producerOrg": "", "search.filters.publicationYear": "", - "search.filters.publisher": "", + "search.filters.publisherOrg": "", "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", diff --git a/translations/pt.json b/translations/pt.json index 2465a82ba4..761e097ab1 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "", "search.filters.organization": "", "search.filters.otherRecords": "", - "search.filters.producer": "", + "search.filters.producerOrg": "", "search.filters.publicationYear": "", - "search.filters.publisher": "", + "search.filters.publisherOrg": "", "search.filters.representationType": "", "search.filters.resourceType": "", "search.filters.standard": "", diff --git a/translations/sk.json b/translations/sk.json index 125dd26f1c..aa56902b73 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -397,9 +397,9 @@ "search.filters.myRecordsHelp": "Keď je táto možnosť zapnutá, zobrazia sa len záznamy vytvorené mnou; záznamy vytvorené inými sa nezobrazia.", "search.filters.organization": "", "search.filters.otherRecords": "Zobrazenie záznamov od inej osoby", - "search.filters.producer": "", + "search.filters.producerOrg": "", "search.filters.publicationYear": "Rok zverejnenia", - "search.filters.publisher": "", + "search.filters.publisherOrg": "", "search.filters.representationType": "Typ reprezentácie", "search.filters.resourceType": "Typ zdroja", "search.filters.standard": "Štandard", From 7296aed0086a2eee5e80843dfd0a8d9ec09e651e Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 5 Jul 2024 15:10:10 +0200 Subject: [PATCH 049/378] feat: remove producerOrg & publsherOrg from default filters --- .../app/home/search/search-filters/search-filters.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts index f3e144f1ac..28c6774eaf 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.ts @@ -59,8 +59,6 @@ export class SearchFiltersComponent implements OnInit { 'topic', 'isSpatial', 'license', - 'producer', - 'publisher', ] ) .filter((adv_filter) => { From 032a2c4519af3761262ae5698f58029819d0dd54 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 5 Jul 2024 15:24:25 +0200 Subject: [PATCH 050/378] fix: change all occurences of publisher to organization and fix ut --- apps/datahub-e2e/src/e2e/datasets.cy.ts | 6 +++--- .../app/home/home-header/home-header.component.spec.ts | 2 +- .../search-filters/search-filters.component.spec.ts | 6 +++--- .../organization-details.component.html | 4 ++-- .../organization-details.component.spec.ts | 2 +- conf/default.toml | 6 +++--- docs/guide/configure.md | 4 ++-- .../router/src/lib/default/state/router.effects.spec.ts | 2 +- .../search/src/lib/utils/service/fields.service.spec.ts | 8 ++++---- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasets.cy.ts b/apps/datahub-e2e/src/e2e/datasets.cy.ts index c19ff052f7..ae4fcd8811 100644 --- a/apps/datahub-e2e/src/e2e/datasets.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasets.cy.ts @@ -192,13 +192,13 @@ describe('datasets', () => { 'keyword', 'resourceType', 'representationType', - 'producer', - 'publisher', + 'producerOrg', + 'publisherOrg', ]) cy.screenshot({ capture: 'viewport' }) }) - describe('publisher filter', () => { + describe('organization filter', () => { beforeEach(() => { cy.get('@filters').eq(0).click() getFilterOptions() diff --git a/apps/datahub/src/app/home/home-header/home-header.component.spec.ts b/apps/datahub/src/app/home/home-header/home-header.component.spec.ts index 1d764bdd72..2ff6c089db 100644 --- a/apps/datahub/src/app/home/home-header/home-header.component.spec.ts +++ b/apps/datahub/src/app/home/home-header/home-header.component.spec.ts @@ -31,7 +31,7 @@ jest.mock('@geonetwork-ui/util/app-config', () => { { sort: '-createDate', name: 'sortCeatedDateAndOrg', - filters: { publisher: ['DREAL'] }, + filters: { organization: ['DREAL'] }, }, { name: 'filterCarto', diff --git a/apps/datahub/src/app/home/search/search-filters/search-filters.component.spec.ts b/apps/datahub/src/app/home/search/search-filters/search-filters.component.spec.ts index 96b24e0406..9b838003c4 100644 --- a/apps/datahub/src/app/home/search/search-filters/search-filters.component.spec.ts +++ b/apps/datahub/src/app/home/search/search-filters/search-filters.component.spec.ts @@ -27,7 +27,7 @@ import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform. jest.mock('@geonetwork-ui/util/app-config', () => ({ getOptionalSearchConfig: () => ({ ADVANCED_FILTERS: [ - 'publisher', + 'publisherOrg', 'format', 'isSpatial', 'documentStandard', @@ -92,7 +92,7 @@ class FieldsServiceMock { ) public get supportedFields() { return [ - 'publisher', + 'publisherOrg', 'format', 'isSpatial', 'documentStandard', @@ -294,7 +294,7 @@ describe('SearchFiltersComponent', () => { filter_format: {}, filter_publicationYear: {}, filter_isSpatial: {}, - filter_publisher: {}, + filter_publisherOrg: {}, filter_topic: {}, filter_license: {}, filter_documentStandard: {}, 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 4147dd8a79..39f8da6180 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 @@ -33,7 +33,7 @@ { expect(orgDetailsSearchAllBtn).toBeTruthy() expect(orgDetailsSearchAllBtn?.getAttribute('href')).toEqual( - `/${ROUTER_ROUTE_SEARCH}?publisher=${encodeURIComponent( + `/${ROUTER_ROUTE_SEARCH}?organization=${encodeURIComponent( anOrganizationWithManyDatasets.name )}` ) diff --git a/conf/default.toml b/conf/default.toml index f0640402a3..33cf49335d 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -79,9 +79,9 @@ background_color = "#fdfbff" # filter_geometry_data = '{ "coordinates": [...], "type": "Polygon" }' # The advanced search filters available to the user can be customized with this setting. -# The following fields can be used for filtering: 'publisher', 'format', 'publicationYear', 'standard', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType' +# The following fields can be used for filtering: 'organization', 'format', 'publicationYear', 'standard', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType' # any other field will be ignored -# advanced_filters = ['publisher', 'format', 'publicationYear', 'topic', 'isSpatial', 'license'] +# advanced_filters = ['organization', 'format', 'publicationYear', 'topic', 'isSpatial', 'license'] # One or several search presets can be defined here; every search preset is composed of: # - a name (which can be a translation key) @@ -90,7 +90,7 @@ background_color = "#fdfbff" # [[search_preset]] # name = 'filterByName' # filters.q = 'Full text search' -# filters.publisher = ['Org 1', 'Org 2'] +# filters.organization = ['Org 1', 'Org 2'] # filters.format = ['format 1', 'format 2'] # filters.documentStandard = ['iso19115-3.2018'] # filters.inspireKeyword = ['keyword 1', 'keyword 2'] diff --git a/docs/guide/configure.md b/docs/guide/configure.md index 601788b76b..5be8bb6cd3 100644 --- a/docs/guide/configure.md +++ b/docs/guide/configure.md @@ -151,7 +151,7 @@ For a list of supported search fields, see [this documentation page](../referenc The filters should be provided as an array, for instance: ```toml -advanced_filters = ['publisher', 'inspireKeyword', 'keyword', 'topic'] +advanced_filters = ['organization', 'inspireKeyword', 'keyword', 'topic'] ``` - `[[search_preset]]` (multiple, optional) @@ -171,7 +171,7 @@ advanced_filters = ['publisher', 'inspireKeyword', 'keyword', 'topic'] [[search_preset]] name = 'filterByName' filters.q = 'full text search' - filters.publisher = ['Org 1', 'Org 2'] + filters.organization = ['Org 1', 'Org 2'] filters.format = ['format 1', 'format 2'] filters.documentStandard = ['iso19115-3.2018'] filters.inspireKeyword = ['keyword 1', 'keyword 2'] diff --git a/libs/feature/router/src/lib/default/state/router.effects.spec.ts b/libs/feature/router/src/lib/default/state/router.effects.spec.ts index 4936c377e5..53c7591496 100644 --- a/libs/feature/router/src/lib/default/state/router.effects.spec.ts +++ b/libs/feature/router/src/lib/default/state/router.effects.spec.ts @@ -44,7 +44,7 @@ const initialParams: Params = { class FieldsServiceMock { mapping = { - publisher: 'OrgForResource', + organization: 'OrgForResource', q: 'any', } buildFiltersFromFieldValues = jest.fn((fieldValues) => 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 3a71e24620..eaf8b09deb 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 @@ -99,8 +99,8 @@ describe('FieldsService', () => { 'q', 'license', 'owner', - 'producer', - 'publisher', + 'producerOrg', + 'publisherOrg', ]) }) }) @@ -182,8 +182,8 @@ describe('FieldsService', () => { resourceType: [], topic: [], owner: [], - producer: [], - publisher: [], + producerOrg: [], + publisherOrg: [], }) }) }) From dac0dd6096196332e4624c04fe4cb7e69fdd7f2b Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Fri, 5 Jul 2024 15:43:33 +0200 Subject: [PATCH 051/378] feat: update doc --- conf/default.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/default.toml b/conf/default.toml index 33cf49335d..17f646eeed 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -79,7 +79,7 @@ background_color = "#fdfbff" # filter_geometry_data = '{ "coordinates": [...], "type": "Polygon" }' # The advanced search filters available to the user can be customized with this setting. -# The following fields can be used for filtering: 'organization', 'format', 'publicationYear', 'standard', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType' +# The following fields can be used for filtering: 'organization', 'format', 'publicationYear', 'standard', 'inspireKeyword', 'keyword', 'topic', 'isSpatial', 'license', 'resourceType', 'representationType', 'producerOrg', 'publisherOrg' # any other field will be ignored # advanced_filters = ['organization', 'format', 'publicationYear', 'topic', 'isSpatial', 'license'] From 0501e198e7c6f9b3752f229df1992a2d8a17be3c Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Tue, 9 Jul 2024 15:51:02 +0200 Subject: [PATCH 052/378] feat: edit doc --- docs/reference/search-fields.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/reference/search-fields.md b/docs/reference/search-fields.md index 614f8495e9..977934c0b1 100644 --- a/docs/reference/search-fields.md +++ b/docs/reference/search-fields.md @@ -16,12 +16,24 @@ These fields are used in the following context: ## Fields -### Publisher +### Organization -> Field id: `publisher` +> Field id: `organization` This field targets the owner organization of a record. The exact meaning of a record's organization is defined by the "organization strategy" used; see [this documentation page](./organizations.md) for more details. +### Publisher + +> Field id: `publisherOrg` + +This field targets the organization publishing the record. The exact meaning of a record's organization publisher is defined by the "organization strategy" used; see [this documentation page](./organizations.md) for more details. + +### Producer + +> Field id: `producerOrg` + +This field targets the organization producing the record. The exact meaning of a record's organization producer is defined by the "organization strategy" used; see [this documentation page](./organizations.md) for more details. + ### Format > Field id: `format` From c674335a306710907f620d75e84beff0b25da17e Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 10 Jul 2024 14:20:53 +0200 Subject: [PATCH 053/378] fix(keywords): map keyword link to key --- .../src/lib/gn4/atomic-operations.spec.ts | 54 +++++++++++++++---- .../src/lib/gn4/atomic-operations.ts | 1 + .../src/lib/gn4/gn4.converter.spec.ts | 1 + .../src/lib/gn4/types/metadata.model.ts | 1 + 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/libs/api/metadata-converter/src/lib/gn4/atomic-operations.spec.ts b/libs/api/metadata-converter/src/lib/gn4/atomic-operations.spec.ts index 6fe2fb8111..b08ab2ee95 100644 --- a/libs/api/metadata-converter/src/lib/gn4/atomic-operations.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/atomic-operations.spec.ts @@ -30,29 +30,65 @@ describe('atomic operations', () => { id: '1', theme: 'theme', keywords: [ - { en: 'keyword1', fr: 'mot-clé1' }, - { en: 'keyword2', fr: 'mot-clé2' }, + { + en: 'keyword1', + fr: 'mot-clé1', + link: 'https://some-uri.org/thematique/categories/culture_tourisme_sport', + }, + { + en: 'keyword2', + fr: 'mot-clé2', + link: 'https://some-uri.org/thematique/categories/services_social_sante', + }, ], }, { id: '2', theme: 'place', keywords: [ - { en: 'keyword3', fr: 'mot-clé3' }, - { en: 'keyword4', fr: 'mot-clé4' }, + { + en: 'keyword3', + fr: 'mot-clé3', + link: 'https://some-uri.org/place/france', + }, + { + en: 'keyword4', + fr: 'mot-clé4', + link: 'https://some-uri.org/place/europe', + }, ], }, ] const expected = [ - { label: 'keyword1', type: 'theme', thesaurus: { id: '1' } }, - { label: 'keyword2', type: 'theme', thesaurus: { id: '1' } }, - { label: 'keyword3', type: 'place', thesaurus: { id: '2' } }, - { label: 'keyword4', type: 'place', thesaurus: { id: '2' } }, + { + label: 'keyword1', + type: 'theme', + key: 'https://some-uri.org/thematique/categories/culture_tourisme_sport', + thesaurus: { id: '1' }, + }, + { + label: 'keyword2', + type: 'theme', + key: 'https://some-uri.org/thematique/categories/services_social_sante', + thesaurus: { id: '1' }, + }, + { + label: 'keyword3', + type: 'place', + key: 'https://some-uri.org/place/france', + thesaurus: { id: '2' }, + }, + { + label: 'keyword4', + type: 'place', + key: 'https://some-uri.org/place/europe', + thesaurus: { id: '2' }, + }, ] expect(mapKeywords(thesauri, 'en')).toEqual(expected) }) - it('should default type to "other" if theme is not provided', () => { + it('should default type to "other" if theme is not provided and not break without link', () => { const thesauri = [ { id: '1', diff --git a/libs/api/metadata-converter/src/lib/gn4/atomic-operations.ts b/libs/api/metadata-converter/src/lib/gn4/atomic-operations.ts index c3a783cd08..6c58fbd8c9 100644 --- a/libs/api/metadata-converter/src/lib/gn4/atomic-operations.ts +++ b/libs/api/metadata-converter/src/lib/gn4/atomic-operations.ts @@ -120,6 +120,7 @@ export const mapKeywords = (thesauri: Thesaurus[], language: string) => { keywords.push({ label: selectTranslatedValue(keyword, language), type: getKeywordTypeFromKeywordTypeCode(rawThesaurus.theme), + ...(keyword.link && { key: keyword.link }), ...(thesaurus && { thesaurus }), }) } diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts index 6b093b140a..fceaa6e32d 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts @@ -954,6 +954,7 @@ describe('Gn4Converter', () => { name: 'GEMET themes', }, type: 'theme', + key: 'http://www.eionet.europa.eu/gemet/theme/1', }, { label: 'Unterschlupf', diff --git a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts index c71e9f6433..b181561fa8 100644 --- a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +++ b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts @@ -5,6 +5,7 @@ type MultilingualField = { [K in `lang${LangCode}`]: string } & { default: string + link?: string } type BooleanString = 'true' | 'false' From 5b86a2a276e916c3c5a2192935e662e7bdc40328 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 11 Jul 2024 17:20:00 +0200 Subject: [PATCH 054/378] feat: fix wfs link error on qgis --- .../src/e2e/datasetDetailPage.cy.ts | 198 +++++++++--------- .../src/e2e/dashboard.cy.ts | 2 +- .../src/lib/api-card/api-card.component.html | 58 ++--- 3 files changed, 133 insertions(+), 125 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 0e9813850e..3a88a79922 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -616,117 +616,125 @@ describe('record with file distributions', () => { describe('api cards', () => { beforeEach(() => { - cy.visit('/dataset/accroche_velos') - cy.get('gn-ui-api-card').first().as('firstCard') + cy.visit('/dataset/04bcec79-5b25-4b16-b635-73115f7456e4') + cy.get('gn-ui-api-card').eq(1).as('firstCard') }) it('should display the open panel button', () => { cy.get('@firstCard') .find('button') .children('mat-icon') + .eq(1) .should('have.text', 'more_horiz') }) it('should open and close the panel on click on open panel button', () => { - cy.get('@firstCard').click() + cy.get('@firstCard').find('button').eq(1).click() cy.get('gn-ui-record-api-form').should('be.visible') cy.screenshot({ capture: 'fullPage' }) - cy.get('@firstCard').click() + cy.get('@firstCard').find('button').eq(1).click() cy.get('gn-ui-record-api-form').should('not.be.visible') }) }) describe('api form', () => { - beforeEach(() => { - cy.visit('/dataset/accroche_velos') - cy.get('gn-ui-api-card').first().find('button').click() - cy.get('gn-ui-record-api-form').children('div').as('apiForm') - }) - it('should have request inputs', () => { - cy.get('@apiForm').find('gn-ui-text-input').should('have.length', 2) - cy.get('@apiForm').find('gn-ui-dropdown-selector').should('have.length', 1) - cy.get('@apiForm') - .children('div') - .first() - .children('div') - .first() - .find('button') - .should('have.length', 1) - cy.get('@apiForm').find('gn-ui-copy-text-button').should('have.length', 1) - }) - it('should change url on input change', () => { - cy.get('@apiForm') - .find('gn-ui-copy-text-button') - .find('input') - .invoke('val') - .then((url) => { - cy.get('@apiForm').find('gn-ui-text-input').first().clear() - cy.get('@apiForm').find('gn-ui-text-input').first().type('54') - cy.get('@apiForm') - .find('gn-ui-copy-text-button') - .find('input') - .invoke('val') - .should('not.eq', url) - .and('include', '54') - }) - }) - it('should set limit to zero on click on "All" button', () => { - cy.get('@apiForm').find('gn-ui-text-input').first().clear() - cy.get('@apiForm').find('gn-ui-text-input').first().type('54') - cy.get('@apiForm').find('input[type="checkbox"]').check() - cy.get('@apiForm').find('gn-ui-text-input').first().should('have.value', '') - }) - it('should reset all 3 inputs and link on click', () => { - cy.get('@apiForm').find('gn-ui-text-input').first().as('firstInput') - cy.get('@firstInput').clear() - cy.get('@firstInput').type('54') - - cy.get('@apiForm').find('gn-ui-text-input').eq(1).as('secondInput') - cy.get('@secondInput').clear() - cy.get('@secondInput').type('87') - - cy.get('@apiForm').find('gn-ui-dropdown-selector').as('dropdown') - cy.get('@dropdown').eq(0).selectDropdownOption('geojson') - - cy.get('@apiForm') - .find('gn-ui-copy-text-button') - .find('input') - .invoke('val') - .should('include', 'f=geojson&limit=54&offset=87') - - cy.get('@apiForm').children('div').first().find('button').first().click() - - cy.get('@firstInput').find('input').should('have.value', '') - cy.get('@secondInput').find('input').should('have.value', '') - cy.get('@apiForm') - .find('gn-ui-dropdown-selector') - .find('button') - .children('div') - .should('have.text', ' JSON ') - cy.get('@apiForm') - .find('gn-ui-copy-text-button') - .find('input') - .invoke('val') - .should('include', 'f=json&limit=-1') - }) - it('should close the panel on click', () => { - cy.get('gn-ui-record-api-form').prev().find('button').click() - cy.get('gn-ui-record-api-form').should('not.be.visible') - }) - it('should switch to other card url if card already open', () => { - cy.get('@apiForm') - .find('gn-ui-copy-text-button') - .find('input') - .invoke('val') - .then((url) => { - cy.get('@apiForm').find('gn-ui-text-input').first().clear() - cy.get('@apiForm').find('gn-ui-text-input').first().type('54') - cy.get('gn-ui-api-card').eq(1).find('button').click() - cy.get('@apiForm') - .find('gn-ui-copy-text-button') - .find('input') - .invoke('val') - .should('not.eq', url) - }) + describe('When the api link is ok', () => { + beforeEach(() => { + cy.visit('/dataset/accroche_velos') + cy.get('gn-ui-api-card').first().find('button').eq(1).click() + cy.get('gn-ui-record-api-form').children('div').as('apiForm') + }) + it('should have request inputs', () => { + cy.get('@apiForm').find('gn-ui-text-input').should('have.length', 2) + cy.get('@apiForm') + .find('gn-ui-dropdown-selector') + .should('have.length', 1) + cy.get('@apiForm') + .children('div') + .first() + .children('div') + .first() + .find('button') + .should('have.length', 1) + cy.get('@apiForm').find('gn-ui-copy-text-button').should('have.length', 1) + }) + it('should change url on input change', () => { + cy.get('@apiForm') + .find('gn-ui-copy-text-button') + .find('input') + .invoke('val') + .then((url) => { + cy.get('@apiForm').find('gn-ui-text-input').first().clear() + cy.get('@apiForm').find('gn-ui-text-input').first().type('54') + cy.get('@apiForm') + .find('gn-ui-copy-text-button') + .find('input') + .invoke('val') + .should('not.eq', url) + .and('include', '54') + }) + }) + it('should set limit to zero on click on "All" button', () => { + cy.get('@apiForm').find('gn-ui-text-input').first().clear() + cy.get('@apiForm').find('gn-ui-text-input').first().type('54') + cy.get('@apiForm').find('input[type="checkbox"]').check() + cy.get('@apiForm') + .find('gn-ui-text-input') + .first() + .should('have.value', '') + }) + it('should reset all 3 inputs and link on click', () => { + cy.get('@apiForm').find('gn-ui-text-input').first().as('firstInput') + cy.get('@firstInput').clear() + cy.get('@firstInput').type('54') + + cy.get('@apiForm').find('gn-ui-text-input').eq(1).as('secondInput') + cy.get('@secondInput').clear() + cy.get('@secondInput').type('87') + + cy.get('@apiForm').find('gn-ui-dropdown-selector').as('dropdown') + cy.get('@dropdown').eq(0).selectDropdownOption('geojson') + + cy.get('@apiForm') + .find('gn-ui-copy-text-button') + .find('input') + .invoke('val') + .should('include', 'f=geojson&limit=54&offset=87') + + cy.get('@apiForm').children('div').first().find('button').first().click() + + cy.get('@firstInput').find('input').should('have.value', '') + cy.get('@secondInput').find('input').should('have.value', '') + cy.get('@apiForm') + .find('gn-ui-dropdown-selector') + .find('button') + .children('div') + .should('have.text', ' JSON ') + cy.get('@apiForm') + .find('gn-ui-copy-text-button') + .find('input') + .invoke('val') + .should('include', 'f=json&limit=-1') + }) + it('should close the panel on click', () => { + cy.get('gn-ui-record-api-form').prev().find('button').click() + cy.get('gn-ui-record-api-form').should('not.be.visible') + }) + it('should switch to other card url if card already open', () => { + cy.get('@apiForm') + .find('gn-ui-copy-text-button') + .find('input') + .invoke('val') + .then((url) => { + cy.get('@apiForm').find('gn-ui-text-input').first().clear() + cy.get('@apiForm').find('gn-ui-text-input').first().type('54') + cy.get('gn-ui-api-card').eq(1).find('button').eq(1).click() + cy.get('@apiForm') + .find('gn-ui-copy-text-button') + .find('input') + .invoke('val') + .should('not.eq', url) + }) + }) }) }) diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 89322e4ee5..2e905e5fb5 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -36,7 +36,7 @@ describe('dashboard', () => { }) describe('sorting', () => { - it.only('should order the result list on click', () => { + it('should order the result list on click', () => { cy.visit('/catalog/search') cy.get('gn-ui-results-table') .find('.table-row-cell') 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 676de845c2..c381e4fbf5 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 @@ -1,7 +1,5 @@
    {{ link.accessServiceProtocol }} - - + more_horiz + +
    From 12f9e47ea5dcf06cf831d9be77b6814e92918217 Mon Sep 17 00:00:00 2001 From: mmohad Date: Fri, 12 Jul 2024 11:06:01 +0200 Subject: [PATCH 055/378] fix(getCap): false contion pass et to ou --- .../record/src/lib/ign-api-dl/ign-api-dl.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index 126ba2f05b..ec0060591b 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -204,16 +204,22 @@ export class IgnApiDlComponent implements OnInit { if (choicesTest) { return choicesTest } else { - while (choicesTest === undefined && response.data.pageCount > page) { + console.log(page) + + while (choicesTest === undefined || response.data.pageCount > page) { + console.log(choicesTest) ;[response] = await Promise.all([ axios.get(this.url.concat(`&pageSize=200&page=${page}`)), ]) + console.log(response.data.entry) + choicesTest = response.data.entry.filter( (element) => element['id'] == this.apiBaseUrl )[0] page += 1 } } + return choicesTest } async getFields() { From 79ac3f332cf9ce4f67206cbfc06e866c7cb56e10 Mon Sep 17 00:00:00 2001 From: mmohad Date: Fri, 12 Jul 2024 16:25:49 +0200 Subject: [PATCH 056/378] refacto(api): fix and refacto --- .../lib/ign-api-dl/ign-api-dl.component.ts | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index ec0060591b..a817a1ed35 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -14,6 +14,7 @@ import { iif, map, mergeMap, + startWith, switchMap, tap, } from 'rxjs' @@ -91,10 +92,10 @@ export class IgnApiDlComponent implements OnInit { } ngOnInit(): void { - this.getFields() this.bucketPromisesZone = [{ value: '', label: 'ZONE' }] this.bucketPromisesFormat = [{ value: '', label: 'FORMAT' }] this.bucketPromisesCrs = [{ value: '', label: 'CRS' }] + this.getFields() } apiQueryUrl$ = combineLatest([ @@ -107,6 +108,8 @@ export class IgnApiDlComponent implements OnInit { ]).pipe( map(([zone, format, editionDate, crs, pageSize, page]) => { let outputUrl + console.log(zone, format, editionDate, crs, pageSize, page) + if (this.apiBaseUrl) { const url = new URL(this.apiBaseUrl) // initialisation de l'url avec l'url de base const params = { @@ -125,15 +128,21 @@ export class IgnApiDlComponent implements OnInit { } } outputUrl = url.toString() + } else { + console.error('erreur apibaseUrl null') } return outputUrl }) + // startWith(() => this.apiBaseUrl) ) listFilteredProduct$ = this.apiQueryUrl$.pipe( mergeMap((url) => { + console.log(url) + return this.getFilteredProduct$(url).pipe( map((response) => response['entry']) + // startWith([]) ) }) ) @@ -141,6 +150,7 @@ export class IgnApiDlComponent implements OnInit { mergeMap((url) => { return this.getFilteredProduct$(url).pipe( map((response) => response['totalentries']) + // startWith(0) ) }) ) @@ -193,31 +203,19 @@ export class IgnApiDlComponent implements OnInit { async getCapabilities() { let page = 0 - let choicesTest = null - let [response] = await Promise.all([ - axios.get(this.url.concat(`&pageSize=200&page=${page}`)), - ]) - choicesTest = response.data.entry.filter( - (element) => element['id'] == this.apiBaseUrl - )[0] - - if (choicesTest) { - return choicesTest - } else { - console.log(page) - - while (choicesTest === undefined || response.data.pageCount > page) { - console.log(choicesTest) - ;[response] = await Promise.all([ - axios.get(this.url.concat(`&pageSize=200&page=${page}`)), - ]) - console.log(response.data.entry) - - choicesTest = response.data.entry.filter( - (element) => element['id'] == this.apiBaseUrl - )[0] - page += 1 - } + let choicesTest = undefined + let pageCount = 1 + + while (choicesTest === undefined && pageCount > page) { + const response = await axios.get( + this.url.concat(`&pagesize=200&page=${page}`) + ) + + choicesTest = response.data.entry.filter( + (element) => element['id'] == this.apiBaseUrl + )[0] + page += 1 + pageCount = response.data.pagecount } return choicesTest @@ -225,26 +223,32 @@ export class IgnApiDlComponent implements OnInit { async getFields() { this.choices = await this.getCapabilities() - this.bucketPromisesZone = this.choices.zone.map((bucket) => ({ + const tempZone = this.choices.zone.map((bucket) => ({ value: bucket.label, label: bucket.term, })) - this.bucketPromisesZone.sort((a, b) => (a.label > b.label ? 1 : -1)) - this.bucketPromisesZone.unshift({ value: 'null', label: 'ZONE' }) + tempZone.sort((a, b) => (a.label > b.label ? 1 : -1)) + tempZone.unshift({ value: 'null', label: 'ZONE' }) + + this.bucketPromisesZone = tempZone - this.bucketPromisesFormat = this.choices.format.map((bucket) => ({ + const tempFormat = this.choices.format.map((bucket) => ({ value: bucket.label, label: bucket.term, })) - this.bucketPromisesFormat.sort((a, b) => (a.label > b.label ? 1 : -1)) - this.bucketPromisesFormat.unshift({ value: 'null', label: 'FORMAT' }) + tempFormat.sort((a, b) => (a.label > b.label ? 1 : -1)) + tempFormat.unshift({ value: 'null', label: 'FORMAT' }) - this.bucketPromisesCrs = this.choices.category.map((bucket) => ({ + this.bucketPromisesFormat = tempFormat + + const tempCrs = this.choices.category.map((bucket) => ({ value: bucket.label, - label: bucket.term, + label: bucket.label, })) - this.bucketPromisesCrs.sort((a, b) => (a.label > b.label ? 1 : -1)) - this.bucketPromisesCrs.unshift({ value: 'null', label: 'CRS' }) + tempCrs.sort((a, b) => (a.label > b.label ? 1 : -1)) + tempCrs.unshift({ value: 'null', label: 'CRS' }) + + this.bucketPromisesCrs = tempCrs // eslint-disable-next-line @typescript-eslint/no-non-null-assertion } From 6f27406269538a8affb950fd942c32e9d9d18c19 Mon Sep 17 00:00:00 2001 From: mmohad Date: Fri, 12 Jul 2024 20:57:46 +0200 Subject: [PATCH 057/378] fix(ignApi): fix param api limit and page --- .../lib/ign-api-dl/ign-api-dl.component.ts | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts index a817a1ed35..903a0f1759 100644 --- a/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts +++ b/libs/feature/record/src/lib/ign-api-dl/ign-api-dl.component.ts @@ -5,22 +5,9 @@ import { OnInit, } from '@angular/core' import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' -import { - BehaviorSubject, - Observable, - combineLatest, - filter, - first, - iif, - map, - mergeMap, - startWith, - switchMap, - tap, -} from 'rxjs' -import { fromFetch } from 'rxjs/fetch' +import { BehaviorSubject, Observable, combineLatest, map, mergeMap } from 'rxjs' import { HttpClient } from '@angular/common/http' -import { Choice, DropdownChoice } from '@geonetwork-ui/ui/inputs' +import { Choice } from '@geonetwork-ui/ui/inputs' import axios from 'axios' export interface Label { @@ -68,15 +55,15 @@ export interface Field { export class IgnApiDlComponent implements OnInit { isOpen = false collapsed = false - initialPageSize = '200' + initialLimit = '50' apiBaseUrl: string editionDate$ = new BehaviorSubject('') zone$ = new BehaviorSubject('') format$ = new BehaviorSubject('') crs$ = new BehaviorSubject('') - pageSize$ = new BehaviorSubject(this.initialPageSize) - page$ = new BehaviorSubject('0') - size$ = new BehaviorSubject(this.initialPageSize) + limit$ = new BehaviorSubject(this.initialLimit) + page$ = new BehaviorSubject('1') + size$ = new BehaviorSubject(this.initialLimit) // a passer en config url = 'https://data.geopf.fr/telechargement/capabilities?outputFormat=application/json' @@ -103,13 +90,11 @@ export class IgnApiDlComponent implements OnInit { this.format$, this.editionDate$, this.crs$, - this.pageSize$, + this.limit$, this.page$, ]).pipe( - map(([zone, format, editionDate, crs, pageSize, page]) => { + map(([zone, format, editionDate, crs, limit, page]) => { let outputUrl - console.log(zone, format, editionDate, crs, pageSize, page) - if (this.apiBaseUrl) { const url = new URL(this.apiBaseUrl) // initialisation de l'url avec l'url de base const params = { @@ -117,7 +102,7 @@ export class IgnApiDlComponent implements OnInit { format: format, editionDate: editionDate, crs: crs, - pageSize: pageSize, + limit: limit, page: page, } // initialisation des paramètres de filtres for (const [key, value] of Object.entries(params)) { @@ -189,11 +174,11 @@ export class IgnApiDlComponent implements OnInit { this.format$.next('null') this.crs$.next('null') this.page$.next('0') - this.size$.next(this.initialPageSize) + this.size$.next(this.initialLimit) } moreResult(): void { const page = Number(this.page$.value) + 1 - const size = (page + 1) * Number(this.initialPageSize) + const size = (page + 1) * Number(this.initialLimit) this.size$.next(String(size)) this.page$.next(String(page)) } @@ -208,7 +193,7 @@ export class IgnApiDlComponent implements OnInit { while (choicesTest === undefined && pageCount > page) { const response = await axios.get( - this.url.concat(`&pagesize=200&page=${page}`) + this.url.concat(`&limit=200&page=${page}`) ) choicesTest = response.data.entry.filter( From c0095de02d7d1f36b2cf7127fb55f27839e7f9dc Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Mon, 15 Jul 2024 09:10:16 +0200 Subject: [PATCH 058/378] feat(ui): make component standalone, simplify on change logic, remove imports in ui-inputs module --- .../lib/switch-toggle/switch-toggle.component.ts | 15 ++++++++------- .../lib/switch-toggle/switch-toggle.stories.ts | 3 ++- libs/ui/inputs/src/lib/ui-inputs.module.ts | 4 ---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts index 81dc52fb09..466c21a262 100644 --- a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, @@ -5,6 +6,7 @@ import { Input, Output, } from '@angular/core' +import { MatButtonToggleModule } from '@angular/material/button-toggle' export type SwitchToggleOption = { label: string @@ -17,6 +19,8 @@ export type SwitchToggleOption = { templateUrl: './switch-toggle.component.html', styleUrls: ['./switch-toggle.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [MatButtonToggleModule, CommonModule], }) export class SwitchToggleComponent { @Input() options: SwitchToggleOption[] @@ -25,13 +29,10 @@ export class SwitchToggleComponent { @Output() selectedValue = new EventEmitter() onChange(selectedOption: SwitchToggleOption) { - this.options.forEach((option) => { - if (option.value === selectedOption.value) { - option.checked = true - } else { - option.checked = false - } - }) + this.options.find( + (option) => option.value === selectedOption.value + ).checked = true + this.selectedValue.emit(selectedOption) } } diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts index 0a7082d251..7c265a6cd0 100644 --- a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.stories.ts @@ -1,6 +1,7 @@ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' import { SwitchToggleComponent } from './switch-toggle.component' import { MatButtonToggleModule } from '@angular/material/button-toggle' +import { CommonModule } from '@angular/common' export default { title: 'Inputs/SwitchToggle', @@ -8,7 +9,7 @@ export default { decorators: [ moduleMetadata({ declarations: [], - imports: [MatButtonToggleModule], + imports: [SwitchToggleComponent, MatButtonToggleModule, CommonModule], }), ], } as Meta diff --git a/libs/ui/inputs/src/lib/ui-inputs.module.ts b/libs/ui/inputs/src/lib/ui-inputs.module.ts index 7f224e91b5..8f2e7b92bd 100644 --- a/libs/ui/inputs/src/lib/ui-inputs.module.ts +++ b/libs/ui/inputs/src/lib/ui-inputs.module.ts @@ -33,8 +33,6 @@ import { MatDatepickerModule } from '@angular/material/datepicker' import { MatNativeDateModule } from '@angular/material/core' import { EditableLabelDirective } from './editable-label/editable-label.directive' import { ImageInputComponent } from './image-input/image-input.component' -import { SwitchToggleComponent } from './switch-toggle/switch-toggle.component' -import { MatButtonToggleModule } from '@angular/material/button-toggle' @NgModule({ declarations: [ @@ -48,7 +46,6 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle' CopyTextButtonComponent, CheckboxComponent, SearchInputComponent, - SwitchToggleComponent, ], imports: [ CommonModule, @@ -76,7 +73,6 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle' DateRangePickerComponent, CheckToggleComponent, BadgeComponent, - MatButtonToggleModule, ], exports: [ DropdownSelectorComponent, From e50812edf63ce4f8a3d457f50c56a433e12b4ef5 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Mon, 15 Jul 2024 09:28:37 +0200 Subject: [PATCH 059/378] chore: fix import in test --- .../src/lib/switch-toggle/switch-toggle.component.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts index d2488596dd..2253cad174 100644 --- a/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts +++ b/libs/ui/inputs/src/lib/switch-toggle/switch-toggle.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { SwitchToggleComponent } from './switch-toggle.component' import { MatButtonToggleModule } from '@angular/material/button-toggle' +import { CommonModule } from '@angular/common' describe('SwitchToggleComponent', () => { let component: SwitchToggleComponent @@ -9,8 +10,8 @@ describe('SwitchToggleComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [SwitchToggleComponent], - imports: [MatButtonToggleModule], + declarations: [], + imports: [MatButtonToggleModule, SwitchToggleComponent, CommonModule], }) fixture = TestBed.createComponent(SwitchToggleComponent) component = fixture.componentInstance From 5bb338b5e258887855d30f5e1c2d847074a8cda7 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Mon, 15 Jul 2024 10:14:01 +0200 Subject: [PATCH 060/378] feat: add condition to display share tool and comment bootstrap mention --- .../app/record/record-metadata/record-metadata.component.html | 1 + apps/webcomponents/src/app/webcomponents.module.ts | 2 +- .../data-view-permalink/data-view-permalink.component.spec.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html index 8cd37d1680..0a28b804b9 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html @@ -110,6 +110,7 @@
    BaseComponent, string][] = [ }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - bootstrap: [AppComponent], + // bootstrap: [AppComponent], }) export class WebcomponentsModule { constructor(private injector: Injector) { 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 848ee4bac4..381b66ecbf 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 @@ -4,7 +4,7 @@ import { DataViewPermalinkComponent, WEB_COMPONENT_EMBEDDER_URL, } from './data-view-permalink.component' -import { BehaviorSubject, firstValueFrom, lastValueFrom, takeLast } from 'rxjs' +import { BehaviorSubject, firstValueFrom } from 'rxjs' import { MdViewFacade } from '../state' import { Component, Input } from '@angular/core' import { TranslateModule } from '@ngx-translate/core' From 13c159aa13b25697aa76a2af6be71a4a62d7091e Mon Sep 17 00:00:00 2001 From: cde-barros Date: Mon, 15 Jul 2024 17:03:29 +0200 Subject: [PATCH 061/378] =?UTF-8?q?fix=20issue=2025=2020=20et=20ajout=20ta?= =?UTF-8?q?g=20Beta=20dans=20l'ent=C3=AAte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/datahub/src/app/app.component.html | 1 + .../app/home/search/search-page/search-page.component.scss | 3 +++ .../src/app/home/search/search-page/search-page.component.ts | 1 + conf/default.toml | 4 ++-- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/datahub/src/app/app.component.html b/apps/datahub/src/app/app.component.html index ad6c36c01d..af631638c4 100644 --- a/apps/datahub/src/app/app.component.html +++ b/apps/datahub/src/app/app.component.html @@ -50,6 +50,7 @@ link: 'https://cartes.gouv.fr/tableau-de-bord' } ]" + [beta]="true" > Date: Tue, 2 Jul 2024 13:13:20 +0200 Subject: [PATCH 062/378] docs: add remote debug port to gn, document --- docs/assets/intellij-create-debug-config.png | Bin 0 -> 150282 bytes docs/assets/intellij-edit-configs.png | Bin 0 -> 48616 bytes docs/assets/intellij-remote-debug.png | Bin 0 -> 52155 bytes docs/guide/dev-environment.md | 15 +++++++++++++++ support-services/docker-compose.yml | 10 ++++++++++ 5 files changed, 25 insertions(+) create mode 100644 docs/assets/intellij-create-debug-config.png create mode 100644 docs/assets/intellij-edit-configs.png create mode 100644 docs/assets/intellij-remote-debug.png diff --git a/docs/assets/intellij-create-debug-config.png b/docs/assets/intellij-create-debug-config.png new file mode 100644 index 0000000000000000000000000000000000000000..c244f29428c0d1caf4c0ee886080649f9d134beb GIT binary patch literal 150282 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
    aM$%a=JxF&V@FcgTBF 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 literal 0 HcmV?d00001 diff --git a/docs/assets/intellij-edit-configs.png b/docs/assets/intellij-edit-configs.png new file mode 100644 index 0000000000000000000000000000000000000000..2cd4d78387e8a7323962e8ba67f555a49b82471c GIT binary patch literal 48616 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 a1=b={`xrb#k~;MdKObaNq{}2tzx*Gr#_xy# literal 0 HcmV?d00001 diff --git a/docs/assets/intellij-remote-debug.png b/docs/assets/intellij-remote-debug.png new file mode 100644 index 0000000000000000000000000000000000000000..1aa1b023a7ebb2092c03408d2cdf237967b2d940 GIT binary patch literal 52155 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

    )6Xot|4QD)KZy`F@c$mf+SMqh&<%YmO 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 + -Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF + -Djava.security.egd=file:/dev/./urandom -Djava.awt.headless=true + -Xms512M -Xss512M -Xmx2G -XX:+UseConcMarkSweepGC + -Dgeonetwork.resources.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/data/resources + -Dgeonetwork.data.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/data + -Dgeonetwork.codeList.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/config/codelist + -Dgeonetwork.schema.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/config/schema_plugins + -Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005 depends_on: database: condition: service_healthy @@ -98,6 +107,7 @@ services: - geonetwork_data:/var/lib/jetty/webapps/geonetwork/WEB-INF/data/ ports: - '8080:8080' + - '5005:5005' init-pipeline: image: geonetwork/geonetwork-ui-tools-pipelines:latest From d0522e96cd11e1cd7ee72da07b6b7a37377a6ff7 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Mon, 22 Jul 2024 14:50:46 +0200 Subject: [PATCH 063/378] feat(header-record): remove background color from geo data badge to make it more readable in a generic way --- .../record/header-record/header-record.component.html | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/datahub/src/app/record/header-record/header-record.component.html b/apps/datahub/src/app/record/header-record/header-record.component.html index 468272bcc0..6edea676e0 100644 --- a/apps/datahub/src/app/record/header-record/header-record.component.html +++ b/apps/datahub/src/app/record/header-record/header-record.component.html @@ -38,19 +38,16 @@ {{ metadata.title }}

    -
    +
    my_location

    record.metadata.type

    -
    +

    record.metadata.lastUpdate

    From 8f36820f06b535a7a04a5799635c2e766146014c Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Wed, 3 Jul 2024 11:51:42 +0200 Subject: [PATCH 064/378] feat(ME): adapt display of sidebar, add black bar, org name and org logo, user logo and shut icon, fix and add translations --- .../dashboard/sidebar/sidebar.component.html | 61 ++++++++++++++++++- .../dashboard/sidebar/sidebar.component.ts | 36 ++++++++++- .../src/assets/editor-logo.svg | 10 +++ .../src/assets/system-shut.svg | 11 ++++ .../user-preview/user-preview.component.html | 2 +- translations/de.json | 1 + translations/en.json | 1 + translations/es.json | 1 + translations/fr.json | 1 + translations/it.json | 1 + translations/nl.json | 1 + translations/pt.json | 1 + translations/sk.json | 1 + 13 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 apps/metadata-editor/src/assets/editor-logo.svg create mode 100644 apps/metadata-editor/src/assets/system-shut.svg 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 7487ff5bf0..a6529d42fe 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html @@ -1,5 +1,62 @@ -
    -
    +
    +
    +
    +
    + Editor logo +
    +
    +
    + editor.sidebar.menu.editor +
    +
    +
    + + +
    + + + + + Log out + +
    +
    +
    + +
    + + + + Organization logo + {{ organisations[0].name }} + + +
    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 4f9d569be7..c2bbfe3026 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.ts +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.ts @@ -1,7 +1,14 @@ import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component } from '@angular/core' +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 { LetDirective } from '@ngrx/component' +import { UiElementsModule } from '@geonetwork-ui/ui/elements' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' +import { Observable, combineLatest } from 'rxjs' +import { Organization } from '@geonetwork-ui/common/domain/model/record' @Component({ selector: 'md-editor-sidebar', @@ -9,6 +16,29 @@ import { DashboardMenuComponent } from '../dashboard-menu/dashboard-menu.compone styleUrls: ['./sidebar.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [DashboardMenuComponent, CommonModule, TranslateModule], + imports: [ + DashboardMenuComponent, + CommonModule, + TranslateModule, + LetDirective, + UiElementsModule, + ], }) -export class SidebarComponent {} +export class SidebarComponent implements OnInit { + public placeholder$ = this.avatarService.getPlaceholder() + organisations$: Observable + + constructor( + public platformService: PlatformServiceInterface, + private avatarService: AvatarServiceInterface, + public organisationsService: OrganizationsServiceInterface + ) {} + + ngOnInit(): void { + this.organisations$ = combineLatest( + this.organisationsService.organisations$, + this.platformService.getMe(), + (orgs, me) => orgs.filter((org) => org.name === me.organisation) + ) + } +} diff --git a/apps/metadata-editor/src/assets/editor-logo.svg b/apps/metadata-editor/src/assets/editor-logo.svg new file mode 100644 index 0000000000..a20169b45b --- /dev/null +++ b/apps/metadata-editor/src/assets/editor-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/metadata-editor/src/assets/system-shut.svg b/apps/metadata-editor/src/assets/system-shut.svg new file mode 100644 index 0000000000..ce81ac10de --- /dev/null +++ b/apps/metadata-editor/src/assets/system-shut.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/libs/ui/elements/src/lib/user-preview/user-preview.component.html b/libs/ui/elements/src/lib/user-preview/user-preview.component.html index 327eefbf84..361fa66a73 100644 --- a/libs/ui/elements/src/lib/user-preview/user-preview.component.html +++ b/libs/ui/elements/src/lib/user-preview/user-preview.component.html @@ -1,6 +1,6 @@
    Date: Thu, 4 Jul 2024 09:42:39 +0200 Subject: [PATCH 065/378] fix(test): Provide services in test --- .../sidebar/sidebar.component.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 08fcdb3314..362fb48249 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 @@ -6,11 +6,26 @@ import { ActivatedRoute } from '@angular/router' import { TranslateModule } from '@ngx-translate/core' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' +import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' class RecordsRepositoryMock { getAllDrafts = jest.fn().mockReturnValue(of(DATASET_RECORDS)) } +class PlatformServiceMock { + getMe = jest.fn().mockReturnValue(of({ organisation: 'organisation' })) +} + +class AvatarServiceInterfaceMock { + getPlaceholder = () => of('http://placeholder.com') +} + +class OrganisationsServiceMock { + organisations$ = of([{ name: 'organisation' }]) +} + describe('SidebarComponent', () => { let component: SidebarComponent let fixture: ComponentFixture @@ -27,6 +42,18 @@ describe('SidebarComponent', () => { provide: RecordsRepositoryInterface, useClass: RecordsRepositoryMock, }, + { + provide: PlatformServiceInterface, + useClass: PlatformServiceMock, + }, + { + provide: AvatarServiceInterface, + useClass: AvatarServiceInterfaceMock, + }, + { + provide: OrganizationsServiceInterface, + useClass: OrganisationsServiceMock, + }, ], schemas: [NO_ERRORS_SCHEMA], }) From 2c0e92bf2f2a42139de3dad5b2cb07c9e3dc1966 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Thu, 4 Jul 2024 09:44:25 +0200 Subject: [PATCH 066/378] fix errors reading undefined --- .../src/app/dashboard/sidebar/sidebar.component.html | 2 +- .../src/app/dashboard/sidebar/sidebar.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 a6529d42fe..69efa62300 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html @@ -41,7 +41,7 @@
    - + orgs.filter((org) => org.name === me.organisation) + (orgs, me) => orgs.filter((org) => org.name === me?.organisation) ) } } From 60f463f9d5694c80563b7ed5f2159201db052015 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Mon, 22 Jul 2024 15:43:08 +0200 Subject: [PATCH 067/378] Change let directive to ngIf --- .../src/app/dashboard/sidebar/sidebar.component.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 69efa62300..cf5229c4e4 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.html @@ -13,7 +13,7 @@
    - +
    Date: Wed, 10 Jul 2024 16:49:25 +0200 Subject: [PATCH 068/378] feature(datahub): add Petrona font to the project./ --- assets-common/css/default-fonts.css | 8 ++++++++ assets-common/fonts/Petrona-Regular.woff2 | Bin 0 -> 21296 bytes tailwind.base.config.js | 1 + 3 files changed, 9 insertions(+) create mode 100644 assets-common/fonts/Petrona-Regular.woff2 diff --git a/assets-common/css/default-fonts.css b/assets-common/css/default-fonts.css index fc8d76c604..34941cbd8c 100644 --- a/assets-common/css/default-fonts.css +++ b/assets-common/css/default-fonts.css @@ -1,3 +1,11 @@ +/* Petrona */ +@font-face { + font-family: 'Petrona'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(../fonts/Petrona-Regular.woff2) format('woff2'); +} /* arabic */ @font-face { font-family: 'Readex Pro'; diff --git a/assets-common/fonts/Petrona-Regular.woff2 b/assets-common/fonts/Petrona-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bac09590b6045853dd96334768991328183a94b7 GIT binary patch literal 21296 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 NtxT>8=|*}0005{qHc Date: Wed, 10 Jul 2024 16:50:09 +0200 Subject: [PATCH 069/378] feature(datahub): add custom filter pipe. --- apps/metadata-editor/src/app/pipes/filter.pipe.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 apps/metadata-editor/src/app/pipes/filter.pipe.ts diff --git a/apps/metadata-editor/src/app/pipes/filter.pipe.ts b/apps/metadata-editor/src/app/pipes/filter.pipe.ts new file mode 100644 index 0000000000..4cbff21914 --- /dev/null +++ b/apps/metadata-editor/src/app/pipes/filter.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ + name: 'filter', + standalone: true, +}) +export class FilterPipe implements PipeTransform { + transform(array: any[], index: number): any { + return array.find((item) => item.index === index) + } +} From 93b253875d32a99f310ed09751a86e7baf7a60db Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 10 Jul 2024 16:52:41 +0200 Subject: [PATCH 070/378] chore(datahub/metadata-editor): update TypeScript config to target ES2020 - Updated tsconfig.json to set target and lib to ES2020 - Ensured compatibility with modern JavaScript features like Array.prototype.flat --- apps/metadata-editor/tsconfig.json | 8 +++++++- libs/feature/editor/tsconfig.json | 8 +++++++- libs/feature/editor/tsconfig.lib.json | 11 +++++++++-- libs/util/shared/tsconfig.lib.json | 6 +++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/metadata-editor/tsconfig.json b/apps/metadata-editor/tsconfig.json index 310da87429..d2c6daf3e1 100644 --- a/apps/metadata-editor/tsconfig.json +++ b/apps/metadata-editor/tsconfig.json @@ -18,7 +18,13 @@ "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "target": "es2020" + "target": "es2020", + "lib": [ + "dom", + "es2020", + "dom.iterable" + ], + "downlevelIteration": true }, "angularCompilerOptions": { "strictInjectionParameters": true, diff --git a/libs/feature/editor/tsconfig.json b/libs/feature/editor/tsconfig.json index aeb1c9ace3..cdea0bdb38 100644 --- a/libs/feature/editor/tsconfig.json +++ b/libs/feature/editor/tsconfig.json @@ -15,7 +15,13 @@ "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "target": "es2020" + "target": "es2020", + "lib": [ + "dom", + "es2020", + "dom.iterable" + ], + "downlevelIteration": true }, "angularCompilerOptions": { "strictInjectionParameters": true, diff --git a/libs/feature/editor/tsconfig.lib.json b/libs/feature/editor/tsconfig.lib.json index 1f4078133c..7ca4377996 100644 --- a/libs/feature/editor/tsconfig.lib.json +++ b/libs/feature/editor/tsconfig.lib.json @@ -7,7 +7,12 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"] + "lib": [ + "dom", + "es2020", + "dom.iterable" + ], + "downlevelIteration": true }, "exclude": [ "src/test-setup.ts", @@ -15,5 +20,7 @@ "**/*.test.ts", "jest.config.ts" ], - "include": ["**/*.ts"] + "include": [ + "**/*.ts" + ] } diff --git a/libs/util/shared/tsconfig.lib.json b/libs/util/shared/tsconfig.lib.json index 1f4078133c..9bb502669e 100644 --- a/libs/util/shared/tsconfig.lib.json +++ b/libs/util/shared/tsconfig.lib.json @@ -7,7 +7,11 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"] + "lib": [ + "dom", + "es2020", + "dom.iterable" + ] }, "exclude": [ "src/test-setup.ts", From fac18eddd84e207ffd23e0e528415709dc797f5c Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 10 Jul 2024 17:03:12 +0200 Subject: [PATCH 071/378] feat(metadata-editor): organize fields into pages and sections. --- .../breadcrumbs/page-selector.component.css | 0 .../breadcrumbs/page-selector.component.html | 39 +++ .../page-selector.component.spec.ts | 26 ++ .../breadcrumbs/page-selector.component.ts | 30 +++ .../top-toolbar/top-toolbar.component.html | 2 +- .../src/app/edit/edit-page.component.html | 45 +++- .../src/app/edit/edit-page.component.spec.ts | 12 +- .../src/app/edit/edit-page.component.ts | 44 +++- .../src/app/pipes/filter.pipe.ts | 11 - apps/metadata-editor/tsconfig.json | 6 +- libs/feature/editor/src/index.ts | 1 + .../editor/src/lib/+state/editor.reducer.ts | 4 +- .../src/lib/+state/editor.selectors.spec.ts | 78 +++--- .../editor/src/lib/+state/editor.selectors.ts | 19 +- .../form-field/form-field.component.html | 18 +- .../form-field/form-field.component.spec.ts | 1 - .../form-field/form-field.component.ts | 3 +- .../form-field/form-field.model.ts | 43 ---- .../record-form/form-field/index.ts | 1 - .../record-form/record-form.component.html | 54 +++- .../record-form/record-form.component.spec.ts | 5 +- .../record-form/record-form.component.ts | 28 ++- .../editor/src/lib/expressions.spec.ts | 11 +- libs/feature/editor/src/lib/expressions.ts | 2 +- libs/feature/editor/src/lib/fields.config.ts | 236 +++++++++++++----- .../src/lib/fixtures/editor.fixtures.ts | 158 ++++++++++++ libs/feature/editor/src/lib/fixtures/index.ts | 1 + .../src/lib/models/editor-config.model.ts | 55 ++++ .../editor/src/lib/models/fields.model.ts | 29 --- libs/feature/editor/src/lib/models/index.ts | 1 + .../editor/src/lib/services/editor.service.ts | 12 +- libs/feature/editor/tsconfig.json | 6 +- libs/feature/editor/tsconfig.lib.json | 10 +- libs/util/shared/tsconfig.lib.json | 6 +- translations/de.json | 28 +++ translations/en.json | 28 +++ translations/es.json | 28 +++ translations/fr.json | 28 +++ translations/it.json | 28 +++ translations/nl.json | 28 +++ translations/pt.json | 28 +++ translations/sk.json | 28 +++ 42 files changed, 936 insertions(+), 285 deletions(-) create mode 100644 apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.css create mode 100644 apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html create mode 100644 apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.spec.ts create mode 100644 apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.ts delete mode 100644 apps/metadata-editor/src/app/pipes/filter.pipe.ts delete mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field.model.ts create mode 100644 libs/feature/editor/src/lib/fixtures/editor.fixtures.ts create mode 100644 libs/feature/editor/src/lib/fixtures/index.ts create mode 100644 libs/feature/editor/src/lib/models/editor-config.model.ts delete mode 100644 libs/feature/editor/src/lib/models/fields.model.ts diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.css b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html new file mode 100644 index 0000000000..b38938f323 --- /dev/null +++ b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html @@ -0,0 +1,39 @@ +
    + +
    +
    + +
    + {{ index }} +
    +
    + {{ page.labelKey }} +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.spec.ts b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.spec.ts new file mode 100644 index 0000000000..0e6f756aed --- /dev/null +++ b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { TranslateModule } from '@ngx-translate/core' +import { PageSelectorComponent } from './page-selector.component' +import { EDITOR_CONFIG } from '@geonetwork-ui/feature/editor' + +describe('BreadcrumbsComponent', () => { + let component: PageSelectorComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [], + }).compileComponents() + + fixture = TestBed.createComponent(PageSelectorComponent) + component = fixture.componentInstance + component.pages = EDITOR_CONFIG().pages + component.selectedPage = 0 + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.ts b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.ts new file mode 100644 index 0000000000..7b3c3df71c --- /dev/null +++ b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.ts @@ -0,0 +1,30 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { CommonModule } from '@angular/common' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' +import { EditorFieldPage } from '@geonetwork-ui/feature/editor' + +@Component({ + selector: 'md-editor-page-selector', + standalone: true, + imports: [CommonModule, ButtonComponent, TranslateModule], + templateUrl: './page-selector.component.html', + styleUrls: ['./page-selector.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PageSelectorComponent { + @Input() selectedPage = 0 + @Input() pages: EditorFieldPage[] + + @Output() selectedPageChange = new EventEmitter() + + pageSectionClickHandler(index: number) { + this.selectedPageChange.emit(index) + } +} 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 aab1948240..89cd41f9d4 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,4 +1,4 @@ -
    +
    side_navigation 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 9fc5cfabfe..d93d4aab53 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.html +++ b/apps/metadata-editor/src/app/edit/edit-page.component.html @@ -1,11 +1,38 @@ -
    -
    - -
    -
    -
    - + +
    +
    + + +
    +
    +
    + +
    + +
    +
    + + {{ + selectedPage === 0 + ? ('editor.record.form.bottomButtons.comeBackLater' | translate) + : ('editor.record.form.bottomButtons.previous' | translate) + }} + + editor.record.form.bottomButtons.next
    -
    -
    + diff --git a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts index 401d9541ce..36fc290d2d 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts @@ -1,12 +1,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { EditPageComponent } from './edit-page.component' import { ActivatedRoute, Router } from '@angular/router' -import { EditorFacade } from '@geonetwork-ui/feature/editor' +import { EDITOR_CONFIG, EditorFacade } from '@geonetwork-ui/feature/editor' import { NO_ERRORS_SCHEMA } from '@angular/core' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { BehaviorSubject, Subject } from 'rxjs' import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { TranslateModule } from '@ngx-translate/core' +import { FindPipe } from '../pipes/filter.pipe' +import { PageSelectorComponent } from './components/breadcrumbs/page-selector.component' const getRoute = () => ({ snapshot: { @@ -25,6 +27,7 @@ class RouterMock { class EditorFacadeMock { record$ = new BehaviorSubject(DATASET_RECORDS[0]) + recordFields$ = new BehaviorSubject(EDITOR_CONFIG()) openRecord = jest.fn() saveError$ = new Subject() saveSuccess$ = new Subject() @@ -42,7 +45,12 @@ describe('EditPageComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EditPageComponent, TranslateModule.forRoot()], + imports: [ + EditPageComponent, + TranslateModule.forRoot(), + FindPipe, + PageSelectorComponent, + ], schemas: [NO_ERRORS_SCHEMA], providers: [ { 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 7327286a2c..8586f05362 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.ts @@ -3,6 +3,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { EditorFacade, + EditorFieldPage, RecordFormComponent, } from '@geonetwork-ui/feature/editor' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' @@ -13,8 +14,14 @@ import { NotificationsContainerComponent, NotificationsService, } from '@geonetwork-ui/feature/notifications' -import { TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' import { filter, Subscription, take } from 'rxjs' +import { PageSelectorComponent } from './components/breadcrumbs/page-selector.component' +import { marker } from '@biesbjerg/ngx-translate-extract-marker' + +marker('editor.record.form.bottomButtons.comeBackLater') +marker('editor.record.form.bottomButtons.previous') +marker('editor.record.form.bottomButtons.next') @Component({ selector: 'md-editor-edit', @@ -29,18 +36,30 @@ import { filter, Subscription, take } from 'rxjs' PublishButtonComponent, TopToolbarComponent, NotificationsContainerComponent, + PageSelectorComponent, + TranslateModule, ], }) export class EditPageComponent implements OnInit, OnDestroy { subscription = new Subscription() + fields$ = this.facade.recordFields$ + totalPages = 0 + selectedPage = 0 + constructor( private route: ActivatedRoute, private facade: EditorFacade, private notificationsService: NotificationsService, private translateService: TranslateService, private router: Router - ) {} + ) { + this.subscription.add( + this.fields$.subscribe((fields) => { + this.totalPages = fields.pages.length + }) + ) + } ngOnInit(): void { const [currentRecord, currentRecordSource, currentRecordAlreadySaved] = @@ -109,4 +128,25 @@ export class EditPageComponent implements OnInit, OnDestroy { ngOnDestroy() { this.subscription.unsubscribe() } + + getSelectedPageFields(pages: EditorFieldPage[]) { + return pages[this.selectedPage] + } + + previousPageButtonHandler() { + if (this.selectedPage === 0) { + this.router.navigate(['catalog', 'search']) + } else { + this.selectedPage-- + } + } + + nextPageButtonHandler() { + if (this.selectedPage === this.totalPages - 1) return + this.selectedPage++ + } + + selectedPageChange(index: number) { + this.selectedPage = index + } } diff --git a/apps/metadata-editor/src/app/pipes/filter.pipe.ts b/apps/metadata-editor/src/app/pipes/filter.pipe.ts deleted file mode 100644 index 4cbff21914..0000000000 --- a/apps/metadata-editor/src/app/pipes/filter.pipe.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' - -@Pipe({ - name: 'filter', - standalone: true, -}) -export class FilterPipe implements PipeTransform { - transform(array: any[], index: number): any { - return array.find((item) => item.index === index) - } -} diff --git a/apps/metadata-editor/tsconfig.json b/apps/metadata-editor/tsconfig.json index d2c6daf3e1..54f2b40f0e 100644 --- a/apps/metadata-editor/tsconfig.json +++ b/apps/metadata-editor/tsconfig.json @@ -19,11 +19,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "target": "es2020", - "lib": [ - "dom", - "es2020", - "dom.iterable" - ], + "lib": ["dom", "es2020", "dom.iterable"], "downlevelIteration": true }, "angularCompilerOptions": { diff --git a/libs/feature/editor/src/index.ts b/libs/feature/editor/src/index.ts index 5e6dd85211..c95afdefd4 100644 --- a/libs/feature/editor/src/index.ts +++ b/libs/feature/editor/src/index.ts @@ -9,3 +9,4 @@ export * from './lib/components/record-form/record-form.component' export * from './lib/components/wizard/wizard.component' export * from './lib/components/wizard-field/wizard-field.component' export * from './lib/components/wizard-summarize/wizard-summarize.component' +export * from './lib/fixtures' diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts index 425e3d0c41..506f3436fb 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts @@ -2,7 +2,7 @@ import { Action, createReducer, on } from '@ngrx/store' import * as EditorActions from './editor.actions' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { SaveRecordError } from './editor.models' -import { EditorFieldsConfig } from '../models/fields.model' +import { EditorConfig } from '../models' import { DEFAULT_FIELDS } from '../fields.config' export const EDITOR_FEATURE_KEY = 'editor' @@ -22,7 +22,7 @@ export interface EditorState { saving: boolean saveError: SaveRecordError | null changedSinceSave: boolean - fieldsConfig: EditorFieldsConfig + fieldsConfig: EditorConfig } export interface EditorPartialState { 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 0d61773f97..e3ee07bfd9 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts @@ -58,45 +58,26 @@ describe('Editor Selectors', () => { describe('selectRecordFields', () => { it('should return the config and value for each field', () => { const result = EditorSelectors.selectRecordFields(state) - expect(result).toEqual([ - { - config: DEFAULT_FIELDS[0], - value: DATASET_RECORDS[0].title, - }, - { - config: DEFAULT_FIELDS[1], - value: DATASET_RECORDS[0].abstract, - }, - { - config: DEFAULT_FIELDS[2], - value: DATASET_RECORDS[0].uniqueIdentifier, - }, - { - config: DEFAULT_FIELDS[3], - value: DATASET_RECORDS[0].recordUpdated, - }, - { - config: DEFAULT_FIELDS[4], - value: DATASET_RECORDS[0].licenses, - }, - { - config: DEFAULT_FIELDS[5], - value: DATASET_RECORDS[0].resourceUpdated, - }, - { - config: DEFAULT_FIELDS[6], - value: DATASET_RECORDS[0].updateFrequency, - }, - { - config: DEFAULT_FIELDS[7], - value: DATASET_RECORDS[0].temporalExtents, - }, - { - config: DEFAULT_FIELDS[8], - value: DATASET_RECORDS[0].keywords, - }, - ]) + + const actualSections = result.pages.map((page) => page.sections).flat() + + const expectedSections = DEFAULT_FIELDS.pages + .map((page) => page.sections) + .flat() + + expect(actualSections).toEqual(expectedSections) + + const actualFields = actualSections + .map((section) => section.fields) + .flat() + + const expectedFields = expectedSections + .map((section) => section.fields) + .flat() + + expect(actualFields).toEqual(expectedFields) }) + it('should not coerce falsy values to null', () => { const result = EditorSelectors.selectRecordFields({ ...state, @@ -109,14 +90,19 @@ describe('Editor Selectors', () => { }, }, }) - expect(result).toContainEqual({ - config: DEFAULT_FIELDS[0], - value: '', - }) - expect(result).toContainEqual({ - config: DEFAULT_FIELDS[1], - value: '', - }) + + const resultFields = result.pages + .flatMap((page) => page.sections) + .flatMap((section) => section.fields) + + const abstractField = resultFields.find( + (field) => field.model === 'abstract' + ) + + const titleField = resultFields.find((field) => field.model === 'title') + + expect(abstractField.value).toEqual('') + expect(titleField.value).toEqual('') }) }) }) diff --git a/libs/feature/editor/src/lib/+state/editor.selectors.ts b/libs/feature/editor/src/lib/+state/editor.selectors.ts index e9a601da80..3661ace5f1 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.ts @@ -41,9 +41,18 @@ export const selectRecordFieldsConfig = createSelector( export const selectRecordFields = createSelector( selectEditorState, - (state: EditorState) => - state.fieldsConfig.map((fieldConfig) => ({ - config: fieldConfig, - value: state.record?.[fieldConfig.model] ?? null, - })) + (state: EditorState) => { + const fieldsConfig = state.fieldsConfig + fieldsConfig.pages.forEach((page) => { + page.sections.forEach((section) => { + section.fields.forEach((field) => { + if (state.record) { + field.value = state.record[field.model] + } + }) + }) + }) + + return fieldsConfig + } ) 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 98f8503d27..ec1b774a7b 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 @@ -4,8 +4,8 @@ @@ -15,14 +15,14 @@
    -

    {{ formControl.value }} -

    + help @@ -41,14 +41,14 @@ 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 aa81387dba..6337d1d9b3 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 @@ -23,7 +23,6 @@ describe('FormFieldComponent', () => { fixture = TestBed.createComponent(FormFieldComponent) component = fixture.componentInstance component.config = { - type: 'text', labelKey: 'my.label', } }) 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 5077db478e..44621437a5 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 @@ -25,10 +25,10 @@ import { FormFieldObjectComponent } from './form-field-object/form-field-object. import { FormFieldRichComponent } from './form-field-rich/form-field-rich.component' import { FormFieldSimpleComponent } from './form-field-simple/form-field-simple.component' import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/form-field-spatial-extent.component' -import { FormFieldConfig } from './form-field.model' import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component' import { CatalogRecordKeys } from '@geonetwork-ui/common/domain/model/record' import { FormFieldKeywordsComponent } from './form-field-keywords/form-field-keywords.component' +import { FormFieldConfig } from '../../../models' @Component({ selector: 'gn-ui-form-field', @@ -65,6 +65,7 @@ export class FormFieldComponent { emitEvent: false, }) } + @Output() valueChange: Observable @ViewChild('titleInput') titleInput: ElementRef diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.model.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.model.ts deleted file mode 100644 index cbc9d543b3..0000000000 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.model.ts +++ /dev/null @@ -1,43 +0,0 @@ -type BaseFormFieldType = - | 'text' - | 'number' - | 'rich' - | 'date' - | 'list' - | 'spatial_extent' - | 'temporal_extent' - | 'url' - | 'file' - | 'toggle' - -type AllFormFieldType = BaseFormFieldType | 'object' | 'array' - -interface FormFieldConfigBase { - type: AllFormFieldType - labelKey: string - hintKey?: string - tooltipKey?: string - required?: boolean - locked?: boolean - invalid?: boolean - invalidHintKey?: string -} - -export interface FormFieldConfigSimple extends FormFieldConfigBase { - type: BaseFormFieldType -} - -export interface FormFieldConfigArray extends FormFieldConfigBase { - type: 'array' - items: FormFieldConfig -} - -export interface FormFieldConfigObject extends FormFieldConfigBase { - type: 'object' - fields: Array -} - -export type FormFieldConfig = - | FormFieldConfigSimple - | FormFieldConfigArray - | FormFieldConfigObject 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 053dede8e5..8555c39727 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 @@ -9,4 +9,3 @@ export * from './form-field-object/form-field-object.component' export * from './form-field-array/form-field-array.component' export * from './form-field-spatial-extent/form-field-spatial-extent.component' export * from './form-field.component' -export * from './form-field.model' diff --git a/libs/feature/editor/src/lib/components/record-form/record-form.component.html b/libs/feature/editor/src/lib/components/record-form/record-form.component.html index 7b07459330..ed64b72b3a 100644 --- a/libs/feature/editor/src/lib/components/record-form/record-form.component.html +++ b/libs/feature/editor/src/lib/components/record-form/record-form.component.html @@ -1,11 +1,43 @@ -
    - - - -
    + +
    + + +
    +
    +
    + {{ section.labelKey }} +
    +
    + {{ section.descriptionKey }} +
    +
    + + + + + +
    +
    +
    +
    +
    diff --git a/libs/feature/editor/src/lib/components/record-form/record-form.component.spec.ts b/libs/feature/editor/src/lib/components/record-form/record-form.component.spec.ts index ca1d821f0a..2042e4c857 100644 --- a/libs/feature/editor/src/lib/components/record-form/record-form.component.spec.ts +++ b/libs/feature/editor/src/lib/components/record-form/record-form.component.spec.ts @@ -32,10 +32,7 @@ describe('RecordFormComponent', () => { describe('handleFieldValueChange', () => { it('should call facade.updateRecordField', () => { - component.handleFieldValueChange( - { config: { model: 'title' }, value: 'old title' }, - 'new title' - ) + component.handleFieldValueChange('title', 'new title') expect(component.facade.updateRecordField).toHaveBeenCalledWith( 'title', 'new title' diff --git a/libs/feature/editor/src/lib/components/record-form/record-form.component.ts b/libs/feature/editor/src/lib/components/record-form/record-form.component.ts index 310fa77fea..06863d04b7 100644 --- a/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/record-form.component.ts @@ -1,8 +1,14 @@ import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component } from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { EditorFacade } from '../../+state/editor.facade' -import { EditorFieldState, EditorFieldValue } from '../../models/fields.model' +import { + EditorField, + EditorFieldPage, + EditorFieldValue, + EditorSection, +} from '../../models' import { FormFieldComponent } from './form-field' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-record-form', @@ -10,21 +16,25 @@ import { FormFieldComponent } from './form-field' styleUrls: ['./record-form.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, FormFieldComponent], + imports: [CommonModule, FormFieldComponent, TranslateModule], }) export class RecordFormComponent { - fields$ = this.facade.recordFields$ + @Input() page: EditorFieldPage constructor(public facade: EditorFacade) {} - handleFieldValueChange(field: EditorFieldState, newValue: EditorFieldValue) { - if (!field.config.model) { + handleFieldValueChange(model: string, newValue: EditorFieldValue) { + if (!model) { return } - this.facade.updateRecordField(field.config.model, newValue) + this.facade.updateRecordField(model, newValue) } - fieldTracker(index: number, field: EditorFieldState) { - return field.config.model + fieldTracker(index: number, field: EditorField): any { + return field.model + } + + sectionTracker(index: number, section: EditorSection): any { + return section.labelKey } } diff --git a/libs/feature/editor/src/lib/expressions.spec.ts b/libs/feature/editor/src/lib/expressions.spec.ts index 6de626a823..e36b9e7fc2 100644 --- a/libs/feature/editor/src/lib/expressions.spec.ts +++ b/libs/feature/editor/src/lib/expressions.spec.ts @@ -1,13 +1,4 @@ import { evaluate, ExpressionEvaluator } from './expressions' -import { EditorFieldConfig } from './models/fields.model' - -const SAMPLE_CONFIG: EditorFieldConfig = { - formFieldConfig: { - labelKey: 'Metadata title', - type: 'text', - }, - model: 'myModel', -} const originalDate = window.Date window.Date = function () { @@ -22,7 +13,7 @@ describe('expressions', () => { evaluator = evaluate('${dateNow()}') }) it('returns the current time at evaluation', () => { - expect(evaluator({ config: SAMPLE_CONFIG, value: 'bla' })).toEqual( + expect(evaluator({ model: 'keywords', value: 'bla' })).toEqual( new Date() ) }) diff --git a/libs/feature/editor/src/lib/expressions.ts b/libs/feature/editor/src/lib/expressions.ts index 59aabd6885..253f52a2fa 100644 --- a/libs/feature/editor/src/lib/expressions.ts +++ b/libs/feature/editor/src/lib/expressions.ts @@ -1,4 +1,4 @@ -import { EditorFieldState, EditorFieldValue } from './models/fields.model' +import { EditorFieldState, EditorFieldValue } from './models/' export type ExpressionEvaluator = (field: EditorFieldState) => EditorFieldValue diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index 6591a3090c..62a7d83bbe 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -1,71 +1,185 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker' -import { EditorFieldsConfig } from './models/fields.model' - -export const DEFAULT_FIELDS: EditorFieldsConfig = [ - { - model: 'title', - formFieldConfig: { - labelKey: marker('editor.record.form.metadata.title'), - type: 'text', - }, +import { + EditorConfig, + EditorField, + EditorSection, +} from './models/editor-config.model' + +/** + * This file contains the configuration of the fields that will be displayed in the editor. + * To add a new field, you need to create a new EditorField object in the fields part of this file. + * Then add it to the corresponding section in the sections part of this file. + * Finally, add the section to the corresponding page in the pages part of this file. + */ + +/************************************************************ + *************** FIELDS ***************** + ************************************************************ + */ + +export const RECORD_LICENSE_FIELD: EditorField = { + model: 'licenses', + formFieldConfig: { + labelKey: marker('editor.record.form.field.license'), }, - { - model: 'abstract', - formFieldConfig: { - labelKey: marker('editor.record.form.abstract'), - type: 'rich', - }, +} + +export const RECORD_KEYWORDS_FIELD: EditorField = { + model: 'keywords', + formFieldConfig: { + labelKey: marker('editor.record.form.field.keywords'), }, - { - model: 'uniqueIdentifier', - formFieldConfig: { - labelKey: marker('editor.record.form.unique.identifier'), - type: 'text', - locked: true, - }, +} + +export const RECORD_UNIQUE_IDENTIFIER_FIELD: EditorField = { + model: 'uniqueIdentifier', + formFieldConfig: { + labelKey: marker('editor.record.form.field.uniqueIdentifier'), + locked: true, }, - { - model: 'recordUpdated', - formFieldConfig: { - labelKey: marker('editor.record.form.record.updated'), - type: 'text', - locked: true, - }, - onSaveProcess: '${dateNow()}', +} + +export const RECORD_RESOURCE_UPDATED_FIELD: EditorField = { + model: 'resourceUpdated', + formFieldConfig: { + labelKey: marker('editor.record.form.field.resourceUpdated'), }, - { - model: 'licenses', - formFieldConfig: { - labelKey: marker('editor.record.form.license'), - type: 'list', - }, +} + +export const RECORD_UPDATED_FIELD: EditorField = { + model: 'recordUpdated', + formFieldConfig: { + labelKey: marker('editor.record.form.field.recordUpdated'), + locked: true, }, - { - model: 'resourceUpdated', - formFieldConfig: { - labelKey: marker('editor.record.form.resourceUpdated'), - type: 'date', - }, + onSaveProcess: '${dateNow()}', +} + +export const RECORD_UPDATE_FREQUENCY_FIELD: EditorField = { + model: 'updateFrequency', + formFieldConfig: { + labelKey: marker('editor.record.form.field.updateFrequency'), }, - { - model: 'updateFrequency', - formFieldConfig: { - labelKey: marker('editor.record.form.updateFrequency'), - type: 'text', - }, +} + +export const RECORD_TEMPORAL_EXTENTS_FIELD: EditorField = { + model: 'temporalExtents', + formFieldConfig: { + labelKey: marker('editor.record.form.field.temporalExtents'), }, - { - model: 'temporalExtents', - formFieldConfig: { - labelKey: marker('editor.record.form.temporalExtents'), - type: 'list', - }, +} + +export const RECORD_TITLE_FIELD: EditorField = { + model: 'title', + formFieldConfig: { + labelKey: marker('editor.record.form.field.title'), }, - { - model: 'keywords', - formFieldConfig: { - labelKey: marker('editor.record.form.keywords'), - type: 'list', - }, +} + +export const RECORD_ABSTRACT_FIELD: EditorField = { + model: 'abstract', + formFieldConfig: { + labelKey: marker('editor.record.form.field.abstract'), }, -] +} + +/************************************************************ + *************** SECTIONS ***************** + ************************************************************ + */ + +export const TITLE_SECTION: EditorSection = { + hidden: false, + fields: [RECORD_TITLE_FIELD, RECORD_ABSTRACT_FIELD], +} + +export const ABOUT_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.about.label'), + descriptionKey: marker('editor.record.form.section.about.description'), + hidden: false, + fields: [ + RECORD_UNIQUE_IDENTIFIER_FIELD, + RECORD_RESOURCE_UPDATED_FIELD, + RECORD_UPDATED_FIELD, + RECORD_UPDATE_FREQUENCY_FIELD, + RECORD_TEMPORAL_EXTENTS_FIELD, + ], +} + +export const GEOGRAPHICAL_COVERAGE_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.geographicalCoverage.label'), + hidden: false, + fields: [], +} + +export const ASSOCIATED_RESOURCES_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.associatedResources.label'), + descriptionKey: marker( + 'editor.record.form.section.associatedResources.description' + ), + hidden: false, + fields: [], +} + +export const ANNEXES_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.annexes.label'), + hidden: false, + fields: [], +} + +export const CLASSIFICATION_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.classification.label'), + descriptionKey: marker( + 'editor.record.form.section.classification.description' + ), + hidden: false, + fields: [RECORD_KEYWORDS_FIELD], +} + +export const USE_AND_ACCESS_CONDITIONS_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.useAndAccessConditions.label'), + hidden: false, + fields: [RECORD_LICENSE_FIELD], +} + +export const DATA_MANAGERS_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.dataManagers.label'), + descriptionKey: marker('editor.record.form.section.dataManagers.description'), + hidden: false, + fields: [], +} + +export const DATA_POINT_OF_CONTACT_SECTION: EditorSection = { + labelKey: marker('editor.record.form.section.dataPointOfContact.label'), + descriptionKey: marker( + 'editor.record.form.section.dataPointOfContact.description' + ), + hidden: false, + fields: [], +} + +/************************************************************ + *************** PAGES ***************** + ************************************************************ + */ +export const DEFAULT_FIELDS: EditorConfig = { + pages: [ + { + labelKey: marker('editor.record.form.page.description'), + sections: [TITLE_SECTION, ABOUT_SECTION, GEOGRAPHICAL_COVERAGE_SECTION], + }, + { + labelKey: marker('editor.record.form.page.ressources'), + sections: [ASSOCIATED_RESOURCES_SECTION, ANNEXES_SECTION], + }, + { + labelKey: marker('editor.record.form.page.accessAndContact'), + sections: [ + CLASSIFICATION_SECTION, + USE_AND_ACCESS_CONDITIONS_SECTION, + DATA_MANAGERS_SECTION, + DATA_POINT_OF_CONTACT_SECTION, + ], + }, + ], +} diff --git a/libs/feature/editor/src/lib/fixtures/editor.fixtures.ts b/libs/feature/editor/src/lib/fixtures/editor.fixtures.ts new file mode 100644 index 0000000000..d220a192ef --- /dev/null +++ b/libs/feature/editor/src/lib/fixtures/editor.fixtures.ts @@ -0,0 +1,158 @@ +import { EditorConfig, EditorField, EditorSection } from '../models' + +export const EDITOR_CONFIG = (): EditorConfig => ({ + pages: [ + { + labelKey: 'Resource description', + sections: [EDITOR_SECTION_ABOUT()], + }, + { + labelKey: 'Resources', + sections: [EDITOR_SECTION_CLASSIFICATION()], + }, + { + labelKey: 'Access and contact', + sections: [ + EDITOR_SECTION_USE_AND_ACCESS_CONDITIONS(), + EDITOR_SECTION_DATA_MANAGER(), + ], + }, + ], +}) + +export const EDITOR_SECTION_ABOUT = (): EditorSection => ({ + labelKey: 'About the resource', + descriptionKey: 'This section describes the resource.', + hidden: false, + fields: [ + EDITOR_FIELD_TITLE(), + EDITOR_FIELD_ABSTRACT(), + EDITOR_FIELD_RESOURCE_UPDATED(), + EDITOR_FIELD_RECORD_UPDATED(), + EDITOR_FIELD_UPDATE_FREQUENCY(), + EDITOR_FIELD_TEMPORAL_EXTENTS(), + ], +}) + +export const EDITOR_SECTION_DATA_MANAGER = (): EditorSection => ({ + labelKey: 'Data manager', + descriptionKey: '', + hidden: false, + fields: [], +}) + +export const EDITOR_SECTION_USE_AND_ACCESS_CONDITIONS = (): EditorSection => ({ + labelKey: 'Data manager', + descriptionKey: '', + hidden: false, + fields: [EDITOR_FIELD_LICENSE()], +}) + +export const EDITOR_SECTION_CLASSIFICATION = (): EditorSection => ({ + labelKey: 'Classification', + descriptionKey: 'The classification has an impact on the access to the data.', + hidden: false, + fields: [EDITOR_FIELD_KEYWORDS(), EDITOR_FIELD_UNIQUE_IDENTIFIER()], +}) + +export const EDITOR_FIELD_TITLE = (): EditorField => ({ + model: 'title', + hidden: false, + value: 'Accroches vélos MEL', + formFieldConfig: { + labelKey: 'editor.record.form.field.title', + }, +}) + +export const EDITOR_FIELD_ABSTRACT = (): EditorField => ({ + model: 'abstract', + hidden: false, + value: 'Abstract', + formFieldConfig: { + labelKey: 'editor.record.form.field.abstract', + }, +}) + +export const EDITOR_FIELD_RESOURCE_UPDATED = (): EditorField => ({ + model: 'resourceUpdated', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.resourceUpdated', + }, +}) + +export const EDITOR_FIELD_RECORD_UPDATED = (): EditorField => ({ + model: 'recordUpdated', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.recordUpdated', + locked: true, + }, + value: '2024-07-16T05:18:53.000Z', + onSaveProcess: '${dateNow()}', +}) + +export const EDITOR_FIELD_UPDATE_FREQUENCY = (): EditorField => ({ + model: 'updateFrequency', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.updateFrequency', + }, + value: 'unknown', +}) + +export const EDITOR_FIELD_TEMPORAL_EXTENTS = (): EditorField => ({ + model: 'temporalExtents', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.temporalExtents', + }, + value: [], +}) + +export const EDITOR_FIELD_SPATIAL_EXTENTS = (): EditorField => ({ + model: 'spatialExtents', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.spatialExtents', + }, +}) + +export const EDITOR_FIELD_KEYWORDS = (): EditorField => ({ + model: 'keywords', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.keywords', + }, +}) + +export const EDITOR_FIELD_UNIQUE_IDENTIFIER = (): EditorField => ({ + model: 'uniqueIdentifier', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.uniqueIdentifier', + locked: true, + }, + value: 'accroche_velos', +}) + +export const EDITOR_FIELD_LICENSE = (): EditorField => ({ + model: 'licenses', + hidden: false, + formFieldConfig: { + labelKey: 'editor.record.form.field.license', + locked: true, + }, +}) + +export const EDITOR_FIELDS = (): EditorField[] => [ + EDITOR_FIELD_TITLE(), + EDITOR_FIELD_ABSTRACT(), + EDITOR_FIELD_RESOURCE_UPDATED(), + EDITOR_FIELD_RECORD_UPDATED(), + EDITOR_FIELD_UPDATE_FREQUENCY(), + EDITOR_FIELD_TEMPORAL_EXTENTS(), + EDITOR_FIELD_SPATIAL_EXTENTS(), + EDITOR_FIELD_KEYWORDS(), + EDITOR_FIELD_UNIQUE_IDENTIFIER(), +] diff --git a/libs/feature/editor/src/lib/fixtures/index.ts b/libs/feature/editor/src/lib/fixtures/index.ts new file mode 100644 index 0000000000..ed9fbc8aeb --- /dev/null +++ b/libs/feature/editor/src/lib/fixtures/index.ts @@ -0,0 +1 @@ +export * from './editor.fixtures' diff --git a/libs/feature/editor/src/lib/models/editor-config.model.ts b/libs/feature/editor/src/lib/models/editor-config.model.ts new file mode 100644 index 0000000000..8530049354 --- /dev/null +++ b/libs/feature/editor/src/lib/models/editor-config.model.ts @@ -0,0 +1,55 @@ +import { CatalogRecordKeys } from '@geonetwork-ui/common/domain/model/record' + +// Expressions should be enclosed in `${}` to be recognized as such +// eg. ${dateNow()} +export type EditorFieldExpression = `$\{${string}}` + +export type EditorFieldValue = string | number | boolean | unknown + +export interface FormFieldConfig { + labelKey?: string + hintKey?: string + tooltipKey?: string + required?: boolean + locked?: boolean + invalid?: boolean + invalidHintKey?: string +} + +export interface EditorField { + // configuration of the form field used as presentation + formFieldConfig: FormFieldConfig + + // name of the target field in the record; will not change the record directly if not defined + model?: CatalogRecordKeys + + // a hidden field won't show but can still be used to modify the record + // FIXME: currently this is redundant with an absence of formFieldConfig but necessary for clarity + hidden?: boolean + + // the result of this expression will replace the field value on save + onSaveProcess?: EditorFieldExpression + + value?: EditorFieldValue +} + +export interface EditorSection { + labelKey?: string + descriptionKey?: string + hidden: boolean + fields: EditorField[] +} + +export interface EditorFieldPage { + labelKey?: string + sections: EditorSection[] +} + +export interface EditorConfig { + pages: EditorFieldPage[] +} + +export interface EditorFieldState { + model: string + value: EditorFieldValue +} diff --git a/libs/feature/editor/src/lib/models/fields.model.ts b/libs/feature/editor/src/lib/models/fields.model.ts deleted file mode 100644 index 0dae06b007..0000000000 --- a/libs/feature/editor/src/lib/models/fields.model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { FormFieldConfig } from '../components/record-form/form-field' - -// Expressions should be enclosed in `${}` to be recognized as such -// eg. ${dateNow()} -export type EditorFieldExpression = `$\{${string}}` - -export interface EditorFieldConfig { - // configuration of the form field used as presentation; optional, nothing shown if not defined - formFieldConfig?: FormFieldConfig - - // name of the target field in the record; will not change the record directly if not defined - model?: string - - // a hidden field won't show but can still be used to modify the record - // FIXME: currently this is redundant with an absence of formFieldConfig but necessary for clarity - hidden?: boolean - - // the result of this expression will replace the field value on save - onSaveProcess?: EditorFieldExpression -} - -export type EditorFieldsConfig = EditorFieldConfig[] - -export type EditorFieldValue = string | number | boolean | unknown - -export interface EditorFieldState { - config: EditorFieldConfig - value: string | number | boolean | unknown -} diff --git a/libs/feature/editor/src/lib/models/index.ts b/libs/feature/editor/src/lib/models/index.ts index 65975121b2..1adb10500d 100644 --- a/libs/feature/editor/src/lib/models/index.ts +++ b/libs/feature/editor/src/lib/models/index.ts @@ -1,2 +1,3 @@ export * from './wizard-field.model' export * from './wizard-field.type' +export * from './editor-config.model' diff --git a/libs/feature/editor/src/lib/services/editor.service.ts b/libs/feature/editor/src/lib/services/editor.service.ts index 9b7c005438..1cc1b67791 100644 --- a/libs/feature/editor/src/lib/services/editor.service.ts +++ b/libs/feature/editor/src/lib/services/editor.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core' import { Observable, switchMap } from 'rxjs' import { map, tap } from 'rxjs/operators' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' -import { EditorFieldsConfig } from '../models/fields.model' +import { EditorConfig } from '../models/' import { evaluate } from '../expressions' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @@ -15,17 +15,21 @@ export class EditorService { // returns the record as it was when saved, alongside its source saveRecord( record: CatalogRecord, - fieldsConfig: EditorFieldsConfig, + fieldsConfig: EditorConfig, generateNewUniqueIdentifier = false ): Observable<[CatalogRecord, string]> { const savedRecord = { ...record } + const fields = fieldsConfig.pages.flatMap((page) => + page.sections.flatMap((section) => section.fields) + ) + // run onSave processes - for (const field of fieldsConfig) { + for (const field of fields) { if (field.onSaveProcess && field.model) { const evaluator = evaluate(field.onSaveProcess) savedRecord[field.model] = evaluator({ - config: field, + model: field.model, value: record[field.model], }) } diff --git a/libs/feature/editor/tsconfig.json b/libs/feature/editor/tsconfig.json index cdea0bdb38..9e29358f76 100644 --- a/libs/feature/editor/tsconfig.json +++ b/libs/feature/editor/tsconfig.json @@ -16,11 +16,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "target": "es2020", - "lib": [ - "dom", - "es2020", - "dom.iterable" - ], + "lib": ["dom", "es2020", "dom.iterable"], "downlevelIteration": true }, "angularCompilerOptions": { diff --git a/libs/feature/editor/tsconfig.lib.json b/libs/feature/editor/tsconfig.lib.json index 7ca4377996..2a4c9a5e36 100644 --- a/libs/feature/editor/tsconfig.lib.json +++ b/libs/feature/editor/tsconfig.lib.json @@ -7,11 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": [ - "dom", - "es2020", - "dom.iterable" - ], + "lib": ["dom", "es2020", "dom.iterable"], "downlevelIteration": true }, "exclude": [ @@ -20,7 +16,5 @@ "**/*.test.ts", "jest.config.ts" ], - "include": [ - "**/*.ts" - ] + "include": ["**/*.ts"] } diff --git a/libs/util/shared/tsconfig.lib.json b/libs/util/shared/tsconfig.lib.json index 9bb502669e..02d129fa3b 100644 --- a/libs/util/shared/tsconfig.lib.json +++ b/libs/util/shared/tsconfig.lib.json @@ -7,11 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": [ - "dom", - "es2020", - "dom.iterable" - ] + "lib": ["dom", "es2020", "dom.iterable"] }, "exclude": [ "src/test-setup.ts", diff --git a/translations/de.json b/translations/de.json index 25539c677e..12b58965fd 100644 --- a/translations/de.json +++ b/translations/de.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "unbekannt", "downloads.wfs.featuretype.not.found": "Der Layer wurde nicht gefunden", "dropFile": "Datei ablegen", + "editor.record.form.bottomButtons.comeBackLater": "", + "editor.record.form.bottomButtons.next": "", + "editor.record.form.bottomButtons.previous": "", + "editor.record.form.field.abstract": "", + "editor.record.form.field.keywords": "Schlagwörter", + "editor.record.form.field.license": "Lizenz", + "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceUpdated": "", + "editor.record.form.field.temporalExtents": "", + "editor.record.form.field.title": "", + "editor.record.form.field.uniqueIdentifier": "", + "editor.record.form.field.updateFrequency": "", "editor.record.form.abstract": "Kurzbeschreibung", "editor.record.form.keywords": "Schlüsselwörter", "editor.record.form.license": "Lizenz", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "Open Data Commons ODC-By", "editor.record.form.license.pddl": "Open Data Commons PDDL", "editor.record.form.license.unknown": "Unbekannt oder nicht vorhanden", + "editor.record.form.page.accessAndContact": "", + "editor.record.form.page.description": "", + "editor.record.form.page.ressources": "", + "editor.record.form.section.about.description": "", + "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.label": "", + "editor.record.form.section.associatedResources.description": "", + "editor.record.form.section.associatedResources.label": "", + "editor.record.form.section.classification.description": "", + "editor.record.form.section.classification.label": "", + "editor.record.form.section.dataManagers.description": "", + "editor.record.form.section.dataManagers.label": "", + "editor.record.form.section.dataPointOfContact.description": "", + "editor.record.form.section.dataPointOfContact.label": "", + "editor.record.form.section.geographicalCoverage.label": "", + "editor.record.form.section.useAndAccessConditions.label": "", "editor.record.form.metadata.title": "Metadaten-Titel", "editor.record.form.record.updated": "Datensatz zuletzt aktualisiert", "editor.record.form.resourceUpdated": "Letztes Aktualisierungsdatum", diff --git a/translations/en.json b/translations/en.json index c3f80d04d0..4055d950f6 100644 --- a/translations/en.json +++ b/translations/en.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "unknown", "downloads.wfs.featuretype.not.found": "The layer was not found", "dropFile": "drop file", + "editor.record.form.bottomButtons.comeBackLater": "Come back later", + "editor.record.form.bottomButtons.next": "Next", + "editor.record.form.bottomButtons.previous": "Previous", + "editor.record.form.field.abstract": "Abstract", + "editor.record.form.field.keywords": "Keywords", + "editor.record.form.field.license": "License", + "editor.record.form.field.recordUpdated": "Record Updated", + "editor.record.form.field.resourceUpdated": "Resource Updated", + "editor.record.form.field.temporalExtents": "Temporal extents", + "editor.record.form.field.title": "Metadata title", + "editor.record.form.field.uniqueIdentifier": "Unique identifier", + "editor.record.form.field.updateFrequency": "Update frequency", "editor.record.form.abstract": "Abstract", "editor.record.form.keywords": "Keywords", "editor.record.form.license": "License", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "Open Data Commons ODC-By", "editor.record.form.license.pddl": "Open Data Commons PDDL", "editor.record.form.license.unknown": "Unknown or absent", + "editor.record.form.page.accessAndContact": "Access and contact", + "editor.record.form.page.description": "Resource description", + "editor.record.form.page.ressources": "Resources", + "editor.record.form.section.about.description": "This section describes the resource.", + "editor.record.form.section.about.label": "About the resource", + "editor.record.form.section.annexes.label": "Annexes", + "editor.record.form.section.associatedResources.description": "Drop files here to associate them with the resource.", + "editor.record.form.section.associatedResources.label": "Associated resources", + "editor.record.form.section.classification.description": "The classification has an impact on the access to the data.", + "editor.record.form.section.classification.label": "Classification", + "editor.record.form.section.dataManagers.description": "The data managers are responsible for the data.", + "editor.record.form.section.dataManagers.label": "Data managers", + "editor.record.form.section.dataPointOfContact.description": "This information concerns the metadata.", + "editor.record.form.section.dataPointOfContact.label": "Data point of contact", + "editor.record.form.section.geographicalCoverage.label": "Geographical coverage", + "editor.record.form.section.useAndAccessConditions.label": "Use and access conditions", "editor.record.form.metadata.title": "Metadata title", "editor.record.form.record.updated": "Record updated", "editor.record.form.resourceUpdated": "Last update date", diff --git a/translations/es.json b/translations/es.json index e74e4256e4..54dfd827fb 100644 --- a/translations/es.json +++ b/translations/es.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.bottomButtons.comeBackLater": "", + "editor.record.form.bottomButtons.next": "", + "editor.record.form.bottomButtons.previous": "", + "editor.record.form.field.abstract": "", + "editor.record.form.field.keywords": "", + "editor.record.form.field.license": "", + "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceUpdated": "", + "editor.record.form.field.temporalExtents": "", + "editor.record.form.field.title": "", + "editor.record.form.field.uniqueIdentifier": "", + "editor.record.form.field.updateFrequency": "", "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", + "editor.record.form.page.accessAndContact": "", + "editor.record.form.page.description": "", + "editor.record.form.page.ressources": "", + "editor.record.form.section.about.description": "", + "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.label": "", + "editor.record.form.section.associatedResources.description": "", + "editor.record.form.section.associatedResources.label": "", + "editor.record.form.section.classification.description": "", + "editor.record.form.section.classification.label": "", + "editor.record.form.section.dataManagers.description": "", + "editor.record.form.section.dataManagers.label": "", + "editor.record.form.section.dataPointOfContact.description": "", + "editor.record.form.section.dataPointOfContact.label": "", + "editor.record.form.section.geographicalCoverage.label": "", + "editor.record.form.section.useAndAccessConditions.label": "", "editor.record.form.metadata.title": "", "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", diff --git a/translations/fr.json b/translations/fr.json index c3124afef3..29374cb89a 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "inconnu", "downloads.wfs.featuretype.not.found": "La couche n'a pas été retrouvée", "dropFile": "Faites glisser votre fichier", + "editor.record.form.bottomButtons.comeBackLater": "Revenir plus tard", + "editor.record.form.bottomButtons.next": "Suivant", + "editor.record.form.bottomButtons.previous": "Précédent", + "editor.record.form.field.abstract": "Résumé", + "editor.record.form.field.keywords": "Mots-clés", + "editor.record.form.field.license": "Licence", + "editor.record.form.field.recordUpdated": "Date de dernière révision", + "editor.record.form.field.resourceUpdated": "Date de dernière révision", + "editor.record.form.field.temporalExtents": "Étendue temporelle", + "editor.record.form.field.title": "Titre", + "editor.record.form.field.uniqueIdentifier": "Identifiant unique", + "editor.record.form.field.updateFrequency": "Fréquence de mise à jour", "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "Licence", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Non-reconnue ou absente", + "editor.record.form.page.accessAndContact": "Acces et contact", + "editor.record.form.page.description": "Description de la ressource", + "editor.record.form.page.ressources": "Ressources", + "editor.record.form.section.about.description": "Ces informations concernent la donnée.", + "editor.record.form.section.about.label": "A propos de la ressource", + "editor.record.form.section.annexes.label": "Annexes", + "editor.record.form.section.associatedResources.description": "Déposez les jeux de données associées à cette fiche de métadonnée.", + "editor.record.form.section.associatedResources.label": "Ressources associees", + "editor.record.form.section.classification.description": "La classification a un impact sur la recherche du jeux de données.", + "editor.record.form.section.classification.label": "Classification", + "editor.record.form.section.dataManagers.description": "Cette information concerne la donnée.", + "editor.record.form.section.dataManagers.label": "Responsables de la donnee", + "editor.record.form.section.dataPointOfContact.description": "Cette information concerne la fiche de métadonnée.", + "editor.record.form.section.dataPointOfContact.label": "Point de contact de la metadonee", + "editor.record.form.section.geographicalCoverage.label": "Couverture geographique", + "editor.record.form.section.useAndAccessConditions.label": "Conditions d'acces et usage", "editor.record.form.metadata.title": "", "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "Date de dernière révision", diff --git a/translations/it.json b/translations/it.json index fd42ba00df..b945330f98 100644 --- a/translations/it.json +++ b/translations/it.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "sconosciuto", "downloads.wfs.featuretype.not.found": "Il layer non è stato trovato", "dropFile": "Trascina il suo file", + "editor.record.form.bottomButtons.comeBackLater": "", + "editor.record.form.bottomButtons.next": "", + "editor.record.form.bottomButtons.previous": "", + "editor.record.form.field.abstract": "", + "editor.record.form.field.keywords": "", + "editor.record.form.field.license": "Licenza", + "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceUpdated": "", + "editor.record.form.field.temporalExtents": "", + "editor.record.form.field.title": "", + "editor.record.form.field.uniqueIdentifier": "", + "editor.record.form.field.updateFrequency": "", "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "Licenza", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Non riconosciuta o assente", + "editor.record.form.page.accessAndContact": "", + "editor.record.form.page.description": "", + "editor.record.form.page.ressources": "", + "editor.record.form.section.about.description": "", + "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.label": "", + "editor.record.form.section.associatedResources.description": "", + "editor.record.form.section.associatedResources.label": "", + "editor.record.form.section.classification.description": "", + "editor.record.form.section.classification.label": "", + "editor.record.form.section.dataManagers.description": "", + "editor.record.form.section.dataManagers.label": "", + "editor.record.form.section.dataPointOfContact.description": "", + "editor.record.form.section.dataPointOfContact.label": "", + "editor.record.form.section.geographicalCoverage.label": "", + "editor.record.form.section.useAndAccessConditions.label": "", "editor.record.form.metadata.title": "", "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", diff --git a/translations/nl.json b/translations/nl.json index f714c53ec2..4fcfa583f1 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.bottomButtons.comeBackLater": "", + "editor.record.form.bottomButtons.next": "", + "editor.record.form.bottomButtons.previous": "", + "editor.record.form.field.abstract": "", + "editor.record.form.field.keywords": "", + "editor.record.form.field.license": "", + "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceUpdated": "", + "editor.record.form.field.temporalExtents": "", + "editor.record.form.field.title": "", + "editor.record.form.field.uniqueIdentifier": "", + "editor.record.form.field.updateFrequency": "", "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", + "editor.record.form.page.accessAndContact": "", + "editor.record.form.page.description": "", + "editor.record.form.page.ressources": "", + "editor.record.form.section.about.description": "", + "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.label": "", + "editor.record.form.section.associatedResources.description": "", + "editor.record.form.section.associatedResources.label": "", + "editor.record.form.section.classification.description": "", + "editor.record.form.section.classification.label": "", + "editor.record.form.section.dataManagers.description": "", + "editor.record.form.section.dataManagers.label": "", + "editor.record.form.section.dataPointOfContact.description": "", + "editor.record.form.section.dataPointOfContact.label": "", + "editor.record.form.section.geographicalCoverage.label": "", + "editor.record.form.section.useAndAccessConditions.label": "", "editor.record.form.metadata.title": "", "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", diff --git a/translations/pt.json b/translations/pt.json index a702a9c851..f6782e7344 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "", "downloads.wfs.featuretype.not.found": "", "dropFile": "", + "editor.record.form.bottomButtons.comeBackLater": "", + "editor.record.form.bottomButtons.next": "", + "editor.record.form.bottomButtons.previous": "", + "editor.record.form.field.abstract": "", + "editor.record.form.field.keywords": "", + "editor.record.form.field.license": "", + "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceUpdated": "", + "editor.record.form.field.temporalExtents": "", + "editor.record.form.field.title": "", + "editor.record.form.field.uniqueIdentifier": "", + "editor.record.form.field.updateFrequency": "", "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "", + "editor.record.form.page.accessAndContact": "", + "editor.record.form.page.description": "", + "editor.record.form.page.ressources": "", + "editor.record.form.section.about.description": "", + "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.label": "", + "editor.record.form.section.associatedResources.description": "", + "editor.record.form.section.associatedResources.label": "", + "editor.record.form.section.classification.description": "", + "editor.record.form.section.classification.label": "", + "editor.record.form.section.dataManagers.description": "", + "editor.record.form.section.dataManagers.label": "", + "editor.record.form.section.dataPointOfContact.description": "", + "editor.record.form.section.dataPointOfContact.label": "", + "editor.record.form.section.geographicalCoverage.label": "", + "editor.record.form.section.useAndAccessConditions.label": "", "editor.record.form.metadata.title": "", "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", diff --git a/translations/sk.json b/translations/sk.json index f3dd2a423b..2df4e8b133 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -165,6 +165,18 @@ "downloads.format.unknown": "neznámy", "downloads.wfs.featuretype.not.found": "Vrstva nebola nájdená", "dropFile": "nahrať súbor", + "editor.record.form.bottomButtons.comeBackLater": "", + "editor.record.form.bottomButtons.next": "", + "editor.record.form.bottomButtons.previous": "", + "editor.record.form.field.abstract": "", + "editor.record.form.field.keywords": "", + "editor.record.form.field.license": "Licencia", + "editor.record.form.field.recordUpdated": "", + "editor.record.form.field.resourceUpdated": "", + "editor.record.form.field.temporalExtents": "", + "editor.record.form.field.title": "", + "editor.record.form.field.uniqueIdentifier": "", + "editor.record.form.field.updateFrequency": "", "editor.record.form.abstract": "", "editor.record.form.keywords": "", "editor.record.form.license": "Licencia", @@ -177,6 +189,22 @@ "editor.record.form.license.odc-by": "", "editor.record.form.license.pddl": "", "editor.record.form.license.unknown": "Neznáme alebo chýbajúce", + "editor.record.form.page.accessAndContact": "", + "editor.record.form.page.description": "", + "editor.record.form.page.ressources": "", + "editor.record.form.section.about.description": "", + "editor.record.form.section.about.label": "", + "editor.record.form.section.annexes.label": "", + "editor.record.form.section.associatedResources.description": "", + "editor.record.form.section.associatedResources.label": "", + "editor.record.form.section.classification.description": "", + "editor.record.form.section.classification.label": "", + "editor.record.form.section.dataManagers.description": "", + "editor.record.form.section.dataManagers.label": "", + "editor.record.form.section.dataPointOfContact.description": "", + "editor.record.form.section.dataPointOfContact.label": "", + "editor.record.form.section.geographicalCoverage.label": "", + "editor.record.form.section.useAndAccessConditions.label": "", "editor.record.form.metadata.title": "", "editor.record.form.record.updated": "", "editor.record.form.resourceUpdated": "", From 5eb47b23ffe620f1751dd6e4583ea19b80271a18 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 18 Jul 2024 10:10:06 +0200 Subject: [PATCH 072/378] wip store current page in state --- .../breadcrumbs/page-selector.component.html | 12 +++-- .../breadcrumbs/page-selector.component.ts | 18 +++----- .../src/app/edit/edit-page.component.html | 12 ++--- .../src/app/edit/edit-page.component.ts | 44 ++++++++----------- .../editor/src/lib/+state/editor.actions.ts | 5 +++ .../editor/src/lib/+state/editor.effects.ts | 4 +- .../editor/src/lib/+state/editor.facade.ts | 10 ++++- .../editor/src/lib/+state/editor.models.ts | 11 +++++ .../editor/src/lib/+state/editor.reducer.ts | 12 +++-- .../src/lib/+state/editor.selectors.spec.ts | 6 +-- .../editor/src/lib/+state/editor.selectors.ts | 35 ++++++++------- .../record-form/record-form.component.html | 22 +++++++--- .../record-form/record-form.component.ts | 21 ++++----- .../src/lib/models/editor-config.model.ts | 2 +- 14 files changed, 119 insertions(+), 95 deletions(-) diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html index b38938f323..3085adc2f5 100644 --- a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html +++ b/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html @@ -1,19 +1,23 @@
    {{ index }} @@ -21,7 +25,7 @@
    config.pages)) - @Output() selectedPageChange = new EventEmitter() + constructor(public facade: EditorFacade) {} pageSectionClickHandler(index: number) { - this.selectedPageChange.emit(index) + this.facade.setCurrentPage(index) // TODO } } 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 d93d4aab53..a71413574e 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.html +++ b/apps/metadata-editor/src/app/edit/edit-page.component.html @@ -2,19 +2,13 @@
    - +
    - +
    {{ - selectedPage === 0 + (currentPage$ | async) === 0 ? ('editor.record.form.bottomButtons.comeBackLater' | translate) : ('editor.record.form.bottomButtons.previous' | translate) }} 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 8586f05362..16d2ecc7d3 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.ts @@ -3,7 +3,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { EditorFacade, - EditorFieldPage, RecordFormComponent, } from '@geonetwork-ui/feature/editor' import { ButtonComponent } from '@geonetwork-ui/ui/inputs' @@ -15,9 +14,10 @@ import { NotificationsService, } from '@geonetwork-ui/feature/notifications' import { TranslateModule, TranslateService } from '@ngx-translate/core' -import { filter, Subscription, take } from 'rxjs' +import { filter, firstValueFrom, Subscription, take } from 'rxjs' import { PageSelectorComponent } from './components/breadcrumbs/page-selector.component' import { marker } from '@biesbjerg/ngx-translate-extract-marker' +import { map } from 'rxjs/operators' marker('editor.record.form.bottomButtons.comeBackLater') marker('editor.record.form.bottomButtons.previous') @@ -43,9 +43,11 @@ marker('editor.record.form.bottomButtons.next') export class EditPageComponent implements OnInit, OnDestroy { subscription = new Subscription() - fields$ = this.facade.recordFields$ - totalPages = 0 - selectedPage = 0 + fields$ = this.facade.currentSections$ + currentPage$ = this.facade.currentPage$ + pagesLength$ = this.facade.editorConfig$.pipe( + map((config) => config.pages.length) + ) constructor( private route: ActivatedRoute, @@ -53,13 +55,7 @@ export class EditPageComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private translateService: TranslateService, private router: Router - ) { - this.subscription.add( - this.fields$.subscribe((fields) => { - this.totalPages = fields.pages.length - }) - ) - } + ) {} ngOnInit(): void { const [currentRecord, currentRecordSource, currentRecordAlreadySaved] = @@ -129,24 +125,20 @@ export class EditPageComponent implements OnInit, OnDestroy { this.subscription.unsubscribe() } - getSelectedPageFields(pages: EditorFieldPage[]) { - return pages[this.selectedPage] - } - - previousPageButtonHandler() { - if (this.selectedPage === 0) { + async previousPageButtonHandler() { + const currentPage = await firstValueFrom(this.currentPage$) + if (currentPage === 0) { this.router.navigate(['catalog', 'search']) } else { - this.selectedPage-- + this.facade.setCurrentPage(currentPage - 1) // TODO } } - nextPageButtonHandler() { - if (this.selectedPage === this.totalPages - 1) return - this.selectedPage++ - } - - selectedPageChange(index: number) { - this.selectedPage = index + async nextPageButtonHandler() { + const currentPage = await firstValueFrom(this.currentPage$) + const pagesCount = await firstValueFrom(this.pagesLength$) + if (currentPage < pagesCount - 1) { + this.facade.setCurrentPage(currentPage + 1) + } } } diff --git a/libs/feature/editor/src/lib/+state/editor.actions.ts b/libs/feature/editor/src/lib/+state/editor.actions.ts index f2c6b3f93d..47152c22e6 100644 --- a/libs/feature/editor/src/lib/+state/editor.actions.ts +++ b/libs/feature/editor/src/lib/+state/editor.actions.ts @@ -28,3 +28,8 @@ export const saveRecordFailure = createAction( ) export const draftSaveSuccess = createAction('[Editor] Draft save success') + +export const setCurrentPage = createAction( + '[Editor] Set current page', + props<{ page: number }>() +) diff --git a/libs/feature/editor/src/lib/+state/editor.effects.ts b/libs/feature/editor/src/lib/+state/editor.effects.ts index 7c28a2890f..4b07925dac 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.ts @@ -6,9 +6,9 @@ import * as EditorActions from './editor.actions' import { EditorService } from '../services/editor.service' import { Store } from '@ngrx/store' import { + selectEditorConfig, selectRecord, selectRecordAlreadySavedOnce, - selectRecordFieldsConfig, } from './editor.selectors' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' @@ -24,7 +24,7 @@ export class EditorEffects { ofType(EditorActions.saveRecord), withLatestFrom( this.store.select(selectRecord), - this.store.select(selectRecordFieldsConfig), + this.store.select(selectEditorConfig), this.store.select(selectRecordAlreadySavedOnce) ), switchMap(([, record, fieldsConfig, alreadySavedOnce]) => diff --git a/libs/feature/editor/src/lib/+state/editor.facade.ts b/libs/feature/editor/src/lib/+state/editor.facade.ts index d6d0992df9..861cb03f1f 100644 --- a/libs/feature/editor/src/lib/+state/editor.facade.ts +++ b/libs/feature/editor/src/lib/+state/editor.facade.ts @@ -25,8 +25,12 @@ export class EditorFacade { changedSinceSave$ = this.store.pipe( select(EditorSelectors.selectRecordChangedSinceSave) ) - recordFields$ = this.store.pipe(select(EditorSelectors.selectRecordFields)) + currentSections$ = this.store.pipe( + select(EditorSelectors.selectRecordSections) + ) draftSaveSuccess$ = this.actions$.pipe(ofType(EditorActions.draftSaveSuccess)) + currentPage$ = this.store.pipe(select(EditorSelectors.selectCurrentPage)) + editorConfig$ = this.store.pipe(select(EditorSelectors.selectEditorConfig)) openRecord( record: CatalogRecord, @@ -45,4 +49,8 @@ export class EditorFacade { updateRecordField(field: string, value: unknown) { this.store.dispatch(EditorActions.updateRecordField({ field, value })) } + + setCurrentPage(page: number) { + this.store.dispatch(EditorActions.setCurrentPage({ page })) + } } diff --git a/libs/feature/editor/src/lib/+state/editor.models.ts b/libs/feature/editor/src/lib/+state/editor.models.ts index 903988428c..e59136767b 100644 --- a/libs/feature/editor/src/lib/+state/editor.models.ts +++ b/libs/feature/editor/src/lib/+state/editor.models.ts @@ -1 +1,12 @@ +import { EditorField, EditorFieldValue, EditorSection } from '../models' + export type SaveRecordError = string + +export interface EditorFieldWithValue { + config: EditorField + value: EditorFieldValue +} + +export type EditorSectionWithValues = EditorSection & { + fieldsWithValues: EditorFieldWithValue[] +} diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts index 506f3436fb..0006741a46 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts @@ -13,7 +13,7 @@ export const EDITOR_FEATURE_KEY = 'editor' * @property saving * @property saveError * @property changedSinceSave - * @property fieldsConfig Configuration for the fields in the editor + * @property editorConfig Configuration for the fields in the editor */ export interface EditorState { record: CatalogRecord | null @@ -22,7 +22,8 @@ export interface EditorState { saving: boolean saveError: SaveRecordError | null changedSinceSave: boolean - fieldsConfig: EditorConfig + editorConfig: EditorConfig + currentPage: number } export interface EditorPartialState { @@ -36,7 +37,8 @@ export const initialEditorState: EditorState = { saving: false, saveError: null, changedSinceSave: false, - fieldsConfig: DEFAULT_FIELDS, + editorConfig: DEFAULT_FIELDS, + currentPage: 0, } const reducer = createReducer( @@ -77,6 +79,10 @@ const reducer = createReducer( on(EditorActions.markRecordAsChanged, (state) => ({ ...state, changedSinceSave: true, + })), + on(EditorActions.setCurrentPage, (state, { page }) => ({ + ...state, + currentPage: page, })) ) 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 e3ee07bfd9..1b94e59ca0 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts @@ -51,13 +51,13 @@ describe('Editor Selectors', () => { }) it('selectRecordFieldsConfig() should return the current "fieldsConfig" state', () => { - const result = EditorSelectors.selectRecordFieldsConfig(state) + const result = EditorSelectors.selectEditorConfig(state) expect(result).toEqual(DEFAULT_FIELDS) }) describe('selectRecordFields', () => { it('should return the config and value for each field', () => { - const result = EditorSelectors.selectRecordFields(state) + const result = EditorSelectors.selectRecordSections(state) const actualSections = result.pages.map((page) => page.sections).flat() @@ -79,7 +79,7 @@ describe('Editor Selectors', () => { }) it('should not coerce falsy values to null', () => { - const result = EditorSelectors.selectRecordFields({ + const result = EditorSelectors.selectRecordSections({ ...state, editor: { ...state.editor, diff --git a/libs/feature/editor/src/lib/+state/editor.selectors.ts b/libs/feature/editor/src/lib/+state/editor.selectors.ts index 3661ace5f1..886daa909a 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.ts @@ -1,5 +1,6 @@ import { createFeatureSelector, createSelector } from '@ngrx/store' import { EDITOR_FEATURE_KEY, EditorState } from './editor.reducer' +import { EditorSectionWithValues } from './editor.models' export const selectEditorState = createFeatureSelector(EDITOR_FEATURE_KEY) @@ -34,25 +35,29 @@ export const selectRecordAlreadySavedOnce = createSelector( (state: EditorState) => state.alreadySavedOnce ) -export const selectRecordFieldsConfig = createSelector( +export const selectEditorConfig = createSelector( selectEditorState, - (state: EditorState) => state.fieldsConfig + (state: EditorState) => state.editorConfig ) -export const selectRecordFields = createSelector( +export const selectCurrentPage = createSelector( + selectEditorState, + (state: EditorState) => state.currentPage +) + +export const selectRecordSections = createSelector( selectEditorState, (state: EditorState) => { - const fieldsConfig = state.fieldsConfig - fieldsConfig.pages.forEach((page) => { - page.sections.forEach((section) => { - section.fields.forEach((field) => { - if (state.record) { - field.value = state.record[field.model] - } - }) - }) - }) - - return fieldsConfig + const currentPage = state.editorConfig.pages[state.currentPage] + if (!currentPage) { + return [] as EditorSectionWithValues[] + } + return currentPage.sections.map((section) => ({ + ...section, + fieldsWithValues: section.fields.map((fieldConfig) => ({ + config: fieldConfig, + value: state.record?.[fieldConfig.model] ?? null, + })), + })) as EditorSectionWithValues[] } ) diff --git a/libs/feature/editor/src/lib/components/record-form/record-form.component.html b/libs/feature/editor/src/lib/components/record-form/record-form.component.html index ed64b72b3a..277b79d049 100644 --- a/libs/feature/editor/src/lib/components/record-form/record-form.component.html +++ b/libs/feature/editor/src/lib/components/record-form/record-form.component.html @@ -1,7 +1,10 @@ - +
    @@ -25,14 +28,19 @@
    - + diff --git a/libs/feature/editor/src/lib/components/record-form/record-form.component.ts b/libs/feature/editor/src/lib/components/record-form/record-form.component.ts index 06863d04b7..54d4e59b87 100644 --- a/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/record-form.component.ts @@ -1,14 +1,13 @@ import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { ChangeDetectionStrategy, Component } from '@angular/core' import { EditorFacade } from '../../+state/editor.facade' -import { - EditorField, - EditorFieldPage, - EditorFieldValue, - EditorSection, -} from '../../models' +import { EditorFieldValue } from '../../models' import { FormFieldComponent } from './form-field' import { TranslateModule } from '@ngx-translate/core' +import { + EditorFieldWithValue, + EditorSectionWithValues, +} from '../../+state/editor.models' @Component({ selector: 'gn-ui-record-form', @@ -19,8 +18,6 @@ import { TranslateModule } from '@ngx-translate/core' imports: [CommonModule, FormFieldComponent, TranslateModule], }) export class RecordFormComponent { - @Input() page: EditorFieldPage - constructor(public facade: EditorFacade) {} handleFieldValueChange(model: string, newValue: EditorFieldValue) { @@ -30,11 +27,11 @@ export class RecordFormComponent { this.facade.updateRecordField(model, newValue) } - fieldTracker(index: number, field: EditorField): any { - return field.model + fieldTracker(index: number, field: EditorFieldWithValue): any { + return field.config.model } - sectionTracker(index: number, section: EditorSection): any { + sectionTracker(index: number, section: EditorSectionWithValues): any { return section.labelKey } } 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 8530049354..1380ebe9d0 100644 --- a/libs/feature/editor/src/lib/models/editor-config.model.ts +++ b/libs/feature/editor/src/lib/models/editor-config.model.ts @@ -30,7 +30,7 @@ export interface EditorField { // the result of this expression will replace the field value on save onSaveProcess?: EditorFieldExpression - value?: EditorFieldValue + // value?: EditorFieldValue } export interface EditorSection { From 7ae1be76d460ea92159665558b81553dbd337e1c Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Tue, 23 Jul 2024 09:47:58 +0200 Subject: [PATCH 073/378] code review fix --- .../page-selector.component.css | 0 .../page-selector.component.html | 2 +- .../page-selector.component.spec.ts | 20 +++++-- .../page-selector.component.ts | 2 +- .../src/app/edit/edit-page.component.spec.ts | 10 ++-- .../src/app/edit/edit-page.component.ts | 4 +- apps/metadata-editor/tsconfig.json | 3 +- assets-common/css/default-fonts.css | 8 --- assets-common/fonts/Petrona-Regular.woff2 | Bin 21296 -> 0 bytes libs/common/fixtures/src/index.ts | 2 + .../src/lib/editor}/editor.fixtures.ts | 34 ++++++------ .../fixtures/src/lib/editor}/index.ts | 0 libs/feature/editor/src/index.ts | 1 - .../src/lib/+state/editor.effects.spec.ts | 3 +- .../src/lib/+state/editor.selectors.spec.ts | 50 +++++++++++------- libs/feature/editor/tsconfig.json | 3 +- libs/feature/editor/tsconfig.lib.json | 3 +- tailwind.base.config.js | 1 - 18 files changed, 77 insertions(+), 69 deletions(-) rename apps/metadata-editor/src/app/edit/components/{breadcrumbs => page-selector}/page-selector.component.css (100%) rename apps/metadata-editor/src/app/edit/components/{breadcrumbs => page-selector}/page-selector.component.html (97%) rename apps/metadata-editor/src/app/edit/components/{breadcrumbs => page-selector}/page-selector.component.spec.ts (59%) rename apps/metadata-editor/src/app/edit/components/{breadcrumbs => page-selector}/page-selector.component.ts (94%) delete mode 100644 assets-common/fonts/Petrona-Regular.woff2 rename libs/{feature/editor/src/lib/fixtures => common/fixtures/src/lib/editor}/editor.fixtures.ts (72%) rename libs/{feature/editor/src/lib/fixtures => common/fixtures/src/lib/editor}/index.ts (100%) diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.css b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.css similarity index 100% rename from apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.css rename to apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.css diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.html similarity index 97% rename from apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html rename to apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.html index 3085adc2f5..279daf25d9 100644 --- a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.html +++ b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.html @@ -20,7 +20,7 @@ : 'bg-gray-200' " > - {{ index }} + {{ index + 1 }}
    { +class EditorFacadeMock { + editorConfig$ = new BehaviorSubject(EDITOR_CONFIG()) + setCurrentPage = jest.fn() +} + +describe('PageSelectorComponent', () => { let component: PageSelectorComponent let fixture: ComponentFixture beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], - providers: [], + providers: [ + { + provide: EditorFacade, + useClass: EditorFacadeMock, + }, + ], }).compileComponents() fixture = TestBed.createComponent(PageSelectorComponent) component = fixture.componentInstance - component.pages = EDITOR_CONFIG().pages - component.selectedPage = 0 fixture.detectChanges() }) diff --git a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.ts b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.ts similarity index 94% rename from apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.ts rename to apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.ts index 0cb4d03769..0170944011 100644 --- a/apps/metadata-editor/src/app/edit/components/breadcrumbs/page-selector.component.ts +++ b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.ts @@ -19,6 +19,6 @@ export class PageSelectorComponent { constructor(public facade: EditorFacade) {} pageSectionClickHandler(index: number) { - this.facade.setCurrentPage(index) // TODO + this.facade.setCurrentPage(index) } } diff --git a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts index 36fc290d2d..4889c0822c 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts @@ -1,14 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { EditPageComponent } from './edit-page.component' import { ActivatedRoute, Router } from '@angular/router' -import { EDITOR_CONFIG, EditorFacade } from '@geonetwork-ui/feature/editor' import { NO_ERRORS_SCHEMA } from '@angular/core' -import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { DATASET_RECORDS, EDITOR_CONFIG } from '@geonetwork-ui/common/fixtures' import { BehaviorSubject, Subject } from 'rxjs' import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { TranslateModule } from '@ngx-translate/core' -import { FindPipe } from '../pipes/filter.pipe' -import { PageSelectorComponent } from './components/breadcrumbs/page-selector.component' +import { PageSelectorComponent } from './components/page-selector/page-selector.component' +import { EditorFacade } from '@geonetwork-ui/feature/editor' const getRoute = () => ({ snapshot: { @@ -27,11 +26,11 @@ class RouterMock { class EditorFacadeMock { record$ = new BehaviorSubject(DATASET_RECORDS[0]) - recordFields$ = new BehaviorSubject(EDITOR_CONFIG()) openRecord = jest.fn() saveError$ = new Subject() saveSuccess$ = new Subject() draftSaveSuccess$ = new Subject() + editorConfig$ = new BehaviorSubject(EDITOR_CONFIG()) } class NotificationsServiceMock { showNotification = jest.fn() @@ -48,7 +47,6 @@ describe('EditPageComponent', () => { imports: [ EditPageComponent, TranslateModule.forRoot(), - FindPipe, PageSelectorComponent, ], schemas: [NO_ERRORS_SCHEMA], 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 16d2ecc7d3..5c7a54a539 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.ts @@ -15,7 +15,7 @@ import { } from '@geonetwork-ui/feature/notifications' import { TranslateModule, TranslateService } from '@ngx-translate/core' import { filter, firstValueFrom, Subscription, take } from 'rxjs' -import { PageSelectorComponent } from './components/breadcrumbs/page-selector.component' +import { PageSelectorComponent } from './components/page-selector/page-selector.component' import { marker } from '@biesbjerg/ngx-translate-extract-marker' import { map } from 'rxjs/operators' @@ -130,7 +130,7 @@ export class EditPageComponent implements OnInit, OnDestroy { if (currentPage === 0) { this.router.navigate(['catalog', 'search']) } else { - this.facade.setCurrentPage(currentPage - 1) // TODO + this.facade.setCurrentPage(currentPage - 1) } } diff --git a/apps/metadata-editor/tsconfig.json b/apps/metadata-editor/tsconfig.json index 54f2b40f0e..099e2430b3 100644 --- a/apps/metadata-editor/tsconfig.json +++ b/apps/metadata-editor/tsconfig.json @@ -19,8 +19,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "target": "es2020", - "lib": ["dom", "es2020", "dom.iterable"], - "downlevelIteration": true + "lib": ["dom", "es2020", "dom.iterable"] }, "angularCompilerOptions": { "strictInjectionParameters": true, diff --git a/assets-common/css/default-fonts.css b/assets-common/css/default-fonts.css index 34941cbd8c..fc8d76c604 100644 --- a/assets-common/css/default-fonts.css +++ b/assets-common/css/default-fonts.css @@ -1,11 +1,3 @@ -/* Petrona */ -@font-face { - font-family: 'Petrona'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(../fonts/Petrona-Regular.woff2) format('woff2'); -} /* arabic */ @font-face { font-family: 'Readex Pro'; diff --git a/assets-common/fonts/Petrona-Regular.woff2 b/assets-common/fonts/Petrona-Regular.woff2 deleted file mode 100644 index bac09590b6045853dd96334768991328183a94b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21296 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 NtxT>8=|*}0005{qHc ({ +export const EDITOR_CONFIG = () => ({ pages: [ { labelKey: 'Resource description', @@ -20,7 +18,7 @@ export const EDITOR_CONFIG = (): EditorConfig => ({ ], }) -export const EDITOR_SECTION_ABOUT = (): EditorSection => ({ +export const EDITOR_SECTION_ABOUT = () => ({ labelKey: 'About the resource', descriptionKey: 'This section describes the resource.', hidden: false, @@ -34,28 +32,28 @@ export const EDITOR_SECTION_ABOUT = (): EditorSection => ({ ], }) -export const EDITOR_SECTION_DATA_MANAGER = (): EditorSection => ({ +export const EDITOR_SECTION_DATA_MANAGER = () => ({ labelKey: 'Data manager', descriptionKey: '', hidden: false, fields: [], }) -export const EDITOR_SECTION_USE_AND_ACCESS_CONDITIONS = (): EditorSection => ({ +export const EDITOR_SECTION_USE_AND_ACCESS_CONDITIONS = () => ({ labelKey: 'Data manager', descriptionKey: '', hidden: false, fields: [EDITOR_FIELD_LICENSE()], }) -export const EDITOR_SECTION_CLASSIFICATION = (): EditorSection => ({ +export const EDITOR_SECTION_CLASSIFICATION = () => ({ labelKey: 'Classification', descriptionKey: 'The classification has an impact on the access to the data.', hidden: false, fields: [EDITOR_FIELD_KEYWORDS(), EDITOR_FIELD_UNIQUE_IDENTIFIER()], }) -export const EDITOR_FIELD_TITLE = (): EditorField => ({ +export const EDITOR_FIELD_TITLE = () => ({ model: 'title', hidden: false, value: 'Accroches vélos MEL', @@ -64,7 +62,7 @@ export const EDITOR_FIELD_TITLE = (): EditorField => ({ }, }) -export const EDITOR_FIELD_ABSTRACT = (): EditorField => ({ +export const EDITOR_FIELD_ABSTRACT = () => ({ model: 'abstract', hidden: false, value: 'Abstract', @@ -73,7 +71,7 @@ export const EDITOR_FIELD_ABSTRACT = (): EditorField => ({ }, }) -export const EDITOR_FIELD_RESOURCE_UPDATED = (): EditorField => ({ +export const EDITOR_FIELD_RESOURCE_UPDATED = () => ({ model: 'resourceUpdated', hidden: false, formFieldConfig: { @@ -81,7 +79,7 @@ export const EDITOR_FIELD_RESOURCE_UPDATED = (): EditorField => ({ }, }) -export const EDITOR_FIELD_RECORD_UPDATED = (): EditorField => ({ +export const EDITOR_FIELD_RECORD_UPDATED = () => ({ model: 'recordUpdated', hidden: false, formFieldConfig: { @@ -92,7 +90,7 @@ export const EDITOR_FIELD_RECORD_UPDATED = (): EditorField => ({ onSaveProcess: '${dateNow()}', }) -export const EDITOR_FIELD_UPDATE_FREQUENCY = (): EditorField => ({ +export const EDITOR_FIELD_UPDATE_FREQUENCY = () => ({ model: 'updateFrequency', hidden: false, formFieldConfig: { @@ -101,7 +99,7 @@ export const EDITOR_FIELD_UPDATE_FREQUENCY = (): EditorField => ({ value: 'unknown', }) -export const EDITOR_FIELD_TEMPORAL_EXTENTS = (): EditorField => ({ +export const EDITOR_FIELD_TEMPORAL_EXTENTS = () => ({ model: 'temporalExtents', hidden: false, formFieldConfig: { @@ -110,7 +108,7 @@ export const EDITOR_FIELD_TEMPORAL_EXTENTS = (): EditorField => ({ value: [], }) -export const EDITOR_FIELD_SPATIAL_EXTENTS = (): EditorField => ({ +export const EDITOR_FIELD_SPATIAL_EXTENTS = () => ({ model: 'spatialExtents', hidden: false, formFieldConfig: { @@ -118,7 +116,7 @@ export const EDITOR_FIELD_SPATIAL_EXTENTS = (): EditorField => ({ }, }) -export const EDITOR_FIELD_KEYWORDS = (): EditorField => ({ +export const EDITOR_FIELD_KEYWORDS = () => ({ model: 'keywords', hidden: false, formFieldConfig: { @@ -126,7 +124,7 @@ export const EDITOR_FIELD_KEYWORDS = (): EditorField => ({ }, }) -export const EDITOR_FIELD_UNIQUE_IDENTIFIER = (): EditorField => ({ +export const EDITOR_FIELD_UNIQUE_IDENTIFIER = () => ({ model: 'uniqueIdentifier', hidden: false, formFieldConfig: { @@ -136,7 +134,7 @@ export const EDITOR_FIELD_UNIQUE_IDENTIFIER = (): EditorField => ({ value: 'accroche_velos', }) -export const EDITOR_FIELD_LICENSE = (): EditorField => ({ +export const EDITOR_FIELD_LICENSE = () => ({ model: 'licenses', hidden: false, formFieldConfig: { @@ -145,7 +143,7 @@ export const EDITOR_FIELD_LICENSE = (): EditorField => ({ }, }) -export const EDITOR_FIELDS = (): EditorField[] => [ +export const EDITOR_FIELDS = () => [ EDITOR_FIELD_TITLE(), EDITOR_FIELD_ABSTRACT(), EDITOR_FIELD_RESOURCE_UPDATED(), diff --git a/libs/feature/editor/src/lib/fixtures/index.ts b/libs/common/fixtures/src/lib/editor/index.ts similarity index 100% rename from libs/feature/editor/src/lib/fixtures/index.ts rename to libs/common/fixtures/src/lib/editor/index.ts diff --git a/libs/feature/editor/src/index.ts b/libs/feature/editor/src/index.ts index c95afdefd4..5e6dd85211 100644 --- a/libs/feature/editor/src/index.ts +++ b/libs/feature/editor/src/index.ts @@ -9,4 +9,3 @@ export * from './lib/components/record-form/record-form.component' export * from './lib/components/wizard/wizard.component' export * from './lib/components/wizard-field/wizard-field.component' export * from './lib/components/wizard-summarize/wizard-summarize.component' -export * from './lib/fixtures' 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 7ec73663fe..49bdb73887 100644 --- a/libs/feature/editor/src/lib/+state/editor.effects.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.effects.spec.ts @@ -26,7 +26,8 @@ const initialEditorState = { saveError: null, changedSinceSave: false, alreadySavedOnce: true, - fieldsConfig: [], + editorConfig: [], + currentPage: 0, } describe('EditorEffects', () => { 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 1b94e59ca0..58053095e6 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts @@ -1,7 +1,12 @@ -import { EditorPartialState, initialEditorState } from './editor.reducer' +import { + EDITOR_FEATURE_KEY, + EditorPartialState, + initialEditorState, +} from './editor.reducer' import * as EditorSelectors from './editor.selectors' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { DEFAULT_FIELDS } from '../fields.config' +import { EditorSectionWithValues } from './editor.models' describe('Editor Selectors', () => { let state: EditorPartialState @@ -56,22 +61,27 @@ describe('Editor Selectors', () => { }) describe('selectRecordFields', () => { - it('should return the config and value for each field', () => { - const result = EditorSelectors.selectRecordSections(state) - - const actualSections = result.pages.map((page) => page.sections).flat() - - const expectedSections = DEFAULT_FIELDS.pages - .map((page) => page.sections) - .flat() - - expect(actualSections).toEqual(expectedSections) - - const actualFields = actualSections + it('should return the config and value for specified page', () => { + const recordSections = EditorSelectors.selectRecordSections(state) + + const expectedResult = DEFAULT_FIELDS.pages[0].sections.map( + (section) => ({ + ...section, + fieldsWithValues: section.fields.map((fieldConfig) => ({ + config: fieldConfig, + value: + state[EDITOR_FEATURE_KEY].record?.[fieldConfig.model] ?? null, + })), + }) + ) as EditorSectionWithValues[] + + expect(recordSections).toEqual(expectedResult) + + const actualFields = recordSections .map((section) => section.fields) .flat() - const expectedFields = expectedSections + const expectedFields = expectedResult .map((section) => section.fields) .flat() @@ -91,15 +101,17 @@ describe('Editor Selectors', () => { }, }) - const resultFields = result.pages - .flatMap((page) => page.sections) - .flatMap((section) => section.fields) + const resultFields = result.flatMap( + (section) => section.fieldsWithValues + ) const abstractField = resultFields.find( - (field) => field.model === 'abstract' + (field) => field.config.model === 'abstract' ) - const titleField = resultFields.find((field) => field.model === 'title') + const titleField = resultFields.find( + (field) => field.config.model === 'title' + ) expect(abstractField.value).toEqual('') expect(titleField.value).toEqual('') diff --git a/libs/feature/editor/tsconfig.json b/libs/feature/editor/tsconfig.json index 9e29358f76..5879bfb75c 100644 --- a/libs/feature/editor/tsconfig.json +++ b/libs/feature/editor/tsconfig.json @@ -16,8 +16,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "target": "es2020", - "lib": ["dom", "es2020", "dom.iterable"], - "downlevelIteration": true + "lib": ["dom", "es2020", "dom.iterable"] }, "angularCompilerOptions": { "strictInjectionParameters": true, diff --git a/libs/feature/editor/tsconfig.lib.json b/libs/feature/editor/tsconfig.lib.json index 2a4c9a5e36..02d129fa3b 100644 --- a/libs/feature/editor/tsconfig.lib.json +++ b/libs/feature/editor/tsconfig.lib.json @@ -7,8 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2020", "dom.iterable"], - "downlevelIteration": true + "lib": ["dom", "es2020", "dom.iterable"] }, "exclude": [ "src/test-setup.ts", diff --git a/tailwind.base.config.js b/tailwind.base.config.js index 6464667647..faea732f64 100644 --- a/tailwind.base.config.js +++ b/tailwind.base.config.js @@ -51,7 +51,6 @@ module.exports = { title: 'var(--font-family-title, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif)', // alias for serif mono: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - petrona: ['Petrona', 'serif'], }, fontSize: { 13: '13px', From f2ae2cfea2f2129208c93ceb333639a089cfaabc Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Tue, 23 Jul 2024 10:52:22 +0200 Subject: [PATCH 074/378] added test for page selector component --- .../page-selector.component.spec.ts | 33 ++++++++++- .../src/app/edit/edit-page.component.html | 58 +++++++++---------- .../src/app/edit/edit-page.component.ts | 1 - .../editor/src/lib/+state/editor.reducer.ts | 4 +- .../src/lib/+state/editor.selectors.spec.ts | 6 +- .../form-field/form-field.component.html | 2 +- libs/feature/editor/src/lib/fields.config.ts | 2 +- .../src/lib/models/editor-config.model.ts | 2 - .../src/lib/services/editor.service.spec.ts | 8 ++- 9 files changed, 72 insertions(+), 44 deletions(-) diff --git a/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.spec.ts b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.spec.ts index 5b18f3550a..14f024b9bc 100644 --- a/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.spec.ts +++ b/apps/metadata-editor/src/app/edit/components/page-selector/page-selector.component.spec.ts @@ -4,19 +4,22 @@ import { PageSelectorComponent } from './page-selector.component' import { EDITOR_CONFIG } from '@geonetwork-ui/common/fixtures' import { BehaviorSubject } from 'rxjs' import { EditorFacade } from '@geonetwork-ui/feature/editor' +import { By } from '@angular/platform-browser' class EditorFacadeMock { editorConfig$ = new BehaviorSubject(EDITOR_CONFIG()) + currentPage$ = new BehaviorSubject(0) setCurrentPage = jest.fn() } describe('PageSelectorComponent', () => { let component: PageSelectorComponent let fixture: ComponentFixture + let facade: EditorFacadeMock beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), PageSelectorComponent], providers: [ { provide: EditorFacade, @@ -27,10 +30,38 @@ describe('PageSelectorComponent', () => { fixture = TestBed.createComponent(PageSelectorComponent) component = fixture.componentInstance + facade = TestBed.inject(EditorFacade) as unknown as EditorFacadeMock fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + + it('should render the correct number of pages', () => { + const pages = fixture.debugElement.queryAll(By.css('.w-10.h-10')) + expect(pages.length).toBe(EDITOR_CONFIG().pages.length) + }) + + it('should highlight the current page', () => { + const currentPageIndex = facade.currentPage$.getValue() + const currentPageElement = fixture.debugElement.queryAll( + By.css('.w-10.h-10') + )[currentPageIndex] + expect(currentPageElement.nativeElement.classList).toContain('bg-primary') + }) + + it('should call pageSectionClickHandler with the correct index', () => { + jest.spyOn(component, 'pageSectionClickHandler') + const button = fixture.debugElement.queryAll(By.css('gn-ui-button'))[1] + button.triggerEventHandler('buttonClick', null) + fixture.detectChanges() + expect(component.pageSectionClickHandler).toHaveBeenCalledWith(1) + }) + + it('should call facade.setCurrentPage with the correct index', () => { + const index = 1 + component.pageSectionClickHandler(index) + expect(facade.setCurrentPage).toHaveBeenCalledWith(index) + }) }) 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 a71413574e..5400a6648f 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.html +++ b/apps/metadata-editor/src/app/edit/edit-page.component.html @@ -1,32 +1,30 @@ - -
    -
    - - -
    -
    -
    - -
    - -
    -
    - - {{ - (currentPage$ | async) === 0 - ? ('editor.record.form.bottomButtons.comeBackLater' | translate) - : ('editor.record.form.bottomButtons.previous' | translate) - }} - - editor.record.form.bottomButtons.next +
    +
    + + +
    +
    +
    +
    + +
    +
    + + {{ + (currentPage$ | async) === 0 + ? ('editor.record.form.bottomButtons.comeBackLater' | translate) + : ('editor.record.form.bottomButtons.previous' | translate) + }} + + editor.record.form.bottomButtons.next
    - +
    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 5c7a54a539..13813bb548 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.ts @@ -43,7 +43,6 @@ marker('editor.record.form.bottomButtons.next') export class EditPageComponent implements OnInit, OnDestroy { subscription = new Subscription() - fields$ = this.facade.currentSections$ currentPage$ = this.facade.currentPage$ pagesLength$ = this.facade.editorConfig$.pipe( map((config) => config.pages.length) diff --git a/libs/feature/editor/src/lib/+state/editor.reducer.ts b/libs/feature/editor/src/lib/+state/editor.reducer.ts index 0006741a46..d2b4e96a5c 100644 --- a/libs/feature/editor/src/lib/+state/editor.reducer.ts +++ b/libs/feature/editor/src/lib/+state/editor.reducer.ts @@ -3,7 +3,7 @@ import * as EditorActions from './editor.actions' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { SaveRecordError } from './editor.models' import { EditorConfig } from '../models' -import { DEFAULT_FIELDS } from '../fields.config' +import { DEFAULT_CONFIGURATION } from '../fields.config' export const EDITOR_FEATURE_KEY = 'editor' @@ -37,7 +37,7 @@ export const initialEditorState: EditorState = { saving: false, saveError: null, changedSinceSave: false, - editorConfig: DEFAULT_FIELDS, + editorConfig: DEFAULT_CONFIGURATION, currentPage: 0, } 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 58053095e6..6241cc02f4 100644 --- a/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts +++ b/libs/feature/editor/src/lib/+state/editor.selectors.spec.ts @@ -5,7 +5,7 @@ import { } from './editor.reducer' import * as EditorSelectors from './editor.selectors' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' -import { DEFAULT_FIELDS } from '../fields.config' +import { DEFAULT_CONFIGURATION } from '../fields.config' import { EditorSectionWithValues } from './editor.models' describe('Editor Selectors', () => { @@ -57,14 +57,14 @@ describe('Editor Selectors', () => { it('selectRecordFieldsConfig() should return the current "fieldsConfig" state', () => { const result = EditorSelectors.selectEditorConfig(state) - expect(result).toEqual(DEFAULT_FIELDS) + expect(result).toEqual(DEFAULT_CONFIGURATION) }) describe('selectRecordFields', () => { it('should return the config and value for specified page', () => { const recordSections = EditorSelectors.selectRecordSections(state) - const expectedResult = DEFAULT_FIELDS.pages[0].sections.map( + const expectedResult = DEFAULT_CONFIGURATION.pages[0].sections.map( (section) => ({ ...section, fieldsWithValues: section.fields.map((fieldConfig) => ({ 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 ec1b774a7b..5ac61e653d 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 @@ -17,7 +17,7 @@
    diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index 62a7d83bbe..8c22644b48 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -162,7 +162,7 @@ export const DATA_POINT_OF_CONTACT_SECTION: EditorSection = { *************** PAGES ***************** ************************************************************ */ -export const DEFAULT_FIELDS: EditorConfig = { +export const DEFAULT_CONFIGURATION: EditorConfig = { pages: [ { labelKey: marker('editor.record.form.page.description'), 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 1380ebe9d0..5e420e1893 100644 --- a/libs/feature/editor/src/lib/models/editor-config.model.ts +++ b/libs/feature/editor/src/lib/models/editor-config.model.ts @@ -29,8 +29,6 @@ export interface EditorField { // the result of this expression will replace the field value on save onSaveProcess?: EditorFieldExpression - - // value?: EditorFieldValue } export interface EditorSection { diff --git a/libs/feature/editor/src/lib/services/editor.service.spec.ts b/libs/feature/editor/src/lib/services/editor.service.spec.ts index c58845c36b..425b844750 100644 --- a/libs/feature/editor/src/lib/services/editor.service.spec.ts +++ b/libs/feature/editor/src/lib/services/editor.service.spec.ts @@ -5,7 +5,7 @@ import { HttpTestingController, } from '@angular/common/http/testing' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' -import { DEFAULT_FIELDS } from '../fields.config' +import { DEFAULT_CONFIGURATION } from '../fields.config' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { firstValueFrom, of } from 'rxjs' @@ -57,7 +57,7 @@ describe('EditorService', () => { let savedRecord: [CatalogRecord, string] beforeEach(async () => { savedRecord = await firstValueFrom( - service.saveRecord(SAMPLE_RECORD, DEFAULT_FIELDS) + service.saveRecord(SAMPLE_RECORD, DEFAULT_CONFIGURATION) ) }) it('calls repository.saveRecord and repository.clearRecordDraft', () => { @@ -77,7 +77,9 @@ describe('EditorService', () => { }) describe('if a new one has to be generated', () => { beforeEach(() => { - service.saveRecord(SAMPLE_RECORD, DEFAULT_FIELDS, true).subscribe() + service + .saveRecord(SAMPLE_RECORD, DEFAULT_CONFIGURATION, true) + .subscribe() }) it('clears the unique identifier of the record', () => { const expected = { From d443c910bf340a6b17aacb1f27d24fae32444fa6 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 22 Jul 2024 14:36:15 +0200 Subject: [PATCH 075/378] fix(gn4-api): adjust content-type for records api to XML Also regenerates API client This fixes an error when trying to save a record with GN 4.2.5+ --- .../gn4/src/openapi/api/records.api.service.ts | 6 +----- libs/data-access/gn4/src/spec.yaml | 8 -------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/libs/data-access/gn4/src/openapi/api/records.api.service.ts b/libs/data-access/gn4/src/openapi/api/records.api.service.ts index 6884ce91a7..1867765db7 100644 --- a/libs/data-access/gn4/src/openapi/api/records.api.service.ts +++ b/libs/data-access/gn4/src/openapi/api/records.api.service.ts @@ -7201,11 +7201,7 @@ export class RecordsApiService { } // to determine the Content-Type header - const consumes: string[] = [ - 'application/xml', - 'application/json', - 'application/x-www-form-urlencoded', - ] + const consumes: string[] = ['application/xml'] const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes) if (httpContentTypeSelected !== undefined) { diff --git a/libs/data-access/gn4/src/spec.yaml b/libs/data-access/gn4/src/spec.yaml index ee451ac7a1..bfe1eb0b59 100644 --- a/libs/data-access/gn4/src/spec.yaml +++ b/libs/data-access/gn4/src/spec.yaml @@ -1832,14 +1832,6 @@ paths: schema: type: string description: XML fragment. - application/json: - schema: - type: string - description: XML fragment. - application/x-www-form-urlencoded: - schema: - type: string - description: XML fragment. responses: default: description: default response From 2c0e9e67e777ae04df5bef05cc0a4e610f1ec25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laure-H=C3=A9l=C3=A8ne=20Bruneton?= Date: Fri, 12 Jul 2024 15:27:21 +0200 Subject: [PATCH 076/378] feat(editor): new route to duplicate local record --- apps/metadata-editor/src/app/app.routes.ts | 6 ++ .../src/app/duplicate-record.resolver.spec.ts | 84 +++++++++++++++++++ .../src/app/duplicate-record.resolver.ts | 42 ++++++++++ libs/api/metadata-converter/src/index.ts | 1 + .../src/lib/gn4/gn4-repository.spec.ts | 48 +++++++++++ .../repository/src/lib/gn4/gn4-repository.ts | 23 +++++ .../records-repository.interface.ts | 12 +++ 7 files changed, 216 insertions(+) create mode 100644 apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts create mode 100644 apps/metadata-editor/src/app/duplicate-record.resolver.ts diff --git a/apps/metadata-editor/src/app/app.routes.ts b/apps/metadata-editor/src/app/app.routes.ts index 878df29ce9..d8c5da1256 100644 --- a/apps/metadata-editor/src/app/app.routes.ts +++ b/apps/metadata-editor/src/app/app.routes.ts @@ -10,6 +10,7 @@ import { SearchRecordsComponent } from './records/search-records/search-records- import { MyOrgUsersComponent } from './my-org-users/my-org-users.component' import { MyOrgRecordsComponent } from './records/my-org-records/my-org-records.component' import { NewRecordResolver } from './new-record.resolver' +import { DuplicateRecordResolver } from './duplicate-record.resolver' export const appRoutes: Route[] = [ { path: '', redirectTo: 'catalog/search', pathMatch: 'prefix' }, @@ -101,6 +102,11 @@ export const appRoutes: Route[] = [ component: EditPageComponent, resolve: { record: NewRecordResolver }, }, + { + path: 'duplicate/:uuid', + component: EditPageComponent, + resolve: { record: DuplicateRecordResolver }, + }, { path: 'edit/:uuid', component: EditPageComponent, diff --git a/apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts b/apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts new file mode 100644 index 0000000000..643e6f4f3c --- /dev/null +++ b/apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts @@ -0,0 +1,84 @@ +import { TestBed } from '@angular/core/testing' +import { DuplicateRecordResolver } from './duplicate-record.resolver' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { NotificationsService } from '@geonetwork-ui/feature/notifications' +import { of, throwError } from 'rxjs' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { ActivatedRouteSnapshot, convertToParamMap } from '@angular/router' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { TranslateModule } from '@ngx-translate/core' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' + +class NotificationsServiceMock { + showNotification = jest.fn() +} +class RecordsRepositoryMock { + openRecordForDuplication = jest.fn(() => + of([DATASET_RECORDS[0], 'blabla', false]) + ) +} + +const activatedRoute = { + paramMap: convertToParamMap({ id: DATASET_RECORDS[0].uniqueIdentifier }), +} as ActivatedRouteSnapshot + +describe('DuplicateRecordResolver', () => { + let resolver: DuplicateRecordResolver + let recordsRepository: RecordsRepositoryInterface + let notificationsService: NotificationsService + let resolvedData: [CatalogRecord, string, boolean] + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, TranslateModule.forRoot()], + providers: [ + { provide: NotificationsService, useClass: NotificationsServiceMock }, + { + provide: RecordsRepositoryInterface, + useClass: RecordsRepositoryMock, + }, + ], + }) + resolver = TestBed.inject(DuplicateRecordResolver) + recordsRepository = TestBed.inject(RecordsRepositoryInterface) + notificationsService = TestBed.inject(NotificationsService) + }) + + it('should be created', () => { + expect(resolver).toBeTruthy() + }) + + describe('load record success', () => { + beforeEach(() => { + resolvedData = undefined + resolver.resolve(activatedRoute).subscribe((r) => (resolvedData = r)) + }) + it('should load record by uuid', () => { + expect(resolvedData).toEqual([ + DATASET_RECORDS[0], + 'blabla', + false, + ]) + }) + }) + + describe('load record failure', () => { + beforeEach(() => { + recordsRepository.openRecordForDuplication = () => + throwError(() => new Error('oopsie')) + resolvedData = undefined + resolver.resolve(activatedRoute).subscribe((r) => (resolvedData = r)) + }) + it('should not emit anything', () => { + 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', + }) + }) + }) +}) diff --git a/apps/metadata-editor/src/app/duplicate-record.resolver.ts b/apps/metadata-editor/src/app/duplicate-record.resolver.ts new file mode 100644 index 0000000000..89df9a5699 --- /dev/null +++ b/apps/metadata-editor/src/app/duplicate-record.resolver.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core' +import { ActivatedRouteSnapshot } from '@angular/router' +import { catchError, EMPTY, Observable } from 'rxjs' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { NotificationsService } from '@geonetwork-ui/feature/notifications' +import { TranslateService } from '@ngx-translate/core' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' + +@Injectable({ + providedIn: 'root', +}) +export class DuplicateRecordResolver { + constructor( + private recordsRepository: RecordsRepositoryInterface, + private notificationsService: NotificationsService, + private translateService: TranslateService + ) {} + + resolve( + route: ActivatedRouteSnapshot + ): Observable<[CatalogRecord, string, boolean]> { + return this.recordsRepository + .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' + ), + }) + return EMPTY + }) + ) + } +} diff --git a/libs/api/metadata-converter/src/index.ts b/libs/api/metadata-converter/src/index.ts index 169d8dfcb6..a5e5f3d06e 100644 --- a/libs/api/metadata-converter/src/index.ts +++ b/libs/api/metadata-converter/src/index.ts @@ -1,3 +1,4 @@ +export * from './lib/base.converter' export * from './lib/iso19139' export * from './lib/iso19115-3' export * from './lib/find-converter' 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 3853a2e45f..d9e978b790 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -323,6 +323,54 @@ describe('Gn4Repository', () => { }) }) }) + describe('openRecordForDuplication', () => { + let record: CatalogRecord + let recordSource: string + let savedOnce: boolean + + const date = new Date('2024-07-11') + jest.useFakeTimers().setSystemTime(date) + + beforeEach(async () => { + ;(gn4RecordsApi.getRecordAs as jest.Mock).mockReturnValueOnce( + of(DATASET_RECORD_SIMPLE_AS_XML).pipe(map((xml) => ({ body: xml }))) + ) + ;[record, recordSource, savedOnce] = await lastValueFrom( + repository.openRecordForDuplication('1234-5678') + ) + }) + it('calls the API to get the record as XML', () => { + expect(gn4RecordsApi.getRecordAs).toHaveBeenCalledWith( + '1234-5678', + undefined, + expect.anything(), + undefined, + undefined, + undefined, + expect.anything(), + expect.anything(), + undefined, + expect.anything() + ) + }) + it('parses the XML record into a native object, and updates the id and title', () => { + expect(record).toMatchObject({ + uniqueIdentifier: `TEMP-ID-1720656000000`, + title: + 'A very interesting dataset (un jeu de données très intéressant) (Copy)', + }) + }) + it('saves the duplicated record as draft', () => { + const hasDraft = repository.recordHasDraft(`TEMP-ID-1720656000000`) + expect(hasDraft).toBe(true) + }) + it('tells the record it has not been saved yet', () => { + expect(savedOnce).toBe(false) + }) + it('returns the record as serialized', () => { + expect(recordSource).toMatch(/ { let recordSource: string diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index fa17388957..00e2015e2f 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -24,6 +24,7 @@ import { } from '@geonetwork-ui/common/domain/model/search' import { catchError, map, tap } from 'rxjs/operators' import { + BaseConverter, findConverterForDocument, Gn4Converter, Gn4SearchResults, @@ -230,6 +231,28 @@ export class Gn4Repository implements RecordsRepositoryInterface { ) } + openRecordForDuplication( + uniqueIdentifier: string + ): Observable<[CatalogRecord, string, false] | null> { + return this.loadRecordAsXml(uniqueIdentifier).pipe( + switchMap(async (xml) => { + const converter = findConverterForDocument(xml) + const record = await converter.readRecord(xml) + return [record, converter] as [CatalogRecord, BaseConverter] + }), + switchMap(async ([record, converter]) => { + record.uniqueIdentifier = `TEMP-ID-${Date.now()}` + record.title = `${record.title} (Copy)` + const xml = await converter.writeRecord(record) + window.localStorage.setItem( + this.getLocalStorageKeyForRecord(record.uniqueIdentifier), + xml + ) + return [record, xml, false] as [CatalogRecord, string, false] + }) + ) + } + private serializeRecordToXml( record: CatalogRecord, referenceRecordSource?: string 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 a8cc83aca2..039eac3812 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -30,6 +30,18 @@ export abstract class RecordsRepositoryInterface { uniqueIdentifier: string ): Observable<[CatalogRecord, string, boolean] | null> + /** + * This emits once: + * - record object with a new unique identifier and suffixed title + * - serialized representation of the record as text + * - false, as the duplicated record is always a draft + * @param uniqueIdentifier + * @returns Observable<[CatalogRecord, string, false] | null> + */ + abstract openRecordForDuplication( + uniqueIdentifier: string + ): Observable<[CatalogRecord, string, false] | null> + /** * @param record * @param referenceRecordSource From 2f64df252d10c9cd5758194594a255e9231adff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laure-H=C3=A9l=C3=A8ne=20Bruneton?= Date: Wed, 17 Jul 2024 12:05:35 +0200 Subject: [PATCH 077/378] feat(editor): add record action menu to duplicate --- .../app/records/records-list.component.html | 1 + .../records/records-list.component.spec.ts | 14 ++++++ .../src/app/records/records-list.component.ts | 4 ++ .../search-records-list.component.html | 1 + .../search-records-list.component.spec.ts | 14 ++++++ .../search-records-list.component.ts | 4 ++ .../results-table-container.component.html | 1 + .../results-table-container.component.spec.ts | 47 ++++++++++++++----- .../results-table-container.component.ts | 5 ++ .../results-table.component.html | 34 ++++++++++++-- .../results-table.component.spec.ts | 41 ++++++++++++---- .../results-table/results-table.component.ts | 30 +++++++----- 12 files changed, 159 insertions(+), 37 deletions(-) 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 7b214aa574..710f8cfa65 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.html +++ b/apps/metadata-editor/src/app/records/records-list.component.html @@ -53,6 +53,7 @@

    >
    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 579cf33780..4cd15fdd74 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 @@ -22,6 +22,7 @@ const totalPages = 25 }) export class ResultsTableContainerComponent { @Output() recordClick = new EventEmitter() + @Output() duplicateRecord = new EventEmitter() } @Component({ @@ -136,6 +137,19 @@ describe('RecordsListComponent', () => { expect(router.navigate).toHaveBeenCalledWith(['/edit', 123]) }) }) + describe('when asking for record duplication', () => { + const uniqueIdentifier = 123 + const singleRecord = { + ...DATASET_RECORDS[0], + uniqueIdentifier, + } + beforeEach(() => { + table.duplicateRecord.emit(singleRecord) + }) + it('routes to record duplication', () => { + expect(router.navigate).toHaveBeenCalledWith(['/duplicate', 123]) + }) + }) describe('when click on pagination', () => { beforeEach(() => { pagination.newCurrentPageEvent.emit(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 a9f4b9801f..5ef8dc82b3 100644 --- a/apps/metadata-editor/src/app/records/records-list.component.ts +++ b/apps/metadata-editor/src/app/records/records-list.component.ts @@ -67,6 +67,10 @@ export class RecordsListComponent { this.router.navigate(['/edit', record.uniqueIdentifier]) } + duplicateRecord(record: CatalogRecord) { + this.router.navigate(['/duplicate', record.uniqueIdentifier]) + } + showUsers() { this.router.navigate(['/users/my-org']) } diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html index 3ad6cb07f9..d07c2c50ef 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html +++ b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html @@ -43,6 +43,7 @@

    diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts index 600317c1ff..bbbe974ba2 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts +++ b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.spec.ts @@ -30,6 +30,7 @@ const totalPages = 25 }) export class ResultsTableContainerComponent { @Output() recordClick = new EventEmitter() + @Output() duplicateRecord = new EventEmitter() } @Component({ @@ -152,6 +153,19 @@ describe('SearchRecordsComponent', () => { expect(router.navigate).toHaveBeenCalledWith(['/edit', 123]) }) }) + describe('when asking for record duplication', () => { + const uniqueIdentifier = 123 + const singleRecord = { + ...DATASET_RECORDS[0], + uniqueIdentifier, + } + beforeEach(() => { + table.duplicateRecord.emit(singleRecord) + }) + it('routes to record duplication', () => { + expect(router.navigate).toHaveBeenCalledWith(['/duplicate', 123]) + }) + }) describe('when click on pagination', () => { beforeEach(() => { pagination.newCurrentPageEvent.emit(3) diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts index 7bd7ee44b8..1a6b4ade82 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts +++ b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.ts @@ -48,6 +48,10 @@ export class SearchRecordsComponent { this.router.navigate(['/edit', record.uniqueIdentifier]) } + duplicateRecord(record: CatalogRecord) { + this.router.navigate(['/duplicate', record.uniqueIdentifier]) + } + createRecord() { this.router.navigate(['/create']) } diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.html b/libs/feature/search/src/lib/results-table/results-table-container.component.html index 0853ac710e..b0aeb07004 100644 --- a/libs/feature/search/src/lib/results-table/results-table-container.component.html +++ b/libs/feature/search/src/lib/results-table/results-table-container.component.html @@ -4,6 +4,7 @@ [selectedRecordsIdentifiers]="selectedRecords$ | async" [sortOrder]="sortBy$ | async" (recordClick)="handleRecordClick($event)" + (duplicateRecord)="handleDuplicateRecord($event)" (recordsSelectedChange)="handleRecordsSelectedChange($event[0], $event[1])" (sortByChange)="handleSortByChange($event[0], $event[1])" > 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 0c663abd04..08bb59e223 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 @@ -1,14 +1,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' -import { ResultsTableContainerComponent } from './results-table-container.component' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { By } from '@angular/platform-browser' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { SelectionService } from '@geonetwork-ui/api/repository' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { TranslateModule } from '@ngx-translate/core' import { BehaviorSubject } from 'rxjs' import { SearchFacade } from '../state/search.facade' import { SearchService } from '../utils/service/search.service' -import { SelectionService } from '@geonetwork-ui/api/repository' -import { TranslateModule } from '@ngx-translate/core' -import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { ResultsTableContainerComponent } from './results-table-container.component' class SearchFacadeMock { results$ = new BehaviorSubject(DATASET_RECORDS) @@ -40,7 +41,7 @@ describe('ResultsTableContainerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), NoopAnimationsModule], providers: [ { provide: SearchFacade, @@ -106,7 +107,7 @@ describe('ResultsTableContainerComponent', () => { }) }) - describe('clicking on a dataset', () => { + describe('clicking on a title', () => { let clickedRecord: CatalogRecord beforeEach(() => { @@ -115,14 +116,36 @@ describe('ResultsTableContainerComponent', () => { }) it('emits a recordClick event', () => { - const tableRow = fixture.debugElement.queryAll( - By.css('.table-row-cell') - )[1].nativeElement as HTMLDivElement - tableRow.parentElement.click() + const titleCell = fixture.debugElement.query( + By.css('[data-test="record-title-cell"]') + ).nativeElement as HTMLDivElement + titleCell.click() expect(clickedRecord).toEqual(DATASET_RECORDS[0]) }) }) + describe('duplicating a dataset', () => { + let recordToBeDuplicated: CatalogRecord + + beforeEach(() => { + recordToBeDuplicated = null + component.duplicateRecord.subscribe((r) => (recordToBeDuplicated = r)) + }) + + it('emits a duplicateRecord event', () => { + const menuButton = fixture.debugElement.query( + By.css('[data-test="record-menu-button"]') + ).nativeElement as HTMLButtonElement + menuButton.click() + fixture.detectChanges() + const duplicateButton = fixture.debugElement.query( + By.css('[data-test="record-menu-duplicate-button"]') + ).nativeElement as HTMLButtonElement + duplicateButton.click() + expect(recordToBeDuplicated).toEqual(DATASET_RECORDS[0]) + }) + }) + describe('#hasDraft', () => { it('calls the repository service', () => { const record = DATASET_RECORDS[0] 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 10c076e4c4..6b1cf8ebce 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 @@ -16,6 +16,7 @@ import { CommonModule } from '@angular/common' }) export class ResultsTableContainerComponent { @Output() recordClick = new EventEmitter() + @Output() duplicateRecord = new EventEmitter() records$ = this.searchFacade.results$ selectedRecords$ = this.selectionService.selectedRecordsIdentifiers$ @@ -35,6 +36,10 @@ export class ResultsTableContainerComponent { this.recordClick.emit(item as CatalogRecord) } + handleDuplicateRecord(item: unknown) { + this.duplicateRecord.emit(item as CatalogRecord) + } + handleSortByChange(col: string, order: 'asc' | 'desc') { this.searchService.setSortBy([order, col]) } 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 1b0e3d1225..8ca4f64814 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 @@ -1,7 +1,4 @@ - + @@ -34,7 +31,11 @@ record.metadata.title -
    +
    {{ item.title }} + + + + + + + + + + + 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 d940a4a614..d484cbd6e7 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 @@ -1,9 +1,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' -import { ResultsTableComponent } from './results-table.component' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { By } from '@angular/platform-browser' +import { NoopAnimationsModule } from '@angular/platform-browser/animations' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { TranslateModule } from '@ngx-translate/core' +import { ResultsTableComponent } from './results-table.component' describe('ResultsTableComponent', () => { let component: ResultsTableComponent @@ -11,7 +12,7 @@ describe('ResultsTableComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), NoopAnimationsModule], }).compileComponents() fixture = TestBed.createComponent(ResultsTableComponent) @@ -133,7 +134,7 @@ describe('ResultsTableComponent', () => { }) }) - describe('clicking on a dataset', () => { + describe('clicking on a title', () => { let clickedRecord: CatalogRecord beforeEach(() => { @@ -142,11 +143,33 @@ describe('ResultsTableComponent', () => { }) it('emits a recordClick event', () => { - const tableRow = fixture.debugElement.queryAll( - By.css('.table-row-cell') - )[1].nativeElement as HTMLDivElement - tableRow.parentElement.click() + const tableRow = fixture.debugElement.query( + By.css('[data-test="record-title-cell"]') + ).nativeElement as HTMLDivElement + tableRow.click() expect(clickedRecord).toEqual(DATASET_RECORDS[0]) }) }) + + describe('duplicating a dataset', () => { + let recordToBeDuplicated: CatalogRecord + + beforeEach(() => { + recordToBeDuplicated = null + component.duplicateRecord.subscribe((r) => (recordToBeDuplicated = r)) + }) + + it('emits a duplicateRecord event', () => { + const menuButton = fixture.debugElement.query( + By.css('[data-test="record-menu-button"]') + ).nativeElement as HTMLButtonElement + menuButton.click() + fixture.detectChanges() + const duplicateButton = fixture.debugElement.query( + By.css('[data-test="record-menu-duplicate-button"]') + ).nativeElement as HTMLButtonElement + duplicateButton.click() + expect(recordToBeDuplicated).toEqual(DATASET_RECORDS[0]) + }) + }) }) diff --git a/libs/ui/search/src/lib/results-table/results-table.component.ts b/libs/ui/search/src/lib/results-table/results-table.component.ts index 6752d70fdc..ec3307df0c 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 @@ -1,23 +1,24 @@ +import { CommonModule } from '@angular/common' import { Component, EventEmitter, Input, Output } from '@angular/core' +import { MatIconModule } from '@angular/material/icon' +import { MatMenuModule } from '@angular/material/menu' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { - FileFormat, - getBadgeColor, - getFileFormat, - getFormatPriority, -} from '@geonetwork-ui/util/shared' + FieldSort, + SortByField, +} from '@geonetwork-ui/common/domain/model/search' import { BadgeComponent, UiInputsModule } from '@geonetwork-ui/ui/inputs' import { InteractiveTableColumnComponent, InteractiveTableComponent, } from '@geonetwork-ui/ui/layout' -import { MatIconModule } from '@angular/material/icon' -import { TranslateModule } from '@ngx-translate/core' -import { CommonModule } from '@angular/common' import { - FieldSort, - SortByField, -} from '@geonetwork-ui/common/domain/model/search' + FileFormat, + getBadgeColor, + getFileFormat, + getFormatPriority, +} from '@geonetwork-ui/util/shared' +import { TranslateModule } from '@ngx-translate/core' @Component({ selector: 'gn-ui-results-table', @@ -32,6 +33,7 @@ import { MatIconModule, TranslateModule, BadgeComponent, + MatMenuModule, ], }) export class ResultsTableComponent { @@ -43,6 +45,7 @@ export class ResultsTableComponent { // emits the column (field) as well as the order @Output() sortByChange = new EventEmitter<[string, 'asc' | 'desc']>() @Output() recordClick = new EventEmitter() + @Output() duplicateRecord = new EventEmitter() @Output() recordsSelectedChange = new EventEmitter< [CatalogRecord[], boolean] >() @@ -89,6 +92,11 @@ export class ResultsTableComponent { this.recordClick.emit(item as CatalogRecord) } + handleDuplicateClick(event: Event, item: unknown) { + event.stopPropagation() + this.duplicateRecord.emit(item as CatalogRecord) + } + setSortBy(col: string, order: 'asc' | 'desc') { this.sortByChange.emit([col, order]) } From 88aebab6db10a06f00ddf12d51322074fe31de22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laure-H=C3=A9l=C3=A8ne=20Bruneton?= Date: Wed, 17 Jul 2024 12:12:33 +0200 Subject: [PATCH 078/378] chore: i18n --- translations/de.json | 1 + translations/en.json | 1 + translations/es.json | 1 + translations/fr.json | 1 + translations/it.json | 1 + translations/nl.json | 1 + translations/pt.json | 1 + translations/sk.json | 1 + 8 files changed, 8 insertions(+) diff --git a/translations/de.json b/translations/de.json index 12b58965fd..0c68cda1c5 100644 --- a/translations/de.json +++ b/translations/de.json @@ -306,6 +306,7 @@ "pagination.pageOf": "von", "previous": "zurück", "record.action.download": "Herunterladen", + "record.action.duplicate": "", "record.action.view": "Anzeigen", "record.externalViewer.open": "In externem Kartenviewer öffnen", "record.metadata.about": "Beschreibung", diff --git a/translations/en.json b/translations/en.json index 4055d950f6..e9e90492da 100644 --- a/translations/en.json +++ b/translations/en.json @@ -306,6 +306,7 @@ "pagination.pageOf": "of", "previous": "previous", "record.action.download": "Download", + "record.action.duplicate": "Duplicate", "record.action.view": "View", "record.externalViewer.open": "Open in the external map viewer", "record.metadata.about": "Description", diff --git a/translations/es.json b/translations/es.json index 54dfd827fb..09cc05c920 100644 --- a/translations/es.json +++ b/translations/es.json @@ -306,6 +306,7 @@ "pagination.pageOf": "", "previous": "", "record.action.download": "", + "record.action.duplicate": "", "record.action.view": "", "record.externalViewer.open": "", "record.metadata.about": "", diff --git a/translations/fr.json b/translations/fr.json index 29374cb89a..22fc4b9a14 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -306,6 +306,7 @@ "pagination.pageOf": "sur", "previous": "précédent", "record.action.download": "Télécharger", + "record.action.duplicate": "Dupliquer", "record.action.view": "Voir", "record.externalViewer.open": "Ouvrir dans le visualiseur externe", "record.metadata.about": "Description", diff --git a/translations/it.json b/translations/it.json index b945330f98..fa5713b6dd 100644 --- a/translations/it.json +++ b/translations/it.json @@ -306,6 +306,7 @@ "pagination.pageOf": "di", "previous": "precedente", "record.action.download": "Scarica", + "record.action.duplicate": "", "record.action.view": "Visualizza", "record.externalViewer.open": "Apri nell'visualizzatore esterno", "record.metadata.about": "Descrizione", diff --git a/translations/nl.json b/translations/nl.json index 4fcfa583f1..49b4c7bdf2 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -306,6 +306,7 @@ "pagination.pageOf": "", "previous": "", "record.action.download": "", + "record.action.duplicate": "", "record.action.view": "", "record.externalViewer.open": "", "record.metadata.about": "", diff --git a/translations/pt.json b/translations/pt.json index f6782e7344..eae9dbab81 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -306,6 +306,7 @@ "pagination.pageOf": "", "previous": "", "record.action.download": "", + "record.action.duplicate": "", "record.action.view": "", "record.externalViewer.open": "", "record.metadata.about": "", diff --git a/translations/sk.json b/translations/sk.json index 2df4e8b133..bafcd95d21 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -306,6 +306,7 @@ "pagination.pageOf": "z", "previous": "predchádzajúci", "record.action.download": "Stiahnuť", + "record.action.duplicate": "", "record.action.view": "Zobraziť", "record.externalViewer.open": "Otvoriť v externom mapovom prehliadači", "record.metadata.about": "O", From 0adaf9e626bd634adfcacee28f269a49874e6ecf Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Tue, 23 Jul 2024 16:44:31 +0200 Subject: [PATCH 079/378] fix(datahub): fixed RECORD_HAS_NO_LINK error block. --- .../record/record-metadata/record-metadata.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 43c712f165..3d485c2c0f 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 @@ -55,13 +55,17 @@ export class RecordMetadataComponent { ) displayDatasetHasNoLinkBlock$ = combineLatest([ + this.metadataViewFacade.isMetadataLoading$, this.displayDownload$, this.displayApi$, this.displayOtherLinks, ]).pipe( map( - ([displayDownload, displayApi, displayOtherLinks]) => - !displayDownload && !displayApi && !displayOtherLinks + ([isMetadataLoading, displayDownload, displayApi, displayOtherLinks]) => + !isMetadataLoading && + !displayDownload && + !displayApi && + !displayOtherLinks ) ) From c0329f42cce22892c7d532d88ad09c75f37fc513 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 22 Jul 2024 17:26:56 +0200 Subject: [PATCH 080/378] e2e(me): write basic test for editor form --- apps/metadata-editor-e2e/src/e2e/edit.cy.ts | 60 +++++++++++++++++++ .../top-toolbar/top-toolbar.component.html | 2 + tools/e2e/commands.ts | 13 ++++ 3 files changed, 75 insertions(+) create mode 100644 apps/metadata-editor-e2e/src/e2e/edit.cy.ts diff --git a/apps/metadata-editor-e2e/src/e2e/edit.cy.ts b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts new file mode 100644 index 0000000000..ed249ca863 --- /dev/null +++ b/apps/metadata-editor-e2e/src/e2e/edit.cy.ts @@ -0,0 +1,60 @@ +describe('editor form', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + + // Alpine convention record + cy.visit('/edit/8698bf0b-fceb-4f0f-989b-111e7c4af0a4') + + cy.clearRecordDrafts() + + // aliases + cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea').as( + 'abstractField' + ) + cy.get('@abstractField').invoke('val').as('abstractFieldInitialValue') + cy.get('[data-cy=save-status]') + .invoke('attr', 'data-cy-value') + .as('saveStatus') + }) + + it('form shows correctly', () => { + cy.get('gn-ui-record-form').should('be.visible') + cy.get('gn-ui-record-form gn-ui-form-field').should('have.length.gt', 0) + cy.get('@abstractField') + .invoke('val') + .should('contain', 'Perimeter der Alpenkonvention in der Schweiz.') + cy.get('@saveStatus').should('eq', 'record_up_to_date') + cy.screenshot({ capture: 'fullPage' }) + }) + + it('draft record is kept', () => { + cy.get('@abstractField').clear() + cy.get('@abstractField').type('modified abstract') + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(1000) // waiting for draft saving to kick in + cy.reload() + cy.get('@abstractField').invoke('val').should('eq', 'modified abstract') + cy.get('@saveStatus').should('eq', 'draft_changes_pending') + + cy.clearRecordDrafts() + + cy.get('@saveStatus').should('eq', 'record_up_to_date') + cy.get('@abstractField') + .invoke('val') + .should('contain', 'Perimeter der Alpenkonvention in der Schweiz.') + }) + + it('saving record works', () => { + cy.get('@abstractField').clear() + cy.get('@abstractField').type('modified abstract before saving') + cy.get('md-editor-publish-button').click() + cy.get('@saveStatus').should('eq', 'record_up_to_date') + + // restore abstract + cy.get('@abstractField').clear() + cy.get('@abstractField').then(function (field) { + cy.wrap(field).type(this.abstractFieldInitialValue) + }) + cy.get('md-editor-publish-button').click() + }) +}) 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 89cd41f9d4..b1f5cec581 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 @@ -13,6 +13,8 @@
    diff --git a/tools/e2e/commands.ts b/tools/e2e/commands.ts index 0c06e446e3..736b225890 100644 --- a/tools/e2e/commands.ts +++ b/tools/e2e/commands.ts @@ -15,6 +15,7 @@ declare namespace Cypress { login(username?: string, password?: string, redirect?: boolean): void signOut(): void clearFavorites(): void + clearRecordDrafts(): void // interaction with gn-ui-dropdown-selector openDropdown(): Chainable> @@ -139,6 +140,18 @@ Cypress.Commands.add( } ) +Cypress.Commands.add('clearRecordDrafts', () => { + cy.window().then((window) => { + const items = { ...window.localStorage } + const draftKeys = Object.keys(items).filter((key) => + key.startsWith('geonetwork-ui-draft-') + ) + draftKeys.forEach((key) => window.localStorage.removeItem(key)) + cy.log(`Cleared ${draftKeys.length} draft(s).`) + }) + cy.reload() +}) + // -- This is a parent command -- // Cypress.Commands.add('login', (email, password) => { ... }) // From 0af639709ecf55520db6925fb204b5ec95574d46 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 22 Jul 2024 17:27:55 +0200 Subject: [PATCH 081/378] feat(support): handle GN versions 4.4+, use ES 7.17 minor improvements to init service to improve readability --- support-services/.env | 1 + support-services/docker-compose.yml | 25 +++++++++++++------ .../docker-entrypoint.d/04-upload-thesauri.sh | 1 + 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/support-services/.env b/support-services/.env index 4ee8267eca..9438647d17 100644 --- a/support-services/.env +++ b/support-services/.env @@ -1 +1,2 @@ GEONETWORK_VERSION=4.2.2 +ELASTICSEARCH_VERSION=7.17.15 diff --git a/support-services/docker-compose.yml b/support-services/docker-compose.yml index 5bcc64c8ea..783cf1563b 100644 --- a/support-services/docker-compose.yml +++ b/support-services/docker-compose.yml @@ -21,7 +21,7 @@ services: - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.15.1 + image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION} ulimits: memlock: soft: -1 @@ -80,15 +80,26 @@ services: GEONETWORK_DB_NAME: geonetwork GEONETWORK_DB_USERNAME: geonetwork GEONETWORK_DB_PASSWORD: geonetwork + DATA_DIR: /catalogue-data + VIRTUAL_HOST: localhost + JAVA_OPTS: > -Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF -Djava.security.egd=file:/dev/./urandom -Djava.awt.headless=true -Xms512M -Xss512M -Xmx2G -XX:+UseConcMarkSweepGC - -Dgeonetwork.resources.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/data/resources - -Dgeonetwork.data.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/data - -Dgeonetwork.codeList.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/config/codelist - -Dgeonetwork.schema.dir=/var/lib/jetty/webapps/geonetwork/WEB-INF/data/config/schema_plugins -Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:5005 + -Dgeonetwork.resources.dir=/catalogue-data/data/resources + -Dgeonetwork.data.dir=/catalogue-data/data + -Dgeonetwork.codeList.dir=/catalogue-data/config/codelist + -Dgeonetwork.schema.dir=/catalogue-data/config/schema_plugins + -Dgeonetwork.config.dir=/catalogue-data/config + -Dgeonetwork.indexConfig.dir=/catalogue-data/config/index + -Des.host=elasticsearch + -Des.protocol=http + -Des.port=9200 + -Des.url=http://elasticsearch:9200 + -Des.username= + -Des.password= depends_on: database: condition: service_healthy @@ -104,7 +115,7 @@ services: timeout: 10s retries: 10 volumes: - - geonetwork_data:/var/lib/jetty/webapps/geonetwork/WEB-INF/data/ + - geonetwork_data:/catalogue-data/ ports: - '8080:8080' - '5005:5005' @@ -123,7 +134,7 @@ services: init: image: alpine/curl # only run init if volumes were cleared - command: sh -c "if [ ! -f /done ]; then run-parts /docker-entrypoint.d --exit-on-error; else echo 'Nothing to do.'; exit 0; fi" + command: sh -c -e "if [ ! -f /done ]; then run-parts /docker-entrypoint.d --exit-on-error; else echo 'Nothing to do.'; exit 0; fi" environment: GEONETWORK_VERSION: ${GEONETWORK_VERSION} depends_on: diff --git a/support-services/docker-entrypoint.d/04-upload-thesauri.sh b/support-services/docker-entrypoint.d/04-upload-thesauri.sh index 738b0eed69..ae898293be 100755 --- a/support-services/docker-entrypoint.d/04-upload-thesauri.sh +++ b/support-services/docker-entrypoint.d/04-upload-thesauri.sh @@ -15,5 +15,6 @@ do -H 'Accept: application/json, text/plain, */*' \ -H "Cookie: JSESSIONID=$jsessionid; XSRF-TOKEN=$xsrf_token" \ -H "X-XSRF-TOKEN: $xsrf_token" + echo "" done From 8ddb832a32efeca77856ffc115714bc37c5c1196 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 24 Jul 2024 08:27:43 +0200 Subject: [PATCH 082/378] update e2e tests --- .../src/e2e/datasetDetailPage.cy.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 2b7dde9933..747469a5e5 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -1,5 +1,4 @@ import 'cypress-real-events' -import { tile } from 'ol/loadingstrategy' import path from 'path' beforeEach(() => { @@ -94,6 +93,13 @@ describe('dataset pages', () => { }) describe('GENERAL : display & functions', () => { + describe('no-link-error block', () => { + it("shouldn't be there until metadata is fully loaded", () => { + cy.visit('/dataset/a3774ef6-809d-4dd1-984f-9254f49cbd0a') + cy.get('[data-test=dataset-has-no-link-block]').should('not.exist') + }) + }) + describe('header', () => { it('should display the title, favorite star group and arrow back', () => { cy.get('datahub-header-record') @@ -590,11 +596,25 @@ describe('dataset pages', () => { }) }) - describe('When there is no link', () => { - beforeEach(() => { - cy.visit('/dataset/a3774ef6-809d-4dd1-984f-9254f49cbd0a') - }) + describe.only('When there is no link', () => { it('display the error datasetHasNoLink error block', () => { + cy.login() + + cy.intercept( + 'GET', + '/geonetwork/srv/api/userfeedback?metadataUuid=a3774ef6-809d-4dd1-984f-9254f49cbd0a', + (req) => { + // Test if the error block is not shown before the metadata is fully loaded + cy.get('[data-test="dataset-has-no-link-block"]').should( + 'not.exist' + ) + } + ).as('getData') + + cy.visit('/dataset/a3774ef6-809d-4dd1-984f-9254f49cbd0a') + + cy.wait('@getData') + cy.get('[data-test="dataset-has-no-link-block"]').should('exist') }) }) From 6a2ba38bdc5aa3e7d9d3bc9635bd35fb217d50c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laure-H=C3=A9l=C3=A8ne=20Bruneton?= Date: Wed, 24 Jul 2024 09:17:23 +0200 Subject: [PATCH 083/378] feat(editor): revert click on record, use gn-ui-button for menu --- .../repository/src/lib/gn4/gn4-repository.ts | 11 +++---- .../results-table-container.component.spec.ts | 10 +++---- .../action-menu/action-menu.component.css | 0 .../action-menu/action-menu.component.html | 17 +++++++++++ .../action-menu/action-menu.component.spec.ts | 22 ++++++++++++++ .../action-menu/action-menu.component.ts | 22 ++++++++++++++ .../results-table.component.html | 29 +++++-------------- .../results-table.component.spec.ts | 10 +++---- .../results-table/results-table.component.ts | 16 ++++++---- 9 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 libs/ui/search/src/lib/results-table/action-menu/action-menu.component.css create mode 100644 libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html create mode 100644 libs/ui/search/src/lib/results-table/action-menu/action-menu.component.spec.ts create mode 100644 libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 00e2015e2f..d16e72c14d 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -235,15 +235,12 @@ export class Gn4Repository implements RecordsRepositoryInterface { uniqueIdentifier: string ): Observable<[CatalogRecord, string, false] | null> { return this.loadRecordAsXml(uniqueIdentifier).pipe( - switchMap(async (xml) => { - const converter = findConverterForDocument(xml) - const record = await converter.readRecord(xml) - return [record, converter] as [CatalogRecord, BaseConverter] - }), - switchMap(async ([record, converter]) => { + switchMap(async (recordAsXml) => { + const converter = findConverterForDocument(recordAsXml) + const record = await converter.readRecord(recordAsXml) record.uniqueIdentifier = `TEMP-ID-${Date.now()}` record.title = `${record.title} (Copy)` - const xml = await converter.writeRecord(record) + const xml = await converter.writeRecord(record, recordAsXml) window.localStorage.setItem( this.getLocalStorageKeyForRecord(record.uniqueIdentifier), xml 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 08bb59e223..eb843061a3 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 @@ -107,7 +107,7 @@ describe('ResultsTableContainerComponent', () => { }) }) - describe('clicking on a title', () => { + describe('clicking on a dataset', () => { let clickedRecord: CatalogRecord beforeEach(() => { @@ -116,10 +116,10 @@ describe('ResultsTableContainerComponent', () => { }) it('emits a recordClick event', () => { - const titleCell = fixture.debugElement.query( - By.css('[data-test="record-title-cell"]') - ).nativeElement as HTMLDivElement - titleCell.click() + const tableRow = fixture.debugElement.queryAll( + By.css('.table-row-cell') + )[1].nativeElement as HTMLDivElement + tableRow.parentElement.click() expect(clickedRecord).toEqual(DATASET_RECORDS[0]) }) }) diff --git a/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.css b/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html b/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html new file mode 100644 index 0000000000..f423d03193 --- /dev/null +++ b/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html @@ -0,0 +1,17 @@ + + more_vert + + + + diff --git a/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.spec.ts b/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.spec.ts new file mode 100644 index 0000000000..0477818afe --- /dev/null +++ b/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { TranslateModule } from '@ngx-translate/core' +import { ActionMenuComponent } from './action-menu.component' + +describe('ActionMenuComponent', () => { + let component: ActionMenuComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(ActionMenuComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts b/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts new file mode 100644 index 0000000000..155692316e --- /dev/null +++ b/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts @@ -0,0 +1,22 @@ +import { Component, EventEmitter, Output, ViewChild } from '@angular/core' +import { MatIconModule } from '@angular/material/icon' +import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' + +@Component({ + selector: 'gn-ui-action-menu', + templateUrl: './action-menu.component.html', + styleUrls: ['./action-menu.component.css'], + standalone: true, + imports: [MatIconModule, ButtonComponent, MatMenuModule, TranslateModule], +}) +export class ActionMenuComponent { + @Output() duplicate = new EventEmitter() + + @ViewChild(MatMenuTrigger) trigger: MatMenuTrigger + + openMenu() { + this.trigger.openMenu() + } +} 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 8ca4f64814..c062fd521c 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 @@ -1,4 +1,7 @@ - + @@ -31,11 +34,7 @@ record.metadata.title -
    +
    {{ item.title }} - - - - + + 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 d484cbd6e7..53b25e12c7 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 @@ -134,7 +134,7 @@ describe('ResultsTableComponent', () => { }) }) - describe('clicking on a title', () => { + describe('clicking on a dataset', () => { let clickedRecord: CatalogRecord beforeEach(() => { @@ -143,10 +143,10 @@ describe('ResultsTableComponent', () => { }) it('emits a recordClick event', () => { - const tableRow = fixture.debugElement.query( - By.css('[data-test="record-title-cell"]') - ).nativeElement as HTMLDivElement - tableRow.click() + const tableRow = fixture.debugElement.queryAll( + By.css('.table-row-cell') + )[1].nativeElement as HTMLDivElement + tableRow.parentElement.click() expect(clickedRecord).toEqual(DATASET_RECORDS[0]) }) }) diff --git a/libs/ui/search/src/lib/results-table/results-table.component.ts b/libs/ui/search/src/lib/results-table/results-table.component.ts index ec3307df0c..3d7dba9f0a 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 @@ -1,7 +1,13 @@ import { CommonModule } from '@angular/common' -import { Component, EventEmitter, Input, Output } from '@angular/core' +import { + Component, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core' import { MatIconModule } from '@angular/material/icon' -import { MatMenuModule } from '@angular/material/menu' +import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { FieldSort, @@ -19,6 +25,7 @@ import { getFormatPriority, } from '@geonetwork-ui/util/shared' import { TranslateModule } from '@ngx-translate/core' +import { ActionMenuComponent } from './action-menu/action-menu.component' @Component({ selector: 'gn-ui-results-table', @@ -33,7 +40,7 @@ import { TranslateModule } from '@ngx-translate/core' MatIconModule, TranslateModule, BadgeComponent, - MatMenuModule, + ActionMenuComponent, ], }) export class ResultsTableComponent { @@ -92,8 +99,7 @@ export class ResultsTableComponent { this.recordClick.emit(item as CatalogRecord) } - handleDuplicateClick(event: Event, item: unknown) { - event.stopPropagation() + handleDuplicate(item: unknown) { this.duplicateRecord.emit(item as CatalogRecord) } From 0587e8a29bfaa21444b118047aa69b1d068414fc Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 24 Jul 2024 08:29:09 +0200 Subject: [PATCH 084/378] removed the duplicate "Record with no link" dataset --- .../src/e2e/datasetDetailPage.cy.ts | 2 +- .../record-metadata.component.spec.ts | 46 +++++++++++++----- .../docker-entrypoint-initdb.d/dump | Bin 460655 -> 460017 bytes 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 747469a5e5..cc8a846dd5 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -596,7 +596,7 @@ describe('dataset pages', () => { }) }) - describe.only('When there is no link', () => { + describe('When there is no link', () => { it('display the error datasetHasNoLink error block', () => { cy.login() 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 0b23b693b5..7726e60cdd 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 @@ -45,6 +45,7 @@ class MdViewFacadeMock { otherLinks$ = new BehaviorSubject([]) related$ = new BehaviorSubject(null) error$ = new BehaviorSubject(null) + isMetadataLoading$ = new BehaviorSubject(false) } class SearchServiceMock { @@ -659,20 +660,39 @@ describe('RecordMetadataComponent', () => { }) describe('When there are no link (download, api or other links)', () => { - beforeEach(() => { - facade.apiLinks$.next([]) - facade.downloadLinks$.next([]) - facade.otherLinks$.next([]) - fixture.detectChanges() + describe('When the metadata is not fully loaded', () => { + beforeEach(() => { + facade.isMetadataLoading$.next(true) + facade.apiLinks$.next([]) + facade.downloadLinks$.next([]) + facade.otherLinks$.next([]) + fixture.detectChanges() + }) + it("doesn' show the no link error block", () => { + const result = fixture.debugElement.query( + By.css('[data-test="dataset-has-no-link-block"]') + ) + expect(result).toBeFalsy() + }) }) - it('shows the no link error block', () => { - const result = fixture.debugElement.query( - By.css('[data-test="dataset-has-no-link-block"]') - ) - expect(result).toBeTruthy() - expect(result.componentInstance.type).toBe( - ErrorType.DATASET_HAS_NO_LINK - ) + + describe('When the metadata is not fully loaded', () => { + beforeEach(() => { + facade.isMetadataLoading$.next(false) + facade.apiLinks$.next([]) + facade.downloadLinks$.next([]) + facade.otherLinks$.next([]) + fixture.detectChanges() + }) + it('shows the no link error block', () => { + const result = fixture.debugElement.query( + By.css('[data-test="dataset-has-no-link-block"]') + ) + expect(result).toBeTruthy() + expect(result.componentInstance.type).toBe( + ErrorType.DATASET_HAS_NO_LINK + ) + }) }) }) }) diff --git a/support-services/docker-entrypoint-initdb.d/dump b/support-services/docker-entrypoint-initdb.d/dump index f1f88acdffc6bb1d1fc7a5db1139104e15296fc2..55bf76ca5eed2c3e04405d78e96c6f7c7012b1ee 100644 GIT binary patch delta 20396 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 delta 21061 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 Date: Wed, 24 Jul 2024 11:27:28 +0200 Subject: [PATCH 085/378] feat(header-record): use gn-ui-badge and make its opacity configurable --- .../header-record/header-record.component.html | 15 ++++++++++----- tailwind.base.css | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/datahub/src/app/record/header-record/header-record.component.html b/apps/datahub/src/app/record/header-record/header-record.component.html index 6edea676e0..93eaf33a33 100644 --- a/apps/datahub/src/app/record/header-record/header-record.component.html +++ b/apps/datahub/src/app/record/header-record/header-record.component.html @@ -41,12 +41,17 @@ class="flex flex-row flex-wrap gap-4 mb-4 ml-4 sm:mr-[332px]" [style.color]="foregroundColor" > -
    - + + my_location - -

    record.metadata.type

    -
    + + record.metadata.type +

    record.metadata.lastUpdate diff --git a/tailwind.base.css b/tailwind.base.css index 9bce5e4729..db753d1abc 100644 --- a/tailwind.base.css +++ b/tailwind.base.css @@ -115,7 +115,8 @@ --padding: var(--gn-ui-badge-padding, 0.375em 0.75em); --text-color: var(--gn-ui-badge-text-color, var(--color-gray-50)); --background-color: var(--gn-ui-badge-background-color, black); - @apply inline-block opacity-70 p-[--padding] rounded-[--rounded] + --opacity: var(--gn-ui-badge-opacity, 0.7); + @apply inline-block opacity-[--opacity] p-[--padding] rounded-[--rounded] font-medium text-[length:0.875em] leading-none text-[color:--text-color] bg-[color:--background-color]; } /* makes sure icons will not make the badges grow vertically; also make size proportional */ From b242a958e6c4110d021f62322029918d8d42dcb9 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 24 Jul 2024 12:03:44 +0200 Subject: [PATCH 086/378] test(header-record): fix e2e test --- apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 2b7dde9933..455b3a0735 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -118,7 +118,7 @@ describe('dataset pages', () => { .find('.font-title') .next() .as('infoBar') - cy.get('@infoBar').children('div').should('have.length', 3) + cy.get('@infoBar').children().should('have.length', 3) }) it('should return to the dataset list', () => { cy.get('datahub-header-record') From 7664e9f5d8d8b99dcaa107ccab68b551e833c2a7 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 23 Jul 2024 11:24:19 +0200 Subject: [PATCH 087/378] ci: run e2e tests with various GN versions + misc improvements to the workflow --- .github/workflows/artifacts.yml | 6 ++-- .github/workflows/checks.yml | 52 ++++++++++++++++------------- .github/workflows/snyk-security.yml | 2 +- .github/workflows/webcomponents.yml | 2 +- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index ce7c56b06b..73a86dab8a 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ needs.checks.outputs.ref }} # use the PR head ref if applicable; otherwise keep default behaviour persist-credentials: false @@ -57,9 +57,7 @@ jobs: cache: 'npm' - name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: nrwl/nx-set-shas@v2 - with: - main-branch-name: 'main' + uses: nrwl/nx-set-shas@v3 - name: Install dependencies run: npm ci diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 96aeaa13e3..6fa3fe4f24 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 @@ -38,9 +38,7 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: nrwl/nx-set-shas@v2 - with: - main-branch-name: 'main' + uses: nrwl/nx-set-shas@v3 - run: npm ci - run: npx nx format:check - run: npx nx affected -t lint --parallel=3 @@ -79,7 +77,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 @@ -89,9 +87,7 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: nrwl/nx-set-shas@v2 - with: - main-branch-name: 'main' + uses: nrwl/nx-set-shas@v3 - run: npm ci - run: npx nx affected -t build --parallel=3 @@ -102,7 +98,7 @@ jobs: steps: - name: Checkout branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 @@ -142,13 +138,18 @@ jobs: comment_tag: build-options GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - cypress-run: - name: End-to-end tests + e2e-run: + name: End-to-end tests, GeoNetwork v${{ matrix.gn_version }} runs-on: ubuntu-latest - outputs: - screenshotsUrl: ${{ steps.upload-screenshots.outputs.artifact-url }} + strategy: + fail-fast: false + matrix: + gn_version: [4.2.2, 4.2.8, 4.4.0] steps: - uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v3 @@ -156,24 +157,29 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: 'npm' + - name: Derive appropriate SHAs for base and head for `nx affected` commands + uses: nrwl/nx-set-shas@v3 + - name: Create pipeline docker image - run: cd tools && docker build . -f pipelines/Dockerfile -t geonetwork/geonetwork-ui-tools-pipelines:latest + working-directory: tools + run: docker build . -f pipelines/Dockerfile -t geonetwork/geonetwork-ui-tools-pipelines:latest - - name: Build the backend - run: sudo docker-compose -f support-services/docker-compose.yml up -d init + - name: Start up backend support services + env: + GEONETWORK_VERSION: ${{ matrix.gn_version }} + working-directory: support-services + run: docker compose up -d init - - name: Install dependencies - run: | - npm ci + - run: npm ci - - name: Run tests - run: npx nx run-many --target=e2e + - name: Run e2e tests + run: npx nx affected --target=e2e - uses: actions/upload-artifact@v4 if: always() id: upload-screenshots with: - name: cypress-screenshots + name: cypress-screenshots-gn-${{ matrix.gn_version }} path: | apps/datahub-e2e/cypress/screenshots/**/* apps/metadata-editor-e2e/cypress/screenshots/**/* @@ -193,7 +199,7 @@ jobs: steps: - name: Checkout branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/snyk-security.yml b/.github/workflows/snyk-security.yml index e114be3fed..57461efd08 100644 --- a/.github/workflows/snyk-security.yml +++ b/.github/workflows/snyk-security.yml @@ -35,7 +35,7 @@ jobs: security-events: write # for github/codeql-action/upload-sarif to upload SARIF results runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run Snyk to check for vulnerabilities uses: snyk/actions/node@master diff --git a/.github/workflows/webcomponents.yml b/.github/workflows/webcomponents.yml index e090eb78bb..fbcea28878 100644 --- a/.github/workflows/webcomponents.yml +++ b/.github/workflows/webcomponents.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ needs.checks.outputs.ref }} persist-credentials: false From 9696425a9fc932d33b996ca0291861e1fb79de53 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 23 Jul 2024 11:38:21 +0200 Subject: [PATCH 088/378] feat(es): improve md-quality pipeline to handle multilingual orgs --- tools/pipelines/register-es-pipelines.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/pipelines/register-es-pipelines.js b/tools/pipelines/register-es-pipelines.js index 5acb18ccfd..d61d3ea77b 100644 --- a/tools/pipelines/register-es-pipelines.js +++ b/tools/pipelines/register-es-pipelines.js @@ -69,9 +69,14 @@ if(ctx.resourceTitleObject != null && ctx.resourceTitleObject.default != null && if(ctx.resourceAbstractObject != null && ctx.resourceAbstractObject.default != null && ctx.resourceAbstractObject.default != '') { ok++ } +// this checks for single-language Organizations (GN 4.2.2) if(ctx.contact != null && ctx.contact.length > 0 && ctx.contact[0].organisation != null && ctx.contact[0].organisation != '') { ok++ } +// this checks for multilingual Organizations (GN 4.2.3+) +if(ctx.contact != null && ctx.contact.length > 0 && ctx.contact[0].organisationObject != null && ctx.contact[0].organisationObject.default != '') { + ok++ +} if(ctx.contact != null && ctx.contact.length > 0 && ctx.contact[0].email != null && ctx.contact[0].email != '') { ok++ } From e848b2cfec0c21babd72002228cd478853673a40 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 23 Jul 2024 12:18:19 +0200 Subject: [PATCH 089/378] chore(e2e): execute tests on different ports for each app --- apps/datahub-e2e/project.json | 3 ++- apps/metadata-editor-e2e/project.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/datahub-e2e/project.json b/apps/datahub-e2e/project.json index f188bfa518..7a3d5997a3 100644 --- a/apps/datahub-e2e/project.json +++ b/apps/datahub-e2e/project.json @@ -10,7 +10,8 @@ "cypressConfig": "apps/datahub-e2e/cypress.config.js", "devServerTarget": "datahub:serve:development", "testingType": "e2e", - "browser": "chrome" + "browser": "chrome", + "port": "cypress-auto" }, "configurations": { "production": { diff --git a/apps/metadata-editor-e2e/project.json b/apps/metadata-editor-e2e/project.json index fe64bd1e65..306cdf0d07 100644 --- a/apps/metadata-editor-e2e/project.json +++ b/apps/metadata-editor-e2e/project.json @@ -10,7 +10,8 @@ "cypressConfig": "apps/metadata-editor-e2e/cypress.config.js", "devServerTarget": "metadata-editor:serve:development", "testingType": "e2e", - "browser": "chrome" + "browser": "chrome", + "port": "cypress-auto" }, "configurations": { "production": { From 583c63fd84a4878a8419e3a677cfa37ce7ee9366 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Wed, 24 Jul 2024 10:26:05 +0200 Subject: [PATCH 090/378] wip --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6fa3fe4f24..c6cc169081 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -168,7 +168,7 @@ jobs: env: GEONETWORK_VERSION: ${{ matrix.gn_version }} working-directory: support-services - run: docker compose up -d init + run: docker compose up --quiet-pull init - run: npm ci From 38b8d51f2c22b41e32a74388b7007990f2986acc Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 30 Jul 2024 17:07:37 +0200 Subject: [PATCH 091/378] e2e: make no-link-block test more reliable The record soes not require a login anymore, and no http interception either --- .../src/e2e/datasetDetailPage.cy.ts | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 1a1a9626b2..0fa64a3f0b 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -94,7 +94,7 @@ describe('dataset pages', () => { describe('GENERAL : display & functions', () => { describe('no-link-error block', () => { - it("shouldn't be there until metadata is fully loaded", () => { + it("shouldn't be there if there are links", () => { cy.visit('/dataset/a3774ef6-809d-4dd1-984f-9254f49cbd0a') cy.get('[data-test=dataset-has-no-link-block]').should('not.exist') }) @@ -597,24 +597,15 @@ describe('dataset pages', () => { }) describe('When there is no link', () => { - it('display the error datasetHasNoLink error block', () => { - cy.login() - - cy.intercept( - 'GET', - '/geonetwork/srv/api/userfeedback?metadataUuid=a3774ef6-809d-4dd1-984f-9254f49cbd0a', - (req) => { - // Test if the error block is not shown before the metadata is fully loaded - cy.get('[data-test="dataset-has-no-link-block"]').should( - 'not.exist' - ) - } - ).as('getData') - + beforeEach(() => { cy.visit('/dataset/a3774ef6-809d-4dd1-984f-9254f49cbd0a') - - cy.wait('@getData') - + }) + it('do not display the no-link-error warning initially, only after loading', () => { + // wait for metadata info to show up + cy.get('gn-ui-metadata-info').should('exist') + // first, the block is not visible + cy.get('[data-test="dataset-has-no-link-block"]').should('not.exist') + // then the block shows up cy.get('[data-test="dataset-has-no-link-block"]').should('exist') }) }) From 6533a55613d494980c4bac446173ac84d65ec12f Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 30 Jul 2024 17:10:45 +0200 Subject: [PATCH 092/378] chore: update db dump The "record with no link" record is now public --- .../docker-entrypoint-initdb.d/dump | Bin 460017 -> 460029 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/support-services/docker-entrypoint-initdb.d/dump b/support-services/docker-entrypoint-initdb.d/dump index 55bf76ca5eed2c3e04405d78e96c6f7c7012b1ee..a8db47c2717ff32c53aa84be43d0f8c1caa5c048 100644 GIT binary patch delta 2139 zcmY*ZYgAKL7QQ>YL@7~ug@U}4f)5Bu2uVQERxA#PfV8m`6+tjXC`u_Ll0w8s5g8vC z@lts%bxONNY&$8QiaccL0$Efj=u}6mh>s$a7NUa&5ty@+{^(?#bMM;ce0%S2e|z8P zi(RLRT|HyM!=hrqmNB0N01W+E<6|~HxcG1@^;|3vdH9BV8VVrL?H}3(W+LV-ZK185Zff`g0I^O! z;sF$?{M{Qkfihk7ACZV09*qDH>BRa_FcULN*ty%mL09r@BbZ5;0JNNS)6bP)E|xJt zY*Gr?X~!z9b!Fo$m6_9IU|ambneqhwT-ZwMlF5k2JjlXK z$LE8bQYS-pa14=o4#+@4E;>RoZ~|mx0S{0nlICopw1oYp792?Hn?My3Gl6KjkU&=` zLbs*`=q8}?-A$DwdVsMJzS_C1_T)dFfSn-1g%F5v#UfY@9HESy^9Fy+a`!EWMLK@I zKy?xm)na^0$dx6KiR~q#Whj||WcWce0-OAy6oKmlkdK6sVMFZMZ-rn(t_k3C>@Djv>&nUvuh3za8O3q)*U3Y|u~&F~Mr^20GFJzJmw2hQI#7nNIK z057F~=|gQYv|vo?eIDho-40jK?o(h?DalQPdbAg%o9=I9z&*6T%Y=VoEU{J);7ZiH z;1ZrnK7mu{)t3zi(N4&LFVG&(HF6|hsHhjU*W^)}^!_)sDav0NUCf6Lyerf|7h29uIkC#W-(%cSe2lSb|=4e3C3l;bG>DYC;UU=d$mVX7*&%BYp8 z1GY8B%%r{+t|2J*G?pwX@;VE2bLcjs+njC-;&s+CEnE{!|8G^5D{e*}e-vP2-rJB< zZheqhna{S&$lC1_(=Fsn7R1h;msu0?%x&w+Y4C*$fx%as} zE~uGmdLG>4c6PGn#VSv$P}K{sx(s?zx5Civ!%il8UV53+^@my<-{03d@^RTY#qR## znWgIL!B=JZ_1SzbcllVq=2p&>)gE5mZqhVj+3Mh3VUn@5=ZKxaroE@S?zYoMsT~uK z{(Iq|*)GO9@5A4{*SoT{9IH1qrz6!CV+=day?pnI-@5&?zDc+ztvTr~THeh~{CPAr zbjpukmuh)mm{51j;Bo!dUhnXmefisZlf^#c^0Nt@rz_w2DdS+&+H~(|-np#owvwQ* z8g*Rz9k|c8Tv?<@$k_FROVR3%kkeJ#g#(2{mzxy&=&>Z(-Nx%#gIV#niRR|;PxACk zgY&5zz1NOkk_SqAe#t-XUnOjeIIJ#de|jcpSRTHxm#$;zL+vN3z&!G#Qi*xOcwhgWA7``Fjwjn0! d(|u*b56)|v>jtL+qh^?d_PS!$#oX%W{{e~r4hsMP delta 2123 zcmY*ZYgAKL7S2urNWq}x737UPRl-fkQv|dXTTv=VeSs~CRzzqj)+PwljEVw7i^V8c zG(wiGi@vNlsT>(muqcJ4mQkl;Ef%%TplHCL!+;QB&Q9h>C+nPh*FNXl-`@M%`{reZ z-%y2LaOxWiQ&K^|;ZFeo4*iMoF#{it_z+eHM~M|dkqh6;&Rwq#63ax2Xt8K(&c+b7ryAHi4(9^4TijDLD*UKBkH1THwnWj+|Y; z5#03_gR|+-Fbg~hClXIQO$$fTk>l{_`^XWgZ!>zB{Kl4Jv=7Et09V17I;?9k+RO=C zH1*^MG@~=Uprjb$&4obVDjA~W0cKCMeEO#8GwcNs_>k)k@E(N|g%d~+zhw&K0awM4 zTh5RQjuH+-g<-)JyvXY=6iPkk=#7nD}k~DikHgLrZ@fQNhL~?!XDa~R}8NrjZ&H$>AoFhfkmq`7f4BcA% z(M>}61MQV$1%a&*k@{(^9^`f~U?+%VCP)#U6AG^bHfp&>(PrJY*u!kN_JI2=i&9_zH%~d;QN| zWbOi}M0m$*phNhgg;=zl>`kIYQ+WR(niQzWf;Va3oRkblh<$QP1|fT3F<3!N_9ufk z&XF%$oeUo2{1TWB>_iIqu%D*DD`aRXOhw|!6o>(kk(g!Ri>mvw*O!79Tb~M3NM0&9 z0Z+m4v7M31G-}e=`sI|$QN;C$q!M3RxdYe{3gHK-xa!mitRVaQe(XNIN zjCN!Vd=5MnC&pIf%1mm3l+D^mC(->KIDuEuIO3%HT{wfoCbG|jVIADZOXVMWV9bUK z$Vt7wLn#ECpdIZSImoIanp`-Ec4(gc{$@V>hW2SSe1lvvy_oSM#arPVo+`G%5%jw8 zA(W$?`4QBjJ*=_iNNNkI7qyd$Xqxo?NwJ;fj~O+83_s%CE*)G!Y|1X=kg_zQkWB6d z10EVm;V^<^`#_I&R~gixy{-bjMBBL%KPV|nNrFh{C(w+CyQ|Xh z(j3QCjzg@*Wp3l6jgx$?XF#F?+n41UI1C;jt` z?ThO)4O`Y4M{lO=m)%V-4p((0iJpaaD@Gc|9$w1{H=XL&jK=AA6qacV%Uncf?}1xk zn|qz5)Vt-LL%qehe(PA=%VQJATOY-YRoY%`>C zC9(HTd(V=L2mNQY`C^<_FsKvO76%@?`mtNYO~s=?^ZWeTx{D(nFS6F84~*Z+7p1Z_;9@@!5PO|Jd)Pd$y|`mQ<~-EQt6j zDf6vEXUcQCmPhSPHHByj&d7cg?9n_zlh~ ze{$z(P0p{2W?p_b%mYR>b0`1$NwqF(yZ`*b#A#2(gZ}>UDZxvHg2Q**TW04)=QVfe z6G{v{aL4zZ&EYD??rWP%ShuuokHp5LRck$RqM8EEZ~8vtQbgJLjH{Pdc(@$Qf3l$o z*mT7lP8R$1lPeC7e^!(0zumsk`+WL?f9`m8zpXcMLI1;>za;k7{FGL^IHV*0D<6$K z>z57JF6&b()XL^BkFHHhU+;RMYJ0w+X~v=5D^92zZ_K+hd}?IyMPtvv1gs(re_|u} HPXG8X3x@uo From 0e37a8b737a865fa58e1420730bb44169c4e41de Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 30 Jul 2024 20:00:41 +0200 Subject: [PATCH 093/378] e2e(dh): adapt tests for new record order --- apps/datahub-e2e/src/e2e/datasets.cy.ts | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/datahub-e2e/src/e2e/datasets.cy.ts b/apps/datahub-e2e/src/e2e/datasets.cy.ts index ae4fcd8811..f2f7518c9d 100644 --- a/apps/datahub-e2e/src/e2e/datasets.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasets.cy.ts @@ -6,7 +6,7 @@ describe('datasets', () => { // aliases cy.get('gn-ui-results-list-item').find('a').as('results') - cy.get('@results').first().as('firstResult') + cy.get('@results').eq(1).as('sampleResult') cy.get('@results') .then(($results) => $results.length) .as('resultsCount') @@ -54,12 +54,12 @@ describe('datasets', () => { describe('display of dataset previews', () => { it('should display a logo for first and a placeholder for second result', () => { cy.get('@sortBy').selectDropdownOption('desc,createDate') // this makes the order reliable - cy.get('@firstResult') + cy.get('@sampleResult') .find('gn-ui-thumbnail') .children('div') .invoke('attr', 'data-cy-is-placeholder') .should('equal', 'false') - cy.get('@firstResult') + cy.get('@sampleResult') .find('gn-ui-thumbnail') .find('img') .invoke('attr', 'src') @@ -68,45 +68,45 @@ describe('datasets', () => { 'https://geocat-dev.dev.bgdi.ch/geonetwork/srv/api/records/9e1ea778-d0ce-4b49-90b7-37bc0e448300/attachments/test.png' ) cy.get('@results') - .eq(1) + .first() .find('gn-ui-thumbnail') .children('div') .invoke('attr', 'data-cy-is-placeholder') .should('equal', 'true') }) it('should display the title', () => { - cy.get('@firstResult') + cy.get('@sampleResult') .find('[data-cy="recordTitle"]') .should('be.visible') }) it('should display the summary', () => { - cy.get('@firstResult') + cy.get('@sampleResult') .find('[data-cy="recordAbstract"]') .should('be.visible') }) it('should display the organization', () => { - cy.get('@firstResult').find('[data-cy="recordOrg"]').should('be.visible') + cy.get('@sampleResult').find('[data-cy="recordOrg"]').should('be.visible') }) it('should display the star and like count', () => { - cy.get('@firstResult').find('[data-cy="recordFav"]').should('be.visible') + cy.get('@sampleResult').find('[data-cy="recordFav"]').should('be.visible') }) }) describe('interactions with dataset', () => { beforeEach(() => { - cy.get('@firstResult') + cy.get('@sampleResult') .find('gn-ui-favorite-star') .eq(0) .as('favoriteStar') }) it('should open the dataset page in the same application on click', () => { - cy.get('@firstResult').click() + cy.get('@sampleResult').click() cy.url().should('match', /^http:\/\/localhost:[0-9]+\/dataset\/.+/) }) describe('not logged in', () => { it('should show a popover with login link when hovering the favorite star', () => { cy.get('@favoriteStar').trigger('mouseenter') - cy.get('[id="tippy-1"]') + cy.get('[id="tippy-2"]') .find('a') .invoke('attr', 'href') .should('include', 'catalog.signin') @@ -437,8 +437,6 @@ describe('datasets', () => { describe('multiple filters', () => { beforeEach(() => { - cy.get('datahub-search-filters').scrollIntoView() - // filter by org cy.get('@filters').eq(0).click() getFilterOptions() @@ -559,7 +557,7 @@ describe('datasets', () => { it('should display quality widget', () => { cy.get('@sortBy').selectDropdownOption('desc,createDate') cy.get('gn-ui-progress-bar') - .eq(0) + .eq(1) .should('have.attr', 'ng-reflect-value', 87) }) From e8989096b656cfe12232bd99bc2ce73618b3a676 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Tue, 30 Jul 2024 20:52:28 +0200 Subject: [PATCH 094/378] ci: show thesauri loaded in docker compose init --- .../docker-entrypoint.d/04-upload-thesauri.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/support-services/docker-entrypoint.d/04-upload-thesauri.sh b/support-services/docker-entrypoint.d/04-upload-thesauri.sh index ae898293be..a288bbb530 100755 --- a/support-services/docker-entrypoint.d/04-upload-thesauri.sh +++ b/support-services/docker-entrypoint.d/04-upload-thesauri.sh @@ -18,3 +18,12 @@ do echo "" done +curl "http://$host/geonetwork/srv/fre/thesaurus?_content_type=json" \ + -H 'Accept: application/json, text/plain, */*' \ + -H 'Content-Type: multipart/form-data' \ + -H 'Accept: application/json, text/plain, */*' \ + -H "Cookie: JSESSIONID=$jsessionid; XSRF-TOKEN=$xsrf_token" \ + -H "X-XSRF-TOKEN: $xsrf_token" + +echo "" +echo "Thesauri uploaded to GeoNetwork." From 1a17ebcc6e47a04f5de0654bf42a0c0e8e87178b Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Wed, 31 Jul 2024 17:04:12 +0200 Subject: [PATCH 095/378] fix(editor): fix dashboard e2e tests --- apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts index 2e905e5fb5..1001c7a5a3 100644 --- a/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts +++ b/apps/metadata-editor-e2e/src/e2e/dashboard.cy.ts @@ -93,7 +93,7 @@ describe('dashboard', () => { .get('gn-ui-checkbox') .first() .click() - cy.get('[data-test=selected-count]').contains('14 selected') + cy.get('[data-test=selected-count]').contains('15 selected') }) }) }) From c7d519f7adf8698ace823fe68687731dc5424c7e Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Thu, 20 Jun 2024 14:16:05 +0200 Subject: [PATCH 096/378] refactor(metadata-editor): changed resourceFileName to imageAltText. --- .../overview-upload/overview-upload.component.html | 2 +- .../overview-upload.component.spec.ts | 10 +++++----- .../overview-upload/overview-upload.component.ts | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html index 97d6aea100..54da3e35a7 100644 --- a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html +++ b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html @@ -1,7 +1,7 @@ { it('should get all resources corresponding to the metadata UUID on init', () => { expect(recordsApiService.getAllResources).toHaveBeenCalledWith(metadataUuid) - expect(component.resourceFileName).toEqual('filenameGet') + expect(component.imageAltText).toEqual('filenameGet') expect(component.resourceUrl).toEqual('urlGet') }) @@ -63,7 +63,7 @@ describe('OverviewUploadComponent', () => { someFile, 'public' ) - expect(component.resourceFileName).toEqual('filenamePut') + expect(component.imageAltText).toEqual('filenamePut') expect(component.resourceUrl).toEqual('urlPut') }) @@ -74,18 +74,18 @@ describe('OverviewUploadComponent', () => { 'someUrl', 'public' ) - expect(component.resourceFileName).toEqual('filenamePutUrl') + expect(component.imageAltText).toEqual('filenamePutUrl') expect(component.resourceUrl).toEqual('urlPutUrl') }) it('should delete the resource corresponding to the metadata UUID on delete', () => { - component.resourceFileName = 'filenameDelete' + component.imageAltText = 'filenameDelete' component.handleDelete() expect(recordsApiService.delResource).toHaveBeenCalledWith( metadataUuid, 'filenameDelete' ) - expect(component.resourceFileName).toBeNull() + expect(component.imageAltText).toBeNull() expect(component.resourceUrl).toBeNull() }) }) diff --git a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts index 4e437f3064..66535d5533 100644 --- a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts +++ b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts @@ -20,7 +20,7 @@ import { UiInputsModule } from '@geonetwork-ui/ui/inputs' export class OverviewUploadComponent implements OnInit { @Input() metadataUuid: string - resourceFileName: string + imageAltText: string resourceUrl: string constructor( @@ -32,7 +32,7 @@ export class OverviewUploadComponent implements OnInit { this.recordsApiService .getAllResources(this.metadataUuid) .subscribe((resources) => { - this.resourceFileName = resources[0]?.filename + this.imageAltText = resources[0]?.filename this.resourceUrl = resources[0]?.url this.cd.markForCheck() }) @@ -42,7 +42,7 @@ export class OverviewUploadComponent implements OnInit { this.recordsApiService .putResource(this.metadataUuid, file, 'public') .subscribe((resource) => { - this.resourceFileName = resource.filename + this.imageAltText = resource.filename this.resourceUrl = resource.url this.cd.markForCheck() }) @@ -52,7 +52,7 @@ export class OverviewUploadComponent implements OnInit { this.recordsApiService .putResourceFromURL(this.metadataUuid, url, 'public') .subscribe((resource) => { - this.resourceFileName = resource.filename + this.imageAltText = resource.filename this.resourceUrl = resource.url this.cd.markForCheck() }) @@ -60,9 +60,9 @@ export class OverviewUploadComponent implements OnInit { handleDelete() { this.recordsApiService - .delResource(this.metadataUuid, this.resourceFileName) + .delResource(this.metadataUuid, this.imageAltText) .subscribe(() => { - this.resourceFileName = null + this.imageAltText = null this.resourceUrl = null this.cd.markForCheck() }) From 46dd4e8e30b6b7359f91af91e7d5bc76da77adb8 Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Thu, 20 Jun 2024 14:18:51 +0200 Subject: [PATCH 097/378] feature(metadata-editor): added FormFieldOverviews component to the FormFieldComponent. --- .../model/metadataResource.api.model.ts | 2 +- .../overview-upload.component.html | 1 + .../overview-upload.component.ts | 115 +++++++++++++++--- .../form-field-overviews.component.css | 0 .../form-field-overviews.component.html | 5 + .../form-field-overviews.component.spec.ts | 40 ++++++ .../form-field-overviews.component.ts | 22 ++++ .../form-field/form-field.component.html | 6 + .../form-field/form-field.component.spec.ts | 36 +++++- .../form-field/form-field.component.ts | 15 ++- libs/feature/editor/src/lib/fields.config.ts | 12 ++ .../image-input/image-input.component.html | 2 +- .../lib/image-input/image-input.component.ts | 9 +- 13 files changed, 239 insertions(+), 26 deletions(-) create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.css create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.html create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.spec.ts create mode 100644 libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts diff --git a/libs/data-access/gn4/src/openapi/model/metadataResource.api.model.ts b/libs/data-access/gn4/src/openapi/model/metadataResource.api.model.ts index b7d69cb4a2..206f8f87d2 100644 --- a/libs/data-access/gn4/src/openapi/model/metadataResource.api.model.ts +++ b/libs/data-access/gn4/src/openapi/model/metadataResource.api.model.ts @@ -19,7 +19,7 @@ export interface MetadataResourceApiModel { metadataResourceExternalManagementProperties?: MetadataResourceExternalManagementPropertiesApiModel lastModification?: string version?: string - url?: string + url?: URL filename?: string id?: string size?: number diff --git a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html index 54da3e35a7..78ac6cee4f 100644 --- a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html +++ b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.html @@ -2,6 +2,7 @@ [maxSizeMB]="5" [previewUrl]="resourceUrl" [altText]="imageAltText" + [formControl]="formControl" (fileChange)="handleFileChange($event)" (urlChange)="handleUrlChange($event)" (delete)="handleDelete()" diff --git a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts index 66535d5533..ce32eddf0f 100644 --- a/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts +++ b/libs/feature/editor/src/lib/components/overview-upload/overview-upload.component.ts @@ -2,12 +2,27 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + EventEmitter, Input, + OnDestroy, OnInit, + Output, } from '@angular/core' import { CommonModule } from '@angular/common' import { RecordsApiService } from '@geonetwork-ui/data-access/gn4' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { FormControl } from '@angular/forms' +import { GraphicOverview } from '@geonetwork-ui/common/domain/model/record' +import { Subject, takeUntil } from 'rxjs' + +const extractFileNameFormUrl = (url: URL, metadataUuid: string): string => { + const pattern = new RegExp( + `records/${metadataUuid}/attachments/([^/?#]+)(?:[/?#]|$)`, + 'i' + ) + const match = url.href.match(pattern) + return match ? match[1] : '' +} @Component({ selector: 'gn-ui-overview-upload', @@ -17,11 +32,15 @@ import { UiInputsModule } from '@geonetwork-ui/ui/inputs' styleUrls: ['./overview-upload.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class OverviewUploadComponent implements OnInit { +export class OverviewUploadComponent implements OnInit, OnDestroy { @Input() metadataUuid: string + @Input() formControl!: FormControl + @Output() overviewChange = new EventEmitter() imageAltText: string - resourceUrl: string + resourceUrl: URL + + private destroy$ = new Subject() constructor( private recordsApiService: RecordsApiService, @@ -31,40 +50,98 @@ export class OverviewUploadComponent implements OnInit { ngOnInit(): void { this.recordsApiService .getAllResources(this.metadataUuid) - .subscribe((resources) => { - this.imageAltText = resources[0]?.filename - this.resourceUrl = resources[0]?.url - this.cd.markForCheck() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (resources) => { + if (resources && resources.length > 0) { + this.resourceUrl = new URL(resources[0]?.url) + this.imageAltText = resources[0].filename + } else if (this.formControl.value[0]) { + this.resourceUrl = new URL(this.formControl.value[0].url.href) + this.imageAltText = this.formControl.value[0].description + } else { + this.resourceUrl = null + this.imageAltText = '' + } + + this.cd.markForCheck() + }, + error: this.errorHandle, }) } handleFileChange(file: File) { this.recordsApiService .putResource(this.metadataUuid, file, 'public') - .subscribe((resource) => { - this.imageAltText = resource.filename - this.resourceUrl = resource.url - this.cd.markForCheck() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (resource) => { + this.resourceUrl = new URL(resource.url) + this.imageAltText = resource.filename + + this.overviewChange.emit({ + url: new URL(resource.url), + description: resource.filename, + }) + + this.cd.markForCheck() + }, + error: this.errorHandle, }) } handleUrlChange(url: string) { this.recordsApiService .putResourceFromURL(this.metadataUuid, url, 'public') - .subscribe((resource) => { - this.imageAltText = resource.filename - this.resourceUrl = resource.url - this.cd.markForCheck() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (resource) => { + this.resourceUrl = new URL(resource.url) + this.imageAltText = resource.filename + + this.overviewChange.emit({ + url: new URL(resource.url), + description: resource.filename, + }) + + this.cd.markForCheck() + }, + error: this.errorHandle, }) } handleDelete() { + const fileName = extractFileNameFormUrl(this.resourceUrl, this.metadataUuid) + this.recordsApiService - .delResource(this.metadataUuid, this.imageAltText) - .subscribe(() => { - this.imageAltText = null - this.resourceUrl = null - this.cd.markForCheck() + .delResource(this.metadataUuid, fileName) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + this.imageAltText = null + this.resourceUrl = null + + this.overviewChange.emit(null) + + this.cd.markForCheck() + }, + error: this.errorHandle, }) } + + private errorHandle = (error: never) => { + console.error(error) + + this.resourceUrl = null + this.imageAltText = '' + + this.overviewChange.emit(null) + + this.cd.markForCheck() + } + + ngOnDestroy(): void { + this.destroy$.next() + this.destroy$.complete() + } } diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.css b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.css new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..31a67c2683 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.html @@ -0,0 +1,5 @@ + 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 new file mode 100644 index 0000000000..95021ce4f5 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { FormFieldOverviewsComponent } from './form-field-overviews.component' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { TranslateModule } from '@ngx-translate/core' +import { FormControl } from '@angular/forms' + +describe('FormFieldOverviewsComponent', () => { + let component: FormFieldOverviewsComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + FormFieldOverviewsComponent, + HttpClientTestingModule, + TranslateModule.forRoot(), + ], + }).compileComponents() + + fixture = TestBed.createComponent(FormFieldOverviewsComponent) + component = fixture.componentInstance + component.metadataUuid = '8505d991-e38f-4704-a47a-e7d335dfbef5' + const control = new FormControl() + control.setValue([ + { + description: 'doge.jpg', + url: new URL( + 'http://localhost:8080/geonetwork/srv/api/0.1/records/8505d991-e38f-4704-a47a-e7d335dfbef5/attachments/doge.jpg' + ), + }, + ]) + component.control = control + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-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 new file mode 100644 index 0000000000..88af229862 --- /dev/null +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts @@ -0,0 +1,22 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { OverviewUploadComponent } from '../../../overview-upload/overview-upload.component' +import { FormControl } from '@angular/forms' +import { GraphicOverview } from '@geonetwork-ui/common/domain/model/record' + +@Component({ + selector: 'gn-ui-form-field-overviews', + templateUrl: './form-field-overviews.component.html', + styleUrls: ['./form-field-overviews.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, OverviewUploadComponent], +}) +export class FormFieldOverviewsComponent { + @Input() metadataUuid: string + @Input() control!: FormControl + + handleOverViewChange(overView: GraphicOverview | null) { + this.control.setValue(overView ? [overView] : []) + } +} 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 5ac61e653d..f6fa1f9ee2 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 @@ -76,6 +76,12 @@ + + + { let component: FormFieldComponent @@ -17,7 +27,18 @@ describe('FormFieldComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FormFieldComponent, TranslateModule.forRoot()], + imports: [ + FormFieldComponent, + TranslateModule.forRoot(), + HttpClientTestingModule, + ], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { + provide: EditorFacade, + useClass: EditorFacadeMock, + }, + ], }).compileComponents() fixture = TestBed.createComponent(FormFieldComponent) @@ -149,4 +170,17 @@ describe('FormFieldComponent', () => { expect(formField).toBeTruthy() }) }) + describe('overviews field', () => { + let formField + beforeEach(() => { + component.model = 'overviews' + fixture.detectChanges() + formField = fixture.debugElement.query( + By.directive(FormFieldOverviewsComponent) + ).componentInstance + }) + it('creates an array form field', () => { + expect(formField).toBeTruthy() + }) + }) }) diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts index 44621437a5..d4b20e4fe9 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 @@ -28,7 +28,9 @@ import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/for import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component' import { CatalogRecordKeys } from '@geonetwork-ui/common/domain/model/record' import { FormFieldKeywordsComponent } from './form-field-keywords/form-field-keywords.component' -import { FormFieldConfig } from '../../../models' +import { FormFieldOverviewsComponent } from './form-field-overviews/form-field-overviews.component' +import { map, take } from 'rxjs/operators' +import { EditorFacade } from '../../../+state/editor.facade' @Component({ selector: 'gn-ui-form-field', @@ -55,6 +57,7 @@ import { FormFieldConfig } from '../../../models' FormFieldArrayComponent, FormFieldKeywordsComponent, TranslateModule, + FormFieldOverviewsComponent, ], }) export class FormFieldComponent { @@ -70,9 +73,14 @@ export class FormFieldComponent { @ViewChild('titleInput') titleInput: ElementRef + metadataUuid$ = this.facade.record$.pipe( + take(1), + map((record) => record.uniqueIdentifier) + ) + formControl = new FormControl() - constructor() { + constructor(private facade: EditorFacade) { this.valueChange = this.formControl.valueChanges } @@ -101,6 +109,9 @@ export class FormFieldComponent { get isSpatialExtentField() { return this.model === 'spatialExtents' } + get isGraphicOverview() { + return this.model === 'overviews' + } get isSimpleField() { return this.model === 'uniqueIdentifier' || this.model === 'recordUpdated' } diff --git a/libs/feature/editor/src/lib/fields.config.ts b/libs/feature/editor/src/lib/fields.config.ts index 8c22644b48..cf72fef64d 100644 --- a/libs/feature/editor/src/lib/fields.config.ts +++ b/libs/feature/editor/src/lib/fields.config.ts @@ -167,6 +167,18 @@ export const DEFAULT_CONFIGURATION: EditorConfig = { { labelKey: marker('editor.record.form.page.description'), sections: [TITLE_SECTION, ABOUT_SECTION, GEOGRAPHICAL_COVERAGE_SECTION], + { + model: 'overviews', + formFieldConfig: { + labelKey: marker('editor.record.form.overviews'), + type: 'list', + }, + }, + { + model: 'keywords', + formFieldConfig: { + labelKey: marker('editor.record.form.keywords'), + type: 'list', }, { labelKey: marker('editor.record.form.page.ressources'), diff --git a/libs/ui/inputs/src/lib/image-input/image-input.component.html b/libs/ui/inputs/src/lib/image-input/image-input.component.html index ceeb82456a..ec41523dd2 100644 --- a/libs/ui/inputs/src/lib/image-input/image-input.component.html +++ b/libs/ui/inputs/src/lib/image-input/image-input.component.html @@ -51,7 +51,7 @@