@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.200

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 (165) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
  8. package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
  13. package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
  15. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  16. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  17. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  18. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  19. package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
  20. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  21. package/dist/components-next.css +1 -52
  22. package/dist/components-next.js +196 -150
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
  26. package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
  27. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  28. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  29. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  30. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  31. package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +24 -14
  35. package/src/components/AppLink.vue +3 -0
  36. package/src/components/BannerAction.vue +6 -2
  37. package/src/components/Chart/ChartViewer.vue +320 -0
  38. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  39. package/src/components/CopyButton.vue +6 -2
  40. package/src/components/DataserviceCard.vue +24 -5
  41. package/src/components/DataserviceQuality.vue +82 -0
  42. package/src/components/DataserviceQualityInline.vue +31 -0
  43. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  44. package/src/components/DatasetCard.vue +24 -9
  45. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  46. package/src/components/DatasetQuality.vue +11 -11
  47. package/src/components/DatasetQualityInline.vue +2 -2
  48. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  49. package/src/components/DiscussionMessageCard.vue +14 -15
  50. package/src/components/Form/Listbox.vue +101 -0
  51. package/src/components/Form/SchemaSelect.vue +2 -0
  52. package/src/components/FormattedDate.vue +81 -0
  53. package/src/components/InfiniteLoader.vue +53 -0
  54. package/src/components/LoadingBlock.vue +4 -1
  55. package/src/components/ObjectCardHeader.vue +11 -4
  56. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  57. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  58. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  59. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  60. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  61. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  62. package/src/components/OpenApiViewer/openapi.ts +150 -0
  63. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  64. package/src/components/Pagination.vue +25 -62
  65. package/src/components/PostCard.vue +13 -15
  66. package/src/components/RadioInput.vue +7 -2
  67. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  68. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  69. package/src/components/ResourceAccordion/Downloads.vue +172 -0
  70. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  71. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  72. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  73. package/src/components/ResourceAccordion/Metadata.vue +4 -6
  74. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  75. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  76. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  77. package/src/components/ResourceAccordion/Preview.vue +19 -15
  78. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  79. package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
  80. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  81. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  82. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  83. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  84. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  85. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  86. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  87. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
  88. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  89. package/src/components/ResourceListItem.vue +168 -0
  90. package/src/components/ReuseCard.vue +28 -9
  91. package/src/components/ReuseDetails.vue +2 -2
  92. package/src/components/ReuseHorizontalCard.vue +2 -2
  93. package/src/components/SchemaCard.vue +10 -1
  94. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  95. package/src/components/Search/GlobalSearch.vue +229 -123
  96. package/src/components/Search/SearchInput.vue +5 -4
  97. package/src/components/Search/Sidemenu.vue +4 -3
  98. package/src/components/SmallChart.vue +11 -10
  99. package/src/components/StatBox.vue +13 -5
  100. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  101. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  102. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  103. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  104. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  105. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  106. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  107. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  108. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  109. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  110. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  111. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  112. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  113. package/src/components/TabularExplorer/types.ts +84 -0
  114. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  115. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  116. package/src/components/TopicCard.vue +13 -14
  117. package/src/composables/useActiveDescendant.ts +5 -0
  118. package/src/composables/useDatasetResources.ts +164 -0
  119. package/src/composables/useHasTabularData.ts +15 -1
  120. package/src/composables/useMetrics.ts +1 -1
  121. package/src/composables/useResizable.ts +46 -0
  122. package/src/composables/useResourceCapabilities.ts +48 -40
  123. package/src/composables/useSearchFilter.ts +120 -0
  124. package/src/composables/useStableQueryParams.ts +38 -6
  125. package/src/composables/useTabularProfile.ts +70 -0
  126. package/src/composables/useTranslation.ts +24 -9
  127. package/src/config.ts +21 -3
  128. package/src/functions/activities.ts +3 -3
  129. package/src/functions/api.ts +20 -42
  130. package/src/functions/api.types.ts +1 -0
  131. package/src/functions/charts.ts +99 -0
  132. package/src/functions/datasets.ts +0 -17
  133. package/src/functions/dates.ts +6 -7
  134. package/src/functions/helpers.ts +15 -0
  135. package/src/functions/metrics.ts +63 -60
  136. package/src/functions/paginate.ts +46 -0
  137. package/src/functions/resourceCapabilities.ts +34 -0
  138. package/src/functions/resources.ts +136 -1
  139. package/src/functions/schemas.ts +27 -0
  140. package/src/functions/scroll.ts +20 -0
  141. package/src/functions/tabular.ts +214 -0
  142. package/src/functions/tabularApi.ts +96 -11
  143. package/src/main.ts +119 -14
  144. package/src/types/dataservices.ts +17 -0
  145. package/src/types/organizations.ts +10 -0
  146. package/src/types/pages.ts +0 -5
  147. package/src/types/posts.ts +2 -2
  148. package/src/types/reports.ts +5 -1
  149. package/src/types/resources.ts +0 -1
  150. package/src/types/reuses.ts +12 -1
  151. package/src/types/search.ts +63 -1
  152. package/src/types/site.ts +5 -3
  153. package/src/types/ui.ts +2 -0
  154. package/src/types/users.ts +2 -1
  155. package/src/types/visualizations.ts +93 -0
  156. package/assets/swagger-themes/newspaper.css +0 -1670
  157. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  158. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  159. package/dist/Swagger.client-B4y0hXME.js +0 -4
  160. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  161. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  162. package/src/functions/pagination.ts +0 -9
  163. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  164. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  165. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -0,0 +1,214 @@
