geonetwork-ui 2.10.0-dev.de2fa8e42 → 2.10.0-dev.e2dc4fd62

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 (95) hide show
  1. package/fesm2022/geonetwork-ui.mjs +514 -5790
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +122 -67
  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/atomic-operations.ts +48 -7
  22. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts +1 -0
  23. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +5 -4
  24. package/src/libs/api/metadata-converter/src/lib/gn4/metadata-url.service.ts +2 -2
  25. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +1 -0
  26. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +7 -0
  27. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +8 -0
  28. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +8 -0
  29. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +11 -0
  30. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +33 -0
  31. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +33 -0
  32. package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +8 -4
  33. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +2 -2
  34. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +54 -32
  35. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts +1 -1
  36. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts +1 -1
  37. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +48 -2
  38. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +13 -5
  39. package/src/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts +2 -1
  40. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  41. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  42. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  43. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +5 -0
  44. package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -2
  45. package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts +0 -2
  46. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +4 -1
  47. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +6 -7
  48. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  49. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +0 -34
  50. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +35 -0
  51. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +5 -1
  52. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +35 -19
  53. package/src/libs/feature/editor/src/lib/fields.config.ts +60 -47
  54. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  55. package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
  56. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
  57. package/src/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +2 -2
  58. package/src/libs/ui/dataviz/src/index.ts +0 -1
  59. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +48 -56
  60. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +7 -3
  61. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.css +13 -0
  62. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +13 -3
  63. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +43 -6
  64. package/src/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts +2 -2
  65. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +18 -8
  66. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +29 -1
  67. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
  68. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +2 -2
  69. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +4 -54
  70. package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
  71. package/src/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.ts +1 -1
  72. package/src/libs/ui/widgets/src/lib/popover/popover.component.ts +7 -1
  73. package/src/libs/util/app-config/src/lib/app-config.ts +35 -10
  74. package/src/libs/util/app-config/src/lib/model.ts +3 -0
  75. package/src/libs/util/app-config/src/lib/parse-utils.ts +27 -0
  76. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +1 -2
  77. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +6 -3
  78. package/src/libs/util/i18n/src/index.ts +0 -1
  79. package/src/libs/util/i18n/src/lib/i18n.constants.ts +17 -11
  80. package/src/libs/util/i18n/src/lib/i18n.interceptor.ts +2 -2
  81. package/src/libs/util/i18n/src/lib/i18n.providers.ts +14 -17
  82. package/src/libs/util/i18n/src/lib/test.translate.loader.ts +48 -0
  83. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  84. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  85. package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
  86. package/style.css +0 -1
  87. package/tailwind.base.css +8 -0
  88. package/translations/de.json +23 -19
  89. package/translations/en.json +23 -19
  90. package/translations/es.json +23 -19
  91. package/translations/fr.json +23 -19
  92. package/translations/it.json +23 -19
  93. package/translations/nl.json +23 -19
  94. package/translations/pt.json +23 -19
  95. package/translations/sk.json +23 -19
@@ -128,7 +128,7 @@ export class OrganizationsFromMetadataService
128
128
  ) {}
129
129
 
130
130
  private get langIndex() {
131
- return `lang${toLang3(this.translateService.currentLang)}`
131
+ return `lang${toLang3(this.translateService.getCurrentLang())}`
132
132
  }
133
133
 
