@datagouv/components-next 1.0.2-dev.209 → 1.0.2-dev.210
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-DpdHdQSx.js → Datafair.client-CbuEIwkL.js} +1 -1
- package/dist/{ImagePreview.client-CtbvW_Q_.js → ImagePreview.client-CEHAHHAi.js} +1 -1
- package/dist/{JsonPreview.client-CboMjF2M.js → JsonPreview.client-BTE1Tkyx.js} +2 -2
- package/dist/{MapContainer.client-BPAbtzkr.js → MapContainer.client-DZcQPJZz.js} +2 -2
- package/dist/{PdfPreview.client-DQF8gS3o.js → PdfPreview.client-Blkihbpx.js} +2 -2
- package/dist/{Pmtiles.client-B-X5pTXq.js → Pmtiles.client-bKJX-Qr8.js} +1 -1
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-CzFj5TX2.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-Btbi8cZr.js} +1 -1
- package/dist/{XmlPreview.client-BvlwpV00.js → XmlPreview.client-BxLfQYbe.js} +3 -3
- package/dist/components-next.js +160 -155
- package/dist/components.css +1 -1
- package/dist/{index-D2eZ5uVt.js → index-DBfW8lYm.js} +1 -1
- package/dist/{leaflet-src-CM7H_PEp.js → leaflet-src-BVQOwwAz.js} +1 -1
- package/dist/{main-D1VXLvop.js → main-ajZVLXAR.js} +30613 -25286
- package/dist/{vue3-xml-viewer.common-D8NyU_-4.js → vue3-xml-viewer.common-BZ1O_76n.js} +1 -1
- package/package.json +3 -1
- package/src/components/FormattedDate.vue +2 -14
- package/src/components/TabularExplorer/TabularCellPopover.vue +19 -3
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +23 -2
- package/src/components/TabularExplorer/TabularFilterContent.vue +25 -26
- package/src/components/TabularExplorer/TabularFilterPopover.vue +1 -1
- package/src/components/TabularExplorer/TabularTable.vue +4 -1
- package/src/components/TabularExplorer/types.ts +11 -0
- package/src/functions/dates.ts +20 -0
- package/src/functions/tabular.ts +67 -4
|
@@ -61,22 +61,28 @@
|
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
63
|
|
|
64
|
-
<!--
|
|
64
|
+
<!-- Date filter -->
|
|
65
|
+
<TabularDateFilter
|
|
66
|
+
v-if="columnType === 'date'"
|
|
67
|
+
v-model:filters="filters"
|
|
68
|
+
:column="column"
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<!-- Search (contains) -->
|
|
65
72
|
<div
|
|
66
|
-
v-if="columnType !== 'boolean'"
|
|
73
|
+
v-if="columnType !== 'boolean' && columnType !== 'date'"
|
|
67
74
|
class="px-3 py-2 border-b border-black/10"
|
|
68
75
|
>
|
|
69
76
|
<div class="relative">
|
|
70
|
-
<
|
|
71
|
-
:is="searchField.icon"
|
|
77
|
+
<RiSearchLine
|
|
72
78
|
class="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-gray-medium"
|
|
73
79
|
aria-hidden="true"
|
|
74
80
|
/>
|
|
75
81
|
<input
|
|
76
82
|
v-model="search"
|
|
77
|
-
:type="
|
|
83
|
+
:type="isNumeric ? 'number' : 'text'"
|
|
78
84
|
class="w-full h-8 text-sm border border-transparent rounded-lg py-1 pl-8 pr-3 bg-[#f3f3f5] focus:outline-none focus:border-new-primary"
|
|
79
|
-
:placeholder="
|
|
85
|
+
:placeholder="t('Rechercher...')"
|
|
80
86
|
>
|
|
81
87
|
</div>
|
|
82
88
|
</div>
|
|
@@ -181,14 +187,7 @@
|
|
|
181
187
|
:max="profileMax"
|
|
182
188
|
>
|
|
183
189
|
</div>
|
|
184
|
-
<div class="flex items-center gap-2">
|
|
185
|
-
<BrandedButton
|
|
186
|
-
color="primary"
|
|
187
|
-
size="2xs"
|
|
188
|
-
type="submit"
|
|
189
|
-
>
|
|
190
|
-
{{ t('Appliquer') }}
|
|
191
|
-
</BrandedButton>
|
|
190
|
+
<div class="flex items-center justify-end gap-2">
|
|
192
191
|
<BrandedButton
|
|
193
192
|
color="tertiary"
|
|
194
193
|
size="2xs"
|
|
@@ -198,6 +197,13 @@
|
|
|
198
197
|
>
|
|
199
198
|
{{ t('Effacer') }}
|
|
200
199
|
</BrandedButton>
|
|
200
|
+
<BrandedButton
|
|
201
|
+
color="primary"
|
|
202
|
+
size="2xs"
|
|
203
|
+
type="submit"
|
|
204
|
+
>
|
|
205
|
+
{{ t('Appliquer') }}
|
|
206
|
+
</BrandedButton>
|
|
201
207
|
</div>
|
|
202
208
|
</form>
|
|
203
209
|
</div>
|
|
@@ -210,13 +216,13 @@ import {
|
|
|
210
216
|
RiArrowUpLine,
|
|
211
217
|
RiArrowDownLine,
|
|
212
218
|
RiSearchLine,
|
|
213
|
-
RiCalendarLine,
|
|
214
219
|
RiCheckLine,
|
|
215
220
|
} from '@remixicon/vue'
|
|
216
221
|
import { useTranslation } from '../../composables/useTranslation'
|
|
217
222
|
import { useFormatTabular } from '../../functions/tabular'
|
|
218
223
|
import BrandedButton from '../BrandedButton.vue'
|
|
219
224
|
import ProgressBar from '../ProgressBar.vue'
|
|
225
|
+
import TabularDateFilter from './TabularDateFilter.vue'
|
|
220
226
|
import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig, SortDirection } from './types'
|
|
221
227
|
|
|
222
228
|
const props = defineProps<{
|
|
@@ -236,20 +242,13 @@ const { formatNumber } = useFormatTabular()
|
|
|
236
242
|
|
|
237
243
|
const search = ref('')
|
|
238
244
|
|
|
239
|
-
// Numbers
|
|
240
|
-
//
|
|
241
|
-
const
|
|
242
|
-
switch (props.columnType) {
|
|
243
|
-
case 'date': return { icon: RiCalendarLine, type: 'date', placeholder: '' }
|
|
244
|
-
case 'number':
|
|
245
|
-
case 'year': return { icon: RiSearchLine, type: 'number', placeholder: t('Rechercher...') }
|
|
246
|
-
default: return { icon: RiSearchLine, type: 'text', placeholder: t('Rechercher...') }
|
|
247
|
-
}
|
|
248
|
-
})
|
|
245
|
+
// Numbers and years are matched exactly (the API has no `contains` for them),
|
|
246
|
+
// so the field offers a number input instead of a text search.
|
|
247
|
+
const isNumeric = computed(() => props.columnType === 'number' || props.columnType === 'year')
|
|
249
248
|
|
|
250
249
|
watchDebounced(search, (q) => {
|
|
251
250
|
const existing = filters.value[props.column] ?? {}
|
|
252
|
-
const operator =
|
|
251
|
+
const operator = isNumeric.value ? 'exact' : 'contains'
|
|
253
252
|
if (q) {
|
|
254
253
|
filters.value = { ...filters.value, [props.column]: { ...existing, [operator]: q } }
|
|
255
254
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
v-show="open"
|
|
23
23
|
ref="panel"
|
|
24
24
|
static
|
|
25
|
-
class="bg-white border border-black/10 rounded-lg shadow-md w-
|
|
25
|
+
class="bg-white border border-black/10 rounded-lg shadow-md w-80 absolute z-[800]"
|
|
26
26
|
:style="floatingStyles"
|
|
27
27
|
:data-testid="`column-filter-${column}`"
|
|
28
28
|
>
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
]"
|
|
36
36
|
:aria-busy="isRefreshing"
|
|
37
37
|
>
|
|
38
|
-
<table
|
|
38
|
+
<table
|
|
39
|
+
class="text-sm border-collapse"
|
|
40
|
+
data-testid="data-table"
|
|
41
|
+
>
|
|
39
42
|
<thead
|
|
40
43
|
class="sticky top-0 z-10 bg-gray-some transition-shadow"
|
|
41
44
|
:class="scrolled ? 'shadow-[inset_0_-1px_0_0_#E5E5E5,0_6px_8px_-6px_rgba(0,0,0,0.15)]' : 'shadow-[inset_0_-1px_0_0_#E5E5E5]'"
|
|
@@ -67,6 +67,16 @@ export type TabularTopValue = {
|
|
|
67
67
|
|
|
68
68
|
export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean' | 'year'
|
|
69
69
|
|
|
70
|
+
export type DateFilterOperator = 'is' | 'before' | 'after' | 'between'
|
|
71
|
+
|
|
72
|
+
export type DateFilter = {
|
|
73
|
+
operator: DateFilterOperator
|
|
74
|
+
/** ISO calendar date (YYYY-MM-DD). */
|
|
75
|
+
start: string
|
|
76
|
+
/** ISO calendar date, the inclusive upper bound. Only set when `operator` is 'between'. */
|
|
77
|
+
end?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
export type ColumnFilters = {
|
|
71
81
|
in?: string[]
|
|
72
82
|
exact?: string
|
|
@@ -74,6 +84,7 @@ export type ColumnFilters = {
|
|
|
74
84
|
max?: number
|
|
75
85
|
contains?: string
|
|
76
86
|
null?: 'only' | 'exclude'
|
|
87
|
+
date?: DateFilter
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
export type SortDirection = 'asc' | 'desc'
|
package/src/functions/dates.ts
CHANGED
|
@@ -2,6 +2,26 @@ import { useTranslation } from '../composables/useTranslation'
|
|
|
2
2
|
|
|
3
3
|
const SECONDS_IN_A_DAY = 3600 * 24
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* `2026`, `2026-04` and `2026-04-24` carry no time of day. `new Date()` reads them as
|
|
7
|
+
* midnight UTC, an instant they never claimed, and then converts it: a reader west of
|
|
8
|
+
* UTC gets the day before — and with it the month, and sometimes the year.
|
|
9
|
+
*/
|
|
10
|
+
export const PLAIN_DATE = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A date value read as the day it names. A plain date is built in local time, where no
|
|
14
|
+
* conversion can move it; anything else is an instant and keeps being read as one.
|
|
15
|
+
*/
|
|
16
|
+
export function parseDateValue(value: Date | string | null | undefined): Date | null {
|
|
17
|
+
if (!value) return null
|
|
18
|
+
const plain = typeof value === 'string' ? value.match(PLAIN_DATE) : null
|
|
19
|
+
const date = plain
|
|
20
|
+
? new Date(Number(plain[1]), Number(plain[2] ?? 1) - 1, Number(plain[3] ?? 1))
|
|
21
|
+
: new Date(value)
|
|
22
|
+
return isNaN(date.getTime()) ? null : date
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
export function useFormatDate() {
|
|
6
26
|
const { t, locale } = useTranslation()
|
|
7
27
|
|
package/src/functions/tabular.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Component } from 'vue'
|
|
2
|
+
import { parseDate, type CalendarDate } from '@internationalized/date'
|
|
2
3
|
import {
|
|
3
4
|
RiHashtag,
|
|
4
5
|
RiPriceTag3Line,
|
|
@@ -22,12 +23,74 @@ import {
|
|
|
22
23
|
} from '@remixicon/vue'
|
|
23
24
|
import { useTranslation } from '../composables/useTranslation'
|
|
24
25
|
import type { TranslationFunction } from '../composables/useTranslation'
|
|
25
|
-
import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
|
|
26
|
+
import type { ColumnFilters, ColumnType, DateFilter } from '../components/TabularExplorer/types'
|
|
27
|
+
import { parseDateValue } from './dates'
|
|
26
28
|
|
|
27
29
|
export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
|
|
28
30
|
const f = filters[column]
|
|
29
31
|
if (!f) return false
|
|
30
|
-
return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null)
|
|
32
|
+
return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null || f.date)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// `initialFilters` is a public prop of TabularExplorer, so a filter can carry
|
|
36
|
+
// anything a consumer put in it. An unparseable date is dropped rather than
|
|
37
|
+
// thrown, the way a non-numeric `min` is already ignored by `Number.isFinite`.
|
|
38
|
+
export function parseIsoDate(value: string | undefined): CalendarDate | null {
|
|
39
|
+
if (!value) return null
|
|
40
|
+
try {
|
|
41
|
+
return parseDate(value)
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The calendar day a cell of a date column falls on, as an ISO date, or null
|
|
50
|
+
* when the value is not one the API produced. Timestamps keep only their day:
|
|
51
|
+
* filtering a whole day is the useful reading of "filter by this value", where
|
|
52
|
+
* the exact millisecond would only ever match that one row.
|
|
53
|
+
*/
|
|
54
|
+
export function toIsoDay(value: unknown): string | null {
|
|
55
|
+
const day = String(value ?? '').slice(0, 10)
|
|
56
|
+
return parseIsoDate(day) ? day : null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The day interval a date filter selects, half-open: `[lower, upper)`.
|
|
61
|
+
* An absent bound means the interval is open on that side.
|
|
62
|
+
*/
|
|
63
|
+
function dateFilterBounds(filter: DateFilter): { lower: CalendarDate | null, upper: CalendarDate | null } {
|
|
64
|
+
const start = parseIsoDate(filter.start)
|
|
65
|
+
if (!start) return { lower: null, upper: null }
|
|
66
|
+
switch (filter.operator) {
|
|
67
|
+
case 'is':
|
|
68
|
+
return { lower: start, upper: start.add({ days: 1 }) }
|
|
69
|
+
case 'before':
|
|
70
|
+
return { lower: null, upper: start }
|
|
71
|
+
case 'after':
|
|
72
|
+
return { lower: start.add({ days: 1 }), upper: null }
|
|
73
|
+
case 'between': {
|
|
74
|
+
const end = parseIsoDate(filter.end)
|
|
75
|
+
return { lower: start, upper: end ? end.add({ days: 1 }) : null }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Query params for a date filter, as a half-open day interval.
|
|
82
|
+
*
|
|
83
|
+
* The same two operators cover `date` and `datetime` columns, which
|
|
84
|
+
* `resolveColumnType` merges into one display type. `__exact` would not: a
|
|
85
|
+
* timestamp is never equal to a bare day, so an exact filter silently matches
|
|
86
|
+
* nothing on a `datetime` column.
|
|
87
|
+
*/
|
|
88
|
+
export function buildDateFilterParams(column: string, filter: DateFilter): Record<string, string> {
|
|
89
|
+
const { lower, upper } = dateFilterBounds(filter)
|
|
90
|
+
const params: Record<string, string> = {}
|
|
91
|
+
if (lower) params[`${column}__greater`] = lower.toString()
|
|
92
|
+
if (upper) params[`${column}__strictly_less`] = upper.toString()
|
|
93
|
+
return params
|
|
31
94
|
}
|
|
32
95
|
|
|
33
96
|
export type TypeDisplay = {
|
|
@@ -148,8 +211,8 @@ export function useFormatTabular() {
|
|
|
148
211
|
|
|
149
212
|
function formatCellDate(value: unknown): string {
|
|
150
213
|
if (value == null || value === '') return '–'
|
|
151
|
-
const d =
|
|
152
|
-
if (
|
|
214
|
+
const d = parseDateValue(String(value))
|
|
215
|
+
if (!d) return String(value)
|
|
153
216
|
return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d)
|
|
154
217
|
}
|
|
155
218
|
|