1
+ import type { Component } from 'vue'
2
+ import {
3
+ RiHashtag,
4
+ RiPriceTag3Line,
5
+ RiText,
6
+ RiCalendarLine,
7
+ RiCalendarEventLine,
8
+ RiTimeLine,
9
+ RiCheckboxLine,
10
+ RiMapPin2Line,
11
+ RiGlobalLine,
12
+ RiFocus3Line,
13
+ RiFingerprintLine,
14
+ RiMailLine,
15
+ RiLinksLine,
16
+ RiPhoneLine,
17
+ RiUserLine,
18
+ RiMoneyEuroCircleLine,
19
+ RiPercentLine,
20
+ RiBracesLine,
21
+ RiCodeLine,
22
+ } from '@remixicon/vue'
23
+ import { useTranslation } from '../composables/useTranslation'
24
+ import type { TranslationFunction } from '../composables/useTranslation'
25
+ import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
26
+
27
+ export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
28
+ const f = filters[column]
29
+ if (!f) return false
30
+ return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null)
31
+ }
32
+
33
+ export type TypeDisplay = {
34
+ icon: Component
35
+ label: string
36
+ }
37
+
38
+ export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, TypeDisplay> {
39
+ return {
40
+ number: { icon: RiHashtag, label: t('Nombre') },
41
+ categorical: { icon: RiPriceTag3Line, label: t('Catégoriel') },
42
+ text: { icon: RiText, label: t('Texte') },
43
+ date: { icon: RiCalendarLine, label: t('Date') },
44
+ boolean: { icon: RiCheckboxLine, label: t('Booléen') },
45
+ year: { icon: RiCalendarLine, label: t('Année') },
46
+ }
47
+ }
48
+
49
+ export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
50
+ if (colInfo.format === 'year') return 'year'
51
+ if (['int', 'float'].includes(colInfo.python_type)) return 'number'
52
+ if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
53
+ if (colInfo.python_type === 'bool') return 'boolean'
54
+ if (isCategorical) return 'categorical'
55
+ return 'text'
56
+ }
57
+
58
+ // csv-detective formats that carry no semantic meaning beyond the generic
59
+ // display type already resolved by `ColumnType` — these fall back to
60
+ // `buildTypeConfig` (Texte / Nombre / Date / Booléen) in the header.
61
+ export const GENERIC_FORMATS = new Set(['string', 'int', 'float', 'date', 'bool', 'booleen'])
62
+
63
+ // Semantic types detected by csv-detective (see ../../../csv-detective/csv_detective/formats).
64
+ // The list is intentionally exhaustive, but unknown formats are tolerated:
65
+ // `getColumnDisplay` humanizes any format missing from this map.
66
+ export function buildFormatConfig(t: TranslationFunction): Record<string, TypeDisplay> {
67
+ return {
68
+ // Geo — territories & addresses
69
+ adresse: { icon: RiMapPin2Line, label: t('Adresse') },
70
+ commune: { icon: RiMapPin2Line, label: t('Commune') },
71
+ code_commune: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
72
+ code_commune_insee: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
73
+ departement: { icon: RiMapPin2Line, label: t('Département') },
74
+ code_departement: { icon: RiMapPin2Line, label: t('Code département') },
75
+ region: { icon: RiMapPin2Line, label: t('Région') },
76
+ code_region: { icon: RiMapPin2Line, label: t('Code région') },
77
+ code_epci: { icon: RiMapPin2Line, label: t('Code EPCI') },
78
+ code_postal: { icon: RiMapPin2Line, label: t('Code postal') },
79
+ insee_canton: { icon: RiMapPin2Line, label: t('Canton') },
80
+ code_fantoir: { icon: RiMapPin2Line, label: t('Code FANTOIR') },
81
+ id_rnb: { icon: RiMapPin2Line, label: t('Identifiant RNB') },
82
+ // Geo — countries
83
+ pays: { icon: RiGlobalLine, label: t('Pays') },
84
+ iso_country_code_alpha2: { icon: RiGlobalLine, label: t('Code pays (ISO α-2)') },
85
+ iso_country_code_alpha3: { icon: RiGlobalLine, label: t('Code pays (ISO α-3)') },
86
+ iso_country_code_numeric: { icon: RiGlobalLine, label: t('Code pays (ISO num.)') },
87
+ // Geo — coordinates
88
+ latitude_wgs: { icon: RiFocus3Line, label: t('Latitude') },
89
+ latitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Latitude') },
90
+ longitude_wgs: { icon: RiFocus3Line, label: t('Longitude') },
91
+ longitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Longitude') },
92
+ latitude_l93: { icon: RiFocus3Line, label: t('Latitude (Lambert 93)') },
93
+ longitude_l93: { icon: RiFocus3Line, label: t('Longitude (Lambert 93)') },
94
+ latlon_wgs: { icon: RiFocus3Line, label: t('Latitude / longitude') },
95
+ lonlat_wgs: { icon: RiFocus3Line, label: t('Longitude / latitude') },
96
+ // Identifiers
97
+ siren: { icon: RiFingerprintLine, label: t('SIREN') },
98
+ siret: { icon: RiFingerprintLine, label: t('SIRET') },
99
+ code_rna: { icon: RiFingerprintLine, label: t('Identifiant RNA') },
100
+ code_waldec: { icon: RiFingerprintLine, label: t('Identifiant WALDEC') },
101
+ code_import: { icon: RiFingerprintLine, label: t('Code Import') },
102
+ uai: { icon: RiFingerprintLine, label: t('UAI') },
103
+ insee_ape700: { icon: RiFingerprintLine, label: t('Code APE') },
104
+ code_csp_insee: { icon: RiFingerprintLine, label: t('Code CSP') },
105
+ uuid: { icon: RiFingerprintLine, label: t('UUID') },
106
+ mongo_object_id: { icon: RiFingerprintLine, label: t('Identifiant Mongo') },
107
+ // Contact
108
+ email: { icon: RiMailLine, label: t('Email') },
109
+ url: { icon: RiLinksLine, label: t('URL') },
110
+ tel_fr: { icon: RiPhoneLine, label: t('Téléphone') },
111
+ username: { icon: RiUserLine, label: t('Nom d\'utilisateur') },
112
+ // People
113
+ sexe: { icon: RiUserLine, label: t('Sexe') },
114
+ csp_insee: { icon: RiUserLine, label: t('Catégorie socio-pro.') },
115
+ // Quantities
116
+ money: { icon: RiMoneyEuroCircleLine, label: t('Montant') },
117
+ percent: { icon: RiPercentLine, label: t('Pourcentage') },
118
+ // Temporal
119
+ year: { icon: RiCalendarLine, label: t('Année') },
120
+ date_fr: { icon: RiCalendarLine, label: t('Date') },
121
+ jour_de_la_semaine: { icon: RiCalendarEventLine, label: t('Jour de la semaine') },
122
+ mois_de_lannee: { icon: RiCalendarEventLine, label: t('Mois de l\'année') },
123
+ datetime_naive: { icon: RiTimeLine, label: t('Date et heure') },
124
+ datetime_aware: { icon: RiTimeLine, label: t('Date et heure') },
125
+ datetime_rfc822: { icon: RiTimeLine, label: t('Date et heure') },
126
+ // Structured
127
+ json: { icon: RiBracesLine, label: t('JSON') },
128
+ geojson: { icon: RiBracesLine, label: t('GeoJSON') },
129
+ binary: { icon: RiCodeLine, label: t('Binaire') },
130
+ }
131
+ }
132
+
133
+ // Tolerant fallback for a semantic format we don't explicitly know about:
134
+ // "code_truc_machin" -> "Code truc machin".
135
+ export function humanizeFormat(format: string): string {
136
+ const spaced = format.replace(/_/g, ' ').trim()
137
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1)
138
+ }
139
+
140
+ export function useFormatTabular() {
141
+ const { locale } = useTranslation()
142
+
143
+ function formatNumber(value: unknown): string {
144
+ const num = Number(value)
145
+ if (Number.isNaN(num)) return String(value)
146
+ return num.toLocaleString(locale)
147
+ }
148
+
149
+ function formatCellDate(value: unknown): string {
150
+ if (value == null || value === '') return '–'
151
+ const d = new Date(String(value))
152
+ if (Number.isNaN(d.getTime())) return String(value)
153
+ return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d)
154
+ }
155
+
156
+ return { formatNumber, formatCellDate }
157
+ }
158
+
159
+ const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
160
+ const FALSY_VALUES = ['false', '0', 'non', 'no']
161
+
162
+ // `encodeURIComponent` leaves `.`, `(` and `)` as-is, but they are the operator
163
+ // separator and the delimiters of the API's `or(...)` grammar: a search value
164
+ // containing one makes the parser reject the whole query with a 400. Wrapping
165
+ // the value in double quotes (as the API README suggests) does not work here —
166
+ // the API keeps them as part of the searched string and returns no result. It
167
+ // does percent-decode the value after parsing, so encoding them is enough.
168
+ function encodeConditionValue(value: string): string {
169
+ return encodeURIComponent(value)
170
+ .replace(/\./g, '%2E')
171
+ .replace(/\(/g, '%28')
172
+ .replace(/\)/g, '%29')
173
+ }
174
+
175
+ // A decimal literal, not `Number()`: the API compares against a number column,
176
+ // and it answers an error for the whole `or(...)` when it is handed something it
177
+ // cannot parse as one — which `Number()` happily accepts (`0x10`, `1e5`, ` `).
178
+ const NUMBER_LITERAL_RE = /^-?\d+(\.\d+)?$/
179
+
180
+ /**
181
+ * Builds the OR conditions for global search across all columns.
182
+ * Text and categorical columns get a `__contains` filter; number columns
183
+ * get a `__exact` filter only when the search value is numeric.
184
+ * Year, date and boolean columns are excluded.
185
+ */
186
+ export function buildGlobalSearchConditions(
187
+ allColumns: string[],
188
+ getColumnType: (col: string) => ColumnType,
189
+ searchValue: string,
190
+ ): string[] {
191
+ const conditions: string[] = []
192
+ for (const col of allColumns) {
193
+ const type = getColumnType(col)
194
+ if (type === 'text' || type === 'categorical') {
195
+ conditions.push(col + '__contains.' + encodeConditionValue(searchValue))
196
+ }
197
+ else if (type === 'number' && NUMBER_LITERAL_RE.test(searchValue)) {
198
+ conditions.push(col + '__exact.' + encodeConditionValue(searchValue))
199
+ }
200
+ }
201
+ return conditions
202
+ }
203
+
204
+ export function isTruthy(value: unknown): boolean {
205
+ if (typeof value === 'boolean') return value
206
+ if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
207
+ return Boolean(value)
208
+ }
209
+
210
+ export function isFalsy(value: unknown): boolean {
211
+ if (typeof value === 'boolean') return !value
212
+ if (typeof value === 'string') return FALSY_VALUES.includes(value.toLowerCase())
213
+ return !value
214
+ }
@@ -1,20 +1,105 @@
1
1
  import { ofetch } from 'ofetch'
