@datagouv/components-next 1.0.2-dev.154 → 1.0.2-dev.155

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.
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-B4HZYPjy.js";
1
+ import { g as Ke } from "./main-B3yaCPy_.js";
2
2
  import We from "vue";
3
3
  function Fe($, 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.154",
3
+ "version": "1.0.2-dev.155",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -31,7 +31,7 @@
31
31
  "@vueuse/core": "^14.3.0",
32
32
  "@vueuse/router": "^14.3.0",
33
33
  "chart.js": "^4.5.1",
34
- "dompurify": "^3.4.7",
34
+ "dompurify": "^3.4.11",
35
35
  "echarts": "^6.1.0",
36
36
  "geopf-extensions-openlayers": "1.0.0-beta.11",
37
37
  "leaflet": "^1.9.4",
@@ -20,10 +20,6 @@
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>
27
23
  <span
28
24
  v-else-if="columnType === 'number'"
29
25
  :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
@@ -32,10 +28,6 @@
32
28
  v-else-if="columnType === 'date'"
33
29
  :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
34
30
  >{{ 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>
39
31
  <span
40
32
  v-else
41
33
  class="text-gray-title truncate block text-xs"
@@ -52,7 +44,6 @@ defineProps<{
52
44
  columnType: ColumnType
53
45
  categoryBadgeStyle?: BadgeStyle
54
46
  compact?: boolean
55
- noNumberFormat?: boolean
56
47
  }>()
57
48
 
58
49
  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' || cell.value.columnType === 'year') {
131
+ if (cell.value.columnType === 'categorical' || cell.value.columnType === 'text' || cell.value.columnType === 'date') {
132
132
  const current = existing.in ?? []
133
133
  if (!current.includes(val)) {
134
134
  filters.value = { ...filters.value, [col]: { ...existing, in: [...current, val] } }
@@ -63,7 +63,6 @@
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" />
67
66
  <div>
68
67
  <div class="flex items-center py-3 gap-2">
69
68
  <!-- Mobile: filter & sort button -->
@@ -380,32 +379,15 @@
380
379
  v-for="col in displayedColumns"
381
380
  :key="col"
382
381
  data-cell
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
- ]"
382
+ class="p-2 align-middle whitespace-nowrap border-r border-gray-default last:border-r-0 overflow-hidden cursor-pointer hover:bg-gray-200/50"
383
+ :class="{ 'text-right font-mono tabular-nums text-sm': getColumnType(col) === 'number' || getColumnType(col) === 'date' }"
388
384
  :style="columnWidths[col] ? { maxWidth: columnWidths[col] + 'px' } : { maxWidth: '300px' }"
389
385
  @click="onCellClick(col, row[col], $event)"
390
386
  >
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>
403
387
  <TabularCell
404
- v-else
405
388
  :value="row[col]"
406
389
  :column-type="getColumnType(col)"
407
390
  :category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
408
- :no-number-format="props.noFormatColumns?.includes(col)"
409
391
  />
410
392
  </td>
411
393
  </tr>
@@ -420,7 +402,6 @@
420
402
 
421
403
  <!-- Cell popover -->
422
404
  <TabularCellPopover
423
- v-if="!props.disablePopover"
424
405
  v-model:cell="activeCell"
425
406
  v-model:filters="filters"
426
407
  />
@@ -469,25 +450,11 @@
469
450
  class="min-w-0 pl-4 cursor-pointer"
470
451
  @click="onCellClick(col, row[col], $event)"
471
452
  >
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>
484
453
  <TabularCell
485
- v-else
486
454
  :value="row[col]"
487
455
  :column-type="getColumnType(col)"
488
456
  :category-badge-style="getColumnType(col) === 'categorical' ? getCategoryBadgeStyle(col, String(row[col])) : undefined"
489
457
  compact
490
- :no-number-format="props.noFormatColumns?.includes(col)"
491
458
  />
492
459
  </div>
493
460
  </div>
@@ -647,7 +614,7 @@ import { useFetch } from '../../functions/api'
647
614
  import { useComponentsConfig } from '../../config'
648
615
  import { useTranslation } from '../../composables/useTranslation'
649
616
  import { injectTabularProfile } from '../../composables/useTabularProfile'
650
- import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType, buildGlobalSearchConditions } from '../../functions/tabular'
617
+ import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy, resolveColumnType } from '../../functions/tabular'
651
618
  import ClientOnly from '../ClientOnly.vue'
652
619
  import SimpleBanner from '../SimpleBanner.vue'
653
620
  import BrandedButton from '../BrandedButton.vue'
@@ -666,37 +633,8 @@ const props = defineProps<{
666
633
  // the host's padding. Used on the standalone explore page where the table
667
634
  // should span the whole screen while the toolbar stays inside the container.
668
635
  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[]
688
636
  }>()
689
637
 
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
-
700
638
  const { t } = useTranslation()
701
639
  const config = useComponentsConfig()
702
640
 
@@ -720,10 +658,6 @@ const dataUrl = computed(() =>
720
658
  const sort = ref<SortConfig | null>(null)
721
659
  const filters = ref<Record<string, ColumnFilters>>({})
722
660
 
723
- if (props.initialFilters) {
724
- filters.value = { ...props.initialFilters }
725
- }
726
-
727
661
  const PAGE_SIZE = 50
728
662
 
729
663
  const dataQuery = computed(() => {
@@ -754,10 +688,6 @@ const dataQuery = computed(() => {
754
688
  q[`${col}__isnotnull`] = ''
755
689
  }
756
690
  }
757
- if (props.globalSearch && profileData.value?.profile) {
758
- const conditions = buildGlobalSearchConditions(allColumns.value, getColumnType, props.globalSearch)
759
- q.or = '(' + conditions.join(',') + ')'
760
- }
761
691
  return q
762
692
  })
763
693
 
@@ -1008,7 +938,6 @@ onUnmounted(() => {
1008
938
  const activeCell = ref<CellInfo | null>(null)
1009
939
 
1010
940
  function onCellClick(col: string, value: unknown, event: MouseEvent) {
1011
- if (props.disablePopover) return
1012
941
  const el = (event.target as HTMLElement).closest('[data-cell]') as HTMLElement | null
1013
942
  if (!el) return
1014
943
  if (activeCell.value && activeCell.value.element === el) {
@@ -1032,12 +961,7 @@ const activeFilters = computed<ActiveFilter[]>(() => {
1032
961
  parts.push(`= ${filter.in.join(', ')}`)
1033
962
  }
1034
963
  if (filter.exact != null) {
1035
- if (getColumnType(col) === 'boolean') {
1036
- parts.push(`= ${filter.exact === 'true' ? t('Vrai') : t('Faux')}`)
1037
- }
1038
- else {
1039
- parts.push(`= ${filter.exact}`)
1040
- }
964
+ parts.push(`= ${filter.exact === 'true' ? t('Vrai') : t('Faux')}`)
1041
965
  }
1042
966
  if (filter.contains) {
1043
967
  parts.push(`${t('contient')} "${filter.contains}"`)
@@ -154,14 +154,14 @@
154
154
 
155
155
  <!-- Number range -->
156
156
  <form
157
- v-if="(columnType === 'number' || columnType === 'year') && columnProfile"
157
+ v-if="columnType === 'number' && 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">{{ columnType === 'year' ? profileMin : formatNumber(profileMin) }}</span>
162
+ <span class="tabular-nums">{{ formatNumber(profileMin) }}</span>
163
163
  <span class="text-gray-medium">—</span>
164
- <span class="tabular-nums">{{ columnType === 'year' ? profileMax : formatNumber(profileMax) }}</span>
164
+ <span class="tabular-nums">{{ 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' | 'year'
65
+ export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean'
66
66
 
67
67
  export type ColumnFilters = {
68
68
  in?: string[]
69
69
  exact?: string
70
- min?: number | string
71
- max?: number | string
70
+ min?: number
71
+ max?: number
72
72
  contains?: string
73
73
  null?: 'only' | 'exclude'
74
74
  }
@@ -42,13 +42,12 @@ 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') },
46
45
  }
47
46
  }
48
47
 
49
48
  export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
50
- if (colInfo.format === 'year') return 'year'
51
49
  if (['int', 'float'].includes(colInfo.python_type)) return 'number'
50
+ if (colInfo.format === 'year') return 'date'
52
51
  if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
53
52
  if (colInfo.python_type === 'bool') return 'boolean'
54
53
  if (isCategorical) return 'categorical'
@@ -159,30 +158,6 @@ export function useFormatTabular() {
159
158
  const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
160
159
  const FALSY_VALUES = ['false', '0', 'non', 'no']
161
160
 
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
-
186
161
  export function isTruthy(value: unknown): boolean {
187
162
  if (typeof value === 'boolean') return value
188
163
  if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())