geonetwork-ui 2.11.0-dev.8442dae6e → 2.11.0-dev.9ba9c4d3d

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 (88) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1333 -691
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +228 -81
  4. package/index.d.ts.map +1 -1
  5. package/package.json +12 -11
  6. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +43 -13
  7. package/src/libs/api/metadata-converter/src/lib/gn4/types/elasticsearch.model.ts +13 -1
  8. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +70 -10
  9. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
  10. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +22 -0
  11. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +13 -1
  12. package/src/libs/common/domain/src/lib/model/search/sort-by.model.ts +1 -5
  13. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
  14. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  15. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  16. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
  17. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-associated-records/form-field-associated-records.component.css +0 -0
  18. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-associated-records/form-field-associated-records.component.html +33 -0
  19. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-associated-records/form-field-associated-records.component.ts +94 -0
  20. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.html +16 -12
  21. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +6 -0
  22. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +6 -0
  23. package/src/libs/feature/editor/src/lib/fields.config.ts +5 -0
  24. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  25. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  26. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  27. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
  28. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
  29. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  30. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  31. package/src/libs/feature/router/src/lib/default/router.service.ts +1 -1
  32. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  33. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +9 -4
  34. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +46 -19
  35. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +17 -8
  36. package/src/libs/feature/search/src/lib/sort-by/sort-by.component.ts +1 -1
  37. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +3 -1
  38. package/src/libs/feature/search/src/lib/utils/service/fields.ts +27 -2
  39. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  40. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  41. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  42. package/src/libs/ui/inputs/src/index.ts +2 -0
  43. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  44. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  45. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  46. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  47. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  48. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  49. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  50. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  51. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  52. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
  53. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css +0 -0
  54. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +117 -0
  55. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +191 -0
  56. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  57. package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
  58. package/src/libs/util/app-config/src/lib/model.ts +1 -0
  59. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  60. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  61. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  62. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  63. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  64. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  65. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  66. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  67. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  68. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  69. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  70. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  71. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  72. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  73. package/src/libs/util/shared/src/index.ts +1 -0
  74. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  75. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  76. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  77. package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
  78. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  79. package/tailwind.base.css +5 -0
  80. package/translations/de.json +26 -3
  81. package/translations/en.json +25 -2
  82. package/translations/es.json +23 -0
  83. package/translations/fr.json +26 -3
  84. package/translations/it.json +24 -1
  85. package/translations/nl.json +23 -0
  86. package/translations/pt.json +23 -0
  87. package/translations/sk.json +24 -1
  88. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
@@ -11,6 +11,7 @@ import {
11
11
  AggregationsParams,
12
12
  FieldFilter,
13
13
  FieldFilters,
14
+ FieldSort,
14
15
  FilterQuery,
15
16
  FiltersAggregationParams,
16
17
  SortByField,
@@ -33,7 +34,12 @@ import {
33
34
  LanguageCode,
34
35
  } from '../../../../../../../libs/common/domain/src/lib/model/record'
35
36
  import { TranslateService } from '@ngx-translate/core'
36
- import { getGeometryBoundingBox } from '../../../../../../../libs/util/shared/src'
37
+ import {
38
+ bboxToPolygon,
39
+ BoundingBox,
40
+ getGeometryBoundingBox,
41
+ isBoundingBox,
42
+ } from '../../../../../../../libs/util/shared/src'
37
43
  import { getLength as getGeodesicLength } from 'ol/sphere.js'
38
44
  import { LineString } from 'ol/geom.js'
39
45
 
@@ -141,6 +147,7 @@ export class ElasticsearchService {
141
147
  values: uuids,
142
148
  },
143
149
  },
150
+ size: uuids.length,
144
151
  }
145
152
  }
146
153
 
@@ -199,9 +206,27 @@ export class ElasticsearchService {
199
206
  }
200
207
 
