diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts index 0a0c695e94..5cc0775345 100644 --- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts +++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts @@ -497,3 +497,120 @@ describe('record with file distributions', () => { .should('deep.eq', ['csv (csv)', 'json (json)', 'geojson (geojson)']) }) }) + +describe('api cards', () => { + beforeEach(() => { + cy.visit('/dataset/accroche_velos') + cy.get('gn-ui-api-card').first().as('firstCard') + }) + + it('should display the open panel button', () => { + cy.get('@firstCard') + .find('button') + .children('mat-icon') + .should('have.text', 'more_horiz') + }) + it('should open and close the panel on click on open panel button', () => { + cy.get('@firstCard').find('button').click() + cy.get('gn-ui-record-api-form').should('be.visible') + cy.get('@firstCard').find('button').click() + cy.get('gn-ui-record-api-form').should('not.exist') + }) +}) + +describe.only('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() + .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') + .then((newUrl) => { + expect(newUrl).to.not.eq(url) + expect(newUrl).to.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').click() + cy.get('button[data-cy-value="csv"]').click() + + cy.get('@apiForm') + .find('gn-ui-copy-text-button') + .find('input') + .invoke('val') + .should('include', 'offset=87&limit=54&f=csv') + + cy.get('@apiForm').children('div').first().find('button').first().click() + + cy.get('@firstInput').find('input').should('have.value', 0) + cy.get('@secondInput').find('input').should('have.value', 0) + 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') + }) + 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.exist') + }) + 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') + .then((newUrl) => { + expect(newUrl).to.not.eq(url) + }) + }) + }) +}) diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.css b/apps/datahub/src/app/record/record-apis/record-apis.component.css index e69de29bb2..5b19499cd1 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.css +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.css @@ -0,0 +1,3 @@ +.tab-header-label { + @apply uppercase text-sm text-primary opacity-75 hover:text-primary-darker; +} diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.html b/apps/datahub/src/app/record/record-apis/record-apis.component.html index b7b623d5e7..23dd9ac594 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.html +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.html @@ -15,11 +15,15 @@ let last = last " [link]="link" + [currentLink]="facade.selectedApiLink$ | async" class="w-80" [ngClass]="{ 'mr-[var(--container-outside-width)]': last, - 'ml-[var(--container-outside-width)]': first + 'ml-[var(--container-outside-width)]': first, + 'shadow-xl bg-white': link === (facade.selectedApiLink$ | async), + 'bg-neutral-100': link !== (facade.selectedApiLink$ | async) }" + (openRecordApiForm)="openRecordApiForm($event)" > 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 948b6b2fc0..da34ff5128 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 @@ -1,19 +1,29 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { Subject } from 'rxjs' import { RecordApisComponent } from './record-apis.component' +import { TranslateModule } from '@ngx-translate/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { BehaviorSubject, firstValueFrom } from 'rxjs' class MdViewFacadeMock { - apiLinks$ = new Subject() + selectedApiLink$ = new BehaviorSubject([]) } -describe('DataApisComponent', () => { +const serviceDistributionMock = { + type: 'service', + url: new URL('http://myogcapifeatures.test'), + accessServiceProtocol: 'ogcFeatures', +} as DatasetServiceDistribution + +describe('RecordApisComponent', () => { let component: RecordApisComponent let fixture: ComponentFixture + let facade beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [RecordApisComponent], + imports: [TranslateModule.forRoot()], providers: [ { provide: MdViewFacade, @@ -21,15 +31,24 @@ describe('DataApisComponent', () => { }, ], }).compileComponents() - }) - beforeEach(() => { fixture = TestBed.createComponent(RecordApisComponent) component = fixture.componentInstance + facade = TestBed.inject(MdViewFacade) fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + + describe('#openRecordApiForm', () => { + beforeEach(() => { + component.openRecordApiForm(serviceDistributionMock) + }) + it('should update value in facade.selectedApiLink$', async () => { + const apiLink = await firstValueFrom(facade.selectedApiLink$) + expect(apiLink).toEqual(serviceDistributionMock) + }) + }) }) diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.ts b/apps/datahub/src/app/record/record-apis/record-apis.component.ts index 8bcfd1ce86..7d0f8928f1 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.ts +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.ts @@ -1,4 +1,11 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core' +import { + Component, + ChangeDetectionStrategy, + Output, + EventEmitter, + Input, +} from '@angular/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' @Component({ @@ -9,4 +16,8 @@ import { MdViewFacade } from '@geonetwork-ui/feature/record' }) export class RecordApisComponent { constructor(public facade: MdViewFacade) {} + + openRecordApiForm(link: DatasetServiceDistribution) { + this.facade.selectedApiLink$.next(link) + } } 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 f7f020a96c..757db1212f 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 @@ -120,7 +120,35 @@
- + +
+ +
+
+
+

