Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metadata Editor: constraints fields #1032

Merged
merged 11 commits into from
Nov 17, 2024
122 changes: 118 additions & 4 deletions apps/metadata-editor-e2e/src/e2e/edit.cy.ts
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ describe('editor form', () => {
return
}
// there is a copy: delete it
cy.get('[data-test="record-menu-button"]').eq(1).click()
cy.get('[data-test="record-menu-button"]').eq(0).click()
cy.get('[data-test="record-menu-delete-button"]').click()
cy.get('[data-cy="confirm-button"]').click()
cy.log('An existing copy of the test record was found and deleted.')
@@ -675,6 +675,9 @@ describe('editor form', () => {
})
})
describe('Access and constraints', () => {
beforeEach(() => {
cy.get('@accessContactPageBtn').click()
})
// TEMPORARY - to be removed when the open data switch is back
// describe('Open data switch', () => {
// beforeEach(() => {
@@ -699,9 +702,6 @@ describe('editor form', () => {
// })
// })
describe('licenses', () => {
beforeEach(() => {
cy.get('@accessContactPageBtn').click()
})
it('should select a new license and show it on reload', () => {
cy.get('gn-ui-form-field-license')
.find('button')
@@ -727,6 +727,120 @@ describe('editor form', () => {
.should('eq', ' Creative Commons CC-0 ')
})
})
describe('constraints', () => {
it('should add a few constraints and show it on reload', () => {
cy.editor_wrapPreviousDraft()
cy.get('[data-cy=legalConstraints]')
.find('gn-ui-button[data-cy=add-constraint-btn] button')
.click()
cy.get('[data-cy=legalConstraints]')
.find('textarea')
.last()
.type('new legal constraint')

// add from shortcuts
cy.get('[data-cy=constraints-shortcut-btns]')
.find('gn-ui-button')
.eq(1)
.click()
cy.get('[data-cy=securityConstraints]')
.find('textarea')
.last()
.type('new security constraint')

// add from shortcuts
cy.get('[data-cy=constraints-shortcut-btns]')
.find('gn-ui-button')
.eq(2)
.click()
cy.get('[data-cy=otherConstraints]')
.find('textarea')
.last()
.type('new other constraint')
cy.get('[data-cy=otherConstraints]')
.find('gn-ui-button[data-cy=add-url-btn] button')
.click()
cy.get('[data-cy=otherConstraints]')
.find('gn-ui-url-input')
.last()
.find('input')
.type('http://www.example.com/abcd/1234')

cy.screenshot({ capture: 'fullPage' })
cy.editor_publishAndReload()
cy.get('@saveStatus').should('eq', 'record_up_to_date')
cy.get('@accessContactPageBtn').click()

cy.get('[data-cy=legalConstraints]')
.find('gn-ui-constraint-card')
.should('have.length', 5)
cy.get('[data-cy=legalConstraints]')
.find('textarea')
.last()
.invoke('val')
.should('eq', 'new legal constraint')

cy.get('[data-cy=securityConstraints]')
.find('gn-ui-constraint-card')
.should('have.length', 1)
cy.get('[data-cy=securityConstraints]')
.find('textarea')
.last()
.invoke('val')
.should('eq', 'new security constraint')

cy.get('[data-cy=otherConstraints]')
.find('gn-ui-constraint-card')
.should('have.length', 1)
cy.get('[data-cy=otherConstraints]')
.find('textarea')
.last()
.invoke('val')
.should('eq', 'new other constraint')
cy.get('[data-cy=otherConstraints]')
.find('gn-ui-url-input input')
.invoke('val')
.should('eq', 'http://www.example.com/abcd/1234')
})

it('should enable "no applicable constraints" and stay enabled', () => {
cy.editor_wrapPreviousDraft()
cy.get('[data-cy=constraints-shortcut-toggles]')
.find('gn-ui-check-toggle label')
.eq(0)
.click()

cy.editor_publishAndReload()
cy.get('@saveStatus').should('eq', 'record_up_to_date')
cy.get('@accessContactPageBtn').click()

cy.get('[data-cy=constraints-shortcut-toggles]')
.find('gn-ui-check-toggle input[type=checkbox]')
.eq(0)
.invoke('val')
.should('eq', 'on')

// constraints are hidden
cy.get('[data-cy=legalConstraints]').should('not.exist')
cy.get('[data-cy=securityConstraints]').should('not.exist')
cy.get('[data-cy=otherConstraints]').should('not.exist')

// uncheck toggle
cy.get('[data-cy=constraints-shortcut-toggles]')
.find('gn-ui-check-toggle label')
.eq(0)
.click()

// remaining constraints are shown
cy.get('[data-cy=legalConstraints]').should('not.exist')
cy.get('[data-cy=securityConstraints]')
.find('gn-ui-constraint-card')
.should('have.length', 1)
cy.get('[data-cy=otherConstraints]')
.find('gn-ui-constraint-card')
.should('have.length', 1)
})
})
})
})
})
Original file line number Diff line number Diff line change
@@ -398,7 +398,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
<gmd:MD_ClassificationCode codeList="http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_ClassificationCode" codeListValue="restricted"/>
</gmd:classification>
<gmd:useLimitation>
<gco:CharacterString>Contains sensitive information related to national defense</gco:CharacterString>
<gmx:Anchor xlink:href="https://security.org/document.pdf">Contains sensitive information related to national defense</gmx:Anchor>
<gmd:PT_FreeText>
<gmd:textGroup>
<gmd:LocalisedCharacterString locale="#EN">Contains sensitive information related to national defense</gmd:LocalisedCharacterString>
Original file line number Diff line number Diff line change
@@ -514,7 +514,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
codeListValue="restricted"/>
</mco:classification>
<mco:useLimitation>
<gco:CharacterString>Contains sensitive information related to national defense</gco:CharacterString>
<gcx:Anchor xlink:href="https://security.org/document.pdf">Contains sensitive information related to national defense</gcx:Anchor>
<lan:PT_FreeText>
<mdb:textGroup>
<mdb:LocalisedCharacterString locale="#EN">Contains sensitive information related to national defense</mdb:LocalisedCharacterString>
Original file line number Diff line number Diff line change
@@ -377,7 +377,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
codeListValue="restricted"/>
</mco:classification>
<mco:useLimitation>
<gco:CharacterString>Contains sensitive information related to national defense</gco:CharacterString>
<gcx:Anchor xlink:href="https://security.org/document.pdf">Contains sensitive information related to national defense</gcx:Anchor>
<lan:PT_FreeText>
<mdb:textGroup>
<mdb:LocalisedCharacterString locale="#EN">Contains sensitive information related to national defense</mdb:LocalisedCharacterString>
Original file line number Diff line number Diff line change
@@ -316,7 +316,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
<gmd:MD_ClassificationCode codeList="http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_ClassificationCode" codeListValue="restricted"/>
</gmd:classification>
<gmd:useLimitation>
<gco:CharacterString>Contains sensitive information related to national defense</gco:CharacterString>
<gmx:Anchor xlink:href="https://security.org/document.pdf">Contains sensitive information related to national defense</gmx:Anchor>
<gmd:PT_FreeText>
<gmd:textGroup>
<gmd:LocalisedCharacterString locale="#EN">Contains sensitive information related to national defense</gmd:LocalisedCharacterString>
Original file line number Diff line number Diff line change
@@ -211,6 +211,7 @@ As such, **it is not very interesting at all.**`,
securityConstraints: [
{
text: 'Contains sensitive information related to national defense',
url: new URL('https://security.org/document.pdf'),
translations: {
text: {
fr: 'Contient des informations sensibles liées à la défense nationale',
54 changes: 54 additions & 0 deletions libs/api/metadata-converter/src/lib/iso19139/write-parts.spec.ts
Original file line number Diff line number Diff line change
@@ -12,10 +12,13 @@ import {
writeContacts,
writeContactsForResource,
writeKeywords,
writeLegalConstraints,
writeOnlineResources,
writeOtherConstraints,
writeResourceCreated,
writeResourcePublished,
writeResourceUpdated,
writeSecurityConstraints,
writeSpatialExtents,
writeSpatialRepresentation,
writeTemporalExtents,
@@ -973,6 +976,57 @@ describe('write parts', () => {
<gmd:identificationInfo>
<gmd:MD_DataIdentification/>
</gmd:identificationInfo>
</root>`)
})
})

describe('write constraints', () => {
it('writes elements without deleting others, remove empty constraints', () => {
writeSecurityConstraints(datasetRecord, rootEl)
writeLegalConstraints(datasetRecord, rootEl)
writeOtherConstraints(datasetRecord, rootEl)
writeLegalConstraints({ ...datasetRecord, legalConstraints: [] }, rootEl)
writeOtherConstraints(
{
...datasetRecord,
otherConstraints: [
{
text: 'new constraint',
},
],
},
rootEl
)
expect(rootAsString()).toEqual(`<root>
<gmd:identificationInfo>
<gmd:MD_DataIdentification>
<gmd:resourceConstraints>
<gmd:MD_SecurityConstraints>
<gmd:classification>
<gmd:MD_ClassificationCode codeList="http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_ClassificationCode" codeListValue="restricted"/>
</gmd:classification>
<gmd:useLimitation>
<gmx:Anchor xlink:href="https://security.org/document.pdf">Contains sensitive information related to national defense</gmx:Anchor>
<gmd:PT_FreeText>
<gmd:textGroup>
<gmd:LocalisedCharacterString locale="#EN">Contains sensitive information related to national defense</gmd:LocalisedCharacterString>
</gmd:textGroup>
<gmd:textGroup>
<gmd:LocalisedCharacterString locale="#FR">Contient des informations sensibles liées à la défense nationale</gmd:LocalisedCharacterString>
</gmd:textGroup>
</gmd:PT_FreeText>
</gmd:useLimitation>
</gmd:MD_SecurityConstraints>
</gmd:resourceConstraints>
<gmd:resourceConstraints>
<gmd:MD_Constraints>
<gmd:useLimitation>
<gco:CharacterString>new constraint</gco:CharacterString>
</gmd:useLimitation>
</gmd:MD_Constraints>
</gmd:resourceConstraints>
</gmd:MD_DataIdentification>
</gmd:identificationInfo>
</root>`)
})
})
178 changes: 130 additions & 48 deletions libs/api/metadata-converter/src/lib/iso19139/write-parts.ts
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@
tap,
} from '../function-utils'
import {
allChildrenElement,
appendChildren,
createChild,
createElement,
@@ -53,20 +54,13 @@
import { namePartsToFull } from './utils/individual-name'
import { LANG_2_TO_3_MAPPER } from '@geonetwork-ui/util/i18n/language-codes'

export function writeCharacterString(
text: string
): ChainableFunction<XmlElement, XmlElement> {
return tap(
pipe(findChildOrCreate('gco:CharacterString'), setTextContent(text))
)
}

export function writeLocalizedCharacterString(
function writeLocalizedElement(
writeFn: ChainableFunction<XmlElement, XmlElement>,
text: string,
translations: FieldTranslation,
defaultLanguage: LanguageCode
): ChainableFunction<XmlElement, XmlElement> {
if (!translations) return writeCharacterString(text)
if (!translations) return writeFn
function createLocalized(lang: LanguageCode, translation: string) {
return pipe(
createNestedElement('gmd:textGroup', 'gmd:LocalisedCharacterString'),
@@ -75,7 +69,7 @@
)
}
return pipe(
writeCharacterString(text),
writeFn,
removeChildrenByName('gmd:PT_FreeText'),
createChild('gmd:PT_FreeText'),
appendChildren(
@@ -87,6 +81,27 @@
)
}

export function writeCharacterString(
text: string
): ChainableFunction<XmlElement, XmlElement> {
return tap(
pipe(findChildOrCreate('gco:CharacterString'), setTextContent(text))
)
}

export function writeLocalizedCharacterString(
text: string,
translations: FieldTranslation,
defaultLanguage: LanguageCode
): ChainableFunction<XmlElement, XmlElement> {
return writeLocalizedElement(
writeCharacterString(text),
text,
translations,
defaultLanguage
)
}

export function writeLinkage(
url: URL
): ChainableFunction<XmlElement, XmlElement> {
@@ -111,6 +126,20 @@
)
}

export function writeLocalizedAnchor(
url: URL,
text: string,
translations: FieldTranslation,
defaultLanguage: LanguageCode
): ChainableFunction<XmlElement, XmlElement> {
return writeLocalizedElement(
writeAnchor(url, text),
text,
translations,
defaultLanguage
)
}

export function writeDateTime(
date: Date
): ChainableFunction<XmlElement, XmlElement> {
@@ -272,8 +301,8 @@

function appendResponsibleParty(
contact: Individual,
translations: RecordTranslations,

Check warning on line 304 in libs/api/metadata-converter/src/lib/iso19139/write-parts.ts

GitHub Actions / Format check, lint, unit tests

'translations' is defined but never used
defaultLanguage: LanguageCode

Check warning on line 305 in libs/api/metadata-converter/src/lib/iso19139/write-parts.ts

GitHub Actions / Format check, lint, unit tests

'defaultLanguage' is defined but never used
) {
const fullName = namePartsToFull(contact.firstName, contact.lastName)

@@ -466,11 +495,18 @@
),
pipe(
createElement('gmd:useLimitation'),
writeLocalizedCharacterString(
constraint.text,
constraint.translations?.text,
defaultLanguage
)
'url' in constraint
? writeLocalizedAnchor(
constraint.url,
constraint.text,
constraint.translations?.text,
defaultLanguage
)
: writeLocalizedCharacterString(
constraint.text,
constraint.translations?.text,
defaultLanguage
)
)
)
)
@@ -491,78 +527,121 @@
),
pipe(
createElement('gmd:otherConstraints'),
writeLocalizedCharacterString(
constraint.text,
constraint.translations?.text,
defaultLanguage
)
'url' in constraint
? writeLocalizedAnchor(
constraint.url,
constraint.text,
constraint.translations?.text,
defaultLanguage
)
: writeLocalizedCharacterString(
constraint.text,
constraint.translations?.text,
defaultLanguage
)
)
)
)
}

