geonetwork-ui 2.10.0-dev.e97ea6950 → 2.10.0-dev.fb2893f44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1622 -6598
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +196 -87
  4. package/index.d.ts.map +1 -1
  5. package/material-styles.css +483 -0
  6. package/package.json +25 -35
  7. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +9 -0
  8. package/src/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts +2 -0
  9. package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +1 -0
  10. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+ongules.ts +7 -0
  11. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+roilaye.ts +1 -0
  12. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts +1 -0
  13. package/src/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts +1 -0
  14. package/src/libs/api/metadata-converter/src/lib/fixtures/georhena.records.ts +1 -0
  15. package/src/libs/api/metadata-converter/src/lib/fixtures/metadata-for-i18n.records.ts +2 -0
  16. package/src/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +1 -0
  17. package/src/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts +1 -0
  18. package/src/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts +2 -0
  19. package/src/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts +1 -0
  20. package/src/libs/api/metadata-converter/src/lib/fixtures/wallonie.records.reuse.ts +8 -0
  21. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts +1 -0
  22. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +2 -2
  23. package/src/libs/api/metadata-converter/src/lib/gn4/metadata-url.service.ts +2 -2
  24. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +7 -0
  25. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +8 -0
  26. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +8 -0
  27. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +11 -0
  28. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +33 -0
  29. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +33 -0
  30. package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +8 -4
  31. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +2 -2
  32. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +68 -34
  33. package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
  34. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts +1 -1
  35. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts +1 -1
  36. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +48 -2
  37. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -5
  38. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  39. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  40. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  41. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +5 -0
  42. package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -2
  43. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +6 -0
  44. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +0 -1
  45. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +10 -1
  46. package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts +0 -2
  47. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  48. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  49. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  50. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.html +18 -3
  51. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.ts +33 -40
  52. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +41 -0
  53. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +32 -34
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +13 -13
  55. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +18 -4
  56. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html +8 -7
  57. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts +6 -6
  58. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.css +0 -0
  59. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.html +5 -0
  60. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.ts +89 -0
  61. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  62. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +3 -0
  63. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
  64. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -0
  65. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  66. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +35 -0
  67. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +5 -1
  68. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +77 -5
  69. package/src/libs/feature/editor/src/lib/fields.config.ts +60 -47
  70. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
  71. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  72. package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
  73. package/src/libs/feature/notify-reuse/src/index.ts +1 -0
  74. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css +0 -0
  75. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +1 -0
  76. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +21 -0
  77. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
  78. package/src/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +2 -2
  79. package/src/libs/ui/dataviz/src/index.ts +0 -1
  80. package/src/libs/ui/elements/src/index.ts +2 -0
  81. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -0
  82. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  83. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +41 -0
  84. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
  85. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.css +13 -0
  86. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +13 -3
  87. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +43 -6
  88. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
  89. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
  90. package/src/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts +2 -2
  91. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  92. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  93. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +28 -67
  94. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  95. package/src/libs/ui/inputs/src/lib/button/button.component.ts +8 -0
  96. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +2 -2
  97. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  98. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  99. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -1
  100. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +15 -64
  101. package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
  102. package/src/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.ts +1 -1
  103. package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
  104. package/src/libs/util/app-config/src/lib/app-config.ts +70 -9
  105. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  106. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  107. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +1 -2
  108. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +6 -3
  109. package/src/libs/util/i18n/src/index.ts +0 -1
  110. package/src/libs/util/i18n/src/lib/i18n.constants.ts +17 -11
  111. package/src/libs/util/i18n/src/lib/i18n.interceptor.ts +2 -2
  112. package/src/libs/util/i18n/src/lib/i18n.providers.ts +14 -17
  113. package/src/libs/util/i18n/src/lib/test.translate.loader.ts +48 -0
  114. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  115. package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
  116. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  117. package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
  118. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  119. package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
  120. package/style.css +0 -1
  121. package/tailwind.base.css +19 -2
  122. package/translations/de.json +29 -20
  123. package/translations/en.json +29 -20
  124. package/translations/es.json +29 -20
  125. package/translations/fr.json +29 -20
  126. package/translations/it.json +29 -20
  127. package/translations/nl.json +29 -20
  128. package/translations/pt.json +29 -20
  129. package/translations/sk.json +29 -20
@@ -2,7 +2,7 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  Component,
4
4
  OnDestroy,
5
- OnInit,
5
+ afterNextRender,
6
6
  inject,
