geonetwork-ui 2.10.0-dev.896c4b637 → 2.10.0-dev.91673b3ba

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 (33) hide show
  1. package/fesm2022/geonetwork-ui.mjs +916 -834
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +22 -7
  4. package/index.d.ts.map +1 -1
  5. package/package.json +2 -2
  6. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +14 -2
  7. package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
  8. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +0 -1
  9. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +8 -15
  10. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +6 -4
  11. 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
  12. 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
  13. package/src/libs/ui/elements/src/index.ts +1 -0
  14. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +16 -0
  15. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +26 -0
  16. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
  17. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
  18. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  19. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +27 -66
  20. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  21. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
  22. package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
  23. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  24. package/src/libs/util/shared/src/lib/utils/user-display.ts +23 -0
  25. package/tailwind.base.css +6 -0
  26. package/translations/de.json +1 -0
  27. package/translations/en.json +1 -0
  28. package/translations/es.json +1 -0
  29. package/translations/fr.json +1 -0
  30. package/translations/it.json +1 -0
  31. package/translations/nl.json +1 -0
  32. package/translations/pt.json +1 -0
  33. package/translations/sk.json +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geonetwork-ui",
3
- "version": "2.10.0-dev.896c4b637",
3
+ "version": "2.10.0-dev.91673b3ba",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@biesbjerg/ngx-translate-extract-marker": "~1.0.0",
50
- "@camptocamp/ogc-client": "1.3.1-dev.12086e8",
50
+ "@camptocamp/ogc-client": "1.3.1-dev.afd9c26",
51
51
  "@geospatial-sdk/core": "0.0.5-dev.61",
52
52
  "@geospatial-sdk/geocoding": "0.0.5-dev.61",
53
53
  "@geospatial-sdk/legend": "0.0.5-dev.61",
@@ -3,7 +3,7 @@ import {
3
3
  HttpErrorResponse,
4
4
  HttpHeaders,
5
5
  } from '@angular/common/http'
