geonetwork-ui 2.10.0-dev.e97ea6950 → 2.10.0-dev.fb2893f44

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 (129) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1622 -6598
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +196 -87
  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 +68 -34
  33. package/src/libs/api/repository/src/lib/gn4/gn4.provider.ts +6 -1
  34. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-groups.service.ts +1 -1
  35. package/src/libs/api/repository/src/lib/gn4/organizations/organizations-from-metadata.service.ts +1 -1
  36. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +48 -2
  37. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -5
  38. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  39. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  40. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  41. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +5 -0
  42. package/src/libs/feature/catalog/src/lib/sources/sources.service.ts +5 -2
  43. package/src/libs/feature/editor/src/lib/+state/editor.actions.ts +6 -0
  44. package/src/libs/feature/editor/src/lib/+state/editor.effects.ts +0 -1
  45. package/src/libs/feature/editor/src/lib/+state/editor.facade.ts +10 -1
  46. package/src/libs/feature/editor/src/lib/components/constraint-card/constraint-card.component.ts +0 -2
  47. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.css +0 -0
  48. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.html +67 -0
  49. package/src/libs/feature/editor/src/lib/components/contact-details/contact-details-form.component.ts +39 -0
  50. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.html +18 -3
  51. package/src/libs/feature/editor/src/lib/components/metadata-quality-panel/metadata-quality-panel.component.ts +33 -40
  52. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +41 -0
  53. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +32 -34
  54. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.html +13 -13
  55. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +18 -4
  56. 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
  57. 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
  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.css +0 -0
  59. 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
  60. 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
  61. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  62. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +3 -0
  63. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +1 -0
  64. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +5 -0
  65. package/src/libs/feature/editor/src/lib/components/record-form/form-field/index.ts +1 -0
  66. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +35 -0
  67. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +5 -1
  68. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +77 -5
  69. package/src/libs/feature/editor/src/lib/fields.config.ts +60 -47
  70. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +4 -0
  71. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  72. package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
  73. package/src/libs/feature/notify-reuse/src/index.ts +1 -0
  74. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.css +0 -0
  75. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.html +1 -0
  76. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +21 -0
  77. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
  78. package/src/libs/ui/catalog/src/lib/language-switcher/language-switcher.component.ts +2 -2
  79. package/src/libs/ui/dataviz/src/index.ts +0 -1
  80. package/src/libs/ui/elements/src/index.ts +2 -0
  81. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +96 -0
  82. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.ts +45 -0
  83. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +41 -0
  84. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.ts +70 -0
  85. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.css +13 -0
  86. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.html +13 -3
  87. package/src/libs/ui/elements/src/lib/image-overlay-preview/image-overlay-preview.component.ts +43 -6
  88. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.html +1 -1
  89. package/src/libs/ui/elements/src/lib/internal-link-card/internal-link-card.component.ts +0 -1
  90. package/src/libs/ui/elements/src/lib/markdown-parser/markdown-parser.component.ts +2 -2
  91. package/src/libs/ui/elements/src/lib/metadata-contact/metadata-contact.component.ts +2 -5
  92. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.css +0 -4
  93. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +28 -67
  94. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.ts +30 -10
  95. package/src/libs/ui/inputs/src/lib/button/button.component.ts +8 -0
  96. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +2 -2
  97. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.html +2 -2
  98. package/src/libs/ui/inputs/src/lib/url-input/url-input.component.ts +2 -1
  99. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -1
  100. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +15 -64
  101. package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
  102. package/src/libs/ui/search/src/lib/record-preview-feed/record-preview-feed.component.ts +1 -1
  103. package/src/libs/ui/search/src/lib/record-preview-row/record-preview-row.component.html +0 -1
  104. package/src/libs/util/app-config/src/lib/app-config.ts +70 -9
  105. package/src/libs/util/app-config/src/lib/model.ts +7 -0
  106. package/src/libs/util/app-config/src/lib/parse-utils.ts +50 -1
  107. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +1 -2
  108. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +6 -3
  109. package/src/libs/util/i18n/src/index.ts +0 -1
  110. package/src/libs/util/i18n/src/lib/i18n.constants.ts +17 -11
  111. package/src/libs/util/i18n/src/lib/i18n.interceptor.ts +2 -2
  112. package/src/libs/util/i18n/src/lib/i18n.providers.ts +14 -17
  113. package/src/libs/util/i18n/src/lib/test.translate.loader.ts +48 -0
  114. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
  115. package/src/libs/util/shared/src/lib/record/quality-score.util.ts +33 -18
  116. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  117. package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
  118. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  119. package/src/libs/util/shared/src/lib/utils/user-display.ts +32 -0
  120. package/style.css +0 -1
  121. package/tailwind.base.css +19 -2
  122. package/translations/de.json +29 -20
  123. package/translations/en.json +29 -20
  124. package/translations/es.json +29 -20
  125. package/translations/fr.json +29 -20
  126. package/translations/it.json +29 -20
  127. package/translations/nl.json +29 -20
  128. package/translations/pt.json +29 -20
  129. package/translations/sk.json +29 -20
