geonetwork-ui 2.10.0-dev.b5836aad7 → 2.10.0-dev.b6696be21

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 (98) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1610 -1130
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +130 -36
  4. package/index.d.ts.map +1 -1
  5. package/package.json +3 -3
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +9 -0
  7. package/src/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts +2 -0
  8. package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +1 -0
  9. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+ongules.ts +7 -0
  10. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+roilaye.ts +1 -0
  11. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts +1 -0
  12. package/src/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts +1 -0
  13. package/src/libs/api/metadata-converter/src/lib/fixtures/georhena.records.ts +1 -0
  14. package/src/libs/api/metadata-converter/src/lib/fixtures/metadata-for-i18n.records.ts +2 -0
  15. package/src/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +1 -0
  16. package/src/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts +1 -0
  17. package/src/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts +2 -0
  18. package/src/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts +1 -0
  19. package/src/libs/api/metadata-converter/src/lib/fixtures/wallonie.records.reuse.ts +8 -0
  20. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts +1 -0
  21. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +7 -0
  22. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +8 -0
  23. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +8 -0
  24. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +11 -0
  25. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +33 -0
  26. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +33 -0
  27. package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +4 -0
  28. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +68 -34
  29. package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
  30. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +46 -0
  31. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -5
  32. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  33. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  34. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  35. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +5 -0
  36. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +6 -0
  37. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +0 -1
  38. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +10 -1
  39. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  40. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  41. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  42. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.html +18 -3
  43. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.ts +33 -40
  44. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +38 -0
  45. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +33 -34
  46. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +13 -13
  47. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +18 -4
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +37 -0
  55. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
  56. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -0
  57. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  58. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +59 -3
  59. package/src/libs/feature/editor/src/lib/fields.config.ts +60 -47
  60. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
  61. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  62. package/src/libs/feature/notify-reuse/src/index.ts +1 -0
  63. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css +0 -0
  64. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +1 -0
  65. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +21 -0
  66. package/src/libs/ui/elements/src/index.ts +2 -0
  67. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -0
  68. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  69. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +41 -0
  70. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
  71. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
  72. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
  73. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  74. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  75. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +28 -67
  76. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  77. package/src/libs/ui/inputs/src/lib/button/button.component.ts +8 -0
  78. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  79. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  80. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -1
  81. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +11 -10
  82. package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
  83. package/src/libs/util/app-config/src/lib/app-config.ts +49 -0
  84. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  85. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  86. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  87. package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
  88. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  89. package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
  90. package/tailwind.base.css +17 -2
  91. package/translations/de.json +29 -20
  92. package/translations/en.json +29 -20
  93. package/translations/es.json +29 -20
  94. package/translations/fr.json +29 -20
  95. package/translations/it.json +29 -20
  96. package/translations/nl.json +29 -20
  97. package/translations/pt.json +29 -20
  98. package/translations/sk.json +29 -20
