@datagouv/components-next 1.0.2-dev.19 → 1.0.2-dev.191

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 (156) 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-fxOwwaND.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-XSR9THWR.js +36 -0
  8. package/dist/JsonPreview.client-CU1FWFIy.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D0-Dn7jq.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-B7eDgq8y.js} +835 -856
  13. package/dist/Pmtiles.client-CCpvggkZ.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BzhNjYoY.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-CyjpNh5i.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 +194 -149
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-BBnxH-Nj.js} +21842 -15810
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-Be0Ohp2k.js} +185 -187
  26. package/dist/{main-vqhkVhVS.js → main-C2kCZkPD.js} +41515 -73245
  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-__BZsi8i.js} +16 -16
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +3 -2
  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 +9 -0
  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 +9 -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/Form/Listbox.vue +101 -0
  49. package/src/components/Form/SchemaSelect.vue +2 -0
  50. package/src/components/InfiniteLoader.vue +53 -0
  51. package/src/components/ObjectCardHeader.vue +11 -4
  52. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  53. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  54. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  55. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  56. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  57. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  58. package/src/components/OpenApiViewer/openapi.ts +150 -0
  59. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  60. package/src/components/Pagination.vue +23 -58
  61. package/src/components/RadioInput.vue +7 -2
  62. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  63. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  64. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  65. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  66. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  67. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  68. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  69. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  70. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  71. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  72. package/src/components/ResourceAccordion/Preview.vue +11 -11
  73. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  74. package/src/components/ResourceAccordion/ResourceAccordion.vue +23 -114
  75. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  76. package/src/components/ResourceExplorer/ResourceExplorer.vue +102 -210
  77. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +74 -0
  78. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +103 -71
  79. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  80. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  81. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  82. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +115 -0
  83. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  84. package/src/components/ResourceListItem.vue +161 -0
  85. package/src/components/ReuseCard.vue +14 -6
  86. package/src/components/ReuseDetails.vue +2 -2
  87. package/src/components/ReuseHorizontalCard.vue +2 -2
  88. package/src/components/SchemaCard.vue +10 -1
  89. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  90. package/src/components/Search/GlobalSearch.vue +220 -117
  91. package/src/components/Search/SearchInput.vue +5 -4
  92. package/src/components/Search/Sidemenu.vue +4 -3
  93. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  94. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  95. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  96. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  97. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  98. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  99. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  100. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  101. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  102. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  103. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  104. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  105. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  106. package/src/components/TabularExplorer/types.ts +81 -0
  107. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  108. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  109. package/src/composables/useActiveDescendant.ts +5 -0
  110. package/src/composables/useDatasetResources.ts +164 -0
  111. package/src/composables/useHasTabularData.ts +15 -1
  112. package/src/composables/useMetrics.ts +1 -1
  113. package/src/composables/useResizable.ts +46 -0
  114. package/src/composables/useResourceCapabilities.ts +48 -40
  115. package/src/composables/useSearchFilter.ts +120 -0
  116. package/src/composables/useStableQueryParams.ts +38 -6
  117. package/src/composables/useTabularProfile.ts +70 -0
  118. package/src/composables/useTranslation.ts +24 -9
  119. package/src/config.ts +21 -3
  120. package/src/functions/activities.ts +3 -3
  121. package/src/functions/api.ts +20 -42
  122. package/src/functions/api.types.ts +1 -0
  123. package/src/functions/charts.ts +99 -0
  124. package/src/functions/datasets.ts +0 -17
  125. package/src/functions/dates.ts +3 -2
  126. package/src/functions/helpers.ts +15 -0
  127. package/src/functions/metrics.ts +63 -60
  128. package/src/functions/paginate.ts +46 -0
  129. package/src/functions/resourceCapabilities.ts +34 -0
  130. package/src/functions/resources.ts +136 -1
  131. package/src/functions/schemas.ts +27 -0
  132. package/src/functions/tabular.ts +214 -0
  133. package/src/functions/tabularApi.ts +96 -11
  134. package/src/main.ts +117 -14
  135. package/src/types/dataservices.ts +17 -0
  136. package/src/types/organizations.ts +10 -0
  137. package/src/types/pages.ts +0 -5
  138. package/src/types/posts.ts +2 -2
  139. package/src/types/reports.ts +5 -1
  140. package/src/types/resources.ts +0 -1
  141. package/src/types/reuses.ts +12 -1
  142. package/src/types/search.ts +63 -1
  143. package/src/types/site.ts +5 -3
  144. package/src/types/ui.ts +2 -0
  145. package/src/types/users.ts +2 -1
  146. package/src/types/visualizations.ts +93 -0
  147. package/assets/swagger-themes/newspaper.css +0 -1670
  148. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  149. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  150. package/dist/Swagger.client-B4y0hXME.js +0 -4
  151. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  152. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  153. package/src/functions/pagination.ts +0 -9
  154. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  155. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  156. /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,7 +40,13 @@ 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'
