@datagouv/components-next 1.0.2-dev.15 → 1.0.2-dev.150

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 (115) 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-BtQqFBTQ.js → Datafair.client-BRgmha-O.js} +4 -4
  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/JsonPreview.client-Cs87KC-b.js +40 -0
  8. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  9. package/dist/{MapContainer.client-DB6p6KF8.js → MapContainer.client-DLJuagQI.js} +38 -39
  10. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  11. package/dist/{PdfPreview.client-Dr4VcIur.js → PdfPreview.client-BI90pK6j.js} +823 -855
  12. package/dist/Pmtiles.client-bJzz1c3r.js +26142 -0
  13. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DS_Tvpti.js +61 -0
  14. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  15. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  16. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  17. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  18. package/dist/XmlPreview.client-L8ZyxNdY.js +34 -0
  19. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  20. package/dist/components-next.css +1 -52
  21. package/dist/components-next.js +174 -142
  22. package/dist/components.css +2 -2
  23. package/dist/{index-D3rieddI.js → index-sr5KjUOp.js} +21147 -15136
  24. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-D34jQJmR.js} +185 -187
  25. package/dist/main-CWzZZ7OE.js +73583 -0
  26. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  27. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  28. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  29. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  30. package/dist/{vue3-xml-viewer.common-CJXShyPB.js → vue3-xml-viewer.common-9Hozz8ne.js} +16 -16
  31. package/package.json +50 -36
  32. package/src/chart.ts +5 -0
  33. package/src/components/ActivityList/ActivityList.vue +3 -2
  34. package/src/components/Chart/ChartViewer.vue +233 -0
  35. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  36. package/src/components/DataserviceCard.vue +3 -0
  37. package/src/components/DatasetCard.vue +9 -4
  38. package/src/components/Form/Listbox.vue +101 -0
  39. package/src/components/Form/SearchableSelect.vue +2 -1
  40. package/src/components/InfiniteLoader.vue +53 -0
  41. package/src/components/ObjectCardHeader.vue +11 -4
  42. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  43. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  44. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  45. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  46. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  47. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  48. package/src/components/OpenApiViewer/openapi.ts +150 -0
  49. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  50. package/src/components/Pagination.vue +8 -5
  51. package/src/components/RadioInput.vue +7 -2
  52. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  53. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  54. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  55. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
  56. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  57. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -87
  58. package/src/components/ResourceAccordion/Pmtiles.client.vue +3 -1
  59. package/src/components/ResourceAccordion/Preview.vue +11 -11
  60. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  61. package/src/components/ResourceAccordion/ResourceAccordion.vue +12 -111
  62. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  63. package/src/components/ResourceExplorer/ResourceExplorer.vue +22 -19
  64. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  65. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +57 -148
  66. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  67. package/src/components/ReuseCard.vue +14 -6
  68. package/src/components/ReuseDetails.vue +2 -2
  69. package/src/components/ReuseHorizontalCard.vue +2 -2
  70. package/src/components/Search/GlobalSearch.vue +219 -117
  71. package/src/components/Search/SearchInput.vue +5 -4
  72. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  73. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  74. package/src/components/TabularExplorer/TabularExplorer.vue +1132 -0
  75. package/src/components/TabularExplorer/TabularFilterContent.vue +353 -0
  76. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  77. package/src/components/TabularExplorer/types.ts +86 -0
  78. package/src/composables/useHasTabularData.ts +15 -1
  79. package/src/composables/useMetrics.ts +1 -1
  80. package/src/composables/useResourceCapabilities.ts +22 -9
  81. package/src/composables/useSearchFilter.ts +120 -0
  82. package/src/composables/useStableQueryParams.ts +38 -6
  83. package/src/composables/useTabularProfile.ts +70 -0
  84. package/src/config.ts +20 -3
  85. package/src/functions/activities.ts +3 -3
  86. package/src/functions/api.ts +20 -42
  87. package/src/functions/api.types.ts +1 -0
  88. package/src/functions/charts.ts +79 -0
  89. package/src/functions/datasets.ts +0 -17
  90. package/src/functions/dates.ts +3 -2
  91. package/src/functions/metrics.ts +6 -4
  92. package/src/functions/resources.ts +110 -1
  93. package/src/functions/tabular.ts +171 -0
  94. package/src/functions/tabularApi.ts +96 -11
  95. package/src/main.ts +97 -10
  96. package/src/types/dataservices.ts +2 -0
  97. package/src/types/organizations.ts +10 -0
  98. package/src/types/pages.ts +0 -5
  99. package/src/types/posts.ts +2 -2
  100. package/src/types/reports.ts +5 -1
  101. package/src/types/resources.ts +0 -1
  102. package/src/types/reuses.ts +12 -1
  103. package/src/types/search.ts +63 -1
  104. package/src/types/site.ts +5 -3
  105. package/src/types/ui.ts +2 -0
  106. package/src/types/users.ts +2 -1
  107. package/src/types/visualizations.ts +93 -0
  108. package/assets/swagger-themes/newspaper.css +0 -1670
  109. package/dist/JsonPreview.client-DojrmGyU.js +0 -72
  110. package/dist/Pmtiles.client-DbMjfK00.js +0 -25185
  111. package/dist/Swagger.client-Db_xYKJv.js +0 -4
  112. package/dist/XmlPreview.client-CHxSFhd7.js +0 -64
  113. package/dist/main-D70Xt4a7.js +0 -105854
  114. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  115. package/src/functions/pagination.ts +0 -9
