geonetwork-ui 2.10.0-dev.cbf02ead8 → 2.10.0-dev.cc63fa135

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.
@@ -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>
@@ -1,26 +1,70 @@
1
- import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
1
+ import { ConnectedPosition, OverlayModule } from '@angular/cdk/overlay'
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ ElementRef,
6
+ inject,
7
+ Input,
8
+ } from '@angular/core'
2
9
  import { Individual } from '../../../../../../libs/common/domain/src/lib/model/record'
3
10
  import { NgIcon, provideIcons } from '@ng-icons/core'
11
+ import { matClose } from '@ng-icons/material-icons/baseline'
4
12
  import { matInfoOutline } from '@ng-icons/material-icons/outline'
5
13
  import { ButtonComponent } from '../../../../../../libs/ui/inputs/src'
6
14
  import { getIndividualDisplayName } from '../../../../../../libs/util/shared/src'
15
+ import { ContactDetailsComponent } from '../contact-details/contact-details.component'
7
16
 
8
17
  @Component({
9
18
  selector: 'gn-ui-contact-pill',
10
19
  templateUrl: './contact-pill.component.html',
11
20
  changeDetection: ChangeDetectionStrategy.OnPush,
12
21
  standalone: true,
13
- imports: [NgIcon, ButtonComponent],
14
- viewProviders: [
15
- provideIcons({
16
- matInfoOutline,
17
- }),
18
- ],
22
+ imports: [NgIcon, ButtonComponent, OverlayModule, ContactDetailsComponent],
23
+ viewProviders: [provideIcons({ matClose, matInfoOutline })],
19
24
  })
20
25
  export class ContactPillComponent {
21
26
  @Input() contact: Individual
22
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
+
23
50
  get displayName(): string {
24
51
  return getIndividualDisplayName(this.contact)
25
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
+ }
26
70
  }
@@ -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() {
@@ -53,7 +53,7 @@
53
53
  data-test="usage-panel"
54
54
  >
55
55
  <div class="flex flex-col gap-[10px] mr-4 py-[12px] rounded text-gray-900">
56
- @for (license of licenses; track license) {
56
+ @for (license of licenses; track $index) {
57
57
  @if (license.url) {
58
58
  <div class="text-primary">
59
59
  <a
@@ -117,7 +117,7 @@
117
117
  <p class="text-xs font-normal text-black">
118
118
  {{ group.roleLabel | translate }}
119
119
  </p>
120
- <div class="grid gap-0.5 grid-cols-1 md:grid-cols-2">
120
+ <div class="grid gap-1 grid-cols-1 md:grid-cols-2">
121
121
  @for (contact of group.contacts; track contact.email) {
122
122
  <gn-ui-contact-pill [contact]="contact"></gn-ui-contact-pill>
123
123
  }
@@ -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)
@@ -12,6 +12,15 @@ export function getIndividualDisplayName(individual: Individual): string {
12
12
  return individual.organization?.name ?? individual.email ?? ''
13
13
  }
14
14
 
15
+ export function getAddressLines(address: string | undefined): string[] {
16
+ return address
17
+ ? address
18
+ .split(',')
19
+ .map((part) => part.trim())
20
+ .filter(Boolean)
21
+ : []
22
+ }
23
+
15
24
  export function toIndividual(user: UserModel): Individual {
16
25
  return {
17
26
  firstName: user.name,