@@ -47,8 +57,8 @@ import LicenseBadge from './components/LicenseBadge.vue'
47
57
  import Tag from './components/Tag.vue'
48
58
  import DatasetQuality from './components/DatasetQuality.vue'
49
59
  import DatasetQualityInline from './components/DatasetQualityInline.vue'
50
- import DatasetQualityItem from './components/DatasetQualityItem.vue'
51
- import DatasetQualityScore from './components/DatasetQualityScore.vue'
60
+ import QualityItem from './components/QualityItem.vue'
61
+ import QualityScore from './components/QualityScore.vue'
52
62
  import ProgressBar from './components/ProgressBar.vue'
53
63
  import DatasetQualityTooltipContent from './components/DatasetQualityTooltipContent.vue'
54
64
  import ExtraAccordion from './components/ExtraAccordion.vue'
@@ -57,6 +67,7 @@ import LoadingBlock from './components/LoadingBlock.vue'
57
67
  import MarkdownViewer from './components/MarkdownViewer.vue'
58
68
  import OrganizationCard from './components/OrganizationCard.vue'
59
69
  import OrganizationHorizontalCard from './components/OrganizationHorizontalCard.vue'
70
+ import ObjectCardOwner from './components/ObjectCardOwner.vue'
60
71
  import OrganizationLogo from './components/OrganizationLogo.vue'
61
72
  import OrganizationNameWithCertificate from './components/OrganizationNameWithCertificate.vue'
62
73
  import OwnerType from './components/OwnerType.vue'
@@ -70,16 +81,16 @@ import PostCard from './components/PostCard.vue'
70
81
  import ReadMore from './components/ReadMore.vue'
71
82
  import ResourceAccordion from './components/ResourceAccordion/ResourceAccordion.vue'
72
83
  import ResourceIcon from './components/ResourceAccordion/ResourceIcon.vue'
84
+ import ResourceSelector from './components/ResourceExplorer/ResourceSelector.vue'
73
85
  import ResourceExplorer from './components/ResourceExplorer/ResourceExplorer.vue'
74
86
  import ResourceExplorerSidebar from './components/ResourceExplorer/ResourceExplorerSidebar.vue'
75
87
  import ResourceExplorerViewer from './components/ResourceExplorer/ResourceExplorerViewer.vue'
76
- import Swagger from './components/ResourceAccordion/Swagger.client.vue'
88
+ import OpenApiViewer from './components/OpenApiViewer/OpenApiViewer.vue'
77
89
  import ReuseCard from './components/ReuseCard.vue'
78
90
  import ReuseHorizontalCard from './components/ReuseHorizontalCard.vue'
79
91
  import ReuseDetails from './components/ReuseDetails.vue'
80
92
  import SchemaCard from './components/SchemaCard.vue'
81
93
  import SimpleBanner from './components/SimpleBanner.vue'
82
- import SmallChart from './components/SmallChart.vue'
83
94
  import StatBox from './components/StatBox.vue'
84
95
  import Tab from './components/Tabs/Tab.vue'
85
96
  import TabGroup from './components/Tabs/TabGroup.vue'
@@ -94,16 +105,29 @@ import GlobalSearch from './components/Search/GlobalSearch.vue'
94
105
  import SearchInput from './components/Search/SearchInput.vue'
95
106
  import SearchableSelect from './components/Form/SearchableSelect.vue'
