geonetwork-ui 2.10.0-dev.300bb3d2d → 2.10.0-dev.37a1e4c4d

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 (124) hide show
  1. package/fesm2022/geonetwork-ui.mjs +796 -5789
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +191 -84
  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/gn4.converter.ts +1 -0
  22. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +2 -2
  23. package/src/libs/api/metadata-converter/src/lib/gn4/metadata-url.service.ts +2 -2
  24. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +7 -0
  25. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +8 -0
  26. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +8 -0
  27. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +11 -0
  28. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +33 -0
  29. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +33 -0
  30. package/src/libs/api/repository/src/lib/gn4/auth/auth.service.ts +8 -4
  31. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +2 -2
  32. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +54 -32
  33. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts +1 -1
  34. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts +1 -1
  35. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +48 -2
  36. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -5
  37. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  38. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  39. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  40. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +5 -0
  41. package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -2
  42. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +6 -0
  43. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +10 -1
  44. package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts +0 -2
  45. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  46. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  47. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  48. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.html +18 -3
  49. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.ts +33 -40
  50. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +41 -0
  51. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +32 -34
  52. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +13 -13
  53. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +18 -4
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  60. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +3 -0
  61. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
  62. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -0
  63. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  64. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +35 -0
  65. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +5 -1
  66. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +77 -5
  67. package/src/libs/feature/editor/src/lib/fields.config.ts +60 -47
  68. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
  69. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  70. package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
  71. package/src/libs/feature/notify-reuse/src/index.ts +1 -0
  72. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css +0 -0
  73. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +1 -0
  74. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +21 -0
  75. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
  76. package/src/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +2 -2
  77. package/src/libs/ui/dataviz/src/index.ts +0 -1
  78. package/src/libs/ui/elements/src/index.ts +2 -0
  79. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -0
  80. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  81. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +41 -0
  82. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
  83. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.css +13 -0
  84. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +13 -3
  85. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +43 -6
  86. package/src/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts +2 -2
  87. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  88. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  89. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +28 -67
  90. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  91. package/src/libs/ui/inputs/src/lib/button/button.component.ts +8 -0
  92. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +2 -2
  93. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  94. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  95. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -1
  96. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +15 -64
  97. package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
  98. package/src/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.ts +1 -1
  99. package/src/libs/util/app-config/src/lib/app-config.ts +70 -9
  100. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  101. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  102. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +1 -2
  103. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +6 -3
  104. package/src/libs/util/i18n/src/index.ts +0 -1
  105. package/src/libs/util/i18n/src/lib/i18n.constants.ts +17 -11
  106. package/src/libs/util/i18n/src/lib/i18n.interceptor.ts +2 -2
  107. package/src/libs/util/i18n/src/lib/i18n.providers.ts +14 -17
  108. package/src/libs/util/i18n/src/lib/test.translate.loader.ts +48 -0
  109. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  110. package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
  111. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  112. package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
  113. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  114. package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
  115. package/style.css +0 -1
  116. package/tailwind.base.css +19 -2
  117. package/translations/de.json +29 -20
  118. package/translations/en.json +29 -20
  119. package/translations/es.json +29 -20
  120. package/translations/fr.json +29 -20
  121. package/translations/it.json +29 -20
  122. package/translations/nl.json +29 -20
  123. package/translations/pt.json +29 -20
  124. package/translations/sk.json +29 -20
@@ -108,11 +108,6 @@ export interface Keyword {
108
108
  translations?: KeywordTranslations
109
109
  }
110
110
 
