@datagouv/components-next 1.0.2-dev.231 → 1.0.2-dev.233

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 (34) hide show
  1. package/dist/{ImagePreview.client-H7jGf9xS.js → ImagePreview.client-CSRz7eUF.js} +2 -2
  2. package/dist/{JsonPreview.client-CQSCGT6V.js → JsonPreview.client-wT2hfYLv.js} +2 -2
  3. package/dist/{PdfPreview.client-Cz_jnvla.js → PdfPreview.client-DifxM8Th.js} +2 -2
  4. package/dist/{Pmtiles.client-CSoax_YA.js → Pmtiles.client-OyXiX_7g.js} +4 -4
  5. package/dist/{PreviewWrapper-DBlpLgs2.js → PreviewWrapper-D7yfQyyH.js} +1 -1
  6. package/dist/{XmlPreview.client-8vye8TAa.js → XmlPreview.client-BkPGZLn6.js} +2 -2
  7. package/dist/components-next.js +10226 -9938
  8. package/dist/components.css +1 -1
  9. package/dist/{france-Bu5x7PSp.js → france-BNatwStf.js} +1 -1
  10. package/dist/{resources-BBaRfEol.js → resources-kuI_FosB.js} +5 -5
  11. package/dist/{vue-D62CcCib.js → vue-D5eMxsCr.js} +78 -74
  12. package/package.json +1 -1
  13. package/src/components/ResourceExplorer/ResourceExplorer.vue +16 -5
  14. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +140 -90
  15. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +20 -2
  16. package/src/components/ResourceExplorer/ResourceSelector.vue +3 -0
  17. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +3 -5
  18. package/src/components/ResourceIconBadge.vue +28 -0
  19. package/src/components/ResourceListItem.vue +83 -27
  20. package/src/components/TabularExplorer/TabularActiveFilters.vue +94 -54
  21. package/src/components/TabularExplorer/TabularExplorer.vue +102 -25
  22. package/src/components/TabularExplorer/TabularFilterContent.vue +2 -1
  23. package/src/components/TabularExplorer/TabularMobileFilters.vue +4 -3
  24. package/src/components/TabularExplorer/TabularSearchInput.vue +40 -0
  25. package/src/components/TabularExplorer/TabularSkeleton.vue +14 -6
  26. package/src/components/TabularExplorer/TabularTable.vue +12 -5
  27. package/src/components/TabularExplorer/TabularToolbar.vue +7 -4
  28. package/src/components/TabularExplorer/types.ts +9 -1
  29. package/src/components/TabularExplorer/useTabularContext.ts +12 -1
  30. package/src/components/TabularExplorer/useTabularUrlState.ts +69 -0
  31. package/src/composables/useResourceCapabilities.ts +5 -0
  32. package/src/config.ts +7 -0
  33. package/src/functions/tabular.ts +134 -1
  34. package/src/main.ts +4 -1