134
134
  equalsNormalizedStrings(
@@ -1,5 +1,5 @@
1
1
  import { HttpClient, HttpEventType } from '@angular/common/http'
2
- import { Injectable, InjectionToken, inject } from '@angular/core'
2
+ import { inject, Injectable, InjectionToken } from '@angular/core'
3
3
  import {
4
4
  KeywordApiResponse,
5
5
  ThesaurusApiResponse,
@@ -12,11 +12,13 @@ import {
12
12
  } from '../../../../../../../libs/common/domain/src/lib/model/record'
13
13
  import { KeywordType } from '../../../../../../../libs/common/domain/src/lib/model/thesaurus'
14
14
  import { UserModel } from '../../../../../../../libs/common/domain/src/lib/model/user/user.model'
15
+ import { GroupModel } from '../../../../../../../libs/common/domain/src/lib/model/user/group.model'
15
16
  import {
16
17
  PlatformServiceInterface,
17
18
  UploadEvent,
18
19
  } from '../../../../../../../libs/common/domain/src/lib/platform.service.interface'
19
20
  import {
21
+ GroupsApiService,
20
22
  MeApiService,
21
23
  RecordsApiService,
22
24
  RegistriesApiService,
@@ -57,6 +59,7 @@ export const DISABLE_AUTH = new InjectionToken<boolean>('gnDisableAuth', {
57
59
  export class Gn4PlatformService implements PlatformServiceInterface {
58
60
  private meApi = inject(MeApiService)
59
61
  private usersApi = inject(UsersApiService)
62
+ private groupsApi = inject(GroupsApiService)
60
63
  private mapper = inject(Gn4PlatformMapper)
61
64
  private toolsApiService = inject(ToolsApiService)
62
65
  private registriesApiService = inject(RegistriesApiService)
@@ -97,7 +100,7 @@ export class Gn4PlatformService implements PlatformServiceInterface {
97
100
  private keywordsByThesauri: Record<string, Observable<Keyword[]>> = {}
98
101
 
99
102
  private get lang3() {
100
- return toLang3(this.translateService.currentLang)
103
+ return toLang3(this.translateService.getCurrentLang())
101
104
  }
102
105
 
103
106
  constructor() {
@@ -155,6 +158,49 @@ export class Gn4PlatformService implements PlatformServiceInterface {
155
158
  return this.users$
156
159
  }
157
160
 
161
+ getUserPermissionsByGroup(): Observable<GroupModel[]> {
162
+ if (this.disableAuth) return of([])
163
+ return combineLatest([this.meApi.getMe(), this.groupsApi.getGroups()]).pipe(
164
+ map(([meResponse, groups]) => {
165
+ if (!meResponse) return []
166
+ if (meResponse.admin) {
167
+ return groups.map((group) => ({
168
+ groupId: group.id,
169
+ groupName: group.name,
170
+ isMember: true,
171
+ canEdit: true,
172
+ canApprove: true,
173
+ canAdministrate: true,
174
+ }))
175
+ }
176
+ const reviewerIds = meResponse.groupsWithReviewer ?? []
177
+ const editorIds = meResponse.groupsWithEditor ?? []
178
+ const memberIds = meResponse.groupsWithRegisteredUser ?? []
179
+ const adminIds = meResponse.groupsWithUserAdmin ?? []
180
+ const groupsById = new Map(groups.map((g) => [g.id, g]))
181
+ return [
182
+ ...new Set([
183
+ ...reviewerIds,
184
+ ...editorIds,
185
+ ...groups.map((g) => g.id),
186
+ ]),
187
+ ]
188
+ .filter((id) => groupsById.has(id))
189
+ .map((id) => {
190
+ const group = groupsById.get(id)
191
+ return {
192
+ groupId: group.id,
193
+ groupName: group.name,
194
+ isMember: memberIds.includes(id),
195
+ canEdit: editorIds.includes(id),
196
+ canApprove: reviewerIds.includes(id),
197
+ canAdministrate: adminIds.includes(id),
198
+ }
199
+ })
200
+ })
201
+ )
202
+ }
203
+
158
204
  translateKey(key: string): Observable<string> {
159
205
  // if the key is a URI, use the registries API to look for the translation
160
206
  if (key.match(/^https?:\/\//)) {
@@ -104,15 +104,12 @@ export interface Keyword {
104
104
  type: KeywordType
105
105
  thesaurus?: ThesaurusModel
106
106
  bbox?: [number, number, number, number]
107
+ // thesaurus hierarchy path incl. the keyword as last element; unset for root-level keywords
108
+ hierarchyPath?: string[]
107
109
 
108
110
  translations?: KeywordTranslations
109
111
  }
110
112
 
111
- export interface INSPIRE_topic {
112
- value: string
113
- label: string
114
- }
115
-
116
113
  export interface ResourceIdentifier {
117
114
  code: string
118
115
  codeSpace?: string
@@ -254,10 +251,20 @@ export interface DatasetTemporalExtent {
254
251
  end?: Date
255
252
  }
256
253
 
254
+ /**
255
+ * Represents a source dataset referenced from a lineage entry.
256
+ */
257
+ export interface SourceRecord {
258
+ uuid?: string
259
+ title?: string
260
+ href?: string
261
+ }
262
+
257
263
  export interface DatasetRecord extends BaseRecord {
258
264
  kind: 'dataset'
259
265
  status: RecordStatus
260
266
  lineage: string // Explanation of the origin of this record (e.g: how, why)"
267
+ sourceRecords: Array<SourceRecord>
261
268
  onlineResources: Array<DatasetOnlineResource>
262
269
  spatialExtents: Array<DatasetSpatialExtent>
263
270
  temporalExtents: Array<DatasetTemporalExtent>
@@ -285,6 +292,7 @@ export interface ServiceRecord extends BaseRecord {
285
292
  export interface ReuseRecord extends BaseRecord {
286
293
  kind: 'reuse'
287
294
  lineage: string // Explanation of the origin of this record (e.g: how, why)"
295
+ sourceRecords: Array<SourceRecord>
288
296
  onlineResources: Array<DatasetOnlineResource>
289
297
  reuseType: ReuseType
290
298
  spatialExtents: Array<DatasetSpatialExtent>
@@ -1,7 +1,8 @@
1
1
  export type KeywordType = 'place' | 'temporal' | 'theme' | 'other'
2
2
 
3
3
  export interface ThesaurusModel {
4
- id: string
4
+ // absent when the source provides no identifier for the thesaurus
5
+ id?: string
5
6
  name?: string
6
7
  url?: URL
7
8
  thesaurusKey?: string
@@ -0,0 +1,8 @@
1
+ export interface GroupModel {
2
+ groupId: number
3
+ groupName: string
4
+ isMember: boolean
5
+ canEdit: boolean
6
+ canApprove: boolean
7
+ canAdministrate: boolean
8
+ }
@@ -1 +1,2 @@
1
1
  export * from './user.model'
2
+ export * from './group.model'
@@ -1,5 +1,6 @@
1
1
  import type { Observable } from 'rxjs'
2
2
  import type { UserModel } from './model/user/user.model'
3
+ import type { GroupModel } from './model/user/group.model'
3
4
  import type { Organization } from './model/record/organization.model'
4
5
  import { CatalogRecord, Keyword, UserFeedback } from './model/record'
5
6
  import { KeywordType } from './model/thesaurus'
@@ -28,6 +29,7 @@ export abstract class PlatformServiceInterface {
28
29
  abstract getMe(): Observable<UserModel>
29
30
  abstract isAnonymous(): Observable<boolean>
30
31
  abstract getUsers(): Observable<UserModel[]>
32
+ abstract getUserPermissionsByGroup(): Observable<GroupModel[]>
31
33
  abstract getUsersByOrganization(
32
34
  organisation: Organization
33
35
  ): Observable<UserModel[]>
@@ -134,6 +134,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
134
134
  lineage: `This record was edited manually to test the conversion processes
135
135
 
136
136
  As such, **it is not very interesting at all.**`,
137
+ sourceRecords: [],
137
138
  licenses: [
138
139
  {
139
140
  text: 'Licence ODbL mai 2013 (basée sur ODbL 1.0)',
@@ -310,6 +311,7 @@ Malgré l'attention portée à la création de ces données, il est rappelé que
310
311
  ],
311
312
  lineage: `Document d’urbanisme numérisé conformément aux prescriptions nationales du CNIG par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne.
312
313
  Ce lot de données produit en 2019, a été numérisé à partir du PCI Vecteur de 2019 et contrôlé par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne.`,
314
+ sourceRecords: [],
313
315
  legalConstraints: [],
314
316
  securityConstraints: [],
315
317
  otherConstraints: [],
@@ -361,6 +363,7 @@ export const simpleDatasetRecordFixture = (): DatasetRecord => ({
361
363
  overviews: [],
362
364
  spatialExtents: [],
363
365
  temporalExtents: [],
366
+ sourceRecords: [],
364
367
  onlineResources: [
365
368
  {
366
369
  type: 'download',
@@ -413,6 +416,7 @@ export const simpleDatasetRecordWithFcatsFixture = (): DatasetRecord => ({
413
416
  overviews: [],
414
417
  spatialExtents: [],
415
418
  temporalExtents: [],
419
+ sourceRecords: [],
416
420
  onlineResources: [],
417
421
  updateFrequency: { per: 'month', updatedTimes: 3 },
418
422
  translations: {},
@@ -978,6 +982,7 @@ export const multilingualDatasetFixture: () => DatasetRecord = () => ({
978
982
  title: 'English Title',
979
983
  abstract: 'English Abstract',
980
984
  lineage: 'English Lineage',
985
+ sourceRecords: [],
981
986
  translations: {
982
987
  title: { fr: 'Titre Français', de: 'Titel DE' },
983
988
  abstract: { fr: 'Résumé Français', de: 'Beschreibung DE' },
@@ -1,4 +1,4 @@
1
- import { Injectable, inject } from '@angular/core'
1
+ import { inject, Injectable } from '@angular/core'
2
2
  import { SourcesApiService } from '../../../../../../libs/data-access/gn4/src'
3
3
  import { Observable } from 'rxjs'
4
4
  import { filter, map, shareReplay } from 'rxjs/operators'
@@ -21,7 +21,10 @@ export class SourcesService {
21
21
  return this.sources$.pipe(
22
22
  map((sources) => sources.filter((source) => source.uuid === uuid)[0]),
23
23
  filter((source) => !!source),
24
- map((source) => source.label[toLang3(this.translateService.currentLang)])
24
+ map(
25
+ (source) =>
26
+ source.label[toLang3(this.translateService.getCurrentLang())]
27
+ )
25
28
  )
26
29
  }
27
30
  }
@@ -8,7 +8,6 @@ import {
8
8
 
9
9
  import { FormFieldRichComponent } from '../record-form/form-field/form-field-rich/form-field-rich.component'
10
10
  import { ButtonComponent, UrlInputComponent } from '../../../../../../../libs/ui/inputs/src'
11
- import { MatIconModule } from '@angular/material/icon'
12
11
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
13
12
  import { Constraint } from '../../../../../../../libs/common/domain/src/lib/model/record'
14
13
  import {
@@ -26,7 +25,6 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker'
26
25
  FormFieldRichComponent,
27
26
  UrlInputComponent,
28
27
  ButtonComponent,
29
- MatIconModule,
30
28
  TranslateDirective,
31
29
  TranslatePipe,
32
30
  NgIconComponent,
@@ -10,7 +10,7 @@ export class FieldFocusDirective {
10
10
 
11
11
  private el = inject(ElementRef)
12
12
 
13
- public focusField() {
13
+ public focusField(focusInnerTarget = true) {
14
14
  setTimeout(() => {
15
15
  const host = this.el.nativeElement as HTMLElement
16
16
  const glowClass = this.gnUiFieldFocusGlowClass
@@ -25,6 +25,9 @@ export class FieldFocusDirective {
25
25
  )
26
26
 
27
27
  host.scrollIntoView({ behavior: 'smooth', block: 'start' })
28
+ if (!focusInnerTarget) {
29
+ return
30
+ }
28
31
  const target =
29
32
  host.querySelector<HTMLElement>(
30
33
  'input, textarea, select, [contenteditable="true"]'
@@ -131,13 +131,12 @@ export class FormFieldConstraintsShortcutsComponent implements OnDestroy {
131
131
  map((c) => c.length > 0),
132
132
  distinctUntilChanged()
133
133
  )
134
- combineLatest([
135
- isConstraintNotEmpty$,
136
- this.anyToggleActivated$,
137
- ]).subscribe(([isNotEmpty, anyToggleActivated]) => {
138
- const visible = isNotEmpty && !anyToggleActivated
139
- this.editorFacade.setFieldVisibility({ model }, visible)
140
- })
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
+ })
141
140
  }
142
141
  hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
143
142
  hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
@@ -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,
@@ -1,37 +1,3 @@
1
1
  :host {
2
2
  scroll-margin-top: 90px;
3
3
  }
4
-
5
- :host.gn-ui-field-focus-glow {
6
- position: relative;
7
- }
8
-
9
- :host.gn-ui-field-focus-glow::after {
10
- content: '';
11
- position: absolute;
12
- inset: -8px;
13
- border-radius: 8px;
14
- background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
15
- pointer-events: none;
16
- animation: gn-ui-field-focus-glow 3s ease-out forwards;
17
- }
18
-
19
- :host.gn-ui-field-focus-glow::before {
20
- content: '';
21
- position: absolute;
22
- inset: -16px;
23
- border: 2px dashed var(--color-primary);
24
- border-radius: 10px;
25
- pointer-events: none;
26
- animation: gn-ui-field-focus-glow 3s ease-out forwards;
27
- }
28
-
29
- @keyframes gn-ui-field-focus-glow {
30
- 0%,
31
- 55% {
32
- opacity: 1;
33
- }
34
- 100% {
35
- opacity: 0;
36
- }
37
- }
@@ -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>
@@ -9,7 +9,7 @@ import {
9
9
  } from '@angular/core'
10
10
  import { EditorFacade } from '../../+state/editor.facade'
11
11
  import { EditorFieldValue } from '../../models'
12
- import { FormFieldComponent } from './form-field'
12
+ import { FieldFocusDirective, FormFieldComponent } from './form-field'
13
13
  import { TranslateDirective } from '@ngx-translate/core'
14
14
  import {
15
15
  EditorFieldWithValue,
@@ -25,7 +25,12 @@ import { switchMap } from 'rxjs/operators'
25
25
  styleUrls: ['./record-form.component.css'],
26
26
  changeDetection: ChangeDetectionStrategy.OnPush,
27
27
  standalone: true,
28
- imports: [CommonModule, FormFieldComponent, TranslateDirective],
28
+ imports: [
29
+ CommonModule,
30
+ FormFieldComponent,
31
+ FieldFocusDirective,
32
+ TranslateDirective,
33
+ ],
29
34
  })
30
35
  export class RecordFormComponent implements OnInit, OnDestroy {
31
36
  facade = inject(EditorFacade)
@@ -36,17 +41,22 @@ export class RecordFormComponent implements OnInit, OnDestroy {
36
41
  )
37
42
 
38
43
  focusFieldWithPage$ = this.facade.focusedField$.pipe(
39
- switchMap(
40
- async (field) => [field, await this.getPageIndexForField(field)] as const
41
- )
44
+ switchMap(async (field) => {
45
+ const location = await this.getFieldLocation(field)
46
+ return [field, location?.page ?? null, location?.section ?? -1] as const
47
+ })
42
48
  )
43
49
 
44
50
  formFields = viewChildren(FormFieldComponent)
51
+ sectionFocusDirectives = viewChildren<FieldFocusDirective>('sectionFocus')
45
52
 
46
- focusField(model: CatalogRecordKeys) {
47
- const fields = this.formFields()
48
- const field = fields.find((f) => f.model === model)
49
- field?.fieldFocus.focusField()
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)
50
60
  }
51
61
 
52
62
  ngOnInit() {
@@ -55,15 +65,15 @@ export class RecordFormComponent implements OnInit, OnDestroy {
55
65
  .pipe(
56
66
  withLatestFrom(
57
67
  this.facade.currentPage$,
58
- ([field, fieldPage], currentPage) =>
59
- [field, fieldPage, currentPage] as const
68
+ ([field, fieldPage, fieldSection], currentPage) =>
69
+ [field, fieldPage, fieldSection, currentPage] as const
60
70
  )
61
71
  )
62
- .subscribe(([field, fieldPage, currentPage]) => {
72
+ .subscribe(([field, fieldPage, fieldSection, currentPage]) => {
63
73
  if (fieldPage !== null && fieldPage !== currentPage) {
64
74
  this.facade.setCurrentPage(fieldPage)
65
75
  }
66
- setTimeout(() => this.focusField(field))
76
+ setTimeout(() => this.focusField(field, fieldSection))
67
77
  })
68
78
  )
69
79
  }
@@ -87,13 +97,19 @@ export class RecordFormComponent implements OnInit, OnDestroy {
87
97
  return section.labelKey
88
98
  }
89
99
 
90
- async getPageIndexForField(model: CatalogRecordKeys): Promise<number | null> {
100
+ async getFieldLocation(
101
+ model: CatalogRecordKeys
102
+ ): Promise<{ page: number; section: number } | null> {
91
103
  const config = await firstValueFrom(this.facade.editorConfig$)
92
- const pageIndex = config.pages.findIndex((page) =>
93
- page.sections.some((section) =>
94
- section.fields.some((field) => field.model === model)
95
- )
104
+ const page = config.pages.findIndex((p) =>
105
+ p.sections.some((s) => s.fields.some((f) => f.model === model))
96
106
  )
97
- return pageIndex >= 0 ? pageIndex : null
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 }
98
114
  }
99
115
  }
@@ -4,10 +4,12 @@ import {
4
4
  EditorField,
5
5
  EditorSection,
6
6
  } from './models/editor-config.model'
7
- import {
8
- INSPIRE_topic,
9
- Keyword,
10
- } from '../../../../../libs/common/domain/src/lib/model/record'
7
+ import { Keyword } from '../../../../../libs/common/domain/src/lib/model/record'
8
+
9
+ export interface ISOTopic {
10
+ value: string
11
+ label: string
12
+ }
11
13
 
12
14
  /**
13
15
  * This file contains the configuration of the fields that will be displayed in the editor.
@@ -359,75 +361,86 @@ export const SPATIAL_SCOPES: Keyword[] = [
359
361
  ]
360
362
 
361
363
  /************************************************************
362
- *************** INSPIRE TOPICS **************
364
+ *************** ISO TOPICS **************
363
365
  ************************************************************
364
366
  */
365
367
 
366
- export const INSPIRE_TOPICS: INSPIRE_topic[] = [
367
- { value: 'biota', label: 'editor.record.form.topics.inspire.biota' },
368
+ // label keys mirror the ISO MD_TopicCategoryCode value for consistency
369
+ // TODO: correctly handle code lists; for instance, this code list is specific to ISO 19139
370
+ export const ISO_TOPICS: ISOTopic[] = [
371
+ { value: 'biota', label: marker('editor.record.form.topics.iso.biota') },
368
372
  {
369
373
  value: 'boundaries',
370
- label: 'editor.record.form.topics.inspire.boundaries',
374
+ label: marker('editor.record.form.topics.iso.boundaries'),
371
375
  },
372
376
  {
373
377
  value: 'climatologyMeteorologyAtmosphere',
374
- label: 'editor.record.form.topics.inspire.climatology',
378
+ label: marker(
379
+ 'editor.record.form.topics.iso.climatologyMeteorologyAtmosphere'
380
+ ),
381
+ },
382
+ {
383
+ value: 'economy',
384
+ label: marker('editor.record.form.topics.iso.economy'),
385
+ },
386
+ {
387
+ value: 'elevation',
388
+ label: marker('editor.record.form.topics.iso.elevation'),
375
389
  },
376
- { value: 'economy', label: 'editor.record.form.topics.inspire.economy' },
377
- { value: 'elevation', label: 'editor.record.form.topics.inspire.elevation' },
378
390
  {
379
391
  value: 'environment',
380
- label: 'editor.record.form.topics.inspire.environnement',
392
+ label: marker('editor.record.form.topics.iso.environment'),
393
+ },
394
+ {
395
+ value: 'farming',
396
+ label: marker('editor.record.form.topics.iso.farming'),
397
+ },
398
+ {
399
+ value: 'geoscientificInformation',
400
+ label: marker('editor.record.form.topics.iso.geoscientificInformation'),
381
401
  },
382
- { value: 'farming', label: 'editor.record.form.topics.inspire.farming' },
383
402
  {
384
- value: 'geoscientific information',
385
- label: 'editor.record.form.topics.inspire.geoscientific',
403
+ value: 'health',
404
+ label: marker('editor.record.form.topics.iso.health'),
386
405
  },
387
- { value: 'health', label: 'editor.record.form.topics.inspire.health' },
388
406
  {
389
407
  value: 'imageryBaseMapsEarthCover',
390
- label: 'editor.record.form.topics.inspire.imagery',
408
+ label: marker('editor.record.form.topics.iso.imageryBaseMapsEarthCover'),
409
+ },
410
+ {
411
+ value: 'inlandWaters',
412
+ label: marker('editor.record.form.topics.iso.inlandWaters'),
391
413
  },
392
- { value: 'inlandWaters', label: 'editor.record.form.topics.inspire.waters' },
393
414
  {
394
415
  value: 'intelligenceMilitary',
395
- label: 'editor.record.form.topics.inspire.intelligence',
416
+ label: marker('editor.record.form.topics.iso.intelligenceMilitary'),
417
+ },
418
+ {
419
+ value: 'location',
420
+ label: marker('editor.record.form.topics.iso.location'),
421
+ },
422
+ {
423
+ value: 'oceans',
424
+ label: marker('editor.record.form.topics.iso.oceans'),
396
425
  },
397
- { value: 'Location', label: 'editor.record.form.topics.inspire.location' },
398
- { value: 'Oceans', label: 'editor.record.form.topics.inspire.oceans' },
399
426
  {
400
427
  value: 'planningCadastre',
401
- label: 'editor.record.form.topics.inspire.planning',
428
+ label: marker('editor.record.form.topics.iso.planningCadastre'),
402
429
  },
403
- { value: 'Society', label: 'editor.record.form.topics.inspire.society' },
404
- { value: 'Structure', label: 'editor.record.form.topics.inspire.structure' },
405
430
  {
406
- value: 'Transportation',
407
- label: 'editor.record.form.topics.inspire.transportation',
431
+ value: 'society',
432
+ label: marker('editor.record.form.topics.iso.society'),
433
+ },
434
+ {
435
+ value: 'structure',
436
+ label: marker('editor.record.form.topics.iso.structure'),
437
+ },
438
+ {
439
+ value: 'transportation',
440
+ label: marker('editor.record.form.topics.iso.transportation'),
408
441
  },
409
442
  {
410
443
  value: 'utilitiesCommunication',
411
- label: 'editor.record.form.topics.inspire.utilities',
444
+ label: marker('editor.record.form.topics.iso.utilitiesCommunication'),
412
445
  },
413
446
  ]
414
-
415
- marker('editor.record.form.topics.inspire.biota')
416
- marker('editor.record.form.topics.inspire.boundaries')
417
- marker('editor.record.form.topics.inspire.climatology')
418
- marker('editor.record.form.topics.inspire.economy')
419
- marker('editor.record.form.topics.inspire.elevation')
420
- marker('editor.record.form.topics.inspire.environnement')
421
- marker('editor.record.form.topics.inspire.farming')
422
- marker('editor.record.form.topics.inspire.geoscientific')
423
- marker('editor.record.form.topics.inspire.health')
424
- marker('editor.record.form.topics.inspire.imagery')
425
- marker('editor.record.form.topics.inspire.intelligence')
426
- marker('editor.record.form.topics.inspire.location')
427
- marker('editor.record.form.topics.inspire.oceans')
428
- marker('editor.record.form.topics.inspire.planning')
429
- marker('editor.record.form.topics.inspire.society')
430
- marker('editor.record.form.topics.inspire.structure')
431
- marker('editor.record.form.topics.inspire.transportation')
432
- marker('editor.record.form.topics.inspire.utilities')
433
- marker('editor.record.form.topics.inspire.waters')
@@ -1,5 +1,5 @@
1
1
  import { Injectable, inject } from '@angular/core'
2
- import { forkJoin, Observable, of, switchMap } from 'rxjs'
2
+ import { Observable, switchMap } from 'rxjs'
3
3
  import { map, tap } from 'rxjs/operators'
4
4
  import { CatalogRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
5
5
  import { EditorConfig } from '../models/'