geonetwork-ui 2.11.0-dev.f5ee66b24 → 2.11.0-dev.fb48cfeee

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 (74) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1570 -1196
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +153 -71
  4. package/index.d.ts.map +1 -1
  5. package/package.json +6 -5
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +2 -0
  7. package/src/libs/api/metadata-converter/src/lib/fixtures/mel.records.dataset+featurecatalog.ts +395 -0
  8. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +35 -8
  9. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +19 -0
  10. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +17 -6
  11. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +110 -0
  12. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +131 -0
  13. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +9 -0
  14. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +2 -1
  15. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +29 -3
  16. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +83 -35
  17. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +11 -0
  18. package/src/libs/common/domain/src/lib/model/record/flags.model.ts +1 -0
  19. package/src/libs/common/domain/src/lib/model/record/index.ts +1 -0
  20. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +14 -18
  21. package/src/libs/common/domain/src/lib/model/search/aggregation.model.ts +2 -0
  22. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +4 -1
  23. package/src/libs/common/domain/src/lib/platform.service.interface.ts +1 -0
  24. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +20 -1
  25. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +42 -62
  26. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +32 -19
  27. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints/form-field-constraints.component.ts +3 -5
  28. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +2 -2
  29. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.css +0 -0
  30. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.html +18 -0
  31. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.ts +86 -0
  32. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +100 -88
  33. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +11 -4
  34. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +18 -4
  35. package/src/libs/feature/editor/src/lib/expressions.ts +67 -0
  36. package/src/libs/feature/editor/src/lib/fields.config.ts +13 -0
  37. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +10 -4
  38. package/src/libs/feature/map/src/index.ts +0 -1
  39. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  40. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +3 -1
  41. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -1
  42. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +7 -1
  43. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +26 -4
  44. package/src/libs/feature/search/src/lib/geocoding/geocoding.service.ts +52 -8
  45. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +11 -1
  46. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +77 -3
  47. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +1 -0
  48. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  49. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +56 -4
  50. package/src/libs/feature/search/src/lib/utils/service/fields.ts +33 -9
  51. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +3 -3
  52. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.ts +2 -2
  53. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css +0 -4
  54. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +2 -2
  55. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +3 -3
  56. package/src/libs/ui/inputs/src/lib/search-feature-catalog/search-feature-catalog.component.ts +2 -1
  57. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +8 -2
  58. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +34 -5
  59. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  60. package/src/libs/util/app-config/src/lib/app-config.ts +34 -0
  61. package/src/libs/util/app-config/src/lib/fixtures.ts +37 -1
  62. package/src/libs/util/app-config/src/lib/model.ts +24 -1
  63. package/src/libs/util/shared/src/lib/utils/geojson.ts +16 -0
  64. package/translations/de.json +12 -1
  65. package/translations/en.json +14 -3
  66. package/translations/es.json +12 -1
  67. package/translations/fr.json +19 -8
  68. package/translations/it.json +12 -1
  69. package/translations/nl.json +12 -1
  70. package/translations/pt.json +12 -1
  71. package/translations/sk.json +12 -1
  72. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  73. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  74. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  CatalogRecord,
3
+ DatasetFeatureAttribute,
4
+ DatasetFeatureAttributeValue,
5
+ DatasetFeatureType,
3
6
  DatasetRecord,
4
7
  Individual,
5
8
  LanguageCode,
@@ -616,3 +619,131 @@ export function writeAssociatedRecords(
616
619
  )
617
620
  )(rootEl)
618
621
  }