111
- export interface INSPIRE_topic {
112
- value: string
113
- label: string
114
- }
115
-
116
111
  export interface ResourceIdentifier {
117
112
  code: string
118
113
  codeSpace?: string
@@ -254,10 +249,20 @@ export interface DatasetTemporalExtent {
254
249
  end?: Date
255
250
  }
256
251
 
252
+ /**
253
+ * Represents a source dataset referenced from a lineage entry.
254
+ */
255
+ export interface SourceRecord {
256
+ uuid?: string
257
+ title?: string
258
+ href?: string
259
+ }
260
+
257
261
  export interface DatasetRecord extends BaseRecord {
258
262
  kind: 'dataset'
259
263
  status: RecordStatus
260
264
  lineage: string // Explanation of the origin of this record (e.g: how, why)"
265
+ sourceRecords: Array<SourceRecord>
261
266
  onlineResources: Array<DatasetOnlineResource>
262
267
  spatialExtents: Array<DatasetSpatialExtent>
263
268
  temporalExtents: Array<DatasetTemporalExtent>
@@ -285,6 +290,7 @@ export interface ServiceRecord extends BaseRecord {
285
290
  export interface ReuseRecord extends BaseRecord {
286
291
  kind: 'reuse'
287
292
  lineage: string // Explanation of the origin of this record (e.g: how, why)"
293
+ sourceRecords: Array<SourceRecord>
288
294
  onlineResources: Array<DatasetOnlineResource>
289
295
  reuseType: ReuseType
290
296
  spatialExtents: Array<DatasetSpatialExtent>
@@ -0,0 +1,8 @@
1
+ export interface GroupModel {
2
+ groupId: number
3
+ groupName: string
4
+ isMember: boolean
5
+ canEdit: boolean
6
+ canApprove: boolean
7
+ canAdministrate: boolean
8
+ }
@@ -1 +1,2 @@
1
1
  export * from './user.model'
2
+ export * from './group.model'
@@ -1,5 +1,6 @@
1
1
  import type { Observable } from 'rxjs'
2
2
  import type { UserModel } from './model/user/user.model'
3
+ import type { GroupModel } from './model/user/group.model'
3
4
  import type { Organization } from './model/record/organization.model'
4
5
  import { CatalogRecord, Keyword, UserFeedback } from './model/record'
5
6
  import { KeywordType } from './model/thesaurus'
@@ -28,6 +29,7 @@ export abstract class PlatformServiceInterface {
28
29
  abstract getMe(): Observable<UserModel>
29
30
  abstract isAnonymous(): Observable<boolean>
30
31
  abstract getUsers(): Observable<UserModel[]>
32
+ abstract getUserPermissionsByGroup(): Observable<GroupModel[]>
31
33
  abstract getUsersByOrganization(
32
34
  organisation: Organization
33
35
  ): Observable<UserModel[]>
@@ -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,4 +1,4 @@
1
- import { Injectable, inject } from '@angular/core'
1
+ import { inject, Injectable } from '@angular/core'
2
2
  import { SourcesApiService } from '../../../../../../libs/data-access/gn4/src'
3
3
  import { Observable } from 'rxjs'
4
4
  import { filter, map, shareReplay } from 'rxjs/operators'
@@ -21,7 +21,10 @@ export class SourcesService {
21
21
  return this.sources$.pipe(
22
22
  map((sources) => sources.filter((source) => source.uuid === uuid)[0]),
23
23
  filter((source) => !!source),
24
- map((source) => source.label[toLang3(this.translateService.currentLang)])
24
+ map(
25
+ (source) =>
26
+ source.label[toLang3(this.translateService.getCurrentLang())]
27
+ )
25
28
  )
26
29
  }
27
30
  }
@@ -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
+ )
@@ -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
  }
@@ -8,7 +8,6 @@ import {
8
8
 
9
9
  import { FormFieldRichComponent } from '../record-form/form-field/form-field-rich/form-field-rich.component'
10
10
  import { ButtonComponent, UrlInputComponent } from '../../../../../../../libs/ui/inputs/src'
11
- import { MatIconModule } from '@angular/material/icon'
12
11
  import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
13
12
  import { Constraint } from '../../../../../../../libs/common/domain/src/lib/model/record'
14
13
  import {
@@ -26,7 +25,6 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker'
26
25
  FormFieldRichComponent,
27
26
  UrlInputComponent,
28
27
  ButtonComponent,
29
- MatIconModule,
30
28
  TranslateDirective,
31
29
  TranslatePipe,
32
30
  NgIconComponent,
@@ -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,41 @@
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(focusInnerTarget = true) {
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
+ if (!focusInnerTarget) {
29
+ return
30
+ }
31
+ const target =
32
+ host.querySelector<HTMLElement>(
33
+ 'input, textarea, select, [contenteditable="true"]'
34
+ ) ??
35
+ host.querySelector<HTMLElement>(
36
+ 'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
37
+ )
38
+ target?.focus({ preventScroll: true })
39
+ })
40
+ }
41
+ }
@@ -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,37 @@ 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([isConstraintNotEmpty$, this.anyToggleActivated$])
135
+ .pipe(takeUntil(this.onDestroy$))
136
+ .subscribe(([isNotEmpty, anyToggleActivated]) => {
137
+ const visible = isNotEmpty && !anyToggleActivated
138
+ this.editorFacade.setFieldVisibility({ model }, visible)
139
+ })
140
+ }
141
+ hideEmptyConstraints(this.securityConstraints$, 'securityConstraints')
142
+ hideEmptyConstraints(this.otherConstraints$, 'otherConstraints')
143
+ })
146
144
  }
147
145
 
148
146
  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"