201
208
  private buildPayloadSort(sortBy: SortByField): SortParams {
202
- if (sortBy === null) return undefined
203
- const fields = Array.isArray(sortBy[0]) ? sortBy : [sortBy]
204
- return fields.map((field) => ({ [field[1]]: field[0] }))
209
+ if (!sortBy || sortBy.length === 0) return undefined
210
+ const fields = Array.isArray(sortBy[0])
211
+ ? (sortBy as FieldSort[])
212
+ : [sortBy as FieldSort]
213
+ return fields.map((field) => {
214
+ // Sort by nested array of dates only works with the explicit syntax
215
+ if (field[1].endsWith('.date')) {
216
+ const nestedPath = field[1].slice(0, field[1].lastIndexOf('.date'))
217
+ return {
218
+ [field[1]]: {
219
+ order: field[0] as 'desc' | 'asc',
220
+ mode: field[0] === 'desc' ? 'max' : 'min',
221
+ missing: '_last',
222
+ nested: {
223
+ path: nestedPath,
224
+ },
225
+ },
226
+ }
227
+ }
228
+ return { [field[1]]: field[0] }
229
+ })
205
230
  }
206
231
 
207
232
  private injectLangInQueryStringFields(
@@ -244,8 +269,18 @@ export class ElasticsearchService {
244
269
  return this.metadataLang === 'current'
245
270
  }
246
271
 
247
- private filtersToQuery(
272
+ private findSpatialFilterExtent(
248
273
  filters: FieldFilters | FiltersAggregationParams | string
274
+ ): BoundingBox | undefined {
275
+ if (typeof filters === 'string') {
276
+ return undefined
277
+ }
278
+ return Object.values(filters).find(isBoundingBox)
279
+ }
280
+
281
+ private filtersToQuery(
282
+ filters: FieldFilters | FiltersAggregationParams | string,
283
+ spatialFilterExtent = this.findSpatialFilterExtent(filters)
249
284
  ): FilterQuery {
250
285
  const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
251
286
  const makeQuery = (filter: FieldFilter): string => {
@@ -266,7 +301,9 @@ export class ElasticsearchService {
266
301
  ? filters
267
302
  : Object.keys(filters)
268
303
  .filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
304
+ .filter((fieldname) => !isBoundingBox(filters[fieldname]))
269
305
  .filter((fieldname) => !isDateRange(filters[fieldname]))
306
+ .filter((fieldname) => !Array.isArray(filters[fieldname]))
270
307
  .filter(
271
308
  (fieldname) =>
272
309
  filters[fieldname] &&
@@ -310,6 +347,21 @@ export class ElasticsearchService {
310
347
  },
311
348
  },
312
349
  },
350
+ spatialFilterExtent && {
351
+ geo_shape: {
352
+ geom: {
353
+ shape: {
354
+ type: 'envelope',
355
+ // spatialFilterExtent is [minX, minY, maxX, maxY]; envelope coordinates are [top-left, bottom-right]
356
+ coordinates: [
357
+ [spatialFilterExtent[0], spatialFilterExtent[3]],
358
+ [spatialFilterExtent[2], spatialFilterExtent[1]],
359
+ ],
360
+ },
361
+ relation: 'intersects',
362
+ },
363
+ },
364
+ },
313
365
  ].filter(Boolean)
314
366
  return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
315
367
  }
@@ -348,7 +400,12 @@ export class ElasticsearchService {
348
400
  },
349
401
  })
350
402
  }
351
- const queryFilters = this.filtersToQuery(fieldSearchFilters)
403
+ // a spatial extent filter takes precedence over the preference geometry for boosting
404
+ const spatialFilterExtent = this.findSpatialFilterExtent(fieldSearchFilters)
405
+ const queryFilters = this.filtersToQuery(
406
+ fieldSearchFilters,
407
+ spatialFilterExtent
408
+ )
352
409
  if (queryFilters) {
353
410
  filter.push(...queryFilters)
354
411
  }
@@ -359,7 +416,10 @@ export class ElasticsearchService {
359
416
  },
360
417
  })
361
418
  }