2
2
  import { useComponentsConfig, type PluginConfig } from '../config'
3
+ import type { GenericFilter } from '../types/visualizations'
4
+ import type { SortConfig, TabularProfileResponse } from '../components/TabularExplorer/types'
3
5
 
4
- export type SortConfig = {
5
- column: string
6
- type: string
7
- } | null
6
+ export type { SortConfig }
7
+
8
+ export type TabularDataResponse = {
9
+ data: Array<Record<string, unknown>>
10
+ links: {
11
+ profile: string
12
+ swagger: string
13
+ next: string
14
+ }
15
+ meta: { total: number }
16
+ }
17
+
18
+ export type TabularAggregateType = 'avg' | 'sum' | 'count' | 'min' | 'max'
19
+
20
+ export type FetchTabularDataOptions = {
21
+ resourceId: string
22
+ page?: number
23
+ pageSize?: number
24
+ columns?: Array<string> | undefined
25
+ sort?: SortConfig
26
+ groupBy?: string | undefined
27
+ aggregation?: {
28
+ column: string
29
+ type: TabularAggregateType
30
+ } | undefined
31
+ filters?: GenericFilter | undefined
32
+ }
8
33
 
9
34
  /**
10
- * Call Tabular-api to get table content
35
+ * Call Tabular-api to get table content with options object
11
36
  */
