geonetwork-ui 2.10.0-dev.d0cd0940b → 2.10.0-dev.d26e9afe1

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 (137) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1294 -6077
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +200 -161
  4. package/index.d.ts.map +1 -1
  5. package/material-styles.css +483 -0
  6. package/package.json +25 -35
  7. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +9 -0
  8. package/src/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts +2 -0
  9. package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +1 -0
  10. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+ongules.ts +7 -0
  11. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+roilaye.ts +1 -0
  12. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts +1 -0
  13. package/src/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts +1 -0
  14. package/src/libs/api/metadata-converter/src/lib/fixtures/georhena.records.ts +1 -0
  15. package/src/libs/api/metadata-converter/src/lib/fixtures/metadata-for-i18n.records.ts +2 -0
  16. package/src/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +1 -0
  17. package/src/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts +1 -0
  18. package/src/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts +2 -0
  19. package/src/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts +1 -0
  20. package/src/libs/api/metadata-converter/src/lib/fixtures/wallonie.records.reuse.ts +8 -0
  21. package/src/libs/api/metadata-converter/src/lib/gn4/atomic-operations.ts +48 -7
  22. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts +1 -0
  23. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +5 -4
  24. package/src/libs/api/metadata-converter/src/lib/gn4/metadata-url.service.ts +2 -2
  25. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +1 -0
  26. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +7 -0
  27. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +8 -0
  28. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +8 -0
  29. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +11 -0
  30. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +33 -0
  31. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +33 -0
  32. package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +8 -4
  33. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +2 -2
  34. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +59 -37
  35. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts +1 -1
  36. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts +1 -1
  37. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +48 -2
  38. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +13 -5
  39. package/src/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts +2 -1
  40. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  41. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  42. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  43. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +56 -0
  44. package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -2
  45. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +4 -1
  46. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +26 -19
  47. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +5 -1
  48. package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts +0 -2
  49. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  50. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  51. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  52. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html +5 -5
  53. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts +2 -1
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +41 -0
  55. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +6 -8
  56. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +9 -2
  57. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +12 -0
  58. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts +1 -1
  59. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts +12 -5
  60. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.html +4 -4
  61. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.ts +7 -0
  62. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  63. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html +6 -2
  64. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts +3 -1
  65. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +3 -0
  66. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +21 -16
  67. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +14 -0
  68. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  69. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +34 -2
  70. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +18 -6
  71. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +92 -33
  72. package/src/libs/feature/editor/src/lib/expressions.ts +380 -8
  73. package/src/libs/feature/editor/src/lib/fields.config.ts +310 -336
  74. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +6 -3
  75. package/src/libs/feature/editor/src/lib/services/editor.service.ts +9 -13
  76. package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
  77. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
  78. package/src/libs/feature/search/src/lib/results-table/results-table-container.component.ts +10 -5
  79. package/src/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +2 -2
  80. package/src/libs/ui/dataviz/src/index.ts +0 -1
  81. package/src/libs/ui/elements/src/index.ts +1 -0
  82. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +88 -0
  83. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  84. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +29 -4
  85. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +51 -7
  86. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.css +13 -0
  87. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +13 -3
  88. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +43 -6
  89. package/src/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts +2 -2
  90. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  91. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +20 -10
  92. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +29 -1
  93. package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html +4 -5
  94. package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +4 -0
  95. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
  96. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +2 -2
  97. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
  98. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +3 -3
  99. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  100. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  101. package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.html +5 -5
  102. package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.ts +9 -1
  103. package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html +1 -1
  104. package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts +3 -8
  105. package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.html +7 -7
  106. package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.ts +14 -1
  107. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +4 -54
  108. package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
  109. package/src/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.ts +1 -1
  110. package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html +12 -8
  111. package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts +3 -1
  112. package/src/libs/ui/search/src/lib/results-table/results-table.component.html +1 -0
  113. package/src/libs/ui/widgets/src/lib/popover/popover.component.ts +7 -1
  114. package/src/libs/util/app-config/src/lib/app-config.ts +70 -9
  115. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  116. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  117. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +1 -2
  118. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +6 -3
  119. package/src/libs/util/i18n/src/index.ts +0 -1
  120. package/src/libs/util/i18n/src/lib/i18n.constants.ts +17 -11
  121. package/src/libs/util/i18n/src/lib/i18n.interceptor.ts +2 -2
  122. package/src/libs/util/i18n/src/lib/i18n.providers.ts +14 -17
  123. package/src/libs/util/i18n/src/lib/test.translate.loader.ts +48 -0
  124. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  125. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  126. package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
  127. package/src/libs/util/shared/src/lib/utils/user-display.ts +9 -0
  128. package/style.css +0 -1
  129. package/tailwind.base.css +8 -0
  130. package/translations/de.json +58 -48
  131. package/translations/en.json +63 -53
  132. package/translations/es.json +32 -22
  133. package/translations/fr.json +58 -48
  134. package/translations/it.json +58 -48
  135. package/translations/nl.json +31 -21
  136. package/translations/pt.json +32 -22
  137. package/translations/sk.json +32 -22