96
107
  import SelectGroup from './components/Form/SelectGroup.vue'
108
+ import Listbox from './components/Form/Listbox.vue'
109
+ import InfiniteLoader from './components/InfiniteLoader.vue'
110
+ import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
111
+ import TabularMobileFilters from './components/TabularExplorer/TabularMobileFilters.vue'
112
+ import TabularTable from './components/TabularExplorer/TabularTable.vue'
113
+ import TabularToolbar from './components/TabularExplorer/TabularToolbar.vue'
97
114
  import type { UseFetchFunction } from './functions/api.types'
98
115
  import { configKey, useComponentsConfig, type PluginConfig } from './config.js'
116
+ import { ofetch } from 'ofetch'
117
+ import { useTranslation } from './composables/useTranslation'
99
118
 
100
119
  export { Toaster, toast } from 'vue-sonner'
101
120
 
102
121
  export * from './composables/useActiveDescendant'
122
+ export * from './composables/useDebouncedRef'
103
123
  export * from './composables/useMetrics'
104
124
  export * from './composables/useReuseType'
105
125
  export * from './composables/useTranslation'
106
126
  export * from './composables/useHasTabularData'
127
+ export * from './composables/useResourceCapabilities'
128
+ export * from './composables/useTabularProfile'
129
+ export * from './composables/useDatasetResources'
130
+ export * from './composables/useResizable'
107
131
 
108
132
  export * from './functions/activities'
109
133
  export * from './functions/datasets'
@@ -116,17 +140,33 @@ export * from './functions/metrics'
116
140
  export * from './functions/never'
117
141
  export * from './functions/organizations'
118
142
  export * from './functions/owned'
119
- export * from './functions/pagination'
120
143
  export * from './functions/resources'
121
144
  export * from './functions/reuses'
122
145
  export * from './functions/schemas'
146
+ export * from './functions/tabular'
123
147
  export * from './functions/users'
148
+ export * from './functions/tabularApi'
149
+ export * from './functions/charts'
124
150
  export * from './types/access_types'
125
151
 
