geonetwork-ui 2.10.0-dev.6fa5006eb → 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 (42) hide show
  1. package/fesm2022/geonetwork-ui.mjs +270 -55
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +48 -16
  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/app-config/src/lib/app-config.ts +36 -0
  32. package/src/libs/util/app-config/src/lib/model.ts +4 -0
  33. package/src/libs/util/app-config/src/lib/parse-utils.ts +23 -1
  34. package/src/libs/util/shared/src/lib/utils/user-display.ts +9 -0
  35. package/translations/de.json +8 -1
  36. package/translations/en.json +8 -1
  37. package/translations/es.json +8 -1
  38. package/translations/fr.json +8 -1
  39. package/translations/it.json +8 -1
  40. package/translations/nl.json +8 -1
  41. package/translations/pt.json +8 -1
  42. package/translations/sk.json +8 -1
@@ -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>
@@ -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)
@@ -1,6 +1,7 @@
1
1
  import * as TOML from '@ltd/j-toml'
2
2
  import {
3
3
  checkMetadataLanguage,
4
+ checkNewRecordDefaultLanguage,
4
5
  parseConfigSection,
5
6
  parseMultiConfigSection,
6
7
  parseTranslationsConfigSection,
@@ -8,6 +9,7 @@ import {
8
9
  import {
9
10
  CustomTranslations,
10
11
  CustomTranslationsAllLanguages,
12
+ EditorConfig,
11
13
  GlobalConfig,
12
14
  LayerConfig,
13
15
  MapConfig,
@@ -51,6 +53,12 @@ export function getOptionalSearchConfig(): SearchConfig | null {
51
53
  return searchConfig
52
54
  }
53
55
 
56
+ let editorConfig: EditorConfig = null
57
+
58
+ export function getOptionalEditorConfig(): EditorConfig | null {
59
+ return editorConfig
60
+ }
61
+
54
62
  let metadataQualityConfig: MetadataQualityConfig = null
55
63
  export function getMetadataQualityConfig(): MetadataQualityConfig {
56
64
  return (
@@ -285,6 +293,33 @@ export function loadAppConfig(configUrl = 'assets/configuration/default.toml') {
285
293
  SORTABLE: parsedMetadataQualitySection.sortable,
286
294
  } as MetadataQualityConfig)
287
295
 
296
+ let parsedEditingSection = parseConfigSection(
297
+ parsed,
298
+ 'editing',
299
+ [],
300
+ ['new_record_default_language'],
301
+ warnings,
302
+ errors
303
+ )
304
+ if (
305
+ parsedEditingSection !== null &&
306
+ parsedEditingSection.new_record_default_language !== undefined
307
+ ) {
308
+ parsedEditingSection = checkNewRecordDefaultLanguage(
309
+ parsedEditingSection,
310
+ warnings
311
+ )
312
+ }
313
+ editorConfig =
314
+ parsedEditingSection === null
315
+ ? null
316
+ : ({
317
+ NEW_RECORD_DEFAULT_LANGUAGE:
318
+ parsedEditingSection.new_record_default_language as
319
+ | string
320
+ | undefined,
321
+ } as EditorConfig)
322
+
288
323
  customTranslations = parseTranslationsConfigSection(
289
324
  parsed,
290
325
  'translations'
@@ -309,6 +344,7 @@ export function isConfigLoaded() {
309
344
  export function _reset() {
310
345
  globalConfig = null
311
346
  themeConfig = null
347
+ editorConfig = null
312
348
  customTranslations = null
313
349
  }
314
350