@@ -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'
@@ -44,8 +48,7 @@ export interface EditorField extends EditorFieldIdentification {
44
48
  gridColumnSpan?: number
45
49
 
46
50
  // a hidden field won't show but can still be used to modify the record
47
- // FIXME: currently this is redundant with an absence of formFieldConfig but necessary for clarity
48
- hidden?: boolean
51
+ hidden?: boolean | EditorFieldExpression
49
52
 
50
53
  // the result of this expression will replace the field value on save
51
54
  onSaveProcess?: EditorFieldExpression
@@ -54,7 +57,7 @@ export interface EditorField extends EditorFieldIdentification {
54
57
  export interface EditorSection {
55
58
  labelKey?: string
56
59
  descriptionKey?: string
57
- hidden: boolean
60
+ hidden?: boolean | EditorFieldExpression
58
61
  fields: EditorField[]
59
62
  }
60
63
 
@@ -1,5 +1,5 @@
1
- import { Injectable, inject } from '@angular/core'
2
- import { forkJoin, Observable, of, switchMap } from 'rxjs'
1
+ import { inject, Injectable } from '@angular/core'
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/'
@@ -16,7 +16,8 @@ export class EditorService {
16
16
  saveRecord(
17
17
  record: CatalogRecord,
18
18
  recordSource: string,
19
- fieldsConfig: EditorConfig
19
+ fieldsConfig: EditorConfig,
20
+ publish: boolean
20
21
  ): Observable<[CatalogRecord, string]> {
21
22
  const savedRecord = { ...record }
22
23
 
@@ -27,22 +28,17 @@ export class EditorService {
27
28
  // run onSave processes
28
29
  for (const field of fields) {
29
30
  if (field.onSaveProcess && field.model) {
30
- const evaluator = evaluate(field.onSaveProcess)
31
+ const { evaluator } = evaluate(field.onSaveProcess)
31
32
  savedRecord[field.model] = evaluator({
32
- model: field.model,
33
- value: record[field.model],
33
+ globals: {
34
+ record,
35
+ },
34
36
  })
35
37
  }
36
38
  }
37
- let publishToAll = true
38
- // if the record is new, generate a new unique identifier and pass publishToAll as false
39
- if (!record.uniqueIdentifier) {
40
- savedRecord.uniqueIdentifier = null
41
- publishToAll = false
42
- }
43
39
 
44
40
  return this.recordsRepository
45
- .saveRecord(savedRecord, recordSource, publishToAll)
41
+ .saveRecord(savedRecord, recordSource, publish)
46
42
  .pipe(
47
43
  switchMap((uniqueIdentifier) =>
48
44
  this.recordsRepository.openRecordForEdition(uniqueIdentifier)
@@ -2,6 +2,23 @@ import { Injectable } from '@angular/core'
2
2
  import { extend } from 'ol/extent.js'
3
3
  import { CatalogRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
4
4
  import { BoundingBox, getGeometryBoundingBox } from '../../../../../../libs/util/shared/src'
5
+ import { MapContextLayer } from '@geospatial-sdk/core'
6
+ import {
7
+ createSpatialExtentLayer,
8
+ SpatialExtentLayerStyle,
9
+ } from '../../../../../../libs/ui/map/src'
10
+
11
+ /**
12
+ * Style of the extent overlay drawn on top of the previewed data: a dashed
13
+ * black outline over a very light fill, so the extent stays readable without
14
+ * hiding the data underneath.
15
+ */
16
+ const RECORD_EXTENT_OVERLAY_STYLE: SpatialExtentLayerStyle = {
17
+ 'stroke-color': 'rgba(0, 0, 0, 0.6)',
18
+ 'stroke-width': 2,
19
+ 'stroke-line-dash': [8, 6],
20
+ 'fill-color': 'rgba(0, 0, 0, 0.03)',
21
+ }
5
22
 
6
23
  @Injectable({
7
24
  providedIn: 'root',
@@ -26,4 +43,20 @@ export class MapUtilsService {
26
43
  [Infinity, Infinity, -Infinity, -Infinity]
27
44
  )
28
45
  }
46
+
47
+ /**
48
+ * Builds a non-interactive overlay layer drawing the spatial extent(s)
49
+ * declared in the record's metadata (bounding boxes and/or geometries).
50
+ * Returns null when the record has no usable spatial extent.
51
+ *
52
+ * This is purely for display and is independent from the map's initial view,
53
+ * which is derived separately (see {@link getRecordExtent}).
54
+ */
55
+ getRecordExtentLayer(record: Partial<CatalogRecord>): MapContextLayer | null {
56
+ return createSpatialExtentLayer(record.spatialExtents ?? [], {
57
+ label: 'Spatial extent',
58
+ clickable: false,
59
+ style: RECORD_EXTENT_OVERLAY_STYLE,
60
+ })
61
+ }
29
62
  }
@@ -431,11 +431,19 @@ export class MapViewComponent implements AfterViewInit {
431
431
  }),
432
432
  withLatestFrom(this.mdViewFacade.metadata$),
433
433
  map(([context, metadata]) => {
434
- if (context.view) return context
434
+ // overlay the record's declared spatial extent on top of the data layers
435
+ const extentLayer = this.mapUtils.getRecordExtentLayer(metadata)
436
+ const layers = extentLayer
437
+ ? [...context.layers, extentLayer]
438
+ : context.layers
439
+ // the view (initial zoom) is derived from the data layer or, as a
440
+ // fallback, from the record extent — independently from the overlay above
441
+ if (context.view) return { ...context, layers }
435
442
  const extent = this.mapUtils.getRecordExtent(metadata)
436
443
  const view = extent ? { extent } : null
437
444
  return {
438
445
  ...context,
446
+ layers,
439
447
  view,
440
448
  }
441
449
  }),
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  Component,
3
3
  EventEmitter,
4
+ inject,
4
5
  Input,
5
6
  OnDestroy,
6
7
  Output,
7
- inject,
8
8
  } from '@angular/core'
9
9
  import { CatalogRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
10
10
  import { SearchFacade } from '../state/search.facade'
@@ -67,6 +67,7 @@ export class ResultsTableContainerComponent implements OnDestroy {
67
67
 
68
68
  async handleDeleteRecord(item: unknown) {
69
69
  const uniqueIdentifier = (item as CatalogRecord).uniqueIdentifier
70
+ const recordKind = (item as CatalogRecord).kind
70
71
  this.subscription.add(
71
72
  this.recordsRepository.deleteRecord(uniqueIdentifier).subscribe({
72
73
  next: () => {
@@ -76,10 +77,12 @@ export class ResultsTableContainerComponent implements OnDestroy {
76
77
  {
77
78
  type: 'success',
78
79
  title: this.translateService.instant(
79
- 'editor.record.deleteSuccess.title'
80
+ 'editor.record.deleteSuccess.title',
81
+ { recordKind }
80
82
  ),
81
83
  text: `${this.translateService.instant(
82
- 'editor.record.deleteSuccess.body'
84
+ 'editor.record.deleteSuccess.body',
85
+ { recordKind }
83
86
  )}`,
84
87
  },
85
88
  2500
@@ -90,10 +93,12 @@ export class ResultsTableContainerComponent implements OnDestroy {
90
93
  {
91
94
  type: 'error',
92
95
  title: this.translateService.instant(
93
- 'editor.record.deleteError.title'
96
+ 'editor.record.deleteError.title',
97
+ { recordKind }
94
98
  ),
95
99
  text: `${this.translateService.instant(
96
- 'editor.record.deleteError.body'
100
+ 'editor.record.deleteError.body',
101
+ { recordKind }
97
102
  )} ${error}`,
98
103
  closeMessage: this.translateService.instant(
99
104
  'editor.record.deleteError.closeMessage'
@@ -1,4 +1,4 @@
1
- import { Component, InjectionToken, inject } from '@angular/core'
1
+ import { Component, inject, InjectionToken } from '@angular/core'
2
2
  import { LANGUAGE_STORAGE_KEY } from '../../../../../../libs/util/i18n/src'
3
3
  import { TranslateService } from '@ngx-translate/core'
4
4
  import { DropdownSelectorComponent } from '../../../../../../libs/ui/inputs/src'
@@ -26,7 +26,7 @@ export class LanguageSwitcherComponent {
26
26
  )
27
27
 
28
28
  get currentLang() {
29
- return this.translate.currentLang
29
+ return this.translate.getCurrentLang()
30
30
  }
31
31
 
32
32
  changeLanguage(value: unknown) {
@@ -1,4 +1,3 @@
1
1
  export * from './lib/chart/chart.component'
2
2
  export * from './lib/data-table/data-table.component'
3
- export * from './lib/data-table/data-table.fixtures'
4
3
  export * from './lib/figure/figure.component'
@@ -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,88 @@
1
+ <div
2
+ class="bg-gray-50 text-black rounded border border-gray-200 shadow-md p-4 flex flex-col gap-2 w-full"
3
+ data-test="contact-details"
4
+ >
5
+ @if (displayName) {
6
+ <div class="flex items-center gap-2.5">
7
+ @if (organization?.logoUrl?.href) {
8
+ <div
9
+ class="flex items-center justify-center rounded-lg 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
+ @if (contact.email || contact.phone) {
27
+ <div
28
+ class="grid gap-1 items-start"
29
+ [class.grid-cols-2]="contact.email && contact.phone"
30
+ >
31
+ @if (contact.email) {
32
+ <div class="bg-gray-100 rounded-lg flex items-center gap-2 px-3 py-2">
33
+ <ng-icon
34
+ class="!w-5 !h-5 !text-[20px] shrink-0 opacity-30"
35
+ name="matMailOutline"
36
+ ></ng-icon>
37
+ <a
38
+ [href]="'mailto:' + contact.email"
39
+ class="text-sm break-all hover:underline"
40
+ data-test="contact-details-email"
41
+ >{{ contact.email }}</a
42
+ >
43
+ </div>
44
+ }
45
+ @if (contact.phone) {
46
+ <div class="bg-gray-100 rounded-lg flex items-center gap-2 px-3 py-2">
47
+ <ng-icon
48
+ class="!w-5 !h-5 !text-[20px] shrink-0 opacity-30"
49
+ name="matCallOutline"
50
+ ></ng-icon>
51
+ <span class="text-sm" data-test="contact-details-phone">{{
52
+ contact.phone
53
+ }}</span>
54
+ </div>
55
+ }
56
+ </div>
57
+ }
58
+
59
+ @if (addressLines.length) {
60
+ <div class="bg-gray-100 rounded-lg 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 opacity-30"
63
+ name="matLocationOnOutline"
64
+ ></ng-icon>
65
+ <div class="flex flex-col" data-test="contact-details-address">
66
+ @for (line of addressLines; track line) {
67
+ <p class="text-sm m-0">{{ line }}</p>
68
+ }
69
+ </div>
70
+ </div>
71
+ }
72
+
73
+ @if (organization?.website) {
74
+ <div class="bg-gray-100 rounded-lg flex items-center gap-2 px-3 py-2">
75
+ <ng-icon
76
+ class="!w-5 !h-5 !text-[20px] shrink-0 opacity-30"
77
+ name="matOpenInNew"
78
+ ></ng-icon>
79
+ <a
80
+ [href]="organization.website.href"
81
+ target="_blank"
82
+ class="text-sm break-all hover:underline"
83
+ data-test="contact-details-website"
84
+ >{{ organization.website.href }}</a
85
+ >
86
+ </div>
87
+ }
88
+ </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,16 +1,41 @@
1
1
  <gn-ui-button
2
- type="primary-light"
2
+ [type]="overlayOpen ? 'gray-light' : '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
- class="font-title font-medium text-base leading-tight truncate group-hover:text-white"
10
+ class="font-title font-medium text-base leading-tight truncate"
11
+ [class]="!overlayOpen ? 'text-primary-black group-hover:text-white' : ''"
8
12
  [title]="displayName"
9
13
  >{{ displayName }}</span
10
14
  >
11
15
  <div
12
- class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
16
+ class="gn-ui-card-icon items-center justify-center w-10 h-8"
17
+ [class]="
18
+ !overlayOpen ? 'group-hover:border-white group-hover:text-white' : ''
19
+ "
13
20
  >
14
- <ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
21
+ @if (overlayOpen) {
22
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matClose"></ng-icon>
23
+ } @else {
24
+ <ng-icon class="!w-6 !h-6 !text-[24px]" name="matInfoOutline"></ng-icon>
25
+ }
15
26
  </div>
16
27
  </gn-ui-button>
28
+
29
+ <ng-template
30
+ cdkConnectedOverlay
31
+ [cdkConnectedOverlayOrigin]="overlayOrigin"
32
+ [cdkConnectedOverlayOpen]="overlayOpen"
33
+ [cdkConnectedOverlayPositions]="overlayPositions"
34
+ [cdkConnectedOverlayOffsetX]="overlayOffsetX"
35
+ (overlayOutsideClick)="closeOverlay()"
36
+ (detach)="closeOverlay()"
37
+ >
38
+ <div [style.width.px]="overlayWidth">
39
+ <gn-ui-contact-details [contact]="contact"></gn-ui-contact-details>
40
+ </div>
41
+ </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
  }
@@ -0,0 +1,13 @@
1
+ /** the animation values are made to be identical to the cdk overlay values */
2
+ .lightbox-image {
3
+ animation: show 400ms cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
4
+ opacity: 0;
5
+ transform: scale(0.9);
6
+ }
7
+
8
+ @keyframes show {
9
+ 100% {
10
+ opacity: 1;
11
+ transform: none;
12
+ }
13
+ }
@@ -1,5 +1,5 @@
1
1
  <gn-ui-content-ghost
2
- [showContent]="imageUrl !== undefined"
2
+ [showContent]="imageUrl() !== undefined"
3
3
  ghostClass="h-full w-full"
4
4
  >
5
5
  @if (imageUrl) {
@@ -9,7 +9,7 @@
9
9
  >
10
10
  <gn-ui-thumbnail
11
11
  class="relative h-full w-full"
12
- [thumbnailUrl]="imageUrl"
12
+ [thumbnailUrl]="imageUrl()"
13
13
  fit="cover"
14
14
  (placeholderShown)="isPlaceholderShown.emit($event)"
15
15
  ></gn-ui-thumbnail>
@@ -18,7 +18,7 @@
18
18
  class="absolute bottom-0 right-0 z-10 mr-2 mb-2"
19
19
  [type]="'outline'"
20
20
  [extraClass]="'!py-2 !px-0'"
21
- (buttonClick)="openLightbox(imageUrl)"
21
+ (buttonClick)="openLightbox()"
22
22
  >
23
23
  <ng-icon name="matZoomOutMap"></ng-icon>
24
24
  </gn-ui-button>
@@ -26,3 +26,13 @@
26
26
  </div>
27
27
  }
28
28
  </gn-ui-content-ghost>
29
+
30
+ <ng-template #imageTemplate>
31
+ <img
32
+ data-test="lightbox-image"
33
+ class="lightbox-image bg-white max-h-[90vh] max-w-[90vw]"
34
+ [src]="imageUrl()"
35
+ alt="Preview image"
36
+ (click)="closeLightbox()"
37
+ />
38
+ </ng-template>
@@ -1,10 +1,19 @@
1
- import { Component, EventEmitter, Input, Output } from '@angular/core'
2
- import * as basicLightbox from 'basiclightbox'
1
+ import {
2
+ Component,
3
+ inject,
4
+ input,
5
+ output,
6
+ TemplateRef,
7
+ viewChild,
8
+ ViewContainerRef,
9
+ } from '@angular/core'
3
10
  import { ContentGhostComponent } from '../content-ghost/content-ghost.component'
4
11
  import { ThumbnailComponent } from '../thumbnail/thumbnail.component'
5
12
  import { ButtonComponent } from '../../../../../../libs/ui/inputs/src'
6
13
  import { NgIcon, provideIcons, provideNgIconsConfig } from '@ng-icons/core'
7
14
  import { matZoomOutMap } from '@ng-icons/material-icons/baseline'
15
+ import { Overlay, OverlayRef } from '@angular/cdk/overlay'
16
+ import { TemplatePortal } from '@angular/cdk/portal'
8
17
 
9
18
  @Component({
10
19
  selector: 'gn-ui-image-overlay-preview',
@@ -20,9 +29,37 @@ import { matZoomOutMap } from '@ng-icons/material-icons/baseline'
20
29
  ],
21
30
  })
22
31
  export class ImageOverlayPreviewComponent {
23
- @Input() imageUrl: string
24
- @Output() isPlaceholderShown = new EventEmitter<boolean>()
25
- openLightbox(src: string) {
26
- basicLightbox.create(`<img src="${src}"/>`).show()
32
+ imageUrl = input<string>()
33
+ isPlaceholderShown = output<boolean>({})
34
+
35
+ private imageTemplate = viewChild<TemplateRef<HTMLElement>>('imageTemplate')
36
+ private overlay = inject(Overlay)
37
+ private viewContainerRef = inject(ViewContainerRef)
38
+ private overlayRef: OverlayRef
39
+
40
+ openLightbox() {
41
+ const positionStrategy = this.overlay
42
+ .position()
43
+ .global()
44
+ .centerHorizontally()
45
+ .centerVertically()
46
+ const scrollStrategy = this.overlay.scrollStrategies.block()
47
+
48
+ this.overlayRef = this.overlay.create({
49
+ hasBackdrop: true,
50
+ backdropClass: 'bg-black/80',
51
+ positionStrategy,
52
+ scrollStrategy,
53
+ })
54
+ this.overlayRef.backdropClick().subscribe(() => this.overlayRef.dispose())
55
+ const portal = new TemplatePortal(
56
+ this.imageTemplate(),
57
+ this.viewContainerRef
58
+ )
59
+ this.overlayRef.attach(portal)
60
+ }
61
+
62
+ closeLightbox() {
63
+ this.overlayRef?.dispose()
27
64
  }
28
65
  }
@@ -1,5 +1,5 @@
1
1
  import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
2
- import { marked } from 'marked'
2
+ import { parse } from 'marked'
3
3
 
4
4
  @Component({
5
5
  selector: 'gn-ui-markdown-parser',
@@ -14,6 +14,6 @@ export class MarkdownParserComponent {
14
14
  @Input() whitoutStyles?: boolean
15
15
 
16
16
  get parsedMarkdown() {
17
- return marked.parse(this.textContent ?? '')
17
+ return parse(this.textContent ?? '')
18
18
  }
19
19
  }
@@ -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() {