geonetwork-ui 2.10.0-dev.e40bfbf0e → 2.10.0-dev.e7b9cb6a0

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 (96) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1401 -934
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +124 -30
  4. package/index.d.ts.map +1 -1
  5. package/package.json +2 -2
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +9 -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 +1 -0
  9. package/src/libs/api/metadata-converter/src/lib/fixtures/geo2france.records.reuse+ongules.ts +7 -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 +1 -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 +8 -0
  20. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.converter.ts +1 -0
  21. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +7 -0
  22. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +8 -0
  23. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +8 -0
  24. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +11 -0
  25. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +33 -0
  26. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +33 -0
  27. package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +4 -0
  28. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +68 -34
  29. package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
  30. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +46 -0
  31. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -0
  32. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  33. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  34. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  35. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +5 -0
  36. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +6 -0
  37. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +0 -1
  38. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +10 -1
  39. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  40. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  41. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  42. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.html +18 -3
  43. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.ts +33 -40
  44. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +38 -0
  45. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +33 -34
  46. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +13 -13
  47. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +18 -4
  48. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.html +8 -7
  49. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts-for-resource/form-field-contacts-for-resource.component.ts +6 -6
  50. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.css +0 -0
  51. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.html +5 -0
  52. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-single-link-resource/form-field-online-single-link-resource.component.ts +89 -0
  53. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +37 -0
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
  55. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -0
  56. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  57. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +59 -3
  58. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
  59. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  60. package/src/libs/feature/notify-reuse/src/index.ts +1 -0
  61. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css +0 -0
  62. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +1 -0
  63. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +21 -0
  64. package/src/libs/ui/elements/src/index.ts +2 -0
  65. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -0
  66. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  67. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +37 -0
  68. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
  69. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
  70. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
  71. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  72. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  73. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +28 -67
  74. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  75. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
  76. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  77. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  78. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -1
  79. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +11 -10
  80. package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
  81. package/src/libs/util/app-config/src/lib/app-config.ts +49 -0
  82. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  83. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  84. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  85. package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
  86. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  87. package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
  88. package/tailwind.base.css +11 -2
  89. package/translations/de.json +10 -1
  90. package/translations/en.json +10 -1
  91. package/translations/es.json +10 -1
  92. package/translations/fr.json +10 -1
  93. package/translations/it.json +10 -1
  94. package/translations/nl.json +10 -1
  95. package/translations/pt.json +10 -1
  96. package/translations/sk.json +10 -1
@@ -134,6 +134,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
134
134
  lineage: `This record was edited manually to test the conversion processes
135
135
 
136
136
  As such, **it is not very interesting at all.**`,