6
- import { Injectable, inject } from '@angular/core'
6
+ import { Injectable, InjectionToken, inject } from '@angular/core'
7
7
  import {
8
8
  assertValidXml,
9
9
  findConverterForDocument,
@@ -57,6 +57,10 @@ const TEMPORARY_ID_PREFIX = 'TEMP-ID-'
57
57
 
58
58
  export type RecordAsXml = string
59
59
 
60
+ export const DISABLE_DRAFT = new InjectionToken<boolean>('gnDisableDraft', {
61
+ factory: () => false,
62
+ })
63
+
60
64
  @Injectable()
61
65
  export class Gn4Repository implements RecordsRepositoryInterface {
62
66
  private httpClient = inject(HttpClient)
@@ -67,6 +71,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
67
71
  private platformService = inject(PlatformServiceInterface)
68
72
  private gn4LanguagesApi = inject(LanguagesApiService)
69
73
  private settingsService = inject(Gn4SettingsService)
74
+ private disableDraft = inject(DISABLE_DRAFT, { optional: true }) ?? false
70
75
 
71
76
  _draftsChanged = new Subject<void>()
72
77
  draftsChanged$ = this._draftsChanged.asObservable()
@@ -350,7 +355,9 @@ export class Gn4Repository implements RecordsRepositoryInterface {
350
355
  openRecordForEdition(
351
356
  uniqueIdentifier: string
352
357
  ): Observable<[CatalogRecord, string, boolean] | null> {
353
- const draft$ = of(this.getRecordFromLocalStorage(uniqueIdentifier))
358
+ const draft$ = this.disableDraft
359
+ ? of(null)
360
+ : of(this.getRecordFromLocalStorage(uniqueIdentifier))
354
361
  const recordAsXml$ = this.getRecordAsXml(uniqueIdentifier)
355
362
 
356
363
  return combineLatest([draft$, recordAsXml$]).pipe(
@@ -475,6 +482,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
475
482
  record: CatalogRecord,
476
483
  referenceRecordSource?: string
477
484
  ): Observable<string> {
485
+ if (this.disableDraft) return of('')
478
486
  return this.serializeRecordToXml(record, referenceRecordSource).pipe(
479
487
  tap((recordXml) => {
480
488
  this.saveRecordToLocalStorage(recordXml, record.uniqueIdentifier)
@@ -484,16 +492,19 @@ export class Gn4Repository implements RecordsRepositoryInterface {
484
492
  }
485
493
 
486
494
  clearRecordDraft(uniqueIdentifier: string): void {
495
+ if (this.disableDraft) return
487
496
  this.removeRecordFromLocalStorage(uniqueIdentifier)
488
497
  this._draftsChanged.next()
489
498
  }
490
499
 
491
500
  recordHasDraft(uniqueIdentifier: string): boolean {
501
+ if (this.disableDraft) return false
492
502
  return this.getRecordFromLocalStorage(uniqueIdentifier) !== null
493
503
  }
494
504
 
495
505
  // generated by copilot
496
506
  getAllDrafts(): Observable<CatalogRecord[]> {
507
+ if (this.disableDraft) return of([])
497
508
  const items = { ...window.localStorage }
498
509
  const drafts = Object.keys(items)
499
510
  .filter((key) => key.startsWith('geonetwork-ui-draft-'))
@@ -509,6 +520,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
509
520
  }
510
521
 
511
522
  getDraftsCount(): Observable<number> {
523
+ if (this.disableDraft) return of(0)
512
524
  const items = { ...window.localStorage }
513
525
  const draftCount = Object.keys(items)
514
526
  .filter((key) => key.startsWith('geonetwork-ui-draft-'))
@@ -6,7 +6,7 @@ import {
6
6
  } from './platform/gn4-platform.service'
7
7
  import { Gn4PlatformMapper } from './platform/gn4-platform.mapper'
8
8
  import { RecordsRepositoryInterface } from '../../../../../../libs/common/domain/src/lib/repository/records-repository.interface'
9
- import { Gn4Repository } from './gn4-repository'
9
+ import { Gn4Repository, DISABLE_DRAFT } from './gn4-repository'
10
10
  import { AvatarServiceInterface, GravatarService } from './auth'
11
11
  import { OrganizationsServiceInterface } from '../../../../../../libs/common/domain/src/lib/organizations.service.interface'
12
12
  import {
@@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core'
22
22
 
23
23
  interface Gn4ProvideOptions {
24
24
  disableAuth?: boolean
25
+ disableDraft?: boolean
25
26
  }
26
27
 
27
28
  export function provideGn4(provideOptions?: Gn4ProvideOptions): Provider[] {
@@ -30,6 +31,10 @@ export function provideGn4(provideOptions?: Gn4ProvideOptions): Provider[] {
30
31
  provide: DISABLE_AUTH,
31
32
  useValue: provideOptions?.disableAuth,
32
33
  },
34
+ {
35
+ provide: DISABLE_DRAFT,
36
+ useValue: provideOptions?.disableDraft,
37
+ },
33
38
  {
34
39
  provide: PlatformServiceInterface,
35
40
  useClass: Gn4PlatformService,
@@ -12,7 +12,6 @@ import {
12
12
  } from './editor.selectors'
13
13
  import { RecordsRepositoryInterface } from '../../../../../../libs/common/domain/src/lib/repository/records-repository.interface'
14
14
  import { PlatformServiceInterface } from '../../../../../../libs/common/domain/src/lib/platform.service.interface'
15
-
16
15
  @Injectable()
17
16
  export class EditorEffects {
18
17
  private actions$ = inject(Actions)
@@ -10,21 +10,14 @@
10
10
  </gn-ui-autocomplete>
11
11
 
12
12
  @if (contacts.length > 0) {
13
- @if (contacts.length === 1) {
14
- @for (contact of contacts; track contact; let index = $index) {
15
- <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
16
- }
17
- }
18
- @if (contacts.length > 1) {
19
- <gn-ui-sortable-list
20
- [items]="contacts"
21
- (itemsOrderChange)="handleContactsChanged($event)"
22
- [elementTemplate]="contactTemplate"
23
- ></gn-ui-sortable-list>
24
- <ng-template #contactTemplate let-contact>
25
- <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
26
- </ng-template>
27
- }
13
+ <gn-ui-sortable-list
14
+ [items]="contacts"
15
+ (itemsOrderChange)="handleContactsChanged($event)"
16
+ [elementTemplate]="contactTemplate"
17
+ ></gn-ui-sortable-list>
18
+ <ng-template #contactTemplate let-contact>
19
+ <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
20
+ </ng-template>
28
21
  } @else {
29
22
  <div
30
23
  class="p-4 text-sm border border-primary bg-primary-lightest rounded-lg"
@@ -28,7 +28,11 @@ import { UserModel } from '../../../../../../../../../libs/common/domain/src/lib
28
28
  import { PlatformServiceInterface } from '../../../../../../../../../libs/common/domain/src/lib/platform.service.interface'
29
29
  import { OrganizationsServiceInterface } from '../../../../../../../../../libs/common/domain/src/lib/organizations.service.interface'
30
30
  import { ContactCardComponent } from '../../../contact-card/contact-card.component'
31
- import { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
31
+ import {
32
+ createFuzzyFilter,
33
+ getIndividualDisplayName,
34
+ toIndividual,
35
+ } from '../../../../../../../../../libs/util/shared/src'
32
36
  import { map } from 'rxjs/operators'
33
37
  import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
34
38
 
@@ -117,9 +121,7 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
117
121
  * gn-ui-autocomplete
118
122
  */
119
123
  displayWithFn: (user: UserModel) => string = (user) =>
120
- `${user.name} ${user.surname} ${
121
- user.organisation ? `(${user.organisation})` : ''
122
- }`
124
+ getIndividualDisplayName(toIndividual(user))
123
125
 
124
126
  /**
125
127
  * gn-ui-autocomplete
@@ -8,6 +8,14 @@
8
8
  </gn-ui-button>
9
9
  }
10
10
  </div>
11
+ @if (value.length === 0) {
12
+ <div
13
+ class="p-4 border border-primary bg-primary-lightest rounded-lg"
14
+ translate
15
+ >
16
+ editor.record.form.field.contactsForResource.noContact
17
+ </div>
18
+ }
11
19
  @if (roleSectionsToDisplay && roleSectionsToDisplay.length > 0) {
12
20
  <div class="mt-8" data-test="displayedRoles">
13
21
  @for (
@@ -53,12 +61,5 @@
53
61
  </div>
54
62
  }
55
63
  </div>
56
- } @else {
57
- <div
58
- class="p-4 border border-primary bg-primary-lightest rounded-lg"
59
- translate
60
- >
61
- editor.record.form.field.contactsForResource.noContact
62
- </div>
63
64
  }
64
65
  </div>
@@ -23,7 +23,11 @@ import {
23
23
  AutocompleteComponent,
24
24
  ButtonComponent,
25
25
  } from '../../../../../../../../../libs/ui/inputs/src'
26
- import { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
26
+ import {
27
+ createFuzzyFilter,
28
+ getIndividualDisplayName,
29
+ toIndividual,
30
+ } from '../../../../../../../../../libs/util/shared/src'
27
31
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
28
32
  import {
29
33
  debounceTime,
@@ -169,11 +173,7 @@ export class FormFieldContactsForResourceComponent
169
173
  * gn-ui-autocomplete
170
174
  */
171
175
  displayWithFn: (user: UserModel) => string = (user) =>
172
- user.name
173
- ? `${user.name} ${user.surname} ${
174
- user.organisation ? `(${user.organisation})` : ''
175
- }`
176
- : ``
176
+ getIndividualDisplayName(toIndividual(user))
177
177
 
178
178
  /**
179
179
  * gn-ui-autocomplete
@@ -18,6 +18,7 @@ export * from './lib/metadata-doi/metadata-doi.component'
18
18
  export * from './lib/metadata-info/metadata-info.component'
19
19
  export * from './lib/metadata-quality-item/metadata-quality-item.component'
20
20
  export * from './lib/metadata-quality/metadata-quality.component'
21
+ export * from './lib/contact-pill/contact-pill.component'
21
22
  export * from './lib/notification/notification.component'
22
23
  export * from './lib/record-api-form/record-api-form.component'
23
24
  export * from './lib/thumbnail/thumbnail.component'
@@ -0,0 +1,16 @@
1
+ <gn-ui-button
2
+ type="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
+ >
6
+ <span
7
+ class="font-title font-medium text-base leading-tight truncate group-hover:text-white"
8
+ [title]="displayName"
9
+ >{{ displayName }}</span
10
+ >
11
+ <div
12
+ class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
13
+ >
14
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
15
+ </div>
16
+ </gn-ui-button>
@@ -0,0 +1,26 @@
1
+ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
2
+ import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
3
+ import { NgIcon, provideIcons } from '@ng-icons/core'
4
+ import { matInfoOutline } from '@ng-icons/material-icons/outline'
5
+ import { ButtonComponent } from '../../../../../../libs/ui/inputs/src'
6
+ import { getIndividualDisplayName } from '../../../../../../libs/util/shared/src'
7
+
8
+ @Component({
9
+ selector: 'gn-ui-contact-pill',
10
+ templateUrl: './contact-pill.component.html',
11
+ changeDetection: ChangeDetectionStrategy.OnPush,
12
+ standalone: true,
13
+ imports: [NgIcon, ButtonComponent],
14
+ viewProviders: [
15
+ provideIcons({
16
+ matInfoOutline,
17
+ }),
18
+ ],
19
+ })
20
+ export class ContactPillComponent {
21
+ @Input() contact: Individual
22
+
23
+ get displayName(): string {
24
+ return getIndividualDisplayName(this.contact)
25
+ }
26
+ }
@@ -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 }>
@@ -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>
@@ -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 bg-gray-50 py-4 px-2">
117
+ <p class="text-xs font-normal text-black">
118
+ {{ group.roleLabel | translate }}
119
+ </p>
120
+ <div class="grid gap-0.5 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
  }
@@ -26,6 +26,7 @@ export class ButtonComponent {
26
26
  | 'light'
27
27
  | 'gray'
28
28
  | 'black'
29
+ | 'primary-light'
29
30
  ) {
30
31
  // btn-classes are written in full to be picked up by tailwind
31
32
  switch (value) {
@@ -47,6 +48,9 @@ export class ButtonComponent {
47
48
  case 'black':
48
49
  this.btnClass = 'gn-ui-btn-black'
49
50
  break
51
+ case 'primary-light':
52
+ this.btnClass = 'gn-ui-btn-primary-light'
53
+ break
50
54
  case 'default':
51
55
  default:
52
56
  this.btnClass = 'gn-ui-btn-default'
@@ -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"
@@ -13,3 +13,4 @@ export * from './sort-by'
13
13
  export * from './strip-html'
14
14
  export * from './temporal-extent-union'
15
15
  export * from './url'
16
+ export * from './user-display'
@@ -0,0 +1,23 @@
1
+ import { UserModel } from '../../../../../../libs/common/domain/src/lib/model/user'
2
+ import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
3
+
4
+ export function getIndividualDisplayName(individual: Individual): string {
5
+ const nameParts = [individual.firstName, individual.lastName]
6
+ .filter(Boolean)
7
+ .join(' ')
8
+ const orgPart = individual.organization?.name
9
+ ? ` (${individual.organization.name})`
10
+ : ''
11
+ if (nameParts) return `${nameParts}${orgPart}`
12
+ return individual.organization?.name ?? individual.email ?? ''
13
+ }
14
+
15
+ export function toIndividual(user: UserModel): Individual {
16
+ return {
17
+ firstName: user.name,
18
+ lastName: user.surname,
19
+ email: user.email,
20
+ role: 'unspecified',
21
+ organization: user.organisation ? { name: user.organisation } : undefined,
22
+ }
23
+ }
package/tailwind.base.css CHANGED
@@ -134,6 +134,12 @@
134
134
  bg-white border-white focus:ring-4 focus:ring-gray-300;
135
135
  }
136
136
 
137
+ .gn-ui-btn-primary-light {
138
+ @apply gn-ui-btn text-primary-darkest
139
+ bg-primary-white hover:bg-primary focus:bg-primary hover:text-white focus:text-white active:bg-primary-darker
140
+ border-transparent;
141
+ }
142
+
137
143
  .gn-ui-card-s {
138
144
  @apply min-h-[64px] w-full xs:min-w-[260px] xs:max-w-[280px] sm:min-w-[300px] sm:max-w-[500px] md:min-w-[230px];
139
145
  }
@@ -520,6 +520,7 @@
520
520
  "record.metadata.quality.updateFrequency.failed": "Aktualisierungsfrequenz nicht angegeben",
521
521
  "record.metadata.quality.updateFrequency.success": "Aktualisierungsfrequenz angegeben",
522
522
  "record.metadata.related": "Entdecken Sie den Katalog",
523
+ "record.metadata.resource.contacts": "Kontakte zur Ressource",
523
524
  "record.metadata.resourceCreated": "Erstellt",
524
525
  "record.metadata.resourcePublished": "Veröffentlicht",
525
526
  "record.metadata.resourceUpdated": "Zuletzt aktualisiert",
@@ -520,6 +520,7 @@
520
520
  "record.metadata.quality.updateFrequency.failed": "Update frequency is not specified",
521
521
  "record.metadata.quality.updateFrequency.success": "Update frequency is specified",
522
522
  "record.metadata.related": "Explore the catalog",
523
+ "record.metadata.resource.contacts": "Contacts for the resource",
523
524
  "record.metadata.resourceCreated": "Created",
524
525
  "record.metadata.resourcePublished": "Published",
525
526
  "record.metadata.resourceUpdated": "Last updated",
@@ -520,6 +520,7 @@
520
520
  "record.metadata.quality.updateFrequency.failed": "",
521
521
  "record.metadata.quality.updateFrequency.success": "",
522
522
  "record.metadata.related": "",
523
+ "record.metadata.resource.contacts": "",
523
524
  "record.metadata.resourceCreated": "",
524
525
  "record.metadata.resourcePublished": "",
525
526
  "record.metadata.resourceUpdated": "",
@@ -520,6 +520,7 @@
520
520
  "record.metadata.quality.updateFrequency.failed": "La fréquence de mise à jour n'est pas renseignée",
521
521
  "record.metadata.quality.updateFrequency.success": "La fréquence de mise à jour est renseignée",
522
522
  "record.metadata.related": "Explorez le catalogue",
523
+ "record.metadata.resource.contacts": "Contacts liés à la donnée",
523
524
  "record.metadata.resourceCreated": "Créé",
524
525
  "record.metadata.resourcePublished": "Publié",
525
526
  "record.metadata.resourceUpdated": "Mis à jour",
@@ -520,6 +520,7 @@
520
520
  "record.metadata.quality.updateFrequency.failed": "La frequenza di aggiornamento non è specificata",
521
521
  "record.metadata.quality.updateFrequency.success": "La frequenza di aggiornamento è specificata",
522
522
  "record.metadata.related": "Vedi anche",
523
+ "record.metadata.resource.contacts": "Contatti per la risorsa",
523
524
  "record.metadata.resourceCreated": "Creato",
524
525
  "record.metadata.resourcePublished": "Pubblicato",
525
526
  "record.metadata.resourceUpdated": "Ultimo aggiornamento",
@@ -520,6 +520,7 @@
520
520
  "record.metadata.quality.updateFrequency.failed": "",
521
521
  "record.metadata.quality.updateFrequency.success": "",
522
522
  "record.metadata.related": "",
523
+ "record.metadata.resource.contacts": "",
523
524
  "record.metadata.resourceCreated": "",
524
525
  "record.metadata.resourcePublished": "",
525
526
  "record.metadata.resourceUpdated": "",