@@ -0,0 +1,69 @@
1
+ import { computed, toValue } from 'vue'
2
+ import type { MaybeRefOrGetter, Ref } from 'vue'
3
+ import { useRouteQuery } from '@vueuse/router'
4
+ import { useRoute } from 'vue-router'
5
+ import type { LocationQueryValue } from 'vue-router'
6
+ import { useComponentsConfig } from '../../config'
7
+ import {
8
+ TABULAR_FILTERS_PARAM,
9
+ TABULAR_SEARCH_PARAM,
10
+ TABULAR_SORT_PARAM,
11
+ filtersFromUrlQuery,
12
+ filtersToUrlQuery,
13
+ searchFromUrlParam,
14
+ sortFromUrlParam,
15
+ sortToUrlParam,
16
+ } from '../../functions/tabular'
17
+ import type { ColumnFilters, SortConfig, TabularUrlAlias } from './types'
18
+
19
+ type QueryRef = Ref<LocationQueryValue | LocationQueryValue[] | undefined>
20
+
21
+ /**
22
+ * Sort, filters and global search of the explorer, stored in the URL: going back
23
+ * to the page finds them again, and a link to it shares them. Only the `columns`
24
+ * of the resource are read from it.
25
+ *
26
+ * Every param goes through `useRouteQuery`, which batches the writes of a tick
27
+ * into a single `router.replace`: a change never stacks history entries, and
28
+ * moving a filter from an alias to the JSON param is one navigation, not two.
29
+ */
30
+ export function useTabularUrlState(
31
+ aliases: Record<string, TabularUrlAlias>,
32
+ defaultSort: SortConfig | null,
33
+ columns: MaybeRefOrGetter<readonly string[]>,
34
+ ) {
35
+ const config = useComponentsConfig()
36
+ const route = (config.useRoute ?? useRoute)()
37
+ const filterRefs = [...Object.keys(aliases), TABULAR_FILTERS_PARAM]
38
+ .map(param => [param, useRouteQuery(param, undefined, { route }) as QueryRef] as const)
39
+ const sortRef = useRouteQuery(TABULAR_SORT_PARAM, undefined, { route }) as QueryRef
40
+ const searchRef = useRouteQuery(TABULAR_SEARCH_PARAM, undefined, { route }) as QueryRef
41
+
42
+ const filters = computed<Record<string, ColumnFilters>>({
43
+ get: () => filtersFromUrlQuery(
44
+ Object.fromEntries(filterRefs.map(([param, ref]) => [param, ref.value])),
45
+ aliases,
46
+ toValue(columns),
47
+ ),
48
+ set: (value) => {
49
+ const query = filtersToUrlQuery(value, aliases)
50
+ for (const [param, ref] of filterRefs) ref.value = query[param]
51
+ },
52
+ })
53
+
54
+ const sort = computed<SortConfig | null>({
55
+ get: () => sortFromUrlParam(sortRef.value, defaultSort, toValue(columns)),
56
+ set: (value) => {
57
+ sortRef.value = sortToUrlParam(value, defaultSort)
58
+ },
59
+ })
60
+
61
+ const globalSearch = computed<string>({
62
+ get: () => searchFromUrlParam(searchRef.value),
63
+ set: (value) => {
64
+ searchRef.value = value || undefined
65
+ },
66
+ })
67
+
68
+ return { filters, sort, globalSearch }
69
+ }
@@ -82,6 +82,10 @@ export function useResourceCapabilities(
82
82
  return 'unavailable'
83
83
  })
84
84
 
