@datagouv/components-next 1.2.0 → 1.4.0

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 (138) hide show
  1. package/README.md +7 -6
  2. package/dist/{Control-ZFh5ta_U.js → Control-B9WQxlpJ.js} +1 -1
  3. package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-D_6yzJUG.js} +9 -9
  4. package/dist/{Event-DSQcW7OF.js → Event-iXDrp-fy.js} +11 -11
  5. package/dist/{Image-BijNEG0p.js → Image-mNMBOofs.js} +5 -5
  6. package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
  7. package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DSl1kiwC.js} +5 -5
  8. package/dist/{Map-BUtPf5GN.js → Map-BfqKMw0S.js} +1306 -1166
  9. package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-BSfpk_rT.js} +15 -15
  10. package/dist/{OSM-D4MTdBtk.js → OSM-CcrUhItr.js} +15 -23
  11. package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BANFeM6K.js} +441 -434
  12. package/dist/Pmtiles.client-DH9ZrsZR.js +26333 -0
  13. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
  14. package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-VGQ7nkMS.js} +2 -2
  15. package/dist/{Tile-Dcl7oIVu.js → Tile-CSpn0Ca8.js} +153 -156
  16. package/dist/{TileImage-BJeHipMX.js → TileImage-CpbaEFwd.js} +143 -131
  17. package/dist/{View-xp_P_OHw.js → View-BYBpt-AU.js} +167 -117
  18. package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-KCB5mwVf.js} +5 -5
  19. package/dist/{common-BjQlan3k.js → common-BoeTUnDZ.js} +26 -26
  20. package/dist/components-next.css +1 -52
  21. package/dist/components-next.js +201 -170
  22. package/dist/components.css +2 -2
  23. package/dist/{index-NofRBuyf.js → index-DHWzX5vK.js} +5999 -5472
  24. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-pVQfonBI.js} +162 -164
  25. package/dist/{main-Iz1ZCL6k.js → main-BDcYkhOc.js} +36671 -29402
  26. package/dist/{proj-CsNo9yH1.js → proj-DxGbobp7.js} +312 -296
  27. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  28. package/dist/{tilecoord-A0fLnBZr.js → tilecoord-DvX7WWiA.js} +264 -255
  29. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  30. package/dist/{vue3-xml-viewer.common-tVI9uXUz.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
  31. package/package.json +40 -39
  32. package/src/components/ActivityList/ActivityList.vue +21 -12
  33. package/src/components/AppLink.vue +3 -0
  34. package/src/components/BannerAction.vue +6 -2
  35. package/src/components/Chart/ChartViewer.vue +111 -17
  36. package/src/components/Chart/ChartViewerWrapper.vue +28 -16
  37. package/src/components/ChartCard.vue +112 -0
  38. package/src/components/CopyButton.vue +6 -2
  39. package/src/components/DataserviceCard.vue +23 -6
  40. package/src/components/DataserviceQuality.vue +82 -0
  41. package/src/components/DataserviceQualityInline.vue +31 -0
  42. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  43. package/src/components/DatasetCard.vue +17 -6
  44. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  45. package/src/components/DatasetQuality.vue +11 -11
  46. package/src/components/DatasetQualityInline.vue +2 -2
  47. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  48. package/src/components/DiscussionMessageCard.vue +14 -15
  49. package/src/components/Form/SchemaSelect.vue +2 -0
  50. package/src/components/FormattedDate.vue +69 -0
  51. package/src/components/LoadingBlock.vue +4 -1
  52. package/src/components/ObjectCard.vue +4 -1
  53. package/src/components/ObjectCardOwner.vue +13 -1
  54. package/src/components/Pagination.vue +18 -58
  55. package/src/components/Placeholder.vue +3 -2
  56. package/src/components/PostCard.vue +13 -15
  57. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  58. package/src/components/ResourceAccordion/Downloads.vue +19 -7
  59. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  60. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
  61. package/src/components/ResourceAccordion/Metadata.vue +21 -5
  62. package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
  63. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  64. package/src/components/ResourceAccordion/Pmtiles.client.vue +30 -19
  65. package/src/components/ResourceAccordion/Preview.vue +8 -4
  66. package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
  67. package/src/components/ResourceAccordion/ResourceAccordion.vue +31 -11
  68. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  69. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  70. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  71. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +140 -184
  72. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  73. package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
  74. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
  75. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
  76. package/src/components/ResourceListItem.vue +168 -0
  77. package/src/components/ReuseCard.vue +18 -6
  78. package/src/components/ReuseDetails.vue +2 -2
  79. package/src/components/ReuseHorizontalCard.vue +4 -3
  80. package/src/components/SchemaCard.vue +10 -1
  81. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  82. package/src/components/Search/GlobalSearch.vue +28 -10
  83. package/src/components/Search/SearchInput.vue +1 -1
  84. package/src/components/Search/Sidemenu.vue +4 -3
  85. package/src/components/SmallChart.vue +11 -10
  86. package/src/components/StatBox.vue +13 -5
  87. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  88. package/src/components/TabularExplorer/TabularCell.vue +7 -8
  89. package/src/components/TabularExplorer/TabularCellPopover.vue +21 -5
  90. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  91. package/src/components/TabularExplorer/TabularDateField.vue +41 -0
  92. package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
  93. package/src/components/TabularExplorer/TabularExplorer.vue +127 -780
  94. package/src/components/TabularExplorer/TabularFilterContent.vue +48 -38
  95. package/src/components/TabularExplorer/TabularFilterPopover.vue +27 -32
  96. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  97. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  98. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  99. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  100. package/src/components/TabularExplorer/TabularTable.vue +484 -0
  101. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  102. package/src/components/TabularExplorer/types.ts +40 -28
  103. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  104. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  105. package/src/components/TopicCard.vue +15 -14
  106. package/src/composables/useActiveDescendant.ts +5 -0
  107. package/src/composables/useDatasetResources.ts +164 -0
  108. package/src/composables/useHasTabularData.ts +2 -1
  109. package/src/composables/useResizable.ts +46 -0
  110. package/src/composables/useResourceCapabilities.ts +48 -40
  111. package/src/composables/useSearchFilter.ts +7 -5
  112. package/src/composables/useTranslation.ts +24 -9
  113. package/src/config.ts +1 -0
  114. package/src/functions/api.ts +13 -7
  115. package/src/functions/charts.ts +32 -1
  116. package/src/functions/dates.ts +26 -7
  117. package/src/functions/helpers.ts +15 -0
  118. package/src/functions/metrics.ts +57 -56
  119. package/src/functions/owned.ts +21 -0
  120. package/src/functions/paginate.ts +46 -0
  121. package/src/functions/resourceCapabilities.ts +34 -0
  122. package/src/functions/resources.ts +88 -0
  123. package/src/functions/schemas.ts +27 -0
  124. package/src/functions/scroll.ts +20 -0
  125. package/src/functions/tabular.ts +222 -5
  126. package/src/functions/tabularApi.ts +1 -43
  127. package/src/main.ts +32 -6
  128. package/src/types/dataservices.ts +15 -0
  129. package/src/types/organizations.ts +10 -0
  130. package/src/types/resources.ts +0 -1
  131. package/src/types/reuses.ts +12 -1
  132. package/src/types/search.ts +2 -2
  133. package/src/types/visualizations.ts +5 -0
  134. package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
  135. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js +0 -61
  136. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  137. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  138. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -31,6 +31,33 @@ export function getSchemaVersion(schema: RegisteredSchema | null) {
31
31
  }
32
32
  }
