@datagouv/components-next 1.0.2-dev.222 → 1.0.2-dev.223

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 (25) hide show
  1. package/dist/{Datafair.client-B1OKZr6h.js → Datafair.client-Df0Q4FOf.js} +1 -1
  2. package/dist/{ImagePreview.client-D04HNejY.js → ImagePreview.client-C1nnlB26.js} +1 -1
  3. package/dist/{JsonPreview.client-B0_IvXry.js → JsonPreview.client-C50x2wrd.js} +2 -2
  4. package/dist/{MapContainer.client-zzejMZGK.js → MapContainer.client-B5GhGdk1.js} +2 -2
  5. package/dist/{PdfPreview.client-yeS69omp.js → PdfPreview.client-95sxmcl7.js} +2 -2
  6. package/dist/{Pmtiles.client-CxixL6Rw.js → Pmtiles.client-B7ItZ-PQ.js} +1 -1
  7. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-DunhHltO.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-D07VQ55G.js} +1 -1
  8. package/dist/{XmlPreview.client-Bi-Dtuq8.js → XmlPreview.client-BWlVL8M8.js} +3 -3
  9. package/dist/components-next.css +1 -1
  10. package/dist/components-next.js +127 -125
  11. package/dist/{index-B7ZsPIob.js → index-AG-SbiCt.js} +1 -1
  12. package/dist/{leaflet-src-ClDGecfN.js → leaflet-src-xwWnVeIm.js} +1 -1
  13. package/dist/{main-DAxkqaSe.js → main-BzR7HPLe.js} +14236 -14204
  14. package/dist/{vue3-xml-viewer.common-Dux7EzjM.js → vue3-xml-viewer.common-DCzadi-B.js} +1 -1
  15. package/package.json +1 -1
  16. package/src/components/ResourceAccordion/EditButton.vue +1 -4
  17. package/src/components/ResourceAccordion/ResourceAccordion.vue +1 -6
  18. package/src/components/TabularExplorer/TabularCellPopover.vue +4 -30
  19. package/src/functions/charts.ts +2 -2
  20. package/src/functions/resources.ts +0 -4
  21. package/src/functions/tabular.ts +42 -8
  22. package/src/functions/tabularApi.ts +52 -25
  23. package/src/main.ts +2 -1
  24. package/src/types/resources.ts +0 -6
  25. package/src/types/visualizations.ts +6 -1
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-DAxkqaSe.js";
1
+ import { g as Ke } from "./main-BzR7HPLe.js";
2
2
  import We from "vue";
3
3
  function Fe(I, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.222",
3
+ "version": "1.0.2-dev.223",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -4,8 +4,7 @@
4
4
  icon-only
5
5
  :icon="RiPencilLine"
6
6
  color="warning"
7
- :disabled="!!disabledReason"
8
- :title="disabledReason ?? t('Éditer le fichier')"
7
+ :title="t('Éditer le fichier')"
9
8
  data-testid="edit-button"
10
9
  />
11
10
  </template>
@@ -20,12 +19,10 @@ type Props = {
20
19
  datasetId: string
21
20
  isCommunityResource?: boolean
22
21
  resourceId: string
23
- disabledReason?: string | null
24
22
  }
25
23
 
26
24
  const props = withDefaults(defineProps<Props>(), {
27
25
  isCommunityResource: false,
28
- disabledReason: null,
29
26
  })
30
27
 
31
28
  const { t } = useTranslation()
@@ -173,7 +173,6 @@
173
173
  :dataset-id="dataset.id"
174
174
  :resource-id="resource.id"
175
175
  :is-community-resource="isCommunityResource"
176
- :disabled-reason="editDisabledReason"
177
176
  size="xs"
178
177
  />
179
178
  </p>
@@ -328,7 +327,7 @@ import { trackEvent } from '../../functions/matomo'
328
327
  import CopyButton from '../CopyButton.vue'
329
328
  import { useComponentsConfig } from '../../config'
330
329
  import { getOwnerName } from '../../functions/owned'
331
- import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceFilesize, isGeopfSynced, isImagePreviewFormat, resolveResourceExternalUrl } from '../../functions/resources'
330
+ import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceFilesize, isImagePreviewFormat, resolveResourceExternalUrl } from '../../functions/resources'
332
331
  import BrandedButton from '../BrandedButton.vue'