@@ -0,0 +1,41 @@
1
+ <gn-ui-button
2
+ [type]="overlayOpen ? 'gray-light' : 'primary-light'"
3
+ extraClass="group w-full min-h-12 gap-3 justify-between py-2 pl-5 pr-4 rounded"
4
+ data-test="contact-pill"
5
+ (buttonClick)="toggleOverlay()"
6
+ cdkOverlayOrigin
7
+ #overlayOrigin="cdkOverlayOrigin"
8
+ >
9
+ <span
10
+ class="font-title font-medium text-base leading-tight truncate"
11
+ [class]="!overlayOpen ? 'text-primary-black group-hover:text-white' : ''"
12
+ [title]="displayName"
13
+ >{{ displayName }}</span
14
+ >
15
+ <div
16
+ class="gn-ui-card-icon items-center justify-center w-10 h-8"
17
+ [class]="
18
+ !overlayOpen ? 'group-hover:border-white group-hover:text-white' : ''
19
+ "
20
+ >
21
+ @if (overlayOpen) {
22
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matClose"></ng-icon>
23
+ } @else {
24
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
25
+ }
26
+ </div>
27
+ </gn-ui-button>
28
+
29
+ <ng-template
30
+ cdkConnectedOverlay
31
+ [cdkConnectedOverlayOrigin]="overlayOrigin"
32
+ [cdkConnectedOverlayOpen]="overlayOpen"
33
+ [cdkConnectedOverlayPositions]="overlayPositions"
34
+ [cdkConnectedOverlayOffsetX]="overlayOffsetX"
35
+ (overlayOutsideClick)="closeOverlay()"
36
+ (detach)="closeOverlay()"
37
+ >
38
+ <div [style.width.px]="overlayWidth">
39
+ <gn-ui-contact-details [contact]="contact"></gn-ui-contact-details>
40
+ </div>
41
+ </ng-template>
@@ -0,0 +1,70 @@
1
+ import { ConnectedPosition, OverlayModule } from '@angular/cdk/overlay'
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ ElementRef,
6
+ inject,
7
+ Input,
8
+ } from '@angular/core'
9
+ import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
10
+ import { NgIcon, provideIcons } from '@ng-icons/core'
11
+ import { matClose } from '@ng-icons/material-icons/baseline'
12
+ import { matInfoOutline } from '@ng-icons/material-icons/outline'
13
+ import { ButtonComponent } from '../../../../../../libs/ui/inputs/src'
14
+ import { getIndividualDisplayName } from '../../../../../../libs/util/shared/src'
15
+ import { ContactDetailsComponent } from '../contact-details/contact-details.component'
16
+
17
+ @Component({
18
+ selector: 'gn-ui-contact-pill',
19
+ templateUrl: './contact-pill.component.html',
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ standalone: true,
22
+ imports: [NgIcon, ButtonComponent, OverlayModule, ContactDetailsComponent],
23
+ viewProviders: [provideIcons({ matClose, matInfoOutline })],
24
+ })
25
+ export class ContactPillComponent {
26
+ @Input() contact: Individual
27
+
28
+ private host = inject(ElementRef<HTMLElement>)
29
+
30
+ overlayOpen = false
31
+ overlayWidth = 0
32
+ overlayOffsetX = 0
33
+ overlayPositions: ConnectedPosition[] = [
34
+ {
35
+ originX: 'start',
36
+ originY: 'bottom',
37
+ overlayX: 'start',
38
+ overlayY: 'top',
39
+ offsetY: 4,
40
+ },
41
+ {
42
+ originX: 'start',
43
+ originY: 'top',
44
+ overlayX: 'start',
45
+ overlayY: 'bottom',
46
+ offsetY: -4,
47
+ },
48
+ ]
49
+
50
+ get displayName(): string {
51
+ return getIndividualDisplayName(this.contact)
52
+ }
53
+
54
+ toggleOverlay() {
55
+ if (!this.overlayOpen) {
56
+ // Calculate the width and horizontal offset of the overlay to align it with the parent element
57
+ const parent =
58
+ this.host.nativeElement.parentElement.getBoundingClientRect()
59
+ const pill = this.host.nativeElement.getBoundingClientRect()
60
+ this.overlayWidth = parent.width
61
+ this.overlayOffsetX = parent.left - pill.left
62
+ }
63
+
64
+ this.overlayOpen = !this.overlayOpen
65
+ }
66
+
67
+ closeOverlay() {
68
+ this.overlayOpen = false
69
+ }
70
+ }
@@ -1,6 +1,6 @@
1
1
  <a
2
2
  [attr.href]="linkHref"
3
- [target]="linkTarget"
3
+ target="_self"
4
4
  class="record-card"
5
5
  [ngClass]="cardClass"
6
6
  >