137
+ sourceRecords: [],
137
138
  licenses: [
138
139
  {
139
140
  text: 'Licence ODbL mai 2013 (basée sur ODbL 1.0)',
@@ -310,6 +311,7 @@ Malgré l'attention portée à la création de ces données, il est rappelé que
310
311
  ],
311
312
  lineage: `Document d’urbanisme numérisé conformément aux prescriptions nationales du CNIG par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne.
312
313
  Ce lot de données produit en 2019, a été numérisé à partir du PCI Vecteur de 2019 et contrôlé par le Service d'Information Géographique de l'Agglomération de la Région de Compiègne.`,
314
+ sourceRecords: [],
313
315
  legalConstraints: [],
314
316
  securityConstraints: [],
315
317
  otherConstraints: [],
@@ -361,6 +363,7 @@ export const simpleDatasetRecordFixture = (): DatasetRecord => ({
361
363
  overviews: [],
362
364
  spatialExtents: [],
363
365
  temporalExtents: [],
366
+ sourceRecords: [],
364
367
  onlineResources: [
365
368
  {
366
369
  type: 'download',
@@ -413,6 +416,7 @@ export const simpleDatasetRecordWithFcatsFixture = (): DatasetRecord => ({
413
416
  overviews: [],
414
417
  spatialExtents: [],
415
418
  temporalExtents: [],
419
+ sourceRecords: [],
416
420
  onlineResources: [],
417
421
  updateFrequency: { per: 'month', updatedTimes: 3 },
418
422
  translations: {},
@@ -978,6 +982,7 @@ export const multilingualDatasetFixture: () => DatasetRecord = () => ({
978
982
  title: 'English Title',
979
983
  abstract: 'English Abstract',
980
984
  lineage: 'English Lineage',
985
+ sourceRecords: [],
981
986
  translations: {
982
987
  title: { fr: 'Titre Français', de: 'Titel DE' },
983
988
  abstract: { fr: 'Résumé Français', de: 'Beschreibung DE' },
@@ -1,6 +1,7 @@
1
1
  import { createAction, props } from '@ngrx/store'
2
2
  import {
3
3
  CatalogRecord,
4
+ CatalogRecordKeys,
4
5
  LanguageCode,
5
6
  } from '../../../../../../libs/common/domain/src/lib/model/record'
6
7
  import { SaveRecordError } from './editor.models'
@@ -73,3 +74,8 @@ export const canEditRecord = createAction(
73
74
  '[Editor] User can edit record',
74
75
  props<{ canEditRecord: boolean }>()
75
76
  )
77
+
78
+ export const setFocusedField = createAction(
79
+ '[Editor] Set focused field',
80
+ props<{ model: CatalogRecordKeys }>()
81
+ )
@@ -12,7 +12,6 @@ import {
12
12
  } from './editor.selectors'
13
13
  import { RecordsRepositoryInterface } from '../../../../../../libs/common/domain/src/lib/repository/records-repository.interface'
14
14
  import { PlatformServiceInterface } from '../../../../../../libs/common/domain/src/lib/platform.service.interface'
15
-
16
15
  @Injectable()
17
16
  export class EditorEffects {
18
17
  private actions$ = inject(Actions)
@@ -4,9 +4,10 @@ import * as EditorActions from './editor.actions'
4
4
  import * as EditorSelectors from './editor.selectors'
5
5
  import {
6
6
  CatalogRecord,
7
+ CatalogRecordKeys,
7
8
  LanguageCode,
8
9
  } from '../../../../../../libs/common/domain/src/lib/model/record'
9
- import { filter } from 'rxjs'
10
+ import { filter, map } from 'rxjs'
10
11
  import { Actions, ofType } from '@ngrx/effects'
11
12
  import { EditorConfig, EditorFieldIdentification } from '../models'
12
13
 
@@ -37,6 +38,10 @@ export class EditorFacade {
37
38
  )
38
39
  isPublished$ = this.store.pipe(select(EditorSelectors.selectIsPublished))
39
40
  canEditRecord$ = this.store.pipe(select(EditorSelectors.selectCanEditRecord))
41
+ focusedField$ = this.actions$.pipe(
42
+ ofType(EditorActions.setFocusedField),
43
+ map(({ model }) => model)
44
+ )
40
45
 
41
46
  openRecord(record: CatalogRecord, recordSource: string) {
42
47
  this.store.dispatch(
@@ -77,6 +82,10 @@ export class EditorFacade {
77
82
  this.store.dispatch(EditorActions.setCurrentPage({ page }))
78
83
  }
79
84
 
85
+ setFocusedField(model: CatalogRecordKeys) {
86
+ this.store.dispatch(EditorActions.setFocusedField({ model }))
87
+ }
88
+
80
89
  setFieldVisibility(field: EditorFieldIdentification, visible: boolean) {
81
90
  this.store.dispatch(EditorActions.setFieldVisibility({ field, visible }))
82
91
  }
@@ -0,0 +1,67 @@
1
+ <div class="flex flex-col gap-7">
2
+ <div class="grid grid-cols-2 gap-4">
3
+ <div class="min-w-0">
4
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
5
+ editor.record.form.field.contactDetails.lastName
6
+ </h3>
7
+ <gn-ui-text-input
8
+ class="block w-full"
9
+ extraClass="w-full"
10
+ [(value)]="contact.lastName"
11
+ (valueChange)="emitContactChange()"
12
+ [placeholder]="
13
+ 'editor.record.form.field.contactDetails.lastName.placeholder'
14
+ | translate
15
+ "
16
+ data-test="contactDetailsLastName"
17
+ ></gn-ui-text-input>
18
+ </div>
19
+ <div class="min-w-0">
20
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
21
+ editor.record.form.field.contactDetails.firstName
22
+ </h3>
23
+ <gn-ui-text-input
24
+ class="block w-full"
25
+ extraClass="w-full"
26
+ [(value)]="contact.firstName"
27
+ (valueChange)="emitContactChange()"
28
+ [placeholder]="
29
+ 'editor.record.form.field.contactDetails.firstName.placeholder'
30
+ | translate
31
+ "
32
+ data-test="contactDetailsFirstName"
33
+ ></gn-ui-text-input>
34
+ </div>
35
+ </div>
36
+ <div>
37
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
38
+ editor.record.form.field.contactDetails.email
39
+ </h3>
40
+ <gn-ui-text-input
41
+ class="block w-full"
42
+ extraClass="w-full"
43
+ [value]="contact.organization?.email ?? ''"
44
+ (valueChange)="handleOrganizationChange({ email: $event })"
45
+ [placeholder]="
46
+ 'editor.record.form.field.contactDetails.email.placeholder' | translate
47
+ "
48
+ data-test="contactDetailsEmail"
49
+ ></gn-ui-text-input>
50
+ </div>
51
+ <div>
52
+ <h3 class="text-[16px] font-bold text-main mb-[12px]" translate>
53
+ editor.record.form.field.contactDetails.organization
54
+ </h3>
55
+ <gn-ui-text-input
56
+ class="block w-full"
57
+ extraClass="w-full"
58
+ [value]="contact.organization?.name ?? ''"
59
+ (valueChange)="handleOrganizationChange({ name: $event })"
60
+ [placeholder]="
61
+ 'editor.record.form.field.contactDetails.organization.placeholder'
62
+ | translate
63
+ "
64
+ data-test="contactDetailsOrganization"
65
+ ></gn-ui-text-input>
66
+ </div>
67
+ </div>
@@ -0,0 +1,39 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ Input,
6
+ Output,
7
+ } from '@angular/core'
8
+ import {
9
+ Individual,
10
+ Organization,
11
+ } from '../../../../../../../libs/common/domain/src/lib/model/record'
12
+ import { TextInputComponent } from '../../../../../../../libs/ui/inputs/src'
13
+ import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
14
+
15
+ @Component({
16
+ selector: 'gn-ui-contact-details-form',
17
+ templateUrl: './contact-details-form.component.html',
18
+ styleUrls: ['./contact-details-form.component.css'],
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ standalone: true,
21
+ imports: [TextInputComponent, TranslateDirective, TranslatePipe],
22
+ })
23
+ export class ContactDetailsFormComponent {
24
+ @Input() contact: Individual
25
+ @Output() contactChange = new EventEmitter<Individual>()
26
+
27
+ emitContactChange() {
28
+ this.contactChange.emit(this.contact)
29
+ }
30
+
31
+ handleOrganizationChange(change: Partial<Organization>) {
32
+ this.contact.organization = {
33
+ ...(this.contact.organization ?? ({} as Organization)),
34
+ ...change,
35
+ } as Organization
36
+
37
+ this.emitContactChange()
38
+ }
39
+ }
@@ -7,11 +7,26 @@
7
7
  >editor.record.form.metadataQuality.title</span
8
8
  >
9
9
  </div>
10
- @for (properties of propertiesByPage; track properties; let i = $index) {
10
+ @for (
11
+ properties of (propertiesByPage$ | async) ?? [];
12
+ track properties;
13
+ let isLast = $last
14
+ ) {
11
15
  <div class="flex flex-col gap-2">
12
16
  @for (property of properties; track property) {
13
17
  <gn-ui-button
14
- [extraClass]="getExtraClass(property.value)"
18
+ style="
19
+ --gn-ui-button-justify: space-between;
20
+ --gn-ui-button-height: 34px;
21
+ --gn-ui-button-width: 100%;
22
+ --gn-ui-button-border-width: 0;
23
+ --gn-ui-button-color: black;
24
+ --gn-ui-button-background: transparent;
25
+ --gn-ui-button-bg-hover: #f3f4f6;
26
+ --gn-ui-button-disabled-opacity: 1;
27
+ "
28
+ [disabled]="property.value"
29
+ (buttonClick)="onCriterionClick(property)"
15
30
  type="outline"
16
31
  attr.data-cy="md-quality-btn-{{ property.label }}"
17
32
  >
@@ -28,7 +43,7 @@
28
43
  </div>
29
44
  </gn-ui-button>
30
45
  }
31
- @if (i !== propertiesByPage.length - 1) {
46
+ @if (!isLast) {
32
47
  <hr class="border-gray-300 w-11/12 mx-auto" />
33
48
  }
34
49
  </div>
@@ -1,5 +1,5 @@
1
- import { Component, Input, OnChanges } from '@angular/core'
2
- import { CatalogRecord } from '../../../../../../../libs/common/domain/src/lib/model/record'
1
+ import { Component, inject } from '@angular/core'
2
+ import { CatalogRecordKeys } from '../../../../../../../libs/common/domain/src/lib/model/record'
3
3
  import { ButtonComponent } from '../../../../../../../libs/ui/inputs/src'
4
4
  import {
5
5
  getAllKeysValidator,
@@ -13,8 +13,10 @@ import {
13
13
  } from '@ng-icons/core'
14
14
  import { iconoirBadgeCheck, iconoirSystemShut } from '@ng-icons/iconoir'
15
15
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
16
- import { EditorConfig } from '../../models'
17
16
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
17
+ import { EditorFacade } from '../../+state/editor.facade'
18
+ import { combineLatest, map } from 'rxjs'
19
+ import { AsyncPipe } from '@angular/common'
18
20
 
19
21
  //forced translations that are not available in fields.config.ts
20
22
  marker('editor.record.form.field.keywords')
@@ -29,6 +31,7 @@ marker('editor.record.form.field.organisation')
29
31
  TranslatePipe,
30
32
  ButtonComponent,
31
33
  NgIconComponent,
34
+ AsyncPipe,
32
35
  ],
33
36
  providers: [
34
37
  provideIcons({
@@ -42,47 +45,37 @@ marker('editor.record.form.field.organisation')
42
45
  templateUrl: './metadata-quality-panel.component.html',
43
46
  styleUrl: './metadata-quality-panel.component.css',
44
47
  })
45
- export class MetadataQualityPanelComponent implements OnChanges {
48
+ export class MetadataQualityPanelComponent {
49
+ facade = inject(EditorFacade)
46
50
  propsToValidate: ValidatorMapperKeys[] = getAllKeysValidator()
47
- propertiesByPage: { label: string; value: boolean }[][] = []
48
- @Input() editorConfig: EditorConfig
49
- @Input() record: CatalogRecord
50
51
 
51
- ngOnChanges() {
52
- if (this.editorConfig && this.record) {
53
- const fieldsByPage = this.editorConfig.pages.map((page) =>
54
- page.sections.flatMap((section) =>
55
- section.fields
56
- .filter((field) => this.propsToValidate.includes(field.model))
57
- .map((field) => field.model as ValidatorMapperKeys)
58
- )
59
- )
60
- // FIXME: temporarily add topics and organisation to the first and third page
61
- // as long as they are not handled by the editor
62
- if (fieldsByPage.length > 0) {
63
- fieldsByPage[0].includes('topics') || fieldsByPage[0].push('topics')
64
- fieldsByPage[2].includes('organisation') ||
65
- fieldsByPage[2].push('organisation')
66
- }
67
- this.propertiesByPage = fieldsByPage
68
- .map((fields) =>
69
- getQualityValidators(
70
- this.record,
71
- fields as ValidatorMapperKeys[]
72
- ).map(({ name, validator }) => ({
73
- label: `editor.record.form.field.${name}`, // use same translations as in fields.config.ts
74
- value: validator(),
75
- }))
52
+ propertiesByPage$ = combineLatest([
53
+ this.facade.editorConfig$,
54
+ this.facade.record$,
55
+ ]).pipe(
56
+ map(([editorConfig, record]) => {
57
+ if (!editorConfig || !record) return []
58
+ const validators = getQualityValidators(record, this.propsToValidate)
59
+ return editorConfig.pages
60
+ .map((page) =>
61
+ page.sections
62
+ .flatMap((section) => section.fields)
63
+ .flatMap(({ model }) =>
64
+ validators.filter((v) => (v.alias ?? v.name) === model)
65
+ )
66
+ .map(({ name, validator, alias }) => ({
67
+ label: `editor.record.form.field.${name}`,
68
+ value: validator(),
69
+ model: (alias ?? name) as CatalogRecordKeys,
70
+ }))
76
71
  )
77
72
  .filter((arr) => arr.length > 0)
78
- }
79
- }
73
+ })
74
+ )
80
75
 
81
- getExtraClass(checked: boolean): string {
82
- const baseClasses =
83
- 'flex flex-row justify-between rounded mb-1 h-[34px] w-full focus:ring-0 hover:border-none border-none hover:text-black text-black cursor-default'
84
- return checked
85
- ? `${baseClasses} bg-neutral-100 hover:bg-neutral-100`
86
- : `${baseClasses} bg-transparent hover:bg-transparent`
76
+ onCriterionClick(property: { value: boolean; model: CatalogRecordKeys }) {
77
+ if (!property.value) {
78
+ this.facade.setFocusedField(property.model)
79
+ }
87
80
  }
88
81
  }
@@ -0,0 +1,38 @@
1
+ import { Directive, ElementRef, inject, Input } from '@angular/core'
2
+
3
+ @Directive({
4
+ selector: '[gnUiFieldFocus]',
5
+ standalone: true,
6
+ exportAs: 'fieldFocus',
7
+ })
8
+ export class FieldFocusDirective {
9
+ @Input() gnUiFieldFocusGlowClass = 'gn-ui-field-focus-glow'
10
+
11
+ private el = inject(ElementRef)
12
+
13
+ public focusField() {
14
+ setTimeout(() => {
15
+ const host = this.el.nativeElement as HTMLElement
16
+ const glowClass = this.gnUiFieldFocusGlowClass
17
+
18
+ host.classList.remove(glowClass)
19
+ void host.offsetWidth
20
+ host.classList.add(glowClass)
21
+ host.addEventListener(
22
+ 'animationend',
23
+ () => host.classList.remove(glowClass),
24
+ { once: true }
25
+ )
26
+
27
+ host.scrollIntoView({ behavior: 'smooth', block: 'start' })
28
+ const target =
29
+ host.querySelector<HTMLElement>(
30
+ 'input, textarea, select, [contenteditable="true"]'
31
+ ) ??
32
+ host.querySelector<HTMLElement>(
33
+ 'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
34
+ )
35
+ target?.focus({ preventScroll: true })
36
+ })
37
+ }
38
+ }
@@ -2,7 +2,7 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  Component,
4
4
  OnDestroy,
5
- OnInit,
5
+ afterNextRender,
6
6
  inject,
7
7
  } from '@angular/core'
8
8
  import { CommonModule } from '@angular/common'
@@ -67,9 +67,7 @@ export type ConstraintChoice =
67
67
  styleUrls: ['./form-field-constraints-shortcuts.component.css'],
68
68
  changeDetection: ChangeDetectionStrategy.OnPush,
69
69
  })
70
- export class FormFieldConstraintsShortcutsComponent
71
- implements OnInit, OnDestroy
72
- {
70
+ export class FormFieldConstraintsShortcutsComponent implements OnDestroy {
73
71
  private editorFacade = inject(EditorFacade)
74
72
 
75
73
  legalConstraints$ = this.editorFacade.record$.pipe(
@@ -112,37 +110,38 @@ export class FormFieldConstraintsShortcutsComponent
112
110
 
113
111
  onDestroy$ = new Subject<void>()
114
112
 
115
- ngOnInit(): void {
116
- // hide all constraints if any toggle is activated
117
- this.anyToggleActivated$
118
- .pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
119
- .subscribe((anyToggleActivated) => {
120
- if (anyToggleActivated) {
121
- this.hideAllConstraintSections()
122
- }
123
- })
113
+ constructor() {
114
+ // Deferred to afterNextRender to avoid dispatching store actions
115
+ // synchronously during Angular's change detection cycle (NG0100)
116
+ afterNextRender(() => {
117
+ this.anyToggleActivated$
118
+ .pipe(takeUntil(this.onDestroy$), distinctUntilChanged())
119
+ .subscribe((anyToggleActivated) => {
120
+ if (anyToggleActivated) {
121
+ this.hideAllConstraintSections()
122
+ }
123
+ })
124
124
 
125
- // also hide constraints which are empty arrays
126
- const hideEmptyConstraints = (
127
- constraints$: Observable<Constraint[]>,
128
- model: ConstraintChoice
129
- ) => {
130
- const isConstraintNotEmpty$ = constraints$.pipe(
131
- takeUntil(this.onDestroy$),
132
- map((c) => c.length > 0),
133
- distinctUntilChanged()
134
- )
135
- combineLatest([
136
- isConstraintNotEmpty$,
137
- this.anyToggleActivated$,
138
- ]).subscribe(([isNotEmpty, anyToggleActivated]) => {
139
- const visible = isNotEmpty && !anyToggleActivated
140
- this.editorFacade.setFieldVisibility({ model }, visible)
141
- })
142
- }
143
- hideEmptyConstraints(this.legalConstraints$, 'legalConstraints')
144
- hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
145
- hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
125
+ const hideEmptyConstraints = (
126
+ constraints$: Observable<Constraint[]>,
127
+ model: ConstraintChoice
128
+ ) => {
129
+ const isConstraintNotEmpty$ = constraints$.pipe(
130
+ takeUntil(this.onDestroy$),
131
+ map((c) => c.length > 0),
132
+ distinctUntilChanged()
133
+ )
134
+ combineLatest([
135
+ isConstraintNotEmpty$,
136
+ this.anyToggleActivated$,
137
+ ]).subscribe(([isNotEmpty, anyToggleActivated]) => {
138
+ const visible = isNotEmpty && !anyToggleActivated
139
+ this.editorFacade.setFieldVisibility({ model }, visible)
140
+ })
141
+ }
142
+ hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
143
+ hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
144
+ })
146
145
  }
147
146
 
148
147
  ngOnDestroy() {
@@ -10,21 +10,21 @@
10
10
  </gn-ui-autocomplete>
11
11
 
12
12
  @if (contacts.length > 0) {
13
- @if (contacts.length === 1) {
14
- @for (contact of contacts; track contact; let index = $index) {
13
+ <gn-ui-sortable-list
14
+ [items]="contacts"
15
+ (itemsOrderChange)="handleContactsChanged($event)"
16
+ [elementTemplate]="contactTemplate"
17
+ ></gn-ui-sortable-list>
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 {
15
25
  <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
16
26
  }
17
- }
18
- @if (contacts.length > 1) {
19
- <gn-ui-sortable-list
20
- [items]="contacts"
21
- (itemsOrderChange)="handleContactsChanged($event)"
22
- [elementTemplate]="contactTemplate"
23
- ></gn-ui-sortable-list>
24
- <ng-template #contactTemplate let-contact>
25
- <gn-ui-contact-card [contact]="contact"></gn-ui-contact-card>
26
- </ng-template>
27
- }
27
+ </ng-template>
28
28
  } @else {
29
29
  <div
30
30
  class="p-4 text-sm border border-primary bg-primary-lightest rounded-lg"
@@ -28,9 +28,15 @@ 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 { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
31
+ import { ContactDetailsFormComponent } from '../../../contact-details/contact-details-form.component'
32
+ import {
33
+ createFuzzyFilter,
34
+ getIndividualDisplayName,
35
+ toIndividual,
36
+ } from '../../../../../../../../../libs/util/shared/src'
32
37
  import { map } from 'rxjs/operators'
33
38
  import { SortableListComponent } from '../../../../../../../../../libs/ui/layout/src'
39
+ import { FieldModelSpecifier } from '../../../../models/editor-config.model'
34
40
 
35
41
  @Component({
36
42
  selector: 'gn-ui-form-field-contacts',
@@ -43,6 +49,7 @@ import { SortableListComponent } from '../../../../../../../../../libs/ui/layout
43
49
  TranslateDirective,
44
50
  TranslatePipe,
45
51
  ContactCardComponent,
52
+ ContactDetailsFormComponent,
46
53
  SortableListComponent,
47
54
  ],
48
55
  })
@@ -52,6 +59,7 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
52
59
  private changeDetectorRef = inject(ChangeDetectorRef)
53
60
 
54
61
  @Input() value: Individual[]
62
+ @Input() modelSpecifier: FieldModelSpecifier
55
63
  @Output() valueChange: EventEmitter<Individual[]> = new EventEmitter()
56
64
 
57
65
  contacts: Individual[] = []
@@ -113,13 +121,19 @@ export class FormFieldContactsComponent implements OnDestroy, OnChanges {
113
121
  this.valueChange.emit(contacts)
114
122
  }
115
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
+
116
132
  /**
117
133
  * gn-ui-autocomplete
118
134
  */
119
135
  displayWithFn: (user: UserModel) => string = (user) =>
120
- `${user.name} ${user.surname} ${
121
- user.organisation ? `(${user.organisation})` : ''
122
- }`
136
+ getIndividualDisplayName(toIndividual(user))
123
137
 
124
138
  /**
125
139
  * gn-ui-autocomplete
@@ -8,6 +8,14 @@
8
8
  </gn-ui-button>
9
9
  }
10
10
  </div>
11
+ @if (value.length === 0) {
12
+ <div
13
+ class="p-4 border border-primary bg-primary-lightest rounded-lg"
14
+ translate
15
+ >
16
+ editor.record.form.field.contactsForResource.noContact
17
+ </div>
18
+ }
11
19
  @if (roleSectionsToDisplay && roleSectionsToDisplay.length > 0) {
12
20
  <div class="mt-8" data-test="displayedRoles">
13
21
  @for (
@@ -53,12 +61,5 @@
53
61
  </div>
54
62
  }
55
63
  </div>
56
- } @else {
57
- <div
58
- class="p-4 border border-primary bg-primary-lightest rounded-lg"
59
- translate
60
- >
61
- editor.record.form.field.contactsForResource.noContact
62
- </div>
63
64
  }
64
65
  </div>
@@ -23,7 +23,11 @@ import {
23
23
  AutocompleteComponent,
24
24
  ButtonComponent,
25
25
  } from '../../../../../../../../../libs/ui/inputs/src'
26
- import { createFuzzyFilter } from '../../../../../../../../../libs/util/shared/src'
26
+ import {
27
+ createFuzzyFilter,
28
+ getIndividualDisplayName,
29
+ toIndividual,
30
+ } from '../../../../../../../../../libs/util/shared/src'
27
31
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
28
32
  import {
29
33
  debounceTime,
@@ -169,11 +173,7 @@ export class FormFieldContactsForResourceComponent
169
173
  * gn-ui-autocomplete
170
174
  */
171
175
  displayWithFn: (user: UserModel) => string = (user) =>
172
- user.name
173
- ? `${user.name} ${user.surname} ${
174
- user.organisation ? `(${user.organisation})` : ''
175
- }`
176
- : ``
176
+ getIndividualDisplayName(toIndividual(user))
177
177
 
178
178
  /**
179
179
  * gn-ui-autocomplete
@@ -0,0 +1,5 @@
1
+ <gn-ui-url-input
2
+ [value]="displayUrl"
3
+ [showValidateButton]="false"
4
+ (valueChange)="handleUrlChange($event)"
5
+ ></gn-ui-url-input>