geonetwork-ui 2.10.0-dev.d0cd0940b → 2.10.0-dev.d26e9afe1

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 (137) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1294 -6077
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +200 -161
  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 +59 -37
  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 +56 -0
  44. package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -2
  45. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +4 -1
  46. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +26 -19
  47. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +5 -1
  48. package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts +0 -2
  49. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  50. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  51. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  52. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html +5 -5
  53. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts +2 -1
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +41 -0
  55. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +6 -8
  56. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +9 -2
  57. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +12 -0
  58. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts +1 -1
  59. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts +12 -5
  60. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.html +4 -4
  61. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.ts +7 -0
  62. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  63. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html +6 -2
  64. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts +3 -1
  65. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +3 -0
  66. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +21 -16
  67. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +14 -0
  68. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  69. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +34 -2
  70. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +18 -6
  71. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +92 -33
  72. package/src/libs/feature/editor/src/lib/expressions.ts +380 -8
  73. package/src/libs/feature/editor/src/lib/fields.config.ts +310 -336
  74. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +6 -3
  75. package/src/libs/feature/editor/src/lib/services/editor.service.ts +9 -13
  76. package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
  77. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
  78. package/src/libs/feature/search/src/lib/results-table/results-table-container.component.ts +10 -5
  79. package/src/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +2 -2
  80. package/src/libs/ui/dataviz/src/index.ts +0 -1
  81. package/src/libs/ui/elements/src/index.ts +1 -0
  82. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +88 -0
  83. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  84. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +29 -4
  85. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +51 -7
  86. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.css +13 -0
  87. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +13 -3
  88. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +43 -6
  89. package/src/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts +2 -2
  90. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  91. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +20 -10
  92. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +29 -1
  93. package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html +4 -5
  94. package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +4 -0
  95. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -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/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
  98. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +3 -3
  99. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  100. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  101. package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.html +5 -5
  102. package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.ts +9 -1
  103. package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html +1 -1
  104. package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts +3 -8
  105. package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.html +7 -7
  106. package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.ts +14 -1
  107. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +4 -54
  108. package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
  109. package/src/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.ts +1 -1
  110. package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html +12 -8
  111. package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts +3 -1
  112. package/src/libs/ui/search/src/lib/results-table/results-table.component.html +1 -0
  113. package/src/libs/ui/widgets/src/lib/popover/popover.component.ts +7 -1
  114. package/src/libs/util/app-config/src/lib/app-config.ts +70 -9
  115. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  116. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  117. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +1 -2
  118. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +6 -3
  119. package/src/libs/util/i18n/src/index.ts +0 -1
  120. package/src/libs/util/i18n/src/lib/i18n.constants.ts +17 -11
  121. package/src/libs/util/i18n/src/lib/i18n.interceptor.ts +2 -2
  122. package/src/libs/util/i18n/src/lib/i18n.providers.ts +14 -17
  123. package/src/libs/util/i18n/src/lib/test.translate.loader.ts +48 -0
  124. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  125. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  126. package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
  127. package/src/libs/util/shared/src/lib/utils/user-display.ts +9 -0
  128. package/style.css +0 -1
  129. package/tailwind.base.css +8 -0
  130. package/translations/de.json +58 -48
  131. package/translations/en.json +63 -53
  132. package/translations/es.json +32 -22
  133. package/translations/fr.json +58 -48
  134. package/translations/it.json +58 -48
  135. package/translations/nl.json +31 -21
  136. package/translations/pt.json +32 -22
  137. package/translations/sk.json +32 -22