@@ -1,13 +1,15 @@
1
1
  import { readonly, type Component } from 'vue'
2
2
 
3
3
  import { RiEarthLine, RiMap2Line } from '@remixicon/vue'
4
+ import { useComponentsConfig } from '../config'
4
5
  import Archive from '../components/Icons/Archive.vue'
5
6
  import Code from '../components/Icons/Code.vue'
7
+ import type { Dataset, DatasetV2 } from '../types/datasets'
6
8
  import Documentation from '../components/Icons/Documentation.vue'
7
9
  import Image from '../components/Icons/Image.vue'
8
10
  import Link from '../components/Icons/Link.vue'
9
11
  import Table from '../components/Icons/Table.vue'
10
- import type { Resource } from '../types/resources'
12
+ import type { CommunityResource, Resource } from '../types/resources'
11
13
  import { useTranslation } from '../composables/useTranslation'
12
14
 
13
15
  export function getResourceFormatIcon(format: string): Component | null {
@@ -89,6 +91,11 @@ export function getResourceTitleId(resource: Resource) {
89
91
  export const RESOURCE_TYPE = readonly(['main', 'documentation', 'update', 'api', 'code', 'other'] as const)
90
92
  export type ResourceType = typeof RESOURCE_TYPE[number]
91
93
 
94
+ // Page size used by ResourceExplorer for its per-type resource fetches.
95
+ // Exported so consumers (e.g. the dataset page's `main` prefetch) can build a
96
+ // byte-for-byte identical cache key and let Nuxt dedupe the request.
97
+ export const RESOURCE_EXPLORER_PAGE_SIZE = 10
98
+
92
99
  export const getResourceLabel = (type: ResourceType, count?: number) => {
93
100
  const { t } = useTranslation()
94
101
  switch (type) {
@@ -98,14 +105,29 @@ export const getResourceLabel = (type: ResourceType, count?: number) => {
98
105
  }
99
106
  return t('Fichiers principaux')
100
107
  case 'documentation':
108
+ if (typeof count === 'number') {
109
+ return t('Aucun fichier de documentation | 1 fichier de documentation | {n} fichiers de documentation', count)
110
+ }
101
111
  return t('Documentation')
102
112
  case 'update':
113
+ if (typeof count === 'number') {
114
+ return t('Aucune mise à jour | 1 mise à jour | {n} mises à jour', count)
115
+ }
103
116
  return t('Mise à jour')
104
117
  case 'api':
118
+ if (typeof count === 'number') {
119
+ return t('Aucune API | 1 API | {n} API', count)
120
+ }
105
121
  return t('API')
106
122
  case 'code':
123
+ if (typeof count === 'number') {
124
+ return t('Aucun fichier de code source | 1 fichier de code source | {n} fichiers de code source', count)
125
+ }
107
126
  return t('Code source')
108
127
  case 'other':
128
+ if (typeof count === 'number') {
129
+ return t('Aucun autre fichier | 1 autre fichier | {n} autres fichiers', count)
130
+ }
109
131
  return t('Autre')
110
132
  }
111
133
  }
@@ -129,3 +151,90 @@ export const detectOgcService = (resource: Resource) => {
129
151
  }
130
152
  return false
131
153
  }
154
+
155
+ export function isCommunityResource(resource: Resource | CommunityResource): boolean {
156
+ return 'organization' in resource || 'owner' in resource
157
+ }
158
+
159
+ export function getResourceExternalUrl(dataset: Dataset | DatasetV2 | Omit<Dataset, 'resources' | 'community_resources'>, resource: Resource | CommunityResource): string {
160
+ return `${dataset.page}${isCommunityResource(resource) ? '/community-resources' : ''}?resource_id=${resource.id}`
161
+ }
162
+
163
+ export function getResourceFilesize(resource: Resource): null | number {
164
+ if (resource.filesize) return resource.filesize
165
+ if ('analysis:content-length' in resource.extras) return resource.extras['analysis:content-length'] as number
166
+
167
+ return null
168
+ }
169
+
170
+ // Parsing errors are stored as `<step>:<message>`, where `<step>` names the
171
+ // analysis step that failed (e.g. `pmtiles_export`, `geojson_export`).
172
+
173
+ /** The analysis step that failed, or null when there is no parsing error. */
174
+ export function getParsingErrorStep(resource: Resource): string | null {
175
+ const error = resource.extras['analysis:parsing:error']
176
+ if (typeof error !== 'string' || !error) return null
177
+ const [step] = error.split(':', 1)
178
+ return step ?? null
179
+ }
180
+
181
+ /** The parsing error message without its `<step>:` prefix, or null when there is none. */
182
+ export function getParsingErrorMessage(resource: Resource): string | null {
183
+ const error = resource.extras['analysis:parsing:error']
184
+ if (typeof error !== 'string' || !error) return null
185
+ const colon = error.indexOf(':')
186
+ return colon === -1 ? error : error.slice(colon + 1)
187
+ }
188
+
189
+ // Geo/format export steps run *after* the tabular table has been built, so a
190
+ // failure here (e.g. a huge geometry timing out the pmtiles export) leaves the
191
+ // tabular data fully usable and must not disable the tabular explorer.
192
+ const NON_TABULAR_PARSING_ERROR_STEPS = ['pmtiles_export', 'geojson_export']
193
+
194
+ /**
195
+ * Whether the resource's parsing error prevents reading its tabular data.
196
+ * Only errors from the tabular parsing itself are fatal; downstream geo/format
197
+ * export failures are not.
198
+ */
199
+ export function hasTabularParsingError(resource: Resource): boolean {
200
+ const step = getParsingErrorStep(resource)
201
+ return step !== null && !NON_TABULAR_PARSING_ERROR_STEPS.includes(step)
202
+ }
203
+
204
+ type CorsStatus = 'allowed' | 'blocked' | 'unknown'
205
+
206
+ export const getResourceCorsStatus = (resource: Resource): CorsStatus => {
207
+ const extras = resource.extras
208
+ if (!extras || !('check:cors:allow-origin' in extras)) {
209
+ return 'unknown'
210
+ }
211
+
212
+ const allowOrigin = extras['check:cors:allow-origin'] as string | undefined
213
+ const rawMethods = extras['check:cors:allow-methods'] as string | undefined
214
+
215
+ // Check if allow-origin is '*' or contains one of our trusted domains
216
+ const config = useComponentsConfig()
217
+ const trustedDomains = config.trustedDomains ?? []
218
+ const hasPublicCors = allowOrigin === '*'
219
+ const hasSpecificCors = allowOrigin
220
+ ? trustedDomains.some((domain) => {
221
+ try {
222
+ const hostname = new URL(allowOrigin).hostname
223
+ return hostname === domain || hostname.endsWith(`.${domain}`)
224
+ }
225
+ catch {
226
+ return false
227
+ }
228
+ })
229
+ : false
230
+
231
+ const isOriginAllowed = hasPublicCors || hasSpecificCors
232
+
233
+ // Ensure GET method is allowed
234
+ const allowedMethods = rawMethods
235
+ ? rawMethods.split(',').map(m => m.trim().toUpperCase())
236
+ : []
237
+ const supportsGet = allowedMethods.length === 0 || allowedMethods.includes('GET')
238
+
239
+ return isOriginAllowed && supportsGet ? 'allowed' : 'blocked'
240
+ }
@@ -0,0 +1,171 @@
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
+ }
46
+ }
47
+
48
+ export function resolveColumnType(colInfo: { python_type: string, format?: string }, isCategorical: boolean): ColumnType {
49
+ if (['int', 'float'].includes(colInfo.python_type)) return 'number'
50
+ if (colInfo.format === 'year') return 'date'
51
+ if (['date', 'datetime'].includes(colInfo.python_type)) return 'date'
52
+ if (colInfo.python_type === 'bool') return 'boolean'
53
+ if (isCategorical) return 'categorical'
54
+ return 'text'
55
+ }
56
+
57
+ // csv-detective formats that carry no semantic meaning beyond the generic
58
+ // display type already resolved by `ColumnType` — these fall back to
59
+ // `buildTypeConfig` (Texte / Nombre / Date / Booléen) in the header.
60
+ export const GENERIC_FORMATS = new Set(['string', 'int', 'float', 'date', 'bool', 'booleen'])
61
+
62
+ // Semantic types detected by csv-detective (see ../../../csv-detective/csv_detective/formats).
63
+ // The list is intentionally exhaustive, but unknown formats are tolerated:
64
+ // `getColumnDisplay` humanizes any format missing from this map.
65
+ export function buildFormatConfig(t: TranslationFunction): Record<string, TypeDisplay> {
66
+ return {
67
+ // Geo — territories & addresses
68
+ adresse: { icon: RiMapPin2Line, label: t('Adresse') },
69
+ commune: { icon: RiMapPin2Line, label: t('Commune') },
70
+ code_commune: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
71
+ code_commune_insee: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
72
+ departement: { icon: RiMapPin2Line, label: t('Département') },
73
+ code_departement: { icon: RiMapPin2Line, label: t('Code département') },
74
+ region: { icon: RiMapPin2Line, label: t('Région') },
75
+ code_region: { icon: RiMapPin2Line, label: t('Code région') },
76
+ code_epci: { icon: RiMapPin2Line, label: t('Code EPCI') },
77
+ code_postal: { icon: RiMapPin2Line, label: t('Code postal') },
78
+ insee_canton: { icon: RiMapPin2Line, label: t('Canton') },
79
+ code_fantoir: { icon: RiMapPin2Line, label: t('Code FANTOIR') },
80
+ id_rnb: { icon: RiMapPin2Line, label: t('Identifiant RNB') },
81
+ // Geo — countries
82
+ pays: { icon: RiGlobalLine, label: t('Pays') },
83
+ iso_country_code_alpha2: { icon: RiGlobalLine, label: t('Code pays (ISO α-2)') },
84
+ iso_country_code_alpha3: { icon: RiGlobalLine, label: t('Code pays (ISO α-3)') },
85
+ iso_country_code_numeric: { icon: RiGlobalLine, label: t('Code pays (ISO num.)') },
86
+ // Geo — coordinates
87
+ latitude_wgs: { icon: RiFocus3Line, label: t('Latitude') },
88
+ latitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Latitude') },
89
+ longitude_wgs: { icon: RiFocus3Line, label: t('Longitude') },
90
+ longitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Longitude') },
91
+ latitude_l93: { icon: RiFocus3Line, label: t('Latitude (Lambert 93)') },
92
+ longitude_l93: { icon: RiFocus3Line, label: t('Longitude (Lambert 93)') },
93
+ latlon_wgs: { icon: RiFocus3Line, label: t('Latitude / longitude') },
94
+ lonlat_wgs: { icon: RiFocus3Line, label: t('Longitude / latitude') },
95
+ // Identifiers
96
+ siren: { icon: RiFingerprintLine, label: t('SIREN') },
97
+ siret: { icon: RiFingerprintLine, label: t('SIRET') },
98
+ code_rna: { icon: RiFingerprintLine, label: t('Identifiant RNA') },
99
+ code_waldec: { icon: RiFingerprintLine, label: t('Identifiant WALDEC') },
100
+ code_import: { icon: RiFingerprintLine, label: t('Code Import') },
101
+ uai: { icon: RiFingerprintLine, label: t('UAI') },
102
+ insee_ape700: { icon: RiFingerprintLine, label: t('Code APE') },
103
+ code_csp_insee: { icon: RiFingerprintLine, label: t('Code CSP') },
104
+ uuid: { icon: RiFingerprintLine, label: t('UUID') },
105
+ mongo_object_id: { icon: RiFingerprintLine, label: t('Identifiant Mongo') },
106
+ // Contact
107
+ email: { icon: RiMailLine, label: t('Email') },
108
+ url: { icon: RiLinksLine, label: t('URL') },
109
+ tel_fr: { icon: RiPhoneLine, label: t('Téléphone') },
110
+ username: { icon: RiUserLine, label: t('Nom d\'utilisateur') },
111
+ // People
112
+ sexe: { icon: RiUserLine, label: t('Sexe') },
113
+ csp_insee: { icon: RiUserLine, label: t('Catégorie socio-pro.') },
114
+ // Quantities
115
+ money: { icon: RiMoneyEuroCircleLine, label: t('Montant') },
116
+ percent: { icon: RiPercentLine, label: t('Pourcentage') },
117
+ // Temporal
118
+ year: { icon: RiCalendarLine, label: t('Année') },
119
+ date_fr: { icon: RiCalendarLine, label: t('Date') },
120
+ jour_de_la_semaine: { icon: RiCalendarEventLine, label: t('Jour de la semaine') },
121
+ mois_de_lannee: { icon: RiCalendarEventLine, label: t('Mois de l\'année') },
122
+ datetime_naive: { icon: RiTimeLine, label: t('Date et heure') },
123
+ datetime_aware: { icon: RiTimeLine, label: t('Date et heure') },
124
+ datetime_rfc822: { icon: RiTimeLine, label: t('Date et heure') },
125
+ // Structured
126
+ json: { icon: RiBracesLine, label: t('JSON') },
127
+ geojson: { icon: RiBracesLine, label: t('GeoJSON') },
128
+ binary: { icon: RiCodeLine, label: t('Binaire') },
129
+ }
130
+ }
131
+
132
+ // Tolerant fallback for a semantic format we don't explicitly know about:
133
+ // "code_truc_machin" -> "Code truc machin".
134
+ export function humanizeFormat(format: string): string {
135
+ const spaced = format.replace(/_/g, ' ').trim()
136
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1)
137
+ }
138
+
139
+ export function useFormatTabular() {
140
+ const { locale } = useTranslation()
141
+
142
+ function formatNumber(value: unknown): string {
143
+ const num = Number(value)
144
+ if (Number.isNaN(num)) return String(value)
145
+ return num.toLocaleString(locale)
146
+ }
147
+
148
+ function formatCellDate(value: unknown): string {
149
+ if (value == null || value === '') return '–'
150
+ const d = new Date(String(value))
151
+ if (Number.isNaN(d.getTime())) return String(value)
152
+ return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d)
153
+ }
154
+
155
+ return { formatNumber, formatCellDate }
156
+ }
157
+
158
+ const TRUTHY_VALUES = ['true', '1', 'oui', 'yes']
159
+ const FALSY_VALUES = ['false', '0', 'non', 'no']
160
+
161
+ export function isTruthy(value: unknown): boolean {
162
+ if (typeof value === 'boolean') return value
163
+ if (typeof value === 'string') return TRUTHY_VALUES.includes(value.toLowerCase())
164
+ return Boolean(value)
165
+ }
166
+
167
+ export function isFalsy(value: unknown): boolean {
168
+ if (typeof value === 'boolean') return !value
169
+ if (typeof value === 'string') return FALSY_VALUES.includes(value.toLowerCase())
170
+ return !value
171
+ }
@@ -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
  }