33
33
 
34
+ // Accents and separators must not stand between a query and a schema: people type
35
+ // "lave linge" for « Lave-linge », or « reparabilite » without its accents
36
+ function normalizeForSearch(value: string): string {
37
+ return value
38
+ .toLowerCase()
39
+ .normalize('NFD')
40
+ .replace(/\p{Diacritic}/gu, '')
41
+ .replace(/[^a-z0-9]+/g, ' ')
42
+ .trim()
43
+ }
44
+
45
+ // The name is searched along with the title and the description: it is the identifier
46
+ // people know and paste, such as `etalab/indice-durabilite-lave-linge`
47
+ const searchableText = (schema: RegisteredSchema) => normalizeForSearch(`${schema.name} ${schema.title ?? ''} ${schema.description ?? ''}`)
48
+
49
+ const queryTokens = (query: string) => normalizeForSearch(query).split(' ').filter(Boolean)
50
+
51
+ export function schemaMatchesQuery(schema: RegisteredSchema, query: string): boolean {
52
+ // A query made only of punctuation holds no word to match. Callers short-circuit the
53
+ // empty query, so matching everything here would only ever answer that case.
54
+ const tokens = queryTokens(query)
55
+ if (!tokens.length) return false
56
+
57
+ const haystack = searchableText(schema)
58
+ return tokens.every(token => haystack.includes(token))
59
+ }
60
+
34
61
  export function findSchemaInCatalog(catalog: Array<RegisteredSchema>, schema: Schema | null): RegisteredSchema | null {
35
62
  if (!schema) return null
36
63
  return catalog.find(registeredSchema => schema.name === registeredSchema.name) || null
@@ -0,0 +1,20 @@
1
+ // Leaves room for the sticky elements sitting at the top of the viewport.
2
+ const SCROLL_MARGIN = 100
3
+
4
+ /**
5
+ * Bring the top of a block back into view after its content changed (paging,
6
+ * searching, filtering), so the reader restarts at the first result.
7
+ *
8
+ * Does nothing when the top of the block is already on screen: aligning the
9
+ * viewport on a block the reader is already looking at moves the page under
10
+ * them for no reason.
11
+ */
12
+ export function scrollToBlockTop(block: HTMLElement | null | undefined) {
13
+ if (!block) return
14
+
15
+ const { top } = block.getBoundingClientRect()
16
+ if (top >= 0 && top <= window.innerHeight) return
17
+
18
+ block.style.scrollMarginTop = `${SCROLL_MARGIN}px`
19
+ block.scrollIntoView({ behavior: 'smooth', block: 'start' })
20
+ }
@@ -1,30 +1,205 @@
1
1
  import type { Component } from 'vue'
2
+ import { parseDate, type CalendarDate } from '@internationalized/date'
2
3
  import {
3
4
  RiHashtag,
4
5
  RiPriceTag3Line,
5
6
  RiText,
6
7
  RiCalendarLine,
8
+ RiCalendarEventLine,
9
+ RiTimeLine,
7
10
  RiCheckboxLine,
11
+ RiMapPin2Line,
12
+ RiGlobalLine,
13
+ RiFocus3Line,
14
+ RiFingerprintLine,
15
+ RiMailLine,
16
+ RiLinksLine,
17
+ RiPhoneLine,
18
+ RiUserLine,
19
+ RiMoneyEuroCircleLine,
20
+ RiPercentLine,
21
+ RiBracesLine,
22
+ RiCodeLine,
8
23
  } from '@remixicon/vue'
9
24
  import { useTranslation } from '../composables/useTranslation'
10
- import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
25
+ import type { TranslationFunction } from '../composables/useTranslation'
26
+ import type { ColumnFilters, ColumnType, DateFilter } from '../components/TabularExplorer/types'
27
+ import { parseDateValue } from './dates'
11
28
 
12
29
  export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
13
30
  const f = filters[column]
14
31
  if (!f) return false
15
- 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)
16
33
  }
