geonetwork-ui 2.11.0-dev.a83f0f51a → 2.11.0-dev.bc1a6d4e2

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 (77) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1244 -677
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +219 -78
  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/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +48 -7
  8. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
  9. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +12 -0
  10. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +13 -1
  11. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
  12. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  13. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  14. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
  15. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  16. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  17. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  18. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
  19. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
  20. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  21. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  22. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  23. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +11 -4
  24. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
  25. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +17 -8
  26. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +3 -1
  27. package/src/libs/feature/search/src/lib/utils/service/fields.ts +27 -2
  28. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  29. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  30. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  31. package/src/libs/ui/inputs/src/index.ts +2 -0
  32. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  33. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  34. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  35. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  36. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  37. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  38. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  39. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  40. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  41. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
  42. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css +0 -0
  43. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  44. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -0
  45. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  46. package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
  47. package/src/libs/util/app-config/src/lib/model.ts +1 -0
  48. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  49. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  50. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  51. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  52. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  53. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  54. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  55. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  56. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  57. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  58. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  59. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  60. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  61. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  62. package/src/libs/util/shared/src/index.ts +1 -0
  63. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  64. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  65. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  66. package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
  67. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  68. package/tailwind.base.css +5 -0
  69. package/translations/de.json +15 -2
  70. package/translations/en.json +14 -1
  71. package/translations/es.json +13 -0
  72. package/translations/fr.json +15 -2
  73. package/translations/it.json +13 -0
  74. package/translations/nl.json +13 -0
  75. package/translations/pt.json +13 -0
  76. package/translations/sk.json +13 -0
  77. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
@@ -1,14 +1,58 @@
1
- import { EndpointError, WfsEndpoint, WfsVersion } from '@camptocamp/ogc-client'
2
- import { DataItem, DatasetInfo, FetchError, PropertyInfo } from '../model'
3
1
  import {
4
- fetchDataAsText,
5
- getJsonDataItemsProxy,
6
- jsonToGeojsonFeature,
7
- } from '../utils'
8
- import { GmlReader, parseGml } from './gml'
9
- import { GeojsonReader, parseGeojson } from './geojson'
10
- import { BaseCacheReader } from './base-cache'
11
- import { generateSqlQuery } from '../sql-utils'
2
+ EndpointError,
3
+ WfsEndpoint,
4
+ WfsFeatureTypeFull,
5
+ } from '@camptocamp/ogc-client'
6
+ import { DataItem, DatasetInfo, FetchError, PropertyInfo } from '../model'
7
+ import { fetchDataAsText } from '../utils'
8
+ import WFS from 'ol/format/WFS'
9
+ import GeoJSON from 'ol/format/GeoJSON'
10
+ import Feature from 'ol/Feature'
11
+ import { GeojsonReader } from './geojson'
12
+ import { GmlReader } from './gml'
13
+ import { BaseReader } from './base'
14
+
15
+ const formatGeojson = new GeoJSON()
16
+
17
+ export function parseGeojson(text: string): DataItem[] {
18
+ const parsed = JSON.parse(text)
19
+ const features =
20
+ parsed.type === 'FeatureCollection' ? parsed.features : parsed
21
+ if (!Array.isArray(features)) {
22
+ throw new Error(
23
+ 'Could not parse GeoJSON, expected a features collection or an array of features at root level'
24
+ )
25
+ }
26
+ return features
27
+ }
28
+
29
+ export function parseGml(
30
+ text: string,
31
+ featureType: string,
32
+ version: string
33
+ ): DataItem[] {
34
+ const parts = featureType.split(':')
35
+ const regex = new RegExp(`xmlns:${parts[0]}=["']([^'"]*)["']`)
36
+ const match = regex.exec(text)
37
+ if (match && match.length >= 2) {
38
+ const wfs = new WFS({
39
+ featureNS: match[1],
40
+ featureType: parts[1],
41
+ version: version,
42
+ })
43
+ let features: Feature[]
44
+ try {
45
+ features = wfs.readFeatures(text)
46
+ } catch (e: unknown) {
47
+ throw Error(
48
+ `Couldn't parse WFS with GML features: ${(e as Error).message}`
49
+ )
50
+ }
51
+ const geojsonItem = formatGeojson.writeFeaturesObject(features)
52
+ return geojsonItem.features
53
+ }
54
+ throw Error("Couldn't retrieve namespace url")
55
+ }
12
56
 