85
+ // Whether opening the resource shows its content (a preview or a map), not just
86
+ // its metadata.
87
+ const hasPreview = computed(() => previewKind.value !== 'unavailable' || !!hasPmtiles.value || ogcWms.value)
88
+
85
89
  const tabsOptions = computed(() => {
86
90
  const r = toValue(resource)
87
91
  const options = []
@@ -122,6 +126,7 @@ export function useResourceCapabilities(
122
126
 
123
127
  return {
124
128
  previewKind,
129
+ hasPreview,
125
130
  hasTabularData,
126
131
  hasPmtiles,
127
132
  hasPmtilesError,
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { inject, type Component, type InjectionKey } from 'vue'
2
2
  import type { UseFetchFunction } from './functions/api.types'
3
3
  import type { $Fetch, FetchOptions } from 'ofetch'
4
+ import type { RouteLocationNormalizedLoaded } from 'vue-router'
4
5
 
5
6
  export type PluginConfig = {
6
7
  name: string // Name of the application (ex: data.gouv.fr)
@@ -43,6 +44,12 @@ export type PluginConfig = {
43
44
  textClamp?: string | Component | null
44
45
  appLink?: Component | null
45
46
  clientOnly?: Component | null
47
+ /**
48
+ * The route of the page a component is rendered in. vue-router's `useRoute` is the
49
+ * global one: while the next page loads, the page being left is still mounted and
50
+ * already sees the next URL. Nuxt's `useRoute` keeps each page on its own route.
51
+ */
52
+ useRoute?: () => RouteLocationNormalizedLoaded
46
53
  searchDebounce?: number
47
54
  forumUrl?: string
48
55
  }
@@ -23,7 +23,9 @@ import {
23
23
  } from '@remixicon/vue'
24
24
  import { useTranslation } from '../composables/useTranslation'
25
25
  import type { TranslationFunction } from '../composables/useTranslation'
26
- import type { ColumnFilters, ColumnType, DateFilter } from '../components/TabularExplorer/types'
26
+ import type { LocationQueryValue } from 'vue-router'
27
+ import { DATE_FILTER_OPERATORS } from '../components/TabularExplorer/types'
28
+ import type { ColumnFilters, ColumnType, DateFilter, DateFilterOperator, SortConfig, TabularUrlAlias } from '../components/TabularExplorer/types'
27
29
  import { parseDateValue } from './dates'
28
30
 
29
31
  export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
@@ -126,6 +128,137 @@ export function buildCellValueFilter(columnType: ColumnType, value: unknown, exi
126
128
  }
127
129
  }
128
130
 
131
+ export const TABULAR_FILTERS_PARAM = 'filters'
132
+ export const TABULAR_SORT_PARAM = 'sort'
133
+ export const TABULAR_SEARCH_PARAM = 'q'
134
+
135
+ function isRecord(value: unknown): value is Record<string, unknown> {
136
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
137
+ }
138
+
139
+ function firstQueryValue(value: LocationQueryValue | LocationQueryValue[] | undefined): string | undefined {
140
+ const first = Array.isArray(value) ? value[0] : value
141
+ return first ?? undefined
142
+ }
143
+
144
+ /**
145
+ * The criteria of a column filter that actually filter something, read from any
146
+ * value: the ones from the URL can be anything a user typed. Empty criteria are
147
+ * dropped too, so that a column left with none of them has no key left.
148
+ */
149
+ export function normalizeColumnFilters(value: unknown): ColumnFilters {
150
+ if (!isRecord(value)) return {}
151
+ const filter: ColumnFilters = {}
152
+ if (Array.isArray(value.in) && value.in.length && value.in.every(v => typeof v === 'string')) filter.in = value.in
153
+ if (typeof value.exact === 'string' && value.exact) filter.exact = value.exact
154
+ if (typeof value.min === 'number' && Number.isFinite(value.min)) filter.min = value.min
155
+ if (typeof value.max === 'number' && Number.isFinite(value.max)) filter.max = value.max
156
+ if (typeof value.contains === 'string' && value.contains) filter.contains = value.contains
157
+ if (value.null === 'only' || value.null === 'exclude') filter.null = value.null
158
+ const date = value.date
159
+ if (isRecord(date) && DATE_FILTER_OPERATORS.includes(date.operator as DateFilterOperator) && typeof date.start === 'string') {
160
+ filter.date = { operator: date.operator as DateFilterOperator, start: date.start }
161
+ if (typeof date.end === 'string') filter.date.end = date.end
162
+ }
163
+ return filter
164
+ }
165
+
166
+ /**
167
+ * The URL params holding `filters`. A column whose only criterion is the one of
168
+ * an alias is written under that alias; every other column goes, whole, into a
169
+ * single JSON param.
170
+ */
171
+ export function filtersToUrlQuery(
172
+ filters: Record<string, ColumnFilters>,
173
+ aliases: Record<string, TabularUrlAlias>,
174
+ ): Record<string, string> {
175
+ const query: Record<string, string> = {}
176
+ const rest: Record<string, ColumnFilters> = {}
177
+ for (const [column, raw] of Object.entries(filters)) {
178
+ const filter = normalizeColumnFilters(raw)
179
+ const criteria = Object.keys(filter)
180
+ if (!criteria.length) continue
181
+ const alias = criteria.length === 1
182
+ ? Object.entries(aliases).find(([, a]) => a.column === column && a.operator === criteria[0])
183
+ : undefined
184
+ const aliasValue = alias && filter[alias[1].operator]
185
+ if (alias && aliasValue) query[alias[0]] = aliasValue
186
+ else rest[column] = filter
187
+ }
188
+ if (Object.keys(rest).length) query[TABULAR_FILTERS_PARAM] = JSON.stringify(rest)
189
+ return query
190
+ }
191
+
192
+ /**
193
+ * The filters a URL holds, restricted to the `columns` of the resource: the
194
+ * Tabular API rejects a query naming any other column, and the explorer would be
195
+ * left without its controls to remove it. Such a column comes from a link to a
196
+ * resource since re-uploaded with other columns, or from a hand-edited URL.
197
+ */
198
+ export function filtersFromUrlQuery(
199
+ query: Record<string, LocationQueryValue | LocationQueryValue[] | undefined>,
200
+ aliases: Record<string, TabularUrlAlias>,
201
+ columns: readonly string[],
202
+ ): Record<string, ColumnFilters> {
203
+ const filters: Record<string, ColumnFilters> = {}
204
+ for (const [param, { column, operator }] of Object.entries(aliases)) {
205
+ const value = firstQueryValue(query[param])
206
+ if (value && columns.includes(column)) filters[column] = { [operator]: value }
207
+ }
208
+ const json = firstQueryValue(query[TABULAR_FILTERS_PARAM])
209
+ if (!json) return filters
210
+ let parsed: unknown
211
+ try {
212
+ parsed = JSON.parse(json)
213
+ }
214
+ catch {
215
+ return filters
216
+ }
217
+ if (!isRecord(parsed)) return filters
218
+ for (const [column, raw] of Object.entries(parsed)) {
219
+ const filter = normalizeColumnFilters(raw)
220
+ if (Object.keys(filter).length && columns.includes(column)) filters[column] = filter
221
+ }
222
+ return filters
223
+ }
224
+
225
+ export function searchFromUrlParam(param: LocationQueryValue | LocationQueryValue[] | undefined): string {
226
+ return firstQueryValue(param) ?? ''
227
+ }
228
+
229
+ export function isSameSort(a: SortConfig | null | undefined, b: SortConfig | null | undefined): boolean {
230
+ return a?.column === b?.column && a?.direction === b?.direction
231
+ }
232
+
233
+ /**
234
+ * The URL param holding `sort`, in the `-column` for descending convention of
235
+ * the udata API. The default sort stays out of the URL; dropping it is written as
236
+ * an empty value, since an absent param already means "the default sort".
237
+ */
238
+ export function sortToUrlParam(sort: SortConfig | null, defaultSort: SortConfig | null): string | undefined {
239
+ if (isSameSort(sort, defaultSort)) return undefined
240
+ if (!sort) return ''
241
+ return sort.direction === 'desc' ? `-${sort.column}` : sort.column
242
+ }
243
+
244
+ /**
245
+ * The sort a URL holds. One on a column the resource does not have falls back to
246
+ * the default sort, for the same reason as in `filtersFromUrlQuery`.
247
+ */
248
+ export function sortFromUrlParam(
249
+ param: LocationQueryValue | LocationQueryValue[] | undefined,
250
+ defaultSort: SortConfig | null,
251
+ columns: readonly string[],
252
+ ): SortConfig | null {
253
+ const value = firstQueryValue(param)
254
+ if (value === undefined) return defaultSort
255
+ if (value === '') return null
256
+ const sort: SortConfig = value.startsWith('-')
257
+ ? { column: value.slice(1), direction: 'desc' }
258
+ : { column: value, direction: 'asc' }
259
+ return columns.includes(sort.column) ? sort : defaultSort
260
+ }
261
+
129
262
  export type TypeDisplay = {
130
263
  icon: Component
131
264
  label: string
package/src/main.ts CHANGED
@@ -24,7 +24,7 @@ import type { Weight, WellType } from './types/ui'
24
24
  import type { User, UserReference } from './types/users'
25
25
  import type { Report, ReportSubject, ReportReason } from './types/reports'
26
26
  import type { Chart, ChartForm, ChartForApi, FilterCondition, Filter, AndFilters, OrFilters, GenericFilter, XAxisType, XAxisSortBy, SortDirection, XAxis, XAxisForm, UnitPosition, YAxis, DataSeriesType, DataSeries, DataSeriesForm, CombinedSort, ColumnDefinition, ColumnsDefinition } from './types/visualizations'
27
- import type { ColumnFilters, ColumnType, TabularRow } from './components/TabularExplorer/types'
27
+ import type { ColumnFilters, ColumnType, TabularRow, TabularUrlAlias } from './components/TabularExplorer/types'
28
28
  import type { GlobalSearchConfig, SearchType, SearchTypeConfig, SortOption, HiddenFilter, BuiltInFilterKey, DatasetSearchConfig, DatasetSearchFilters, DataserviceSearchConfig, DataserviceSearchFilters, ReuseSearchConfig, ReuseSearchFilters, OrganizationSearchConfig, OrganizationSearchFilters, TopicSearchConfig, TopicSearchFilters } from './types/search'
29
29
  import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseConfig, getDefaultOrganizationConfig, getDefaultTopicConfig, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
30
30
  import { useSearchFilter } from './composables/useSearchFilter'
@@ -109,6 +109,7 @@ import SearchableSelect from './components/Form/SearchableSelect.vue'
109
109
  import SelectGroup from './components/Form/SelectGroup.vue'
110
110
  import Listbox from './components/Form/Listbox.vue'
111
111
  import InfiniteLoader from './components/InfiniteLoader.vue'
112
+ import TabularActiveFilters from './components/TabularExplorer/TabularActiveFilters.vue'
112
113
  import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
113
114
  import TabularMobileFilters from './components/TabularExplorer/TabularMobileFilters.vue'
114
115
  import TabularTable from './components/TabularExplorer/TabularTable.vue'
@@ -269,6 +270,7 @@ export type {
269
270
  ColumnType,
270
271
  ColumnsDefinition,
271
272
  TabularRow,
273
+ TabularUrlAlias,
272
274
  FilterCondition,
273
275
  Filter,
274
276
  AndFilters,
@@ -422,6 +424,7 @@ export {
422
424
  SelectGroup,
423
425
  Listbox,
424
426
  InfiniteLoader,
427
+ TabularActiveFilters,
425
428
  TabularExplorer,
426
429
  TabularMobileFilters,
427
430
  TabularTable,