geonetwork-ui 2.10.0-dev.fb2893f44 → 2.10.0

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 (109) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1017 -468
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +112 -111
  4. package/index.d.ts.map +1 -1
  5. package/package.json +1 -1
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +11 -0
  7. package/src/libs/api/metadata-converter/src/lib/fixtures/eu.dcat-ap.records.ts +2 -0
  8. package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +22 -0
  9. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+ongules.ts +1 -0
  10. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+roilaye.ts +1 -0
  11. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.ts +1 -0
  12. package/src/libs/api/metadata-converter/src/lib/fixtures/geocat-ch.records.ts +1 -0
  13. package/src/libs/api/metadata-converter/src/lib/fixtures/georhena.records.ts +1 -0
  14. package/src/libs/api/metadata-converter/src/lib/fixtures/metadata-for-i18n.records.ts +2 -0
  15. package/src/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +22 -0
  16. package/src/libs/api/metadata-converter/src/lib/fixtures/opendataswiss.records.ts +1 -0
  17. package/src/libs/api/metadata-converter/src/lib/fixtures/sextant.records.ts +2 -0
  18. package/src/libs/api/metadata-converter/src/lib/fixtures/vlaanderen.dcat-ap.records.ts +1 -0
  19. package/src/libs/api/metadata-converter/src/lib/fixtures/wallonie.records.reuse.ts +1 -0
  20. package/src/libs/api/metadata-converter/src/lib/gn4/atomic-operations.ts +48 -7
  21. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts +1 -0
  22. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +4 -3
  23. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +1 -0
  24. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +4 -0
  25. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +13 -0
  26. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +45 -1
  27. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +13 -1
  28. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +40 -0
  29. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +47 -1
  30. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +12 -8
  31. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +9 -20
  32. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +12 -0
  33. package/src/libs/common/domain/src/lib/model/thesaurus/thesaurus.model.ts +2 -1
  34. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -1
  35. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +57 -0
  36. package/src/libs/feature/editor/src/index.ts +1 -0
  37. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +4 -1
  38. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +26 -19
  39. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +5 -1
  40. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +7 -6
  41. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +10 -8
  42. package/src/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.html +5 -14
  43. package/src/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.ts +3 -28
  44. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html +5 -5
  45. package/src/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts +2 -1
  46. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +1 -0
  47. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts +1 -1
  48. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-overviews/form-field-overviews.component.ts +12 -5
  49. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.html +4 -4
  50. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-rich/form-field-rich.component.ts +7 -0
  51. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.html +6 -2
  52. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-update-frequency/form-field-update-frequency.component.ts +3 -1
  53. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +27 -16
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +11 -0
  55. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +14 -5
  56. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +41 -0
  57. package/src/libs/feature/editor/src/lib/expressions.ts +380 -8
  58. package/src/libs/feature/editor/src/lib/fields.config.ts +295 -291
  59. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +6 -3
  60. package/src/libs/feature/editor/src/lib/services/editor.service.ts +8 -12
  61. package/src/libs/feature/notify-reuse/src/index.ts +1 -0
  62. package/src/libs/feature/notify-reuse/src/lib/edit-delete-reuse-buttons/edit-delete-reuse-buttons.component.css +0 -0
  63. package/src/libs/feature/notify-reuse/src/lib/edit-delete-reuse-buttons/edit-delete-reuse-buttons.component.html +23 -0
  64. package/src/libs/feature/notify-reuse/src/lib/edit-delete-reuse-buttons/edit-delete-reuse-buttons.component.ts +114 -0
  65. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +111 -1
  66. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +219 -8
  67. package/src/libs/feature/notify-reuse/src/lib/utils/url.ts +16 -0
  68. package/src/libs/feature/search/src/lib/results-table/results-table-container.component.ts +10 -5
  69. package/src/libs/ui/elements/src/index.ts +1 -0
  70. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +48 -56
  71. package/src/libs/ui/elements/src/lib/external-link-card/external-link-card.component.html +2 -1
  72. package/src/libs/ui/elements/src/lib/external-link-card/external-link-card.component.ts +2 -2
  73. package/src/libs/ui/elements/src/lib/keyword-badge/keyword-badge.component.html +14 -0
  74. package/src/libs/ui/elements/src/lib/keyword-badge/keyword-badge.component.ts +85 -0
  75. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +5 -0
  76. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +4 -7
  77. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +2 -2
  78. package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html +4 -5
  79. package/src/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +4 -0
  80. package/src/libs/ui/inputs/src/lib/badge/badge.component.html +33 -26
  81. package/src/libs/ui/inputs/src/lib/badge/badge.component.ts +9 -0
  82. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
  83. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +3 -3
  84. package/src/libs/ui/inputs/src/lib/text-input/text-input.component.css +0 -3
  85. package/src/libs/ui/inputs/src/lib/text-input/text-input.component.html +11 -7
  86. package/src/libs/ui/inputs/src/lib/text-input/text-input.component.ts +56 -30
  87. package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.html +5 -5
  88. package/src/libs/ui/layout/src/lib/form-field-wrapper/form-field-wrapper.component.ts +9 -1
  89. package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html +1 -1
  90. package/src/libs/ui/layout/src/lib/interactive-table/interactive-table.component.ts +3 -8
  91. package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.html +7 -7
  92. package/src/libs/ui/layout/src/lib/sortable-list/sortable-list.component.ts +14 -1
  93. package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.html +12 -8
  94. package/src/libs/ui/search/src/lib/results-table/action-menu/action-menu.component.ts +3 -1
  95. package/src/libs/ui/search/src/lib/results-table/results-table.component.html +1 -0
  96. package/src/libs/ui/widgets/src/lib/popover/popover.component.ts +7 -1
  97. package/src/libs/ui/widgets/src/lib/spinning-loader/spinning-loader.component.html +1 -1
  98. package/src/libs/util/shared/src/lib/links/link-utils.ts +22 -0
  99. package/tailwind.base.css +9 -2
  100. package/translations/de.json +84 -31
  101. package/translations/en.json +94 -41
  102. package/translations/es.json +58 -5
  103. package/translations/fr.json +91 -38
  104. package/translations/it.json +85 -32
  105. package/translations/nl.json +57 -4
  106. package/translations/pt.json +58 -5
  107. package/translations/sk.json +58 -5
  108. package/material-styles.css +0 -483
  109. package/src/libs/feature/editor/src/lib/components/generic-keywords/generic-keywords.component.css +0 -3