333
332
  import { useTranslation } from '../../composables/useTranslation'
334
333
  import { useHasTabularData } from '../../composables/useHasTabularData'
@@ -385,10 +384,6 @@ const hasPmtiles = computed(() => {
385
384
  return props.resource.extras['analysis:parsing:pmtiles_url'] || props.resource.format === 'pmtiles'
386
385
  })
387
386
 
388
- const editDisabledReason = computed(() => {
389
- return isGeopfSynced(props.resource) ? t('Vous ne pouvez pas modifier cette ressource car elle est synchronisée avec cartes.gouv.fr') : null
390
- })
391
-
392
387
  const hasDatafairPreview = computed(() => {
393
388
  // Checks if there are the corresponding extras for a datafair preview.
394
389
  // Limited only to datasets published by certified organizations since it will load an iframe.
@@ -80,7 +80,7 @@ import {
80
80
  } from '@remixicon/vue'
81
81
  import { toast } from 'vue-sonner'
82
82
  import { useTranslation } from '../../composables/useTranslation'
83
- import { buildTypeConfig, toIsoDay } from '../../functions/tabular'
83
+ import { buildCellValueFilter, buildTypeConfig } from '../../functions/tabular'
84
84
  import ClientOnly from '../ClientOnly.vue'
85
85
  import type { ColumnType, ColumnFilters } from './types'
86
86
 
@@ -128,36 +128,10 @@ function close() {
128
128
 
129
129
  function filterByValue() {
130
130
  if (!cell.value) return
131
- const val = String(cell.value.value ?? '')
132
131
  const col = cell.value.column
133
- const existing = filters.value[col] ?? {}
134
- // A date goes through the same `date` filter the column panel writes, so the
135
- // calendar opens on the day that was clicked instead of on an empty month.
136
- if (cell.value.columnType === 'date') {
137
- const day = toIsoDay(cell.value.value)
138
- if (day) {
139
- filters.value = { ...filters.value, [col]: { ...existing, date: { operator: 'is', start: day } } }
140
- }
141
- // An empty cell has no day to select: filtering on the missing values is
142
- // what "this value" means there, and the panel shows it as such.
143
- else if (cell.value.value == null || cell.value.value === '') {
144
- filters.value = { ...filters.value, [col]: { ...existing, null: 'only' } }
145
- }
146
- }
147
- else if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'year') {
148
- const current = existing.in ?? []
149
- if (!current.includes(val)) {
150
- filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
151
- }
152
- }
153
- else if (cell.value.columnType === 'number') {
154
- const num = Number(cell.value.value)
155
- if (Number.isFinite(num)) {
156
- filters.value = { ...filters.value, [col]: { ...existing, min: num, max: num } }
157
- }
158
- }
159
- else if (cell.value.columnType === 'boolean') {
160
- filters.value = { ...filters.value, [col]: { ...existing, exact: val } }
132
+ filters.value = {
133
+ ...filters.value,
134
+ [col]: buildCellValueFilter(cell.value.columnType, cell.value.value, filters.value[col] ?? {}),
161
135
  }
162
136
  close()
163
137
  }
@@ -1,9 +1,9 @@
1
- import type { Chart, ChartForm, ChartForApi, Filter, ColumnDefinition } from '../types/visualizations'
1
+ import type { Chart, ChartForm, ChartForApi, GenericFilter, ColumnDefinition } from '../types/visualizations'
2
2
  import type { ColumnType, TabularProfile } from '../components/TabularExplorer/types'
3
3
  import { resolveColumnType } from './tabular'
4
4
 
5
5
  export function toChartForm(chart: Chart) {
6
- const seriesFilter = chart.series[0]?.filters as Filter | null
6
+ const seriesFilter: GenericFilter | null = chart.series[0]?.filters ?? null
7
7
 
8
8
  return {
9
9
  title: chart.title,
@@ -179,10 +179,6 @@ export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Datas
179
179
  return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
180
180
  }
181
181
 
182
- export function isGeopfSynced(resource: Resource | CommunityResource): boolean {
183
- return resource.geopf?.push_status != null || typeof resource.geopf?.offering_id === 'string'
184
- }
185
-
186
182
  export function resolveResourceExternalUrl<R extends Resource | CommunityResource>(
187
183
  dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>,
188
184
  resource: R,
@@ -93,6 +93,39 @@ export function buildDateFilterParams(column: string, filter: DateFilter): Recor
93
93
  return params
94
94
  }
95
95
 
96
+ /**
97
+ * The filter "filter by this value" sets on a column for the cell that was
98
+ * clicked, merged into the filters already set on that column.
99
+ *
100
+ * An empty cell holds no value to match on: whatever the column type, filtering
101
+ * on the missing values is what "this value" means there — and it is the filter
102
+ * the column panel offers for them.
103
+ */
104
+ export function buildCellValueFilter(columnType: ColumnType, value: unknown, existing: ColumnFilters): ColumnFilters {
105
+ if (value == null || value === '') return { ...existing, null: 'only' }
106
+ switch (columnType) {
107
+ // A date goes through the same `date` filter the column panel writes, so the
108
+ // calendar opens on the day that was clicked instead of on an empty month.
109
+ case 'date': {
110
+ const day = toIsoDay(value)
111
+ return day ? { ...existing, date: { operator: 'is', start: day } } : existing
112
+ }
113
+ case 'number': {
114
+ const num = Number(value)
115
+ return Number.isFinite(num) ? { ...existing, min: num, max: num } : existing
116
+ }
117
+ case 'boolean':
118
+ return { ...existing, exact: String(value) }
119
+ case 'categorical':
120
+ case 'text':
121
+ case 'year': {
122
+ const val = String(value)
123
+ const selected = existing.in ?? []
124
+ return selected.includes(val) ? existing : { ...existing, in: [...selected, val] }
125
+ }
126
+ }
127
+ }
128
+
96
129
  export type TypeDisplay = {
97
130
  icon: Component
98
131
  label: string
@@ -224,15 +257,16 @@ const FALSY_VALUES = ['false', '0', 'non', 'no']
224
257
 
225
258
  // `encodeURIComponent` leaves `.`, `(` and `)` as-is, but they are the operator
226
259
  // separator and the delimiters of the API's `or(...)` grammar: a search value
227
- // containing one makes the parser reject the whole query with a 400. Wrapping
228
- // the value in double quotes (as the API README suggests) does not work here —
229
- // the API keeps them as part of the searched string and returns no result. It
230
- // does percent-decode the value after parsing, so encoding them is enough.
231
- function encodeConditionValue(value: string): string {
260
+ // containing one makes the parser reject the whole query with a 400.
261
+ // The API percent-decodes the query string once before running the grammar
262
+ // parser, then percent-decodes the parsed value: grammar-significant characters
263
+ // must therefore be sent double-encoded (`%252E`) so the first decode leaves
264
+ // them escaped (`%2E`) for the parser to unquote after parsing.
265
+ export function encodeConditionValue(value: string): string {
232
266
  return encodeURIComponent(value)
233
- .replace(/\./g, '%2E')
234
- .replace(/\(/g, '%28')
235
- .replace(/\)/g, '%29')
267
+ .replace(/\./g, '%252E')
268
+ .replace(/\(/g, '%2528')
269
+ .replace(/\)/g, '%2529')
236
270
  }
237
271
 
238
272
  // A decimal literal, not `Number()`: the API compares against a number column,
@@ -1,6 +1,7 @@
1
1
  import { ofetch } from 'ofetch'
2
2
  import { useComponentsConfig, type PluginConfig } from '../config'
3
- import type { GenericFilter } from '../types/visualizations'
3
+ import { encodeConditionValue } from './tabular'
4
+ import type { AndFilters, Filter, GenericFilter, OrFilters } from '../types/visualizations'
4
5
  import type { SortConfig, TabularProfileResponse } from '../components/TabularExplorer/types'
5
6
 
6
7
  export type { SortConfig }
@@ -56,41 +57,67 @@ export async function fetchTabularData(config: PluginConfig, options: FetchTabul
56
57
  return await ofetch<TabularDataResponse>(url)
57
58
  }
58
59
 
59
- function buildFilterQuery(filters: GenericFilter): string {
60
+ /**
61
+ * Serialize a filter tree into tabular-api query params.
62
+ * - `Filter` → `column__condition=value` (ANDed with the other params)
63
+ * - root `AndFilters` → one param per child; `OrFilters` children become `or=(...)` params
64
+ * - root `OrFilters` → a single `or=(...)` param, with `AndFilters` children nested as `and(...)`
65
+ * (nested-condition grammar: `.` instead of `=`, no value for isnull/isnotnull)
66
+ */
67
+ export function buildFilterQuery(filters: GenericFilter): string {
60
68
  const params: Array<string> = []
61
- if ('filters' in filters) {
62
- for (const filter of filters.filters) {
63
- if ('filters' in filter) {
64
- params.push(buildFilterQuery(filter))
69
+ if (filters._cls === 'Filter') {
70
+ const param = serializeParam(filters)
71
+ if (param) params.push(param)
72
+ }
73
+ else if (filters._cls === 'AndFilters') {
74
+ for (const child of filters.filters) {
75
+ if (child._cls === 'Filter') {
76
+ const param = serializeParam(child)
77
+ if (param) params.push(param)
65
78
  }
66
79
  else {
67
- if (filter.condition === 'is_null') {
68
- params.push(`${encodeURIComponent(filter.column)}__isnull`)
69
- }
70
- else if (filter.condition === 'is_not_null') {
71
- params.push(`${encodeURIComponent(filter.column)}__isnotnull`)
72
- }
73
- else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
74
- params.push(`${encodeURIComponent(filter.column)}__${encodeURIComponent(filter.condition)}=${encodeURIComponent(filter.value)}`)
75
- }
80
+ // child is an OrFilters group: serializeGroup returns `or(...)`, the param is `or=(...)`
81
+ const nested = serializeGroup(child)
82
+ if (nested) params.push(`or=${nested.slice(2)}`)
76
83
  }
77
84
  }
78
85
  }
79
86
  else {
80
- const filter = filters
81
- if (filter.condition === 'is_null') {
82
- params.push(`${encodeURIComponent(filter.column)}__isnull`)
83
- }
84
- else if (filter.condition === 'is_not_null') {
85
- params.push(`${encodeURIComponent(filter.column)}__isnotnull`)
86
- }
87
- else if (filter.value !== null && filter.value !== undefined && filter.value !== '') {
88
- params.push(`${encodeURIComponent(filter.column)}__${encodeURIComponent(filter.condition)}=${encodeURIComponent(filter.value)}`)
89
- }
87
+ const nested = serializeGroup(filters)
88
+ if (nested) params.push(`or=${nested.slice(2)}`)
90
89
  }
91
90
  return params.join('&')
92
91
  }
93
92
 
93
+ /** `column__condition=value` param for a top-level (ANDed) filter, null when empty */
94
+ function serializeParam(filter: Filter): string | null {
95
+ const column = encodeURIComponent(filter.column)
96
+ if (filter.condition === 'is_null') return `${column}__isnull`
97
+ if (filter.condition === 'is_not_null') return `${column}__isnotnull`
98
+ if (filter.value === null || filter.value === undefined || filter.value === '') return null
99
+ return `${column}__${encodeURIComponent(filter.condition)}=${encodeURIComponent(filter.value)}`
100
+ }
101
+
102
+ /** `column__condition.value` condition for the `or(...)` / `and(...)` grammar, null when empty */
103
+ function serializeCondition(filter: Filter): string | null {
104
+ const column = encodeURIComponent(filter.column)
105
+ if (filter.condition === 'is_null') return `${column}__isnull`
106
+ if (filter.condition === 'is_not_null') return `${column}__isnotnull`
107
+ if (filter.value === null || filter.value === undefined || filter.value === '') return null
108
+ return `${column}__${encodeURIComponent(filter.condition)}.${encodeConditionValue(filter.value)}`
109
+ }
110
+
111
+ /** `or(a,b,...)` / `and(a,b,...)` for the nested-condition grammar, null when empty */
112
+ function serializeGroup(group: AndFilters | OrFilters): string | null {
113
+ const operator = group._cls === 'OrFilters' ? 'or' : 'and'
114
+ const conditions = group.filters
115
+ .map(child => ('filters' in child ? serializeGroup(child) : serializeCondition(child)))
116
+ .filter((condition): condition is string => condition !== null)
117
+ if (conditions.length === 0) return null
118
+ return `${operator}(${conditions.join(',')})`
119
+ }
120
+
94
121
  /**
95
122
  * Call Tabular-api to get table content
96
123
  */
package/src/main.ts CHANGED
@@ -23,7 +23,7 @@ import type { Site } from './types/site'
23
23
  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
- import type { Chart, ChartForm, ChartForApi, FilterCondition, Filter, AndFilters, GenericFilter, XAxisType, XAxisSortBy, SortDirection, XAxis, XAxisForm, UnitPosition, YAxis, DataSeriesType, DataSeries, DataSeriesForm, CombinedSort, ColumnDefinition, ColumnsDefinition } from './types/visualizations'
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
27
  import type { ColumnFilters, ColumnType, TabularRow } 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'
@@ -272,6 +272,7 @@ export type {
272
272
  FilterCondition,
273
273
  Filter,
274
274
  AndFilters,
275
+ OrFilters,
275
276
  GenericFilter,
276
277
  XAxisType,
277
278
  XAxisSortBy,
@@ -12,11 +12,6 @@ export type FileResourceFileType = 'file'
12
12
 
13
13
  export type ResourceFileType = RemoteResourceFileType | FileResourceFileType
14
14
 
15
- export type GeopfResourceMetadata = {
16
- push_status?: 'pending' | 'done' | 'error' | 'timeout' | null
17
- offering_id?: string | null
18
- }
19
-
20
15
  export type Resource = {
21
16
  id: string
22
17
  title: string
@@ -27,7 +22,6 @@ export type Resource = {
27
22
  extras: Record<string, unknown>
28
23
  metrics: { views: number }
29
24
  harvest: Harvest
30
- geopf: GeopfResourceMetadata | null
31
25
  filesize: number | null
32
26
  filetype: ResourceFileType
33
27
  format: string
@@ -12,10 +12,15 @@ export type Filter = {
12
12
 
13
13
  export type AndFilters = {
14
14
  _cls: 'AndFilters'
15
+ filters: Array<Filter | OrFilters>
16
+ }
17
+
18
+ export type OrFilters = {
19
+ _cls: 'OrFilters'
15
20
  filters: Array<Filter | AndFilters>
16
21
  }
17
22
 
18
- export type GenericFilter = Filter | AndFilters
23
+ export type GenericFilter = Filter | AndFilters | OrFilters
19
24
 
20
25
  export type XAxisType = 'discrete' | 'continuous'
21
26