+ record.metadata.api.form.title +

+ +
+
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 f1ba243c25..d0dacda8c4 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts @@ -1,4 +1,10 @@ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core' +import { + Component, + EventEmitter, + Input, + NO_ERRORS_SCHEMA, + Output, +} from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' import { SourcesService } from '@geonetwork-ui/feature/catalog' @@ -6,18 +12,21 @@ import { MapManagerService } from '@geonetwork-ui/feature/map' import { SearchService } from '@geonetwork-ui/feature/search' import { ErrorType, - MetadataCatalogComponent, - MetadataContactComponent, - MetadataInfoComponent, SearchResultsErrorComponent, - UiElementsModule, } from '@geonetwork-ui/ui/elements' import { TranslateModule } from '@ngx-translate/core' -import { BehaviorSubject, of } from 'rxjs' +import { BehaviorSubject, firstValueFrom, of } from 'rxjs' import { RecordMetadataComponent } from './record-metadata.component' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { + CatalogRecord, + DatasetRecord, + DatasetServiceDistribution, + Individual, + Organization, +} from '@geonetwork-ui/common/domain/model/record' const SAMPLE_RECORD = { ...DATASET_RECORDS[0], @@ -37,6 +46,7 @@ class MdViewFacadeMock { otherLinks$ = new BehaviorSubject([]) related$ = new BehaviorSubject(null) error$ = new BehaviorSubject(null) + selectedApiLink$ = new BehaviorSubject([]) } class SearchServiceMock { @@ -100,6 +110,46 @@ export class MockDataApisComponent {} }) export class MockRelatedComponent {} +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'gn-ui-metadata-info', + template: '
', +}) +export class MockMetadataInfoComponent { + @Input() metadata: Partial + @Input() incomplete: boolean + @Output() keyword = new EventEmitter() +} + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'gn-ui-metadata-contact', + template: '
', +}) +export class MockMetadataContactComponent { + @Input() metadata: Partial + @Output() organizationClick = new EventEmitter() + @Output() contactClick = new EventEmitter() +} + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'gn-ui-metadata-catalog', + template: '
', +}) +export class MockMetadataCatalogComponent { + @Input() sourceLabel: string +} + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'gn-ui-record-api-form', + template: '
', +}) +export class MockRecordApiFormComponent { + @Input() apiLink: DatasetServiceDistribution +} + describe('RecordMetadataComponent', () => { let component: RecordMetadataComponent let fixture: ComponentFixture @@ -119,9 +169,14 @@ describe('RecordMetadataComponent', () => { MockDataApisComponent, MockRelatedComponent, SearchResultsErrorComponent, + MockMetadataInfoComponent, + MockMetadataCatalogComponent, + MockMetadataContactComponent, + + MockRecordApiFormComponent, ], schemas: [NO_ERRORS_SCHEMA], - imports: [UiElementsModule, TranslateModule.forRoot()], + imports: [TranslateModule.forRoot()], providers: [ { provide: MdViewFacade, @@ -161,21 +216,21 @@ describe('RecordMetadataComponent', () => { }) describe('about', () => { - let metadataInfo: MetadataInfoComponent - let metadataContact: MetadataContactComponent - let catalogComponent: MetadataCatalogComponent + let metadataInfo: MockMetadataInfoComponent + let metadataContact: MockMetadataContactComponent + let catalogComponent: MockMetadataCatalogComponent beforeEach(() => { facade.isPresent$.next(true) fixture.detectChanges() metadataInfo = fixture.debugElement.query( - By.directive(MetadataInfoComponent) + By.directive(MockMetadataInfoComponent) ).componentInstance metadataContact = fixture.debugElement.query( - By.directive(MetadataContactComponent) + By.directive(MockMetadataContactComponent) ).componentInstance catalogComponent = fixture.debugElement.query( - By.directive(MetadataCatalogComponent) + By.directive(MockMetadataCatalogComponent) ).componentInstance }) describe('if metadata present', () => { @@ -197,7 +252,7 @@ describe('RecordMetadataComponent', () => { facade.isPresent$.next(false) fixture.detectChanges() metadataInfo = fixture.debugElement.query( - By.directive(MetadataInfoComponent) + By.directive(MockMetadataInfoComponent) ).componentInstance }) it('shows a placeholder', () => { @@ -206,12 +261,12 @@ describe('RecordMetadataComponent', () => { }) it('does not display the metadata contact component', () => { expect( - fixture.debugElement.query(By.directive(MetadataContactComponent)) + fixture.debugElement.query(By.directive(MockMetadataContactComponent)) ).toBeFalsy() }) it('does not display the metadata catalog component', () => { expect( - fixture.debugElement.query(By.directive(MetadataCatalogComponent)) + fixture.debugElement.query(By.directive(MockMetadataCatalogComponent)) ).toBeFalsy() }) }) @@ -545,4 +600,59 @@ describe('RecordMetadataComponent', () => { }) }) }) + describe('#selectApiLink', () => { + const serviceDistributionMockA = { + type: 'service', + url: new URL('http://myogcapifeatures.test'), + accessServiceProtocol: 'ogcFeatures', + } as DatasetServiceDistribution + const serviceDistributionMockB = { + type: 'service', + url: new URL('http://another-mock.test'), + accessServiceProtocol: 'ogcFeatures', + } as DatasetServiceDistribution + describe('calling selectApiLink ones', () => { + it('should pass value of selected card to facade.selectedApiLink$', async () => { + component.selectApiLink(serviceDistributionMockA) + const apiLink = await firstValueFrom(facade.selectedApiLink$) + expect(apiLink).toEqual(serviceDistributionMockA) + }) + }) + describe('calling selectApiLink twice with different values', () => { + it('should pass second value to facade.selectedApiLink$', async () => { + component.selectApiLink(serviceDistributionMockA) + component.selectApiLink(serviceDistributionMockB) + const apiLink = await firstValueFrom(facade.selectedApiLink$) + expect(apiLink).toEqual(serviceDistributionMockB) + }) + }) + describe('calling selectApiLink multiple times with different values', () => { + it('should pass last value to facade.selectedApiLink$', async () => { + component.selectApiLink(serviceDistributionMockA) + component.selectApiLink(serviceDistributionMockB) + component.selectApiLink(serviceDistributionMockA) + component.selectApiLink(serviceDistributionMockB) + component.selectApiLink(serviceDistributionMockA) + const apiLink = await firstValueFrom(facade.selectedApiLink$) + expect(apiLink).toEqual(serviceDistributionMockA) + }) + }) + + describe('calling selectApiLink twice with different values', () => { + it('should pass undefined to facade.selectedApiLink$', async () => { + component.selectApiLink(serviceDistributionMockA) + component.selectApiLink(serviceDistributionMockA) + const apiLink = await firstValueFrom(facade.selectedApiLink$) + expect(apiLink).toBeUndefined() + }) + }) + }) + + describe('#closeForm', () => { + it('should pass undefined to facade.selectedApiLink$', async () => { + component.closeForm() + const apiLink = await firstValueFrom(facade.selectedApiLink$) + expect(apiLink).toBeUndefined() + }) + }) }) 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 d34405d576..9863f3b691 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -1,11 +1,20 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' import { SourcesService } from '@geonetwork-ui/feature/catalog' import { SearchService } from '@geonetwork-ui/feature/search' import { ErrorType, MetadataQualityDisplay } from '@geonetwork-ui/ui/elements' import { BehaviorSubject, combineLatest } from 'rxjs' import { filter, map, mergeMap } from 'rxjs/operators' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' -import { Organization } from '@geonetwork-ui/common/domain/model/record' +import { + Organization, + DatasetServiceDistribution, +} from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' @Component({ @@ -16,6 +25,7 @@ import { MdViewFacade } from '@geonetwork-ui/feature/record' }) export class RecordMetadataComponent { @Input() metadataQualityDisplay: MetadataQualityDisplay + @Output() apiLink = new EventEmitter() displayMap$ = combineLatest([ this.facade.mapApiLinks$, @@ -78,6 +88,16 @@ export class RecordMetadataComponent { .subscribe((filters) => this.searchService.updateFilters(filters)) } + selectApiLink(val) { + this.facade.selectedApiLink$.getValue() === val + ? this.facade.selectedApiLink$.next(undefined) + : this.facade.selectedApiLink$.next(val) + } + + closeForm() { + this.facade.selectedApiLink$.next(undefined) + } + get hasMetadataQualityWidget() { return this.metadataQualityDisplay?.widget === true } diff --git a/libs/api/metadata-converter/src/lib/common/distribution.mapper.ts b/libs/api/metadata-converter/src/lib/common/distribution.mapper.ts index bde80a250e..e5d66e3b66 100644 --- a/libs/api/metadata-converter/src/lib/common/distribution.mapper.ts +++ b/libs/api/metadata-converter/src/lib/common/distribution.mapper.ts @@ -5,6 +5,7 @@ export function matchProtocol(protocol: string): ServiceProtocol { if (/wfs/i.test(protocol)) return 'wfs' if (/wmts/i.test(protocol)) return 'wmts' if (/wps/i.test(protocol)) return 'wps' + if (/ogc\W*api\W*features/i.test(protocol)) return 'ogcFeatures' if (/esri/i.test(protocol)) return 'esriRest' return 'other' } diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.spec.ts index 3263349036..ee7619dc54 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.spec.ts @@ -72,6 +72,7 @@ describe('Gn4FieldMapper', () => { 'link', 'link', 'download', + 'service', ]) }) }) diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts index c9ab874094..cda6afbc21 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts @@ -323,7 +323,8 @@ export class Gn4FieldMapper { (/^ESRI:REST/.test(protocol) && /FeatureServer/.test(url)) || /^OGC:WMS/.test(protocol) || /^OGC:WFS/.test(protocol) || - /^OGC:WMTS/.test(protocol) + /^OGC:WMTS/.test(protocol) || + /ogc\W*api\W*features/i.test(protocol) ) { return 'service' } diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index 90d2fba553..38ebcd6e1e 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -98,6 +98,7 @@ export type ServiceProtocol = | 'wps' | 'wmts' | 'esriRest' + | 'ogcFeatures' | 'other' export type DatasetDistributionType = 'service' | 'download' | 'link' diff --git a/libs/common/fixtures/src/lib/elasticsearch/metadata-links.fixtures.ts b/libs/common/fixtures/src/lib/elasticsearch/metadata-links.fixtures.ts index d09abce176..38b6a80cc2 100644 --- a/libs/common/fixtures/src/lib/elasticsearch/metadata-links.fixtures.ts +++ b/libs/common/fixtures/src/lib/elasticsearch/metadata-links.fixtures.ts @@ -141,4 +141,9 @@ export const ES_LINK_FIXTURES: Record = deepFreeze({ name: 'Vue HTML des métadonnées sur internet', url: 'http://catalogue.geo-ide.developpement-durable.gouv.fr/catalogue/srv/fre/catalog.search#/metadata/fr-120066022-jdd-199fd14c-2abb-4c14-b0f8-6c8d92e7b480', }, + geodataogcfeatures: { + protocol: 'OGC API - Features', + name: 'ogcapi features layer', + url: 'https://mel.integration.apps.gs-fr-prod.camptocamp.com/data/ogcapi/collections/comptages_velo/items?', + }, }) diff --git a/libs/common/fixtures/src/lib/link.fixtures.ts b/libs/common/fixtures/src/lib/link.fixtures.ts index 7d1aa5014b..9c25448e77 100644 --- a/libs/common/fixtures/src/lib/link.fixtures.ts +++ b/libs/common/fixtures/src/lib/link.fixtures.ts @@ -151,4 +151,11 @@ export const LINK_FIXTURES: Record = deepFreeze({ 'http://catalogue.geo-ide.developpement-durable.gouv.fr/catalogue/srv/fre/catalog.search#/metadata/fr-120066022-jdd-199fd14c-2abb-4c14-b0f8-6c8d92e7b480' ), }, + ogcApiFormat: { + name: 'ogc api features layer', + type: 'service', + url: new URL( + 'https://mel.integration.apps.gs-fr-prod.camptocamp.com/data/ogcapi/collections/comptages_velo/items?' + ), + }, }) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index a2e4a0064f..bf205fdec7 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -6,6 +6,7 @@ import * as MdViewSelectors from './mdview.selectors' import { LinkClassifierService, LinkUsage } from '@geonetwork-ui/util/shared' import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { BehaviorSubject } from 'rxjs' @Injectable() /** @@ -78,6 +79,7 @@ export class MdViewFacade { ) ) ) + selectedApiLink$ = new BehaviorSubject(undefined) constructor( private store: Store, 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 ba834b4d0d..26fb22ad2f 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,5 +1,5 @@
{{ link.accessServiceProtocol.toUpperCase() }} +
diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts b/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts index 1261e98035..402012b35d 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts +++ b/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts @@ -1,18 +1,16 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MatIconModule } from '@angular/material/icon' -import { TranslateModule } from '@ngx-translate/core' import { ApiCardComponent } from './api-card.component' +import { TranslateModule } from '@ngx-translate/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' describe('ApiCardComponent', () => { let component: ApiCardComponent let fixture: ComponentFixture - + let openRecordApiFormEmit beforeEach(async () => { await TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], declarations: [ApiCardComponent], - imports: [MatIconModule, TranslateModule.forRoot()], + imports: [TranslateModule.forRoot()], }).compileComponents() }) @@ -20,16 +18,27 @@ describe('ApiCardComponent', () => { fixture = TestBed.createComponent(ApiCardComponent) component = fixture.componentInstance component.link = { - name: 'Allroads', - description: 'A file that contains all roads', - url: new URL('https://roads.com/wfs'), - type: 'service', - accessServiceProtocol: 'wfs', - } + accessServiceProtocol: 'ogcFeatures', + } as DatasetServiceDistribution + openRecordApiFormEmit = component.openRecordApiForm + jest.resetAllMocks() + jest.spyOn(openRecordApiFormEmit, 'emit') fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + + it('should initialize custom property based on accessServiceProtocol', () => { + component.ngOnInit() + expect(component.displayApiFormButton).toBe(true) + }) + + it('should toggle currentlyActive and emit openRecordApiForm event', () => { + component.openPanel() + + expect(component.currentlyActive).toBe(true) + expect(openRecordApiFormEmit.emit).toHaveBeenCalled() + }) }) diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.ts b/libs/ui/elements/src/lib/api-card/api-card.component.ts index 9f9888d3a0..1582f48990 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.ts +++ b/libs/ui/elements/src/lib/api-card/api-card.component.ts @@ -1,5 +1,14 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core' @Component({ selector: 'gn-ui-api-card', @@ -7,6 +16,26 @@ import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/r styleUrls: ['./api-card.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ApiCardComponent { +export class ApiCardComponent implements OnInit, OnChanges { @Input() link: DatasetServiceDistribution + @Input() currentLink: DatasetServiceDistribution + displayApiFormButton: boolean + currentlyActive = false + @Output() openRecordApiForm: EventEmitter = + new EventEmitter() + + ngOnInit() { + this.displayApiFormButton = + this.link.accessServiceProtocol === 'ogcFeatures' ? true : false + } + + ngOnChanges(changes: SimpleChanges) { + this.currentlyActive = + changes.currentLink.currentValue === this.link ? true : false + } + + openPanel() { + this.currentlyActive = !this.currentlyActive + this.openRecordApiForm.emit(this.currentlyActive ? this.link : undefined) + } } diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.css b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.css new file mode 100644 index 0000000000..4d0bb8dd17 --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.css @@ -0,0 +1,15 @@ +gn-ui-text-input::ng-deep input, +input[type='text'] { + color: black; + width: 7rem; + height: 2.2rem; +} + +gn-ui-dropdown-selector::ng-deep div { + width: 7rem; +} + +gn-ui-copy-text-button::ng-deep input, +input[type='text'] { + font-size: 16px; +} diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html new file mode 100644 index 0000000000..92c3a06975 --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html @@ -0,0 +1,71 @@ +
+
+
+
+ record.metadata.api.form.create +
+ +
+
+
+

record.metadata.api.form.limit

+
+ + +
+ + Tous +
+
+
+
+

record.metadata.api.form.offset

+ + +
+
+

record.metadata.api.form.type

+ +
+
+
+
+
+ record.metadata.api.form.customUrl +
+
+ +
+
+
diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts new file mode 100644 index 0000000000..5bbb0a6a8d --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts @@ -0,0 +1,91 @@ +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 { TranslateModule } from '@ngx-translate/core' + +const mockDatasetServiceDistribution: DatasetServiceDistribution = { + url: new URL('https://api.example.com/data'), + type: 'service', + accessServiceProtocol: 'ogcFeatures', +} + +describe('CustomApiComponent', () => { + let component: RecordApiFormComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RecordApiFormComponent], + imports: [UiInputsModule, TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(RecordApiFormComponent) + component = fixture.componentInstance + component.apiLink = mockDatasetServiceDistribution + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + 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.linkHref).toBeDefined() + expect(component.offset$.getValue()).toBe('0') + expect(component.limit$.getValue()).toBe('0') + expect(component.format$.getValue()).toBe('json') + const url = await firstValueFrom(component.apiQueryUrl$) + expect(url).toBe('https://api.example.com/data?f=json') + }) + }) + 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}` + ) + }) + 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}` + ) + }) + 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('#resetUrl', () => { + it('should reset URL to default parameters', () => { + component.resetUrl() + expect(component.offset$.getValue()).toBe('0') + expect(component.limit$.getValue()).toBe('0') + expect(component.format$.getValue()).toBe('json') + }) + }) +}) diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts new file mode 100644 index 0000000000..2bce4cbcf7 --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' +import { BehaviorSubject, combineLatest, map } from 'rxjs' + +const DEFAULT_PARAMS = { + OFFSET: '', + LIMIT: '', + FORMAT: 'json', +} +@Component({ + selector: 'gn-ui-record-api-form', + templateUrl: './record-api-form.component.html', + styleUrls: ['./record-api-form.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RecordApiFormComponent { + @Input() set apiLink(value: DatasetServiceDistribution) { + this.apiBaseUrl = value.url.href + this.resetUrl() + } + offset$ = new BehaviorSubject('') + limit$ = new BehaviorSubject('') + format$ = new BehaviorSubject('') + apiBaseUrl: string + formatsList = [ + { label: 'JSON', value: 'json' }, + { label: 'CSV', value: 'csv' }, + ] + apiQueryUrl$ = combineLatest([this.offset$, this.limit$, this.format$]).pipe( + map(([offset, limit, format]) => { + const url = new URL(this.apiBaseUrl) + const params = { offset: offset, limit: limit, f: format } + for (const [key, value] of Object.entries(params)) { + if (value && value !== '0') { + url.searchParams.set(key, value) + } else { + url.searchParams.delete(key) + } + } + return url.toString() + }) + ) + + setOffset(value: string) { + this.offset$.next(value) + } + + setLimit(value: string) { + this.limit$.next(value) + } + + setFormat(value: string) { + this.format$.next(value) + } + + resetUrl() { + this.offset$.next(DEFAULT_PARAMS.OFFSET) + this.limit$.next(DEFAULT_PARAMS.LIMIT) + this.format$.next(DEFAULT_PARAMS.FORMAT) + } +} diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index b0faaf4305..cfb8ff0c73 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -27,6 +27,7 @@ import { AvatarComponent } from './avatar/avatar.component' import { UserPreviewComponent } from './user-preview/user-preview.component' import { GnUiLinkifyDirective } from './metadata-info/linkify.directive' import { PaginationButtonsComponent } from './pagination-buttons/pagination-buttons.component' +import { RecordApiFormComponent } from './record-api-form/record-api-form.component' @NgModule({ imports: [ @@ -61,6 +62,7 @@ import { PaginationButtonsComponent } from './pagination-buttons/pagination-butt UserPreviewComponent, GnUiLinkifyDirective, PaginationButtonsComponent, + RecordApiFormComponent, ], exports: [ MetadataInfoComponent, @@ -80,6 +82,7 @@ import { PaginationButtonsComponent } from './pagination-buttons/pagination-butt AvatarComponent, UserPreviewComponent, PaginationButtonsComponent, + RecordApiFormComponent, ], }) export class UiElementsModule {} diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.css b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.css new file mode 100644 index 0000000000..58dae5804f --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.css @@ -0,0 +1,3 @@ +mat-datepicker-toggle { + @apply text-primary; +} diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.html b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.html new file mode 100644 index 0000000000..dc7eb44a9e --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.html @@ -0,0 +1,24 @@ +
+
+ + + + +
+ + calendar_today + + +
diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts new file mode 100644 index 0000000000..d3fe405cef --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MatDatepickerInputEvent } from '@angular/material/datepicker' +import { DateRangePickerComponent } from './date-range-picker.component' + +describe('DateRangePickerComponent', () => { + let component: DateRangePickerComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DateRangePickerComponent], + }).compileComponents() + + fixture = TestBed.createComponent(DateRangePickerComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should set start date on startDateSelected', () => { + const event = { + value: new Date('2023-01-01'), + } as MatDatepickerInputEvent + component.startDateSelected(event) + expect(component.startDate).toEqual(new Date('2023-01-01')) + }) + + it('should set end date on endDateSelected', () => { + const event = { + value: new Date('2023-01-31'), + } as MatDatepickerInputEvent + component.endDateSelected(event) + expect(component.endDate).toEqual(new Date('2023-01-31')) + }) +}) diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts new file mode 100644 index 0000000000..a4593cdf75 --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core' +import { MatDatepickerInputEvent } from '@angular/material/datepicker' + +@Component({ + selector: 'gn-ui-date-range-picker', + templateUrl: './date-range-picker.component.html', + styleUrls: ['./date-range-picker.component.css'], +}) +export class DateRangePickerComponent { + startDate: Date + endDate: Date + + startDateSelected(event: MatDatepickerInputEvent) { + this.startDate = event.value + } + + endDateSelected(event: MatDatepickerInputEvent) { + this.endDate = event.value + } +} diff --git a/libs/ui/inputs/src/lib/ui-inputs.module.ts b/libs/ui/inputs/src/lib/ui-inputs.module.ts index a595dbf425..f21a962551 100644 --- a/libs/ui/inputs/src/lib/ui-inputs.module.ts +++ b/libs/ui/inputs/src/lib/ui-inputs.module.ts @@ -35,6 +35,11 @@ import { CopyTextButtonComponent } from './copy-text-button/copy-text-button.com import { MatTooltipModule } from '@angular/material/tooltip' import { CommonModule } from '@angular/common' import { CheckboxComponent } from './checkbox/checkbox.component' +import { DateRangePickerComponent } from './date-range-picker/date-range-picker.component' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatInputModule } from '@angular/material/input' +import { MatDatepickerModule } from '@angular/material/datepicker' +import { MatNativeDateModule } from '@angular/material/core' @NgModule({ declarations: [ @@ -60,6 +65,7 @@ import { CheckboxComponent } from './checkbox/checkbox.component' CheckToggleComponent, CopyTextButtonComponent, CheckboxComponent, + DateRangePickerComponent, ], imports: [ CommonModule, @@ -75,6 +81,10 @@ import { CheckboxComponent } from './checkbox/checkbox.component' OverlayModule, MatCheckboxModule, MatTooltipModule, + MatFormFieldModule, + MatInputModule, + MatDatepickerModule, + MatNativeDateModule, ], exports: [ DropdownSelectorComponent, @@ -92,6 +102,7 @@ import { CheckboxComponent } from './checkbox/checkbox.component' CheckToggleComponent, CopyTextButtonComponent, CheckboxComponent, + DateRangePickerComponent, ], }) export class UiInputsModule {} 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 f25fc65103..a900e0400e 100644 --- a/libs/util/shared/src/lib/links/link-classifier.service.ts +++ b/libs/util/shared/src/lib/links/link-classifier.service.ts @@ -25,6 +25,8 @@ export class LinkClassifierService { case 'wms': case 'wmts': return [LinkUsage.API, LinkUsage.MAP_API] + case 'ogcFeatures': + return [LinkUsage.API] default: return [LinkUsage.UNKNOWN] } diff --git a/translations/de.json b/translations/de.json index 8755763492..f7c234a0cf 100644 --- a/translations/de.json +++ b/translations/de.json @@ -193,6 +193,21 @@ "record.externalViewer.open": "In externem Kartenviewer öffnen", "record.metadata.about": "Beschreibung", "record.metadata.api": "API", + "record.metadata.api.form.access": "", + "record.metadata.api.form.baseUrl": "", + "record.metadata.api.form.closeButton": "", + "record.metadata.api.form.closeForm": "", + "record.metadata.api.form.create": "", + "record.metadata.api.form.customUrl": "", + "record.metadata.api.form.limit": "", + "record.metadata.api.form.offset": "", + "record.metadata.api.form.open": "", + "record.metadata.api.form.reset": "", + "record.metadata.api.format.swagger": "", + "record.metadata.api.form.title": "", + "record.metadata.api.form.type": "", + "record.metadata.api.tab.custom": "", + "record.metadata.api.tab.predefined": "", "record.metadata.author": "", "record.metadata.catalog": "Katalog", "record.metadata.contact": "Kontakt", diff --git a/translations/en.json b/translations/en.json index 19c040d9d0..f92324aad8 100644 --- a/translations/en.json +++ b/translations/en.json @@ -193,6 +193,21 @@ "record.externalViewer.open": "Open in the external map viewer", "record.metadata.about": "Description", "record.metadata.api": "API", + "record.metadata.api.form.access": "You may also access the", + "record.metadata.api.form.baseUrl": "Root URL", + "record.metadata.api.form.closeButton": "CLOSE", + "record.metadata.api.form.closeForm": "Close the form", + "record.metadata.api.form.create": "Create your request", + "record.metadata.api.form.customUrl": "Custom URL", + "record.metadata.api.form.limit": "Count of records", + "record.metadata.api.form.offset": "Count of first record", + "record.metadata.api.form.openForm": "Open the form", + "record.metadata.api.form.reset": "RESET", + "record.metadata.api.format.swagger": "Swagger UI generation console", + "record.metadata.api.form.title": "Generate a custom URL", + "record.metadata.api.form.type": "Output format", + "record.metadata.api.tab.custom": "Custom URLs", + "record.metadata.api.tab.predefined": "Predefined URLs", "record.metadata.author": "Author", "record.metadata.catalog": "Catalog", "record.metadata.contact": "Contact", diff --git a/translations/es.json b/translations/es.json index d35079a512..3330399df1 100644 --- a/translations/es.json +++ b/translations/es.json @@ -193,6 +193,21 @@ "record.externalViewer.open": "", "record.metadata.about": "", "record.metadata.api": "", + "record.metadata.api.form.access": "", + "record.metadata.api.form.baseUrl": "", + "record.metadata.api.form.closeButton": "", + "record.metadata.api.form.closeForm": "", + "record.metadata.api.form.create": "", + "record.metadata.api.form.customUrl": "", + "record.metadata.api.form.limit": "", + "record.metadata.api.form.offset": "", + "record.metadata.api.form.open": "", + "record.metadata.api.form.reset": "", + "record.metadata.api.format.swagger": "", + "record.metadata.api.form.title": "", + "record.metadata.api.form.type": "", + "record.metadata.api.tab.custom": "", + "record.metadata.api.tab.predefined": "", "record.metadata.author": "", "record.metadata.catalog": "", "record.metadata.contact": "", diff --git a/translations/fr.json b/translations/fr.json index c32b0b18d1..5e9a7f8a8d 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -193,6 +193,21 @@ "record.externalViewer.open": "Ouvrir dans le visualiseur externe", "record.metadata.about": "Description", "record.metadata.api": "API", + "record.metadata.api.form.access": "Vous pouvez également accéder à la", + "record.metadata.api.form.baseUrl": "URL racine", + "record.metadata.api.form.closeButton": "FERMER", + "record.metadata.api.form.closeForm": "Fermer le panneau de personnalisation", + "record.metadata.api.form.create": "Créer votre requête", + "record.metadata.api.form.customUrl": "URL personnalisée", + "record.metadata.api.form.limit": "Nombre d'enregistrements", + "record.metadata.api.form.offset": "Numéro du 1er enregistrement", + "record.metadata.api.form.open": "Ouvrir le panneau de personnalisation", + "record.metadata.api.form.reset": "REINITIALISER", + "record.metadata.api.format.swagger": "console de génération Swagger UI", + "record.metadata.api.form.title": "Générer une URL personnalisée", + "record.metadata.api.form.type": "Format en sortie", + "record.metadata.api.tab.custom": "urls personnalisées", + "record.metadata.api.tab.predefined": "urls prédéfinies", "record.metadata.author": "Auteur", "record.metadata.catalog": "Catalogue", "record.metadata.contact": "Contact", diff --git a/translations/it.json b/translations/it.json index 73c62af3af..01496fbdb3 100644 --- a/translations/it.json +++ b/translations/it.json @@ -193,6 +193,21 @@ "record.externalViewer.open": "", "record.metadata.about": "", "record.metadata.api": "", + "record.metadata.api.form.access": "", + "record.metadata.api.form.baseUrl": "", + "record.metadata.api.form.closeButton": "", + "record.metadata.api.form.closeForm": "", + "record.metadata.api.form.create": "", + "record.metadata.api.form.customUrl": "", + "record.metadata.api.form.limit": "", + "record.metadata.api.form.offset": "", + "record.metadata.api.form.open": "", + "record.metadata.api.form.reset": "", + "record.metadata.api.format.swagger": "", + "record.metadata.api.form.title": "", + "record.metadata.api.form.type": "", + "record.metadata.api.tab.custom": "", + "record.metadata.api.tab.predefined": "", "record.metadata.author": "", "record.metadata.catalog": "", "record.metadata.contact": "", diff --git a/translations/nl.json b/translations/nl.json index 00087862f2..449de50180 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -193,6 +193,21 @@ "record.externalViewer.open": "", "record.metadata.about": "", "record.metadata.api": "", + "record.metadata.api.form.access": "", + "record.metadata.api.form.baseUrl": "", + "record.metadata.api.form.closeButton": "", + "record.metadata.api.form.closeForm": "", + "record.metadata.api.form.create": "", + "record.metadata.api.form.customUrl": "", + "record.metadata.api.form.limit": "", + "record.metadata.api.form.offset": "", + "record.metadata.api.form.open": "", + "record.metadata.api.form.reset": "", + "record.metadata.api.format.swagger": "", + "record.metadata.api.form.title": "", + "record.metadata.api.form.type": "", + "record.metadata.api.tab.custom": "", + "record.metadata.api.tab.predefined": "", "record.metadata.author": "", "record.metadata.catalog": "", "record.metadata.contact": "", diff --git a/translations/pt.json b/translations/pt.json index 83a73859c8..a248e38e51 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -193,6 +193,21 @@ "record.externalViewer.open": "", "record.metadata.about": "", "record.metadata.api": "", + "record.metadata.api.form.access": "", + "record.metadata.api.form.baseUrl": "", + "record.metadata.api.form.closeButton": "", + "record.metadata.api.form.closeForm": "", + "record.metadata.api.form.create": "", + "record.metadata.api.form.customUrl": "", + "record.metadata.api.form.limit": "", + "record.metadata.api.form.offset": "", + "record.metadata.api.form.open": "", + "record.metadata.api.form.reset": "", + "record.metadata.api.format.swagger": "", + "record.metadata.api.form.title": "", + "record.metadata.api.form.type": "", + "record.metadata.api.tab.custom": "", + "record.metadata.api.tab.predefined": "", "record.metadata.author": "", "record.metadata.catalog": "", "record.metadata.contact": "",