13
57
  export async function getWfsEndpoint(wfsUrl: string): Promise<WfsEndpoint> {
14
58
  try {
@@ -36,151 +80,163 @@ export async function getWfsEndpoint(wfsUrl: string): Promise<WfsEndpoint> {
36
80
  }
37
81
  }
38
82
 
39
- export class WfsReader extends BaseCacheReader {
40
- endpoint: WfsEndpoint
41
- featureTypeName: string
42
- version: WfsVersion
43
-
44
- constructor(
45
- url: string,
46
- wfsEndpoint: WfsEndpoint,
47
- featureTypeName: string,
48
- cacheActive?: boolean
49
- ) {
50
- super(url, cacheActive)
51
- this.endpoint = wfsEndpoint
52
- this.featureTypeName = featureTypeName
53
- this.version = this.endpoint.getVersion()
83
+ export class WfsReader extends BaseReader {
84
+ endpoint: Promise<WfsEndpoint>
85
+ featureType: Promise<WfsFeatureTypeFull>
86
+
87
+ /**
88
+ * This dataset reader will read the whole data as geojson or gml instead of
89
+ * using the WFS protocol; it is used in the following situations:
90
+ * - the endpoint does not support pagination
91
+ * - aggregations or grouping are set on the dataset (i.e. for chart view)
92
+ */
93
+ backupReader_: Promise<BaseReader>
94
+
95
+ constructor(url: string, featureTypeName: string) {
96
+ super(url)
97
+ this.endpoint = getWfsEndpoint(url)
98
+
99
+ this.featureType = this.endpoint
100
+ .then((endpoint) => {
101
+ const featureTypes = endpoint.getFeatureTypes()
102
+ return endpoint.getFeatureTypeFull(
103
+ featureTypes.length === 1 && !featureTypeName
104
+ ? featureTypes[0].name
105
+ : featureTypeName
106
+ )
107
+ })
108
+ .then((featureType) => {
109
+ if (!featureType) {
110
+ throw new Error('wfs.featuretype.notfound')
111
+ }
112
+ return featureType
113
+ })
54
114
  }
55
115
 
56
- get properties(): Promise<PropertyInfo[]> {
57
- return this.endpoint
58
- .getFeatureTypeFull(this.featureTypeName)
59
- .then((featureType) =>
60
- Object.keys(featureType.properties).map((prop) => {
61
- const originalType = featureType.properties[prop]
62
- const type =
63
- originalType === 'float' || originalType === 'integer'
64
- ? 'number'
65
- : (originalType as PropertyInfo['type']) // FIXME: ogc-client typing is incorrect, should be a string union
66
- return {
67
- name: prop,
68
- label: prop,
69
- type,
116
+ get backupReader(): Promise<BaseReader> {
117
+ if (this.backupReader_) {
118
+ return this.backupReader_
119
+ }
120
+
121
+ this.backupReader_ = Promise.all([this.endpoint, this.featureType]).then(
122
+ ([endpoint, featureType]) => {
123
+ let reader: BaseReader
124
+ if (endpoint.supportsJson(featureType.name)) {
125
+ reader = new GeojsonReader(
126
+ endpoint.getFeatureUrl(featureType.name, {
127
+ asJson: true,
128
+ outputCrs: 'EPSG:4326',
129
+ })
130
+ )
131
+ } else {
132
+ if (
133
+ featureType.outputFormats.find((f) =>
134
+ f.toLowerCase().includes('gml')
135
+ ) &&
136
+ (featureType.defaultCrs === 'EPSG:4326' ||
137
+ featureType.otherCrs?.includes('EPSG:4326'))
138
+ ) {
139
+ reader = new GmlReader(
140
+ endpoint.getFeatureUrl(featureType.name, {
141
+ outputFormat: featureType.outputFormats.find((f) =>
142
+ f.toLowerCase().includes('gml')
143
+ ),
144
+ outputCrs: 'EPSG:4326',
145
+ })
146
+ )
70
147
  }
71
- })
72
- )
148
+ }
149
+ reader.enableCache(this.cacheEnabled)
150
+ reader.load()
151
+ return reader
152
+ }
153
+ )
154
+ return this.backupReader_
155
+ }
156
+
157
+ get properties(): Promise<PropertyInfo[]> {
158
+ return this.featureType.then((featureType) =>
159
+ Object.keys(featureType.properties).map((prop) => {
160
+ const originalType = featureType.properties[prop]
161
+ let type: PropertyInfo['type']
162
+ if (originalType === 'float' || originalType === 'integer') {
163
+ type = 'number'
164
+ } else if (originalType === 'boolean') {
165
+ type = 'string' // we don't handle booleans yet in the data fetcher
166
+ } else {
167
+ type = originalType
168
+ }
169
+ return {
170
+ name: prop,
171
+ label: prop,
172
+ type,
173
+ }
174
+ })
175
+ )
73
176
  }
74
177
 
75
178
  get info(): Promise<DatasetInfo> {
76
- return this.endpoint.getFeatureTypeFull(this.featureTypeName).then(
179
+ return this.featureType.then(
77
180
  (result) =>
78
181
  ({
79
182
  itemsCount: result.objectCount,
183
+ hasGeometry: !!result.geometryName,
80
184
  }) as DatasetInfo
81
185
  )
82
186
  }
83
187
 
84
- static async createReader(wfsUrlEndpoint: string, featureTypeName?: string) {
85
- const wfsEndpoint = await getWfsEndpoint(wfsUrlEndpoint)
86
- const featureTypes = wfsEndpoint.getFeatureTypes()
87
- const featureType = wfsEndpoint.getFeatureTypeSummary(
88
- featureTypes.length === 1 && !featureTypeName
89
- ? featureTypes[0].name
90
- : featureTypeName
91
- )
92
- if (!featureType) {
93
- throw new Error('wfs.featuretype.notfound')
94
- }
188
+ load() {
189
+ // Nothing to load for Wfs
190
+ }
95
191
 
96
- if (wfsEndpoint.supportsStartIndex()) {
97
- return new WfsReader(wfsUrlEndpoint, wfsEndpoint, featureType.name)
98
- } else if (wfsEndpoint.supportsJson(featureType.name)) {
99
- return new GeojsonReader(
100
- wfsEndpoint.getFeatureUrl(featureType.name, {
101
- asJson: true,
102
- outputCrs: 'EPSG:4326',
103
- })
104
- )
105
- } else {
106
- if (
107
- featureType.outputFormats.find((f) =>
108
- f.toLowerCase().includes('gml')
109
- ) &&
110
- (featureType.defaultCrs === 'EPSG:4326' ||
111
- featureType.otherCrs?.includes('EPSG:4326'))
112
- ) {
113
- return new GmlReader(
114
- wfsEndpoint.getFeatureUrl(featureType.name, {
115
- outputFormat: featureType.outputFormats.find((f) =>
116
- f.toLowerCase().includes('gml')
117
- ),
118
- outputCrs: 'EPSG:4326',
119
- }),
120
- featureType.name,
121
- wfsEndpoint.getVersion()
122
- )
192
+ async read(): Promise<DataItem[]> {
193
+ const endpoint = await this.endpoint
194
+ const featureType = await this.featureType
195
+
196
+ // if we can't use the WFS protocol we fall back to the backup reader
197
+ if (this.aggregations || this.groupedBy || !endpoint.supportsStartIndex()) {
198
+ const backupReader = await this.backupReader
199
+ backupReader.selectAll()
200
+ if (this.selected) {
201
+ backupReader.select(...this.selected)
202
+ }
203
+ if (this.aggregations) {
204
+ backupReader.aggregate(...this.aggregations)
205
+ }
206
+ if (this.groupedBy) {
207
+ backupReader.groupBy(...this.groupedBy)
208
+ }
209
+ if (this.sort) {
210
+ backupReader.orderBy(...this.sort)
123
211
  }
124
- throw new Error('wfs.geojsongml.notsupported')
212
+ if (this.startIndex !== null && this.count !== null) {
213
+ backupReader.limit(this.startIndex, this.count)
214
+ }
215
+ return backupReader.read()
125
216
  }
126
- }
127
217
 
128
- public async getData(aggregation?, groupedBy?) {
129
- if (aggregation || groupedBy) {
130
- return { items: await this.getQueryData() }
131
- }
132
- const asJson = this.endpoint.supportsJson(this.featureTypeName)
218
+ const asJson = endpoint.supportsJson(featureType.name)
133
219
  const attributes = this.selected ?? undefined
134
- let url = this.endpoint.getFeatureUrl(this.featureTypeName, {
220
+ let sortBy = null
221
+ if (this.sort) {
222
+ const mapSort = (s) => [s[0] === 'desc' ? 'D' : 'A', s[1]]
223
+ sortBy = Array.isArray(this.sort[0])
224
+ ? this.sort.map(mapSort)
225
+ : mapSort(this.sort)
226
+ }
227
+ const url = endpoint.getFeatureUrl(featureType.name, {
135
228
  ...(this.startIndex !== null && { startIndex: this.startIndex }),
136
229
  ...(this.count !== null && { maxFeatures: this.count }),
137
230
  asJson,
138
231
  outputCrs: 'EPSG:4326',
139
232
  attributes,
140
- // sortBy: this.sort // TODO: no sort in ogc-client?
233
+ sortBy,
141
234
  })
142
235
 
143
- if (Array.isArray(this.sort) && this.sort.length > 0) {
144
- const finalUrl = new URL(url)
145
- const sorts = this.sort
146
- .map(
147
- (fieldSort) => `${fieldSort[1]}+${fieldSort[0] === 'asc' ? 'A' : 'D'}`
148
- )
149
- .join(',')
150
- // Direct update on string url to prevent encoding of +A and +D
151
- url = `${url}${finalUrl.search ? '&' : ''}SORTBY=${sorts}`
152
- }
153
-
154
- return fetchDataAsText(url, this.cacheActive).then((text) =>
236
+ return fetchDataAsText(url, this.cacheEnabled).then((text) =>
155
237
  asJson
156
238
  ? parseGeojson(text)
157
- : parseGml(text, this.featureTypeName, this.version)
158
- )
159
- }
160
-
161
- public async getQueryData() {
162
- const items = (await this.getData()).items
163
- const jsonItems = getJsonDataItemsProxy(items)
164
- const query = generateSqlQuery(
165
- this.selected,
166
- this.filter,
167
- this.sort,
168
- this.startIndex,
169
- this.count,
170
- this.groupedBy,
171
- this.aggregations
172
- )
173
- const result: any[] = await import('alasql').then((module) =>
174
- module.default(query, [jsonItems])
239
+ : parseGml(text, featureType.name, endpoint.getVersion())
175
240
  )
176
- return result.map(jsonToGeojsonFeature)
177
- }
178
-
179
- load() {
180
- // Nothing to load for Wfs
181
- }
182
-
183
- async read(): Promise<DataItem[]> {
184
- return (await this.getData(this.aggregations, this.groupedBy)).items
185
241
  }
186
242
  }
@@ -2,14 +2,11 @@ import {
2
2
  DataItem,
3
3
  DatasetHeaders,
4
4
  FetchError,
5
- PropertyInfo,
6
5
  SupportedType,
7
6
  SupportedTypes,
8
7
  } from './model'
9
8
  import { sharedFetch, useCache } from '@camptocamp/ogc-client'
10
9
  import { parseHeaders } from './headers'
11
- import { parse as parseDate } from 'date-fns/parse'
12
- import { parseISO as parseIsoDate } from 'date-fns/parseISO'
13
10
 
14
11
  export async function inferDatasetType(
15
12
  url: string,
@@ -95,146 +92,6 @@ export function fetchDataAsArrayBuffer(
95
92
  })
96
93
  }
97
94
 
98
- export function tryParseDate(input: unknown): Date | null {
99
- if (typeof input !== 'string') return null
100
-
101
- function tryIso(value: string) {
102
- const parsed = parseIsoDate(value)
103
- return isNaN(parsed.getDate()) ? null : parsed
104
- }
105
- function tryFormat(value: string, format: string) {
106
- const parsed = parseDate(value, format, new Date())
107
- return isNaN(parsed.getDate()) ? null : parsed
108
- }
109
- return (
110
- tryIso(input) ||
111
- tryFormat(input, 'dd/MM/yyyy') ||
112
- tryFormat(input, 'dd.MM.yyyy') ||
113
- tryFormat(input, 'MM/dd/yyyy') ||
114
- null
115
- )
116
- }
117
-
118
- export function tryParseNumber(input: string): number | null {
119
- if (isNaN(input as any)) return null
120
- const parsed = parseFloat(input)
121
- return isNaN(parsed) ? null : parsed
122
- }
123
-
124
- export function jsonToGeojsonFeature(object: { [key: string]: any }): DataItem {
125
- const { id, properties } = Object.keys(object)
126
- .map((property) => (property ? property : 'unknown')) //prevent empty strings
127
- .reduce(
128
- (prev, curr) =>
129
- curr.toLowerCase().endsWith('id')
130
- ? {
131
- ...prev,
132
- id: object[curr],
133
- }
134
- : {
135
- ...prev,
136
- properties: { ...prev.properties, [curr]: object[curr] },
137
- },
138
- { id: undefined, properties: {} }
139
- )
140
- return {
141
- type: 'Feature',
142
- geometry: null,
143
- properties,
144
- ...(id !== undefined && { id }),
145
- }
146
- }
147
-
148
- function mutateProperties(
149
- items: DataItem[],
150
- mutators: { [fieldName: string]: (value: unknown) => unknown }
151
- ): DataItem[] {
152
- const mutatorKeys = Object.keys(mutators)
153
- for (let i = 0, ii = items.length; i < ii; i++) {
154
- const item = items[i]
155
- for (const mutatorField of mutatorKeys) {
156
- if (!(mutatorField in item.properties)) continue
157
- item.properties[mutatorField] = mutators[mutatorField](
158
- item.properties[mutatorField]
159
- )
160
- }
161
- }
162
- return items
163
- }
164
-
165
- const SAMPLE_SIZE = 20
166
-
167
- /**
168
- * This will infer field types from a list of data items and cast the values accordingly
169
- * @param items
170
- * @param inferTypes
171
- */
172
- export function processItemProperties(
173
- items: DataItem[],
174
- inferTypes = false
175
- ): {
176
- items: DataItem[]
177
- properties: PropertyInfo[]
178
- } {
179
- const foundFields: { [name: string]: PropertyInfo } = {}
180
- for (let i = 0, ii = Math.min(SAMPLE_SIZE, items.length); i < ii; i++) {
181
- const item = items[i]
182
- const fields = Object.keys(item.properties)
183
- for (const field of fields) {
184
- if (!(field in foundFields)) {
185
- foundFields[field] = {
186
- label: field,
187
- name: field,
188
- type: null,
189
- }
190
- }
191
- const value = item.properties[field]
192
- const info = foundFields[field]
193
- if (value === undefined || value === '' || value === null) continue
194
-
195
- if (!inferTypes) {
196
- if (info.type === null && typeof value === 'number') {
197
- info.type = 'number'
198
- } else if (info.type === 'number' && typeof value !== 'number') {
199
- info.type = 'string'
200
- }
201
- continue
202
- }
203
-
204
- const parsedNumber = tryParseNumber(value)
205
- if (info.type === null && parsedNumber !== null) {
206
- info.type = 'number'
207
- continue
208
- } else if (info.type === 'number' && parsedNumber === null) {
209
- info.type = 'string'
210
- continue
211
- }
212
- const parsedDate = tryParseDate(value)
213
- if (info.type === null && parsedDate !== null) {
214
- info.type = 'date'
215
- } else if (info.type === 'date' && parsedDate === null) {
216
- info.type = 'string'
217
- }
218
- }
219
- }
220
-
221
- const properties: PropertyInfo[] = []
222
- const mutators = {}
223
- for (const field in foundFields) {
224
- const info = foundFields[field]
225
- if (info.type === 'number') {
226
- mutators[field] = tryParseNumber
227
- } else if (info.type === 'date') {
228
- mutators[field] = tryParseDate
229
- }
230
- properties.push({ ...info, type: info.type || 'string' })
231
- }
232
- if (inferTypes) {
233
- mutateProperties(items, mutators)
234
- }
235
- return { items, properties }
236
- }
237
-
238
95
  /**
239
96
  * This creates a Proxy that allows reading and writing to the data item properties
240
97
  * as if it was a simple array of JSON objects
@@ -1,6 +1,7 @@
1
1
  export * from './lib/services'
2
2
  export * from './lib/utils'
3
3
  export * from './lib/links'
4
+ export * from './lib/autofocus.directive'
4
5
  export * from './lib/image-fallback.directive'
5
6
  export * from './lib/gn-ui-version'
6
7
  export * from './lib/record'
@@ -0,0 +1,26 @@
1
+ import {
2
+ Directive,
3
+ ElementRef,
4
+ Injector,
5
+ Input,
6
+ afterNextRender,
7
+ booleanAttribute,
8
+ inject,
9
+ } from '@angular/core'
10
+
11
+ @Directive({
12
+ selector: '[gnUiAutofocus]',
13
+ standalone: true,
14
+ })
15
+ export class AutofocusDirective {
16
+ private el = inject<ElementRef<HTMLElement>>(ElementRef)
17
+ private injector = inject(Injector)
18
+
19
+ @Input({ alias: 'gnUiAutofocus', transform: booleanAttribute })
20
+ set autofocus(active: boolean) {
21
+ if (!active) return
22
+ afterNextRender(() => this.el.nativeElement.focus(), {
23
+ injector: this.injector,
24
+ })
25
+ }
26
+ }
@@ -35,10 +35,10 @@ export class DateService {
35
35
  return { locale, dateObj }
36
36
  }
37
37
 
38
- private async getDateLocale(): Promise<Locale> {
38
+ async getDateFnsLocale(): Promise<Locale> {
39
39
  const lang = this.translateService.getCurrentLang() || DEFAULT_LANGUAGE
40
40
  const locales = await this.dateLocales
41
- return locales[lang]
41
+ return locales[lang] ?? locales[DEFAULT_LANGUAGE]
42
42
  }
43
43
 
44
44
  formatDate(
@@ -61,7 +61,7 @@ export class DateService {
61
61
  const dateObj = this.getDateObject(date)
62
62
 
63
63
  const now = new Date()
64
- const locale = await this.getDateLocale()
64
+ const locale = await this.getDateFnsLocale()
65
65
 
66
66
  return formatDistance(dateObj, now, {
67
67
  addSuffix: true,
@@ -0,0 +1,15 @@
1
+ export function isFileExtensionValid(
2
+ fileName: string,
3
+ acceptedExtensions: string[]
4
+ ): boolean {
5
+ return acceptedExtensions.some((ext) => fileName.toLowerCase().endsWith(ext))
6
+ }
7
+
8
+ export function readFileAsText(file: File): Promise<string> {
9
+ return new Promise((resolve, reject) => {
10
+ const reader = new FileReader()
11
+ reader.onload = () => resolve(reader.result as string)
12
+ reader.onerror = () => reject(reader.error)
13
+ reader.readAsText(file)
14
+ })
15
+ }
@@ -38,6 +38,14 @@ export function getGeometryFromGeoJSON(
38
38
  // FIXME: this type should be more generic across the project
39
39
  export type BoundingBox = [number, number, number, number]
40
40
 
41
+ export function isBoundingBox(value: unknown): value is BoundingBox {
42
+ return (
43
+ Array.isArray(value) &&
44
+ value.length === 4 &&
45
+ value.every((item) => typeof item === 'number')
46
+ )
47
+ }
48
+
41
49
  export function getGeometryBoundingBox(geometry: Geometry): BoundingBox {
42
50
  // use the bounding box if specified in the GeoJSON object
43
51
  if (geometry.bbox) {
@@ -1,5 +1,6 @@
1
1
  export * from './bytes-convert'
2
2
  export * from './event'
3
+ export * from './file'
3
4
  export * from './format-fields'
4
5
  export * from './fuzzy-filter'
5
6
  export * from './geojson'
package/tailwind.base.css CHANGED
@@ -224,6 +224,11 @@
224
224
  disabled:cursor-not-allowed disabled:border-gray-100 disabled:placeholder-gray-300 disabled:text-gray-700 disabled:bg-white;
225
225
  }
226
226
 
227
+ .gn-ui-gray-outline-input {
228
+ @apply bg-white rounded-lg
229
+ border border-gray-400 hover:border-neutral-300 hover:bg-black/10;
230
+ }
231
+
227
232
  /* GENERIC CARD */
228
233
  .gn-ui-card {
229
234
  @apply flex flex-row border border-gray-200 rounded-xl p-4 gap-2 w-full;
@@ -65,9 +65,10 @@
65
65
  "dataset.error.restrictedAccess": "",
66
66
  "dataset.error.unknown": "Die Daten können nicht angezeigt werden: \"{ info }\"",
67
67
  "dataset.error.unsupportedType": "Der folgende Inhaltstyp wird nicht unterstützt: \"{ info }\"",
68
- "daterange.filter.from": "Von",
68
+ "daterange.filter.datePlaceholder": "tt.mm.jjjj",
69
+ "daterange.filter.from": "Ab dem",
69
70
  "daterange.filter.period": "Zeitraum auswählen",
70
- "daterange.filter.to": "Bis",
71
+ "daterange.filter.to": "Bis zum",
71
72
  "domain.contact.role.author": "Autor",
72
73
  "domain.contact.role.collaborator": "Mitarbeiter",
73
74
  "domain.contact.role.contributor": "Beitragender",
@@ -550,6 +551,7 @@
550
551
  "record.metadata.languages": "Sprachen",
551
552
  "record.metadata.link.postgis.table": "Tabelle:",
552
553
  "record.metadata.link.postgis.tooltip": "PostGIS-Ressourcen haben keinen Zugriffslink.",
554
+ "record.metadata.linked.associatedRecords": "",
553
555
  "record.metadata.linked.datasets": "Verknüpfte Datensätze",
554
556
  "record.metadata.linked.records": "Verknüpfte Inhalte",
555
557
  "record.metadata.linked.reuses": "Verknüpfte Wiederverwendungen",
@@ -689,6 +691,17 @@
689
691
  "search.filters.recordKind.service": "Dienste",
690
692
  "search.filters.representationType": "Repräsentationstyp",
691
693
  "search.filters.resourceType": "Ressourcentyp",
694
+ "search.filters.spatialExtent": "",
695
+ "search.filters.spatialExtent.bboxFromFile": "",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "",
697
+ "search.filters.spatialExtent.bboxInitial": "",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "",
699
+ "search.filters.spatialExtent.error.fileTooLarge": "",
700
+ "search.filters.spatialExtent.error.invalidFormat": "",
701
+ "search.filters.spatialExtent.error.noGeometry": "",
702
+ "search.filters.spatialExtent.error.title": "",
703
+ "search.filters.spatialExtent.helpText": "",
704
+ "search.filters.spatialExtent.import": "",
692
705
  "search.filters.standard": "Standard",
693
706
  "search.filters.summaryLabel.changeDate": "Geändert am: ",
694
707
  "search.filters.summaryLabel.user": "Geändert von: ",