geonetwork-ui 2.10.0-dev.e40bfbf0e → 2.10.0-dev.e7b9cb6a0

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 (96) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1401 -934
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +124 -30
  4. package/index.d.ts.map +1 -1
  5. package/package.json +2 -2
  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 -0
  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.component.css +37 -0
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
  55. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -0
  56. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  57. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +59 -3
  58. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
  59. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  60. package/src/libs/feature/notify-reuse/src/index.ts +1 -0
  61. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css +0 -0
  62. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +1 -0
  63. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +21 -0
  64. package/src/libs/ui/elements/src/index.ts +2 -0
  65. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -0
  66. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  67. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +37 -0
  68. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
  69. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
  70. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
  71. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  72. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  73. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +28 -67
  74. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  75. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
  76. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  77. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  78. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -1
  79. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +11 -10
  80. package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
  81. package/src/libs/util/app-config/src/lib/app-config.ts +49 -0
  82. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  83. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  84. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  85. package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
  86. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  87. package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
  88. package/tailwind.base.css +11 -2
  89. package/translations/de.json +10 -1
  90. package/translations/en.json +10 -1
  91. package/translations/es.json +10 -1
  92. package/translations/fr.json +10 -1
  93. package/translations/it.json +10 -1
  94. package/translations/nl.json +10 -1
  95. package/translations/pt.json +10 -1
  96. package/translations/sk.json +10 -1
@@ -0,0 +1,89 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ Input,
6
+ Output,
7
+ inject,
8
+ } from '@angular/core'
9
+ import { marker } from '@biesbjerg/ngx-translate-extract-marker'
10
+ import {
11
+ OnlineLinkResource,
12
+ OnlineResource,
13
+ } from '../../../../../../../../../libs/common/domain/src/lib/model/record'
14
+ import { NotificationsService } from '../../../../../../../../../libs/feature/notifications/src'
15
+ import { UrlInputComponent } from '../../../../../../../../../libs/ui/inputs/src'
16
+ import { TranslateService } from '@ngx-translate/core'
17
+
18
+ marker('editor.record.form.field.onlineLinkageResource.defaultName')
19
+
20
+ @Component({
21
+ selector: 'gn-ui-form-field-online-single-link-resource',
22
+ templateUrl: './form-field-online-single-link-resource.component.html',
23
+ styleUrls: ['./form-field-online-single-link-resource.component.css'],
24
+ changeDetection: ChangeDetectionStrategy.OnPush,
25
+ standalone: true,
26
+ imports: [UrlInputComponent],
27
+ })
28
+ export class FormFieldOnlineSingleLinkResourceComponent {
29
+ private notificationsService = inject(NotificationsService)
30
+ private translateService = inject(TranslateService)
31
+
32
+ @Input() set value(onlineResources: Array<OnlineResource>) {
33
+ this.allResources = onlineResources ?? []
34
+ const firstResource = this.allResources[0]
35
+ this.displayUrl = firstResource?.url?.toString() ?? ''
36
+ }
37
+ @Output() valueChange: EventEmitter<Array<OnlineResource>> =
38
+ new EventEmitter()
39
+
40
+ private allResources: OnlineResource[] = []
41
+ displayUrl = ''
42
+
43
+ handleUrlChange(url: string | null) {
44
+ if (!url) return
45
+
46
+ try {
47
+ const parsedUrl = new URL(url)
48
+
49
+ if (this.allResources.length === 0) {
50
+ const defaultName = this.translateService.instant(
51
+ 'editor.record.form.field.onlineLinkageResource.defaultName'
52
+ )
53
+ const newResource: OnlineLinkResource = {
54
+ type: 'link',
55
+ url: parsedUrl,
56
+ name: defaultName,
57
+ }
58
+ this.valueChange.emit([newResource])
59
+ } else {
60
+ const updatedFirst: OnlineResource = {
61
+ ...this.allResources[0],
62
+ url: parsedUrl,
63
+ }
64
+ this.valueChange.emit([updatedFirst, ...this.allResources.slice(1)])
65
+ }
66
+ } catch (error) {
67
+ this.handleError(error as Error)
68
+ }
69
+ }
70
+
71
+ private handleError(error: Error) {
72
+ this.notificationsService.showNotification(
73
+ {
74
+ type: 'error',
75
+ title: this.translateService.instant(
76
+ 'editor.record.onlineResourceError.title'
77
+ ),
78
+ text: `${this.translateService.instant(
79
+ 'editor.record.onlineResourceError.body'
80
+ )} ${error.message}`,
81
+ closeMessage: this.translateService.instant(
82
+ 'editor.record.onlineResourceError.closeMessage'
83
+ ),
84
+ },
85
+ undefined,
86
+ error
87
+ )
88
+ }
89
+ }
@@ -0,0 +1,37 @@
1
+ :host {
2
+ scroll-margin-top: 90px;
3
+ }
4
+
5
+ :host.gn-ui-field-focus-glow {
6
+ position: relative;
7
+ }
8
+
9
+ :host.gn-ui-field-focus-glow::after {
10
+ content: '';
11
+ position: absolute;
12
+ inset: -8px;
13
+ border-radius: 8px;
14
+ background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
15
+ pointer-events: none;
16
+ animation: gn-ui-field-focus-glow 3s ease-out forwards;
17
+ }
18
+
19
+ :host.gn-ui-field-focus-glow::before {
20
+ content: '';
21
+ position: absolute;
22
+ inset: -16px;
23
+ border: 2px dashed var(--color-primary);
24
+ border-radius: 10px;
25
+ pointer-events: none;
26
+ animation: gn-ui-field-focus-glow 3s ease-out forwards;
27
+ }
28
+
29
+ @keyframes gn-ui-field-focus-glow {
30
+ 0%,
31
+ 55% {
32
+ opacity: 1;
33
+ }
34
+ 100% {
35
+ opacity: 0;
36
+ }
37
+ }
@@ -150,6 +150,7 @@
150
150
  <ng-container *ngSwitchCase="'contacts'">