362
- if (geometry) {
419
+ const boostGeometry = spatialFilterExtent
420
+ ? bboxToPolygon(spatialFilterExtent)
421
+ : geometry
422
+ if (boostGeometry) {
363
423
  // boosts applied using the filter geometry:
364
424
  // * records completely within the geometry receive a boost of 5
365
425
  // * records intersecting the geometry receive a boost of 2
@@ -369,7 +429,7 @@ export class ElasticsearchService {
369
429
  {
370
430
  geo_shape: {
371
431
  geom: {
372
- shape: geometry,
432
+ shape: boostGeometry,
373
433
  relation: 'within',
374
434
  },
375
435
  boost: 5.0,
@@ -378,7 +438,7 @@ export class ElasticsearchService {
378
438
  {
379
439
  geo_shape: {
380
440
  geom: {
381
- shape: geometry,
441
+ shape: boostGeometry,
382
442
  relation: 'intersects',
383
443
  },
384
444
  boost: 2.0,
@@ -389,7 +449,7 @@ export class ElasticsearchService {
389
449
  // this will boost the results variably depending on their distance from the given geometry
390
450
  // note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
391
451
  // combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
392
- const bbox = getGeometryBoundingBox(geometry)
452
+ const bbox = getGeometryBoundingBox(boostGeometry)
393
453
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
394
454
  const northToCenter = new LineString([
395
455
  [center[0], bbox[3]],
@@ -14,10 +14,13 @@ import {
14
14
  } from '../../../../../../libs/api/metadata-converter/src'
15
15
  import { PublicationVersionError } from '../../../../../../libs/common/domain/src/lib/model/error'
16
16
  import {
17
+ AssociatedRecord,
17
18
  CatalogRecord,
18
19
  DatasetFeatureCatalog,
19
20
  DatasetFeatureType,
20
21
  LanguageCode,
22
+ LinkedRecord,
23
+ RecordRelation,
21
24
  } from '../../../../../../libs/common/domain/src/lib/model/record'
22
25
  import {
23
26
  Aggregations,
@@ -145,7 +148,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
145
148
  return this.gn4SearchApi
146
149
  .search(
147
150
  'bucket',
148
- ['fcats', 'hassources'],
151
+ ['fcats', 'hassources', 'siblings', 'associated'],
149
152
  JSON.stringify(
150
153
  this.gn4SearchHelper.getMetadataByIdsPayload([uniqueIdentifier])
151
154
  )
@@ -249,22 +252,43 @@ export class Gn4Repository implements RecordsRepositoryInterface {
249
252
  )
250
253
  }
251
254
 
252
- getSources(record: CatalogRecord): Observable<CatalogRecord[]> {
253
- const sourcesIdentifiers = record.extras?.['sourcesIdentifiers'] as string[]
254
- if (sourcesIdentifiers && sourcesIdentifiers.length > 0) {
255
- return this.getMultipleRecords(sourcesIdentifiers)
256
- }
257
- return of(null)
258
- }
259
-
260
- getSourceOf(record: CatalogRecord): Observable<CatalogRecord[]> {
261
- const sourceOfIdentifiers = record.extras?.[
262
- 'sourceOfIdentifiers'
263
- ] as string[]
264
- if (sourceOfIdentifiers && sourceOfIdentifiers.length > 0) {
265
- return this.getMultipleRecords(sourceOfIdentifiers)
255
+ getLinkedRecords(record: CatalogRecord): Observable<LinkedRecord[]> {
256
+ const siblings = (record.extras?.['siblings'] ?? []) as AssociatedRecord[]
257
+ const relations: Array<[RecordRelation, string[]]> = [
258
+ ['source', (record.extras?.['sourcesIdentifiers'] ?? []) as string[]],
259
+ ['sourceOf', (record.extras?.['sourceOfIdentifiers'] ?? []) as string[]],
260
+ ['sibling', siblings.map(({ uniqueIdentifier }) => uniqueIdentifier)],
261
+ [
262
+ 'associated',
263
+ (record.extras?.['associatedIdentifiers'] ?? []) as string[],
264
+ ],
265
+ ]
266
+ const requested = relations.filter(
267
+ ([, identifiers]) => identifiers.length > 0
268
+ )
269
+ if (requested.length === 0) {
270
+ return of([])
266
271
  }
267
- return of(null)
272
+ return forkJoin(
273
+ requested.map(([relation, identifiers]) =>
274
+ this.getMultipleRecords(identifiers).pipe(
275
+ map((records) =>
276
+ (records ?? []).map((record) => ({
277
+ record,
278
+ relation,
279
+ associationType:
280
+ relation === 'sibling'
281
+ ? siblings.find(
282
+ ({ uniqueIdentifier }) =>
283
+ uniqueIdentifier === record.uniqueIdentifier
284
+ )?.associationType
285
+ : undefined,
286
+ }))
287
+ ),
288
+ catchError(() => of([]))
289
+ )
290
+ )
291
+ ).pipe(map((groups) => groups.flat()))
268
292
  }
269
293
 
270
294
  aggregate(params: AggregationsParams): Observable<Aggregations> {
@@ -260,6 +260,16 @@ export interface SourceRecord {
260
260
  href?: string
261
261
  }
262
262
 
263
+ marker('domain.record.associationType.crossReference')
264
+ marker('domain.record.associationType.largerWorkCitation')
265
+ marker('domain.record.associationType.partOfSeamlessDatabase')
266
+ marker('domain.record.associationType.stereoMate')
267
+ marker('domain.record.associationType.isComposedOf')
268
+ marker('domain.record.associationType.collectiveTitle')
269
+ marker('domain.record.associationType.series')
270
+ marker('domain.record.associationType.dependency')
271
+ marker('domain.record.associationType.revisionOf')
272
+
263
273
  // DS_AssociationTypeCode values of the ISO19115-3 codelist, any unrecognized value
264
274
  // being mapped to 'crossReference'
265
275
  export const associationTypeValues = [
@@ -284,6 +294,18 @@ export interface AssociatedRecord {
284
294
  associationType: AssociationType
285
295
  }
286
296
 
297
+ /**
298
+ * Nature of the link between a record and another one; the association type is
299
+ * only known for 'sibling' relations, as it is held by the declaring record.
300
+ */
301
+ export type RecordRelation = 'source' | 'sourceOf' | 'sibling' | 'associated'
302
+
303
+ export interface LinkedRecord {
304
+ record: CatalogRecord
305
+ relation: RecordRelation
306
+ associationType?: AssociationType
307
+ }
308
+
287
309
  export interface DatasetRecord extends BaseRecord {
288
310
  kind: 'dataset'
289
311
  status: RecordStatus
@@ -6,11 +6,15 @@ export type FieldFilterByRange = {
6
6
  start?: Date
7
7
  end?: Date
8
8
  }
9
+ // matches util/shared's BoundingBox; duplicated here to avoid a circular
10
+ // dependency (util/shared already depends on common/domain)
11
+ export type FieldFilterByBoundingBox = [number, number, number, number]
9
12
 
10
13
  export type FieldFilter =
11
14
  | FieldFilterByExpression
12
15
  | FieldFilterByValues
13
16
  | FieldFilterByRange
17
+ | FieldFilterByBoundingBox
14
18
  export type FieldFilters = Record<FieldName, FieldFilter>
15
19
 
16
20
  export type QueryString = {
@@ -19,4 +23,12 @@ export type QueryString = {
19
23
  export type QueryRange = {
20
24
  range: Record<string, { format: string; gte: string; lte: string }>
21
25
  }
22
- export type FilterQuery = Array<QueryString | QueryRange>
26
+ export type QueryGeoShape = {
27
+ geo_shape: {
28
+ geom: {
29
+ shape: { type: string; coordinates: number[][] }
30
+ relation: string
31
+ }
32
+ }
33
+ }
34
+ export type FilterQuery = Array<QueryString | QueryRange | QueryGeoShape>
@@ -6,9 +6,5 @@ export const SortByEnum: Record<string, SortByField> = {
6
6
  RELEVANCY: ['desc', '_score'],
7
7
  QUALITY_SCORE: ['desc', 'qualityScore'],
8
8
  CHANGE_DATE: ['desc', 'changeDate'],
9
- RESOURCE_DATES: [
10
- ['desc', 'revisionDateForResource'],
11
- ['desc', 'publicationDateForResource'],
12
- ['desc', 'creationDateForResource'],
13
- ],
9
+ RESOURCE_DATE: ['desc', 'resourceDate.date'],
14
10
  }
@@ -10,6 +10,7 @@ import {
10
10
  CatalogRecord,
11
11
  DatasetFeatureCatalog,
12
12
  LanguageCode,
13
+ LinkedRecord,
13
14
  } from '../model/record'
14
15
 
15
16
  export abstract class RecordsRepositoryInterface {
@@ -23,8 +24,7 @@ export abstract class RecordsRepositoryInterface {
23
24
  abstract getSimilarRecords(
24
25
  similarTo: CatalogRecord
25
26
  ): Observable<CatalogRecord[]>
26
- abstract getSources(record: CatalogRecord): Observable<CatalogRecord[]>
27
- abstract getSourceOf(record: CatalogRecord): Observable<CatalogRecord[]>
27
+ abstract getLinkedRecords(record: CatalogRecord): Observable<LinkedRecord[]>
28
28
  abstract fuzzySearch(query: string): Observable<SearchResults>
29
29
  abstract canDuplicate(record: CatalogRecord): boolean
30
30
  abstract canDelete(record: CatalogRecord): Observable<boolean>
@@ -2,9 +2,9 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  ChangeDetectorRef,
4
4
  Component,
5
+ inject,
5
6
  Input,
6
7
  Output,
7
- inject,
8
8
  } from '@angular/core'
9
9
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
10
10
  import {
@@ -183,7 +183,7 @@ export class ChartViewComponent {
183
183
  yChoices$ = this.properties$.pipe(
184
184
  map((properties) =>
185
185
  properties
186
- .filter((prop) => prop.type === 'number' || prop.type === 'date')
186
+ .filter((prop) => prop.type === 'number')
187
187
  .map((prop) => ({ value: prop.name, label: prop.label || prop.name }))
188
188
  ),
189
189
  tap((choices) => {
@@ -222,6 +222,7 @@ export class ChartViewComponent {
222
222
  switchMap(([dataset, xProp, yProp, aggregation]) => {
223
223
  const fieldAgg: FieldAggregation =
224
224
  aggregation === 'count' ? ['count'] : [aggregation, yProp]
225
+ this.loading = true
225
226
  return dataset
226
227
  .groupBy(['distinct', xProp])
227
228
  .aggregate(fieldAgg)
@@ -2,11 +2,11 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  ChangeDetectorRef,
4
4
  Component,
5
+ inject,
5
6
  Input,
6
7
  OnDestroy,
7
8
  OnInit,
8
9
  ViewChild,
9
- inject,
10
10
  } from '@angular/core'
11
11
  import {
12
12
  DataTableComponent,
@@ -75,6 +75,7 @@ export class GeoTableViewComponent implements OnInit, OnDestroy {
75
75
  }
76
76
 
77
77
  private async initMapContext(): Promise<MapContext> {
78
+ this.dataset.load()
78
79
  this.dataset.selectAll()
79
80
  return {
80
81
  layers: [
@@ -1,15 +1,15 @@
1
- import { Injectable, inject } from '@angular/core'
1
+ import { inject, Injectable } from '@angular/core'
2
2
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
3
3
  import {
4
+ GetCollectionItemsOptions,
4
5
  OgcApiCollectionInfo,
5
6
  OgcApiEndpoint,
6
7
  OgcApiRecord,
7
- WfsEndpoint,
8
- WfsVersion,
9
- TmsEndpoint,
10
8
  StacEndpoint,
11
- GetCollectionItemsOptions,
12
9
  StacItemsDocument,
10
+ TmsEndpoint,
11
+ WfsEndpoint,
12
+ WfsVersion,
13
13
  } from '@camptocamp/ogc-client'
14
14
  import {
15
15
  BaseReader,
@@ -26,7 +26,7 @@ import {
26
26
  } from '../../../../../../libs/util/shared/src'
27
27
  import type { FeatureCollection } from 'geojson'
28
28
  import { from, Observable, throwError } from 'rxjs'
29
- import { catchError, map, switchMap, tap } from 'rxjs/operators'
29
+ import { catchError, map, switchMap } from 'rxjs/operators'
30
30
  import {
31
31
  DatasetOnlineResource,
32
32
  DatasetServiceDistribution,
@@ -347,14 +347,11 @@ export class DataService {
347
347
  if (link.type === 'service' && link.accessServiceProtocol === 'wfs') {
348
348
  const wfsUrlEndpoint = this.proxy.getProxiedUrl(link.url.toString())
349
349
  return from(
350
- openDataset(
351
- wfsUrlEndpoint,
352
- 'wfs',
353
- {
354
- wfsFeatureType: link.name,
355
- },
356
- cacheActive
357
- )
350
+ openDataset(wfsUrlEndpoint, {
351
+ typeHint: 'wfs',
352
+ wfsFeatureType: link.name,
353
+ enableCache: cacheActive,
354
+ })
358
355
  )
359
356
  } else if (link.type === 'download') {
360
357
  const linkProxifiedUrl = this.proxy.getProxiedUrl(link.url.toString())
@@ -364,7 +361,10 @@ export class DataService {
364
361
  ? (format as SupportedType)
365
362
  : undefined
366
363
  return from(
367
- openDataset(linkProxifiedUrl, supportedType, undefined, cacheActive)
364
+ openDataset(linkProxifiedUrl, {
365
+ typeHint: supportedType,
366
+ enableCache: cacheActive,
367
+ })
368
368
  ).pipe()
369
369
  } else if (
370
370
  link.type === 'service' &&
@@ -374,7 +374,9 @@ export class DataService {
374
374
  link.url.toString(),
375
375
  'geojson'
376
376
  )
377
- return from(openDataset(url, 'geojson', undefined, cacheActive)).pipe()
377
+ return from(
378
+ openDataset(url, { typeHint: 'geojson', enableCache: cacheActive })
379
+ ).pipe()
378
380
  } else if (
379
381
  link.type === 'service' &&
380
382
  link.accessServiceProtocol === 'ogcFeatures'
@@ -395,12 +397,10 @@ export class DataService {
395
397
  }
396
398
  const urlWithoutLimit = new URL(geojsonUrl)
397
399
  urlWithoutLimit.searchParams.delete('limit')
398
- return openDataset(
399
- urlWithoutLimit.toString(),
400
- 'geojson',
401
- undefined,
402
- cacheActive
403
- )
400
+ return openDataset(urlWithoutLimit.toString(), {
401
+ typeHint: 'geojson',
402
+ enableCache: cacheActive,
403
+ })
404
404
  })
405
405
  )
406
406
  }
@@ -0,0 +1,33 @@
1
+ <div class="flex flex-col gap-2">
2
+ <gn-ui-check-toggle
3
+ [label]="'editor.record.form.field.parentLink' | translate"
4
+ [value]="hasParentLink"
5
+ (toggled)="onToggle($event)"
6
+ data-cy="associated-record-toggle"
7
+ ></gn-ui-check-toggle>
8
+ @if (hasParentLink) {
9
+ <gn-ui-form-field-wrapper
10
+ [label]="'editor.record.form.field.parentIdentifier' | translate"
11
+ >
12
+ <gn-ui-text-input
13
+ [value]="uniqueIdentifier"
14
+ (valueChange)="onUniqueIdentifierChange($event)"
15
+ placeholder=""
16
+ data-cy="associated-record-identifier"
17
+ ></gn-ui-text-input>
18
+ </gn-ui-form-field-wrapper>
19
+ <gn-ui-form-field-wrapper
20
+ [label]="'editor.record.form.field.parentType' | translate"
21
+ >
22
+ <gn-ui-dropdown-selector
23
+ [title]="'editor.record.form.field.parentType' | translate"
24
+ [showTitle]="false"
25
+ [choices]="choices"
26
+ [selected]="selectedType"
27
+ (selectValue)="onTypeChange($event)"
28
+ [extraBtnClass]="'input-as-button gn-ui-text-input'"
29
+ data-cy="associated-record-type"
30
+ ></gn-ui-dropdown-selector>
31
+ </gn-ui-form-field-wrapper>
32
+ }
33
+ </div>
@@ -0,0 +1,94 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ Input,
6
+ Output,
7
+ } from '@angular/core'
8
+ import {
9
+ AssociatedRecord,
10
+ AssociationType,
11
+ associationTypeValues,
12
+ } from '../../../../../../../../../libs/common/domain/src/lib/model/record'
13
+ import {
14
+ CheckToggleComponent,
15
+ DropdownChoice,
16
+ DropdownSelectorComponent,
17
+ TextInputComponent,
18
+ } from '../../../../../../../../../libs/ui/inputs/src'
19
+ import { FormFieldWrapperComponent } from '../../../../../../../../../libs/ui/layout/src'
20
+ import { TranslatePipe } from '@ngx-translate/core'
21
+
22
+ @Component({
23
+ selector: 'gn-ui-form-field-associated-records',
24
+ templateUrl: './form-field-associated-records.component.html',
25
+ styleUrls: ['./form-field-associated-records.component.css'],
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ standalone: true,
28
+ imports: [
29
+ CheckToggleComponent,
30
+ TextInputComponent,
31
+ DropdownSelectorComponent,
32
+ FormFieldWrapperComponent,
33
+ TranslatePipe,
34
+ ],
35
+ })
36
+ export class FormFieldAssociatedRecordsComponent {
37
+ @Input() value?: AssociatedRecord[]
38
+ @Output() valueChange = new EventEmitter<AssociatedRecord[]>()
39
+
40
+ private get list() {
41
+ return this.value ?? []
42
+ }
43
+ private get first() {
44
+ return this.list[0]
45
+ }
46
+ private get rest() {
47
+ return this.list.slice(1)
48
+ }
49
+
50
+ get hasParentLink() {
51
+ return this.list.length > 0
52
+ }
53
+ get uniqueIdentifier() {
54
+ return this.first?.uniqueIdentifier ?? ''
55
+ }
56
+ get selectedType() {
57
+ return this.first?.associationType
58
+ }
59
+ get choices(): DropdownChoice[] {
60
+ return associationTypeValues.map((value) => ({
61
+ value,
62
+ label: `domain.record.associationType.${value}`,
63
+ }))
64
+ }
65
+
66
+ onToggle(checked: boolean) {
67
+ this.valueChange.emit(
68
+ checked
69
+ ? [
70
+ {
71
+ uniqueIdentifier: '',
72
+ associationType: associationTypeValues[0],
73
+ },
74
+ ...this.rest,
75
+ ]
76
+ : this.rest
77
+ )
78
+ }
79
+
80
+ onUniqueIdentifierChange(uniqueIdentifier: string) {
81
+ this.valueChange.emit(
82
+ uniqueIdentifier
83
+ ? [{ ...this.first, uniqueIdentifier }, ...this.rest]
84
+ : this.rest
85
+ )
86
+ }
87
+
88
+ onTypeChange(associationType: DropdownChoice['value']) {
89
+ this.valueChange.emit([
90
+ { ...this.first, associationType: associationType as AssociationType },
91
+ ...this.rest,
92
+ ])
93
+ }
94
+ }
@@ -1,17 +1,21 @@
1
- <div class="flex gap-2 mb-2">
2
- @for (addOption of addOptions$ | async; track addOption) {
3
- <gn-ui-button type="gray" (buttonClick)="onAdd(addOption.eventName)">
4
- <ng-icon name="iconoirPlus" class="text-primary"></ng-icon> &nbsp;{{
5
- addOption.buttonLabel
6
- }}</gn-ui-button
7
- >
1
+ <div class="flex flex-col gap-2">
2
+ <div class="flex gap-2">
3
+ @for (addOption of addOptions$ | async; track addOption) {
4
+ <gn-ui-button type="gray" (buttonClick)="onAdd(addOption.eventName)">
5
+ <ng-icon name="iconoirPlus" class="text-primary"></ng-icon> &nbsp;{{
6
+ addOption.buttonLabel
7
+ }}</gn-ui-button
8
+ >
9
+ }
10
+ </div>
11
+ @if (extents.length) {
12
+ <gn-ui-sortable-list
13
+ [items]="extents"
14
+ (itemsOrderChange)="onItemsOrderChange($event)"
15
+ [elementTemplate]="template"
16
+ ></gn-ui-sortable-list>
8
17
  }
9
18
  </div>
10
- <gn-ui-sortable-list
11
- [items]="extents"
12
- (itemsOrderChange)="onItemsOrderChange($event)"
13
- [elementTemplate]="template"
14
- ></gn-ui-sortable-list>
15
19
  <ng-template #template let-extent let-index="index">
16
20
  @if (extent.end === undefined) {
17
21
  <div class="border rounded-lg px-4 pb-4">
@@ -98,6 +98,12 @@
98
98
  [recordKind]="recordKind"
99
99
  ></gn-ui-form-field-update-frequency>
100
100
  </ng-container>
101
+ <ng-container *ngSwitchCase="'associatedRecords'">
102
+ <gn-ui-form-field-associated-records
103
+ [value]="valueAsAssociatedRecords"
104
+ (valueChange)="valueChange.emit($event)"
105
+ ></gn-ui-form-field-associated-records>
106
+ </ng-container>
101
107
  <ng-container *ngSwitchCase="'temporalExtents'">
102
108
  <gn-ui-form-field-temporal-extents
103
109
  [value]="valueAsTemporalExtents"