@@ -58,7 +58,6 @@ export class InternalLinkCardComponent implements OnInit {
58
58
  protected elementRef = inject(ElementRef)
59
59
 
60
60
  @Input() record: CatalogRecord
61
- @Input() linkTarget = '_blank'
62
61
  @Input() linkHref: string = null
63
62
  @Input() metadataQualityDisplay: boolean
64
63
  @Input() favoriteTemplate: TemplateRef<{ $implicit: CatalogRecord }>
@@ -21,7 +21,7 @@ import {
21
21
  matCallOutline,
22
22
  matLocationOnOutline,
23
23
  } from '@ng-icons/material-icons/outline'
24
-
24
+ import { getAddressLines } from '../../../../../../libs/util/shared/src'
25
25
  import { TranslateDirective } from '@ngx-translate/core'
26
26
 
27
27
  @Component({
@@ -59,10 +59,7 @@ export class MetadataContactComponent {
59
59
  }
60
60
 
61
61
  get address() {
62
- const addressParts = this.contacts[0].address
63
- .split(',')
64
- .map((part) => part.trim())
65
- return addressParts
62
+ return getAddressLines(this.contacts[0]?.address)
66
63
  }
67
64
 
68
65
  onOrganizationClick() {
@@ -16,7 +16,3 @@
16
16
  --gn-ui-badge-background-color: var(--color-primary-white);
17
17
  --gn-ui-badge-text-color: var(--color-primary-darkest);
18
18
  }
19
-
20
- :host .metadata-info-keywords ::ng-deep gn-ui-badge:hover {
21
- --gn-ui-badge-text-color: white;
22
- }
@@ -28,11 +28,12 @@
28
28
  <gn-ui-max-lines [maxLines]="7">
29
29
  <div class="metadata-info-keywords sm:pb-4 flex flex-wrap gap-2">
30
30
  @for (keyword of metadata.keywords; track keyword) {
31
- <gn-ui-badge
32
- class="inline-block lowercase"
33
- (click)="onKeywordClick(keyword)"
34
- [clickable]="true"
35
- >{{ keyword.label }}</gn-ui-badge
31
+ <gn-ui-button
32
+ type="primary-light"
33
+ class="inline-block opacity-70"
34
+ extraClass="lowercase text-sm py-1 px-2"
35
+ (buttonClick)="onKeywordClick(keyword)"
36
+ >{{ keyword.label }}</gn-ui-button
36
37
  >
37
38
  }
38
39
  </div>
@@ -52,7 +53,7 @@
52
53
  data-test="usage-panel"
53
54
  >
54
55
  <div class="flex flex-col gap-[10px] mr-4 py-[12px] rounded text-gray-900">
55
- @for (license of licenses; track license) {
56
+ @for (license of licenses; track $index) {
56
57
  @if (license.url) {
57
58
  <div class="text-primary">
58
59
  <a
@@ -105,9 +106,29 @@
105
106
  </div>
106
107
  </gn-ui-expandable-panel>
107
108
  }
109
+ @if (metadata.contactsForResource?.length) {
110
+ <gn-ui-expandable-panel
111
+ [title]="'record.metadata.resource.contacts' | translate"
112
+ data-test="contacts-panel"
113
+ >
114
+ <div class="flex flex-col gap-1 pt-3 pb-4">
115
+ @for (group of contactGroups; track group.role) {
116
+ <div class="flex flex-col gap-1 rounded py-4 px-2">
117
+ <p class="text-xs font-normal text-black">
118
+ {{ group.roleLabel | translate }}
119
+ </p>
120
+ <div class="grid gap-1 grid-cols-1 md:grid-cols-2">
121
+ @for (contact of group.contacts; track contact.email) {
122
+ <gn-ui-contact-pill [contact]="contact"></gn-ui-contact-pill>
123
+ }
124
+ </div>
125
+ </div>
126
+ }
127
+ </div>
128
+ </gn-ui-expandable-panel>
129
+ }
108
130
  @if (
109
131
  (metadata.kind === 'dataset' && metadata.lineage) ||
110
- resourceContact ||
111
132
  metadata.resourceCreated ||
112
133
  metadata.resourcePublished ||
113
134
  metadata.resourceUpdated ||
@@ -129,66 +150,6 @@
129
150
  </p>
130
151
  </div>
131
152
  }
132
- @if (resourceContact) {
133
- <div
134
- class="flex flex-row gap-6 mt-5 mb-8 resource-contact"
135
- data-test="details-panel-resource-contact"
136
- >
137
- @if (resourceContact.organization?.logoUrl?.href) {
138
- <div
139
- class="flex items-center justify-center border-solid border border-gray-300 rounded-md bg-white h-32 overflow-hidden"
140
- >
141
- <gn-ui-thumbnail
142
- class="relative h-full w-full"
143
- [thumbnailUrl]="resourceContact.organization.logoUrl.href"
144
- fit="contain"
145
- ></gn-ui-thumbnail>
146
- </div>
147
- }
148
- <div class="flex flex-col gap-1">
149
- <p class="text-sm font-medium" translate>record.metadata.producer</p>
150
- <div
151
- class="text-primary font-title text-21 mr-2 cursor-pointer hover:underline"
152
- data-cy="organization-name"
153
- >
154
- {{ resourceContact.organization?.name }}
155
- </div>
156
- @if (resourceContact.organization?.website) {
157
- <div>
158
- <a
159
- [href]="resourceContact.organization.website"
160
- target="_blank"
161
- class="contact-website text-primary text-sm cursor-pointer hover:underline transition-all"
162
- >{{ resourceContact.organization.website }}
163
- <ng-icon
164
- class="!w-[12px] !h-[12px] !text-[12px] opacity-75 shrink-0"
165
- name="matOpenInNew"
166
- ></ng-icon>
167
- </a>
168
- </div>
169
- }
170
- @if (resourceContact.email) {
171
- <div class="mt-4">
172
- <div class="flex">
173
- <ng-icon
174
- class="!w-5 !h-5 !text-[20px] opacity-75 shrink-0"
175
- name="matMailOutline"
176
- ></ng-icon>
177
- @if (resourceContact.email) {
178
- <a
179
- [href]="'mailto:' + resourceContact.email"
180
- class="text-sm hover:underline ml-2"
181
- target="_blank"
182
- data-cy="contact-email"
183
- >{{ resourceContact?.email }}</a
184
- >
185
- }
186
- </div>
187
- </div>
188
- }
189
- </div>
190
- </div>
191
- }
192
153
  <div
193
154
  class="py-6 px-6 rounded bg-gray-100 grid grid-cols-2 gap-y-6 gap-x-[20px] text-gray-700"
194
155
  >
@@ -8,7 +8,10 @@ import {
8
8
  } from '@angular/core'
9
9
  import {
10
10
  CatalogRecord,
11
+ Individual,
11
12
  Keyword,
13
+ Role,
14
+ RoleLabels,
12
15
  } from '../../../../../../libs/common/domain/src/lib/model/record'
13
16
  import { DateService, getTemporalRangeUnion } from '../../../../../../libs/util/shared/src'
14
17
  import { MarkdownParserComponent } from '../markdown-parser/markdown-parser.component'
@@ -19,17 +22,18 @@ import {
19
22
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
20
23
  import {
21
24
  BadgeComponent,
25
+ ButtonComponent,
22
26
  CopyTextButtonComponent,
23
27
  } from '../../../../../../libs/ui/inputs/src'
24
28
  import { ContentGhostComponent } from '../content-ghost/content-ghost.component'
25
29
  import { NgIcon, provideIcons } from '@ng-icons/core'
26
30
  import { matOpenInNew } from '@ng-icons/material-icons/baseline'
27
31
  import { matMailOutline } from '@ng-icons/material-icons/outline'
28
- import { ThumbnailComponent } from '../thumbnail/thumbnail.component'
29
32
  import { GnUiLinkifyDirective } from './linkify.directive'
30
33
  import { GnUiHumanizeDateDirective } from '../../../../../../libs/util/shared/src'
31
34
 
32
35
  import { SpatialExtentComponent } from '../../../../../../libs/ui/map/src'
36
+ import { ContactPillComponent } from '../contact-pill/contact-pill.component'
33
37
 
34
38
  @Component({
35
39
  selector: 'gn-ui-metadata-info',
@@ -42,15 +46,16 @@ import { SpatialExtentComponent } from '../../../../../../libs/ui/map/src'
42
46
  TranslatePipe,
43
47
  MarkdownParserComponent,
44
48
  ExpandablePanelComponent,
49
+ ButtonComponent,
45
50
  BadgeComponent,
46
51
  ContentGhostComponent,
47
- ThumbnailComponent,
48
52
  MaxLinesComponent,
49
53
  CopyTextButtonComponent,
50
54
  NgIcon,
51
55
  GnUiLinkifyDirective,
52
56
  GnUiHumanizeDateDirective,
53
57
  SpatialExtentComponent,
58
+ ContactPillComponent,
54
59
  ],
55
60
  viewProviders: [
56
61
  provideIcons({
@@ -132,18 +137,33 @@ export class MetadataInfoComponent {
132
137
  return getTemporalRangeUnion(temporalExtents, this.dateService)
133
138
  }
134
139
 
135
- get shownOrganization() {
136
- return this.metadata.ownerOrganization
137
- }
138
-
139
- get resourceContact() {
140
- return this.metadata.contactsForResource?.[0]
141
- }
142
-
143
140
  fieldReady(propName: string) {
144
141
  return !this.incomplete || propName in this.metadata
145
142
  }
146
143
 
144
+ get contactGroups(): {
145
+ role: Role
146
+ roleLabel: string
147
+ contacts: Individual[]
148
+ }[] {
149
+ const groups: { role: Role; roleLabel: string; contacts: Individual[] }[] =
150
+ []
151
+ const indexByRole = new Map<Role, number>()
152
+ for (const contact of this.metadata.contactsForResource ?? []) {
153
+ if (indexByRole.has(contact.role)) {
154
+ groups[indexByRole.get(contact.role)].contacts.push(contact)
155
+ } else {
156
+ indexByRole.set(contact.role, groups.length)
157
+ groups.push({
158
+ role: contact.role,
159
+ roleLabel: RoleLabels.get(contact.role),
160
+ contacts: [contact],
161
+ })
162
+ }
163
+ }
164
+ return groups
165
+ }
166
+
147
167
  onKeywordClick(keyword: Keyword) {
148
168
  this.keyword.emit(keyword)
149
169
  }
@@ -25,7 +25,9 @@ export class ButtonComponent {
25
25
  | 'outline'
26
26
  | 'light'
27
27
  | 'gray'
28
+ | 'gray-light'
28
29
  | 'black'
30
+ | 'primary-light'
29
31
  ) {
30
32
  // btn-classes are written in full to be picked up by tailwind
31
33
  switch (value) {
@@ -44,9 +46,15 @@ export class ButtonComponent {
44
46
  case 'gray':
45
47
  this.btnClass = 'gn-ui-btn-gray'
46
48
  break
49
+ case 'gray-light':
50
+ this.btnClass = 'gn-ui-btn-gray-light'
51
+ break
47
52
  case 'black':
48
53
  this.btnClass = 'gn-ui-btn-black'
49
54
  break
55
+ case 'primary-light':
56
+ this.btnClass = 'gn-ui-btn-primary-light'
57
+ break
50
58
  case 'default':
51
59
  default:
52
60
  this.btnClass = 'gn-ui-btn-default'
@@ -6,7 +6,7 @@
6
6
  type="url"
7
7
  [value]="inputValue"
8
8
  (input)="handleInput($event)"
9
- (keydown.enter)="handleUpload(input)"
9
+ (keydown.enter)="handleUpload(input, $event)"
10
10
  [placeholder]="placeholder"
11
11
  [attr.aria-label]="placeholder"
12
12
  [disabled]="disabled"
@@ -25,7 +25,7 @@
25
25
  extraClass="absolute inset-y-[var(--side-padding)] right-[var(--side-padding)]"
26
26
  type="primary"
27
27
  [disabled]="disabled || input.value === '' || !isValidUrl(input.value)"
28
- (buttonClick)="handleUpload(input)"
28
+ (buttonClick)="handleUpload(input, $event)"
29
29
  >
30
30
  <ng-content>
31
31
  <ng-icon name="iconoirArrowUp"></ng-icon>
@@ -81,7 +81,8 @@ export class UrlInputComponent implements OnChanges {
81
81
  this.valueChange.next(value)
82
82
  }
83
83
 
84
- handleUpload(element: HTMLInputElement) {
84
+ handleUpload(element: HTMLInputElement, event: Event) {
85
+ event.stopPropagation()
85
86
  const value = element.value
86
87
  if (!value || !this.isValidUrl(value)) return
87
88
  this.uploadClick.next(value)
@@ -204,11 +204,12 @@ export class MapContainerComponent implements AfterViewInit, OnChanges {
204
204
 
205
205
  async ngOnChanges(changes: SimpleChanges) {
206
206
  if ('context' in changes && !changes['context'].isFirstChange()) {
207
+ const olMap = await this.openlayersMap
207
208
  const diff = computeMapContextDiff(
208
209
  this.processContext(changes['context'].currentValue),
209
210
  this.processContext(changes['context'].previousValue)
210
211
  )
211
- await applyContextDiffToMap(this.olMap, diff)
212
+ await applyContextDiffToMap(olMap, diff)
212
213
 
213
214
  if (this._resolvedExtentChange && diff.viewChanges) {
214
215
  this._resolvedExtentChange.emit(this.calculateCurrentMapExtent())
@@ -1,4 +1,4 @@
1
- import { Component, Input } from '@angular/core'
1
+ import { ChangeDetectorRef, Component, inject, Input } from '@angular/core'
2
2
  import { CommonModule } from '@angular/common'
3
3
  import { Geometry } from 'geojson'
4
4
  import { GeoJSONFeatureCollection } from 'ol/format/GeoJSON.js'
@@ -10,8 +10,8 @@ import {
10
10
  MapContextLayer,
11
11
  } from '@geospatial-sdk/core'
12
12
  import { MapContainerComponent } from '../map-container/map-container.component'
13
- import { BehaviorSubject, Observable } from 'rxjs'
14
- import { switchMap } from 'rxjs/operators'
13
+ import { BehaviorSubject, from, Observable, of } from 'rxjs'
14
+ import { map, switchMap, tap } from 'rxjs/operators'
15
15
  import { DatasetSpatialExtent } from '../../../../../../../libs/common/domain/src/lib/model/record'
16
16
 
17
17
  @Component({
@@ -22,14 +22,16 @@ import { DatasetSpatialExtent } from '../../../../../../../libs/common/domain/sr
22
22
  styleUrl: './spatial-extent.component.css',
23
23
  })
24
24
  export class SpatialExtentComponent {
25
+ private _cdr = inject(ChangeDetectorRef)
26
+
25
27
  @Input() set spatialExtents(value: DatasetSpatialExtent[]) {
26
28
  this.spatialExtents$.next(value)
27
29
  }
28
30
  spatialExtents$ = new BehaviorSubject<DatasetSpatialExtent[]>([])
29
31
  mapContext$: Observable<MapContext> = this.spatialExtents$.pipe(
30
- switchMap(async (extents) => {
32
+ switchMap((extents) => {
31
33
  if (extents.length === 0) {
32
- return null // null extent means default view
34
+ return of(null)
33
35
  }
34
36
  const featureCollection: GeoJSONFeatureCollection = {
35
37
  type: 'FeatureCollection',
@@ -61,11 +63,10 @@ export class SpatialExtentComponent {
61
63
  'fill-color': 'rgba(153, 153, 153, 0.3)',
62
64
  },
63
65
  }
64
- const view = await createViewFromLayer(layer)
65
- return {
66
- view,
67
- layers: [layer],
68
- }
66
+ return from(createViewFromLayer(layer)).pipe(
67
+ map((view) => ({ view, layers: [layer] }) as MapContext),
68
+ tap(() => this._cdr.markForCheck())
69
+ )
69
70
  })
70
71
  )
71
72
 
@@ -1,6 +1,5 @@
1
1
  <gn-ui-internal-link-card
2
2
  [linkHref]="linkHref"
3
- [linkTarget]="linkTarget"
4
3
  [record]="record"
5
4
  [favoriteTemplate]="favoriteTemplate"
6
5
  [metadataQualityDisplay]="metadataQualityDisplay"
@@ -1,6 +1,8 @@
1
1
  import * as TOML from '@ltd/j-toml'
2
2
  import {
3
3
  checkMetadataLanguage,
4
+ checkNewRecordDefaultLanguage,
5
+ checkNewRecordStandard,
4
6
  parseConfigSection,
5
7
  parseMultiConfigSection,
6
8
  parseTranslationsConfigSection,
@@ -8,6 +10,7 @@ import {
8
10
  import {
9
11
  CustomTranslations,
10
12
  CustomTranslationsAllLanguages,
13
+ EditorConfig,
11
14
  GlobalConfig,
12
15
  LayerConfig,
13
16
  MapConfig,
@@ -51,6 +54,12 @@ export function getOptionalSearchConfig(): SearchConfig | null {
51
54
  return searchConfig
52
55
  }
53
56
 
57
+ let editorConfig: EditorConfig = null
58
+
59
+ export function getOptionalEditorConfig(): EditorConfig | null {
60
+ return editorConfig
61
+ }
62
+
54
63
  let metadataQualityConfig: MetadataQualityConfig = null
55
64
  export function getMetadataQualityConfig(): MetadataQualityConfig {
56
65
  return (
@@ -285,6 +294,45 @@ export function loadAppConfig(configUrl = 'assets/configuration/default.toml') {
285
294
  SORTABLE: parsedMetadataQualitySection.sortable,
286
295
  } as MetadataQualityConfig)
287
296
 
297
+ let parsedEditingSection = parseConfigSection(
298
+ parsed,
299
+ 'editing',
300
+ [],
301
+ ['new_record_default_language', 'new_record_standard'],
302
+ warnings,
303
+ errors
304
+ )
305
+ if (
306
+ parsedEditingSection !== null &&
307
+ parsedEditingSection.new_record_default_language !== undefined
308
+ ) {
309
+ parsedEditingSection = checkNewRecordDefaultLanguage(
310
+ parsedEditingSection,
311
+ warnings
312
+ )
313
+ }
314
+ if (
315
+ parsedEditingSection !== null &&
316
+ parsedEditingSection.new_record_standard !== undefined
317
+ ) {
318
+ parsedEditingSection = checkNewRecordStandard(
319
+ parsedEditingSection,
320
+ warnings
321
+ )
322
+ }
323
+ editorConfig =
324
+ parsedEditingSection === null
325
+ ? null
326
+ : ({
327
+ NEW_RECORD_DEFAULT_LANGUAGE:
328
+ parsedEditingSection.new_record_default_language as
329
+ | string
330
+ | undefined,
331
+ NEW_RECORD_STANDARD: parsedEditingSection.new_record_standard as
332
+ | EditorConfig['NEW_RECORD_STANDARD']
333
+ | undefined,
334
+ } as EditorConfig)
335
+
288
336
  customTranslations = parseTranslationsConfigSection(
289
337
  parsed,
290
338
  'translations'
@@ -309,6 +357,7 @@ export function isConfigLoaded() {
309
357
  export function _reset() {
310
358
  globalConfig = null
311
359
  themeConfig = null
360
+ editorConfig = null
312
361
  customTranslations = null
313
362
  }
314
363
 
@@ -69,6 +69,13 @@ export interface MetadataQualityConfig {
69
69
  ENABLED: boolean
70
70
  }
71
71
 
72
+ export type NewRecordStandard = 'iso19139' | 'iso19115-3'
73
+
74
+ export interface EditorConfig {
75
+ NEW_RECORD_DEFAULT_LANGUAGE?: string
76
+ NEW_RECORD_STANDARD?: NewRecordStandard
77
+ }
78
+
72
79
  export type CustomTranslations = { [translationKey: string]: string }
73
80
  export type CustomTranslationsAllLanguages = {
74
81
  [lang: string]: CustomTranslations
@@ -1,4 +1,4 @@
1
- import { toLang2 } from '../../../../../libs/util/i18n/src'
1
+ import { LANG_2_TO_3_MAPPER, toLang2 } from '../../../../../libs/util/i18n/src'
2
2
 
3
3
  const flatten = (
4
4
  base: string,
@@ -137,3 +137,52 @@ export function checkMetadataLanguage(
137
137
  }
138
138
  return parsedConfigSection
139
139
  }
140
+
141
+ export function checkNewRecordDefaultLanguage(
142
+ parsedConfigSection: any,
143
+ outWarnings: string[]
144
+ ) {
145
+ const lang2 = toLang2(
146
+ parsedConfigSection.new_record_default_language.toLowerCase()
147
+ )
148
+ if (!(lang2 in LANG_2_TO_3_MAPPER)) {
149
+ outWarnings.push(
150
+ `In the [editing] section: new_record_default_language = "${parsedConfigSection.new_record_default_language}" is not a recognized ISO 639 language code`
151
+ )
152
+ return {
153
+ ...parsedConfigSection,
154
+ new_record_default_language: undefined,
155
+ }
156
+ }
157
+ return {
158
+ ...parsedConfigSection,
159
+ new_record_default_language: lang2,
160
+ }
161
+ }
162
+
163
+ export function checkNewRecordStandard(
164
+ parsedConfigSection: any,
165
+ outWarnings: string[]
166
+ ) {
167
+ const standard = parsedConfigSection.new_record_standard
168
+ const normalizedStandard =
169
+ typeof standard === 'string' ? standard.trim().toLowerCase() : null
170
+
171
+ if (
172
+ normalizedStandard === 'iso19139' ||
173
+ normalizedStandard === 'iso19115-3'
174
+ ) {
175
+ return {
176
+ ...parsedConfigSection,
177
+ new_record_standard: normalizedStandard,
178
+ }
179
+ }
180
+
181
+ outWarnings.push(
182
+ `In the [editing] section: new_record_standard = "${standard}" is not a supported metadata standard`
183
+ )
184
+ return {
185
+ ...parsedConfigSection,
186
+ new_record_standard: undefined,
187
+ }
188
+ }
@@ -266,7 +266,7 @@ export function checkFileFormat(
266
266
  new RegExp(`[./]${format}`, 'i').test(link.name.toLowerCase())) ||
267
267
  ('url' in link &&
268
268
  new RegExp(`[./]${format}`, 'i').test(link.url.toString())) ||
269
- ('name' in link && link.name.toLowerCase().includes(format))
269
+ ('name' in link && new RegExp(`\\b${format}\\b`, 'i').test(link.name))
270
270
  )
271
271
  }
272
272