126
152
  export type {
127
153
  GlobalSearchConfig,
128
154
  SearchType,
155
+ SearchTypeConfig,
129
156
  SortOption,
157
+ HiddenFilter,
158
+ BuiltInFilterKey,
159
+ DatasetSearchConfig,
160
+ DatasetSearchFilters,
161
+ DataserviceSearchConfig,
162
+ DataserviceSearchFilters,
163
+ ReuseSearchConfig,
164
+ ReuseSearchFilters,
165
+ OrganizationSearchConfig,
166
+ OrganizationSearchFilters,
167
+ TopicSearchConfig,
168
+ TopicSearchFilters,
169
+ UseSearchFilterOptions,
130
170
  UseFetchFunction,
131
171
  AccessType,
132
172
  AccessAudience,
@@ -168,7 +208,6 @@ export type {
168
208
  Owned,
169
209
  OwnedWithFullObject,
170
210
  OwnedWithId,
171
- Page,
172
211
  PageBloc,
173
212
  ContentBloc,
174
213
  BlocWithTitle,
@@ -197,6 +236,7 @@ export type {
197
236
  ResourceType,
198
237
  ReuseReference,
199
238
  Reuse,
239
+ ReuseV2,
200
240
  ReuseTopic,
201
241
  ReuseType,
202
242
  Schema,
@@ -218,6 +258,29 @@ export type {
218
258
  ValidataError,
219
259
  Weight,
220
260
  WellType,
261
+ Chart,
262
+ ChartForm,
263
+ ChartForApi,
264
+ CombinedSort,
265
+ ColumnDefinition,
266
+ ColumnFilters,
267
+ ColumnType,
268
+ ColumnsDefinition,
269
+ TabularRow,
270
+ FilterCondition,
271
+ Filter,
272
+ AndFilters,
273
+ GenericFilter,
274
+ XAxisType,
275
+ XAxisSortBy,
276
+ SortDirection,
277
+ XAxis,
278
+ XAxisForm,
279
+ UnitPosition,
280
+ YAxis,
281
+ DataSeriesType,
282
+ DataSeries,
283
+ DataSeriesForm,
221
284
  }
222
285
 
223
286
  export {
@@ -225,16 +288,43 @@ export {
225
288
  getDefaultDataserviceConfig,
226
289
  getDefaultReuseConfig,
227
290
  getDefaultOrganizationConfig,
291
+ getDefaultTopicConfig,
228
292
  getDefaultGlobalSearchConfig,
229
293
  defaultDatasetSortOptions,
230
294
  defaultDataserviceSortOptions,
231
295
  defaultReuseSortOptions,
232
296
  defaultOrganizationSortOptions,
297
+ useSearchFilter,
233
298
  }
234
299
 
235
300
  // Vue Plugin
236
301
  const datagouv: Plugin<PluginConfig> = {
237
302
  async install(app: App, options) {
303
+ // Default `$fetch` to an ofetch instance carrying the datagouv API specifics + the consumer's
304
+ // auth hooks, so everything downstream (the default `useFetch`, imperative helpers) can rely on
305
+ // a single configured fetch. A consumer that provides its own `$fetch` keeps full control.
306
+ if (!options.$fetch) {
307
+ options.$fetch = ofetch.create({
308
+ baseURL: options.apiBase,
309
+ onRequest(context) {
310
+ if (options.onRequest) {
311
+ if (Array.isArray(options.onRequest)) options.onRequest.forEach(hook => hook(context))
312
+ else options.onRequest(context)
313
+ }
314
+ context.options.headers.set('Content-Type', 'application/json')
315
+ context.options.headers.set('Accept', 'application/json')
316
+ if (options.devApiKey) context.options.headers.set('X-API-KEY', options.devApiKey)
317
+ const { locale } = useTranslation()
318
+ if (locale) {
319
+ context.options.params ??= {}
320
+ context.options.params['lang'] = locale
321
+ }
322
+ },
323
+ onRequestError: options.onRequestError,
324
+ onResponse: options.onResponse,
325
+ onResponseError: options.onResponseError,
326
+ })
327
+ }
238
328
  app.provide(configKey, options)
239
329
  if (!options.textClamp) {
240
330
  const textClamp = await import('vue3-text-clamp')
@@ -255,7 +345,13 @@ export {
255
345
  BrandedButton,
256
346
  CopyButton,
257
347
  DataserviceCard,
348
+ DataserviceQuality,
349
+ DataserviceQualityInline,
350
+ DataserviceQualityTooltipContent,
258
351
  DatasetCard,
352
+ DataStructure,
353
+ Downloads,
354
+ Metadata,
259
355
  DatasetInformationSection,
260
356
  DatasetTemporalitySection,
261
357
  DatasetSpatialSection,
@@ -266,8 +362,8 @@ export {
266
362
  DiscussionMessageCard,
267
363
  DatasetQuality,
268
364
  DatasetQualityInline,
269
- DatasetQualityItem,
270
- DatasetQualityScore,
365
+ QualityItem,
366
+ QualityScore,
271
367
  DatasetQualityTooltipContent,
272
368
  DateRangeDetails,
273
369
  ExtraAccordion,
@@ -295,15 +391,16 @@ export {
295
391
  ResourceExplorer,
296
392
  ResourceExplorerSidebar,
297
393
  ResourceExplorerViewer,
394
+ ObjectCardOwner,
298
395
  ResourceIcon,
396
+ ResourceSelector,
299
397
  ReuseCard,
300
398
  ReuseDetails,
301
399
  ReuseHorizontalCard,
302
400
  SchemaCard,
303
401
  SimpleBanner,
304
- SmallChart,
305
402
  StatBox,
306
- Swagger,
403
+ OpenApiViewer,
307
404
  Tab,
308
405
  TabGroup,
309
406
  TabList,
@@ -318,4 +415,10 @@ export {
318
415
  SearchInput,
319
416
  SearchableSelect,
320
417
  SelectGroup,
418
+ Listbox,
419
+ InfiniteLoader,
420
+ TabularExplorer,
421
+ TabularMobileFilters,
422
+ TabularTable,
423
+ TabularToolbar,
321
424
  }
@@ -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
  }