17
34
 
18
- export function buildTypeConfig(t: (s: string) => string): Record<ColumnType, { icon: Component, label: string }> {
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
94
+ }
95
+
96
+ export type TypeDisplay = {
97
+ icon: Component
98
+ label: string
99
+ }
100
+
101
+ export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, TypeDisplay> {
19
102
  return {
20
103
  number: { icon: RiHashtag, label: t('Nombre') },
21
104
  categorical: { icon: RiPriceTag3Line, label: t('Catégoriel') },
22
105
  text: { icon: RiText, label: t('Texte') },
23
106
  date: { icon: RiCalendarLine, label: t('Date') },
24
107
  boolean: { icon: RiCheckboxLine, label: t('Booléen') },
108
+ year: { icon: RiCalendarLine, label: t('Année') },
109
+ }
110
+ }
111
+
112
+ export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
113
+ if (colInfo.format === 'year') return 'year'
114
+ if (['int', 'float'].includes(colInfo.python_type)) return 'number'
115
+ if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
116
+ if (colInfo.python_type === 'bool') return 'boolean'
117
+ if (isCategorical) return 'categorical'
118
+ return 'text'
119
+ }
120
+
121
+ // csv-detective formats that carry no semantic meaning beyond the generic
122
+ // display type already resolved by `ColumnType` — these fall back to
123
+ // `buildTypeConfig` (Texte / Nombre / Date / Booléen) in the header.
124
+ export const GENERIC_FORMATS = new Set(['string', 'int', 'float', 'date', 'bool', 'booleen'])
125
+
126
+ // Semantic types detected by csv-detective (see ../../../csv-detective/csv_detective/formats).
127
+ // The list is intentionally exhaustive, but unknown formats are tolerated:
128
+ // `getColumnDisplay` humanizes any format missing from this map.
129
+ export function buildFormatConfig(t: TranslationFunction): Record<string, TypeDisplay> {
130
+ return {
131
+ // Geo — territories & addresses
132
+ adresse: { icon: RiMapPin2Line, label: t('Adresse') },
133
+ commune: { icon: RiMapPin2Line, label: t('Commune') },
134
+ code_commune: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
135
+ code_commune_insee: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
136
+ departement: { icon: RiMapPin2Line, label: t('Département') },
137
+ code_departement: { icon: RiMapPin2Line, label: t('Code département') },
138
+ region: { icon: RiMapPin2Line, label: t('Région') },
139
+ code_region: { icon: RiMapPin2Line, label: t('Code région') },
140
+ code_epci: { icon: RiMapPin2Line, label: t('Code EPCI') },
141
+ code_postal: { icon: RiMapPin2Line, label: t('Code postal') },
142
+ insee_canton: { icon: RiMapPin2Line, label: t('Canton') },
143
+ code_fantoir: { icon: RiMapPin2Line, label: t('Code FANTOIR') },
144
+ id_rnb: { icon: RiMapPin2Line, label: t('Identifiant RNB') },
145
+ // Geo — countries
146
+ pays: { icon: RiGlobalLine, label: t('Pays') },
147
+ iso_country_code_alpha2: { icon: RiGlobalLine, label: t('Code pays (ISO α-2)') },
148
+ iso_country_code_alpha3: { icon: RiGlobalLine, label: t('Code pays (ISO α-3)') },
149
+ iso_country_code_numeric: { icon: RiGlobalLine, label: t('Code pays (ISO num.)') },
150
+ // Geo — coordinates
151
+ latitude_wgs: { icon: RiFocus3Line, label: t('Latitude') },
152
+ latitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Latitude') },
153
+ longitude_wgs: { icon: RiFocus3Line, label: t('Longitude') },
154
+ longitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Longitude') },
155
+ latitude_l93: { icon: RiFocus3Line, label: t('Latitude (Lambert 93)') },
156
+ longitude_l93: { icon: RiFocus3Line, label: t('Longitude (Lambert 93)') },
157
+ latlon_wgs: { icon: RiFocus3Line, label: t('Latitude / longitude') },
158
+ lonlat_wgs: { icon: RiFocus3Line, label: t('Longitude / latitude') },
159
+ // Identifiers
160
+ siren: { icon: RiFingerprintLine, label: t('SIREN') },
161
+ siret: { icon: RiFingerprintLine, label: t('SIRET') },
162
+ code_rna: { icon: RiFingerprintLine, label: t('Identifiant RNA') },
163
+ code_waldec: { icon: RiFingerprintLine, label: t('Identifiant WALDEC') },
164
+ code_import: { icon: RiFingerprintLine, label: t('Code Import') },
165
+ uai: { icon: RiFingerprintLine, label: t('UAI') },
166
+ insee_ape700: { icon: RiFingerprintLine, label: t('Code APE') },
167
+ code_csp_insee: { icon: RiFingerprintLine, label: t('Code CSP') },
168
+ uuid: { icon: RiFingerprintLine, label: t('UUID') },
169
+ mongo_object_id: { icon: RiFingerprintLine, label: t('Identifiant Mongo') },
170
+ // Contact
171
+ email: { icon: RiMailLine, label: t('Email') },
172
+ url: { icon: RiLinksLine, label: t('URL') },
173
+ tel_fr: { icon: RiPhoneLine, label: t('Téléphone') },
174
+ username: { icon: RiUserLine, label: t('Nom d\'utilisateur') },
175
+ // People
176
+ sexe: { icon: RiUserLine, label: t('Sexe') },
177
+ csp_insee: { icon: RiUserLine, label: t('Catégorie socio-pro.') },
178
+ // Quantities
179
+ money: { icon: RiMoneyEuroCircleLine, label: t('Montant') },
180
+ percent: { icon: RiPercentLine, label: t('Pourcentage') },
181
+ // Temporal
182
+ year: { icon: RiCalendarLine, label: t('Année') },
183
+ date_fr: { icon: RiCalendarLine, label: t('Date') },
184
+ jour_de_la_semaine: { icon: RiCalendarEventLine, label: t('Jour de la semaine') },
185
+ mois_de_lannee: { icon: RiCalendarEventLine, label: t('Mois de l\'année') },
186
+ datetime_naive: { icon: RiTimeLine, label: t('Date et heure') },
187
+ datetime_aware: { icon: RiTimeLine, label: t('Date et heure') },
188
+ datetime_rfc822: { icon: RiTimeLine, label: t('Date et heure') },
189
+ // Structured
190
+ json: { icon: RiBracesLine, label: t('JSON') },
191
+ geojson: { icon: RiBracesLine, label: t('GeoJSON') },
192
+ binary: { icon: RiCodeLine, label: t('Binaire') },
25
193
  }
26
194
  }