151
151
  <gn-ui-form-field-contacts
152
152
  [value]="valueAsIndividuals"
153
+ [modelSpecifier]="modelSpecifier"
153
154
  (valueChange)="valueChange.emit($event)"
154
155
  ></gn-ui-form-field-contacts>
155
156
  </ng-container>
@@ -4,6 +4,7 @@ import {
4
4
  Component,
5
5
  ElementRef,
6
6
  EventEmitter,
7
+ inject,
7
8
  Input,
8
9
  Output,
9
10
  ViewChild,
@@ -22,6 +23,7 @@ import {
22
23
  import { FormFieldWrapperComponent } from '../../../../../../../../libs/ui/layout/src'
23
24
  import { TranslatePipe } from '@ngx-translate/core'
24
25
  import {
26
+ FieldFocusDirective,
25
27
  FormFieldDateComponent,
26
28
  FormFieldLicenseComponent,
27
29
  FormFieldTemporalExtentsComponent,
@@ -77,6 +79,7 @@ import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.
77
79
  FormFieldTopicsComponent,
78
80
  TextFieldModule,
79
81
  ],
82
+ hostDirectives: [FieldFocusDirective],
80
83
  })
81
84
  export class FormFieldComponent {
82
85
  @Input() uniqueIdentifier: string
@@ -92,6 +95,8 @@ export class FormFieldComponent {
92
95
  @ViewChild('titleInput') titleInput: ElementRef
93
96
  isOpenData = false
94
97
 
98
+ fieldFocus = inject(FieldFocusDirective)
99
+
95
100
  toggleIsOpenData(event: boolean) {
96
101
  this.isOpenData = event
97
102
  }
@@ -1,3 +1,4 @@
1
+ export * from './field-focus.directive'
1
2
  export * from './form-field-keywords/form-field-keywords.component'
2
3
  export * from './form-field-license/form-field-license.component'
3
4
  export * from './form-field-date/form-field-date.component'
@@ -1,5 +1,12 @@
1
1
  import { CommonModule } from '@angular/common'
2
- import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ inject,
6
+ OnDestroy,
7
+ OnInit,
8
+ viewChildren,
9
+ } from '@angular/core'
3
10
  import { EditorFacade } from '../../+state/editor.facade'
4
11
  import { EditorFieldValue } from '../../models'
5
12
  import { FormFieldComponent } from './form-field'
@@ -8,8 +15,9 @@ import {
8
15
  EditorFieldWithValue,
9
16
  EditorSectionWithValues,
10
17
  } from '../../+state/editor.models'
11
- import { map } from 'rxjs'
18
+ import { firstValueFrom, map, Subscription, withLatestFrom } from 'rxjs'
12
19
  import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
20
+ import { switchMap } from 'rxjs/operators'
13
21
 
14
22
  @Component({
15
23
  selector: 'gn-ui-record-form',
@@ -19,13 +27,51 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
19
27
  standalone: true,
20
28
  imports: [CommonModule, FormFieldComponent, TranslateDirective],
21
29
  })
22
- export class RecordFormComponent {
30
+ export class RecordFormComponent implements OnInit, OnDestroy {
23
31
  facade = inject(EditorFacade)
32
+ subscription = new Subscription()
24
33
 
25
34
  recordUniqueIdentifier$ = this.facade.record$.pipe(
26
35
  map((record) => record.uniqueIdentifier)
27
36
  )
28
37
 
38
+ focusFieldWithPage$ = this.facade.focusedField$.pipe(
39
+ switchMap(
40
+ async (field) => [field, await this.getPageIndexForField(field)] as const
41
+ )
42
+ )
43
+
44
+ formFields = viewChildren(FormFieldComponent)
45
+
46
+ focusField(model: CatalogRecordKeys) {
47
+ const fields = this.formFields()
48
+ const field = fields.find((f) => f.model === model)
49
+ field?.fieldFocus.focusField()
50
+ }
51
+
52
+ ngOnInit() {
53
+ this.subscription.add(
54
+ this.focusFieldWithPage$
55
+ .pipe(
56
+ withLatestFrom(
57
+ this.facade.currentPage$,
58
+ ([field, fieldPage], currentPage) =>
59
+ [field, fieldPage, currentPage] as const
60
+ )
61
+ )
62
+ .subscribe(([field, fieldPage, currentPage]) => {
63
+ if (fieldPage !== null && fieldPage !== currentPage) {
64
+ this.facade.setCurrentPage(fieldPage)
65
+ }
66
+ setTimeout(() => this.focusField(field))
67
+ })
68
+ )
69
+ }
70
+
71
+ ngOnDestroy() {
72
+ this.subscription.unsubscribe()
73
+ }
74
+
29
75
  handleFieldValueChange(model: CatalogRecordKeys, newValue: EditorFieldValue) {
30
76
  if (!model) {
31
77
  return
@@ -40,4 +86,14 @@ export class RecordFormComponent {
40
86
  sectionTracker(index: number, section: EditorSectionWithValues) {
41
87
  return section.labelKey
42
88
  }
89
+
90
+ async getPageIndexForField(model: CatalogRecordKeys): Promise<number | null> {
91
+ const config = await firstValueFrom(this.facade.editorConfig$)
92
+ const pageIndex = config.pages.findIndex((page) =>
93
+ page.sections.some((section) =>
94
+ section.fields.some((field) => field.model === model)
95
+ )
96
+ )
97
+ return pageIndex >= 0 ? pageIndex : null
98
+ }
43
99
  }
@@ -19,9 +19,13 @@ export interface FormFieldConfig {
19
19
  // This is used for instance to target only certain online resources in a field
20
20
  type OnlineLinkResourceSpecifier = `onlineResourceType:link`
21
21
  type DatasetDistributionsSpecifier = `onlineResourceType:!link`
22
+ // When set on the `contacts` field, contacts are rendered as editable detail
23
+ // fields (ContactDetailsFormComponent) instead of cards (ContactCardComponent)
24
+ type EditableContactDetailsSpecifier = `contact:editableDetails`
22
25
  export type FieldModelSpecifier =
23
26
  | OnlineLinkResourceSpecifier
24
27
  | DatasetDistributionsSpecifier
28
+ | EditableContactDetailsSpecifier
25
29
 
26
30
  export type FormFieldComponentName =
27
31
  | 'form-field-constraints-shortcuts'
@@ -1,5 +1,5 @@
1
1
  import { Injectable, inject } from '@angular/core'
2
- import { forkJoin, Observable, of, switchMap } from 'rxjs'
2
+ import { Observable, switchMap } from 'rxjs'
3
3
  import { map, tap } from 'rxjs/operators'
4
4
  import { CatalogRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
5
5
  import { EditorConfig } from '../models/'
@@ -0,0 +1 @@
1
+ export * from './lib/notify-reuse-form/notify-reuse-form.component'
@@ -0,0 +1,21 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ Input,
6
+ Output,
7
+ } from '@angular/core'
8
+ import { ReuseRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
9
+
10
+ @Component({
11
+ selector: 'gn-ui-notify-reuse-form',
12
+ standalone: true,
13
+ imports: [],
14
+ templateUrl: './notify-reuse-form.component.html',
15
+ styleUrl: './notify-reuse-form.component.css',
16
+ changeDetection: ChangeDetectionStrategy.OnPush,
17
+ })
18
+ export class NotifyReuseFormComponent {
19
+ @Input() record: ReuseRecord | null = null
20
+ @Output() recordChange = new EventEmitter<ReuseRecord>()
21
+ }
@@ -18,6 +18,8 @@ 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'
22
+ export * from './lib/contact-details/contact-details.component'
21
23
  export * from './lib/notification/notification.component'
22
24
  export * from './lib/record-api-form/record-api-form.component'
23
25
  export * from './lib/thumbnail/thumbnail.component'
@@ -0,0 +1,96 @@
1
+ <div
2
+ class="bg-gray-50 rounded border border-gray-200 shadow-md p-4 flex flex-col gap-3 w-full"
3
+ data-test="contact-details"
4
+ >
5
+ @if (displayName) {
6
+ <div class="flex items-center gap-3">
7
+ @if (organization?.logoUrl?.href) {
8
+ <div
9
+ class="flex items-center justify-center rounded-md bg-white w-14 h-14 shrink-0 overflow-hidden"
10
+ >
11
+ <gn-ui-thumbnail
12
+ class="relative h-full w-full"
13
+ [thumbnailUrl]="organization.logoUrl.href"
14
+ fit="contain"
15
+ ></gn-ui-thumbnail>
16
+ </div>
17
+ }
18
+ <span
19
+ class="font-title text-xl leading-tight"
20
+ data-test="contact-details-name"
21
+ >{{ displayName }}</span
22
+ >
23
+ </div>
24
+ }
25
+
26
+ <!-- Email + phone: always shown; icon dimmed when field is absent -->
27
+ <div class="grid grid-cols-2 gap-1 rounded-md overflow-hidden">
28
+ <div class="bg-gray-100 flex items-center gap-2 px-3 py-2">
29
+ <ng-icon
30
+ class="!w-5 !h-5 !text-[20px] shrink-0"
31
+ [class.opacity-30]="!contact.email"
32
+ [class.opacity-75]="!!contact.email"
33
+ name="matMailOutline"
34
+ ></ng-icon>
35
+ @if (contact.email) {
36
+ <a
37
+ [href]="'mailto:' + contact.email"
38
+ class="text-sm break-all hover:underline"
39
+ data-test="contact-details-email"
40
+ >{{ contact.email }}</a
41
+ >
42
+ }
43
+ </div>
44
+ <div class="bg-gray-100 flex items-center gap-2 px-3 py-2">
45
+ <ng-icon
46
+ class="!w-5 !h-5 !text-[20px] shrink-0"
47
+ [class.opacity-30]="!contact.phone"
48
+ [class.opacity-75]="!!contact.phone"
49
+ name="matCallOutline"
50
+ ></ng-icon>
51
+ @if (contact.phone) {
52
+ <span class="text-sm" data-test="contact-details-phone">{{
53
+ contact.phone
54
+ }}</span>
55
+ }
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Address: always shown; icon dimmed when absent -->
60
+ <div class="bg-gray-100 rounded-md flex items-start gap-2 px-3 py-2">
61
+ <ng-icon
62
+ class="!w-5 !h-5 !text-[20px] shrink-0 mt-0.5"
63
+ [class.opacity-30]="!addressLines.length"
64
+ [class.opacity-75]="!!addressLines.length"
65
+ name="matLocationOnOutline"
66
+ ></ng-icon>
67
+ @if (addressLines.length) {
68
+ <div class="flex flex-col" data-test="contact-details-address">
69
+ @for (line of addressLines; track line) {
70
+ <p class="text-sm m-0">{{ line }}</p>
71
+ }
72
+ </div>
73
+ }
74
+ </div>
75
+
76
+ <!-- Website: only shown when org exists; icon dimmed when absent -->
77
+ @if (organization) {
78
+ <div class="bg-gray-100 rounded-md flex items-center gap-2 px-3 py-2">
79
+ <ng-icon
80
+ class="!w-5 !h-5 !text-[20px] shrink-0"
81
+ [class.opacity-30]="!organization.website"
82
+ [class.opacity-75]="!!organization.website"
83
+ name="matOpenInNew"
84
+ ></ng-icon>
85
+ @if (organization.website) {
86
+ <a
87
+ [href]="organization.website.href"
88
+ target="_blank"
89
+ class="text-sm break-all hover:underline"
90
+ data-test="contact-details-website"
91
+ >{{ organization.website.href }}</a
92
+ >
93
+ }
94
+ </div>
95
+ }
96
+ </div>
@@ -0,0 +1,45 @@
1
+ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
2
+ import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
3
+ import {
4
+ getAddressLines,
5
+ getIndividualDisplayName,
6
+ } from '../../../../../../libs/util/shared/src'
7
+ import { NgIcon, provideIcons } from '@ng-icons/core'
8
+ import { matMailOutline, matOpenInNew } from '@ng-icons/material-icons/baseline'
9
+ import {
10
+ matCallOutline,
11
+ matLocationOnOutline,
12
+ } from '@ng-icons/material-icons/outline'
13
+ import { ThumbnailComponent } from '../thumbnail/thumbnail.component'
14
+ import { TranslateModule } from '@ngx-translate/core'
15
+
16
+ @Component({
17
+ selector: 'gn-ui-contact-details',
18
+ templateUrl: './contact-details.component.html',
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ standalone: true,
21
+ imports: [NgIcon, ThumbnailComponent, TranslateModule],
22
+ viewProviders: [
23
+ provideIcons({
24
+ matCallOutline,
25
+ matLocationOnOutline,
26
+ matMailOutline,
27
+ matOpenInNew,
28
+ }),
29
+ ],
30
+ })
31
+ export class ContactDetailsComponent {
32
+ @Input() contact: Individual
33
+
34
+ get organization() {
35
+ return this.contact?.organization
36
+ }
37
+
38
+ get displayName(): string {
39
+ return getIndividualDisplayName(this.contact)
40
+ }
41
+
42
+ get addressLines(): string[] {
43
+ return getAddressLines(this.contact?.address)
44
+ }
45
+ }
@@ -0,0 +1,37 @@
1
+ <gn-ui-button
2
+ [type]="overlayOpen ? 'primary' : '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 group-hover:text-white"
11
+ [title]="displayName"
12
+ >{{ displayName }}</span
13
+ >
14
+ <div
15
+ class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
16
+ >
17
+ @if (overlayOpen) {
18
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matClose"></ng-icon>
19
+ } @else {
20
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
21
+ }
22
+ </div>
23
+ </gn-ui-button>
24
+
25
+ <ng-template
26
+ cdkConnectedOverlay
27
+ [cdkConnectedOverlayOrigin]="overlayOrigin"
28
+ [cdkConnectedOverlayOpen]="overlayOpen"
29
+ [cdkConnectedOverlayPositions]="overlayPositions"
30
+ [cdkConnectedOverlayOffsetX]="overlayOffsetX"
31
+ (overlayOutsideClick)="closeOverlay()"
32
+ (detach)="closeOverlay()"
33
+ >
34
+ <div [style.width.px]="overlayWidth">
35
+ <gn-ui-contact-details [contact]="contact"></gn-ui-contact-details>
36
+ </div>
37
+ </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
- }