@@ -1,12 +1,12 @@
1
1
  <div
2
- class="bg-gray-50 text-black rounded border border-gray-200 shadow-md p-4 flex flex-col gap-3 w-full"
2
+ class="bg-gray-50 text-black rounded border border-gray-200 shadow-md p-4 flex flex-col gap-2 w-full"
3
3
  data-test="contact-details"
4
4
  >
5
5
  @if (displayName) {
6
- <div class="flex items-center gap-3">
6
+ <div class="flex items-center gap-2.5">
7
7
  @if (organization?.logoUrl?.href) {
8
8
  <div
9
- class="flex items-center justify-center rounded-md bg-white w-14 h-14 shrink-0 overflow-hidden"
9
+ class="flex items-center justify-center rounded-lg bg-white w-14 h-14 shrink-0 overflow-hidden"
10
10
  >
11
11
  <gn-ui-thumbnail
12
12
  class="relative h-full w-full"
@@ -23,74 +23,66 @@
23
23
  </div>
24
24
  }
25
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>
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
+ >
35
31
  @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
- >
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>
42
44
  }
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
45
  @if (contact.phone) {
52
- <span class="text-sm" data-test="contact-details-phone">{{
53
- contact.phone
54
- }}</span>
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
55
  }
56
56
  </div>
57
- </div>
57
+ }
58
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) {
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>
68
65
  <div class="flex flex-col" data-test="contact-details-address">
69
66
  @for (line of addressLines; track line) {
70
67
  <p class="text-sm m-0">{{ line }}</p>
71
68
  }
72
69
  </div>
73
- }
74
- </div>
70
+ </div>
71
+ }
75
72
 
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">
73
+ @if (organization?.website) {
74
+ <div class="bg-gray-100 rounded-lg flex items-center gap-2 px-3 py-2">
79
75
  <ng-icon
80
- class="!w-5 !h-5 !text-[20px] shrink-0"
81
- [class.opacity-30]="!organization.website"
82
- [class.opacity-75]="!!organization.website"
76
+ class="!w-5 !h-5 !text-[20px] shrink-0 opacity-30"
83
77
  name="matOpenInNew"
84
78
  ></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
- }
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
+ >
94
86
  </div>