27
195
 
196
+ // Tolerant fallback for a semantic format we don't explicitly know about:
197
+ // "code_truc_machin" -> "Code truc machin".
198
+ export function humanizeFormat(format: string): string {
199
+ const spaced = format.replace(/_/g, ' ').trim()
200
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1)
201
+ }
202
+
28
203
  export function useFormatTabular() {
29
204
  const { locale } = useTranslation()
30
205
 
@@ -36,8 +211,8 @@ export function useFormatTabular() {
36
211
 
37
212
  function formatCellDate(value: unknown): string {
38
213
  if (value == null || value === '') return '–'
39
- const d = new Date(String(value))
40
- if (Number.isNaN(d.getTime())) return String(value)
214
+ const d = parseDateValue(String(value))
215
+ if (!d) return String(value)
41
216
  return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d)
42
217
  }
43
218
 
@@ -47,6 +222,48 @@ export function useFormatTabular() {
47
222
  const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
48
223
  const FALSY_VALUES = ['false', '0', 'non', 'no']
49
224
 
225
+ // `encodeURIComponent` leaves `.`, `(` and `)` as-is, but they are the operator
226
+ // 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 {
232
+ return encodeURIComponent(value)
233
+ .replace(/\./g, '%2E')
234
+ .replace(/\(/g, '%28')
235
+ .replace(/\)/g, '%29')
236
+ }
237
+
238
+ // A decimal literal, not `Number()`: the API compares against a number column,
239
+ // and it answers an error for the whole `or(...)` when it is handed something it
240
+ // cannot parse as one — which `Number()` happily accepts (`0x10`, `1e5`, ` `).
241
+ const NUMBER_LITERAL_RE = /^-?\d+(\.\d+)?$/
242
+
243
+ /**
244
+ * Builds the OR conditions for global search across all columns.
245
+ * Text and categorical columns get a `__contains` filter; number columns
246
+ * get a `__exact` filter only when the search value is numeric.
247
+ * Year, date and boolean columns are excluded.
248
+ */
249
+ export function buildGlobalSearchConditions(
250
+ allColumns: string[],
251
+ getColumnType: (col: string) => ColumnType,
252
+ searchValue: string,
253
+ ): string[] {
254
+ const conditions: string[] = []
255
+ for (const col of allColumns) {
256
+ const type = getColumnType(col)
257
+ if (type === 'text' || type === 'categorical') {
258
+ conditions.push(col + '__contains.' + encodeConditionValue(searchValue))
259
+ }
260
+ else if (type === 'number' && NUMBER_LITERAL_RE.test(searchValue)) {
261
+ conditions.push(col + '__exact.' + encodeConditionValue(searchValue))
262
+ }
263
+ }
264
+ return conditions
265
+ }
266
+
50
267
  export function isTruthy(value: unknown): boolean {
51
268
  if (typeof value === 'boolean') return value
52
269
  if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
@@ -1,7 +1,7 @@
1
1
  import { ofetch } from 'ofetch'
2
2
  import { useComponentsConfig, type PluginConfig } from '../config'
3
3
  import type { GenericFilter } from '../types/visualizations'
4
- import type { SortConfig } from '../components/TabularExplorer/types'
4
+ import type { SortConfig, TabularProfileResponse } from '../components/TabularExplorer/types'
5
5
 
6
6
  export type { SortConfig }
7
7
 
@@ -31,48 +31,6 @@ export type FetchTabularDataOptions = {
31
31
  filters?: GenericFilter | undefined
32
32
  }
33
33
 
34
- export type TabularProfileResponse = {
35
- profile: {
36
- header: Array<string>
37
- columns: Record<string, {
38
- score: number
39
- format: string
40
- python_type: string
41
- }>
42
- formats: Record<string, Array<string>>
43
- profile: Record<string, {
44
- tops: Array<{ count: number, value: string }>
45
- nb_distinct: number
46
- nb_missing_values: number
47
- min?: number
48
- max?: number
49
- std?: number
50
- mean?: number
51
- }>
52
- encoding: string
53
- separator: string
54
- categorical: Array<string>
55
- total_lines: number
56
- nb_duplicates: number
57
- columns_fields: Record<string, {
58
- score: number
59
- format: string
60
- python_type: string
61
- }>
62
- columns_labels: Record<string, {
63
- score: number
64
- format: string
65
- python_type: string
66
- }>
67
- header_row_idx: number
68
- heading_columns: number
69
- trailing_columns: number
70
- }
71
- deleted_at: string | null
72
- dataset_id: string
73
- indexes: null
74
- }
75
-
76
34
  /**
77
35
  * Call Tabular-api to get table content with options object
78
36
  */
package/src/main.ts CHANGED
@@ -15,7 +15,7 @@ import type { Owned, OwnedWithFullObject, OwnedWithId } from './types/owned'
15
15
  import type { Comment, Thread } from './types/discussions'
16
16
  import type { PageBloc, ContentBloc, BlocWithTitle, DatasetsListBloc, DataservicesListBloc, ReusesListBloc, LinkInBloc, LinksListBloc, MarkdownBloc, AccordionItemBloc, AccordionListBloc, HeroBloc } from './types/pages'
17
17
  import type { Post } from './types/posts'
18
- import type { ReuseReference, NewReuse, Reuse, ReuseTopic, ReuseType } from './types/reuses'
18
+ import type { ReuseReference, NewReuse, Reuse, ReuseV2, ReuseTopic, ReuseType } from './types/reuses'
19
19
  import type { RegisteredSchema, Schema, SchemaDetails, SchemaField, SchemaPath, SchemaPublicationMode, SchemaResponseData, SchemaVersion, ValidataError } from './types/schemas'
20
20
  import type { TopicV2, TopicElement, TopicElementClass, TopicElementRel } from './types/topics'
21
21
  import type { CommunityResource, FileResourceFileType, RemoteResourceFileType, ResourceFileType, ResourceGroup, ResourceType, Resource } from './types/resources'
@@ -23,7 +23,8 @@ 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 } from './types/visualizations'
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'
27
+ import type { ColumnFilters, ColumnType, TabularRow } from './components/TabularExplorer/types'
27
28
  import type { GlobalSearchConfig, SearchType, SearchTypeConfig, SortOption, HiddenFilter, BuiltInFilterKey, DatasetSearchConfig, DatasetSearchFilters, DataserviceSearchConfig, DataserviceSearchFilters, ReuseSearchConfig, ReuseSearchFilters, OrganizationSearchConfig, OrganizationSearchFilters, TopicSearchConfig, TopicSearchFilters } from './types/search'
28
29
  import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseConfig, getDefaultOrganizationConfig, getDefaultTopicConfig, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
29
30
  import { useSearchFilter } from './composables/useSearchFilter'
@@ -37,8 +38,12 @@ import Avatar from './components/Avatar.vue'
37
38
  import AvatarWithName from './components/AvatarWithName.vue'
38
39
  import BannerAction from './components/BannerAction.vue'
39
40
  import BrandedButton from './components/BrandedButton.vue'
41
+ import ChartCard from './components/ChartCard.vue'
40
42
  import CopyButton from './components/CopyButton.vue'
41
43
  import DataserviceCard from './components/DataserviceCard.vue'
44
+ import DataserviceQuality from './components/DataserviceQuality.vue'
45
+ import DataserviceQualityInline from './components/DataserviceQualityInline.vue'
46
+ import DataserviceQualityTooltipContent from './components/DataserviceQualityTooltipContent.vue'
42
47
  import DatasetCard from './components/DatasetCard.vue'
43
48
  import DataStructure from './components/ResourceAccordion/DataStructure.vue'
44
49
  import Downloads from './components/ResourceAccordion/Downloads.vue'
@@ -47,14 +52,15 @@ import DescriptionListTerm from './components/DescriptionListTerm.vue'
47
52
  import DescriptionListDetails from './components/DescriptionListDetails.vue'
48
53
  import DiscussionMessageCard from './components/DiscussionMessageCard.vue'
49
54
  import DateRangeDetails from './components/DateRangeDetails.vue'
55
+ import FormattedDate from './components/FormattedDate.vue'
50
56
  import { DatasetInformationSection, DatasetTemporalitySection, DatasetSpatialSection, DatasetSchemaSection, DatasetEmbedSection } from './components/DatasetInformation'
51
57
  import LeafletMap from './components/LeafletMap.vue'
52
58
  import LicenseBadge from './components/LicenseBadge.vue'
53
59
  import Tag from './components/Tag.vue'
54
60
  import DatasetQuality from './components/DatasetQuality.vue'
55
61
  import DatasetQualityInline from './components/DatasetQualityInline.vue'
56
- import DatasetQualityItem from './components/DatasetQualityItem.vue'
57
- import DatasetQualityScore from './components/DatasetQualityScore.vue'
62
+ import QualityItem from './components/QualityItem.vue'
63
+ import QualityScore from './components/QualityScore.vue'
58
64
  import ProgressBar from './components/ProgressBar.vue'
59
65
  import DatasetQualityTooltipContent from './components/DatasetQualityTooltipContent.vue'
60
66
  import ExtraAccordion from './components/ExtraAccordion.vue'
@@ -104,6 +110,9 @@ import SelectGroup from './components/Form/SelectGroup.vue'
104
110
  import Listbox from './components/Form/Listbox.vue'
105
111
  import InfiniteLoader from './components/InfiniteLoader.vue'
106
112
  import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
113
+ import TabularMobileFilters from './components/TabularExplorer/TabularMobileFilters.vue'
114
+ import TabularTable from './components/TabularExplorer/TabularTable.vue'
115
+ import TabularToolbar from './components/TabularExplorer/TabularToolbar.vue'
107
116
  import type { UseFetchFunction } from './functions/api.types'
108
117
  import { configKey, useComponentsConfig, type PluginConfig } from './config.js'
109
118
  import { ofetch } from 'ofetch'
@@ -119,6 +128,8 @@ export * from './composables/useTranslation'
119
128
  export * from './composables/useHasTabularData'
120
129
  export * from './composables/useResourceCapabilities'
121
130
  export * from './composables/useTabularProfile'
131
+ export * from './composables/useDatasetResources'
132
+ export * from './composables/useResizable'
122
133
 
123
134
  export * from './functions/activities'
124
135
  export * from './functions/datasets'
@@ -227,6 +238,7 @@ export type {
227
238
  ResourceType,
228
239
  ReuseReference,
229
240
  Reuse,
241
+ ReuseV2,
230
242
  ReuseTopic,
231
243
  ReuseType,
232
244
  Schema,
@@ -251,6 +263,12 @@ export type {
251
263
  Chart,
252
264
  ChartForm,
253
265
  ChartForApi,
266
+ CombinedSort,
267
+ ColumnDefinition,
268
+ ColumnFilters,
269
+ ColumnType,
270
+ ColumnsDefinition,
271
+ TabularRow,
254
272
  FilterCondition,
255
273
  Filter,
256
274
  AndFilters,
@@ -327,8 +345,12 @@ export {
327
345
  AvatarWithName,
328
346
  BannerAction,
329
347
  BrandedButton,
348
+ ChartCard,
330
349
  CopyButton,
331
350
  DataserviceCard,
351
+ DataserviceQuality,
352
+ DataserviceQualityInline,
353
+ DataserviceQualityTooltipContent,
332
354
  DatasetCard,
333
355
  DataStructure,
334
356
  Downloads,
@@ -343,10 +365,11 @@ export {
343
365
  DiscussionMessageCard,
344
366
  DatasetQuality,
345
367
  DatasetQualityInline,
346
- DatasetQualityItem,
347
- DatasetQualityScore,
368
+ QualityItem,
369
+ QualityScore,
348
370
  DatasetQualityTooltipContent,
349
371
  DateRangeDetails,
372
+ FormattedDate,
350
373
  ExtraAccordion,
351
374
  LabelTag,
352
375
  LeafletMap,
@@ -399,4 +422,7 @@ export {
399
422
  Listbox,
400
423
  InfiniteLoader,
401
424
  TabularExplorer,
425
+ TabularMobileFilters,
426
+ TabularTable,
427
+ TabularToolbar,
402
428
  }
@@ -31,6 +31,20 @@ export type BaseDataservice = Owned & WithAccessType & {
31
31
 
32
32
  export type NewDataservice = Omit<BaseDataservice, keyof OwnedWithId> & OwnedWithId
33
33
 
34
+ export type DataserviceQuality = {
35
+ has_description: boolean
36
+ has_machine_documentation: boolean
37
+ has_technical_documentation: boolean
38
+ has_business_documentation: boolean
39
+ license: boolean
40
+ has_contact_point: boolean
41
+ has_base_api_url: boolean
42
+ availability_documented: boolean
43
+ rate_limiting_documented: boolean
44
+ access_conditions_clear: boolean
45
+ score: number
46
+ }
47
+
34
48
  export type Dataservice = Owned & WithAccessType & {
35
49
  acronym: string
36
50
  archived_at: string | null
@@ -72,4 +86,5 @@ export type Dataservice = Owned & WithAccessType & {
72
86
  slug: string
73
87
  tags: Array<string>
74
88
  title: string
89
+ quality?: DataserviceQuality
75
90
  }
@@ -1,5 +1,6 @@
1
1
  import type { User } from './users'
2
2
  import type { Badges } from './badges'
3
+ import type { PageBloc } from './pages'
3
4
 
4
5
  export type MemberRole = 'admin' | 'editor' | 'partial_editor'
5
6
 
@@ -68,4 +69,13 @@ export type Organization = {
68
69
  reuses_by_months: Record<string, number>
69
70
  views: number
70
71
  }
72
+ // Editorial blocs powering the organization "Présentation" tab. Excluded from
73
+ // the default API read mask, so only present when requested with X-Fields.
74
+ // Read through the API's `public_presentation_blocs`: hidden from the public until
75
+ // published, but always returned to organization administrators.
76
+ presentation_blocs?: Array<PageBloc>
77
+ // Publication date of the blocs. `null` means draft (never public); a set date
78
+ // means published. Like `presentation_blocs`, only present when requested with
79
+ // X-Fields.
80
+ presentation_blocs_published_at?: string | null
71
81
  }
@@ -46,7 +46,6 @@ export type OgcLayerInfo = { name: string, default_crs: string | null }
46
46
 
47
47
  export type WfsMetadata = {
48
48
  format: string
49
- layers: Array<OgcLayerInfo>
50
49
  version: string
51
50
  detected_layer: OgcLayerInfo | null
52
51
  output_formats: Array<string>
@@ -1,5 +1,5 @@
1
1
  import type { Owned, OwnedWithId } from './owned'
2
- import type { Dataset } from './datasets'
2
+ import type { Dataset, Rel } from './datasets'
3
3
  import type { Badges } from './badges'
4
4
  import type { Dataservice } from './dataservices'
5
5
 
@@ -51,6 +51,17 @@ export type Reuse = BaseReuse & {
51
51
  permissions: { edit: boolean, delete: boolean }
52
52
  }
53
53
 
54
+ /**
55
+ * Lightweight reuse returned by the listing endpoints (`/api/2/reuses/` and
56
+ * `/api/2/reuses/search/`): the `datasets` and `dataservices` reference lists
57
+ * are replaced by links to their respective listing endpoints filtered on this
58
+ * reuse, so that listing reuses does not dereference every linked dataset.
59
+ */
60
+ export type ReuseV2 = Owned & Omit<Reuse, 'datasets' | 'dataservices'> & {
61
+ datasets: Rel
62
+ dataservices: Rel
63
+ }
64
+
54
65
  export type ReuseType = {
55
66
  id: string
56
67
  label: string
@@ -4,7 +4,7 @@ import type { AccessType } from './access_types'
4
4
  import type { Dataset } from './datasets'
5
5
  import type { Dataservice } from './dataservices'
6
6
  import type { Organization } from './organizations'
7
- import type { Reuse } from './reuses'
7
+ import type { ReuseV2 } from './reuses'
8
8
  import type { TopicV2 } from './topics'
9
9
  import type {
10
10
  CERTIFIED,
@@ -372,7 +372,7 @@ export type GlobalSearchConfig = SearchTypeConfig[]
372
372
  export type SearchResponseByClass = {
373
373
  datasets: DatasetSearchResponse<Dataset>
374
374
  dataservices: DataserviceSearchResponse<Dataservice>
375
- reuses: ReuseSearchResponse<Reuse>
375
+ reuses: ReuseSearchResponse<ReuseV2>
376
376
  organizations: OrganizationSearchResponse<Organization>
377
377
  topics: TopicSearchResponse<TopicV2>
378
378
  }
@@ -1,5 +1,6 @@
1
1
  import type { TabularAggregateType } from '../functions/tabularApi'
2
2
  import type { Owned, OwnedWithId } from './owned'
3
+ import type { ColumnType } from '../components/TabularExplorer/types'
3
4
 
4
5
  export type FilterCondition = 'exact' | 'differs' | 'is_null' | 'is_not_null' | 'greater' | 'less' | 'strictly_greater' | 'strictly_less'
5
6
  export type Filter = {
@@ -66,6 +67,7 @@ export type Chart = Owned & {
66
67
  created_at: string
67
68
  last_modified: string
68
69
  deleted_at: string | null
70
+ image: string | null
69
71
  uri: string
70
72
  page: string
71
73
  x_axis: XAxis
@@ -87,3 +89,6 @@ export type ChartForm = Omit<ChartForApi, 'x_axis' | 'series' | 'owner' | 'organ
87
89
  chart_type?: DataSeriesType | null
88
90
  filter: GenericFilter | null
89
91
  }
92
+
93
+ export type ColumnDefinition = { name: string, type: ColumnType, min?: number, max?: number }
94
+ export type ColumnsDefinition = Record<string, Array<ColumnDefinition>>