@@ -1,5 +1,5 @@
1
1
  import { HttpClient, HttpEventType } from '@angular/common/http'
2
- import { Injectable, InjectionToken, inject } from '@angular/core'
2
+ import { inject, Injectable, InjectionToken } from '@angular/core'
3
3
  import {
4
4
  KeywordApiResponse,
5
5
  ThesaurusApiResponse,
@@ -12,11 +12,13 @@ import {
12
12
  } from '../../../../../../../libs/common/domain/src/lib/model/record'
13
13
  import { KeywordType } from '../../../../../../../libs/common/domain/src/lib/model/thesaurus'
14
14
  import { UserModel } from '../../../../../../../libs/common/domain/src/lib/model/user/user.model'
15
+ import { GroupModel } from '../../../../../../../libs/common/domain/src/lib/model/user/group.model'
15
16
  import {
16
17
  PlatformServiceInterface,
17
18
  UploadEvent,
18
19
  } from '../../../../../../../libs/common/domain/src/lib/platform.service.interface'
19
20
  import {
21
+ GroupsApiService,
20
22
  MeApiService,
21
23
  RecordsApiService,
22
24
  RegistriesApiService,
@@ -57,6 +59,7 @@ export const DISABLE_AUTH = new InjectionToken<boolean>('gnDisableAuth', {
57
59
  export class Gn4PlatformService implements PlatformServiceInterface {
58
60
  private meApi = inject(MeApiService)
59
61
  private usersApi = inject(UsersApiService)
62
+ private groupsApi = inject(GroupsApiService)
60
63
  private mapper = inject(Gn4PlatformMapper)
61
64
  private toolsApiService = inject(ToolsApiService)
62
65
  private registriesApiService = inject(RegistriesApiService)
@@ -97,7 +100,7 @@ export class Gn4PlatformService implements PlatformServiceInterface {
97
100
  private keywordsByThesauri: Record<string, Observable<Keyword[]>> = {}
98
101
 
99
102
  private get lang3() {
100
- return toLang3(this.translateService.currentLang)
103
+ return toLang3(this.translateService.getCurrentLang())
101
104
  }
102
105
 
103
106
  constructor() {
@@ -155,6 +158,49 @@ export class Gn4PlatformService implements PlatformServiceInterface {
155
158
  return this.users$
156
159
  }
157
160
 
161
+ getUserPermissionsByGroup(): Observable<GroupModel[]> {
162
+ if (this.disableAuth) return of([])
163
+ return combineLatest([this.meApi.getMe(), this.groupsApi.getGroups()]).pipe(
164
+ map(([meResponse, groups]) => {
165
+ if (!meResponse) return []
166
+ if (meResponse.admin) {
167
+ return groups.map((group) => ({
168
+ groupId: group.id,
169
+ groupName: group.name,
170
+ isMember: true,
171
+ canEdit: true,
172
+ canApprove: true,
173
+ canAdministrate: true,
174
+ }))
175
+ }
176
+ const reviewerIds = meResponse.groupsWithReviewer ?? []
177
+ const editorIds = meResponse.groupsWithEditor ?? []
178
+ const memberIds = meResponse.groupsWithRegisteredUser ?? []
179
+ const adminIds = meResponse.groupsWithUserAdmin ?? []
180
+ const groupsById = new Map(groups.map((g) => [g.id, g]))
181
+ return [
182
+ ...new Set([
183
+ ...reviewerIds,
184
+ ...editorIds,
185
+ ...groups.map((g) => g.id),
186
+ ]),
187
+ ]
188
+ .filter((id) => groupsById.has(id))
189
+ .map((id) => {
190
+ const group = groupsById.get(id)
191
+ return {
192
+ groupId: group.id,
193
+ groupName: group.name,
194
+ isMember: memberIds.includes(id),
195
+ canEdit: editorIds.includes(id),
196
+ canApprove: reviewerIds.includes(id),
197
+ canAdministrate: adminIds.includes(id),
198
+ }
199
+ })
200
+ })
201
+ )
202
+ }
203
+
158
204
  translateKey(key: string): Observable<string> {
159
205
  // if the key is a URI, use the registries API to look for the translation
160
206
  if (key.match(/^https?:\/\//)) {
@@ -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
+ )
@@ -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
  }
@@ -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
+ }