95
87
  }
96
88
  </div>
@@ -54,8 +54,9 @@
54
54
  class="inline-flex items-center justify-center px-2 py-1 text-13 font-medium leading-none text-white rounded transition-opacity opacity-70 group-hover:opacity-100"
55
55
  [style.background-color]="getLinkColor(link)"
56
56
  data-cy="download-format"
57
+ translate
57
58
  >
58
- {{ getLinkFormat(link) || ('downloads.format.unknown' | translate) }}
59
+ downloads.format.{{ getLinkFormat(link) || 'unknown' }}
59
60
  </span>
60
61
  </div>
61
62
  </div>
@@ -9,7 +9,7 @@ import {
9
9
  import { matOpenInNew } from '@ng-icons/material-icons/baseline'
10
10
  import { iconoirDatabase } from '@ng-icons/iconoir'
11
11
  import { getBadgeColor, getFileFormat } from '../../../../../../libs/util/shared/src'
12
- import { TranslatePipe } from '@ngx-translate/core'
12
+ import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
13
13
 
14
14
  type CardSize = 'L' | 'M' | 'S' | 'XS'
15
15
  @Component({
@@ -18,7 +18,7 @@ type CardSize = 'L' | 'M' | 'S' | 'XS'
18
18
  styleUrls: ['./external-link-card.component.css'],
19
19
  changeDetection: ChangeDetectionStrategy.OnPush,
20
20
  standalone: true,
21
- imports: [CommonModule, NgIconComponent, TranslatePipe],
21
+ imports: [CommonModule, NgIconComponent, TranslateDirective, TranslatePipe],
22
22
  providers: [
23
23
  provideIcons({
24
24
  matOpenInNew,
@@ -0,0 +1,14 @@
1
+ <gn-ui-badge
2
+ [clickable]="!editable"
3
+ [removable]="editable"
4
+ (badgeClicked)="keywordClick.emit(keyword)"
5
+ (badgeRemoveClicked)="keywordRemove.emit(keyword)"
6
+ ><ng-container>{{ keyword.label }}</ng-container>
7
+ @if (editable && isPlaceWithoutExtent) {
8
+ <ng-icon
9
+ [title]="'editor.record.form.keywords.place.withoutExtent' | translate"
10
+ class="!text-[1.4em] ml-1"
11
+ name="matWarningAmberOutline"
12
+ ></ng-icon>
13
+ }
14
+ </gn-ui-badge>
@@ -0,0 +1,85 @@
1
+ import {
2
+ AfterViewInit,
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ ElementRef,
6
+ EventEmitter,
7
+ inject,
8
+ Input,
9
+ Output,
10
+ ViewChild,
11
+ } from '@angular/core'
12
+ import { Keyword } from '../../../../../../libs/common/domain/src/lib/model/record'
13
+ import { BadgeComponent } from '../../../../../../libs/ui/inputs/src'
14
+ import { TranslatePipe, TranslateService } from '@ngx-translate/core'
15
+ import { marker } from '@biesbjerg/ngx-translate-extract-marker'
16
+ import {
17
+ NgIconComponent,
18
+ provideIcons,
19
+ provideNgIconsConfig,
20
+ } from '@ng-icons/core'
21
+ import { matWarningAmberOutline } from '@ng-icons/material-icons/outline'
22
+ import tippy from 'tippy.js'
23
+
24
+ marker('domain.record.keywordType.theme')
25
+ marker('domain.record.keywordType.place')
26
+ marker('domain.record.keywordType.temporal')
27
+ marker('domain.record.keywordType.other')
28
+
29
+ @Component({
30
+ selector: 'gn-ui-keyword-badge',
31
+ templateUrl: './keyword-badge.component.html',
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ standalone: true,
34
+ imports: [BadgeComponent, NgIconComponent, TranslatePipe],
35
+ providers: [
36
+ provideIcons({
37
+ matWarningAmberOutline,
38
+ }),
39
+ provideNgIconsConfig({
40
+ size: '1.5em',
41
+ }),
42
+ ],
43
+ })
44
+ export class KeywordBadgeComponent implements AfterViewInit {
45
+ private translateService = inject(TranslateService)
46
+
47
+ @Input() keyword: Keyword
48
+ @Input() editable = false // if false, keyword is only clickable
49
+ @Output() keywordClick = new EventEmitter<Keyword>()
50
+ @Output() keywordRemove = new EventEmitter<Keyword>()
51
+ @ViewChild(BadgeComponent, { read: ElementRef }) badgeComponent: ElementRef
52
+
53
+ get segments(): string[] {
54
+ if (this.keyword.hierarchyPath?.length) {
55
+ return this.keyword.hierarchyPath
56
+ }
57
+ if (this.keyword.thesaurus?.name) {
58
+ return [this.keyword.thesaurus.name, this.keyword.label]
59
+ }
60
+ return [
61
+ this.translateService.instant(
62
+ `domain.record.keywordType.${this.keyword.type}`
63
+ ),
64
+ this.keyword.label,
65
+ ]
66
+ }
67
+
68
+ get isPlaceWithoutExtent(): boolean {
69
+ return this.keyword.type === 'place' && !this.keyword.bbox
70
+ }
71
+
72
+ ngAfterViewInit() {
73
+ const content = this.segments
74
+ .map(
75
+ (segment, i, arr) =>
76
+ `<span ${i === arr.length - 1 ? 'class="font-bold"' : ''}>${segment}</span>`
77
+ )
78
+ .join(' &gt; ')
79
+ tippy(this.badgeComponent.nativeElement, {
80
+ appendTo: () => document.body,
81
+ content,
82
+ allowHTML: true,
83
+ })
84
+ }
85
+ }
@@ -16,3 +16,8 @@
16
16
  --gn-ui-badge-background-color: var(--color-primary-white);
17
17
  --gn-ui-badge-text-color: var(--color-primary-darkest);
18
18
  }
19
+
20
+ .metadata-info-keywords {
21
+ --gn-ui-badge-padding: 0.25rem 0.5rem;
22
+ text-transform: lowercase;
23
+ }
@@ -28,13 +28,10 @@
28
28
  <gn-ui-max-lines [maxLines]="7">
29
29
  <div class="metadata-info-keywords sm:pb-4 flex flex-wrap gap-2">
30
30
  @for (keyword of metadata.keywords; track keyword) {
31
- <gn-ui-button
32
- type="primary-light"
33
- class="inline-block opacity-70"
34
- extraClass="lowercase text-sm py-1 px-2"
35
- (buttonClick)="onKeywordClick(keyword)"
36
- >{{ keyword.label }}</gn-ui-button
37
- >
31
+ <gn-ui-keyword-badge
32
+ [keyword]="keyword"
33
+ (keywordClick)="onKeywordClick($event)"
34
+ ></gn-ui-keyword-badge>
38
35
  }
39
36
  </div>
40
37
  </gn-ui-max-lines>
@@ -22,10 +22,10 @@ import {
22
22
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
23
23
  import {
24
24
  BadgeComponent,
25
- ButtonComponent,
26
25
  CopyTextButtonComponent,
27
26
  } from '../../../../../../libs/ui/inputs/src'
28
27
  import { ContentGhostComponent } from '../content-ghost/content-ghost.component'
28
+ import { KeywordBadgeComponent } from '../keyword-badge/keyword-badge.component'
29
29
  import { NgIcon, provideIcons } from '@ng-icons/core'
30
30
  import { matOpenInNew } from '@ng-icons/material-icons/baseline'
31
31
  import { matMailOutline } from '@ng-icons/material-icons/outline'
@@ -46,7 +46,6 @@ import { ContactPillComponent } from '../contact-pill/contact-pill.component'
46
46
  TranslatePipe,
47
47
  MarkdownParserComponent,
48
48
  ExpandablePanelComponent,
49
- ButtonComponent,
50
49
  BadgeComponent,
51
50
  ContentGhostComponent,
52
51
  MaxLinesComponent,
@@ -56,6 +55,7 @@ import { ContactPillComponent } from '../contact-pill/contact-pill.component'
56
55
  GnUiHumanizeDateDirective,
57
56
  SpatialExtentComponent,
58
57
  ContactPillComponent,
58
+ KeywordBadgeComponent,
59
59
  ],
60
60
  viewProviders: [
61
61
  provideIcons({
@@ -52,12 +52,11 @@
52
52
  </gn-ui-text-input>
53
53
  @if (!supportOffset) {
54
54
  <div class="flex items-center gap-2 text-orange-500 z-10 ml-3">
55
- <span
56
- class="material-symbols-outlined"
55
+ <ng-icon
56
+ name="matWarningOutline"
57
+ size="24"
57
58
  matTooltip="Not supported on this service"
58
- >
59
- warning
60
- </span>
59
+ ></ng-icon>
61
60
  </div>
62
61
  }
63
62
  </div>
@@ -15,6 +15,8 @@ import {
15
15
  import { CommonModule } from '@angular/common'
16
16
  import { TranslateDirective } from '@ngx-translate/core'
17
17
  import { MatTooltipModule } from '@angular/material/tooltip'
18
+ import { NgIconComponent, provideIcons } from '@ng-icons/core'
19
+ import { matWarningOutline } from '@ng-icons/material-icons/outline'
18
20
 
19
21
  const DEFAULT_PARAMS = {
20
22
  OFFSET: '',
@@ -35,7 +37,9 @@ const DEFAULT_PARAMS = {
35
37
  CopyTextButtonComponent,
36
38
  TranslateDirective,
37
39
  MatTooltipModule,
40
+ NgIconComponent,
38
41
  ],
42
+ providers: [provideIcons({ matWarningOutline })],
39
43
  })
40
44
  export class RecordApiFormComponent {
41
45
  @Input() set apiLink(value: DatasetServiceDistribution) {
@@ -1,27 +1,34 @@
1
- <div
2
- class="gn-ui-badge flex items-center gap-1.5"
3
- [ngClass]="
4
- clickable
5
- ? 'hover:bg-primary cursor-pointer transition-colors duration-100'
6
- : ''
7
- "
8
- >
1
+ <ng-template #badgeContent>
9
2
  <span><ng-content></ng-content></span>
10
- @if (removable) {
11
- <gn-ui-button
12
- type="light"
13
- (buttonClick)="removeBadge()"
14
- extraClass="text-xs border-0 px-0 py-0"
15
- style="
16
- --gn-ui-button-padding: 0;
17
- --gn-ui-button-font-size: 0.8em;
18
- --gn-ui-button-width: 1.4em;
19
- --gn-ui-button-height: 1.4em;
20
- --gn-ui-button-rounded: 1.4em;
21
- --gn-ui-button-background: var(--gn-ui-badge-background-color, white);
22
- "
23
- >
24
- <ng-icon name="matClose"></ng-icon>
25
- </gn-ui-button>
26
- }
27
- </div>
3
+ </ng-template>
4
+
5
+ @if (clickable) {
6
+ <button
7
+ type="button"
8
+ class="gn-ui-badge-clickable"
9
+ (click)="handleClick($event)"
10
+ >
11
+ <ng-container [ngTemplateOutlet]="badgeContent"></ng-container>
12
+ </button>
13
+ } @else {
14
+ <span class="gn-ui-badge gap-1.5">
15
+ <ng-container [ngTemplateOutlet]="badgeContent"></ng-container>
16
+ @if (removable) {
17
+ <gn-ui-button
18
+ type="light"
19
+ (buttonClick)="removeBadge()"
20
+ extraClass="text-xs border-0 px-0 py-0"
21
+ style="
22
+ --gn-ui-button-padding: 0;
23
+ --gn-ui-button-font-size: 0.8em;
24
+ --gn-ui-button-width: 1.4em;
25
+ --gn-ui-button-height: 1.4em;
26
+ --gn-ui-button-rounded: 1.4em;
27
+ --gn-ui-button-background: var(--gn-ui-badge-background-color, white);
28
+ "
29
+ >
30
+ <ng-icon name="matClose"></ng-icon>
31
+ </gn-ui-button>
32
+ }
33
+ </span>
34
+ }
@@ -13,6 +13,7 @@ import {
13
13
  provideNgIconsConfig,
14
14
  } from '@ng-icons/core'
15
15
  import { matClose } from '@ng-icons/material-icons/baseline'
16
+ import { propagateToDocumentOnly } from '../../../../../../libs/util/shared/src'
16
17
 
17
18
  @Component({
18
19
  selector: 'gn-ui-badge',
@@ -34,8 +35,16 @@ export class BadgeComponent {
34
35
  @Input() clickable? = false
35
36
  @Input() removable? = false
36
37
  @Output() badgeRemoveClicked = new EventEmitter<void>()
38
+ @Output() badgeClicked = new EventEmitter<void>()
37
39
 
38
40
  removeBadge() {
39
41
  this.badgeRemoveClicked.emit()
40
42
  }
43
+
44
+ handleClick(event: MouseEvent) {
45
+ this.badgeClicked.emit()
46
+ event.preventDefault()
47
+ ;(event.currentTarget as HTMLElement).blur()
48
+ propagateToDocumentOnly(event)
49
+ }
41
50
  }
@@ -51,7 +51,7 @@
51
51
  <div
52
52
  class="bg-white border border-gray-300 rounded shadow-lg py-2 w-full overflow-x-hidden overflow-y-auto overlay-container"
53
53
  [style.max-height]="overlayMaxHeight"
54
- [style.width]="overlayWidth"
54
+ [style.min-width]="overlayMinWidth"
55
55
  role="listbox"
56
56
  tabindex="-1"
57
57
  [attr.id]="id"
@@ -3,12 +3,12 @@ import {
3
3
  Component,
4
4
  ElementRef,
5
5
  EventEmitter,
6
+ inject,
6
7
  Input,
7
8
  Output,
8
9
  QueryList,
9
10
  ViewChild,
10
11
  ViewChildren,
11
- inject,
12
12
  } from '@angular/core'
13
13
  import {
14
14
  CdkConnectedOverlay,
@@ -85,7 +85,7 @@ export class DropdownMultiselectComponent {
85
85
  ]
86
86
  scrollStrategy = this.scrollStrategies.reposition()
87
87
  overlayOpen = false
88
- overlayWidth = 'auto'
88
+ overlayMinWidth = 'none'
89
89
  overlayMaxHeight = 'none'
90
90
  id = `dropdown-multiselect-${Math.floor(Math.random() * 10000)}`
91
91
 
@@ -120,7 +120,7 @@ export class DropdownMultiselectComponent {
120
120
  }
121
121
 
122
122
  openOverlay() {
123
- this.overlayWidth =
123
+ this.overlayMinWidth =
124
124
  this.overlayOrigin.elementRef.nativeElement.getBoundingClientRect()
125
125
  .width + 'px'
126
126
  this.overlayMaxHeight = this.maxRows
@@ -1,3 +0,0 @@
1
- .invalid {
2
- border-color: var(--color-secondary);
3
- }
@@ -1,13 +1,17 @@
1
1
  <input
2
2
  #input
3
3
  class="gn-ui-text-input"
4
- [ngClass]="extraClass"
5
- type="text"
6
- [value]="value"
4
+ [class]="extraClass()"
5
+ [ngClass]="{
6
+ 'border-red-400 focus:border-red-800 hover:border-red-800 text-red-800':
7
+ !isValid(),
8
+ }"
9
+ [type]="type()"
10
+ [(ngModel)]="inputModel"
7
11
  (change)="handleChange($event)"
8
12
  (input)="handleChange($event)"
9
- [placeholder]="placeholder"
10
- [attr.aria-label]="placeholder"
11
- [attr.required]="required || null"
12
- [disabled]="disabled"
13
+ [placeholder]="placeholder()"
14
+ [attr.aria-label]="placeholder()"
15
+ [attr.required]="required() || null"
16
+ [disabled]="disabled()"
13
17
  />
@@ -1,45 +1,71 @@
1
1
  import {
2
- AfterViewInit,
2
+ ChangeDetectionStrategy,
3
3
  Component,
4
- Input,
5
- Output,
6
- ViewChild,
4
+ computed,
5
+ ElementRef,
6
+ input,
7
+ output,
8
+ signal,
9
+ viewChild,
7
10
  } from '@angular/core'
8
- import { distinctUntilChanged } from 'rxjs/operators'
9
- import { Subject } from 'rxjs'
10
- import { CommonModule } from '@angular/common'
11
+ import { NgClass } from '@angular/common'
12
+ import { FormsModule } from '@angular/forms'
11
13
 
12
14
  @Component({
13
15
  selector: 'gn-ui-text-input',
14
16
  templateUrl: './text-input.component.html',
15
17
  styleUrls: ['./text-input.component.css'],
16
18
  standalone: true,
17
- imports: [CommonModule],
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ imports: [NgClass, FormsModule],
18
21
  })
19
- export class TextInputComponent implements AfterViewInit {
20
- @Input() value = ''
21
- @Input() extraClass = ''
22
- @Input() placeholder: string
23
- @Input() required = false
24
- @Input() disabled: boolean
25
- rawChange = new Subject<string>()
26
- @Output() valueChange = this.rawChange.pipe(distinctUntilChanged())
27
- @ViewChild('input') input
22
+ export class TextInputComponent {
23
+ value = input<string, string>('', {
24
+ transform: (v: string) => {
25
+ this.inputModel.set(v)
26
+ // make sure the native element value is up to date
27
+ // TODO: use form() signal when migrated to angular 22 for cleaner implementation
28
+ if (this.inputRef()) {
29
+ this.inputRef().nativeElement.value = v
30
+ }
31
+ return v
32
+ },
33
+ })
34
+ extraClass = input('')
35
+ placeholder = input<string>()
36
+ required = input(false)
37
+ disabled = input<boolean>()
38
+ type = input('text', {
39
+ transform: (type: string) => {
40
+ // make sure the native element type is up to date
41
+ // TODO: use form() signal when migrated to angular 22 for cleaner implementation
42
+ if (this.inputRef()) {
43
+ this.inputRef().nativeElement.type = type
44
+ }
45
+ return type
46
+ },
47
+ })
28
48
 
29
- ngAfterViewInit() {
30
- this.checkRequired(this.input.nativeElement.value)
31
- }
49
+ valueChange = output<string>()
32
50
 
33
- checkRequired(value) {
34
- this.input.nativeElement.classList.toggle(
35
- 'invalid',
36
- this.required && value === ''
37
- )
38
- }
51
+ inputModel = signal('')
52
+ inputRef = viewChild<ElementRef<HTMLInputElement>>('input')
53
+
54
+ isValid = computed<boolean>(() => {
55
+ const isValid = this.inputRef().nativeElement.checkValidity()
56
+ const isEmpty = this.inputModel().trim() === ''
57
+ const isRequired = this.required()
58
+ return isValid && !(isEmpty && isRequired)
59
+ })
60
+
61
+ private lastEmittedValue: string | undefined = undefined
39
62
 
40
- handleChange($event) {
41
- const value = $event.target.value
42
- this.checkRequired(value)
43
- this.rawChange.next(value)
63
+ handleChange($event: Event) {
64
+ const input = $event.target as HTMLInputElement
65
+ const value = input.value
66
+ if (value === this.lastEmittedValue) return
67
+ this.inputModel.set(value)
68
+ this.lastEmittedValue = value
69
+ this.valueChange.emit(value)
44
70
  }
45
71
  }
@@ -3,14 +3,14 @@
3
3
  <span class="flex-none font-bold" translate="">{{ label }}</span>
4
4
  <div class="flex-1 flex justify-end items-center">
5
5
  <ng-content select="[form-field-interaction]"></ng-content>
6
- <span
6
+ <ng-icon
7
7
  *ngIf="hint"
8
- class="material-symbols-outlined m-2 gn-ui-icon-small"
8
+ name="matHelpOutline"
9
+ size="16"
10
+ class="m-2"
9
11
  [matTooltip]="hint"
10
12
  matTooltipPosition="above"
11
- >
12
- help
13
- </span>
13
+ ></ng-icon>
14
14
  </div>
15
15
  </div>
16
16
  <div class="flex-1 mt-2">