@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.
- package/dist/{Datafair.client-B1OKZr6h.js → Datafair.client-Df0Q4FOf.js} +1 -1
- package/dist/{ImagePreview.client-D04HNejY.js → ImagePreview.client-C1nnlB26.js} +1 -1
- package/dist/{JsonPreview.client-B0_IvXry.js → JsonPreview.client-C50x2wrd.js} +2 -2
- package/dist/{MapContainer.client-zzejMZGK.js → MapContainer.client-B5GhGdk1.js} +2 -2
- package/dist/{PdfPreview.client-yeS69omp.js → PdfPreview.client-95sxmcl7.js} +2 -2
- package/dist/{Pmtiles.client-CxixL6Rw.js → Pmtiles.client-B7ItZ-PQ.js} +1 -1
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-DunhHltO.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-D07VQ55G.js} +1 -1
- package/dist/{XmlPreview.client-Bi-Dtuq8.js → XmlPreview.client-BWlVL8M8.js} +3 -3
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +127 -125
- package/dist/{index-B7ZsPIob.js → index-AG-SbiCt.js} +1 -1
- package/dist/{leaflet-src-ClDGecfN.js → leaflet-src-xwWnVeIm.js} +1 -1
- package/dist/{main-DAxkqaSe.js → main-BzR7HPLe.js} +14236 -14204
- package/dist/{vue3-xml-viewer.common-Dux7EzjM.js → vue3-xml-viewer.common-DCzadi-B.js} +1 -1
- package/package.json +1 -1
- package/src/components/ResourceAccordion/EditButton.vue +1 -4
- package/src/components/ResourceAccordion/ResourceAccordion.vue +1 -6
- package/src/components/TabularExplorer/TabularCellPopover.vue +4 -30
- package/src/functions/charts.ts +2 -2
- package/src/functions/resources.ts +0 -4
- package/src/functions/tabular.ts +42 -8
- package/src/functions/tabularApi.ts +52 -25
- package/src/main.ts +2 -1
- package/src/types/resources.ts +0 -6
- package/src/types/visualizations.ts +6 -1
package/package.json
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
icon-only
|
|
5
5
|
:icon="RiPencilLine"
|
|
6
6
|
color="warning"
|
|
7
|
-
:
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
}
|
package/src/functions/charts.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Chart, ChartForm, ChartForApi,
|
|
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
|
|
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,
|
package/src/functions/tabular.ts
CHANGED
|
@@ -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.
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
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, '%
|
|
234
|
-
.replace(/\(/g, '%
|
|
235
|
-
.replace(/\)/g, '%
|
|
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
|
|
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
|
-
|
|
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 (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
81
|
-
if (
|
|
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,
|
package/src/types/resources.ts
CHANGED
|
@@ -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
|
|