@datagouv/components-next 1.0.2-dev.153 → 1.0.2-dev.154
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-BRgmha-O.js → Datafair.client-CUK3pS6u.js} +1 -1
- package/dist/{JsonPreview.client-Cs87KC-b.js → JsonPreview.client-DXCVNsnV.js} +2 -2
- package/dist/{MapContainer.client-DLJuagQI.js → MapContainer.client-BTwgsANA.js} +2 -2
- package/dist/{PdfPreview.client-BI90pK6j.js → PdfPreview.client-B-W-HknR.js} +2 -2
- package/dist/{Pmtiles.client-bJzz1c3r.js → Pmtiles.client-CCNaGHFD.js} +1 -1
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-DS_Tvpti.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-Cu7pYpdZ.js} +1 -1
- package/dist/{XmlPreview.client-L8ZyxNdY.js → XmlPreview.client-CalqyfB_.js} +3 -3
- package/dist/components-next.js +99 -98
- package/dist/components.css +1 -1
- package/dist/{index-sr5KjUOp.js → index-CeKxF_3e.js} +1 -1
- package/dist/{leaflet-src-D34jQJmR.js → leaflet-src-zZByibj1.js} +1 -1
- package/dist/{main-CWzZZ7OE.js → main-B4HZYPjy.js} +10166 -10100
- package/dist/{vue3-xml-viewer.common-9Hozz8ne.js → vue3-xml-viewer.common-omEYjVTS.js} +1 -1
- package/package.json +1 -1
- package/src/components/TabularExplorer/TabularCell.vue +9 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +1 -1
- package/src/components/TabularExplorer/TabularExplorer.vue +80 -4
- package/src/components/TabularExplorer/TabularFilterContent.vue +3 -3
- package/src/components/TabularExplorer/types.ts +3 -3
- package/src/functions/tabular.ts +26 -1
package/package.json
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
class="inline-block rounded font-medium px-2 py-0.5 text-xs max-w-full truncate"
|
|
21
21
|
:style="categoryBadgeStyle ? { backgroundColor: categoryBadgeStyle.backgroundColor, color: categoryBadgeStyle.color } : undefined"
|
|
22
22
|
>{{ value }}</span>
|
|
23
|
+
<span
|
|
24
|
+
v-else-if="columnType === 'number' && noNumberFormat"
|
|
25
|
+
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
26
|
+
>{{ value }}</span>
|
|
23
27
|
<span
|
|
24
28
|
v-else-if="columnType === 'number'"
|
|
25
29
|
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
@@ -28,6 +32,10 @@
|
|
|
28
32
|
v-else-if="columnType === 'date'"
|
|
29
33
|
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
30
34
|
>{{ formatCellDate(value) }}</span>
|
|
35
|
+
<span
|
|
36
|
+
v-else-if="columnType === 'year'"
|
|
37
|
+
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
38
|
+
>{{ value }}</span>
|
|
31
39
|
<span
|
|
32
40
|
v-else
|
|
33
41
|
class="text-gray-title truncate block text-xs"
|
|
@@ -44,6 +52,7 @@ defineProps<{
|
|
|
44
52
|
columnType: ColumnType
|
|
45
53
|
categoryBadgeStyle?: BadgeStyle
|
|
46
54
|
compact?: boolean
|
|
55
|
+
noNumberFormat?: boolean
|
|
47
56
|
}>()
|
|
48
57
|
|
|
49
58
|
const { t } = useTranslation()
|
|
@@ -128,7 +128,7 @@ function filterByValue() {
|
|
|
128
128
|
const val = String(cell.value.value ?? '')
|
|
129
129
|
const col = cell.value.column
|
|
130
130
|
const existing = filters.value[col] ?? {}
|
|
131
|
-
if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date') {
|
|
131
|
+
if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date' || cell.value.columnType === 'year') {
|
|
132
132
|
const current = existing.in ?? []
|
|
133
133
|
if (!current.includes(val)) {
|
|
134
134
|
filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
ExploreResourceView), so they line up with the resource title /
|
|
64
64
|
download button above. Only the table itself goes edge-to-edge
|
|
65
65
|
(via -mx-4 below). -->
|
|
66
|
+
<slot name="toolbar-top" />
|
|
66
67
|
<div>
|
|
67
68
|
<div class="flex items-center py-3 gap-2">
|
|
68
69
|
<!-- Mobile: filter & sort button -->
|
|
@@ -379,15 +380,32 @@
|
|
|
379
380
|
v-for="col in displayedColumns"
|
|
380
381
|
:key="col"
|
|
381
382
|
data-cell
|
|
382
|
-
class="p-2 align-middle whitespace-nowrap border-r border-gray-default last:border-r-0 overflow-hidden
|
|
383
|
-
:class="
|
|
383
|
+
class="p-2 align-middle whitespace-nowrap border-r border-gray-default last:border-r-0 overflow-hidden"
|
|
384
|
+
:class="[
|
|
385
|
+
!props.disablePopover && 'cursor-pointer',
|
|
386
|
+
{ 'text-right font-mono tabular-nums text-sm': getColumnType(col) === 'number' || getColumnType(col) === 'date' || getColumnType(col) === 'year' },
|
|
387
|
+
]"
|
|
384
388
|
:style="columnWidths[col] ? { maxWidth: columnWidths[col] + 'px' } : { maxWidth: '300px' }"
|
|
385
389
|
@click="onCellClick(col, row[col], $event)"
|
|
386
390
|
>
|
|
391
|
+
<a
|
|
392
|
+
v-if="linkColumns?.has(col)"
|
|
393
|
+
:href="getRowHref(row) ?? ''"
|
|
394
|
+
class="link"
|
|
395
|
+
>
|
|
396
|
+
<TabularCell
|
|
397
|
+
:value="row[col]"
|
|
398
|
+
:column-type="getColumnType(col)"
|
|
399
|
+
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
400
|
+
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
401
|
+
/>
|
|
402
|
+
</a>
|
|
387
403
|
<TabularCell
|
|
404
|
+
v-else
|
|
388
405
|
:value="row[col]"
|
|
389
406
|
:column-type="getColumnType(col)"
|
|
390
407
|
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
408
|
+
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
391
409
|
/>
|
|
392
410
|
</td>
|
|
393
411
|
</tr>
|
|
@@ -402,6 +420,7 @@
|
|
|
402
420
|
|
|
403
421
|
<!-- Cell popover -->
|
|
404
422
|
<TabularCellPopover
|
|
423
|
+
v-if="!props.disablePopover"
|
|
405
424
|
v-model:cell="activeCell"
|
|
406
425
|
v-model:filters="filters"
|
|
407
426
|
/>
|
|
@@ -450,11 +469,25 @@
|
|
|
450
469
|
class="min-w-0 pl-4 cursor-pointer"
|
|
451
470
|
@click="onCellClick(col, row[col], $event)"
|
|
452
471
|
>
|
|
472
|
+
<a
|
|
473
|
+
v-if="linkColumns?.has(col)"
|
|
474
|
+
:href="getRowHref(row) ?? ''"
|
|
475
|
+
>
|
|
476
|
+
<TabularCell
|
|
477
|
+
:value="row[col]"
|
|
478
|
+
:column-type="getColumnType(col)"
|
|
479
|
+
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
480
|
+
compact
|
|
481
|
+
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
482
|
+
/>
|
|
483
|
+
</a>
|
|
453
484
|
<TabularCell
|
|
485
|
+
v-else
|
|
454
486
|
:value="row[col]"
|
|
455
487
|
:column-type="getColumnType(col)"
|
|
456
488
|
:category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
|
|
457
489
|
compact
|
|
490
|
+
:no-number-format="props.noFormatColumns?.includes(col)"
|
|
458
491
|
/>
|
|
459
492
|
</div>
|
|
460
493
|
</div>
|
|
@@ -614,7 +647,7 @@ import { useFetch } from '../../functions/api'
|
|
|
614
647
|
import { useComponentsConfig } from '../../config'
|
|
615
648
|
import { useTranslation } from '../../composables/useTranslation'
|
|
616
649
|
import { injectTabularProfile } from '../../composables/useTabularProfile'
|
|
617
|
-
import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType } from '../../functions/tabular'
|
|
650
|
+
import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType, buildGlobalSearchConditions } from '../../functions/tabular'
|
|
618
651
|
import ClientOnly from '../ClientOnly.vue'
|
|
619
652
|
import SimpleBanner from '../SimpleBanner.vue'
|
|
620
653
|
import BrandedButton from '../BrandedButton.vue'
|
|
@@ -633,8 +666,37 @@ const props = defineProps<{
|
|
|
633
666
|
// the host's padding. Used on the standalone explore page where the table
|
|
634
667
|
// should span the whole screen while the toolbar stays inside the container.
|
|
635
668
|
fullBleed?: boolean
|
|
669
|
+
// When set, searches across multiple columns using the Tabular API's or(...)
|
|
670
|
+
// parameter. Text and categorical columns get a __contains filter; number
|
|
671
|
+
// columns get a __exact filter (since __contains is not supported for numbers
|
|
672
|
+
// by the API). Date and boolean columns are excluded.
|
|
673
|
+
// Note: combined via AND with any existing column-specific `contains` filters,
|
|
674
|
+
// so it acts as an additional narrowing constraint, not a replacement.
|
|
675
|
+
globalSearch?: string
|
|
676
|
+
// When true, clicking a cell does not open the popover. Use when row-click
|
|
677
|
+
// navigation is the primary interaction.
|
|
678
|
+
disablePopover?: boolean
|
|
679
|
+
// Initial filters applied on mount, e.g. { 'Administration': { contains: 'Ministère' } }.
|
|
680
|
+
// Used when navigating from the detail page badge with query params.
|
|
681
|
+
initialFilters?: Record<string, ColumnFilters>
|
|
682
|
+
// When set, renders <a> tags inside the specified column cells for native
|
|
683
|
+
// browser UX (hover URL, ctrl+click, middle-click).
|
|
684
|
+
rowHref?: { columns: string[], href: (row: TabularRow) => string }
|
|
685
|
+
// Columns listed here render their values raw (no number formatting).
|
|
686
|
+
// Useful for identifier columns.
|
|
687
|
+
noFormatColumns?: string[]
|
|
636
688
|
}>()
|
|
637
689
|
|
|
690
|
+
const rowHrefFn = computed(() => props.rowHref?.href ?? null)
|
|
691
|
+
|
|
692
|
+
function getRowHref(row: TabularRow): string | null {
|
|
693
|
+
return rowHrefFn.value?.(row) ?? null
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const linkColumns = computed(() =>
|
|
697
|
+
props.rowHref ? new Set(props.rowHref.columns) : null,
|
|
698
|
+
)
|
|
699
|
+
|
|
638
700
|
const { t } = useTranslation()
|
|
639
701
|
const config = useComponentsConfig()
|
|
640
702
|
|
|
@@ -658,6 +720,10 @@ const dataUrl = computed(() =>
|
|
|
658
720
|
const sort = ref<SortConfig | null>(null)
|
|
659
721
|
const filters = ref<Record<string, ColumnFilters>>({})
|
|
660
722
|
|
|
723
|
+
if (props.initialFilters) {
|
|
724
|
+
filters.value = { ...props.initialFilters }
|
|
725
|
+
}
|
|
726
|
+
|
|
661
727
|
const PAGE_SIZE = 50
|
|
662
728
|
|
|
663
729
|
const dataQuery = computed(() => {
|
|
@@ -688,6 +754,10 @@ const dataQuery = computed(() => {
|
|
|
688
754
|
q[`${col}__isnotnull`] = ''
|
|
689
755
|
}
|
|
690
756
|
}
|
|
757
|
+
if (props.globalSearch && profileData.value?.profile) {
|
|
758
|
+
const conditions = buildGlobalSearchConditions(allColumns.value, getColumnType, props.globalSearch)
|
|
759
|
+
q.or = '(' + conditions.join(',') + ')'
|
|
760
|
+
}
|
|
691
761
|
return q
|
|
692
762
|
})
|
|
693
763
|
|
|
@@ -938,6 +1008,7 @@ onUnmounted(() => {
|
|
|
938
1008
|
const activeCell = ref<CellInfo | null>(null)
|
|
939
1009
|
|
|
940
1010
|
function onCellClick(col: string, value: unknown, event: MouseEvent) {
|
|
1011
|
+
if (props.disablePopover) return
|
|
941
1012
|
const el = (event.target as HTMLElement).closest('[data-cell]') as HTMLElement | null
|
|
942
1013
|
if (!el) return
|
|
943
1014
|
if (activeCell.value && activeCell.value.element === el) {
|
|
@@ -961,7 +1032,12 @@ const activeFilters = computed<ActiveFilter[]>(() => {
|
|
|
961
1032
|
parts.push(`= ${filter.in.join(', ')}`)
|
|
962
1033
|
}
|
|
963
1034
|
if (filter.exact != null) {
|
|
964
|
-
|
|
1035
|
+
if (getColumnType(col) === 'boolean') {
|
|
1036
|
+
parts.push(`= ${filter.exact === 'true' ? t('Vrai') : t('Faux')}`)
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
parts.push(`= ${filter.exact}`)
|
|
1040
|
+
}
|
|
965
1041
|
}
|
|
966
1042
|
if (filter.contains) {
|
|
967
1043
|
parts.push(`${t('contient')} "${filter.contains}"`)
|
|
@@ -154,14 +154,14 @@
|
|
|
154
154
|
|
|
155
155
|
<!-- Number range -->
|
|
156
156
|
<form
|
|
157
|
-
v-if="columnType === 'number' && columnProfile"
|
|
157
|
+
v-if="(columnType === 'number' || columnType === 'year') && columnProfile"
|
|
158
158
|
class="px-3 py-2 border-b border-black/10 space-y-2"
|
|
159
159
|
@submit.prevent="applyRange"
|
|
160
160
|
>
|
|
161
161
|
<div class="flex items-center gap-2 text-xs text-gray-plain">
|
|
162
|
-
<span class="tabular-nums">{{ formatNumber(profileMin) }}</span>
|
|
162
|
+
<span class="tabular-nums">{{ columnType === 'year' ? profileMin : formatNumber(profileMin) }}</span>
|
|
163
163
|
<span class="text-gray-medium">—</span>
|
|
164
|
-
<span class="tabular-nums">{{ formatNumber(profileMax) }}</span>
|
|
164
|
+
<span class="tabular-nums">{{ columnType === 'year' ? profileMax : formatNumber(profileMax) }}</span>
|
|
165
165
|
</div>
|
|
166
166
|
<div class="flex items-center gap-2">
|
|
167
167
|
<input
|
|
@@ -62,13 +62,13 @@ export type TabularTopValue = {
|
|
|
62
62
|
count: number
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean'
|
|
65
|
+
export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean' | 'year'
|
|
66
66
|
|
|
67
67
|
export type ColumnFilters = {
|
|
68
68
|
in?: string[]
|
|
69
69
|
exact?: string
|
|
70
|
-
min?: number
|
|
71
|
-
max?: number
|
|
70
|
+
min?: number | string
|
|
71
|
+
max?: number | string
|
|
72
72
|
contains?: string
|
|
73
73
|
null?: 'only' | 'exclude'
|
|
74
74
|
}
|
package/src/functions/tabular.ts
CHANGED
|
@@ -42,12 +42,13 @@ export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, Type
|
|
|
42
42
|
text: { icon: RiText, label: t('Texte') },
|
|
43
43
|
date: { icon: RiCalendarLine, label: t('Date') },
|
|
44
44
|
boolean: { icon: RiCheckboxLine, label: t('Booléen') },
|
|
45
|
+
year: { icon: RiCalendarLine, label: t('Année') },
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
|
|
50
|
+
if (colInfo.format === 'year') return 'year'
|
|
49
51
|
if (['int', 'float'].includes(colInfo.python_type)) return 'number'
|
|
50
|
-
if (colInfo.format === 'year') return 'date'
|
|
51
52
|
if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
|
|
52
53
|
if (colInfo.python_type === 'bool') return 'boolean'
|
|
53
54
|
if (isCategorical) return 'categorical'
|
|
@@ -158,6 +159,30 @@ export function useFormatTabular() {
|
|
|
158
159
|
const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
|
|
159
160
|
const FALSY_VALUES = ['false', '0', 'non', 'no']
|
|
160
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Builds the OR conditions for global search across all columns.
|
|
164
|
+
* Text and categorical columns get a `__contains` filter; number columns
|
|
165
|
+
* get a `__exact` filter only when the search value is numeric.
|
|
166
|
+
* Date and boolean columns are excluded.
|
|
167
|
+
*/
|
|
168
|
+
export function buildGlobalSearchConditions(
|
|
169
|
+
allColumns: string[],
|
|
170
|
+
getColumnType: (col: string) => ColumnType,
|
|
171
|
+
searchValue: string,
|
|
172
|
+
): string[] {
|
|
173
|
+
const conditions: string[] = []
|
|
174
|
+
for (const col of allColumns) {
|
|
175
|
+
const type = getColumnType(col)
|
|
176
|
+
if (type === 'text' || type === 'categorical') {
|
|
177
|
+
conditions.push(col + '__contains.' + encodeURIComponent(searchValue))
|
|
178
|
+
}
|
|
179
|
+
else if (type === 'number' && searchValue.length > 0 && isFinite(Number(searchValue))) {
|
|
180
|
+
conditions.push(col + '__exact.' + encodeURIComponent(searchValue))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return conditions
|
|
184
|
+
}
|
|
185
|
+
|
|
161
186
|
export function isTruthy(value: unknown): boolean {
|
|
162
187
|
if (typeof value === 'boolean') return value
|
|
163
188
|
if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
|