@@ -28,6 +28,7 @@ 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 { ContactDetailsFormComponent } from '../../../contact-details/contact-details-form.component'
31
32
  import {
32
33
  createFuzzyFilter,
33
34
  getIndividualDisplayName,
@@ -35,6 +36,7 @@ import {
35
36
  } from '../../../../../../../../../libs/util/shared/src'
36
37
  import { map } from 'rxjs/operators'
37
38
  import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
39
+ import { FieldModelSpecifier } from '../../../../models/editor-config.model'
38
40
 
39
41
  @Component({
40
42
  selector: 'gn-ui-form-field-contacts',
@@ -47,6 +49,7 @@ import { SortableListComponent } from '../../../../../../../../../libs/ui/layout
47
49
  TranslateDirective,
48
50
  TranslatePipe,
49
51
  ContactCardComponent,
52
+ ContactDetailsFormComponent,
50
53
  SortableListComponent,
51
54
  ],
52
55
  })
@@ -56,6 +59,7 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
56
59
  private changeDetectorRef = inject(ChangeDetectorRef)
57
60
 
58
61
  @Input() value: Individual[]
62
+ @Input() modelSpecifier: FieldModelSpecifier
59
63
  @Output() valueChange: EventEmitter<Individual[]> = new EventEmitter()
60
64
 
61
65
  contacts: Individual[] = []
@@ -117,6 +121,14 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
117
121
  this.valueChange.emit(contacts)
118
122
  }
119
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
+
120
132
  /**
121
133
  * gn-ui-autocomplete
122
134
  */
@@ -3,11 +3,11 @@ import {
3
3
  ChangeDetectorRef,
4
4
  Component,
5
5
  EventEmitter,
6
+ inject,
6
7
  Input,
7
8
  Output,
8
9
  TemplateRef,
9
10
  ViewChild,
10
- inject,
11
11
  } from '@angular/core'
12
12
  import { MatDialog } from '@angular/material/dialog'
13
13
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
@@ -3,11 +3,14 @@ import {
3
3
  ChangeDetectorRef,
4
4
  Component,
5
5
  EventEmitter,
6
+ inject,
6
7
  Input,
7
8
  Output,
8
- inject,
9
9
  } from '@angular/core'
10
- import { GraphicOverview } from '../../../../../../../../../libs/common/domain/src/lib/model/record'
10
+ import {
11
+ GraphicOverview,
12
+ RecordKind,
13
+ } from '../../../../../../../../../libs/common/domain/src/lib/model/record'
11
14
  import { ImageInputComponent } from '../../../../../../../../../libs/ui/elements/src'
12
15
  import { PlatformServiceInterface } from '../../../../../../../../../libs/common/domain/src/lib/platform.service.interface'
13
16
  import { NotificationsService } from '../../../../../../../../../libs/feature/notifications/src'