12
- export async function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig) {
13
- let url = `${config.tabularApiUrl}/api/resources/${id}/data/?page=${page}&page_size=${config.tabularApiPageSize || 15}`
14
- if (sortConfig) {
15
- url = url + `&${sortConfig.column}__sort=${sortConfig.type}`
37
+ export async function fetchTabularData(config: PluginConfig, options: FetchTabularDataOptions): Promise<TabularDataResponse> {
38
+ const page = options.page ?? 1
39
+ const pageSize = options.pageSize ?? config.tabularApiPageSize ?? 15
40
+ let url = `${config.tabularApiUrl}/api/resources/${options.resourceId}/data/?page=${page}&page_size=${pageSize}`
41
+ if (options.columns) {
42
+ url += `&columns=${options.columns.map(col => encodeURIComponent(col)).join(',')}`
43
+ }
44
+ if (options.sort) {
45
+ url += `&${encodeURIComponent(options.sort.column)}__sort=${encodeURIComponent(options.sort.direction)}`
46
+ }
47
+ if (options.groupBy && options.aggregation?.type) {
48
+ url += `&${encodeURIComponent(options.groupBy)}__groupby&${encodeURIComponent(options.aggregation.column)}__${encodeURIComponent(options.aggregation.type)}`
49
+ }
50
+ if (options.filters) {
51
+ const filterQuery = buildFilterQuery(options.filters)
52
+ if (filterQuery) {
53
+ url += `&${filterQuery}`
54
+ }
16
55
  }
17
- return await ofetch(url)
56
+ return await ofetch<TabularDataResponse>(url)
57
+ }
58
+
59
+ function buildFilterQuery(filters: GenericFilter): string {
60
+ const params: Array<string> = []
61
+ if ('filters' in filters) {
62
+ for (const filter of filters.filters) {
63
+ if ('filters' in filter) {
64
+ params.push(buildFilterQuery(filter))
65
+ }
66
+ else {
67
+ if (filter.condition === 'is_null') {
68
+ params.push(`${encodeURIComponent(filter.column)}__isnull`)
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
+ }
76
+ }
77
+ }
78
+ }
79
+ else {
80
+ const filter = filters
81
+ if (filter.condition === 'is_null') {
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
+ }
90
+ }
91
+ return params.join('&')
92
+ }
93
+
94
+ /**
95
+ * Call Tabular-api to get table content
96
+ */
97
+ export async function getData(config: PluginConfig, id: string, page: number, sortConfig?: SortConfig | null): Promise<TabularDataResponse> {
98
+ return fetchTabularData(config, {
99
+ resourceId: id,
100
+ page,
101
+ sort: sortConfig ?? undefined,
102
+ })
18
103
  }
19
104
 
20
105
  /**
@@ -22,5 +107,5 @@ export async function getData(config: PluginConfig, id: string, page: number, so
22
107
  */
23
108
  export function useGetProfile() {
24
109
  const config = useComponentsConfig()
25
- return (id: string) => ofetch(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
110
+ return (id: string) => ofetch<TabularProfileResponse>(`${config.tabularApiUrl}/api/resources/${id}/profile/`)
26
111
  }
package/src/main.ts CHANGED
@@ -13,9 +13,9 @@ import type { License } from './types/licenses'
13
13
  import type { Member, MemberRole, NewOrganization, Organization, OrganizationOrSuggest, OrganizationReference, OrganizationSuggest } from './types/organizations'
14
14
  import type { Owned, OwnedWithFullObject, OwnedWithId } from './types/owned'
15
15
  import type { Comment, Thread } from './types/discussions'
16
- import type { Page, PageBloc, ContentBloc, BlocWithTitle, DatasetsListBloc, DataservicesListBloc, ReusesListBloc, LinkInBloc, LinksListBloc, MarkdownBloc, AccordionItemBloc, AccordionListBloc, HeroBloc } from './types/pages'
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,8 +23,12 @@ 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 { GlobalSearchConfig, SearchType, SortOption } from './types/search'
27
- import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseConfig, getDefaultOrganizationConfig, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
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'
28
+ import type { GlobalSearchConfig, SearchType, SearchTypeConfig, SortOption, HiddenFilter, BuiltInFilterKey, DatasetSearchConfig, DatasetSearchFilters, DataserviceSearchConfig, DataserviceSearchFilters, ReuseSearchConfig, ReuseSearchFilters, OrganizationSearchConfig, OrganizationSearchFilters, TopicSearchConfig, TopicSearchFilters } from './types/search'
29
+ import { getDefaultDatasetConfig, getDefaultDataserviceConfig, getDefaultReuseConfig, getDefaultOrganizationConfig, getDefaultTopicConfig, getDefaultGlobalSearchConfig, defaultDatasetSortOptions, defaultDataserviceSortOptions, defaultReuseSortOptions, defaultOrganizationSortOptions } from './types/search'
30
+ import { useSearchFilter } from './composables/useSearchFilter'
31
+ import type { UseSearchFilterOptions } from './composables/useSearchFilter'
28
32
 
29
33
  import ActivityList from './components/ActivityList/ActivityList.vue'
30
34
  import UserActivityList from './components/ActivityList/UserActivityList.vue'
@@ -36,19 +40,26 @@ import BannerAction from './components/BannerAction.vue'
36
40
  import BrandedButton from './components/BrandedButton.vue'
37
41
  import CopyButton from './components/CopyButton.vue'
38
42
  import DataserviceCard from './components/DataserviceCard.vue'
43
+ import DataserviceQuality from './components/DataserviceQuality.vue'
44
+ import DataserviceQualityInline from './components/DataserviceQualityInline.vue'
45
+ import DataserviceQualityTooltipContent from './components/DataserviceQualityTooltipContent.vue'
39
46
  import DatasetCard from './components/DatasetCard.vue'
47
+ import DataStructure from './components/ResourceAccordion/DataStructure.vue'
48
+ import Downloads from './components/ResourceAccordion/Downloads.vue'
49
+ import Metadata from './components/ResourceAccordion/Metadata.vue'
40
50
  import DescriptionListTerm from './components/DescriptionListTerm.vue'
41
51
  import DescriptionListDetails from './components/DescriptionListDetails.vue'
42
52
  import DiscussionMessageCard from './components/DiscussionMessageCard.vue'
43
53
  import DateRangeDetails from './components/DateRangeDetails.vue'
54
+ import FormattedDate from './components/FormattedDate.vue'
44
55
  import { DatasetInformationSection, DatasetTemporalitySection, DatasetSpatialSection, DatasetSchemaSection, DatasetEmbedSection } from './components/DatasetInformation'
45
56
  import LeafletMap from './components/LeafletMap.vue'
46
57
  import LicenseBadge from './components/LicenseBadge.vue'
47
58
  import Tag from './components/Tag.vue'
48
59
  import DatasetQuality from './components/DatasetQuality.vue'
49
60
  import DatasetQualityInline from './components/DatasetQualityInline.vue'
50
- import DatasetQualityItem from './components/DatasetQualityItem.vue'
51
- import DatasetQualityScore from './components/DatasetQualityScore.vue'
61
+ import QualityItem from './components/QualityItem.vue'
62
+ import QualityScore from './components/QualityScore.vue'
52
63
  import ProgressBar from './components/ProgressBar.vue'
53
64
  import DatasetQualityTooltipContent from './components/DatasetQualityTooltipContent.vue'
54
65
  import ExtraAccordion from './components/ExtraAccordion.vue'
@@ -57,6 +68,7 @@ import LoadingBlock from './components/LoadingBlock.vue'
57
68
  import MarkdownViewer from './components/MarkdownViewer.vue'
58
69
  import OrganizationCard from './components/OrganizationCard.vue'
59
70
  import OrganizationHorizontalCard from './components/OrganizationHorizontalCard.vue'
71
+ import ObjectCardOwner from './components/ObjectCardOwner.vue'
60
72
  import OrganizationLogo from './components/OrganizationLogo.vue'
61
73
  import OrganizationNameWithCertificate from './components/OrganizationNameWithCertificate.vue'
62
74
  import OwnerType from './components/OwnerType.vue'
@@ -70,16 +82,16 @@ import PostCard from './components/PostCard.vue'
70
82
  import ReadMore from './components/ReadMore.vue'
71
83
  import ResourceAccordion from './components/ResourceAccordion/ResourceAccordion.vue'
72
84
  import ResourceIcon from './components/ResourceAccordion/ResourceIcon.vue'
85
+ import ResourceSelector from './components/ResourceExplorer/ResourceSelector.vue'
73
86
  import ResourceExplorer from './components/ResourceExplorer/ResourceExplorer.vue'
74
87
  import ResourceExplorerSidebar from './components/ResourceExplorer/ResourceExplorerSidebar.vue'
75
88
  import ResourceExplorerViewer from './components/ResourceExplorer/ResourceExplorerViewer.vue'
76
- import Swagger from './components/ResourceAccordion/Swagger.client.vue'
89
+ import OpenApiViewer from './components/OpenApiViewer/OpenApiViewer.vue'
77
90
  import ReuseCard from './components/ReuseCard.vue'
78
91
  import ReuseHorizontalCard from './components/ReuseHorizontalCard.vue'
79
92
  import ReuseDetails from './components/ReuseDetails.vue'
80
93
  import SchemaCard from './components/SchemaCard.vue'
81
94
  import SimpleBanner from './components/SimpleBanner.vue'
82
- import SmallChart from './components/SmallChart.vue'
83
95
  import StatBox from './components/StatBox.vue'
84
96
  import Tab from './components/Tabs/Tab.vue'
85
97
  import TabGroup from './components/Tabs/TabGroup.vue'
@@ -94,16 +106,29 @@ import GlobalSearch from './components/Search/GlobalSearch.vue'
94
106
  import SearchInput from './components/Search/SearchInput.vue'
95
107
  import SearchableSelect from './components/Form/SearchableSelect.vue'
96
108
  import SelectGroup from './components/Form/SelectGroup.vue'
109
+ import Listbox from './components/Form/Listbox.vue'
110
+ import InfiniteLoader from './components/InfiniteLoader.vue'
111
+ import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
112
+ import TabularMobileFilters from './components/TabularExplorer/TabularMobileFilters.vue'
113
+ import TabularTable from './components/TabularExplorer/TabularTable.vue'
114
+ import TabularToolbar from './components/TabularExplorer/TabularToolbar.vue'
97
115
  import type { UseFetchFunction } from './functions/api.types'
98
116
  import { configKey, useComponentsConfig, type PluginConfig } from './config.js'
117
+ import { ofetch } from 'ofetch'
118
+ import { useTranslation } from './composables/useTranslation'
99
119
 
100
120
  export { Toaster, toast } from 'vue-sonner'
101
121
 
102
122
  export * from './composables/useActiveDescendant'
123
+ export * from './composables/useDebouncedRef'
103
124
  export * from './composables/useMetrics'
104
125
  export * from './composables/useReuseType'
105
126
  export * from './composables/useTranslation'
106
127
  export * from './composables/useHasTabularData'
128
+ export * from './composables/useResourceCapabilities'
129
+ export * from './composables/useTabularProfile'
130
+ export * from './composables/useDatasetResources'
131
+ export * from './composables/useResizable'
107
132
 
108
133
  export * from './functions/activities'
109
134
  export * from './functions/datasets'
@@ -116,17 +141,33 @@ export * from './functions/metrics'
116
141
  export * from './functions/never'
117
142
  export * from './functions/organizations'
118
143
  export * from './functions/owned'
119
- export * from './functions/pagination'
120
144
  export * from './functions/resources'
121
145
  export * from './functions/reuses'
122
146
  export * from './functions/schemas'
147
+ export * from './functions/tabular'
123
148
  export * from './functions/users'
149
+ export * from './functions/tabularApi'
150
+ export * from './functions/charts'
124
151
  export * from './types/access_types'
125
152
 
126
153
  export type {
127
154
  GlobalSearchConfig,
128
155
  SearchType,
156
+ SearchTypeConfig,
129
157
  SortOption,
158
+ HiddenFilter,
159
+ BuiltInFilterKey,
160
+ DatasetSearchConfig,
161
+ DatasetSearchFilters,
162
+ DataserviceSearchConfig,
163
+ DataserviceSearchFilters,
164
+ ReuseSearchConfig,
165
+ ReuseSearchFilters,
166
+ OrganizationSearchConfig,
167
+ OrganizationSearchFilters,
168
+ TopicSearchConfig,
169
+ TopicSearchFilters,
170
+ UseSearchFilterOptions,
130
171
  UseFetchFunction,
131
172
  AccessType,
132
173
  AccessAudience,
@@ -168,7 +209,6 @@ export type {
168
209
  Owned,
169
210
  OwnedWithFullObject,
170
211
  OwnedWithId,
171
- Page,
172
212
  PageBloc,
173
213
  ContentBloc,
174
214
  BlocWithTitle,
@@ -197,6 +237,7 @@ export type {
197
237
  ResourceType,
198
238
  ReuseReference,
199
239
  Reuse,
240
+ ReuseV2,
200
241
  ReuseTopic,
201
242
  ReuseType,
202
243
  Schema,
@@ -218,6 +259,29 @@ export type {
218
259
  ValidataError,
219
260
  Weight,
220
261
  WellType,
262
+ Chart,
263
+ ChartForm,
264
+ ChartForApi,
265
+ CombinedSort,
266
+ ColumnDefinition,
267
+ ColumnFilters,
268
+ ColumnType,
269
+ ColumnsDefinition,
270
+ TabularRow,
271
+ FilterCondition,
272
+ Filter,
273
+ AndFilters,
274
+ GenericFilter,
275
+ XAxisType,
276
+ XAxisSortBy,
277
+ SortDirection,
278
+ XAxis,
279
+ XAxisForm,
280
+ UnitPosition,
281
+ YAxis,
282
+ DataSeriesType,
283
+ DataSeries,
284
+ DataSeriesForm,
221
285
  }
222
286
 
223
287
  export {
@@ -225,16 +289,43 @@ export {
225
289
  getDefaultDataserviceConfig,
226
290
  getDefaultReuseConfig,
227
291
  getDefaultOrganizationConfig,
292
+ getDefaultTopicConfig,
228
293
  getDefaultGlobalSearchConfig,
229
294
  defaultDatasetSortOptions,
230
295
  defaultDataserviceSortOptions,
231
296
  defaultReuseSortOptions,
232
297
  defaultOrganizationSortOptions,
298
+ useSearchFilter,
233
299
  }
234
300
 
235
301
  // Vue Plugin
236
302
  const datagouv: Plugin<PluginConfig> = {
237
303
  async install(app: App, options) {
304
+ // Default `$fetch` to an ofetch instance carrying the datagouv API specifics + the consumer's
305
+ // auth hooks, so everything downstream (the default `useFetch`, imperative helpers) can rely on
306
+ // a single configured fetch. A consumer that provides its own `$fetch` keeps full control.
307
+ if (!options.$fetch) {
308
+ options.$fetch = ofetch.create({
309
+ baseURL: options.apiBase,
310
+ onRequest(context) {
311
+ if (options.onRequest) {
312
+ if (Array.isArray(options.onRequest)) options.onRequest.forEach(hook => hook(context))
313
+ else options.onRequest(context)
314
+ }
315
+ context.options.headers.set('Content-Type', 'application/json')
316
+ context.options.headers.set('Accept', 'application/json')
317
+ if (options.devApiKey) context.options.headers.set('X-API-KEY', options.devApiKey)
318
+ const { locale } = useTranslation()
319
+ if (locale) {
320
+ context.options.params ??= {}
321
+ context.options.params['lang'] = locale
322
+ }
323
+ },
324
+ onRequestError: options.onRequestError,
325
+ onResponse: options.onResponse,
326
+ onResponseError: options.onResponseError,
327
+ })
328
+ }
238
329
  app.provide(configKey, options)
239
330
  if (!options.textClamp) {
240
331
  const textClamp = await import('vue3-text-clamp')
@@ -255,7 +346,13 @@ export {
255
346
  BrandedButton,
256
347
  CopyButton,
257
348
  DataserviceCard,
349
+ DataserviceQuality,
350
+ DataserviceQualityInline,
351
+ DataserviceQualityTooltipContent,
258
352
  DatasetCard,
353
+ DataStructure,
354
+ Downloads,
355
+ Metadata,
259
356
  DatasetInformationSection,
260
357
  DatasetTemporalitySection,
261
358
  DatasetSpatialSection,
@@ -266,10 +363,11 @@ export {
266
363
  DiscussionMessageCard,
267
364
  DatasetQuality,
268
365
  DatasetQualityInline,
269
- DatasetQualityItem,
270
- DatasetQualityScore,
366
+ QualityItem,
367
+ QualityScore,
271
368
  DatasetQualityTooltipContent,
272
369
  DateRangeDetails,
370
+ FormattedDate,
273
371
  ExtraAccordion,
274
372
  LabelTag,
275
373
  LeafletMap,
@@ -295,15 +393,16 @@ export {
295
393
  ResourceExplorer,
296
394
  ResourceExplorerSidebar,
297
395
  ResourceExplorerViewer,
396
+ ObjectCardOwner,
298
397
  ResourceIcon,
398
+ ResourceSelector,
299
399
  ReuseCard,
300
400
  ReuseDetails,
301
401
  ReuseHorizontalCard,
302
402
  SchemaCard,
303
403
  SimpleBanner,
304
- SmallChart,
305
404
  StatBox,
306
- Swagger,
405
+ OpenApiViewer,
307
406
  Tab,
308
407
  TabGroup,
309
408
  TabList,
@@ -318,4 +417,10 @@ export {
318
417
  SearchInput,
319
418
  SearchableSelect,
320
419
  SelectGroup,
420
+ Listbox,
421
+ InfiniteLoader,
422
+ TabularExplorer,
423
+ TabularMobileFilters,
424
+ TabularTable,
425
+ TabularToolbar,
321
426
  }
@@ -24,12 +24,27 @@ export type BaseDataservice = Owned & WithAccessType & {
24
24
  license: string | null
25
25
  private: boolean
26
26
  rate_limiting: string
27
+ rate_limiting_url: string | null
27
28
  title: DataserviceReference['title']
28
29
  contact_points: Array<ContactPoint>
29
30
  }
30
31
 
31
32
  export type NewDataservice = Omit<BaseDataservice, keyof OwnedWithId> & OwnedWithId
32
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
+
33
48
  export type Dataservice = Owned & WithAccessType & {
34
49
  acronym: string
35
50
  archived_at: string | null
@@ -65,9 +80,11 @@ export type Dataservice = Owned & WithAccessType & {
65
80
  permissions: { edit: boolean, delete: boolean }
66
81
  private: boolean
67
82
  rate_limiting: string
83
+ rate_limiting_url: string | null
68
84
  self_api_url: DataserviceReference['self_api_url']
69
85
  self_web_url: DataserviceReference['self_web_url']
70
86
  slug: string
71
87
  tags: Array<string>
72
88
  title: string
89
+ quality?: DataserviceQuality
73
90
  }