geonetwork-ui 2.10.0-dev.7e58935b2 → 2.10.0-dev.857c22250

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 (38) hide show
  1. package/fesm2022/geonetwork-ui.mjs +133 -9
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +28 -2
  4. package/index.d.ts.map +1 -1
  5. package/package.json +1 -1
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +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/gn4-repository.ts +54 -32
  28. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +46 -0
  29. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +11 -0
  30. package/src/libs/common/domain/src/lib/model/user/group.model.ts +8 -0
  31. package/src/libs/common/domain/src/lib/model/user/index.ts +1 -0
  32. package/src/libs/common/domain/src/lib/platform.service.interface.ts +2 -0
  33. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +5 -0
  34. package/src/libs/feature/editor/src/lib/services/editor.service.ts +1 -1
  35. package/src/libs/util/app-config/src/lib/app-config.ts +14 -1
  36. package/src/libs/util/app-config/src/lib/model.ts +3 -0
  37. package/src/libs/util/app-config/src/lib/parse-utils.ts +27 -0
  38. package/src/libs/util/shared/src/lib/links/link-utils.ts +1 -1
@@ -77,6 +77,7 @@ export class DcatApConverter extends BaseConverter<string> {
77
77
  updateFrequency: () => 'unknown',
78
78
  overviews: () => [],
79
79
  lineage: () => '',
80
+ sourceRecords: () => [],
80
81
  temporalExtents: () => [],
81
82
  spatialRepresentation: () => undefined,
82
83
  extras: () => undefined,
@@ -114,6 +115,7 @@ export class DcatApConverter extends BaseConverter<string> {
114
115
  spatialRepresentation: () => undefined,
115
116
  overviews: () => undefined,
116
117
  lineage: () => undefined,
118
+ sourceRecords: () => [],
117
119
  onlineResources: () => undefined,
118
120
  temporalExtents: () => undefined,
119
121
  spatialExtents: () => undefined,
@@ -314,6 +316,12 @@ export class DcatApConverter extends BaseConverter<string> {
314
316
  tr,
315
317
  defaultLanguage
316
318
  )
319
+ const sourceRecords = this.readers['sourceRecords'](
320
+ dataStore,
321
+ catalogRecord,
322
+ tr,
323
+ defaultLanguage
324
+ )
317
325
  const onlineResources = this.readers['onlineResources'](
318
326
  dataStore,
319
327
  catalogRecord,
@@ -351,6 +359,7 @@ export class DcatApConverter extends BaseConverter<string> {
351
359
  securityConstraints,
352
360
  otherConstraints,
353
361
  lineage,
362
+ sourceRecords,
354
363
  ...(spatialRepresentation && { spatialRepresentation }),
355
364
  overviews,
356
365
  spatialExtents,
@@ -646,6 +646,7 @@ export const EU_SURVEY_DATASET_RECORD: DatasetRecord = {
646
646
  'http://publications.europa.eu/resource/authority/data-theme/EDUC',
647
647
  ],
648
648
  lineage: '',
649
+ sourceRecords: [],
649
650
  ownerOrganization: {
650
651
  name: 'http://publications.europa.eu/resource/authority/corporate-body/FRA',
651
652
  },
@@ -735,6 +736,7 @@ export const EU_WHOISWHO_DATASET_RECORD: DatasetRecord = {
735
736
  \r
736
737
  [Privacy statement of EU Whoiswho](https://op.europa.eu/en/web/about-us/legal-notices/op_whoiswho).`,
737
738
  lineage: '',
739
+ sourceRecords: [],
738
740
  ownerOrganization: undefined,
739
741
  recordUpdated: undefined,
740
742
  status: 'completed',
@@ -192,6 +192,7 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè
192
192
  lineage: `This record was edited manually to test the conversion processes
193
193
 
194
194
  As such, **it is not very interesting at all.**`,
195
+ sourceRecords: [],
195
196
  otherConstraints: [
196
197
  { text: 'Should only be used as a testing tool', translations: {} },
197
198
  { text: 'Might cause minor annoyance in people', translations: {} },
@@ -237,6 +237,13 @@ export const GEO2FRANCE_REUSE_ONGULES_RECORD: ReuseRecord = {
237
237
  ],
238
238
  translations: {},
239
239
  lineage: null,
240
+ sourceRecords: [
241
+ {
242
+ uuid: '67c60362-0461-4574-8975-f4e7cfa64d20',
243
+ title: 'Répartition des ongulés sauvages en France (données depuis 1980)',
244
+ href: 'https://data.ofb.fr/catalogue/Donnees-geographiques-OFB/fre/csw?service=CSW&request=GetRecordById&version=2.0.2&outputSchema=http://www.isotc211.org/2005/gmd&elementSetName=full&id=67c60362-0461-4574-8975-f4e7cfa64d20',
245
+ },
246
+ ],
240
247
  temporalExtents: [
241
248
  {
242
249
  start: null,
@@ -96,6 +96,7 @@ export const GEO2FRANCE_REUSE_ROILAYE_RECORD: ReuseRecord = {
96
96
  ],
97
97
  translations: {},
98
98
  lineage: null,
99
+ sourceRecords: [],
99
100
  temporalExtents: [{ start: null, end: null }],
100
101
  reuseType: 'map',
101
102
  }
@@ -122,6 +122,7 @@ Malgré l'attention portée à la création de ces données, il est rappelé que
122
122
  ],
123
123
  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.
124
124
  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.`,
125
+ sourceRecords: [],
125
126
  licenses: [
126
127
  {
127
128
  text: "En dépit des efforts et diligences mis en œuvre pour en vérifier la fiabilité, le fournisseur n’est pas en mesure de garantir l’exactitude, la mise à jour, l’intégrité, l’exhaustivité des données et en particulier que les données sont exemptes d'erreurs, notamment de localisation, d’identification ou d’actualisation ou d’imprécisions. Les données ne sont pas fournies en vue d'une utilisation particulière et aucune garantie quant à leur aptitude à un usage particulier n'est apportée par le fournisseur. En conséquence, les utilisateurs utilisent les données sous leur responsabilité pleine et entière, à leurs risques et périls, sans recours possible contre le fournisseur dont la responsabilité ne saurait être engagée du fait d’un dommage résultant directement ou indirectement de l’utilisation de ces données. En particulier, il appartient aux utilisateurs d’apprécier, sous leur seule responsabilité : – l'opportunité d'utiliser les données ; – la compatibilité des fichiers avec leurs systèmes informatiques ; – l’adéquation des données à leurs besoins ; – qu’ils disposent de la compétence suffisante pour utiliser les données ; – l’opportunité d’utiliser la documentation ou les outils d’analyse fournis ou préconisés, en relation avec l’utilisation des données, le cas échéant. Le fournisseur n’est en aucune façon responsable des éléments extérieurs aux données et notamment des outils d’analyse, matériels, logiciels, réseaux..., utilisés pour consulter et/ou traiter les données, même s’il a préconisé ces éléments. L’utilisateur veille à vérifier que l’actualité des informations mises à disposition est compatible avec l’usage qu’il en fait.",
@@ -546,6 +546,7 @@ export const GEOCAT_CH_DATASET_RECORD: DatasetRecord = {
546
546
  ],
547
547
  lineage:
548
548
  'Digitalisiert nach den administrativen Einheiten der Schweiz, die im Anhang des Übereinkommens erscheinen.',
549
+ sourceRecords: [],
549
550
  licenses: [
550
551
  {
551
552
  text: "Licence passée entre l'Office fédéral de l'environnement (OFEV), et le canton de Fribourg, représenté par la Coordination SIT (SITel)",
@@ -206,6 +206,7 @@ export const GEORHENA_REUSE_SOLAIRE_RECORD: ReuseRecord = {
206
206
  },
207
207
  },
208
208
  lineage: null,
209
+ sourceRecords: [],
209
210
  temporalExtents: [
210
211
  {
211
212
  start: new Date('2016-01-01T00:00:00.000Z'),
@@ -2170,6 +2170,7 @@ export const METADATA_FOR_I18N_DATASET_RECORD: DatasetRecord = {
2170
2170
  },
2171
2171
  status: 'completed',
2172
2172
  lineage: 'This is a statement about the lineage.',
2173
+ sourceRecords: [],
2173
2174
  spatialRepresentation: 'vector',
2174
2175
  temporalExtents: [],
2175
2176
  updateFrequency: 'continual',
@@ -4234,6 +4235,7 @@ export const METADATA_FOR_I18N_NO_OTHERLANGUAGE: DatasetRecord = {
4234
4235
  translations: { lineage: {}, title: {}, abstract: {} },
4235
4236
  status: 'completed',
4236
4237
  lineage: 'This is a statement about the lineage.',
4238
+ sourceRecords: [],
4237
4239
  spatialRepresentation: 'vector',
4238
4240
  temporalExtents: [],
4239
4241
  updateFrequency: 'continual',
@@ -419,6 +419,7 @@ Cette donnée a été intégrée dans la base de données routière (BDR). Les D
419
419
 
420
420
  Ces données, intégrées dans la Banque de Données routières (BDR), ont fait l’objet d’une mise à jour massive en 2014-2015.
421
421
  Depuis, ce sont les Districts routiers qui assurent la tenue à jour de ces informations directement dans la base de données.`,
422
+ sourceRecords: [],
422
423
  otherConstraints: [],
423
424
  overviews: [
424
425
  {
@@ -370,6 +370,7 @@ export const OPENDATASWISS_DATASET_RECORD: DatasetRecord = {
370
370
  'http://dcat-ap.ch/vocabulary/themes/geography',
371
371
  ],
372
372
  lineage: '',
373
+ sourceRecords: [],
373
374
  ownerOrganization: {
374
375
  name: 'Bundesamt für Energie',
375
376
  },
@@ -82,6 +82,7 @@ export const SEXTANT_BATHYMETRY_DATASET_RECORD: DatasetRecord = {
82
82
  Le pas de la grille est de 25m.
83
83
  Produit interne Ifremer.`,
84
84
  lineage: '',
85
+ sourceRecords: [],
85
86
  ownerOrganization: null,
86
87
  recordUpdated: new Date('2020-06-03T22:34:05.000Z'),
87
88
  resourceUpdated: new Date('2020-06-03T22:34:05.000Z'),
@@ -322,6 +323,7 @@ export const SEXTANT_CURRENTS_DATASET_RECORD: CatalogRecord = {
322
323
  ],
323
324
  lineage:
324
325
  'Sallée, J.B.; Speer, K and Morrow, R. Southern Ocean fronts and their variability to climate modes, Journ. of Climate, 2008, Vol. 21(12), pp. 3020-3039',
326
+ sourceRecords: [],
325
327
  onlineResources: [
326
328
  {
327
329
  name: 'Digital Object Identifier (DOI)',
@@ -68,6 +68,7 @@ export const VLAANDEREN_DATASET_RECORD: DatasetRecord = {
68
68
  temporalExtents: [],
69
69
  topics: ['biodiversity'],
70
70
  lineage: '',
71
+ sourceRecords: [],
71
72
  recordUpdated: new Date('2024-09-19T01:15:09.732Z'),
72
73
  resourceUpdated: new Date('2021-04-14T11:15+02:00'),
73
74
  status: 'completed',
@@ -447,5 +447,13 @@ export const WALLONIE_REUSE_SPW_RECORD: ReuseRecord = {
447
447
  'https://metawal.wallonie.be/geonetwork/srv/api/records/83809bcd-1763-4d28-b820-2b9828083ba5'
448
448
  ),
449
449
  lineage: "L'application a été développée sur base de l'API GeoViewer",
450
+ sourceRecords: [
451
+ {
452
+ uuid: 'ee965118-2416-4d48-b07e-bbc696f002c2',
453
+ title:
454
+ 'SCoT (Schéma de cohérence territoriale) en région Hauts-de-France',
455
+ href: 'https://metawal.wallonie.be/geonetwork/srv/api/records/ee965118-2416-4d48-b07e-bbc696f002c2',
456
+ },
457
+ ],
450
458
  temporalExtents: [],
451
459
  }
@@ -24,6 +24,7 @@ export class Gn4Converter extends BaseConverter<Gn4Record> {
24
24
  kind: 'dataset',
25
25
  status: null,
26
26
  lineage: null,
27
+ sourceRecords: [],
27
28
  recordUpdated: null,
28
29
  recordPublished: null,
29
30
  ownerOrganization: null,
@@ -9,6 +9,7 @@ import {
9
9
  readKind,
10
10
  readLandingPage,
11
11
  readLineage,
12
+ readSourceRecords,
12
13
  readOnlineResources,
13
14
  readOtherLanguages,
14
15
  readOwnerOrganization,
@@ -25,6 +26,7 @@ import {
25
26
  writeKind,
26
27
  writeLandingPage,
27
28
  writeLineage,
29
+ writeSourceRecords,
28
30
  writeOnlineResources,
29
31
  writeOtherLanguages,
30
32
  writeRecordCreated,
@@ -53,6 +55,7 @@ export class Iso191153Converter extends Iso19139Converter {
53
55
  this.readers['ownerOrganization'] = readOwnerOrganization
54
56
  this.readers['landingPage'] = readLandingPage
55
57
  this.readers['lineage'] = readLineage
58
+ this.readers['sourceRecords'] = readSourceRecords
56
59
  this.readers['onlineResources'] = readOnlineResources
57
60
  this.readers['defaultLanguage'] = readDefaultLanguage
58
61
  this.readers['otherLanguages'] = readOtherLanguages
@@ -72,6 +75,7 @@ export class Iso191153Converter extends Iso19139Converter {
72
75
  this.writers['ownerOrganization'] = () => undefined // fixme: find a way to store this value properly
73
76
  this.writers['landingPage'] = writeLandingPage
74
77
  this.writers['lineage'] = writeLineage
78
+ this.writers['sourceRecords'] = writeSourceRecords
75
79
  this.writers['onlineResources'] = writeOnlineResources
76
80
  this.writers['status'] = writeStatus
77
81
  this.writers['spatialRepresentation'] = writeSpatialRepresentation
@@ -167,6 +171,9 @@ export class Iso191153Converter extends Iso19139Converter {
167
171
  'gmd:fileName': 'mcc:fileName',
168
172
  'gmd:fileDescription': 'mcc:fileDescription',
169
173
 
174
+ // lineage sources
175
+ 'gmd:source': 'mrl:source',
176
+
170
177
  // no more URL elements
171
178
  'gmd:URL': 'gco:CharacterString',
172
179
  })
@@ -21,6 +21,7 @@ import {
21
21
  extractCharacterString,
22
22
  extractDatasetOnlineResources,
23
23
  extractDateTime,
24
+ extractSourceRecords,
24
25
  extractLocalizedCharacterString,
25
26
  extractReuseOnlineResources,
26
27
  extractRole,
@@ -31,6 +32,7 @@ import {
31
32
  import {
32
33
  Individual,
33
34
  LanguageCode,
35
+ SourceRecord,
34
36
  OnlineResource,
35
37
  Organization,
36
38
  OrganizationTranslations,
@@ -291,6 +293,12 @@ export function readLineage(
291
293
  )(rootEl)
292
294
  }
293
295
 
296
+ export function readSourceRecords(rootEl: XmlElement): SourceRecord[] {
297
+ return extractSourceRecords(
298
+ pipe(findNestedElement('mdb:resourceLineage', 'mrl:LI_Lineage'))(rootEl)
299
+ )
300
+ }
301
+
294
302
  function extractDateInfo(
295
303
  type: 'creation' | 'revision' | 'publication'
296
304
  ): ChainableFunction<XmlElement, Date> {
@@ -34,6 +34,7 @@ import {
34
34
  } from '../function-utils'
35
35
  import {
36
36
  appendKeywords,
37
+ appendSourceRecords,
37
38
  appendOnlineResource,
38
39
  appendServiceOnlineResources,
39
40
  createDistributionInfo,
@@ -564,3 +565,10 @@ export function writeOtherLanguages(record: DatasetRecord, rootEl: XmlElement) {
564
565
  )
565
566
  )(rootEl)
566
567
  }
568
+
569
+ export function writeSourceRecords(record: DatasetRecord, rootEl: XmlElement) {
570
+ pipe(
571
+ findNestedChildOrCreate('mdb:resourceLineage', 'mrl:LI_Lineage'),
572
+ appendSourceRecords(record.sourceRecords)
573
+ )(rootEl)
574
+ }
@@ -30,6 +30,7 @@ import {
30
30
  readLegalConstraints,
31
31
  readLicenses,
32
32
  readLineage,
33
+ readSourceRecords,
33
34
  readOnlineResources,
34
35
  readOtherConstraints,
35
36
  readOtherLanguages,
@@ -62,6 +63,7 @@ import {
62
63
  writeLegalConstraints,
63
64
  writeLicenses,
64
65
  writeLineage,
66
+ writeSourceRecords,
65
67
  writeOnlineResources,
66
68
  writeOtherConstraints,
67
69
  writeRecordUpdated,
@@ -112,6 +114,7 @@ export class Iso19139Converter extends BaseConverter<string> {
112
114
  spatialRepresentation: readSpatialRepresentation,
113
115
  overviews: readOverviews,
114
116
  lineage: readLineage,
117
+ sourceRecords: readSourceRecords,
115
118
  onlineResources: readOnlineResources,
116
119
  temporalExtents: readTemporalExtents,
117
120
  spatialExtents: readSpatialExtents,
@@ -153,6 +156,7 @@ export class Iso19139Converter extends BaseConverter<string> {
153
156
  spatialRepresentation: writeSpatialRepresentation,
154
157
  overviews: writeGraphicOverviews,
155
158
  lineage: writeLineage,
159
+ sourceRecords: writeSourceRecords,
156
160
  onlineResources: writeOnlineResources,
157
161
  temporalExtents: writeTemporalExtents,
158
162
  spatialExtents: writeSpatialExtents,
@@ -295,6 +299,7 @@ export class Iso19139Converter extends BaseConverter<string> {
295
299
  )
296
300
  const temporalExtents = this.readers['temporalExtents'](rootEl, tr)
297
301
  const lineage = this.readers['lineage'](rootEl, tr)
302
+ const sourceRecords = this.readers['sourceRecords'](rootEl, tr)
298
303
  const updateFrequency = this.readers['updateFrequency'](rootEl, tr)
299
304
 
300
305
  return this.afterRecordRead({
@@ -302,6 +307,7 @@ export class Iso19139Converter extends BaseConverter<string> {
302
307
  kind,
303
308
  status,
304
309
  lineage,
310
+ ...(sourceRecords && { sourceRecords }),
305
311
  ...(spatialRepresentation && { spatialRepresentation }),
306
312
  temporalExtents,
307
313
  updateFrequency,
@@ -309,6 +315,7 @@ export class Iso19139Converter extends BaseConverter<string> {
309
315
  } as DatasetRecord)
310
316
  } else if (kind === 'reuse') {
311
317
  const lineage = this.readers['lineage'](rootEl, tr)
318
+ const sourceRecords = this.readers['sourceRecords'](rootEl, tr)
312
319
  const temporalExtents = this.readers['temporalExtents'](rootEl, tr)
313
320
  const reuseType = this.readers['reuseType'](rootEl, tr)
314
321
 
@@ -316,6 +323,7 @@ export class Iso19139Converter extends BaseConverter<string> {
316
323
  ...this.readBaseRecord(rootEl, tr),
317
324
  kind,
318
325
  lineage,
326
+ ...(sourceRecords && { sourceRecords }),
319
327
  temporalExtents,
320
328
  reuseType,
321
329
  } as ReuseRecord)
@@ -411,7 +419,10 @@ export class Iso19139Converter extends BaseConverter<string> {
411
419
  this.writers['spatialExtents'](record, rootEl)
412
420
  ;(fieldChanged('lineage') || fieldChanged('translations')) &&
413
421
  this.writers['lineage'](record, rootEl)
422
+ fieldChanged('sourceRecords') &&
423
+ this.writers['sourceRecords'](record, rootEl)
414
424
  }
425
+
415
426
  fieldChanged('otherLanguages') &&
416
427
  this.writers['otherLanguages'](record, rootEl)
417
428
 
@@ -8,6 +8,7 @@ import {
8
8
  Keyword,
9
9
  KeywordTranslations,
10
10
  LanguageCode,
11
+ SourceRecord,
11
12
  ModelTranslations,
12
13
  OnlineLinkResource,
13
14
  OnlineResource,
@@ -894,6 +895,38 @@ export function readLineage(
894
895
  )(rootEl)
895
896
  }
896
897
 
898
+ export function extractSourceRecords(liLineageEl: XmlElement): SourceRecord[] {
899
+ if (!liLineageEl) return []
900
+ return pipe(
901
+ findChildrenElement('gmd:source', false),
902
+ mapArray((el) => {
903
+ const uuid = readAttribute('uuidref')(el)
904
+ const title = readAttribute('xlink:title')(el)
905
+ const href = readAttribute('xlink:href')(el)
906
+ if (!uuid && !title && !href) return null
907
+ return {
908
+ ...(uuid ? { uuid } : {}),
909
+ ...(title ? { title } : {}),
910
+ ...(href ? { href } : {}),
911
+ } as SourceRecord
912
+ }),
913
+ filterArray((s): s is SourceRecord => s !== null)
914
+ )(liLineageEl)
915
+ }
916
+
917
+ export function readSourceRecords(rootEl: XmlElement): SourceRecord[] {
918
+ return extractSourceRecords(
919
+ pipe(
920
+ findNestedElement(
921
+ 'gmd:dataQualityInfo',
922
+ 'gmd:DQ_DataQuality',
923
+ 'gmd:lineage',
924
+ 'gmd:LI_Lineage'
925
+ )
926
+ )(rootEl)
927
+ )
928
+ }
929
+
897
930
  export function readUpdateFrequency(rootEl: XmlElement): UpdateFrequency {
898
931
  return pipe(
899
932
  findIdentification(),
@@ -8,6 +8,7 @@ import {
8
8
  Individual,
9
9
  Keyword,
10
10
  LanguageCode,
11
+ SourceRecord,
11
12
  RecordStatus,
12
13
  RecordTranslations,
13
14
  ReuseRecord,
@@ -1231,6 +1232,38 @@ export function writeLineage(record: DatasetRecord, rootEl: XmlElement) {
1231
1232
  )(rootEl)
1232
1233
  }
1233
1234
 
1235
+ export function appendSourceRecords(
1236
+ sources: SourceRecord[]
1237
+ ): ChainableFunction<XmlElement, XmlElement> {
1238
+ return pipe(
1239
+ removeChildrenByName('gmd:source'),
1240
+ appendChildren(
1241
+ ...sources
1242
+ .filter((source) => source.uuid || source.title || source.href)
1243
+ .map((source) =>
1244
+ pipe(
1245
+ createElement('gmd:source'),
1246
+ source.uuid ? writeAttribute('uuidref', source.uuid) : noop,
1247
+ source.title ? writeAttribute('xlink:title', source.title) : noop,
1248
+ source.href ? writeAttribute('xlink:href', source.href) : noop
1249
+ )
1250
+ )
1251
+ )
1252
+ )
1253
+ }
1254
+
1255
+ export function writeSourceRecords(record: DatasetRecord, rootEl: XmlElement) {
1256
+ pipe(
1257
+ findNestedChildOrCreate(
1258
+ 'gmd:dataQualityInfo',
1259
+ 'gmd:DQ_DataQuality',
1260
+ 'gmd:lineage',
1261
+ 'gmd:LI_Lineage'
1262
+ ),
1263
+ appendSourceRecords(record.sourceRecords)
1264
+ )(rootEl)
1265
+ }
1266
+
1234
1267
  export function getServiceEndpointProtocol(endpoint: ServiceEndpoint): string {
1235
1268
  switch (endpoint.accessServiceProtocol.toLowerCase()) {
1236
1269
  case 'wfs':
@@ -6,6 +6,7 @@ import {
6
6
  import { Injectable, InjectionToken, inject } from '@angular/core'
7
7
  import {
8
8
  assertValidXml,
9
+ BaseConverter,
9
10
  findConverterForDocument,
10
11
  Gn4Converter,
11
12
  Gn4SearchResults,
@@ -61,6 +62,12 @@ export const DISABLE_DRAFT = new InjectionToken<boolean>('gnDisableDraft', {
61
62
  factory: () => false,
62
63
  })
63
64
 
65
+ export const DEFAULT_RECORD_CONVERTER = new InjectionToken<
66
+ BaseConverter<string>
67
+ >('defaultRecordConverter', {
68
+ factory: () => new Iso19139Converter(),
69
+ })
70
+
64
71
  @Injectable()
65
72
  export class Gn4Repository implements RecordsRepositoryInterface {
66
73
  private httpClient = inject(HttpClient)
@@ -72,6 +79,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
72
79
  private gn4LanguagesApi = inject(LanguagesApiService)
73
80
  private settingsService = inject(Gn4SettingsService)
74
81
  private disableDraft = inject(DISABLE_DRAFT, { optional: true }) ?? false
82
+ private defaultConverter = inject(DEFAULT_RECORD_CONVERTER)
75
83
 
76
84
  _draftsChanged = new Subject<void>()
77
85
  draftsChanged$ = this._draftsChanged.asObservable()
@@ -377,38 +385,52 @@ export class Gn4Repository implements RecordsRepositoryInterface {
377
385
  openRecordForDuplication(
378
386
  uniqueIdentifier: string
379
387
  ): Observable<[CatalogRecord, string, true] | null> {
380
- return this.gn4RecordsApi
381
- .create(
382
- uniqueIdentifier,
383
- '2',
384
- 'METADATA',
385
- '',
386
- false,
387
- undefined,
388
- true,
389
- false,
390
- undefined,
391
- 'body',
392
- false,
393
- {
394
- httpHeaderAccept: 'application/json',
395
- httpContentTypeSelected: 'application/json;charset=UTF-8',
396
- }
397
- )
398
- .pipe(
399
- switchMap((uniqueIdentifier) => {
400
- return this.getRecordAsXml(uniqueIdentifier)
401
- }),
402
- switchMap((xml) => {
403
- return from(
404
- findConverterForDocument(xml)
405
- .readRecord(xml)
406
- .then((record) => {
407
- return [record, xml, true] as [CatalogRecord, string, true]
408
- })
388
+ return this.platformService.getUserPermissionsByGroup().pipe(
389
+ map((permissions) => {
390
+ const groupId =
391
+ permissions.find((p) => p.canApprove)?.groupId?.toString() ??
392
+ permissions.find((p) => p.canEdit)?.groupId?.toString()
393
+ if (!groupId)
394
+ throw new Error(
395
+ 'Current user has no writable group to duplicate into'
396
+ )
397
+ return groupId
398
+ }),
399
+ switchMap((groupId) =>
400
+ this.gn4RecordsApi
401
+ .create(
402
+ uniqueIdentifier,
403
+ groupId,
404
+ 'METADATA',
405
+ '',
406
+ false,
407
+ undefined,
408
+ true,
409
+ false,
410
+ undefined,
411
+ 'body',
412
+ false,
413
+ {
414
+ httpHeaderAccept: 'application/json',
415
+ httpContentTypeSelected: 'application/json;charset=UTF-8',
416
+ }
417
+ )
418
+ .pipe(
419
+ switchMap((uniqueIdentifier) => {
420
+ return this.getRecordAsXml(uniqueIdentifier)
421
+ }),
422
+ switchMap((xml) => {
423
+ return from(
424
+ findConverterForDocument(xml)
425
+ .readRecord(xml)
426
+ .then((record) => {
427
+ return [record, xml, true] as [CatalogRecord, string, true]
428
+ })
429
+ )
430
+ })
409
431
  )
410
- })
411
432
  )
433
+ )
412
434
  }
413
435
 
414
436
  saveRecord(
@@ -605,10 +627,10 @@ export class Gn4Repository implements RecordsRepositoryInterface {
605
627
  record: CatalogRecord,
606
628
  referenceRecordSource?: string
607
629
  ): Observable<string> {
608
- // if there's a reference record, use that standard; otherwise, use iso19139
630
+ // if there's a reference record, use that standard; otherwise, use standard based on configuration or default
609
631
  const converter = referenceRecordSource
610
632
  ? findConverterForDocument(referenceRecordSource)
611
- : new Iso19139Converter()
633
+ : this.defaultConverter
612
634
  return from(converter.writeRecord(record, referenceRecordSource))
613
635
  }
614
636
 
@@ -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)
@@ -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?:\/\//)) {