7
7
  } from '@angular/core'
8
8
  import { CommonModule } from '@angular/common'
@@ -67,9 +67,7 @@ export type ConstraintChoice =
67
67
  styleUrls: ['./form-field-constraints-shortcuts.component.css'],
68
68
  changeDetection: ChangeDetectionStrategy.OnPush,
69
69
  })
70
- export class FormFieldConstraintsShortcutsComponent
71
- implements OnInit, OnDestroy
72
- {
70
+ export class FormFieldConstraintsShortcutsComponent implements OnDestroy {
73
71
  private editorFacade = inject(EditorFacade)
74
72
 
75
73
  legalConstraints$ = this.editorFacade.record$.pipe(
@@ -112,37 +110,37 @@ export class FormFieldConstraintsShortcutsComponent
112
110
 
113
111
  onDestroy$ = new Subject<void>()
114
112
 
115
- ngOnInit(): void {
116
- // hide all constraints if any toggle is activated
117
- this.anyToggleActivated$
118
- .pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
119
- .subscribe((anyToggleActivated) => {
120
- if (anyToggleActivated) {
121
- this.hideAllConstraintSections()
122
- }
123
- })
113
+ constructor() {
114
+ // Deferred to afterNextRender to avoid dispatching store actions
115
+ // synchronously during Angular's change detection cycle (NG0100)
116
+ afterNextRender(() => {
117
+ this.anyToggleActivated$
118
+ .pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
119
+ .subscribe((anyToggleActivated) => {
120
+ if (anyToggleActivated) {
121
+ this.hideAllConstraintSections()
122
+ }
123
+ })
124
124
 
125
- // also hide constraints which are empty arrays
126
- const hideEmptyConstraints = (
127
- constraints$: Observable<Constraint[]>,
128
- model: ConstraintChoice
129
- ) => {
130
- const isConstraintNotEmpty$ = constraints$.pipe(
131
- takeUntil(this.onDestroy$),
132
- map((c) => c.length > 0),
133
- distinctUntilChanged()
134
- )
135
- combineLatest([
136
- isConstraintNotEmpty$,
137
- this.anyToggleActivated$,
138
- ]).subscribe(([isNotEmpty, anyToggleActivated]) => {
139
- const visible = isNotEmpty && !anyToggleActivated
140
- this.editorFacade.setFieldVisibility({ model }, visible)
141
- })
142
- }
143
- hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
144
- hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
145
- hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
125
+ const hideEmptyConstraints = (
126
+ constraints$: Observable<Constraint[]>,
127
+ model: ConstraintChoice
128
+ ) => {
129
+ const isConstraintNotEmpty$ = constraints$.pipe(
130
+ takeUntil(this.onDestroy$),
131
+ map((c) => c.length > 0),
132
+ distinctUntilChanged()
133
+ )
134
+ combineLatest([isConstraintNotEmpty$, this.anyToggleActivated$])
135
+ .pipe(takeUntil(this.onDestroy$))
136
+ .subscribe(([isNotEmpty, anyToggleActivated]) => {
137
+ const visible = isNotEmpty && !anyToggleActivated
138
+ this.editorFacade.setFieldVisibility({ model }, visible)
139
+ })
140
+ }
141
+ hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
142
+ hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
143
+ })
146
144
  }
147
145
 
148
146
  ngOnDestroy() {
@@ -10,21 +10,21 @@
10
10
  </gn-ui-autocomplete>
11
11
 
12
12
  @if (contacts.length > 0) {
13
- @if (contacts.length === 1) {
14
- @for (contact of contacts; track contact; let index = $index) {
13
+ <gn-ui-sortable-list
14
+ [items]="contacts"
15
+ (itemsOrderChange)="handleContactsChanged($event)"
16
+ [elementTemplate]="contactTemplate"
17
+ ></gn-ui-sortable-list>
18
+ <ng-template #contactTemplate let-contact let-index="index">
19
+ @if (modelSpecifier === 'contact:editableDetails') {
20
+ <gn-ui-contact-details-form
21
+ [contact]="contact"
22
+ (contactChange)="handleContactChanged($event, index)"
23
+ ></gn-ui-contact-details-form>
24
+ } @else {
15
25
  <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
16
26
  }
17
- }
18
- @if (contacts.length > 1) {
19
- <gn-ui-sortable-list
20
- [items]="contacts"
21
- (itemsOrderChange)="handleContactsChanged($event)"
22
- [elementTemplate]="contactTemplate"
23
- ></gn-ui-sortable-list>
24
- <ng-template #contactTemplate let-contact>
25
- <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
26
- </ng-template>
27
- }
27
+ </ng-template>
28
28
  } @else {
29
29
  <div
30
30
  class="p-4 text-sm border border-primary bg-primary-lightest rounded-lg"
@@ -28,9 +28,15 @@ import { UserModel } from '../../../../../../../../../libs/common/domain/src/lib
28
28
  import { PlatformServiceInterface } from '../../../../../../../../../libs/common/domain/src/lib/platform.service.interface'
29
29
  import { OrganizationsServiceInterface } from '../../../../../../../../../libs/common/domain/src/lib/organizations.service.interface'
30
30
  import { ContactCardComponent } from '../../../contact-card/contact-card.component'
31
- import { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
31
+ import { ContactDetailsFormComponent } from '../../../contact-details/contact-details-form.component'
32
+ import {
33
+ createFuzzyFilter,
34
+ getIndividualDisplayName,
35
+ toIndividual,
36
+ } from '../../../../../../../../../libs/util/shared/src'
32
37
  import { map } from 'rxjs/operators'
33
38
  import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
39
+ import { FieldModelSpecifier } from '../../../../models/editor-config.model'
34
40
 
35
41
  @Component({
36
42
  selector: 'gn-ui-form-field-contacts',
@@ -43,6 +49,7 @@ import { SortableListComponent } from '../../../../../../../../../libs/ui/layout
43
49
  TranslateDirective,
44
50
  TranslatePipe,
45
51
  ContactCardComponent,
52
+ ContactDetailsFormComponent,
46
53
  SortableListComponent,
47
54
  ],
48
55
  })
@@ -52,6 +59,7 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
52
59
  private changeDetectorRef = inject(ChangeDetectorRef)
53
60
 
54
61
  @Input() value: Individual[]
62
+ @Input() modelSpecifier: FieldModelSpecifier
55
63
  @Output() valueChange: EventEmitter<Individual[]> = new EventEmitter()
56
64
 
57
65
  contacts: Individual[] = []
@@ -113,13 +121,19 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
113
121
  this.valueChange.emit(contacts)
114
122
  }
115
123
 
124
+ handleContactChanged(updatedContact: Individual, index: number) {
125
+ const contacts = this.contacts.map((contact, i) =>
126
+ i === index ? updatedContact : contact
127
+ )
128
+
129
+ this.handleContactsChanged(contacts)
130
+ }
131
+
116
132
  /**
117
133
  * gn-ui-autocomplete
118
134
  */
119
135
  displayWithFn: (user: UserModel) => string = (user) =>
120
- `${user.name} ${user.surname} ${
121
- user.organisation ? `(${user.organisation})` : ''
122
- }`
136
+ getIndividualDisplayName(toIndividual(user))
123
137
 
124
138
  /**
125
139
  * gn-ui-autocomplete
@@ -8,6 +8,14 @@
8
8
  </gn-ui-button>
9
9
  }
10
10
  </div>
11
+ @if (value.length === 0) {
12
+ <div
13
+ class="p-4 border border-primary bg-primary-lightest rounded-lg"
14
+ translate
15
+ >
16
+ editor.record.form.field.contactsForResource.noContact
17
+ </div>
18
+ }
11
19
  @if (roleSectionsToDisplay && roleSectionsToDisplay.length > 0) {
12
20
  <div class="mt-8" data-test="displayedRoles">
13
21
  @for (
@@ -53,12 +61,5 @@
53
61
  </div>
54
62
  }
55
63
  </div>
56
- } @else {
57
- <div
58
- class="p-4 border border-primary bg-primary-lightest rounded-lg"
59
- translate
60
- >
61
- editor.record.form.field.contactsForResource.noContact
62
- </div>
63
64
  }
64
65
  </div>
@@ -23,7 +23,11 @@ import {
23
23
  AutocompleteComponent,
24
24
  ButtonComponent,
25
25
  } from '../../../../../../../../../libs/ui/inputs/src'
26
- import { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
26
+ import {
27
+ createFuzzyFilter,
28
+ getIndividualDisplayName,
29
+ toIndividual,
30
+ } from '../../../../../../../../../libs/util/shared/src'
27
31
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
28
32
  import {
29
33
  debounceTime,
@@ -169,11 +173,7 @@ export class FormFieldContactsForResourceComponent
169
173
  * gn-ui-autocomplete
170
174
  */
171
175
  displayWithFn: (user: UserModel) => string = (user) =>
172
- user.name
173
- ? `${user.name} ${user.surname} ${
174
- user.organisation ? `(${user.organisation})` : ''
175
- }`
176
- : ``
176
+ getIndividualDisplayName(toIndividual(user))
177
177
 
178
178
  /**
179
179
  * gn-ui-autocomplete
@@ -0,0 +1,5 @@
1
+ <gn-ui-url-input
2
+ [value]="displayUrl"
3
+ [showValidateButton]="false"
4
+ (valueChange)="handleUrlChange($event)"
5
+ ></gn-ui-url-input>
@@ -0,0 +1,89 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ Input,
6
+ Output,
7
+ inject,
8
+ } from '@angular/core'
9
+ import { marker } from '@biesbjerg/ngx-translate-extract-marker'
10
+ import {
11
+ OnlineLinkResource,
12
+ OnlineResource,
13
+ } from '../../../../../../../../../libs/common/domain/src/lib/model/record'
14
+ import { NotificationsService } from '../../../../../../../../../libs/feature/notifications/src'
15
+ import { UrlInputComponent } from '../../../../../../../../../libs/ui/inputs/src'
16
+ import { TranslateService } from '@ngx-translate/core'
17
+
18
+ marker('editor.record.form.field.onlineLinkageResource.defaultName')
19
+
20
+ @Component({
21
+ selector: 'gn-ui-form-field-online-single-link-resource',
22
+ templateUrl: './form-field-online-single-link-resource.component.html',
23
+ styleUrls: ['./form-field-online-single-link-resource.component.css'],
24
+ changeDetection: ChangeDetectionStrategy.OnPush,
25
+ standalone: true,
26
+ imports: [UrlInputComponent],
27
+ })
28
+ export class FormFieldOnlineSingleLinkResourceComponent {
29
+ private notificationsService = inject(NotificationsService)
30
+ private translateService = inject(TranslateService)
31
+
32
+ @Input() set value(onlineResources: Array<OnlineResource>) {
33
+ this.allResources = onlineResources ?? []
34
+ const firstResource = this.allResources[0]
35
+ this.displayUrl = firstResource?.url?.toString() ?? ''
36
+ }
37
+ @Output() valueChange: EventEmitter<Array<OnlineResource>> =
38
+ new EventEmitter()
39
+
40
+ private allResources: OnlineResource[] = []
41
+ displayUrl = ''
42
+
43
+ handleUrlChange(url: string | null) {
44
+ if (!url) return
45
+
46
+ try {
47
+ const parsedUrl = new URL(url)
48
+
49
+ if (this.allResources.length === 0) {
50
+ const defaultName = this.translateService.instant(
51
+ 'editor.record.form.field.onlineLinkageResource.defaultName'
52
+ )
53
+ const newResource: OnlineLinkResource = {
54
+ type: 'link',
55
+ url: parsedUrl,
56
+ name: defaultName,
57
+ }
58
+ this.valueChange.emit([newResource])
59
+ } else {
60
+ const updatedFirst: OnlineResource = {
61
+ ...this.allResources[0],
62
+ url: parsedUrl,
63
+ }
64
+ this.valueChange.emit([updatedFirst, ...this.allResources.slice(1)])
65
+ }
66
+ } catch (error) {
67
+ this.handleError(error as Error)
68
+ }
69
+ }
70
+
71
+ private handleError(error: Error) {
72
+ this.notificationsService.showNotification(
73
+ {
74
+ type: 'error',
75
+ title: this.translateService.instant(
76
+ 'editor.record.onlineResourceError.title'
77
+ ),
78
+ text: `${this.translateService.instant(
79
+ 'editor.record.onlineResourceError.body'
80
+ )} ${error.message}`,
81
+ closeMessage: this.translateService.instant(
82
+ 'editor.record.onlineResourceError.closeMessage'
83
+ ),
84
+ },
85
+ undefined,
86
+ error
87
+ )
88
+ }
89
+ }
@@ -5,7 +5,7 @@ import {
5
5
  DropdownMultiselectComponent,
6
6
  } from '../../../../../../../../../libs/ui/inputs/src'
7
7
  import { TranslatePipe, TranslateService } from '@ngx-translate/core'
8
- import { INSPIRE_TOPICS } from '../../../../fields.config'
8
+ import { ISO_TOPICS } from '../../../../fields.config'
9
9
 
10
10
  @Component({
11
11
  selector: 'gn-ui-form-field-topics',
@@ -22,7 +22,7 @@ export class FormFieldTopicsComponent {
22
22
  this.topics = topics
23
23
  }
24
24
  @Output() valueChange: EventEmitter<string[]> = new EventEmitter()
25
- availableTopics = INSPIRE_TOPICS.map((topic) => {
25
+ availableTopics = ISO_TOPICS.map((topic) => {
26
26
  return {
27
27
  label: this.translateService.instant(topic.label),
28
28
  value: topic.value,
@@ -150,6 +150,7 @@
150
150
  <ng-container *ngSwitchCase="'contacts'">
151
151
  <gn-ui-form-field-contacts
152
152
  [value]="valueAsIndividuals"
153
+ [modelSpecifier]="modelSpecifier"
153
154
  (valueChange)="valueChange.emit($event)"
154
155
  ></gn-ui-form-field-contacts>
155
156
  </ng-container>
@@ -4,6 +4,7 @@ import {
4
4
  Component,
5
5
  ElementRef,
6
6
  EventEmitter,
7
+ inject,
7
8
  Input,
8
9
  Output,
9
10
  ViewChild,
@@ -22,6 +23,7 @@ import {
22
23
  import { FormFieldWrapperComponent } from '../../../../../../../../libs/ui/layout/src'
23
24
  import { TranslatePipe } from '@ngx-translate/core'
24
25
  import {
26
+ FieldFocusDirective,
25
27
  FormFieldDateComponent,
26
28
  FormFieldLicenseComponent,
27
29
  FormFieldTemporalExtentsComponent,
@@ -77,6 +79,7 @@ import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.
77
79
  FormFieldTopicsComponent,
78
80
  TextFieldModule,
79
81
  ],
82
+ hostDirectives: [FieldFocusDirective],
80
83
  })
81
84
  export class FormFieldComponent {
82
85
  @Input() uniqueIdentifier: string
@@ -92,6 +95,8 @@ export class FormFieldComponent {
92
95
  @ViewChild('titleInput') titleInput: ElementRef
93
96
  isOpenData = false
94
97
 
98
+ fieldFocus = inject(FieldFocusDirective)
99
+
95
100
  toggleIsOpenData(event: boolean) {
96
101
  this.isOpenData = event
97
102
  }
@@ -1,3 +1,4 @@
1
+ export * from './field-focus.directive'
1
2
  export * from './form-field-keywords/form-field-keywords.component'
2
3
  export * from './form-field-license/form-field-license.component'
3
4
  export * from './form-field-date/form-field-date.component'
@@ -0,0 +1,35 @@
1
+ /* Glow applied by FieldFocusDirective (gnUiFieldFocus) to a focused field or,
2
+ when the field is hidden, to its whole section. */
3
+ .gn-ui-field-focus-glow {
4
+ position: relative;
5
+ }
6
+
7
+ .gn-ui-field-focus-glow::after {
8
+ content: '';
9
+ position: absolute;
10
+ inset: -8px;
11
+ border-radius: 8px;
12
+ background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
13
+ pointer-events: none;
14
+ animation: gn-ui-field-focus-glow 3s ease-out forwards;
15
+ }
16
+
17
+ .gn-ui-field-focus-glow::before {
18
+ content: '';
19
+ position: absolute;
20
+ inset: -16px;
21
+ border: 2px dashed var(--color-primary);
22
+ border-radius: 10px;
23
+ pointer-events: none;
24
+ animation: gn-ui-field-focus-glow 3s ease-out forwards;
25
+ }
26
+
27
+ @keyframes gn-ui-field-focus-glow {
28
+ 0%,
29
+ 55% {
30
+ opacity: 1;
31
+ }
32
+ 100% {
33
+ opacity: 0;
34
+ }
35
+ }
@@ -4,7 +4,11 @@
4
4
  track sectionTracker($index, section)
5
5
  ) {
6
6
  @if (!section.hidden) {
7
- <div class="flex flex-col gap-6 border p-8 rounded-[8px] shadow">
7
+ <div
8
+ class="flex flex-col gap-6 border p-8 rounded-[8px] shadow scroll-mt-[90px]"
9
+ gnUiFieldFocus
10
+ #sectionFocus="fieldFocus"
11
+ >
8
12
  <div class="flex flex-col gap-2">
9
13
  @if (section.labelKey) {
10
14
  <div class="text-2xl font-title text-black" translate>
@@ -1,15 +1,23 @@
1
1
  import { CommonModule } from '@angular/common'
2
- import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ inject,
6
+ OnDestroy,
7
+ OnInit,
8
+ viewChildren,
9
+ } from '@angular/core'
3
10
  import { EditorFacade } from '../../+state/editor.facade'
4
11
  import { EditorFieldValue } from '../../models'
5
- import { FormFieldComponent } from './form-field'
12
+ import { FieldFocusDirective, FormFieldComponent } from './form-field'
6
13
  import { TranslateDirective } from '@ngx-translate/core'
7
14
  import {
8
15
  EditorFieldWithValue,
9
16
  EditorSectionWithValues,
10
17
  } from '../../+state/editor.models'
11
- import { map } from 'rxjs'
18
+ import { firstValueFrom, map, Subscription, withLatestFrom } from 'rxjs'
12
19
  import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
20
+ import { switchMap } from 'rxjs/operators'
13
21
 
14
22
  @Component({
15
23
  selector: 'gn-ui-record-form',
@@ -17,15 +25,63 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
17
25
  styleUrls: ['./record-form.component.css'],
18
26
  changeDetection: ChangeDetectionStrategy.OnPush,
19
27
  standalone: true,
20
- imports: [CommonModule, FormFieldComponent, TranslateDirective],
28
+ imports: [
29
+ CommonModule,
30
+ FormFieldComponent,
31
+ FieldFocusDirective,
32
+ TranslateDirective,
33
+ ],
21
34
  })
22
- export class RecordFormComponent {
35
+ export class RecordFormComponent implements OnInit, OnDestroy {
23
36
  facade = inject(EditorFacade)
37
+ subscription = new Subscription()
24
38
 
25
39
  recordUniqueIdentifier$ = this.facade.record$.pipe(
26
40
  map((record) => record.uniqueIdentifier)
27
41
  )
28
42
 
43
+ focusFieldWithPage$ = this.facade.focusedField$.pipe(
44
+ switchMap(async (field) => {
45
+ const location = await this.getFieldLocation(field)
46
+ return [field, location?.page ?? null, location?.section ?? -1] as const
47
+ })
48
+ )
49
+
50
+ formFields = viewChildren(FormFieldComponent)
51
+ sectionFocusDirectives = viewChildren<FieldFocusDirective>('sectionFocus')
52
+
53
+ focusField(model: CatalogRecordKeys, sectionIndex: number) {
54
+ const field = this.formFields().find((f) => f.model === model)
55
+ if (field) {
56
+ field.fieldFocus.focusField()
57
+ return
58
+ }
59
+ this.sectionFocusDirectives()[sectionIndex]?.focusField(false)
60
+ }
61
+
62
+ ngOnInit() {
63
+ this.subscription.add(
64
+ this.focusFieldWithPage$
65
+ .pipe(
66
+ withLatestFrom(
67
+ this.facade.currentPage$,
68
+ ([field, fieldPage, fieldSection], currentPage) =>
69
+ [field, fieldPage, fieldSection, currentPage] as const
70
+ )
71
+ )
72
+ .subscribe(([field, fieldPage, fieldSection, currentPage]) => {
73
+ if (fieldPage !== null && fieldPage !== currentPage) {
74
+ this.facade.setCurrentPage(fieldPage)
75
+ }
76
+ setTimeout(() => this.focusField(field, fieldSection))
77
+ })
78
+ )
79
+ }
80
+
81
+ ngOnDestroy() {
82
+ this.subscription.unsubscribe()
83
+ }
84
+
29
85
  handleFieldValueChange(model: CatalogRecordKeys, newValue: EditorFieldValue) {
30
86
  if (!model) {
31
87
  return
@@ -40,4 +96,20 @@ export class RecordFormComponent {
40
96
  sectionTracker(index: number, section: EditorSectionWithValues) {
41
97
  return section.labelKey
42
98
  }
99
+
100
+ async getFieldLocation(
101
+ model: CatalogRecordKeys
102
+ ): Promise<{ page: number; section: number } | null> {
103
+ const config = await firstValueFrom(this.facade.editorConfig$)
104
+ const page = config.pages.findIndex((p) =>
105
+ p.sections.some((s) => s.fields.some((f) => f.model === model))
106
+ )
107
+ if (page < 0) {
108
+ return null
109
+ }
110
+ const section = config.pages[page].sections
111
+ .filter((s) => !s.hidden)
112
+ .findIndex((s) => s.fields.some((f) => f.model === model))
113
+ return { page, section }
114
+ }
43
115
  }