@@ -30,6 +33,7 @@ export class FormFieldOverviewsComponent {
30
33
  private cd = inject(ChangeDetectorRef)
31
34
 
32
35
  @Input() metadataUuid: string
36
+ @Input() recordKind: RecordKind
33
37
  @Input() value: Array<GraphicOverview>
34
38
  @Output() valueChange: EventEmitter<Array<GraphicOverview>> =
35
39
  new EventEmitter()
@@ -111,13 +115,16 @@ export class FormFieldOverviewsComponent {
111
115
  {
112
116
  type: 'error',
113
117
  title: this.translateService.instant(
114
- 'editor.record.resourceError.title'
118
+ 'editor.record.resourceError.title',
119
+ { recordKind: this.recordKind }
115
120
  ),
116
121
  text: `${this.translateService.instant(
117
- 'editor.record.resourceError.body'
122
+ 'editor.record.resourceError.body',
123
+ { recordKind: this.recordKind }
118
124
  )} ${error.message}`,
119
125
  closeMessage: this.translateService.instant(
120
- 'editor.record.resourceError.closeMessage'
126
+ 'editor.record.resourceError.closeMessage',
127
+ { recordKind: this.recordKind }
121
128
  ),
122
129
  },
123
130
  undefined,
@@ -9,11 +9,11 @@
9
9
  "
10
10
  (buttonClick)="togglePreview()"
11
11
  >
12
- <span
13
- class="material-symbols-outlined gn-ui-icon-small"
12
+ <ng-icon
13
+ [name]="preview ? 'matVisibilityOffOutline' : 'matVisibilityOutline'"
14
+ size="16"
14
15
  [ngClass]="{ 'text-primary': !preview }"
15
- >{{ preview ? 'visibility_off' : 'visibility' }}</span
16
- >
16
+ ></ng-icon>
17
17
  &nbsp;
18
18
  {{ preview ? 'Edit' : 'Preview' }}
19
19
  </gn-ui-button>
@@ -10,6 +10,11 @@ import { ReactiveFormsModule } from '@angular/forms'
10
10
  import { MarkdownEditorComponent } from '../../../../../../../../../libs/ui/elements/src'
11
11
  import { ButtonComponent } from '../../../../../../../../../libs/ui/inputs/src'
12
12
  import { FormFieldWrapperComponent } from '../../../../../../../../../libs/ui/layout/src'
13
+ import { NgIconComponent, provideIcons } from '@ng-icons/core'
14
+ import {
15
+ matVisibilityOffOutline,
16
+ matVisibilityOutline,
17
+ } from '@ng-icons/material-icons/outline'
13
18
 
14
19
  @Component({
15
20
  selector: 'gn-ui-form-field-rich',
@@ -23,7 +28,9 @@ import { FormFieldWrapperComponent } from '../../../../../../../../../libs/ui/la
23
28
  MarkdownEditorComponent,
24
29
  FormFieldWrapperComponent,
25
30
  ButtonComponent,
31
+ NgIconComponent,
26
32
  ],
33
+ providers: [provideIcons({ matVisibilityOutline, matVisibilityOffOutline })],
27
34
  })
28
35
  export class FormFieldRichComponent {
29
36
  @Input() label: string
@@ -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,11 +1,15 @@
1
1
  <div class="flex flex-col gap-2">
2
2
  <gn-ui-check-toggle
3
- [label]="'editor.record.form.updateFrequency.planned' | translate"
3
+ [label]="
4
+ 'editor.record.form.updateFrequency.planned' | translate: { recordKind }
5
+ "
4
6
  [value]="planned"
5
7
  (toggled)="onPlannedToggled()"
6
8
  ></gn-ui-check-toggle>
7
9
  <gn-ui-dropdown-selector
8
- [title]="'editor.record.form.field.updateFrequency' | translate"
10
+ [title]="
11
+ 'editor.record.form.field.updateFrequency' | translate: { recordKind }
12
+ "
9
13
  [showTitle]="false"
10
14
  [choices]="choices"
11
15
  [selected]="selectedFrequency"
@@ -2,12 +2,13 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  Component,
4
4
  EventEmitter,
5
+ inject,
5
6
  Input,
6
7
  OnInit,
7
8
  Output,
8
- inject,
9
9
  } from '@angular/core'
10
10
  import {
11
+ RecordKind,
11
12
  UpdateFrequency,
12
13
  UpdateFrequencyCode,
13
14
  updateFrequencyCodeValues,
@@ -37,6 +38,7 @@ export class FormFieldUpdateFrequencyComponent implements OnInit {
37
38
  private translateService = inject(TranslateService)
38
39
 
39
40
  @Input() value: UpdateFrequency
41
+ @Input() recordKind: RecordKind
40
42
  @Output() valueChange: EventEmitter<UpdateFrequency> = new EventEmitter()
41
43
 
42
44
  protected choices: DropdownChoice[] = []
@@ -12,8 +12,8 @@
12
12
  </ng-container>
13
13
  <ng-template #withGenericWrapper>
14
14
  <gn-ui-form-field-wrapper
15
- [label]="config.labelKey! | translate"
16
- [hint]="config.hintKey! | translate"
15
+ [label]="config.labelKey! | translate: { recordKind }"
16
+ [hint]="config.hintKey! | translate: { recordKind }"
17
17
  >
18
18
  <ng-container *ngTemplateOutlet="fieldContent"></ng-container>
19
19
  </gn-ui-form-field-wrapper>
@@ -34,30 +34,32 @@
34
34
  [value]="valueAsString"
35
35
  (change)="valueChange.emit($event.target.value)"
36
36
  [placeholder]="
37
- 'editor.record.form.field.title.placeholder' | translate
37
+ 'editor.record.form.field.title.placeholder'
38
+ | translate: { recordKind }
38
39
  "
39
40
  ></textarea>
40
41
  <div class="flex flex-row justify-between self-start mt-0.5">
41
- <span
42
- class="material-symbols-outlined gn-ui-icon-small m-2 cursor-pointer"
42
+ <ng-icon
43
+ name="matEditOutline"
44
+ size="16"
45
+ class="m-2 cursor-pointer"
43
46
  (click)="focusTitleInput()"
44
- >edit</span
45
- >
46
- <span
47
+ ></ng-icon>
48
+ <ng-icon
47
49
  *ngIf="config.hintKey"
48
- class="material-symbols-outlined gn-ui-icon-small m-2"
49
- [matTooltip]="config.hintKey! | translate"
50
+ name="matHelpOutline"
51
+ size="16"
52
+ class="m-2"
53
+ [matTooltip]="config.hintKey! | translate: { recordKind }"
50
54
  matTooltipPosition="above"
51
- >
52
- help
53
- </span>
55
+ ></ng-icon>
54
56
  </div>
55
57
  </div>
56
58
  </ng-container>
57
59
  <ng-container *ngSwitchCase="'abstract'">
58
60
  <gn-ui-form-field-rich
59
- [label]="config.labelKey! | translate"
60
- [hint]="config.hintKey! | translate"
61
+ [label]="config.labelKey! | translate: { recordKind }"
62
+ [hint]="config.hintKey! | translate: { recordKind }"
61
63
  [value]="valueAsString"
62
64
  (valueChange)="valueChange.emit($event)"
63
65
  ></gn-ui-form-field-rich>
@@ -65,6 +67,7 @@
65
67
  <ng-container *ngSwitchCase="'overviews'">
66
68
  <gn-ui-form-field-overviews
67
69
  [metadataUuid]="uniqueIdentifier"
70
+ [recordKind]="recordKind"
68
71
  [value]="valueAsOverviews"
69
72
  (valueChange)="valueChange.emit($event)"
70
73
  ></gn-ui-form-field-overviews>
@@ -92,6 +95,7 @@
92
95
  <gn-ui-form-field-update-frequency
93
96
  [value]="valueAsUpdateFrequency"
94
97
  (valueChange)="valueChange.emit($event)"
98
+ [recordKind]="recordKind"
95
99
  ></gn-ui-form-field-update-frequency>
96
100
  </ng-container>
97
101
  <ng-container *ngSwitchCase="'temporalExtents'">
@@ -117,7 +121,7 @@
117
121
  </ng-container>
118
122
  <ng-container *ngSwitchCase="'licenses'">
119
123
  <gn-ui-form-field-license
120
- [label]="config.labelKey! | translate"
124
+ [label]="config.labelKey! | translate: { recordKind }"
121
125
  [recordLicences]="valueAsConstraints"
122
126
  (recordLicencesChange)="valueChange.emit($event)"
123
127
  ></gn-ui-form-field-license>
@@ -150,6 +154,7 @@
150
154
  <ng-container *ngSwitchCase="'contacts'">
151
155
  <gn-ui-form-field-contacts
152
156
  [value]="valueAsIndividuals"
157
+ [modelSpecifier]="modelSpecifier"
153
158
  (valueChange)="valueChange.emit($event)"
154
159
  ></gn-ui-form-field-contacts>
155
160
  </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,
@@ -17,11 +18,18 @@ import {
17
18
  Individual,
18
19
  Keyword,
19
20
  OnlineResource,
21
+ RecordKind,
20
22
  UpdateFrequency,
21
23
  } from '../../../../../../../../libs/common/domain/src/lib/model/record'
22
24
  import { FormFieldWrapperComponent } from '../../../../../../../../libs/ui/layout/src'
25
+ import { NgIconComponent, provideIcons } from '@ng-icons/core'
26
+ import {
27
+ matEditOutline,
28
+ matHelpOutline,
29
+ } from '@ng-icons/material-icons/outline'
23
30
  import { TranslatePipe } from '@ngx-translate/core'
24
31
  import {
32
+ FieldFocusDirective,
25
33
  FormFieldDateComponent,
26
34
  FormFieldLicenseComponent,
27
35
  FormFieldTemporalExtentsComponent,
@@ -76,10 +84,14 @@ import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.
76
84
  FormFieldSpatialToggleComponent,
77
85
  FormFieldTopicsComponent,
78
86
  TextFieldModule,
87
+ NgIconComponent,
79
88
  ],
89
+ providers: [provideIcons({ matEditOutline, matHelpOutline })],
90
+ hostDirectives: [FieldFocusDirective],
80
91
  })
81
92
  export class FormFieldComponent {
82
93
  @Input() uniqueIdentifier: string
94
+ @Input() recordKind: RecordKind
83
95
  @Input() model: CatalogRecordKeys
84
96
  @Input() modelSpecifier: FieldModelSpecifier
85
97
  @Input() componentName: FormFieldComponentName
@@ -92,6 +104,8 @@ export class FormFieldComponent {
92
104
  @ViewChild('titleInput') titleInput: ElementRef
93
105
  isOpenData = false
94
106
 
107
+ fieldFocus = inject(FieldFocusDirective)
108
+
95
109
  toggleIsOpenData(event: boolean) {
96
110
  this.isOpenData = event
97
111
  }
@@ -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'
@@ -1,3 +1,35 @@
1
- gn-ui-form-field {
2
- scroll-margin-top: 90px;
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
+ }
3
35
  }
@@ -3,16 +3,28 @@
3
3
  section of facade.currentSections$ | async;
4
4
  track sectionTracker($index, section)
5
5
  ) {
6
- @if (!section.hidden) {
7
- <div class="flex flex-col gap-6 border p-8 rounded-[8px] shadow">
6
+ @if (evaluateExpression(section.hidden)() !== true) {
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
- <div class="text-2xl font-title text-black" translate>
14
+ <div
15
+ class="text-2xl font-title text-black"
16
+ translate
17
+ [translateParams]="{ recordKind: recordKind$ | async }"
18
+ >
11
19
  {{ section.labelKey }}
12
20
  </div>
13
21
  }
14
22
  @if (section.descriptionKey) {
15
- <div class="text-gray-800 text-sm" translate>
23
+ <div
24
+ class="text-gray-800 text-sm"
25
+ translate
26
+ [translateParams]="{ recordKind: recordKind$ | async }"
27
+ >
16
28
  {{ section.descriptionKey }}
17
29
  </div>
18
30
  }
@@ -22,15 +34,15 @@
22
34
  field of section.fieldsWithValues;
23
35
  track fieldTracker($index, field)
24
36
  ) {
25
- @if (!field.config.hidden) {
37
+ @if (evaluateExpression(field.config.hidden)() !== true) {
26
38
  <gn-ui-form-field
27
- [id]="anchorIdPrefix + field.config.model"
28
39
  [ngClass]="
29
40
  field.config.gridColumnSpan === 1
30
41
  ? 'col-span-1'
31
42
  : 'col-span-2'
32
43
  "
33
44
  [uniqueIdentifier]="recordUniqueIdentifier$ | async"
45
+ [recordKind]="recordKind$ | async"
34
46
  [model]="field.config.model!"
35
47
  [modelSpecifier]="field.config.modelSpecifier!"
36
48
  [config]="field.config.formFieldConfig"
@@ -2,21 +2,28 @@ import { CommonModule } from '@angular/common'
2
2
  import {
3
3
  ChangeDetectionStrategy,
4
4
  Component,
5
- ElementRef,
5
+ computed,
6
6
  inject,
7
7
  OnDestroy,
8
8
  OnInit,
9
+ Signal,
10
+ signal,
11
+ viewChildren,
9
12
  } from '@angular/core'
10
13
  import { EditorFacade } from '../../+state/editor.facade'
11
14
  import { EditorFieldValue } from '../../models'
12
- import { FormFieldComponent } from './form-field'
15
+ import { FieldFocusDirective, FormFieldComponent } from './form-field'
13
16
  import { TranslateDirective } from '@ngx-translate/core'
14
17
  import {
15
18
  EditorFieldWithValue,
16
19
  EditorSectionWithValues,
17
20
  } from '../../+state/editor.models'
18
- import { filter, firstValueFrom, map, Subscription, switchMap } from 'rxjs'
21
+ import { firstValueFrom, map, Subscription, withLatestFrom } from 'rxjs'
19
22
  import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
23
+ import { switchMap } from 'rxjs/operators'
24
+ import { toSignal } from '@angular/core/rxjs-interop'
25
+ import { evaluate, isExpression } from '../../expressions'
26
+ import { NotificationsService } from '../../../../../../../libs/feature/notifications/src'
20
27
 
21
28
  @Component({
22
29
  selector: 'gn-ui-record-form',
@@ -24,44 +31,57 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
24
31
  styleUrls: ['./record-form.component.css'],
25
32
  changeDetection: ChangeDetectionStrategy.OnPush,
26
33
  standalone: true,
27
- imports: [CommonModule, FormFieldComponent, TranslateDirective],
34
+ imports: [
35
+ CommonModule,
36
+ FormFieldComponent,
37
+ FieldFocusDirective,
38
+ TranslateDirective,
39
+ ],
28
40
  })
29
41
  export class RecordFormComponent implements OnInit, OnDestroy {
30
- anchorIdPrefix = 'gn-ui--field-'
31
42
  facade = inject(EditorFacade)
32
- private el = inject(ElementRef)
43
+ notifications = inject(NotificationsService)
33
44
  subscription = new Subscription()
34
45
 
35
46
  recordUniqueIdentifier$ = this.facade.record$.pipe(
36
47
  map((record) => record.uniqueIdentifier)
37
48
  )
49
+ recordKind$ = this.facade.record$.pipe(map((record) => record.kind))
50
+
51
+ focusFieldWithPage$ = this.facade.focusedField$.pipe(
52
+ switchMap(async (field) => {
53
+ const location = await this.getFieldLocation(field)
54
+ return [field, location?.page ?? null, location?.section ?? -1] as const
55
+ })
56
+ )
57
+
58
+ formFields = viewChildren(FormFieldComponent)
59
+ sectionFocusDirectives = viewChildren<FieldFocusDirective>('sectionFocus')
60
+
61
+ focusField(model: CatalogRecordKeys, sectionIndex: number) {
62
+ const field = this.formFields().find((f) => f.model === model)
63
+ if (field) {
64
+ field.fieldFocus.focusField()
65
+ return
66
+ }
67
+ this.sectionFocusDirectives()[sectionIndex]?.focusField(false)
68
+ }
38
69
 
39
70
  ngOnInit() {
40
71
  this.subscription.add(
41
- this.facade.focusedField$
72
+ this.focusFieldWithPage$
42
73
  .pipe(
43
- filter((field) => !!field),
44
- switchMap(async (field) => ({
45
- field: field as CatalogRecordKeys,
46
- pageIndex: await this.getPageIndexForField(
47
- field as CatalogRecordKeys
48
- ),
49
- }))
74
+ withLatestFrom(
75
+ this.facade.currentPage$,
76
+ ([field, fieldPage, fieldSection], currentPage) =>
77
+ [field, fieldPage, fieldSection, currentPage] as const
78
+ )
50
79
  )
51
- .subscribe(async ({ field, pageIndex }) => {
52
- const currentPage = await firstValueFrom(this.facade.currentPage$)
53
- if (pageIndex !== null && pageIndex !== currentPage) {
54
- this.facade.setCurrentPage(pageIndex)
55
- this.el.nativeElement.scrollIntoView({
56
- behavior: 'instant',
57
- block: 'start',
58
- })
80
+ .subscribe(([field, fieldPage, fieldSection, currentPage]) => {
81
+ if (fieldPage !== null && fieldPage !== currentPage) {
82
+ this.facade.setCurrentPage(fieldPage)
59
83
  }
60
- setTimeout(() =>
61
- document
62
- .getElementById(this.anchorIdPrefix + field)
63
- ?.scrollIntoView({ behavior: 'instant', block: 'start' })
64
- )
84
+ setTimeout(() => this.focusField(field, fieldSection))
65
85
  })
66
86
  )
67
87
  }
@@ -85,13 +105,52 @@ export class RecordFormComponent implements OnInit, OnDestroy {
85
105
  return section.labelKey
86
106
  }
87
107
 
88
- async getPageIndexForField(model: CatalogRecordKeys): Promise<number | null> {
108
+ async getFieldLocation(
109
+ model: CatalogRecordKeys
110
+ ): Promise<{ page: number; section: number } | null> {
89
111
  const config = await firstValueFrom(this.facade.editorConfig$)
90
- const pageIndex = config.pages.findIndex((page) =>
91
- page.sections.some((section) =>
92
- section.fields.some((field) => field.model === model)
93
- )
112
+ const page = config.pages.findIndex((p) =>
113
+ p.sections.some((s) => s.fields.some((f) => f.model === model))
114
+ )
115
+ if (page < 0) {
116
+ return null
117
+ }
118
+ const section = config.pages[page].sections
119
+ .filter((s) => !s.hidden)
120
+ .findIndex((s) => s.fields.some((f) => f.model === model))
121
+ return { page, section }
122
+ }
123
+
124
+ private recordSignal = toSignal(this.facade.record$, { requireSync: true })
125
+ private expressionCache = new Map<string, Signal<EditorFieldValue>>()
126
+
127
+ // only compiles the expression into an evaluator the first time;
128
+ // then we store a computed() signal in the cache so that the evaluator is
129
+ // run again if the record changes
130
+ evaluateExpression(
131
+ expressionOrValue: string | EditorFieldValue
132
+ ): Signal<EditorFieldValue> {
133
+ if (!isExpression(expressionOrValue)) {
134
+ return signal(expressionOrValue as EditorFieldValue)
135
+ }
136
+ const expression = expressionOrValue as string
137
+ if (this.expressionCache.has(expression)) {
138
+ return this.expressionCache.get(expression)
139
+ }
140
+ const { evaluator, errors } = evaluate(expression)
141
+ if (errors.length) {
142
+ console.error(`The following errors happened while evaluating the expression '${expression}':
143
+ ${errors.map((err) => `> ${err}`).join('\n')}`)
144
+ this.notifications.showNotification({
145
+ type: 'warning',
146
+ title: 'Technical error',
147
+ text: 'An error happened while evaluating an expression inside the editor configuration; open the developer console for more information',
148
+ })
149
+ }
150
+ this.expressionCache.set(
151
+ expression,
152
+ computed(() => evaluator({ globals: { record: this.recordSignal() } }))
94
153
  )
95
- return pageIndex >= 0 ? pageIndex : null
154
+ return this.expressionCache.get(expression)
96
155
  }
97
156
  }