622
+
623
+ function createNilElement(name: string, nilReason: string) {
624
+ return pipe(createElement(name), writeAttribute('gco:nilReason', nilReason))
625
+ }
626
+
627
+ function createFeatureCatalogueShell() {
628
+ return pipe(
629
+ createNestedElement(
630
+ 'mdb:contentInfo',
631
+ 'mrc:MD_FeatureCatalogue',
632
+ 'mrc:featureCatalogue',
633
+ 'gfc:FC_FeatureCatalogue'
634
+ ),
635
+ // create mandatory elements which are not part of the model yet
636
+ appendChildren(
637
+ createNilElement('cat:name', 'missing'),
638
+ createNilElement('cat:scope', 'missing'),
639
+ createNilElement('cat:versionNumber', 'missing'),
640
+ createNilElement('cat:versionDate', 'missing'),
641
+ createNilElement('gfc:producer', 'missing')
642
+ )
643
+ )
644
+ }
645
+
646
+ function createListedValue(value: DatasetFeatureAttributeValue) {
647
+ return pipe(
648
+ createNestedElement('gfc:listedValue', 'gfc:FC_ListedValue'),
649
+ appendChildren(
650
+ value.label
651
+ ? pipe(createElement('gfc:label'), writeCharacterString(value.label))
652
+ : createNilElement('gfc:label', 'missing'),
653
+ value.value
654
+ ? pipe(createElement('gfc:code'), writeCharacterString(value.value))
655
+ : null,
656
+ value.description
657
+ ? pipe(
658
+ createElement('gfc:definition'),
659
+ writeCharacterString(value.description)
660
+ )
661
+ : null
662
+ )
663
+ )
664
+ }
665
+
666
+ function createFeatureAttribute(attribute: DatasetFeatureAttribute) {
667
+ return pipe(
668
+ createNestedElement(
669
+ 'gfc:carrierOfCharacteristics',
670
+ 'gfc:FC_FeatureAttribute'
671
+ ),
672
+ appendChildren(
673
+ pipe(createElement('gfc:memberName'), setTextContent(attribute.name)),
674
+ attribute.description
675
+ ? pipe(
676
+ createElement('gfc:definition'),
677
+ writeCharacterString(attribute.description)
678
+ )
679
+ : null,
680
+ createNilElement('gfc:cardinality', 'missing'),
681
+ attribute.label
682
+ ? pipe(
683
+ createElement('gfc:designation'),
684
+ writeCharacterString(attribute.label)
685
+ )
686
+ : null,
687
+ attribute.code
688
+ ? pipe(createElement('gfc:code'), writeCharacterString(attribute.code))
689
+ : null,
690
+ attribute.type
691
+ ? pipe(
692
+ createNestedElement('gfc:valueType', 'gco:TypeName', 'gco:aName'),
693
+ writeCharacterString(attribute.type)
694
+ )
695
+ : null,
696
+ ...(attribute.values ?? [])
697
+ .filter((value) => value.value || value.label)
698
+ .map(createListedValue)
699
+ )
700
+ )
701
+ }
702
+
703
+ function createFeatureType(featureType: DatasetFeatureType) {
704
+ return pipe(
705
+ createNestedElement('gfc:featureType', 'gfc:FC_FeatureType'),
706
+ appendChildren(
707
+ pipe(createElement('gfc:typeName'), setTextContent(featureType.name)),
708
+ featureType.description
709
+ ? pipe(
710
+ createElement('gfc:definition'),
711
+ writeCharacterString(featureType.description)
712
+ )
713
+ : null,
714
+ pipe(
715
+ createNestedElement('gfc:isAbstract', 'gco:Boolean'),
716
+ setTextContent('false')
717
+ ),
718
+ featureType.label
719
+ ? pipe(
720
+ createElement('gfc:designation'),
721
+ writeCharacterString(featureType.label)
722
+ )
723
+ : null,
724
+ ...featureType.attributes.map(createFeatureAttribute),
725
+ createNilElement('gfc:featureCatalogue', 'missing')
726
+ )
727
+ )
728
+ }
729
+
730
+ export function writeFeatureTypeDescriptions(
731
+ record: DatasetRecord,
732
+ rootEl: XmlElement
733
+ ) {
734
+ const featureTypes = record.featureTypeDescriptions ?? []
735
+ const catalogues = findNestedElements(
736
+ 'mdb:contentInfo',
737
+ 'mrc:MD_FeatureCatalogue',
738
+ 'mrc:featureCatalogue',
739
+ 'gfc:FC_FeatureCatalogue'
740
+ )(rootEl)
741
+ catalogues.forEach((catalogue) =>
742
+ removeChildrenByName('gfc:featureType')(catalogue)
743
+ )
744
+ if (!featureTypes.length) return
745
+
746
+ const catalogue =
747
+ catalogues[0] ?? appendChildTree(createFeatureCatalogueShell())(rootEl)
748
+ appendChildren(...featureTypes.map(createFeatureType))(catalogue)
749
+ }
@@ -118,6 +118,7 @@ export class Iso19139Converter extends BaseConverter<string> {
118
118
  lineage: readLineage,
119
119
  sourceRecords: readSourceRecords,
120
120
  associatedRecords: readAssociatedRecords,
121
+ featureTypeDescriptions: () => undefined,
121
122
  onlineResources: readOnlineResources,
122
123
  temporalExtents: readTemporalExtents,
123
124
  spatialExtents: readSpatialExtents,
@@ -161,6 +162,7 @@ export class Iso19139Converter extends BaseConverter<string> {
161
162
  lineage: writeLineage,
162
163
  sourceRecords: writeSourceRecords,
163
164
  associatedRecords: writeAssociatedRecords,
165
+ featureTypeDescriptions: () => undefined,
164
166
  onlineResources: writeOnlineResources,
165
167
  temporalExtents: writeTemporalExtents,
166
168
  spatialExtents: writeSpatialExtents,
@@ -306,6 +308,10 @@ export class Iso19139Converter extends BaseConverter<string> {
306
308
  const sourceRecords = this.readers['sourceRecords'](rootEl, tr)
307
309
  const associatedRecords = this.readers['associatedRecords'](rootEl, tr)
308
310
  const updateFrequency = this.readers['updateFrequency'](rootEl, tr)
311
+ const featureTypeDescriptions = this.readers['featureTypeDescriptions'](
312
+ rootEl,
313
+ tr
314
+ ) as DatasetRecord['featureTypeDescriptions']
309
315
 
310
316
  return this.afterRecordRead({
311
317
  ...this.readBaseRecord(rootEl, tr),
@@ -317,6 +323,7 @@ export class Iso19139Converter extends BaseConverter<string> {
317
323
  ...(spatialRepresentation && { spatialRepresentation }),
318
324
  temporalExtents,
319
325
  updateFrequency,
326
+ ...(featureTypeDescriptions?.length && { featureTypeDescriptions }),
320
327
  translations: tr,
321
328
  } as DatasetRecord)
322
329
  } else if (kind === 'reuse') {
@@ -416,6 +423,8 @@ export class Iso19139Converter extends BaseConverter<string> {
416
423
 
417
424
  if (record.kind === 'dataset') {
418
425
  fieldChanged('status') && this.writers['status'](record, rootEl)
426
+ fieldChanged('featureTypeDescriptions') &&
427
+ this.writers['featureTypeDescriptions'](record, rootEl)
419
428
  }
420
429
  if (record.kind === 'dataset' || record.kind === 'reuse') {
421
430
  fieldChanged('updateFrequency') &&
@@ -69,7 +69,8 @@ export function extractCharacterString(): ChainableFunction<
69
69
  fallback(
70
70
  findChildElement('gco:CharacterString', false),
71
71
  findChildElement('gmx:Anchor', false),
72
- findChildElement('gmx:MimeFileType', false)
72
+ findChildElement('gmx:MimeFileType', false),
73
+ findChildElement('gco:LocalName', false)
73
74
  ),
74
75
  readText()
75
76
  )
@@ -59,6 +59,8 @@ export class ElasticsearchService {
59
59
  { script: string; type: 'keyword' | 'date' }
60
60
  > = {}
61
61
 
62
+ private fieldAliases: Record<string, string> = {}
63
+
62
64
  // we're using getters in case the defined languages change over time
63
65
  private get metadataLang(): LanguageCode {
64
66
  const mdLangValue = this.injector.get(METADATA_LANGUAGE, null)
@@ -155,6 +157,10 @@ export class ElasticsearchService {
155
157
  this.runtimeFields[fieldName] = { script: expression, type }
156
158
  }
157
159
 
160
+ registerFieldAlias(fieldIdentifier: string, esFieldName: string) {
161
+ this.fieldAliases[fieldIdentifier] = esFieldName
162
+ }
163
+
158
164
  getMetadataByIdsPayload(uuids: string[]): EsSearchParams {
159
165
  return {
160
166
  query: {
@@ -293,12 +299,25 @@ export class ElasticsearchService {
293
299
  return Object.values(filters).find(isBoundingBox)
294
300
  }
295
301
 
296
- // if a field registered min/max runtime fields, its dates form an interval
297
- // the record matches if that interval intersects the filter range
302
+ // builds a query matching records whose dates intersect the filter range
298
303
  private buildDateRangeQuery(searchField: string, dateRange: DateRange) {
304
+ // fields ending with 'DateRange' maps as ES range type
305
+ if (searchField.endsWith('DateRange')) {
306
+ return {
307
+ range: {
308
+ [searchField]: {
309
+ ...(dateRange.start && { gte: formatDate(dateRange.start) }),
310
+ ...(dateRange.end && { lte: formatDate(dateRange.end) }),
311
+ format: 'yyyy-MM-dd',
312
+ relation: 'intersects',
313
+ },
314
+ },
315
+ }
316
+ }
299
317
  const minField = `${searchField}Min`
300
318
  const maxField = `${searchField}Max`
301
319
  if (minField in this.runtimeFields && maxField in this.runtimeFields) {
320
+ // min/max runtime fields form an interval per record; match if it intersects the filter range
302
321
  const filter = [
303
322
  dateRange.start && {
304
323
  range: {
@@ -362,7 +381,8 @@ export class ElasticsearchService {
362
381
  JSON.stringify(filters[fieldname]) !== '{}'
363
382
  )
364
383
  .map(
365
- (fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`
384
+ (fieldname) =>
385
+ `${this.fieldAliases[fieldname] ?? fieldname}:(${makeQuery(filters[fieldname])})`
366
386
  )
367
387
  .join(' AND ')
368
388
  if (filters['gn-ui-crossFieldFilter']) {
@@ -708,6 +728,12 @@ export class ElasticsearchService {
708
728
  order: {
709
729
  [`_${aggregation.sort[1]}`]: aggregation.sort[0],
710
730
  },
731
+ ...(aggregation.includeValues && {
732
+ include: aggregation.includeValues,
733
+ }),
734
+ ...(aggregation.excludeValues && {
735
+ exclude: aggregation.excludeValues,
736
+ }),
711
737
  },
712
738
  }
713
739
  case 'histogram':
@@ -17,9 +17,9 @@ import {
17
17
  AssociatedRecord,
18
18
  CatalogRecord,
19
19
  DatasetFeatureCatalog,
20
- DatasetFeatureType,
21
20
  LanguageCode,
22
21
  LinkedRecord,
22
+ RecordFlag,
23
23
  RecordRelation,
24
24
  } from '../../../../../../libs/common/domain/src/lib/model/record'
25
25
  import {
@@ -37,6 +37,7 @@ import {
37
37
  LanguagesApiService,
38
38
  RecordsApiService,
39
39
  SearchApiService,
40
+ TagsApiService,
40
41
  } from '../../../../../../libs/data-access/gn4/src'
41
42
  import {
42
43
  combineLatest,
@@ -49,7 +50,13 @@ import {
49
50
  switchMap,
50
51
  throwError,
51
52
  } from 'rxjs'
52
- import { catchError, map, tap } from 'rxjs/operators'
53
+ import {
54
+ catchError,
55
+ map,
56
+ shareReplay,
57
+ tap,
58
+ withLatestFrom,
59
+ } from 'rxjs/operators'
53
60
  import { lt } from 'semver'
54
61
  import { ElasticsearchService } from './elasticsearch'
55
62
  import { toLang2 } from '../../../../../../libs/util/i18n/src'
@@ -81,6 +88,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
81
88
  private platformService = inject(PlatformServiceInterface)
82
89
  private gn4LanguagesApi = inject(LanguagesApiService)
83
90
  private settingsService = inject(Gn4SettingsService)
91
+ private gn4TagsApi = inject(TagsApiService)
84
92
  private disableDraft = inject(DISABLE_DRAFT, { optional: true }) ?? false
85
93
  private defaultConverter = inject(DEFAULT_RECORD_CONVERTER)
86
94
 
@@ -182,43 +190,15 @@ export class Gn4Repository implements RecordsRepositoryInterface {
182
190
  )
183
191
  }
184
192
 
185
- private mapEmbeddedFeatureCatalog(
186
- featureTypes: Array<DatasetFeatureType>
187
- ): DatasetFeatureCatalog {
188
- return {
189
- featureTypes: featureTypes.map((featureType) => ({
190
- name: featureType.typeName || '',
191
- definition: featureType.definition || '',
192
- attributes: Array.isArray(featureType.attributeTable)
193
- ? featureType.attributeTable.map((attr) => {
194
- const values = attr.values
195
- ?.filter((v) => v.code || v.label)
196
- .map((v) => ({
197
- code: v.code,
198
- label: v.label,
199
- }))
200
- return {
201
- name: attr.name,
202
- code: attr.code,
203
- definition: attr.definition,
204
- type: attr.type,
205
- ...(values?.length > 0 ? { values } : {}),
206
- }
207
- })
208
- : [],
209
- })),
210
- }
211
- }
212
- getFeatureCatalog(
193
+ queryLinkedFeatureCatalog(
213
194
  record: CatalogRecord,
214
195
  visited: Set<string> = new Set() // prevent looping
215
196
  ): Observable<DatasetFeatureCatalog | null> {
216
197
  if (
217
- record.extras?.['featureTypes'] &&
218
- Array.isArray(record.extras['featureTypes']) &&
219
- record.extras['featureTypes'].length > 0
198
+ 'featureTypeDescriptions' in record &&
199
+ record.featureTypeDescriptions?.length > 0
220
200
  ) {
221
- return of(this.mapEmbeddedFeatureCatalog(record.extras['featureTypes']))
201
+ return of({ featureTypes: record.featureTypeDescriptions })
222
202
  }
223
203
 
224
204
  const featureCatalogIdentifier = record.extras[
@@ -228,7 +208,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
228
208
  visited.add(featureCatalogIdentifier)
229
209
  return this.getRecord(featureCatalogIdentifier).pipe(
230
210
  switchMap((record) =>
231
- record ? this.getFeatureCatalog(record, visited) : of(null)
211
+ record ? this.queryLinkedFeatureCatalog(record, visited) : of(null)
232
212
  )
233
213
  )
234
214
  }
@@ -704,4 +684,72 @@ export class Gn4Repository implements RecordsRepositoryInterface {
704
684
  private removeRecordFromLocalStorage(recordId: string): void {
705
685
  window.localStorage.removeItem(this.getLocalStorageKeyForRecord(recordId))
706
686
  }
687
+
688
+ /*
689
+ * FLAGS
690
+ * Note: in GN4 API, categories are named "tags"
691
+ */
692
+
693
+ categoriesIdentifierForFlags: Observable<Record<RecordFlag, number>> =
694
+ this.gn4TagsApi.getTags().pipe(
695
+ map((categories) =>
696
+ categories.reduce(
697
+ (acc, category) => {
698
+ if (category.name.startsWith('geonetwork-ui-flag:')) {
699
+ acc[
700
+ category.name.replace(/^geonetwork-ui-flag:/, '') as RecordFlag
701
+ ] = category.id
702
+ }
703
+ return acc
704
+ },
705
+ {} as Record<RecordFlag, number>
706
+ )
707
+ ),
708
+ shareReplay(1)
709
+ )
710
+
711
+ private getCategoryIdForFlag(
712
+ flagName: RecordFlag
713
+ ): Observable<number | null> {
714
+ return this.categoriesIdentifierForFlags.pipe(
715
+ map((categories) => {
716
+ const categoryId = categories[flagName]
717
+ if (categoryId === undefined) {
718
+ console.warn(
719
+ `The record flag ${flagName} is not created on GeoNetwork backend; please create a category with the name geonetwork-ui-flag:${flagName} for this to work.`
720
+ )
721
+ return null
722
+ }
723
+ return categoryId
724
+ })
725
+ )
726
+ }
727
+
728
+ setRecordFlag(
729
+ uniqueIdentifier: string,
730
+ flagName: RecordFlag,
731
+ value: boolean
732
+ ): Observable<void> {
733
+ return this.getCategoryIdForFlag(flagName).pipe(
734
+ switchMap((categoryId) => {
735
+ if (categoryId === null) return of()
736
+ return value
737
+ ? this.gn4RecordsApi.tagRecord(uniqueIdentifier, [categoryId], false)
738
+ : this.gn4RecordsApi.deleteTags(uniqueIdentifier, [categoryId])
739
+ })
740
+ )
741
+ }
742
+
743
+ getRecordFlag(
744
+ uniqueIdentifier: string,
745
+ flagName: RecordFlag
746
+ ): Observable<boolean> {
747
+ return this.gn4RecordsApi.getRecordTags(uniqueIdentifier).pipe(
748
+ withLatestFrom(this.getCategoryIdForFlag(flagName)),
749
+ map(([tags, categoryId]) => {
750
+ if (categoryId === null) return false
751
+ return Array.from(tags).some((tag) => tag.id === categoryId)
752
+ })
753
+ )
754
+ }
707
755
  }
@@ -92,6 +92,13 @@ export class Gn4PlatformService implements PlatformServiceInterface {
92
92
  switchMap((apiUser) => this.mapper.userFromMeApi(apiUser)),
93
93
  shareReplay({ bufferSize: 1, refCount: true })
94
94
  )
95
+ private isAdmin$ = this.disableAuth
96
+ ? of(false)
97
+ : of(true).pipe(
98
+ switchMap(() => this.meApi.getMe()),
99
+ map((apiUser) => !!apiUser?.admin),
100
+ shareReplay({ bufferSize: 1, refCount: true })
101
+ )
95
102
 
96
103
  /**
97
104
  * A map of already loaded thesauri (groups of keywords); the key is a URI
@@ -146,6 +153,10 @@ export class Gn4PlatformService implements PlatformServiceInterface {
146
153
  return this.isUserAnonymous$
147
154
  }
148
155
 
156
+ isAdministrator(): Observable<boolean> {
157
+ return this.isAdmin$
158
+ }
159
+
149
160
  getOrganizations(): Observable<Organization[]> {
150
161
  return undefined
151
162
  }
@@ -0,0 +1 @@
1
+ export type RecordFlag = 'IS_REFERENCE_DATASET'
@@ -3,3 +3,4 @@ export * from './metadata.model'
3
3
  export * from './organization.model'
4
4
  export * from './translation.model'
5
5
  export * from './user-feedbacks.model'
6
+ export * from './flags.model'
@@ -316,6 +316,7 @@ export interface DatasetRecord extends BaseRecord {
316
316
  spatialExtents: Array<DatasetSpatialExtent>
317
317
  temporalExtents: Array<DatasetTemporalExtent>
318
318
  spatialRepresentation?: SpatialRepresentationType
319
+ featureTypeDescriptions?: Array<DatasetFeatureType>
319
320
  }
320
321
 
321
322
  export interface ServiceEndpoint {
@@ -347,33 +348,28 @@ export interface ReuseRecord extends BaseRecord {
347
348
  temporalExtents: Array<DatasetTemporalExtent>
348
349
  }
349
350
 
351
+ export interface DatasetFeatureType {
352
+ name: string
353
+ label?: string
354
+ description?: string
355
+ attributes: DatasetFeatureAttribute[]
356
+ }
350
357
  export type DatasetFeatureCatalog = {
351
- featureTypes: Array<{
352
- name: string
353
- definition: string
354
- attributes: DatasetFeatureAttribute[]
355
- }>
358
+ featureTypes: Array<DatasetFeatureType>
356
359
  }
357
360
  export interface DatasetFeatureAttributeValue {
358
- code?: string
359
- definition?: string
361
+ value: string // empty string when the value has no code
360
362
  label?: string
363
+ description?: string
361
364
  }
362
365
  export interface DatasetFeatureAttribute {
363
366
  name: string
364
- definition: string
365
- type: string
366
- code: string
367
+ label?: string
368
+ description?: string
369
+ type?: string
370
+ code?: string
367
371
  values?: Array<DatasetFeatureAttributeValue>
368
372
  }
369
- export interface DatasetFeatureType {
370
- aliases: string
371
- code: string
372
- isAbstract: string
373
- typeName: string
374
- definition: string
375
- attributeTable: Array<DatasetFeatureAttribute>
376
- }
377
373
 
378
374
  export type ReuseType = 'application' | 'map' | 'other'
379
375
 
@@ -7,6 +7,8 @@ export interface TermsAggregationParams {
7
7
  limit: number
8
8
  sort: AggregationSort
9
9
  filter?: string
10
+ includeValues?: string[]
11
+ excludeValues?: string[]
10
12
  }
11
13
  export interface HistogramAggregationParams {
12
14
  type: 'histogram'
@@ -21,7 +21,10 @@ export type QueryString = {
21
21
  query_string: string
22
22
  }
23
23
  export type QueryRange = {
24
- range: Record<string, { format: string; gte: string; lte: string }>
24
+ range: Record<
25
+ string,
26
+ { format: string; gte?: string; lte?: string; relation?: string }
27
+ >
25
28
  }
26
29
  export type QueryGeoShape = {
27
30
  geo_shape: {
@@ -28,6 +28,7 @@ export abstract class PlatformServiceInterface {
28
28
  abstract supportsAuthentication(): boolean
29
29
  abstract getMe(): Observable<UserModel>
30
30
  abstract isAnonymous(): Observable<boolean>
31
+ abstract isAdministrator(): Observable<boolean>
31
32
  abstract getUsers(): Observable<UserModel[]>
32
33
  abstract getUserPermissionsByGroup(): Observable<GroupModel[]>
33
34
  abstract getUsersByOrganization(
@@ -11,13 +11,14 @@ import {
11
11
  DatasetFeatureCatalog,
12
12
  LanguageCode,
13
13
  LinkedRecord,
14
+ RecordFlag,
14
15
  } from '../model/record'
15
16
 
16
17
  export abstract class RecordsRepositoryInterface {
17
18
  abstract search(params: SearchParams): Observable<SearchResults>
18
19
  abstract getMatchesCount(filters: FieldFilters): Observable<number>
19
20
  abstract getRecord(uniqueIdentifier: string): Observable<CatalogRecord | null>
20
- abstract getFeatureCatalog(
21
+ abstract queryLinkedFeatureCatalog(
21
22
  record: CatalogRecord
22
23
  ): Observable<DatasetFeatureCatalog | null>
23
24
  abstract aggregate(params: AggregationsParams): Observable<Aggregations>
@@ -106,4 +107,22 @@ export abstract class RecordsRepositoryInterface {
106
107
  ): Observable<{ user: string; date: Date }>
107
108
  abstract getRecordPublicationStatus(uuid: string): Observable<boolean>
108
109
  abstract getApplicationLanguages(): Observable<LanguageCode[]>
110
+
111
+ /**
112
+ * Sets a given flag on a record. Flags are not part of the metadata document, which is why they are
113
+ * not part of the Record model.
114
+ */
115
+ abstract setRecordFlag(
116
+ uniqueIdentifier: string,
117
+ flagName: RecordFlag,
118
+ value: boolean
119
+ ): Observable<void>
120
+
121
+ /**
122
+ * Checks whether a given flag is enabled for a record.
123
+ */
124
+ abstract getRecordFlag(
125
+ uniqueIdentifier: string,
126
+ flagName: RecordFlag
127
+ ): Observable<boolean>
109
128
  }