// other
return pipe(
createNestedElement(
'gmd:resourceConstraints',
'gmd:MD_Constraints',
'gmd:useLimitation'
),
writeLocalizedCharacterString(
constraint.text,
constraint.translations?.text,
defaultLanguage
)
'url' in constraint
? writeLocalizedAnchor(
constraint.url,
constraint.text,
constraint.translations?.text,
defaultLanguage
)
: writeLocalizedCharacterString(
constraint.text,
constraint.translations?.text,
defaultLanguage
)
)
}

export function removeOtherConstraints() {
return removeChildren(
return tap(
pipe(
findChildrenElement('gmd:resourceConstraints'),
filterArray(
pipe(
findNestedElements('gmd:MD_Constraints', 'gmd:useLimitation'),
(array) => array.length > 0
mapArray(
removeChildren(
pipe(
findChildrenElement('gmd:MD_Constraints'),
filterArray(
pipe(
findNestedElements('gmd:useLimitation'),
(array) => array.length > 0
)
)
)
)
)
)
)
}

export function removeSecurityConstraints() {
return removeChildren(
return tap(
pipe(
findChildrenElement('gmd:resourceConstraints'),
filterArray(
pipe(
findNestedElements('gmd:MD_SecurityConstraints', 'gmd:useLimitation'),
(array) => array.length > 0
mapArray(
removeChildren(
pipe(
findChildrenElement('gmd:MD_SecurityConstraints'),
filterArray(
pipe(
findNestedElements('gmd:useLimitation'),
(array) => array.length > 0
)
)
)
)
)
)
)
}

export function removeLegalConstraints() {
return removeChildren(
return tap(
pipe(
findChildrenElement('gmd:resourceConstraints'),
filterArray(
pipe(
findNestedElements(
'gmd:MD_LegalConstraints',
'gmd:accessConstraints',
'gmd:MD_RestrictionCode'
),
mapArray(readAttribute('codeListValue')),
(restrictionCodes) =>
restrictionCodes.every((code) => code !== 'license')
mapArray(
removeChildren(
pipe(
findChildrenElement('gmd:MD_LegalConstraints'),
filterArray(
pipe(
findNestedElements(
'gmd:accessConstraints',
'gmd:MD_RestrictionCode'
),
mapArray(readAttribute('codeListValue')),
(restrictionCodes) =>
restrictionCodes.every((code) => code !== 'license')
)
)
)
)
)
)
)
}

export function removeEmptyResourceConstraints() {
return removeChildren(
pipe(
findChildrenElement('gmd:resourceConstraints'),
filterArray(pipe(allChildrenElement, (array) => array.length === 0))
)
)
}

export function removeLicenses() {
return removeChildren(
pipe(
@@ -884,6 +963,7 @@
pipe(
findOrCreateIdentification(),
removeLegalConstraints(),
removeEmptyResourceConstraints(),
appendChildren(
...record.legalConstraints.map((c) =>
createConstraint(c, 'legal', record.defaultLanguage)
@@ -899,6 +979,7 @@
pipe(
findOrCreateIdentification(),
removeSecurityConstraints(),
removeEmptyResourceConstraints(),
appendChildren(
...record.securityConstraints.map((c) =>
createConstraint(c, 'security', record.defaultLanguage)
@@ -914,6 +995,7 @@
pipe(
findOrCreateIdentification(),
removeOtherConstraints(),
removeEmptyResourceConstraints(),
appendChildren(
...record.otherConstraints.map((c) =>
createConstraint(c, 'other', record.defaultLanguage)
6 changes: 6 additions & 0 deletions libs/feature/editor/src/lib/+state/editor.actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createAction, props } from '@ngrx/store'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { SaveRecordError } from './editor.models'
import { EditorFieldIdentification } from '../models'

export const openRecord = createAction(
'[Editor] Open record',
@@ -35,3 +36,8 @@ export const setCurrentPage = createAction(
'[Editor] Set current page',
props<{ page: number }>()
)

export const setFieldVisibility = createAction(
'[Editor] Set field visibility',
props<{ field: EditorFieldIdentification; visible: boolean }>()
)
5 changes: 5 additions & 0 deletions libs/feature/editor/src/lib/+state/editor.facade.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import * as EditorSelectors from './editor.selectors'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { filter } from 'rxjs'
import { Actions, ofType } from '@ngrx/effects'
import { EditorFieldIdentification } from '../models'

@Injectable()
export class EditorFacade {
@@ -58,4 +59,8 @@ export class EditorFacade {
setCurrentPage(page: number) {
this.store.dispatch(EditorActions.setCurrentPage({ page }))
}

setFieldVisibility(field: EditorFieldIdentification, visible: boolean) {
this.store.dispatch(EditorActions.setFieldVisibility({ field, visible }))
}
}
21 changes: 21 additions & 0 deletions libs/feature/editor/src/lib/+state/editor.reducer.ts
Original file line number Diff line number Diff line change
@@ -83,6 +83,27 @@ const reducer = createReducer(
on(EditorActions.setCurrentPage, (state, { page }) => ({
...state,
currentPage: page,
})),
on(EditorActions.setFieldVisibility, (state, { field, visible }) => ({
...state,
editorConfig: {
...state.editorConfig,
pages: state.editorConfig.pages.map((page) => ({
...page,
sections: page.sections.map((section) => ({
...section,
fields: section.fields.map((f) => {
if (f.model === field.model) {
return {
...f,
hidden: !visible,
}
}
return f
}),
})),
})),
},
}))
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:host {
--gn-ui-button-padding: 8px 8px;
--gn-ui-button-rounded: 8px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="flex flex-col gap-1 border rounded-lg p-4">
<gn-ui-form-field-rich
class="mt-[-8px]"
[label]="label"
[hint]="hint"
[value]="constraint_.text ?? ''"
(valueChange)="handleConstraintTextChange($event)"
></gn-ui-form-field-rich>

<div *ngIf="!showUrl" class="flex-none mt-1">
<gn-ui-button
(buttonClick)="showUrl = true"
type="gray"
data-cy="add-url-btn"
>
<ng-icon name="iconoirPlus" class="text-primary"></ng-icon>&nbsp;
<span translate>input.image.displayUrlInput</span>
</gn-ui-button>
</div>
<gn-ui-url-input
*ngIf="showUrl"
class="mt-3.5"
[value]="constraint_.url?.toString()"
(valueChange)="handleURLChange($event)"
[showUploadButton]="false"
>
</gn-ui-url-input>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'

import { ConstraintCardComponent } from './constraint-card.component'
import { importProvidersFrom } from '@angular/core'
import { TranslateModule } from '@ngx-translate/core'

describe('ConstraintCardComponent', () => {
let component: ConstraintCardComponent
let fixture: ComponentFixture<ConstraintCardComponent>

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ConstraintCardComponent],
providers: [importProvidersFrom(TranslateModule.forRoot())],
})
fixture = TestBed.createComponent(ConstraintCardComponent)
component = fixture.componentInstance
component.constraint = {
text: 'This is a multiline and **formatted** constraint text.',
}
fixture.detectChanges()
})

it('should create', () => {
expect(component).toBeTruthy()
})

describe('showUrlInput', () => {
it('returns true if url is not nullish', () => {
component.constraint = {
text: 'abcd',
url: new URL('https://example.com/my-license.pdf'),
}
expect(component.showUrl).toBe(true)
})
it('returns true if showUrl button was clicked once', () => {
component.showUrl = true
expect(component.showUrl).toBe(true)
})
it('returns false otherwise', () => {
expect(component.showUrl).toBe(false)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
applicationConfig,
Meta,
moduleMetadata,
StoryObj,
} from '@storybook/angular'
import { ConstraintCardComponent } from './constraint-card.component'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { importProvidersFrom } from '@angular/core'
import { CommonModule } from '@angular/common'
import { TranslateModule } from '@ngx-translate/core'
import { UtilI18nModule } from '@geonetwork-ui/util/i18n'

export default {
title: 'Elements/ConstraintCardComponent',
component: ConstraintCardComponent,
decorators: [
moduleMetadata({
imports: [CommonModule, ConstraintCardComponent],
}),
applicationConfig({
providers: [
importProvidersFrom(BrowserAnimationsModule),
importProvidersFrom(UtilI18nModule),
importProvidersFrom(TranslateModule.forRoot()),
],
}),
],
argTypes: {
constraintChange: {
action: 'constraintChange',
},
},
} as Meta<ConstraintCardComponent>

type ConstraintCardComponentProps = {
label: string
constraint: {
text: string
url: string
}
}

export const WithUrl: StoryObj<ConstraintCardComponent> = {
args: {
label: 'My constraint',
constraint: {
text: `This is a multiline and **formatted** constraint text.
## introduction
It covers:
- things
- other things
_and it's great_.`,
url: new URL('https://example.com/my-license.pdf'),
},
},
}
export const WithoutUrl: StoryObj<ConstraintCardComponent> = {
args: {
label: 'My constraint',
constraint: {
text: `This is a multiline and **formatted** constraint text.`,
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
import { CommonModule } from '@angular/common'
import { MarkdownEditorComponent } from '@geonetwork-ui/ui/elements'
import { FormFieldRichComponent } from '../record-form/form-field/form-field-rich/form-field-rich.component'
import { ButtonComponent, UrlInputComponent } from '@geonetwork-ui/ui/inputs'
import { MatIconModule } from '@angular/material/icon'
import { TranslateModule } from '@ngx-translate/core'
import { Constraint } from '@geonetwork-ui/common/domain/model/record'
import {
NgIconComponent,
provideIcons,
provideNgIconsConfig,
} from '@ng-icons/core'
import { iconoirPlus } from '@ng-icons/iconoir'

@Component({
selector: 'gn-ui-constraint-card',
standalone: true,
imports: [
CommonModule,
MarkdownEditorComponent,
FormFieldRichComponent,
UrlInputComponent,
ButtonComponent,
MatIconModule,
TranslateModule,
NgIconComponent,
],
providers: [
provideIcons({ iconoirPlus }),
provideNgIconsConfig({
size: '1.5rem',
}),
],
templateUrl: './constraint-card.component.html',
styleUrls: ['./constraint-card.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConstraintCardComponent {
@Input() label: string
constraint_: Constraint
@Input() set constraint(v: Constraint) {
this.constraint_ = v
this.showUrl = this.showUrl || !!v.url
}
@Output() constraintChange = new EventEmitter<Constraint>()

hint = 'editor.record.form.constraint.markdown.placeholder' // TODO: get text and translate
showUrl = false

handleConstraintTextChange(text: string) {
this.constraintChange.emit({
...this.constraint_,
text,
})
}

handleURLChange(url: string | null) {
this.constraintChange.emit({
text: this.constraint_.text,
...(url && { url: new URL(url) }),
})
}
}
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@
<span class="font-bold"> {{ externalImportBackLabel }}</span>
</div>
<gn-ui-url-input
(valueChange)="importRecord($event)"
(uploadClick)="importRecord($event)"
[disabled]="isRecordImportInProgress"
></gn-ui-url-input>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
matchesNoApplicableConstraint,
matchesNoKnownConstraint,
NOT_APPLICABLE_CONSTRAINT,
NOT_KNOWN_CONSTRAINT,
} from './constraints.utils'

describe('constraints utils', () => {
describe('matchesNoApplicableConstraint', () => {
it('matches based on url', () => {
expect(
matchesNoApplicableConstraint({
text: 'hello world',
url: NOT_APPLICABLE_CONSTRAINT.url,
})
).toBe(true)
})
it('matches based on text (1)', () => {
expect(
matchesNoApplicableConstraint({ text: ' No conditions apply ' })
).toBe(true)
})
it('matches based on text (2)', () => {
expect(
matchesNoApplicableConstraint({
text: ' NO conditions apply to access and use ',
})
).toBe(true)
})
it('matches based on text (FR)', () => {
expect(
matchesNoApplicableConstraint({
text: "aucune condition ne s'applique ",
})
).toBe(true)
})
it('matches based on text translation', () => {
expect(
matchesNoApplicableConstraint({
text: ' bonjour monde ',
translations: {
text: { en: 'no conditions apply', de: 'hallo welt' },
},
})
).toBe(true)
})
it('returns false otherwise', () => {
expect(
matchesNoApplicableConstraint({
text: ' bonjour monde ',
translations: {
text: { en: 'hello world', de: 'hallo welt' },
},
url: new URL('https://some.licence.org/abc.pdf'),
})
).toBe(false)
})
})

describe('matchesNoKnownConstraint', () => {
it('matches based on url', () => {
expect(
matchesNoKnownConstraint({
text: 'hello world',
url: NOT_KNOWN_CONSTRAINT.url,
})
).toBe(true)
})
it('matches based on text', () => {
expect(matchesNoKnownConstraint({ text: ' Conditions unknown ' })).toBe(
true
)
})
it('matches based on text (FR)', () => {
expect(
matchesNoKnownConstraint({
text: 'CONDITIONS inconnues ',
})
).toBe(true)
})
it('matches based on text translation', () => {
expect(
matchesNoKnownConstraint({
text: ' bonjour monde ',
translations: {
text: { en: 'Conditions unknown', de: 'hallo welt' },
},
})
).toBe(true)
})
it('returns false otherwise', () => {
expect(
matchesNoKnownConstraint({
text: ' bonjour monde ',
translations: {
text: { en: 'hello world', de: 'hallo welt' },
},
url: new URL('https://some.licence.org/abc.pdf'),
})
).toBe(false)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Constraint } from '@geonetwork-ui/common/domain/model/record'

export const NOT_APPLICABLE_CONSTRAINT: Constraint = {
text: 'No conditions apply to access and use',
url: new URL(
'http://inspire.ec.europa.eu/metadata-codelist/ConditionsApplyingToAccessAndUse/noConditionsApply'
),
}
export const NOT_KNOWN_CONSTRAINT: Constraint = {
text: 'Conditions unknown',
url: new URL(
'http://inspire.ec.europa.eu/metadata-codelist/ConditionsApplyingToAccessAndUse/conditionsUnknown'
),
}

export function matchesNoApplicableConstraint(constraint: Constraint): boolean {
if (constraint.url?.toString() === NOT_APPLICABLE_CONSTRAINT.url.toString()) {
return true
}
const match = (text: string) => {
if (!text) return false
const trimmed = text.toLowerCase().trim()
return (
trimmed == 'no conditions apply' ||
trimmed == 'no conditions apply to access and use' ||
trimmed == "aucune condition ne s'applique"
)
}
return match(constraint.text) || match(constraint.translations?.text?.en)
}

export function matchesNoKnownConstraint(constraint: Constraint): boolean {
if (constraint.url?.toString() === NOT_KNOWN_CONSTRAINT.url.toString()) {
return true
}
const match = (text: string) => {
if (!text) return false
const trimmed = text.toLowerCase().trim()
return trimmed == 'conditions unknown' || trimmed == 'conditions inconnues'
}
return match(constraint.text) || match(constraint.translations?.text?.en)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:host {
--gn-ui-button-padding: 8px 8px;
--gn-ui-button-rounded: 8px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<div class="flex flex-col gap-2 mb-2" data-cy="constraints-shortcut-toggles">
<gn-ui-check-toggle
[label]="'editor.record.form.constraint.not.applicable' | translate"
[value]="noApplicableConstraint$ | async"
(toggled)="onToggleChange('noApplicableConstraint', $event)"
>
</gn-ui-check-toggle>
<gn-ui-check-toggle
[label]="'editor.record.form.constraint.not.known' | translate"
[value]="noKnownConstraint$ | async"
(toggled)="onToggleChange('noKnownConstraint', $event)"
>
</gn-ui-check-toggle>
</div>

<div
*ngIf="(anyToggleActivated$ | async) === false"
class="flex flex-row flex-wrap gap-2"
data-cy="constraints-shortcut-btns"
>
<ng-container *ngFor="let constraint of constraintButtonChoices">
<gn-ui-button
type="gray"
(buttonClick)="addConstraintSectionToDisplay(constraint)"
[disabled]="isConstraintButtonDisabled$(constraint) | async"
>
<ng-icon name="iconoirPlus" class="text-primary"></ng-icon>
&nbsp;
<span>{{
'editor.record.form.constraint.' + constraint | translate
}}</span>
</gn-ui-button>
</ng-container>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormFieldConstraintsShortcutsComponent } from './form-field-constraints-shortcuts.component'
import { MockBuilder, MockProvider } from 'ng-mocks'
import { EditorFacade } from '../../../../+state/editor.facade'
import { importProvidersFrom } from '@angular/core'
import { TranslateModule } from '@ngx-translate/core'
import { BehaviorSubject, firstValueFrom } from 'rxjs'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures'
import {
NOT_APPLICABLE_CONSTRAINT,
NOT_KNOWN_CONSTRAINT,
} from './constraints.utils'

describe('FormFieldConstraintsShortcutsComponent', () => {
let component: FormFieldConstraintsShortcutsComponent
let fixture: ComponentFixture<FormFieldConstraintsShortcutsComponent>
let editorFacade: EditorFacade
let sampleRecord: CatalogRecord
let sampleRecord$: BehaviorSubject<CatalogRecord>

beforeEach(() => MockBuilder(FormFieldConstraintsShortcutsComponent))

beforeEach(() => {
sampleRecord = datasetRecordsFixture()[0]
sampleRecord$ = new BehaviorSubject(sampleRecord)

TestBed.configureTestingModule({
providers: [
MockProvider(EditorFacade, {
record$: sampleRecord$,
}),
importProvidersFrom(TranslateModule.forRoot()),
],
})
editorFacade = TestBed.inject(EditorFacade)
fixture = TestBed.createComponent(FormFieldConstraintsShortcutsComponent)
component = fixture.componentInstance
fixture.detectChanges()
})

it('should create', () => {
expect(component).toBeTruthy()
})

describe('noApplicableConstraints$', () => {
it('should emit true if "no conditions apply" is present', async () => {
sampleRecord$.next({
...sampleRecord,
legalConstraints: [{ text: 'no conditions apply' }],
})
const result = await firstValueFrom(component.noApplicableConstraint$)
expect(result).toBe(true)
})

it('should emit false if "no conditions apply" is not present', async () => {
sampleRecord$.next({
...sampleRecord,
legalConstraints: [{ text: 'Any other constraint' }],
})
const result = await firstValueFrom(component.noApplicableConstraint$)
expect(result).toBe(false)
})
})

describe('noKnownConstraints$', () => {
it('should emit true if "unknown conditions" is present', async () => {
sampleRecord$.next({
...sampleRecord,
legalConstraints: [{ text: 'conditions unknown' }],
})
const result = await firstValueFrom(component.noKnownConstraint$)
expect(result).toBe(true)
})

it('should emit false if "unknown conditions" is not present', async () => {
sampleRecord$.next({
...sampleRecord,
legalConstraints: [{ text: 'any other constraint' }],
})
const result = await firstValueFrom(component.noKnownConstraint$)
expect(result).toBe(false)
})
})

describe('onToggleChange', () => {
beforeEach(() => {
sampleRecord$.next({
...sampleRecord,
legalConstraints: [
{
text: 'no known',
url: NOT_KNOWN_CONSTRAINT.url,
},
{
text: 'another constraint',
},
{
text: 'no applicable',
url: NOT_APPLICABLE_CONSTRAINT.url,
},
],
})
})
it('should update legal constraints and hide all sections when noApplicableConstraint toggled on', () => {
component.onToggleChange('noApplicableConstraint', true)
expect(editorFacade.updateRecordField).toHaveBeenCalledWith(
'legalConstraints',
[NOT_APPLICABLE_CONSTRAINT]
)
expect(editorFacade.setFieldVisibility).toHaveBeenCalledWith(
{ model: 'legalConstraints' },
false
)
expect(editorFacade.setFieldVisibility).toHaveBeenCalledWith(
{ model: 'securityConstraints' },
false
)
expect(editorFacade.setFieldVisibility).toHaveBeenCalledWith(
{ model: 'otherConstraints' },
false
)
})
it('should update legal constraints and hide all sections when noKnownConstraint toggled on', () => {
jest.spyOn(component, 'hideAllConstraintSections')
component.onToggleChange('noKnownConstraint', true)
expect(editorFacade.updateRecordField).toHaveBeenCalledWith(
'legalConstraints',
[NOT_KNOWN_CONSTRAINT]
)
expect(editorFacade.setFieldVisibility).toHaveBeenCalledWith(
{ model: 'legalConstraints' },
false
)
expect(editorFacade.setFieldVisibility).toHaveBeenCalledWith(
{ model: 'securityConstraints' },
false
)
expect(editorFacade.setFieldVisibility).toHaveBeenCalledWith(
{ model: 'otherConstraints' },
false
)
})
it('should remove all legal constraints matching "no applicable" when toggled off', async () => {
await component.onToggleChange('noApplicableConstraint', false)
expect(editorFacade.updateRecordField).toHaveBeenCalledWith(
'legalConstraints',
[
{
text: 'no known',
url: NOT_KNOWN_CONSTRAINT.url,
},
{
text: 'another constraint',
},
]
)
})
it('should remove all legal constraints matching "no known" when toggled off', async () => {
await component.onToggleChange('noKnownConstraint', false)
expect(editorFacade.updateRecordField).toHaveBeenCalledWith(
'legalConstraints',
[
{
text: 'another constraint',
},
{
text: 'no applicable',
url: NOT_APPLICABLE_CONSTRAINT.url,
},
]
)
})
})

describe('fields visibility change', () => {
function getLastCallForField(model: string) {
const calls = (
editorFacade.setFieldVisibility as jest.Mock
).mock.calls.filter(([field]) => field.model === model)
return calls[calls.length - 1]
}

describe.each([
'legalConstraints',
'securityConstraints',
'otherConstraints',
])('for field %s', (fieldName) => {
it('is visible if not empty at first and no toggles activated', () => {
sampleRecord$.next({
...sampleRecord,
[fieldName]: [{ text: 'some constraint' }],
})
fixture.detectChanges()
expect(getLastCallForField(fieldName)).toEqual([
{ model: fieldName },
true,
])
})
it('is hidden if not empty at first and NOT_APPLICABLE_CONSTRAINT present', () => {
sampleRecord$.next({
...sampleRecord,
[fieldName]: [{ text: 'some constraint' }],
legalConstraints: [NOT_APPLICABLE_CONSTRAINT],
})
fixture.detectChanges()
expect(getLastCallForField(fieldName)).toEqual([
{ model: fieldName },
false,
])
})
it('is hidden if field is empty', () => {
sampleRecord$.next({
...sampleRecord,
[fieldName]: [],
})
fixture.detectChanges()
expect(getLastCallForField(fieldName)).toEqual([
{ model: fieldName },
false,
])
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import {
ChangeDetectionStrategy,
Component,
OnDestroy,
OnInit,
} from '@angular/core'
import { CommonModule } from '@angular/common'
import { EditorFacade } from '../../../../+state/editor.facade'
import { ButtonComponent, CheckToggleComponent } from '@geonetwork-ui/ui/inputs'
import { TranslateModule } from '@ngx-translate/core'
import {
combineLatest,
distinctUntilChanged,
firstValueFrom,
map,
Observable,
Subject,
takeUntil,
} from 'rxjs'
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
import { Constraint } from '@geonetwork-ui/common/domain/model/record'
import {
matchesNoApplicableConstraint,
matchesNoKnownConstraint,
NOT_APPLICABLE_CONSTRAINT,
NOT_KNOWN_CONSTRAINT,
} from './constraints.utils'
import {
NgIconComponent,
provideIcons,
provideNgIconsConfig,
} from '@ng-icons/core'
import { iconoirPlus } from '@ng-icons/iconoir'

marker('editor.record.form.constraint.legalConstraints')
marker('editor.record.form.constraint.securityConstraints')
marker('editor.record.form.constraint.otherConstraints')

export type ConstraintChoice =
| 'legalConstraints'
| 'securityConstraints'
| 'otherConstraints'

/**
* This component offers two toggles to easily define common constraints (no applicable constraint
* and no known constraint) and shows and hides the various constraints fields accordingly using
* the facade
*/
@Component({
selector: 'gn-ui-form-field-constraints-shortcuts',
standalone: true,
imports: [
CommonModule,
CheckToggleComponent,
ButtonComponent,
TranslateModule,
NgIconComponent,
],
providers: [
provideIcons({ iconoirPlus }),
provideNgIconsConfig({
size: '1.5rem',
}),
],
templateUrl: './form-field-constraints-shortcuts.component.html',
styleUrls: ['./form-field-constraints-shortcuts.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormFieldConstraintsShortcutsComponent
implements OnInit, OnDestroy
{
legalConstraints$ = this.editorFacade.record$.pipe(
map((record) => record?.legalConstraints ?? [])
)
securityConstraints$ = this.editorFacade.record$.pipe(
map((record) => record?.securityConstraints ?? [])
)
otherConstraints$ = this.editorFacade.record$.pipe(
map((record) => record?.otherConstraints ?? [])
)

noApplicableConstraint$: Observable<boolean> = this.legalConstraints$.pipe(
map((constraints) =>
constraints.some((constraint) =>
matchesNoApplicableConstraint(constraint)
)
)
)
noKnownConstraint$: Observable<boolean> = this.legalConstraints$.pipe(
map((constraints) =>
constraints.some((constraint) => matchesNoKnownConstraint(constraint))
)
)
anyToggleActivated$ = combineLatest([
this.noApplicableConstraint$,
this.noKnownConstraint$,
]).pipe(
map(
([noApplicableConstraint, noKnownConstraint]) =>
noApplicableConstraint || noKnownConstraint
)
)

constraintButtonChoices: ConstraintChoice[] = [
'legalConstraints',
'securityConstraints',
'otherConstraints',
]

onDestroy$ = new Subject<void>()

constructor(private editorFacade: EditorFacade) {}

ngOnInit(): void {
// hide all constraints if any toggle is activated
this.anyToggleActivated$
.pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
.subscribe((anyToggleActivated) => {
if (anyToggleActivated) {
this.hideAllConstraintSections()
}
})

// also hide constraints which are empty arrays
const hideEmptyConstraints = (
constraints$: Observable<Constraint[]>,
model: ConstraintChoice
) => {
const isConstraintNotEmpty$ = constraints$.pipe(
takeUntil(this.onDestroy$),
map((c) => c.length > 0),
distinctUntilChanged()
)
combineLatest([
isConstraintNotEmpty$,
this.anyToggleActivated$,
]).subscribe(([isNotEmpty, anyToggleActivated]) => {
const visible = isNotEmpty && !anyToggleActivated
this.editorFacade.setFieldVisibility({ model }, visible)
})
}
hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
}

ngOnDestroy() {
this.onDestroy$.next()
}

hideAllConstraintSections() {
this.editorFacade.setFieldVisibility({ model: 'legalConstraints' }, false)
this.editorFacade.setFieldVisibility(
{ model: 'securityConstraints' },
false
)
this.editorFacade.setFieldVisibility({ model: 'otherConstraints' }, false)
}

async onToggleChange(
toggleName: 'noApplicableConstraint' | 'noKnownConstraint',
value: boolean
) {
if (value) {
const presetConstraint =
toggleName === 'noApplicableConstraint'
? NOT_APPLICABLE_CONSTRAINT
: NOT_KNOWN_CONSTRAINT
this.editorFacade.updateRecordField('legalConstraints', [
presetConstraint,
])
this.hideAllConstraintSections()
} else {
const matcher =
toggleName === 'noApplicableConstraint'
? matchesNoApplicableConstraint
: matchesNoKnownConstraint
// if the toggle is turned off, remove all matching constraints
const constraints = await firstValueFrom(this.legalConstraints$)
this.editorFacade.updateRecordField(
'legalConstraints',
constraints.filter((c) => !matcher(c))
)
}
}

isConstraintButtonDisabled$(
constraintSection: ConstraintChoice
): Observable<boolean> {
switch (constraintSection) {
case 'legalConstraints':
return this.legalConstraints$.pipe(
map((constraints) => constraints.length > 0)
)
case 'securityConstraints':
return this.securityConstraints$.pipe(
map((constraints) => constraints.length > 0)
)
case 'otherConstraints':
return this.otherConstraints$.pipe(
map((constraints) => constraints.length > 0)
)
}
}

addConstraintSectionToDisplay(constraintSection: ConstraintChoice) {
this.editorFacade.updateRecordField(constraintSection, [{ text: '' }])
this.editorFacade.setFieldVisibility({ model: constraintSection }, true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:host {
--gn-ui-button-padding: 8px 8px;
--gn-ui-button-rounded: 8px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="w-full flex flex-col gap-2" [attr.data-cy]="constraintType">
<span class="border-t border-gray-300 flex grow pb-[32px]"></span>
<span class="font-bold mb-3">{{ constraintsHeader | translate }}</span>
<gn-ui-button
type="gray"
(buttonClick)="addConstraintSectionToDisplay()"
data-cy="add-constraint-btn"
>
<ng-icon name="iconoirPlus" class="text-primary"></ng-icon>
&nbsp;<span>{{ additionalConstraintsButtonLabel | translate }}</span>
</gn-ui-button>
<gn-ui-sortable-list
[items]="value"
(itemsOrderChange)="handleConstraintsOrderChange($event)"
[elementTemplate]="constraintTemplate"
></gn-ui-sortable-list>
<ng-template #constraintTemplate let-constraint let-index="index">
<gn-ui-constraint-card
[label]="label"
[constraint]="constraint"
(constraintChange)="handleConstraintChange($event, index)"
></gn-ui-constraint-card>
</ng-template>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'

import { FormFieldConstraintsComponent } from './form-field-constraints.component'
import { MockBuilder, MockInstance, MockProvider } from 'ng-mocks'
import { EditorFacade } from '../../../../+state/editor.facade'
import { BehaviorSubject, of } from 'rxjs'
import { datasetRecordsFixture } from '@geonetwork-ui/common/fixtures'
import { importProvidersFrom } from '@angular/core'
import { TranslateModule } from '@ngx-translate/core'

const mockLegalConstraints = [...datasetRecordsFixture()[0].legalConstraints]

const mockConstraints = new BehaviorSubject([
{
legalConstraints: mockLegalConstraints,
securityConstraints: [],
otherConstraints: [],
},
])

describe('FormFieldConstraintsComponent', () => {
MockInstance.scope()
let component: FormFieldConstraintsComponent
let fixture: ComponentFixture<FormFieldConstraintsComponent>
const constraintType = 'legalConstraints'

beforeEach(() => {
return MockBuilder(FormFieldConstraintsComponent)
})

beforeEach(() =>
MockInstance(EditorFacade, 'record$', jest.fn(), 'get').mockReturnValue(
of(mockConstraints)
)
)

beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormFieldConstraintsComponent],
providers: [
MockProvider(EditorFacade),
importProvidersFrom(TranslateModule.forRoot()),
],
})
fixture = TestBed.createComponent(FormFieldConstraintsComponent)
component = fixture.componentInstance
component.label = 'Constraints'
component.value = datasetRecordsFixture()[0].legalConstraints
component.constraintType = constraintType
fixture.detectChanges()
})

it('should create', () => {
expect(component).toBeTruthy()
})

it('should use the correct translation keys', () => {
expect(component.additionalConstraintsButtonLabel).toEqual(
`editor.record.form.constraint.add.${constraintType}`
)
expect(component.constraintsHeader).toEqual(
`editor.record.form.constraint.header.${constraintType}`
)
})

it('#handleConstraintChange should emit the new value', () => {
jest.spyOn(component.valueChange, 'emit')
const newConstraint = { text: 'aaa', url: new URL('http://example.com') }
component.handleConstraintChange(newConstraint, 0)
expect(component.valueChange.emit).toHaveBeenCalledWith([
newConstraint,
mockLegalConstraints[1],
])
})

it('#handleConstraintsOrderChange should emit the new value', () => {
jest.spyOn(component.valueChange, 'emit')
const newConstraints = [...datasetRecordsFixture()[0].legalConstraints]
newConstraints.push({ text: 'New constraint' })

component.handleConstraintsOrderChange(newConstraints)

fixture.detectChanges()
expect(component.valueChange.emit).toHaveBeenCalledWith(newConstraints)
})

it('#addConstraintSectionToDisplay should add a new constraint', () => {
jest.spyOn(component.valueChange, 'emit')
const initialConstraints = [...component.value]
component.addConstraintSectionToDisplay()

fixture.detectChanges()
expect(component.valueChange.emit).toHaveBeenCalledWith([
...initialConstraints,
{ text: '' },
])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core'
import { CommonModule } from '@angular/common'
import { SortableListComponent } from '@geonetwork-ui/ui/layout'
import { ConstraintCardComponent } from '../../../constraint-card/constraint-card.component'
import {
CatalogRecordKeys,
Constraint,
} from '@geonetwork-ui/common/domain/model/record'
import { ButtonComponent, UiInputsModule } from '@geonetwork-ui/ui/inputs'
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
import { TranslateModule } from '@ngx-translate/core'
import {
NgIconComponent,
provideIcons,
provideNgIconsConfig,
} from '@ng-icons/core'
import { iconoirPlus } from '@ng-icons/iconoir'

marker('editor.record.form.constraint.add.legalConstraints')
marker('editor.record.form.constraint.add.securityConstraints')
marker('editor.record.form.constraint.add.otherConstraints')
marker('editor.record.form.constraint.header.legalConstraints')
marker('editor.record.form.constraint.header.securityConstraints')
marker('editor.record.form.constraint.header.otherConstraints')

@Component({
selector: 'gn-ui-form-field-constraints',
imports: [
CommonModule,
SortableListComponent,
ConstraintCardComponent,
UiInputsModule,
ButtonComponent,
TranslateModule,
NgIconComponent,
],
providers: [
provideIcons({ iconoirPlus }),
provideNgIconsConfig({
size: '1.5rem',
}),
],
templateUrl: './form-field-constraints.component.html',
styleUrls: ['./form-field-constraints.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class FormFieldConstraintsComponent implements OnInit {
@Input() label: string
@Input() value: Constraint[]
@Input() constraintType: CatalogRecordKeys
@Output() valueChange = new EventEmitter<Constraint[]>()

constraintsHeader = ''
additionalConstraintsButtonLabel = ''

ngOnInit() {
this.additionalConstraintsButtonLabel = `editor.record.form.constraint.add.${this.constraintType}`
this.constraintsHeader = `editor.record.form.constraint.header.${this.constraintType}`
}

handleConstraintChange(constraint: Constraint, index: number) {
const newValue = [...this.value]
newValue[index] = constraint
this.valueChange.emit(newValue)
}

handleConstraintsOrderChange(constraints: Constraint[]) {
const updatedConstraints = [...constraints]
this.valueChange.emit(updatedConstraints)
}

addConstraintSectionToDisplay() {
const updatedConstraints = [...this.value]
updatedConstraints.push({ text: '' }) // url?
this.valueChange.emit(updatedConstraints)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<gn-ui-dropdown-selector
[title]="label"
[showTitle]="false"
[choices]="choices"
[selected]="selected"
(selectValue)="onSelectValue($event)"
>
</gn-ui-dropdown-selector>
<div class="flex flex-col gap-8">
<gn-ui-dropdown-selector
[title]="label"
[showTitle]="false"
[choices]="licenceOptions"
[selected]="selectedLicence"
(selectValue)="handleLicenceSelection($event)"
>
</gn-ui-dropdown-selector>
</div>
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ describe('FormFieldLicenseComponent', () => {
fixture = TestBed.createComponent(FormFieldLicenseComponent)
component = fixture.componentInstance
component.label = 'License' // TODO: translate
component.value = [{ text: 'cc-by' }]
component.recordConstraints = [{ text: 'cc-by' }]
fixture.detectChanges()
})

@@ -24,13 +24,13 @@ describe('FormFieldLicenseComponent', () => {
})
describe('#selected', () => {
it('should get the selected value', () => {
expect(component.selected).toBe('cc-by')
expect(component.selectedLicence).toBe('cc-by')
})
})
describe('#onSelectValue', () => {
it('should emit the selected value', () => {
const spy = jest.spyOn(component.valueChange, 'emit')
component.onSelectValue('cc-by-sa')
const spy = jest.spyOn(component.recordConstraintsChange, 'emit')
component.handleLicenceSelection('cc-by-sa')
expect(spy).toHaveBeenCalledWith([{ text: 'cc-by-sa' }])
})
})
Original file line number Diff line number Diff line change
@@ -3,11 +3,18 @@ import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core'
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
import { Constraint } from '@geonetwork-ui/common/domain/model/record'
import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs'
import { AVAILABLE_LICENSES } from '../../../../fields.config'

type Licence = {
label: string
value: string
}

@Component({
selector: 'gn-ui-form-field-license',
@@ -17,56 +24,37 @@ import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs'
standalone: true,
imports: [DropdownSelectorComponent],
})
export class FormFieldLicenseComponent {
export class FormFieldLicenseComponent implements OnInit {
@Input() label: string
@Input() value: Array<Constraint>
@Input() recordConstraints: Constraint[] = []
@Output() recordConstraintsChange: EventEmitter<Constraint[]> =
new EventEmitter()

@Output() valueChange: EventEmitter<Array<Constraint>> = new EventEmitter()
selectedLicence: string

get selected() {
return this.value[0]?.text
ngOnInit(): void {
// get the licence from the record constraints if it is one of the open data licence list
this.selectedLicence = this.recordConstraints.find((constraint) => {
return this.licenceOptions.find((licence) => {
return licence.value === constraint.text
})
})?.text
// otherwise pre-select the first licence option
if (this.selectedLicence === undefined) {
this.selectedLicence = this.licenceOptions[0].value // cannot select 'etalab' as default as this would toggle the OpenData Toggle
this.recordConstraintsChange.emit([{ text: this.selectedLicence }])
}
}

onSelectValue(value: unknown) {
this.valueChange.emit([{ text: value as string }])
get licenceOptions(): Licence[] {
return AVAILABLE_LICENSES.map((license) => ({
label: marker(`editor.record.form.license.${license}`),
value: license,
}))
}

choices = [
{
value: 'cc-by',
label: marker('editor.record.form.license.cc-by'),
},
{
value: 'cc-by-sa',
label: marker('editor.record.form.license.cc-by-sa'),
},
{
value: 'cc-zero',
label: marker('editor.record.form.license.cc-zero'),
},
{
value: 'etalab',
label: marker('editor.record.form.license.etalab'),
},
{
value: 'etalab-v2',
label: marker('editor.record.form.license.etalab-v2'),
},
{
value: 'odbl',
label: marker('editor.record.form.license.odbl'),
},
{
value: 'odc-by',
label: marker('editor.record.form.license.odc-by'),
},
{
value: 'pddl',
label: marker('editor.record.form.license.pddl'),
},
{
value: 'unknown',
label: marker('editor.record.form.license.unknown'),
},
]
handleLicenceSelection(licenceValue: string) {
this.selectedLicence = licenceValue
this.recordConstraintsChange.emit([{ text: licenceValue }])
}
}
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
class="w-full"
[disabled]="true"
[value]="onlineResource.url"
[showUploadButton]="false"
></gn-ui-url-input>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@
<gn-ui-url-input
class="w-full"
[urlCanParse]="true"
(valueChange)="handleServiceUrlChange($event)"
(uploadClick)="handleServiceUrlChange($event)"
></gn-ui-url-input>
</div>
<div class="h-[8px]"></div>
@@ -66,6 +66,7 @@ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
class="w-full"
[disabled]="true"
[value]="onlineResource.url"
[showUploadButton]="false"
></gn-ui-url-input>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<gn-ui-check-toggle
[label]="'editor.record.form.classification.opendata' | translate"
[value]="openData"
[value]="isOpenDataLicense"
(toggled)="onOpenDataToggled($event)"
data-cy="openDataToggle"
></gn-ui-check-toggle>
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { FormFieldOpenDataComponent } from './form-field-open-data.component'

jest.mock('./../../../../fields.config', () => {
return {
OPEN_DATA_LICENSES: ['CC-BY'],
OPEN_DATA_LICENSE: 'CC-BY',
}
})

Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import {
import { Constraint } from '@geonetwork-ui/common/domain/model/record'
import { CheckToggleComponent } from '@geonetwork-ui/ui/inputs'
import { TranslateModule } from '@ngx-translate/core'
import { OPEN_DATA_LICENSES } from './../../../../fields.config'
import { OPEN_DATA_LICENSE } from './../../../../fields.config'

@Component({
selector: 'gn-ui-form-field-open-data',
@@ -24,25 +24,28 @@ export class FormFieldOpenDataComponent implements OnChanges {
@Output() valueChange = new EventEmitter<Array<Constraint>>()
@Output() openDataChange = new EventEmitter<boolean>()

openData = false

get config() {
return OPEN_DATA_LICENSES
get isOpenDataLicense(): boolean {
return !!this.value.find(
(constraint) => constraint.text === OPEN_DATA_LICENSE
)
}

ngOnChanges() {
if (this.value && this.value.length > 0) {
this.openData = this.config.includes(this.value[0].text)
} else {
this.openData = false
}
this.openDataChange.emit(this.openData)
this.openDataChange.emit(this.isOpenDataLicense)
}

onOpenDataToggled(openData: boolean) {
this.openDataChange.emit(openData)
if (openData) {
this.valueChange.emit([{ text: this.config[0] }])
this.valueChange.emit([
{
text: OPEN_DATA_LICENSE,
},
])
} else {
this.valueChange.emit(
this.value.filter((constraint) => constraint.text !== OPEN_DATA_LICENSE)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
<gn-ui-form-field-open-data
[value]="valueAsConstraints"
(valueChange)="valueChange.emit($event)"
(openDataChange)="isHidden = $event"
(openDataChange)="toggleIsOpenData($event)"
></gn-ui-form-field-open-data>
</ng-container> -->
<div class="flex flex-col h-full">
@@ -92,10 +92,29 @@
<ng-container *ngSwitchCase="'licenses'">
<gn-ui-form-field-license
[label]="config.labelKey! | translate"
[value]="valueAsConstraints"
(valueChange)="valueChange.emit($event)"
[recordConstraints]="valueAsConstraints"
(recordConstraintsChange)="valueChange.emit($event)"
></gn-ui-form-field-license>
</ng-container>

<ng-container *ngSwitchCase="'legalConstraints'">
<ng-container *ngTemplateOutlet="formFieldConstraints"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'securityConstraints'">
<ng-container *ngTemplateOutlet="formFieldConstraints"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="'otherConstraints'">
<ng-container *ngTemplateOutlet="formFieldConstraints"></ng-container
></ng-container>
<ng-template #formFieldConstraints>
<gn-ui-form-field-constraints
[label]="config.labelKey"
[value]="valueAsConstraints"
[constraintType]="model"
(valueChange)="valueChange.emit($event)"
></gn-ui-form-field-constraints
></ng-template>

<ng-container *ngSwitchCase="'contactsForResource'">
<gn-ui-form-field-contacts-for-resource
[value]="valueAsIndividuals"
@@ -125,4 +144,12 @@
></gn-ui-form-field-online-link-resources>
</ng-container>
</ng-container>

<ng-container *ngIf="!model">
<ng-container [ngSwitch]="componentName">
<ng-container *ngSwitchCase="'form-field-constraints-shortcuts'">
<gn-ui-form-field-constraints-shortcuts></gn-ui-form-field-constraints-shortcuts>
</ng-container>
</ng-container>
</ng-container>
</ng-template>
Original file line number Diff line number Diff line change
@@ -27,7 +27,11 @@ import {
FormFieldLicenseComponent,
FormFieldTemporalExtentsComponent,
} from '.'
import { FieldModelSpecifier, FormFieldConfig } from '../../../models'
import {
FieldModelSpecifier,
FormFieldComponentName,
FormFieldConfig,
} from '../../../models'
import { FormFieldArrayComponent } from './form-field-array/form-field-array.component'
import { FormFieldContactsForResourceComponent } from './form-field-contacts-for-resource/form-field-contacts-for-resource.component'
import { FormFieldContactsComponent } from './form-field-contacts/form-field-contacts.component'
@@ -42,6 +46,8 @@ import { FormFieldRichComponent } from './form-field-rich/form-field-rich.compon
import { FormFieldSimpleComponent } from './form-field-simple/form-field-simple.component'
import { FormFieldSpatialExtentComponent } from './form-field-spatial-extent/form-field-spatial-extent.component'
import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency/form-field-update-frequency.component'
import { FormFieldConstraintsShortcutsComponent } from './form-field-constraints-shortcuts/form-field-constraints-shortcuts.component'
import { FormFieldConstraintsComponent } from './form-field-constraints/form-field-constraints.component'

@Component({
selector: 'gn-ui-form-field',
@@ -72,27 +78,41 @@ import { FormFieldUpdateFrequencyComponent } from './form-field-update-frequency
FormFieldOnlineResourcesComponent,
FormFieldOnlineLinkResourcesComponent,
FormFieldContactsComponent,
FormFieldConstraintsComponent,
FormFieldConstraintsShortcutsComponent,
],
})
export class FormFieldComponent {
@Input() uniqueIdentifier: string
@Input() model: CatalogRecordKeys
@Input() modelSpecifier: FieldModelSpecifier
@Input() componentName: FormFieldComponentName

@Input() config: FormFieldConfig
@Input() value: unknown

@Output() valueChange: EventEmitter<unknown> = new EventEmitter()

@ViewChild('titleInput') titleInput: ElementRef
isOpenData = false

isHidden = false
toggleIsOpenData(event: boolean) {
this.isOpenData = event
}

focusTitleInput() {
this.titleInput.nativeElement.children[0].focus()
}

get withoutWrapper() {
return this.model === 'title' || this.model === 'abstract'
return (
this.model === 'title' ||
this.model === 'abstract' ||
this.model === 'legalConstraints' ||
this.model === 'securityConstraints' ||
this.model === 'otherConstraints' ||
this.componentName === 'form-field-constraints-shortcuts'
)
}

get valueAsString() {
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@
[modelSpecifier]="field.config.modelSpecifier!"
[config]="field.config.formFieldConfig"
[value]="field.value"
[componentName]="field.config.componentName"
(valueChange)="
handleFieldValueChange(field.config.model!, $event)
"
52 changes: 50 additions & 2 deletions libs/feature/editor/src/lib/fields.config.ts
Original file line number Diff line number Diff line change
@@ -26,6 +26,32 @@ export const RECORD_UNIQUE_IDENTIFIER_FIELD: EditorField = {
hidden: true,
}

export const CONSTRAINTS_SHORTCUTS: EditorField = {
componentName: 'form-field-constraints-shortcuts',
formFieldConfig: {
labelKey: marker('editor.record.form.field.constraintsShortcuts'),
},
}

export const LEGAL_CONSTRAINTS_FIELD: EditorField = {
model: 'legalConstraints',
formFieldConfig: {
labelKey: marker('editor.record.form.field.legalConstraints'),
},
}
export const SECURITY_CONSTRAINTS_FIELD: EditorField = {
model: 'securityConstraints',
formFieldConfig: {
labelKey: marker('editor.record.form.field.securityConstraints'),
},
}
export const OTHER_CONSTRAINTS_FIELD: EditorField = {
model: 'otherConstraints',
formFieldConfig: {
labelKey: marker('editor.record.form.field.otherConstraints'),
},
}

export const RECORD_LICENSE_FIELD: EditorField = {
model: 'licenses',
formFieldConfig: {
@@ -191,7 +217,13 @@ export const CLASSIFICATION_SECTION: EditorSection = {
export const USE_AND_ACCESS_CONDITIONS_SECTION: EditorSection = {
labelKey: marker('editor.record.form.section.useAndAccessConditions.label'),
hidden: false,
fields: [RECORD_LICENSE_FIELD],
fields: [
RECORD_LICENSE_FIELD,
CONSTRAINTS_SHORTCUTS,
LEGAL_CONSTRAINTS_FIELD,
SECURITY_CONSTRAINTS_FIELD,
OTHER_CONSTRAINTS_FIELD,
],
}

export const DATA_MANAGERS_SECTION: EditorSection = {
@@ -240,14 +272,30 @@ export const DEFAULT_CONFIGURATION: EditorConfig = {
*************** LICENSES **************
************************************************************
*/
export const OPEN_DATA_LICENSES: string[] = [
export const AVAILABLE_LICENSES: string[] = [
'cc-by',
'cc-by-sa',
'cc-zero',
'etalab',
'etalab-v2',
'odbl',
'odc-by',
'pddl',
'unknown',
]

export const OPEN_DATA_LICENSE = 'etalab'

marker('editor.record.form.license.cc-by')
marker('editor.record.form.license.cc-by-sa')
marker('editor.record.form.license.cc-zero')
marker('editor.record.form.license.etalab')
marker('editor.record.form.license.etalab-v2')
marker('editor.record.form.license.odbl')
marker('editor.record.form.license.odc-by')
marker('editor.record.form.license.pddl')
marker('editor.record.form.license.unknown')

export const MAX_UPLOAD_SIZE_MB = 10
/************************************************************
*************** SPATIAL SCOPE ************
13 changes: 10 additions & 3 deletions libs/feature/editor/src/lib/models/editor-config.model.ts
Original file line number Diff line number Diff line change
@@ -23,14 +23,21 @@ export type FieldModelSpecifier =
| OnlineLinkResourceSpecifier
| DatasetDistributionsSpecifier

export interface EditorField {
// configuration of the form field used as presentation
formFieldConfig: FormFieldConfig
export type FormFieldComponentName = 'form-field-constraints-shortcuts'

export interface EditorFieldIdentification {
// name of the target field in the record; will not change the record directly if not defined
model?: CatalogRecordKeys
modelSpecifier?: FieldModelSpecifier

// if no model is given, a component can be shown instead
componentName?: FormFieldComponentName
}

export interface EditorField extends EditorFieldIdentification {
// configuration of the form field used as presentation
formFieldConfig: FormFieldConfig

// grid column span; if unspecified, full width will be used
gridColumnSpan?: number

Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<label class="inline-flex relative items-start cursor-pointer" [title]="title">
<label
class="inline-flex relative items-start cursor-pointer"
[title]="title || label"
>
<span class="shrink-0">
<input
type="checkbox"
6 changes: 3 additions & 3 deletions libs/ui/inputs/src/lib/check-toggle/check-toggle.component.ts
Original file line number Diff line number Diff line change
@@ -20,9 +20,9 @@ export class CheckToggleComponent {
@Input() label: string
@Input() value: boolean
@Input() color: 'primary' | 'secondary' = 'primary'
@Output() toggled = new EventEmitter()
@Output() toggled = new EventEmitter<boolean>()

toggle(event: Event) {
this.toggled.emit(event)
toggle(value: boolean) {
this.toggled.emit(value)
}
}
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@
<gn-ui-url-input
class="w-full"
[disabled]="isUploadInProgress"
(valueChange)="handleUrlChange($event)"
(uploadClick)="handleUrlChange($event)"
></gn-ui-url-input>
</label>
</div>
3 changes: 2 additions & 1 deletion libs/ui/inputs/src/lib/file-input/file-input.component.ts
Original file line number Diff line number Diff line change
@@ -89,7 +89,8 @@ export class FileInputComponent {
this.handleDropFiles(Array.from((event.target as HTMLInputElement).files))
}

handleUrlChange(url: string) {
handleUrlChange(url: string | null) {
if (!url) return
this.urlChange.emit(url)
}

Original file line number Diff line number Diff line change
@@ -140,7 +140,7 @@
<gn-ui-url-input
*ngIf="showUrlInput"
class="mt-3.5"
(valueChange)="downloadUrl($event)"
(uploadClick)="downloadUrl($event)"
[disabled]="isUploadInProgress"
>
</gn-ui-url-input>
15 changes: 6 additions & 9 deletions libs/ui/inputs/src/lib/url-input/url-input.component.html
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@
class="gn-ui-text-input px-[var(--text-padding)]"
[ngClass]="extraClass"
type="url"
[value]="value"
(input)="handleInput()"
(keydown.enter)="handleChange(input)"
[value]="inputValue"
(input)="handleInput($event)"
(keydown.enter)="handleUpload(input)"
[placeholder]="placeholder"
[attr.aria-label]="placeholder"
[disabled]="disabled"
@@ -21,14 +21,11 @@
<ng-icon name="iconoirLink"></ng-icon>
</div>
<gn-ui-button
*ngIf="showUploadButton"
extraClass="absolute inset-y-[var(--side-padding)] right-[var(--side-padding)]"
type="primary"
[disabled]="
disabled ||
input.value === '' ||
(urlCanParse && !URLcanParse(input.value))
"
(buttonClick)="handleChange(input)"
[disabled]="disabled || input.value === '' || !isValidUrl(input.value)"
(buttonClick)="handleUpload(input)"
>
<ng-icon name="iconoirArrowUp"> </ng-icon>
</gn-ui-button>
150 changes: 108 additions & 42 deletions libs/ui/inputs/src/lib/url-input/url-input.component.spec.ts
Original file line number Diff line number Diff line change
@@ -34,72 +34,138 @@ describe('UrlInputComponent', () => {
inputEl = fixture.nativeElement.querySelector('input')
button = fixture.debugElement.query(By.directive(ButtonComponent))
})
it('emits the value on a button click event', () => {
let emitted
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = 'Aaabcd'
button.triggerEventHandler('buttonClick', null)
expect(emitted).toBe('Aaabcd')
})
it('does not the value on an input event', () => {
let emitted = null
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = 'Aaabcd'
inputEl.dispatchEvent(new Event('input'))
expect(emitted).toBe(null)
})
it('emits the value on a enter press event', () => {
let emitted
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = 'Aaabcd'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emitted).toBe('Aaabcd')

it('shows an empty input if given a nullish url', () => {
component.value = null
fixture.detectChanges()
expect(inputEl.value).toEqual('')
})
it('can emit multiple equal values', () => {
let emittedCount = 0
component.valueChange.subscribe(() => emittedCount++)
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emittedCount).toBe(3)

describe('uploadClick', () => {
it('emits the value on a button click event', () => {
let emitted
component.uploadClick.subscribe((v) => (emitted = v))
inputEl.value = 'http://aaa.com/bcd'
button.triggerEventHandler('buttonClick', null)
expect(emitted).toBe('http://aaa.com/bcd')
})
it('does not emit the value on an input event', () => {
let emitted = null
component.uploadClick.subscribe((v) => (emitted = v))
inputEl.value = 'http://aaa.com/bcd'
inputEl.dispatchEvent(new Event('input'))
expect(emitted).toBe(null)
})
it('emits the value on a enter press event', () => {
let emitted
component.uploadClick.subscribe((v) => (emitted = v))
inputEl.value = 'http://aaa.com/bcd'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emitted).toBe('http://aaa.com/bcd')
})
it('can emit multiple equal values', () => {
let emittedCount = 0
component.uploadClick.subscribe(() => emittedCount++)
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emittedCount).toBe(3)
})
it('does not emit empty values', () => {
let emitted = null
component.uploadClick.subscribe((v) => (emitted = v))
inputEl.value = ''
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emitted).toBe(null)
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emitted).toBe('http://bla')
})
})
it('does not emit empty values', () => {
let emitted = null
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = ''
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emitted).toBe(null)
inputEl.value = 'http://bla'
inputEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'enter' }))
expect(emitted).toBe('http://bla')
describe('valueChange', () => {
it('does not the value on a button click event', () => {
let emitted = null
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = 'http://aaa.com/bcd'
button.triggerEventHandler('buttonClick', null)
expect(emitted).toBe(null)
})
it('emits the value on an input event', () => {
let emitted = null
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = 'http://aaa.com/bcd'
inputEl.dispatchEvent(new Event('input'))
expect(emitted).toBe('http://aaa.com/bcd')
})
it('does not emit the value if not a valid URL', () => {
let emitted = null
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = 'blargz'
inputEl.dispatchEvent(new Event('input'))
expect(emitted).toBe(null)
})
it('emits null if the input is cleared', () => {
let emitted = undefined
component.valueChange.subscribe((v) => (emitted = v))
inputEl.value = ''
inputEl.dispatchEvent(new Event('input'))
expect(emitted).toBe(null)
})
})

describe('button', () => {
it('is disabled if parent set it as disabled', () => {
component.disabled = true
inputEl.value = ''
inputEl.dispatchEvent(new Event('input'))
fixture.detectChanges()
expect(button.componentInstance.disabled).toBe(true)
})
it('is disabled if value is empty', () => {
inputEl.value = ''
inputEl.dispatchEvent(new Event('input'))
fixture.detectChanges()
expect(button.componentInstance.disabled).toBe(true)
})
it('is disabled if asking for parseable URL and value is not an URL', () => {
component.urlCanParse = true
it('is disabled if value is not an URL', () => {
inputEl.value = 'hello'
inputEl.dispatchEvent(new Event('input'))
fixture.detectChanges()
expect(button.componentInstance.disabled).toBe(true)
})
it('is not disabled otherwise', () => {
inputEl.value = 'hello'
inputEl.value = 'http://hello.org'
inputEl.dispatchEvent(new Event('input'))
fixture.detectChanges()
expect(button.componentInstance.disabled).toBeFalsy()
})
})

describe('input value', () => {
it('changes if the component input resolves to a different url', () => {
inputEl.value = 'http://aaa.com/1234'
inputEl.dispatchEvent(new Event('input'))
component.value = 'http://aaa.com/bcd'
fixture.detectChanges()
expect(inputEl.value).toEqual('http://aaa.com/bcd')
})
it('does not change if the component input is different that the current value but resolves to the same url', () => {
inputEl.value = 'http://aaa.com/1234 5678'
inputEl.dispatchEvent(new Event('input'))
component.value = 'http://aaa.com/1234%205678'
fixture.detectChanges()
expect(inputEl.value).toEqual('http://aaa.com/1234 5678')
})
it('does not change if both the component input and the current input are not valid urls', () => {
inputEl.value = 'blargz'
inputEl.dispatchEvent(new Event('input'))
component.value = undefined
fixture.detectChanges()
expect(inputEl.value).toEqual('blargz')
})
})
})
})
43 changes: 30 additions & 13 deletions libs/ui/inputs/src/lib/url-input/url-input.component.stories.ts
Original file line number Diff line number Diff line change
@@ -5,37 +5,54 @@ export default {
title: 'Inputs/UrlInputComponent',
component: UrlInputComponent,
decorators: [],
argTypes: {
valueChange: {
action: 'valueChange',
},
uploadClick: {
action: 'uploadClick',
},
},
} as Meta<UrlInputComponent>

export const Primary: StoryObj<UrlInputComponent> = {
args: {
value: '',
value: 'http://aaa.org/bbb',
placeholder: 'write a URL here',
disabled: false,
},
argTypes: {
valueChange: {
action: 'valueChange',
},
showUploadButton: true,
},
}

export const WithCustomStyle: StoryObj<UrlInputComponent> = {
args: {
value: '',
value: 'http://aaa.org/bbb',
disabled: false,
placeholder: 'https://mysite.org/file',
},
argTypes: {
valueChange: {
action: 'valueChange',
},
},
render: (args) => ({
props: args,
template: `
<div style="--gn-ui-text-input-rounded: 8px; --gn-ui-text-input-padding: 14px">
<gn-ui-url-input [value]='value' [disabled]='disabled' [placeholder]='placeholder' (valueChange)='valueChange($event)'>
<gn-ui-url-input [value]='value' [disabled]='disabled' [placeholder]='placeholder'
(valueChange)='valueChange($event)' (uploadClick)='uploadClick($event)'>
</gn-ui-url-input>
</div>`,
}),
}

export const WithoutUploadButton: StoryObj<UrlInputComponent> = {
args: {
value: null,
disabled: false,
placeholder: 'https://mysite.org/file',
showUploadButton: false,
},
render: (args) => ({
props: args,
template: `
<gn-ui-url-input [value]='value' [disabled]='disabled' [placeholder]='placeholder' [showUploadButton]='showUploadButton'
(valueChange)='valueChange($event)' (uploadClick)='uploadClick($event)'>
</gn-ui-url-input>`,
}),
}
54 changes: 36 additions & 18 deletions libs/ui/inputs/src/lib/url-input/url-input.component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core'
import { CommonModule } from '@angular/common'
import { ButtonComponent } from '../button/button.component'
import { filter } from 'rxjs/operators'
import { Subject } from 'rxjs'
import {
NgIconComponent,
provideIcons,
@@ -29,34 +27,54 @@ import { iconoirArrowUp, iconoirLink } from '@ng-icons/iconoir'
size: '1.5em',
}),
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UrlInputComponent implements OnChanges {
@Input() value = ''
export class UrlInputComponent {
@Input() set value(v: string) {
// we're making sure to only update the input if the URL representation of it has changed; otherwise we keep it identical
// to avoid glitches when starting to write a URL and having some characters added/replaced automatically
if (!v || !this.isValidUrl(v)) return
if (
this.isValidUrl(this.inputValue) &&
new URL(v).toString() === new URL(this.inputValue).toString()
)
return
this.inputValue = v
this.cd.markForCheck()
}
@Input() extraClass = ''
@Input() placeholder = 'https://'
@Input() disabled: boolean
@Input() urlCanParse?: boolean
rawChange = new Subject<string>()
@Output() valueChange = this.rawChange.pipe(filter((v) => !!v))
@Input() showUploadButton = true

/**
* This will emit null if the field is emptied
*/
@Output() valueChange = new EventEmitter<string | null>()
@Output() uploadClick = new EventEmitter<string>()

inputValue = ''

constructor(private cd: ChangeDetectorRef) {}

ngOnChanges(changes: SimpleChanges): void {
if (changes.value) {
console.log('changes.value', changes.value)
handleInput(event: Event) {
const value = (event.target as HTMLInputElement).value
this.inputValue = value
if (!value || !this.isValidUrl(value)) {
this.valueChange.next(null)
return
}
}

handleInput() {
this.cd.markForCheck()
this.valueChange.next(value)
}

handleChange(element: HTMLInputElement) {
handleUpload(element: HTMLInputElement) {
const value = element.value
this.rawChange.next(value)
if (!value || !this.isValidUrl(value)) return
this.uploadClick.next(value)
}

URLcanParse(url: string): boolean {
isValidUrl(url: string): boolean {
try {
new URL(url)
return true
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="h-full flex flex-col" *ngIf="label; else onlyContent">
<div class="flex-none w-full flex flex-row items-center">
<span class="flex-none font-bold">{{ label }}</span>
<span class="flex-none font-bold" translate="">{{ label }}</span>
<div class="flex-1 flex justify-end items-center">
<ng-content select="[form-field-interaction]"></ng-content>
<span
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MatTooltipModule } from '@angular/material/tooltip'
import { CommonModule } from '@angular/common'
import { TranslateModule } from '@ngx-translate/core'

@Component({
selector: 'gn-ui-form-field-wrapper',
templateUrl: './form-field-wrapper.component.html',
styleUrls: ['./form-field-wrapper.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatTooltipModule, CommonModule],
imports: [MatTooltipModule, CommonModule, TranslateModule],
})
export class FormFieldWrapperComponent {
@Input() label?: string
33 changes: 24 additions & 9 deletions translations/de.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "",
"editor.record.form.bottomButtons.previous": "",
"editor.record.form.classification.opendata": "",
"editor.record.form.constraint.add.legalConstraints": "Rechtliche Einschränkung hinzufügen",
"editor.record.form.constraint.add.otherConstraints": "Einschränkung hinzufügen",
"editor.record.form.constraint.add.securityConstraints": "Sicherheitseinschränkung hinzufügen",
"editor.record.form.constraint.header.legalConstraints": "Anwendbare rechtliche Bedingungen",
"editor.record.form.constraint.header.otherConstraints": "Andere anwendbare Bedingungen",
"editor.record.form.constraint.header.securityConstraints": "Anwendbare Sicherheitsbedingungen",
"editor.record.form.constraint.legalConstraints": "Rechtliche Einschränkungen",
"editor.record.form.constraint.not.applicable": "Keine Bedingungen gelten.",
"editor.record.form.constraint.not.known": "Die Bedingungen sind unbekannt.",
"editor.record.form.constraint.otherConstraints": "",
"editor.record.form.constraint.securityConstraints": "",
"editor.record.form.field.abstract": "Kurzbeschreibung",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "",
"editor.record.form.field.contactsForResource.noContact": "",
"editor.record.form.field.keywords": "Schlagwörter",
"editor.record.form.field.legalConstraints": "Rechtliche Einschränkung",
"editor.record.form.field.license": "Lizenz",
"editor.record.form.field.onlineLinkResources": "",
"editor.record.form.field.onlineResource.cancel": "",
@@ -223,23 +236,25 @@
"editor.record.form.field.onlineResource.toggle.dataset": "",
"editor.record.form.field.onlineResource.toggle.service": "",
"editor.record.form.field.onlineResources": "",
"editor.record.form.field.otherConstraints": "Allgemeine Einschränkung",
"editor.record.form.field.overviews": "",
"editor.record.form.field.recordUpdated": "Datensatz zuletzt aktualisiert",
"editor.record.form.field.resourceUpdated": "Letztes Aktualisierungsdatum",
"editor.record.form.field.securityConstraints": "Sicherheitseinschränkung",
"editor.record.form.field.spatialExtents": "",
"editor.record.form.field.temporalExtents": "Zeitlicher Umfang",
"editor.record.form.field.title": "Metadaten-Titel",
"editor.record.form.field.uniqueIdentifier": "Eindeutige Kennung (ID)",
"editor.record.form.field.updateFrequency": "Aktualisierungshäufigkeit",
"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.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.unknown": "",
"editor.record.form.page.accessAndContact": "",
"editor.record.form.page.description": "",
"editor.record.form.page.ressources": "",
15 changes: 15 additions & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "Next",
"editor.record.form.bottomButtons.previous": "Previous",
"editor.record.form.classification.opendata": "Open Data",
"editor.record.form.constraint.add.legalConstraints": "Add a legal constraint",
"editor.record.form.constraint.add.otherConstraints": "Add a constraint",
"editor.record.form.constraint.add.securityConstraints": "Add a security constraint",
"editor.record.form.constraint.header.legalConstraints": "Applicable legal conditions",
"editor.record.form.constraint.header.otherConstraints": "Other applicable conditions",
"editor.record.form.constraint.header.securityConstraints": "Applicable security conditions",
"editor.record.form.constraint.legalConstraints": "Legal constraints",
"editor.record.form.constraint.not.applicable": "No conditions apply.",
"editor.record.form.constraint.not.known": "The conditions are unknown.",
"editor.record.form.constraint.otherConstraints": "Other constraints",
"editor.record.form.constraint.securityConstraints": "Security constraints",
"editor.record.form.field.abstract": "Abstract",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "Please provide at least one point of contact.",
"editor.record.form.field.contactsForResource.noContact": "Please provide at least one point of contact responsible for the data.",
"editor.record.form.field.keywords": "Keywords",
"editor.record.form.field.legalConstraints": "Legal constraint",
"editor.record.form.field.license": "License",
"editor.record.form.field.onlineLinkResources": "Attached resources",
"editor.record.form.field.onlineResource.cancel": "Cancel",
@@ -223,9 +236,11 @@
"editor.record.form.field.onlineResource.toggle.dataset": "Link to a dataset",
"editor.record.form.field.onlineResource.toggle.service": "Link to a service",
"editor.record.form.field.onlineResources": "Distribution",
"editor.record.form.field.otherConstraints": "Other constraint",
"editor.record.form.field.overviews": "Overviews",
"editor.record.form.field.recordUpdated": "Record Updated",
"editor.record.form.field.resourceUpdated": "Resource Updated",
"editor.record.form.field.securityConstraints": "Security constraint",
"editor.record.form.field.spatialExtents": "Spatial extents",
"editor.record.form.field.temporalExtents": "Temporal extents",
"editor.record.form.field.title": "Metadata title",
15 changes: 15 additions & 0 deletions translations/es.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "",
"editor.record.form.bottomButtons.previous": "",
"editor.record.form.classification.opendata": "",
"editor.record.form.constraint.add.legalConstraints": "",
"editor.record.form.constraint.add.otherConstraints": "",
"editor.record.form.constraint.add.securityConstraints": "",
"editor.record.form.constraint.header.legalConstraints": "",
"editor.record.form.constraint.header.otherConstraints": "",
"editor.record.form.constraint.header.securityConstraints": "",
"editor.record.form.constraint.legalConstraints": "",
"editor.record.form.constraint.not.applicable": "",
"editor.record.form.constraint.not.known": "",
"editor.record.form.constraint.otherConstraints": "",
"editor.record.form.constraint.securityConstraints": "",
"editor.record.form.field.abstract": "",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "",
"editor.record.form.field.contactsForResource.noContact": "",
"editor.record.form.field.keywords": "",
"editor.record.form.field.legalConstraints": "",
"editor.record.form.field.license": "",
"editor.record.form.field.onlineLinkResources": "",
"editor.record.form.field.onlineResource.cancel": "",
@@ -223,9 +236,11 @@
"editor.record.form.field.onlineResource.toggle.dataset": "",
"editor.record.form.field.onlineResource.toggle.service": "",
"editor.record.form.field.onlineResources": "",
"editor.record.form.field.otherConstraints": "",
"editor.record.form.field.overviews": "",
"editor.record.form.field.recordUpdated": "",
"editor.record.form.field.resourceUpdated": "",
"editor.record.form.field.securityConstraints": "",
"editor.record.form.field.spatialExtents": "",
"editor.record.form.field.temporalExtents": "",
"editor.record.form.field.title": "",
17 changes: 16 additions & 1 deletion translations/fr.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "Suivant",
"editor.record.form.bottomButtons.previous": "Précédent",
"editor.record.form.classification.opendata": "Données ouvertes",
"editor.record.form.constraint.add.legalConstraints": "Ajouter une contrainte légale",
"editor.record.form.constraint.add.otherConstraints": "Ajouter une contrainte",
"editor.record.form.constraint.add.securityConstraints": "Ajouter une contrainte de sécurité",
"editor.record.form.constraint.header.legalConstraints": "Conditions applicables de type légal",
"editor.record.form.constraint.header.otherConstraints": "Autres conditions applicables",
"editor.record.form.constraint.header.securityConstraints": "Conditions applicables de type sécurité",
"editor.record.form.constraint.legalConstraints": "Contraintes légales",
"editor.record.form.constraint.not.applicable": "Aucune condition ne s’applique.",
"editor.record.form.constraint.not.known": "Les conditions sont inconnues.",
"editor.record.form.constraint.otherConstraints": "Autres contraintes",
"editor.record.form.constraint.securityConstraints": "Contraintes de sécurité",
"editor.record.form.field.abstract": "Résumé",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "Veuillez renseigner au moins un point de contact.",
"editor.record.form.field.contactsForResource.noContact": "Veuillez renseigner au moins un point de contact responsable de la donnée.",
"editor.record.form.field.keywords": "Mots-clés",
"editor.record.form.field.legalConstraints": "Contrainte légale",
"editor.record.form.field.license": "Licence",
"editor.record.form.field.onlineLinkResources": "Annexes",
"editor.record.form.field.onlineResource.cancel": "Annuler",
@@ -223,9 +236,11 @@
"editor.record.form.field.onlineResource.toggle.dataset": "Lier un jeu de données",
"editor.record.form.field.onlineResource.toggle.service": "Lier un service",
"editor.record.form.field.onlineResources": "Distribution",
"editor.record.form.field.otherConstraints": "Contrainte générale",
"editor.record.form.field.overviews": "Aperçus",
"editor.record.form.field.recordUpdated": "Date de dernière révision",
"editor.record.form.field.resourceUpdated": "Date de dernière révision",
"editor.record.form.field.securityConstraints": "Contrainte de sécurité",
"editor.record.form.field.spatialExtents": "Étendue spatiale",
"editor.record.form.field.temporalExtents": "Étendue temporelle",
"editor.record.form.field.title": "Titre",
@@ -239,7 +254,7 @@
"editor.record.form.license.odbl": "",
"editor.record.form.license.odc-by": "",
"editor.record.form.license.pddl": "",
"editor.record.form.license.unknown": "Non reconnue ou absente",
"editor.record.form.license.unknown": "",
"editor.record.form.page.accessAndContact": "Accès et contact",
"editor.record.form.page.description": "Description de la ressource",
"editor.record.form.page.ressources": "Ressources",
21 changes: 18 additions & 3 deletions translations/it.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "",
"editor.record.form.bottomButtons.previous": "",
"editor.record.form.classification.opendata": "",
"editor.record.form.constraint.add.legalConstraints": "",
"editor.record.form.constraint.add.otherConstraints": "",
"editor.record.form.constraint.add.securityConstraints": "",
"editor.record.form.constraint.header.legalConstraints": "",
"editor.record.form.constraint.header.otherConstraints": "",
"editor.record.form.constraint.header.securityConstraints": "",
"editor.record.form.constraint.legalConstraints": "",
"editor.record.form.constraint.not.applicable": "",
"editor.record.form.constraint.not.known": "",
"editor.record.form.constraint.otherConstraints": "",
"editor.record.form.constraint.securityConstraints": "",
"editor.record.form.field.abstract": "",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "",
"editor.record.form.field.contactsForResource.noContact": "",
"editor.record.form.field.keywords": "",
"editor.record.form.field.legalConstraints": "",
"editor.record.form.field.license": "Licenza",
"editor.record.form.field.onlineLinkResources": "",
"editor.record.form.field.onlineResource.cancel": "",
@@ -223,9 +236,11 @@
"editor.record.form.field.onlineResource.toggle.dataset": "",
"editor.record.form.field.onlineResource.toggle.service": "",
"editor.record.form.field.onlineResources": "",
"editor.record.form.field.otherConstraints": "",
"editor.record.form.field.overviews": "",
"editor.record.form.field.recordUpdated": "",
"editor.record.form.field.resourceUpdated": "",
"editor.record.form.field.securityConstraints": "",
"editor.record.form.field.spatialExtents": "",
"editor.record.form.field.temporalExtents": "",
"editor.record.form.field.title": "",
@@ -234,12 +249,12 @@
"editor.record.form.license.cc-by": "",
"editor.record.form.license.cc-by-sa": "",
"editor.record.form.license.cc-zero": "",
"editor.record.form.license.etalab": "Licenza aperta (Etalab)",
"editor.record.form.license.etalab-v2": "Licenza aperta v2.0 (Etalab)",
"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.unknown": "Non riconosciuta o assente",
"editor.record.form.license.unknown": "",
"editor.record.form.page.accessAndContact": "",
"editor.record.form.page.description": "",
"editor.record.form.page.ressources": "",
15 changes: 15 additions & 0 deletions translations/nl.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "",
"editor.record.form.bottomButtons.previous": "",
"editor.record.form.classification.opendata": "",
"editor.record.form.constraint.add.legalConstraints": "",
"editor.record.form.constraint.add.otherConstraints": "",
"editor.record.form.constraint.add.securityConstraints": "",
"editor.record.form.constraint.header.legalConstraints": "",
"editor.record.form.constraint.header.otherConstraints": "",
"editor.record.form.constraint.header.securityConstraints": "",
"editor.record.form.constraint.legalConstraints": "",
"editor.record.form.constraint.not.applicable": "",
"editor.record.form.constraint.not.known": "",
"editor.record.form.constraint.otherConstraints": "",
"editor.record.form.constraint.securityConstraints": "",
"editor.record.form.field.abstract": "",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "",
"editor.record.form.field.contactsForResource.noContact": "",
"editor.record.form.field.keywords": "",
"editor.record.form.field.legalConstraints": "",
"editor.record.form.field.license": "",
"editor.record.form.field.onlineLinkResources": "",
"editor.record.form.field.onlineResource.cancel": "",
@@ -223,9 +236,11 @@
"editor.record.form.field.onlineResource.toggle.dataset": "",
"editor.record.form.field.onlineResource.toggle.service": "",
"editor.record.form.field.onlineResources": "",
"editor.record.form.field.otherConstraints": "",
"editor.record.form.field.overviews": "",
"editor.record.form.field.recordUpdated": "",
"editor.record.form.field.resourceUpdated": "",
"editor.record.form.field.securityConstraints": "",
"editor.record.form.field.spatialExtents": "",
"editor.record.form.field.temporalExtents": "",
"editor.record.form.field.title": "",
15 changes: 15 additions & 0 deletions translations/pt.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "",
"editor.record.form.bottomButtons.previous": "",
"editor.record.form.classification.opendata": "",
"editor.record.form.constraint.add.legalConstraints": "",
"editor.record.form.constraint.add.otherConstraints": "",
"editor.record.form.constraint.add.securityConstraints": "",
"editor.record.form.constraint.header.legalConstraints": "",
"editor.record.form.constraint.header.otherConstraints": "",
"editor.record.form.constraint.header.securityConstraints": "",
"editor.record.form.constraint.legalConstraints": "",
"editor.record.form.constraint.not.applicable": "",
"editor.record.form.constraint.not.known": "",
"editor.record.form.constraint.otherConstraints": "",
"editor.record.form.constraint.securityConstraints": "",
"editor.record.form.field.abstract": "",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "",
"editor.record.form.field.contactsForResource.noContact": "",
"editor.record.form.field.keywords": "",
"editor.record.form.field.legalConstraints": "",
"editor.record.form.field.license": "",
"editor.record.form.field.onlineLinkResources": "",
"editor.record.form.field.onlineResource.cancel": "",
@@ -223,9 +236,11 @@
"editor.record.form.field.onlineResource.toggle.dataset": "",
"editor.record.form.field.onlineResource.toggle.service": "",
"editor.record.form.field.onlineResources": "",
"editor.record.form.field.otherConstraints": "",
"editor.record.form.field.overviews": "",
"editor.record.form.field.recordUpdated": "",
"editor.record.form.field.resourceUpdated": "",
"editor.record.form.field.securityConstraints": "",
"editor.record.form.field.spatialExtents": "",
"editor.record.form.field.temporalExtents": "",
"editor.record.form.field.title": "",
17 changes: 16 additions & 1 deletion translations/sk.json
Original file line number Diff line number Diff line change
@@ -206,10 +206,23 @@
"editor.record.form.bottomButtons.next": "",
"editor.record.form.bottomButtons.previous": "",
"editor.record.form.classification.opendata": "",
"editor.record.form.constraint.add.legalConstraints": "",
"editor.record.form.constraint.add.otherConstraints": "",
"editor.record.form.constraint.add.securityConstraints": "",
"editor.record.form.constraint.header.legalConstraints": "",
"editor.record.form.constraint.header.otherConstraints": "",
"editor.record.form.constraint.header.securityConstraints": "",
"editor.record.form.constraint.legalConstraints": "",
"editor.record.form.constraint.not.applicable": "",
"editor.record.form.constraint.not.known": "",
"editor.record.form.constraint.otherConstraints": "",
"editor.record.form.constraint.securityConstraints": "",
"editor.record.form.field.abstract": "",
"editor.record.form.field.constraintsShortcuts": "",
"editor.record.form.field.contacts.noContact": "",
"editor.record.form.field.contactsForResource.noContact": "",
"editor.record.form.field.keywords": "",
"editor.record.form.field.legalConstraints": "",
"editor.record.form.field.license": "Licencia",
"editor.record.form.field.onlineLinkResources": "",
"editor.record.form.field.onlineResource.cancel": "",
@@ -223,9 +236,11 @@
"editor.record.form.field.onlineResource.toggle.dataset": "",
"editor.record.form.field.onlineResource.toggle.service": "",
"editor.record.form.field.onlineResources": "",
"editor.record.form.field.otherConstraints": "",
"editor.record.form.field.overviews": "",
"editor.record.form.field.recordUpdated": "",
"editor.record.form.field.resourceUpdated": "",
"editor.record.form.field.securityConstraints": "",
"editor.record.form.field.spatialExtents": "",
"editor.record.form.field.temporalExtents": "",
"editor.record.form.field.title": "",
@@ -239,7 +254,7 @@
"editor.record.form.license.odbl": "",
"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.license.unknown": "",
"editor.record.form.page.accessAndContact": "",
"editor.record.form.page.description": "",
"editor.record.form.page.ressources": "",

Unchanged files with check annotations Beta

import {
CatalogRecord,
DatasetRecord,

Check warning on line 3 in libs/api/metadata-converter/src/lib/base.converter.ts

GitHub Actions / Format check, lint, unit tests

'DatasetRecord' is defined but never used
} from '@geonetwork-ui/common/domain/model/record'
export class MetadataMapperContext {
)
}
writeRecord(record: CatalogRecord): Promise<Gn4Record> {

Check warning on line 53 in libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts

GitHub Actions / Format check, lint, unit tests

'record' is defined but never used
throw new Error('not implemented')
}
}
runtime_mappings?: Record<string, unknown>
}
export type EsSearchResponse = any

Check warning on line 36 in libs/api/metadata-converter/src/lib/gn4/types/elasticsearch.model.ts

GitHub Actions / Format check, lint, unit tests

Unexpected any. Specify a different type
export type EsTemplateValues = 'y' | 'n' | 's' | 't'
export type EsTemplateType = EsTemplateValues | EsTemplateValues[]
export interface SearchFilters {
any?: string
[x: string]: any

Check warning on line 5 in libs/api/metadata-converter/src/lib/gn4/types/search.model.ts

GitHub Actions / Format check, lint, unit tests

Unexpected any. Specify a different type
}
type SearchFiltersFieldsLeaf = Record<string, boolean>

Check warning on line 8 in libs/api/metadata-converter/src/lib/gn4/types/search.model.ts

GitHub Actions / Format check, lint, unit tests

'SearchFiltersFieldsLeaf' is defined but never used
export interface MetadataContact {
name?: string
await converter.writeRecord(GENERIC_DATASET_RECORD)
)
// unsupported fields need to be filtered out
const { recordPublished, recordCreated, ...withoutDates } =

Check warning on line 94 in libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.spec.ts

GitHub Actions / Format check, lint, unit tests

'recordPublished' is assigned a value but never used

Check warning on line 94 in libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.spec.ts

GitHub Actions / Format check, lint, unit tests

'recordCreated' is assigned a value but never used
GENERIC_DATASET_RECORD
expect(backAndForth).toStrictEqual({
...withoutDates,
translations: () => undefined,
}
protected beforeDocumentCreation(rootElement: XmlElement) {

Check warning on line 156 in libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts

GitHub Actions / Format check, lint, unit tests

'rootElement' is defined but never used
// to override
}