geonetwork-ui 2.10.0-dev.69145ed6a → 2.10.0-dev.7e58935b2

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 (39) hide show
  1. package/fesm2022/geonetwork-ui.mjs +240 -55
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +34 -6
  4. package/index.d.ts.map +1 -1
  5. package/package.json +1 -1
  6. package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +4 -0
  7. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  8. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  9. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  10. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +38 -0
  11. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +0 -1
  12. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +9 -2
  13. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +12 -0
  14. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +37 -0
  15. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
  16. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -0
  17. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  18. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +0 -3
  19. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +0 -1
  20. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +27 -25
  21. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
  22. package/src/libs/ui/elements/src/index.ts +1 -0
  23. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -0
  24. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  25. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +23 -2
  26. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +51 -7
  27. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  28. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +2 -2
  29. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  30. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  31. package/src/libs/util/shared/src/lib/utils/user-display.ts +9 -0
  32. package/translations/de.json +8 -1
  33. package/translations/en.json +8 -1
  34. package/translations/es.json +8 -1
  35. package/translations/fr.json +8 -1
  36. package/translations/it.json +8 -1
  37. package/translations/nl.json +8 -1
  38. package/translations/pt.json +8 -1
  39. package/translations/sk.json +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geonetwork-ui",
3
- "version": "2.10.0-dev.69145ed6a",
3
+ "version": "2.10.0-dev.7e58935b2",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -44,6 +44,10 @@ export class AuthService {
44
44
  window.location.href
45
45
  ).toString()
46
46
  )
47
+ .replace(
48
+ '${current_path}',
49
+ this.location.prepareExternalUrl(this.location.path())
50
+ )
47
51
  .replace('${lang2}', toLang2(this.translateService.currentLang))
48
52
  .replace('${lang3}', toLang3(this.translateService.currentLang))
49
53
  }
