@datagouv/components-next 1.0.2-dev.16 → 1.0.2-dev.160
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.
- package/README.md +7 -6
- package/assets/main.css +4 -0
- package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-BtQqFBTQ.js → Datafair.client-CTp0KeR2.js} +4 -4
- package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
- package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
- package/dist/ImagePreview.client-CpOf0A1Y.js +36 -0
- package/dist/JsonPreview.client-DL3IBzvJ.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
- package/dist/{MapContainer.client-DB6p6KF8.js → MapContainer.client-DwyIXqCs.js} +38 -39
- package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-Dr4VcIur.js → PdfPreview.client-BoR_Mszg.js} +823 -855
- package/dist/Pmtiles.client-BpMMDkeP.js +26289 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-Dnz4oSUe.js +58 -0
- package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
- package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
- package/dist/XmlPreview.client-C3Y-2Eij.js +34 -0
- package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +182 -146
- package/dist/components.css +2 -2
- package/dist/{index-D3rieddI.js → index-B8gyXZY_.js} +21832 -15810
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-BDNw7m8K.js} +185 -187
- package/dist/main-SY-oNn1W.js +73989 -0
- package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-CJXShyPB.js → vue3-xml-viewer.common-CBGfdux9.js} +16 -16
- package/package.json +50 -36
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +3 -2
- package/src/components/Chart/ChartViewer.vue +320 -0
- package/src/components/Chart/ChartViewerWrapper.vue +182 -0
- package/src/components/DataserviceCard.vue +9 -0
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +9 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/Form/Listbox.vue +101 -0
- package/src/components/Form/SearchableSelect.vue +2 -1
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/ObjectCardHeader.vue +11 -4
- package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
- package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
- package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
- package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
- package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
- package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
- package/src/components/OpenApiViewer/openapi.ts +150 -0
- package/src/components/OrganizationNameWithCertificate.vue +3 -2
- package/src/components/Pagination.vue +8 -5
- package/src/components/RadioInput.vue +7 -2
- package/src/components/ResourceAccordion/DataStructure.vue +11 -33
- package/src/components/ResourceAccordion/Downloads.vue +160 -0
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
- package/src/components/ResourceAccordion/Metadata.vue +1 -2
- package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -87
- package/src/components/ResourceAccordion/Pmtiles.client.vue +3 -1
- package/src/components/ResourceAccordion/Preview.vue +11 -11
- package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +19 -113
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- package/src/components/ResourceExplorer/ResourceExplorer.vue +22 -19
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +64 -148
- package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
- package/src/components/ReuseCard.vue +14 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +219 -117
- package/src/components/Search/SearchInput.vue +5 -4
- package/src/components/TabularExplorer/TabularCell.vue +51 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +1132 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +353 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
- package/src/components/TabularExplorer/types.ts +86 -0
- package/src/composables/useHasTabularData.ts +15 -1
- package/src/composables/useMetrics.ts +1 -1
- package/src/composables/useResourceCapabilities.ts +28 -15
- package/src/composables/useSearchFilter.ts +120 -0
- package/src/composables/useStableQueryParams.ts +38 -6
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/config.ts +21 -3
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +20 -42
- package/src/functions/api.types.ts +1 -0
- package/src/functions/charts.ts +95 -0
- package/src/functions/datasets.ts +0 -17
- package/src/functions/dates.ts +3 -2
- package/src/functions/metrics.ts +6 -4
- package/src/functions/resources.ts +117 -1
- package/src/functions/tabular.ts +171 -0
- package/src/functions/tabularApi.ts +96 -11
- package/src/main.ts +107 -14
- package/src/types/dataservices.ts +17 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/pages.ts +0 -5
- package/src/types/posts.ts +2 -2
- package/src/types/reports.ts +5 -1
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +63 -1
- package/src/types/site.ts +5 -3
- package/src/types/ui.ts +2 -0
- package/src/types/users.ts +2 -1
- package/src/types/visualizations.ts +93 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/JsonPreview.client-DojrmGyU.js +0 -72
- package/dist/Pmtiles.client-DbMjfK00.js +0 -25185
- package/dist/Swagger.client-Db_xYKJv.js +0 -4
- package/dist/XmlPreview.client-CHxSFhd7.js +0 -64
- package/dist/main-D70Xt4a7.js +0 -105854
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -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,97 @@ 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
|
+
const IMAGE_PREVIEW_FORMATS = ['png', 'jpg', 'jpeg', 'jpe', 'gif', 'webp'] as const
|
|
171
|
+
|
|
172
|
+
export function isImagePreviewFormat(format: string | undefined | null): boolean {
|
|
173
|
+
if (!format) return false
|
|
174
|
+
return IMAGE_PREVIEW_FORMATS.includes(format.toLowerCase() as typeof IMAGE_PREVIEW_FORMATS[number])
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Parsing errors are stored as `<step>:<message>`, where `<step>` names the
|
|
178
|
+
// analysis step that failed (e.g. `pmtiles_export`, `geojson_export`).
|
|
179
|
+
|
|
180
|
+
/** The analysis step that failed, or null when there is no parsing error. */
|
|
181
|
+
export function getParsingErrorStep(resource: Resource): string | null {
|
|
182
|
+
const error = resource.extras['analysis:parsing:error']
|
|
183
|
+
if (typeof error !== 'string' || !error) return null
|
|
184
|
+
const [step] = error.split(':', 1)
|
|
185
|
+
return step ?? null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The parsing error message without its `<step>:` prefix, or null when there is none. */
|
|
189
|
+
export function getParsingErrorMessage(resource: Resource): string | null {
|
|
190
|
+
const error = resource.extras['analysis:parsing:error']
|
|
191
|
+
if (typeof error !== 'string' || !error) return null
|
|
192
|
+
const colon = error.indexOf(':')
|
|
193
|
+
return colon === -1 ? error : error.slice(colon + 1)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Geo/format export steps run *after* the tabular table has been built, so a
|
|
197
|
+
// failure here (e.g. a huge geometry timing out the pmtiles export) leaves the
|
|
198
|
+
// tabular data fully usable and must not disable the tabular explorer.
|
|
199
|
+
const NON_TABULAR_PARSING_ERROR_STEPS = ['pmtiles_export', 'geojson_export']
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Whether the resource's parsing error prevents reading its tabular data.
|
|
203
|
+
* Only errors from the tabular parsing itself are fatal; downstream geo/format
|
|
204
|
+
* export failures are not.
|
|
205
|
+
*/
|
|
206
|
+
export function hasTabularParsingError(resource: Resource): boolean {
|
|
207
|
+
const step = getParsingErrorStep(resource)
|
|
208
|
+
return step !== null && !NON_TABULAR_PARSING_ERROR_STEPS.includes(step)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
type CorsStatus = 'allowed' | 'blocked' | 'unknown'
|
|
212
|
+
|
|
213
|
+
export const getResourceCorsStatus = (resource: Resource): CorsStatus => {
|
|
214
|
+
const extras = resource.extras
|
|
215
|
+
if (!extras || !('check:cors:allow-origin' in extras)) {
|
|
216
|
+
return 'unknown'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const allowOrigin = extras['check:cors:allow-origin'] as string | undefined
|
|
220
|
+
const rawMethods = extras['check:cors:allow-methods'] as string | undefined
|
|
221
|
+
|
|
222
|
+
// Check if allow-origin is '*' or contains one of our trusted domains
|
|
223
|
+
const config = useComponentsConfig()
|
|
224
|
+
const trustedDomains = config.trustedDomains ?? []
|
|
225
|
+
const hasPublicCors = allowOrigin === '*'
|
|
226
|
+
const hasSpecificCors = allowOrigin
|
|
227
|
+
? trustedDomains.some((domain) => {
|
|
228
|
+
try {
|
|
229
|
+
const hostname = new URL(allowOrigin).hostname
|
|
230
|
+
return hostname === domain || hostname.endsWith(`.${domain}`)
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
: false
|
|
237
|
+
|
|
238
|
+
const isOriginAllowed = hasPublicCors || hasSpecificCors
|
|
239
|
+
|
|
240
|
+
// Ensure GET method is allowed
|
|
241
|
+
const allowedMethods = rawMethods
|
|
242
|
+
? rawMethods.split(',').map(m => m.trim().toUpperCase())
|
|
243
|
+
: []
|
|
244
|
+
const supportsGet = allowedMethods.length === 0 || allowedMethods.includes('GET')
|
|
245
|
+
|
|
246
|
+
return isOriginAllowed && supportsGet ? 'allowed' : 'blocked'
|
|
247
|
+
}
|
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
}
|