@@ -0,0 +1,67 @@
1
+ <div class="flex flex-col gap-7">
2
+ <div class="grid grid-cols-2 gap-4">
3
+ <div class="min-w-0">
4
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
5
+ editor.record.form.field.contactDetails.lastName
6
+ </h3>
7
+ <gn-ui-text-input
8
+ class="block w-full"
9
+ extraClass="w-full"
10
+ [(value)]="contact.lastName"
11
+ (valueChange)="emitContactChange()"
12
+ [placeholder]="
13
+ 'editor.record.form.field.contactDetails.lastName.placeholder'
14
+ | translate
15
+ "
16
+ data-test="contactDetailsLastName"
17
+ ></gn-ui-text-input>
18
+ </div>
19
+ <div class="min-w-0">
20
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
21
+ editor.record.form.field.contactDetails.firstName
22
+ </h3>
23
+ <gn-ui-text-input
24
+ class="block w-full"
25
+ extraClass="w-full"
26
+ [(value)]="contact.firstName"
27
+ (valueChange)="emitContactChange()"
28
+ [placeholder]="
29
+ 'editor.record.form.field.contactDetails.firstName.placeholder'
30
+ | translate
31
+ "
32
+ data-test="contactDetailsFirstName"
33
+ ></gn-ui-text-input>
34
+ </div>
35
+ </div>
36
+ <div>
37
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
38
+ editor.record.form.field.contactDetails.email
39
+ </h3>
40
+ <gn-ui-text-input
41
+ class="block w-full"
42
+ extraClass="w-full"
43
+ [value]="contact.organization?.email ?? ''"
44
+ (valueChange)="handleOrganizationChange({ email: $event })"
45
+ [placeholder]="
46
+ 'editor.record.form.field.contactDetails.email.placeholder' | translate
47
+ "
48
+ data-test="contactDetailsEmail"
49
+ ></gn-ui-text-input>
50
+ </div>
51
+ <div>
52
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
53
+ editor.record.form.field.contactDetails.organization
54
+ </h3>
55
+ <gn-ui-text-input
56
+ class="block w-full"
57
+ extraClass="w-full"
58
+ [value]="contact.organization?.name ?? ''"
59
+ (valueChange)="handleOrganizationChange({ name: $event })"
60
+ [placeholder]="
61
+ 'editor.record.form.field.contactDetails.organization.placeholder'
62
+ | translate
63
+ "
64
+ data-test="contactDetailsOrganization"
65
+ ></gn-ui-text-input>
66
+ </div>
67
+ </div>
@@ -0,0 +1,39 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ Input,
6
+ Output,
7
+ } from '@angular/core'
8
+ import {
9
+ Individual,
10
+ Organization,
11
+ } from '../../../../../../../libs/common/domain/src/lib/model/record'
12
+ import { TextInputComponent } from '../../../../../../../libs/ui/inputs/src'
13
+ import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
14
+
15
+ @Component({
16
+ selector: 'gn-ui-contact-details-form',
17
+ templateUrl: './contact-details-form.component.html',
18
+ styleUrls: ['./contact-details-form.component.css'],
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ standalone: true,
21
+ imports: [TextInputComponent, TranslateDirective, TranslatePipe],
22
+ })
23
+ export class ContactDetailsFormComponent {
24
+ @Input() contact: Individual
25
+ @Output() contactChange = new EventEmitter<Individual>()
26
+
27
+ emitContactChange() {
28
+ this.contactChange.emit(this.contact)
29
+ }
30
+
31
+ handleOrganizationChange(change: Partial<Organization>) {
32
+ this.contact.organization = {
33
+ ...(this.contact.organization ?? ({} as Organization)),
34
+ ...change,
35
+ } as Organization
36
+
37
+ this.emitContactChange()
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import { Directive, ElementRef, inject, Input } from '@angular/core'
2
+
3
+ @Directive({
4
+ selector: '[gnUiFieldFocus]',
5
+ standalone: true,
6
+ exportAs: 'fieldFocus',
7
+ })
8
+ export class FieldFocusDirective {
9
+ @Input() gnUiFieldFocusGlowClass = 'gn-ui-field-focus-glow'
10
+
11
+ private el = inject(ElementRef)
12
+
13
+ public focusField() {
14
+ setTimeout(() => {
15
+ const host = this.el.nativeElement as HTMLElement
16
+ const glowClass = this.gnUiFieldFocusGlowClass
17
+
18
+ host.classList.remove(glowClass)
19
+ void host.offsetWidth
20
+ host.classList.add(glowClass)
21
+ host.addEventListener(
22
+ 'animationend',
23
+ () => host.classList.remove(glowClass),
24
+ { once: true }
25
+ )
26
+
27
+ host.scrollIntoView({ behavior: 'smooth', block: 'start' })
28
+ const target =
29
+ host.querySelector<HTMLElement>(
30
+ 'input, textarea, select, [contenteditable="true"]'
31
+ ) ??
32
+ host.querySelector<HTMLElement>(
33
+ 'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
34
+ )
35
+ target?.focus({ preventScroll: true })
36
+ })
37
+ }
38
+ }
@@ -139,7 +139,6 @@ export class FormFieldConstraintsShortcutsComponent implements OnDestroy {
139
139
  this.editorFacade.setFieldVisibility({ model }, visible)
140
140
  })
141
141
  }
142
- hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
143
142
  hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
144
143
  hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
145
144
  })
@@ -15,8 +15,15 @@
15
15
  (itemsOrderChange)="handleContactsChanged($event)"
16
16
  [elementTemplate]="contactTemplate"
17
17
  ></gn-ui-sortable-list>
18
- <ng-template #contactTemplate let-contact>
19
- <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
18
+ <ng-template #contactTemplate let-contact let-index="index">
19
+ @if (modelSpecifier === 'contact:editableDetails') {
20
+ <gn-ui-contact-details-form
21
+ [contact]="contact"
22
+ (contactChange)="handleContactChanged($event, index)"
23
+ ></gn-ui-contact-details-form>
24
+ } @else {
25
+ <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
26
+ }
20
27
  </ng-template>
21
28
  } @else {
22
29
  <div
@@ -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
  */
@@ -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,3 +0,0 @@
1
- gn-ui-form-field {
2
- scroll-margin-top: 90px;
3
- }
@@ -24,7 +24,6 @@
24
24
  ) {
25
25
  @if (!field.config.hidden) {
26
26
  <gn-ui-form-field
27
- [id]="anchorIdPrefix + field.config.model"
28
27
  [ngClass]="
29
28
  field.config.gridColumnSpan === 1
30
29
  ? 'col-span-1'
@@ -2,10 +2,10 @@ import { CommonModule } from '@angular/common'
2
2
  import {
3
3
  ChangeDetectionStrategy,
4
4
  Component,
5
- ElementRef,
6
5
  inject,
7
6
  OnDestroy,
8
7
  OnInit,
8
+ viewChildren,
9
9
  } from '@angular/core'
10
10
  import { EditorFacade } from '../../+state/editor.facade'
11
11
  import { EditorFieldValue } from '../../models'
@@ -15,8 +15,9 @@ import {
15
15
  EditorFieldWithValue,
16
16
  EditorSectionWithValues,
17
17
  } from '../../+state/editor.models'
18
- import { filter, firstValueFrom, map, Subscription, switchMap } from 'rxjs'
18
+ import { firstValueFrom, map, Subscription, withLatestFrom } from 'rxjs'
19
19
  import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
20
+ import { switchMap } from 'rxjs/operators'
20
21
 
21
22
  @Component({
22
23
  selector: 'gn-ui-record-form',
@@ -27,41 +28,42 @@ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/l
27
28
  imports: [CommonModule, FormFieldComponent, TranslateDirective],
28
29
  })
29
30
  export class RecordFormComponent implements OnInit, OnDestroy {
30
- anchorIdPrefix = 'gn-ui--field-'
31
31
  facade = inject(EditorFacade)
32
- private el = inject(ElementRef)
33
32
  subscription = new Subscription()
34
33
 
35
34
  recordUniqueIdentifier$ = this.facade.record$.pipe(
36
35
  map((record) => record.uniqueIdentifier)
37
36
  )
38
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
+
39
52
  ngOnInit() {
40
53
  this.subscription.add(
41
- this.facade.focusedField$
54
+ this.focusFieldWithPage$
42
55
  .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
- }))
56
+ withLatestFrom(
57
+ this.facade.currentPage$,
58
+ ([field, fieldPage], currentPage) =>
59
+ [field, fieldPage, currentPage] as const
60
+ )
50
61
  )
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
- })
62
+ .subscribe(([field, fieldPage, currentPage]) => {
63
+ if (fieldPage !== null && fieldPage !== currentPage) {
64
+ this.facade.setCurrentPage(fieldPage)
59
65
  }
60
- setTimeout(() =>
61
- document
62
- .getElementById(this.anchorIdPrefix + field)
63
- ?.scrollIntoView({ behavior: 'instant', block: 'start' })
64
- )
66
+ setTimeout(() => this.focusField(field))
65
67
  })
66
68
  )
67
69
  }
@@ -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'
@@ -19,6 +19,7 @@ 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
21
  export * from './lib/contact-pill/contact-pill.component'
22
+ export * from './lib/contact-details/contact-details.component'
22
23
  export * from './lib/notification/notification.component'
23
24
  export * from './lib/record-api-form/record-api-form.component'
24
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
+ }
@@ -1,7 +1,10 @@
1
1
  <gn-ui-button
2
- type="primary-light"
2
+ [type]="overlayOpen ? 'primary' : 'primary-light'"
3
3
  extraClass="group w-full min-h-12 gap-3 justify-between py-2 pl-5 pr-4 rounded"
4
4
  data-test="contact-pill"
5
+ (buttonClick)="toggleOverlay()"
6
+ cdkOverlayOrigin
7
+ #overlayOrigin="cdkOverlayOrigin"
5
8
  >
6
9
  <span
7
10
  class="font-title font-medium text-base leading-tight truncate group-hover:text-white"
@@ -11,6 +14,24 @@
11
14
  <div
12
15
  class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
13
16
  >
14
- <ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
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
+ }
15
22
  </div>
16
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>