@datagouv/components-next 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/{Control-ZFh5ta_U.js → Control-b-43UT2B.js} +10 -10
- package/dist/{Datafair.client-8bXp6UeQ.js → Datafair.client-BrjISlaZ.js} +4 -4
- package/dist/{Event-DSQcW7OF.js → Event-py_AAwJY.js} +18 -18
- package/dist/{Image-BijNEG0p.js → Image-BHQYZWzH.js} +23 -23
- package/dist/{JsonPreview.client-BxuoPK_w.js → JsonPreview.client-DivFAnbN.js} +5 -5
- package/dist/{Map-BUtPf5GN.js → Map-CtP7leHl.js} +1313 -1173
- package/dist/{MapContainer.client-CTz0wmJG.js → MapContainer.client-Cq8tNLZZ.js} +17 -17
- package/dist/{OSM-D4MTdBtk.js → OSM-gdDSIsDA.js} +15 -23
- package/dist/{PdfPreview.client-DpVreUSl.js → PdfPreview.client-BNBgLsK2.js} +6 -10
- package/dist/Pmtiles.client-CQi5Rw8W.js +26123 -0
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-stmU5qEB.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js} +2 -2
- package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-hG-FhNqy.js} +2 -2
- package/dist/{Tile-Dcl7oIVu.js → Tile-D6rFonpT.js} +163 -166
- package/dist/{TileImage-BJeHipMX.js → TileImage-cfrEgz5y.js} +131 -119
- package/dist/{View-xp_P_OHw.js → View-vp7vDb9q.js} +169 -119
- package/dist/{XmlPreview.client-DAOs89cR.js → XmlPreview.client-XCsNvnsZ.js} +5 -5
- package/dist/{common-BjQlan3k.js → common-C8efyyuc.js} +49 -49
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +166 -157
- package/dist/components.css +2 -2
- package/dist/{index-JqjPja1u.js → index-CXUamc8Z.js} +6382 -6057
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-BRKv9Ksv.js} +162 -164
- package/dist/{main-D4WQMky0.js → main-DKu5SOAl.js} +48480 -97188
- package/dist/{proj-CsNo9yH1.js → proj-CsjVeojN.js} +322 -306
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-A0fLnBZr.js → tilecoord-CPcQJnrF.js} +270 -261
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-BGsoNyZe.js → vue3-xml-viewer.common-BIaEIFYG.js} +1 -1
- package/package.json +42 -35
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +3 -0
- package/src/components/DatasetCard.vue +6 -4
- package/src/components/ObjectCardHeader.vue +1 -1
- 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/Pmtiles.client.vue +3 -1
- package/src/components/ResourceAccordion/ResourceAccordion.vue +7 -104
- package/src/components/ResourceExplorer/ResourceExplorer.vue +10 -64
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +30 -136
- package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
- package/src/components/Search/GlobalSearch.vue +27 -6
- package/src/components/TabularExplorer/TabularCellPopover.vue +2 -2
- package/src/components/TabularExplorer/TabularExplorer.vue +404 -163
- package/src/components/TabularExplorer/TabularFilterContent.vue +15 -13
- package/src/components/TabularExplorer/TabularFilterPopover.vue +21 -26
- package/src/components/TabularExplorer/types.ts +9 -9
- package/src/composables/useHasTabularData.ts +7 -0
- package/src/composables/useResourceCapabilities.ts +1 -1
- package/src/composables/useSearchFilter.ts +7 -5
- package/src/composables/useStableQueryParams.ts +7 -3
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +13 -7
- package/src/functions/resources.ts +5 -0
- package/src/functions/tabular.ts +103 -1
- package/src/main.ts +12 -6
- package/src/types/resources.ts +0 -1
- package/src/types/search.ts +11 -0
- package/src/types/ui.ts +1 -1
- package/dist/Pmtiles.client-BwmHo3T8.js +0 -25185
|
@@ -265,91 +265,13 @@
|
|
|
265
265
|
<div
|
|
266
266
|
v-if="tab.key === 'downloads'"
|
|
267
267
|
>
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
>
|
|
273
|
-
{{ t("URL d'origine") }}
|
|
274
|
-
</dt>
|
|
275
|
-
<dt
|
|
276
|
-
v-else
|
|
277
|
-
class="font-bold fr-text--sm fr-mb-0"
|
|
278
|
-
>
|
|
279
|
-
{{ t('Format original') }}
|
|
280
|
-
</dt>
|
|
281
|
-
<dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
|
|
282
|
-
<span v-if="resource.format === 'url'">
|
|
283
|
-
<a
|
|
284
|
-
:href="resource.latest"
|
|
285
|
-
class="fr-link no-icon-after"
|
|
286
|
-
rel="ugc nofollow noopener"
|
|
287
|
-
target="_blank"
|
|
288
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
|
|
289
|
-
>
|
|
290
|
-
<component
|
|
291
|
-
:is="config.textClamp"
|
|
292
|
-
v-if="config && config.textClamp"
|
|
293
|
-
:auto-resize="true"
|
|
294
|
-
:max-lines="1"
|
|
295
|
-
:text="resource.url"
|
|
296
|
-
>
|
|
297
|
-
<template #after>
|
|
298
|
-
<span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm" />
|
|
299
|
-
</template>
|
|
300
|
-
</component>
|
|
301
|
-
</a>
|
|
302
|
-
</span>
|
|
303
|
-
<span v-else>
|
|
304
|
-
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
305
|
-
<a
|
|
306
|
-
:href="resource.latest"
|
|
307
|
-
class="fr-link"
|
|
308
|
-
rel="ugc nofollow noopener"
|
|
309
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
|
|
310
|
-
>
|
|
311
|
-
<span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
|
|
312
|
-
</a>
|
|
313
|
-
</span>
|
|
314
|
-
<CopyButton
|
|
315
|
-
:label="t('Copier le lien')"
|
|
316
|
-
:copied-label="t('Lien copié !')"
|
|
317
|
-
:text="resource.latest"
|
|
318
|
-
class="relative"
|
|
319
|
-
/>
|
|
320
|
-
</dd>
|
|
321
|
-
<template v-if="generatedFormats.length">
|
|
322
|
-
<dt class="font-bold fr-text--sm fr-mb-0">
|
|
323
|
-
{{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
|
|
324
|
-
</dt>
|
|
325
|
-
<dd
|
|
326
|
-
v-for="generatedFormat in generatedFormats"
|
|
327
|
-
:key="generatedFormat.format"
|
|
328
|
-
class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
|
|
329
|
-
>
|
|
330
|
-
<span>
|
|
331
|
-
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
332
|
-
<a
|
|
333
|
-
:href="generatedFormat.url"
|
|
334
|
-
class="fr-link"
|
|
335
|
-
rel="ugc nofollow noopener"
|
|
336
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
|
|
337
|
-
>
|
|
338
|
-
<span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
|
|
339
|
-
</a>
|
|
340
|
-
</span>
|
|
341
|
-
<CopyButton
|
|
342
|
-
:label="t('Copier le lien')"
|
|
343
|
-
:copied-label="t('Lien copié !')"
|
|
344
|
-
:text="generatedFormat.url"
|
|
345
|
-
class="relative"
|
|
346
|
-
/>
|
|
347
|
-
</dd>
|
|
348
|
-
</template>
|
|
349
|
-
</dl>
|
|
268
|
+
<Downloads
|
|
269
|
+
:resource="resource"
|
|
270
|
+
:dataset="dataset"
|
|
271
|
+
/>
|
|
350
272
|
</div>
|
|
351
273
|
<div
|
|
352
|
-
v-if="tab.key === '
|
|
274
|
+
v-if="tab.key === 'api'"
|
|
353
275
|
>
|
|
354
276
|
<div class="fr-mb-4w">
|
|
355
277
|
<p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
|
|
@@ -396,11 +318,11 @@ import SchemaBadge from './SchemaBadge.vue'
|
|
|
396
318
|
import ResourceIcon from './ResourceIcon.vue'
|
|
397
319
|
import EditButton from './EditButton.vue'
|
|
398
320
|
import DataStructure from './DataStructure.vue'
|
|
321
|
+
import Downloads from './Downloads.vue'
|
|
399
322
|
import Preview from './Preview.vue'
|
|
400
323
|
import { isOrganizationCertified } from '../../functions/organizations'
|
|
401
324
|
import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
|
|
402
325
|
|
|
403
|
-
const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
|
|
404
326
|
const URL_FORMATS = ['url', 'doi', 'www:link', ' www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
|
|
405
327
|
|
|
406
328
|
const props = withDefaults(defineProps<{
|
|
@@ -460,24 +382,6 @@ const ogcService = computed(() => detectOgcService(props.resource))
|
|
|
460
382
|
|
|
461
383
|
const ogcWms = computed(() => ogcService.value === 'wms')
|
|
462
384
|
|
|
463
|
-
const generatedFormats = computed(() => {
|
|
464
|
-
const formats = GENERATED_FORMATS
|
|
465
|
-
.filter(format => `analysis:parsing:${format}_url` in props.resource.extras)
|
|
466
|
-
.map(format => ({
|
|
467
|
-
url: props.resource.extras[`analysis:parsing:${format}_url`] as string,
|
|
468
|
-
size: props.resource.extras[`analysis:parsing:${format}_size`] as number | undefined,
|
|
469
|
-
format: format,
|
|
470
|
-
}))
|
|
471
|
-
if ('analysis:parsing:parsing_table' in props.resource.extras) {
|
|
472
|
-
formats.push({
|
|
473
|
-
url: `${config.tabularApiUrl}/api/resources/${props.resource.id}/data/json/`,
|
|
474
|
-
size: undefined,
|
|
475
|
-
format: 'json',
|
|
476
|
-
})
|
|
477
|
-
}
|
|
478
|
-
return formats
|
|
479
|
-
})
|
|
480
|
-
|
|
481
385
|
const open = ref(props.expandedOnMount)
|
|
482
386
|
const toggle = () => {
|
|
483
387
|
open.value = !open.value
|
|
@@ -513,7 +417,7 @@ const tabsOptions = computed(() => {
|
|
|
513
417
|
options.push({ key: 'downloads', label: t('Téléchargements') })
|
|
514
418
|
|
|
515
419
|
if (hasTabularData.value) {
|
|
516
|
-
options.push({ key: '
|
|
420
|
+
options.push({ key: 'api', label: t('API') })
|
|
517
421
|
}
|
|
518
422
|
|
|
519
423
|
return options
|
|
@@ -540,7 +444,6 @@ const communityResource = computed<CommunityResource | null>(() => {
|
|
|
540
444
|
const owner = computed(() => communityResource.value ? getOwnerName(communityResource.value) : null)
|
|
541
445
|
|
|
542
446
|
const lastUpdate = props.resource.last_modified
|
|
543
|
-
const conversionsLastUpdate = computed(() => formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined))
|
|
544
447
|
const availabilityChecked = props.resource.extras && 'check:available' in props.resource.extras
|
|
545
448
|
const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
546
449
|
|
|
@@ -2,24 +2,13 @@
|
|
|
2
2
|
<div v-if="allResources.length || hasAnyResources">
|
|
3
3
|
<div class="flex gap-6">
|
|
4
4
|
<div class="flex-1 min-w-0">
|
|
5
|
-
<div
|
|
6
|
-
v-if="dataset.resources.total > 1"
|
|
7
|
-
class="md:hidden flex justify-end mb-3"
|
|
8
|
-
>
|
|
9
|
-
<BrandedButton
|
|
10
|
-
size="xs"
|
|
11
|
-
color="secondary"
|
|
12
|
-
:icon="RiListUnordered"
|
|
13
|
-
@click="mobileSidebarOpen = true"
|
|
14
|
-
>
|
|
15
|
-
{{ t('Ressources ({count})', { count: dataset.resources.total }) }}
|
|
16
|
-
</BrandedButton>
|
|
17
|
-
</div>
|
|
18
5
|
<ResourceExplorerViewer
|
|
19
6
|
v-if="selectedResource && allResources.length"
|
|
20
7
|
:key="selectedResource.id"
|
|
21
8
|
:dataset
|
|
22
9
|
:resource="selectedResource"
|
|
10
|
+
:resources="flatResources"
|
|
11
|
+
@select="selectResource"
|
|
23
12
|
/>
|
|
24
13
|
<div
|
|
25
14
|
v-else-if="search"
|
|
@@ -56,25 +45,6 @@
|
|
|
56
45
|
/>
|
|
57
46
|
</div>
|
|
58
47
|
</div>
|
|
59
|
-
|
|
60
|
-
<!-- Mobile sidebar panel -->
|
|
61
|
-
<dialog
|
|
62
|
-
ref="mobileSidebarDialog"
|
|
63
|
-
class="mobile-sidebar md:hidden fixed inset-0 m-0 ml-auto p-0 h-dvh max-h-dvh w-80 max-w-[85vw] bg-white shadow-lg overflow-y-auto overscroll-contain backdrop:bg-black/30"
|
|
64
|
-
@close="mobileSidebarOpen = false"
|
|
65
|
-
@click.self="closeMobileSidebar"
|
|
66
|
-
>
|
|
67
|
-
<ResourceExplorerSidebar
|
|
68
|
-
:resources="allResources"
|
|
69
|
-
:selected-resource-id="selectedResource?.id ?? null"
|
|
70
|
-
:collapsed="false"
|
|
71
|
-
:search
|
|
72
|
-
@select="selectResource"
|
|
73
|
-
@load-more="loadMore"
|
|
74
|
-
@update:collapsed="closeMobileSidebar"
|
|
75
|
-
@update:search="updateSearch($event)"
|
|
76
|
-
/>
|
|
77
|
-
</dialog>
|
|
78
48
|
</div>
|
|
79
49
|
<div
|
|
80
50
|
v-else
|
|
@@ -101,13 +71,12 @@ import { useComponentsConfig } from '../../config'
|
|
|
101
71
|
import { useTranslation } from '../../composables/useTranslation'
|
|
102
72
|
import { useDebouncedRef } from '../../composables/useDebouncedRef'
|
|
103
73
|
import { useFetch } from '../../functions/api'
|
|
104
|
-
import { RESOURCE_TYPE } from '../../functions/resources'
|
|
74
|
+
import { RESOURCE_EXPLORER_PAGE_SIZE, RESOURCE_TYPE } from '../../functions/resources'
|
|
105
75
|
import type { PaginatedArray } from '../../types/api'
|
|
106
76
|
import type { DatasetV2 } from '../../types/datasets'
|
|
107
77
|
import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
|
|
108
78
|
import ResourceExplorerSidebar from './ResourceExplorerSidebar.vue'
|
|
109
79
|
import ResourceExplorerViewer from './ResourceExplorerViewer.vue'
|
|
110
|
-
import { RiListUnordered } from '@remixicon/vue'
|
|
111
80
|
import BrandedButton from '../BrandedButton.vue'
|
|
112
81
|
|
|
113
82
|
const props = withDefaults(defineProps<{
|
|
@@ -125,7 +94,6 @@ const sidebarCollapsed = ref(false)
|
|
|
125
94
|
const search = ref('')
|
|
126
95
|
const { debounced: searchDebounced, flush } = useDebouncedRef(search, config.searchDebounce ?? 300)
|
|
127
96
|
|
|
128
|
-
const PAGE_SIZE = 10
|
|
129
97
|
const url = computed(() => `/api/2/datasets/${props.dataset.id}/resources/`)
|
|
130
98
|
|
|
131
99
|
// Resource ID from URL query
|
|
@@ -134,32 +102,32 @@ const resourceIdQuery = useRouteQuery<string | undefined>('resource_id')
|
|
|
134
102
|
// Fetch resources for each type
|
|
135
103
|
const mainParams = computed(() => ({
|
|
136
104
|
type: 'main' as const,
|
|
137
|
-
page_size:
|
|
105
|
+
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
138
106
|
q: searchDebounced.value || undefined,
|
|
139
107
|
}))
|
|
140
108
|
const documentationParams = computed(() => ({
|
|
141
109
|
type: 'documentation' as const,
|
|
142
|
-
page_size:
|
|
110
|
+
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
143
111
|
q: searchDebounced.value || undefined,
|
|
144
112
|
}))
|
|
145
113
|
const updateParams = computed(() => ({
|
|
146
114
|
type: 'update' as const,
|
|
147
|
-
page_size:
|
|
115
|
+
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
148
116
|
q: searchDebounced.value || undefined,
|
|
149
117
|
}))
|
|
150
118
|
const apiParams = computed(() => ({
|
|
151
119
|
type: 'api' as const,
|
|
152
|
-
page_size:
|
|
120
|
+
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
153
121
|
q: searchDebounced.value || undefined,
|
|
154
122
|
}))
|
|
155
123
|
const codeParams = computed(() => ({
|
|
156
124
|
type: 'code' as const,
|
|
157
|
-
page_size:
|
|
125
|
+
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
158
126
|
q: searchDebounced.value || undefined,
|
|
159
127
|
}))
|
|
160
128
|
const otherParams = computed(() => ({
|
|
161
129
|
type: 'other' as const,
|
|
162
|
-
page_size:
|
|
130
|
+
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
163
131
|
q: searchDebounced.value || undefined,
|
|
164
132
|
}))
|
|
165
133
|
|
|
@@ -200,7 +168,7 @@ const loadMoreType = ref<ResourceType>('main')
|
|
|
200
168
|
const loadMorePage = ref(1)
|
|
201
169
|
const loadMoreParams = computed(() => ({
|
|
202
170
|
type: loadMoreType.value,
|
|
203
|
-
page_size:
|
|
171
|
+
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
204
172
|
page: loadMorePage.value,
|
|
205
173
|
q: searchDebounced.value || undefined,
|
|
206
174
|
}))
|
|
@@ -246,21 +214,6 @@ const flatResources = computed(() =>
|
|
|
246
214
|
|
|
247
215
|
// Fetch resource by ID if specified in URL (for SSR)
|
|
248
216
|
const initialResourceId = resourceIdQuery.value
|
|
249
|
-
const mobileSidebarOpen = ref(false)
|
|
250
|
-
const mobileSidebarDialog = ref<HTMLDialogElement | null>(null)
|
|
251
|
-
|
|
252
|
-
watch(mobileSidebarOpen, (open) => {
|
|
253
|
-
if (open) {
|
|
254
|
-
mobileSidebarDialog.value?.showModal()
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
mobileSidebarDialog.value?.close()
|
|
258
|
-
}
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
function closeMobileSidebar() {
|
|
262
|
-
mobileSidebarOpen.value = false
|
|
263
|
-
}
|
|
264
217
|
const { data: fetchedResource } = initialResourceId
|
|
265
218
|
? await useFetch<Resource>(`/api/1/datasets/${props.dataset.id}/resources/${initialResourceId}/`)
|
|
266
219
|
: { data: ref(null) }
|
|
@@ -288,7 +241,6 @@ function updateSearch(newSearch: string) {
|
|
|
288
241
|
|
|
289
242
|
const selectResource = (resource: Resource) => {
|
|
290
243
|
selectedResource.value = resource
|
|
291
|
-
mobileSidebarOpen.value = false
|
|
292
244
|
router.replace({
|
|
293
245
|
query: { ...router.currentRoute.value.query, resource_id: resource.id },
|
|
294
246
|
})
|
|
@@ -303,9 +255,3 @@ watch(flatResources, () => {
|
|
|
303
255
|
}
|
|
304
256
|
})
|
|
305
257
|
</script>
|
|
306
|
-
|
|
307
|
-
<style>
|
|
308
|
-
html:has(dialog.mobile-sidebar[open]) {
|
|
309
|
-
overflow: hidden;
|
|
310
|
-
}
|
|
311
|
-
</style>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="border border-gray-default">
|
|
3
3
|
<header class="p-4 flex flex-wrap md:flex-nowrap gap-4 items-center justify-between">
|
|
4
4
|
<div>
|
|
5
|
-
<div class="flex items-center mb-1">
|
|
5
|
+
<div class="flex items-center gap-1 mb-1">
|
|
6
6
|
<h3 class="m-0 flex items-baseline text-base font-bold leading-tight">
|
|
7
7
|
<ResourceIcon
|
|
8
8
|
:resource
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
/>
|
|
11
11
|
<span class="line-clamp-2">{{ resource.title || t('Fichier sans nom') }}</span>
|
|
12
12
|
</h3>
|
|
13
|
+
<ResourceSelector
|
|
14
|
+
v-if="resources && resources.length > 1"
|
|
15
|
+
:resources
|
|
16
|
+
:selected-id="resource.id"
|
|
17
|
+
@select="emit('select', $event)"
|
|
18
|
+
/>
|
|
13
19
|
<CopyButton
|
|
14
20
|
:label="t('Copier le lien')"
|
|
15
21
|
:copied-label="t('Lien copié !')"
|
|
@@ -139,9 +145,9 @@
|
|
|
139
145
|
v-else-if="hasOpenAPIPreview"
|
|
140
146
|
:url="resource.extras['apidocUrl'] as string"
|
|
141
147
|
/>
|
|
142
|
-
<
|
|
148
|
+
<TabularExplorer
|
|
143
149
|
v-else-if="hasTabularData"
|
|
144
|
-
:resource="resource"
|
|
150
|
+
:resource-id="resource.id"
|
|
145
151
|
/>
|
|
146
152
|
<PreviewUnavailable v-else>
|
|
147
153
|
<!-- "File too large to download" is the only analysis:error value from hydra for now -->
|
|
@@ -174,135 +180,20 @@
|
|
|
174
180
|
<Metadata :resource />
|
|
175
181
|
</div>
|
|
176
182
|
<div v-if="tab.key === 'downloads'">
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
>
|
|
182
|
-
{{ t("URL d'origine") }}
|
|
183
|
-
</dt>
|
|
184
|
-
<dt
|
|
185
|
-
v-else
|
|
186
|
-
class="font-bold fr-text--sm fr-mb-0"
|
|
187
|
-
>
|
|
188
|
-
{{ t('Format original') }}
|
|
189
|
-
</dt>
|
|
190
|
-
<dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
|
|
191
|
-
<span
|
|
192
|
-
v-if="resource.format === 'url'"
|
|
193
|
-
class="inline-flex items-center max-w-full"
|
|
194
|
-
>
|
|
195
|
-
<a
|
|
196
|
-
:href="resource.latest"
|
|
197
|
-
class="fr-link no-icon-after truncate"
|
|
198
|
-
rel="ugc nofollow noopener"
|
|
199
|
-
target="_blank"
|
|
200
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
|
|
201
|
-
>
|
|
202
|
-
{{ resource.url }}
|
|
203
|
-
</a>
|
|
204
|
-
<span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm shrink-0" />
|
|
205
|
-
</span>
|
|
206
|
-
<span v-else>
|
|
207
|
-
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
208
|
-
<a
|
|
209
|
-
:href="resource.latest"
|
|
210
|
-
class="fr-link"
|
|
211
|
-
rel="ugc nofollow noopener"
|
|
212
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
|
|
213
|
-
>
|
|
214
|
-
<span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
|
|
215
|
-
</a>
|
|
216
|
-
</span>
|
|
217
|
-
<CopyButton
|
|
218
|
-
:label="t('Copier le lien')"
|
|
219
|
-
:copied-label="t('Lien copié !')"
|
|
220
|
-
:text="resource.latest"
|
|
221
|
-
class="relative"
|
|
222
|
-
/>
|
|
223
|
-
</dd>
|
|
224
|
-
<template v-if="generatedFormats.length">
|
|
225
|
-
<dt class="font-bold fr-text--sm fr-mb-0">
|
|
226
|
-
{{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
|
|
227
|
-
</dt>
|
|
228
|
-
<dd
|
|
229
|
-
v-for="generatedFormat in generatedFormats"
|
|
230
|
-
:key="generatedFormat.format"
|
|
231
|
-
class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
|
|
232
|
-
>
|
|
233
|
-
<span>
|
|
234
|
-
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
235
|
-
<a
|
|
236
|
-
:href="generatedFormat.url"
|
|
237
|
-
class="fr-link"
|
|
238
|
-
rel="ugc nofollow noopener"
|
|
239
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
|
|
240
|
-
>
|
|
241
|
-
<span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
|
|
242
|
-
</a>
|
|
243
|
-
</span>
|
|
244
|
-
<CopyButton
|
|
245
|
-
:label="t('Copier le lien')"
|
|
246
|
-
:copied-label="t('Lien copié !')"
|
|
247
|
-
:text="generatedFormat.url"
|
|
248
|
-
class="relative"
|
|
249
|
-
/>
|
|
250
|
-
</dd>
|
|
251
|
-
</template>
|
|
252
|
-
<template v-if="wfsFormats.length">
|
|
253
|
-
<dt class="font-bold fr-text--sm fr-mb-0">
|
|
254
|
-
<div class="flex gap-1 items-center">
|
|
255
|
-
{{ t('Formats exportés depuis le service WFS') }}
|
|
256
|
-
<span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
|
|
257
|
-
<Tooltip>
|
|
258
|
-
<RiInformationLine
|
|
259
|
-
class="flex-none size-4"
|
|
260
|
-
:aria-label="t(`Le lien de téléchargement interroge directement le flux WFS distant. Le nombre de features téléchargées peut être limité.`)"
|
|
261
|
-
aria-hidden="true"
|
|
262
|
-
/>
|
|
263
|
-
<template #tooltip>
|
|
264
|
-
<p class="text-sm font-normal mb-0">
|
|
265
|
-
{{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
|
|
266
|
-
</p>
|
|
267
|
-
<p class="text-sm font-normal mb-0">
|
|
268
|
-
{{ t(`Le nombre de features téléchargées peut être limité.`) }}
|
|
269
|
-
</p>
|
|
270
|
-
</template>
|
|
271
|
-
</Tooltip>
|
|
272
|
-
</div>
|
|
273
|
-
</dt>
|
|
274
|
-
<dd
|
|
275
|
-
v-for="wfsFormat in wfsFormats"
|
|
276
|
-
:key="wfsFormat.format"
|
|
277
|
-
class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
|
|
278
|
-
>
|
|
279
|
-
<span>
|
|
280
|
-
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
281
|
-
<a
|
|
282
|
-
:href="wfsFormat.url"
|
|
283
|
-
class="fr-link"
|
|
284
|
-
rel="ugc nofollow noopener"
|
|
285
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
|
|
286
|
-
>
|
|
287
|
-
<span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
|
|
288
|
-
</a>
|
|
289
|
-
</span>
|
|
290
|
-
<CopyButton
|
|
291
|
-
:label="t('Copier le lien')"
|
|
292
|
-
:copied-label="t('Lien copié !')"
|
|
293
|
-
:text="wfsFormat.url"
|
|
294
|
-
class="relative"
|
|
295
|
-
/>
|
|
296
|
-
</dd>
|
|
297
|
-
</template>
|
|
298
|
-
</dl>
|
|
183
|
+
<Downloads
|
|
184
|
+
:resource="resource"
|
|
185
|
+
:dataset="dataset"
|
|
186
|
+
/>
|
|
299
187
|
</div>
|
|
300
|
-
<div v-if="tab.key === '
|
|
188
|
+
<div v-if="tab.key === 'api'">
|
|
301
189
|
<div class="fr-mb-4w">
|
|
302
190
|
<p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
|
|
303
191
|
<p>{{ t("- Si le fichier est modifié, l'API sera mise à jour et sa structure pourra changer.") }}</p>
|
|
304
192
|
<p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
|
|
305
193
|
<p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
|
|
194
|
+
<p v-if="config.tabularApiUrl">
|
|
195
|
+
{{ t("L'URL de base de l'API est {url}", { url: config.tabularApiUrl }) }}
|
|
196
|
+
</p>
|
|
306
197
|
</div>
|
|
307
198
|
<OpenApiViewer
|
|
308
199
|
v-if="hasTabularData"
|
|
@@ -318,7 +209,7 @@
|
|
|
318
209
|
|
|
319
210
|
<script setup lang="ts">
|
|
320
211
|
import { computed, defineAsyncComponent } from 'vue'
|
|
321
|
-
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine,
|
|
212
|
+
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
|
|
322
213
|
import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
|
|
323
214
|
import { toast } from 'vue-sonner'
|
|
324
215
|
import BrandedButton from '../BrandedButton.vue'
|
|
@@ -331,11 +222,12 @@ import TabList from '../Tabs/TabList.vue'
|
|
|
331
222
|
import Tab from '../Tabs/Tab.vue'
|
|
332
223
|
import TabPanels from '../Tabs/TabPanels.vue'
|
|
333
224
|
import TabPanel from '../Tabs/TabPanel.vue'
|
|
334
|
-
import
|
|
335
|
-
import Preview from '../ResourceAccordion/Preview.vue'
|
|
225
|
+
import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
|
|
336
226
|
import DataStructure from '../ResourceAccordion/DataStructure.vue'
|
|
227
|
+
import Downloads from '../ResourceAccordion/Downloads.vue'
|
|
337
228
|
import Metadata from '../ResourceAccordion/Metadata.vue'
|
|
338
229
|
import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
|
|
230
|
+
import ResourceSelector from './ResourceSelector.vue'
|
|
339
231
|
import { filesize, summarize } from '../../functions/helpers'
|
|
340
232
|
import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
|
|
341
233
|
import { trackEvent } from '../../functions/matomo'
|
|
@@ -343,6 +235,7 @@ import { useComponentsConfig } from '../../config'
|
|
|
343
235
|
import { useFormatDate } from '../../functions/dates'
|
|
344
236
|
import { useTranslation } from '../../composables/useTranslation'
|
|
345
237
|
import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
|
|
238
|
+
import { provideTabularProfile } from '../../composables/useTabularProfile'
|
|
346
239
|
import type { Resource } from '../../types/resources'
|
|
347
240
|
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
348
241
|
|
|
@@ -368,6 +261,11 @@ const Pmtiles = defineAsyncComponent(() =>
|
|
|
368
261
|
const props = defineProps<{
|
|
369
262
|
dataset: Dataset | DatasetV2
|
|
370
263
|
resource: Resource
|
|
264
|
+
resources?: Resource[]
|
|
265
|
+
}>()
|
|
266
|
+
|
|
267
|
+
const emit = defineEmits<{
|
|
268
|
+
select: [resource: Resource]
|
|
371
269
|
}>()
|
|
372
270
|
|
|
373
271
|
const { t } = useTranslation()
|
|
@@ -381,13 +279,13 @@ const {
|
|
|
381
279
|
hasOpenAPIPreview,
|
|
382
280
|
ogcService,
|
|
383
281
|
ogcWms,
|
|
384
|
-
generatedFormats,
|
|
385
|
-
wfsFormats,
|
|
386
|
-
defaultWfsProjection,
|
|
387
282
|
isResourceUrl,
|
|
388
283
|
tabsOptions,
|
|
389
284
|
} = useResourceCapabilities(() => props.resource, () => props.dataset)
|
|
390
285
|
|
|
286
|
+
// Share the tabular profile fetch between TabularExplorer and DataStructure tabs.
|
|
287
|
+
await provideTabularProfile(() => props.resource.id)
|
|
288
|
+
|
|
391
289
|
const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
392
290
|
const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
|
|
393
291
|
|
|
@@ -401,10 +299,6 @@ const downloadButtonTitle = computed(() => {
|
|
|
401
299
|
return t('Télécharger le fichier en {format}', { format: format.value })
|
|
402
300
|
})
|
|
403
301
|
|
|
404
|
-
const conversionsLastUpdate = computed(() =>
|
|
405
|
-
formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
|
|
406
|
-
)
|
|
407
|
-
|
|
408
302
|
const copyResourceUrl = async () => {
|
|
409
303
|
try {
|
|
410
304
|
await navigator.clipboard.writeText(props.resource.url)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Popover
|
|
3
|
+
v-slot="{ open, close }"
|
|
4
|
+
class="relative inline-block"
|
|
5
|
+
>
|
|
6
|
+
<slot
|
|
7
|
+
name="trigger"
|
|
8
|
+
:open="open"
|
|
9
|
+
>
|
|
10
|
+
<PopoverButton
|
|
11
|
+
class="inline-flex items-center justify-center size-6 rounded text-gray-plain hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
|
|
12
|
+
:aria-label="t('Choisir une autre ressource')"
|
|
13
|
+
>
|
|
14
|
+
<RiArrowDownSLine
|
|
15
|
+
class="size-4"
|
|
16
|
+
:class="{ 'rotate-180': open }"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
/>
|
|
19
|
+
</PopoverButton>
|
|
20
|
+
</slot>
|
|
21
|
+
<PopoverPanel
|
|
22
|
+
:class="searchable
|
|
23
|
+
? 'absolute left-0 top-full z-50 mt-2 w-96 max-w-[calc(100vw-2rem)] bg-white border border-gray-default rounded shadow-lg p-3 space-y-2'
|
|
24
|
+
: 'absolute left-0 top-full z-50 mt-1 w-80 max-h-96 overflow-auto bg-white border border-gray-default rounded shadow-lg p-1'"
|
|
25
|
+
>
|
|
26
|
+
<input
|
|
27
|
+
v-if="searchable"
|
|
28
|
+
v-model="searchQuery"
|
|
29
|
+
type="search"
|
|
30
|
+
class="w-full border border-gray-default rounded px-2.5 py-1.5 text-sm"
|
|
31
|
+
:placeholder="t('Rechercher dans les ressources…')"
|
|
32
|
+
>
|
|
33
|
+
<ul
|
|
34
|
+
v-if="filteredResources.length > 0"
|
|
35
|
+
class="list-none p-0 m-0 space-y-0.5 max-h-80 overflow-y-auto"
|
|
36
|
+
>
|
|
37
|
+
<li
|
|
38
|
+
v-for="r in filteredResources"
|
|
39
|
+
:key="r.id"
|
|
40
|
+
>
|
|
41
|
+
<button
|
|
42
|
+
v-if="!isDisabled?.(r)"
|
|
43
|
+
type="button"
|
|
44
|
+
class="flex items-center gap-1.5 w-full text-left px-2 py-1.5 rounded text-sm hover:bg-gray-100 focus:outline-none focus-visible:bg-gray-100"
|
|
45
|
+
:class="{ 'font-bold bg-blue-50 text-new-primary': r.id === selectedId }"
|
|
46
|
+
@click="emit('select', r); close()"
|
|
47
|
+
>
|
|
48
|
+
<ResourceIcon
|
|
49
|
+
:resource="r"
|
|
50
|
+
class="size-3.5 shrink-0"
|
|
51
|
+
/>
|
|
52
|
+
<span class="truncate">{{ r.title || t('Fichier sans nom') }}</span>
|
|
53
|
+
<span
|
|
54
|
+
v-if="r.format"
|
|
55
|
+
class="ml-auto text-xs text-gray-medium uppercase shrink-0"
|
|
56
|
+
>
|
|
57
|
+
{{ r.format }}
|
|
58
|
+
</span>
|
|
59
|
+
</button>
|
|
60
|
+
<div
|
|
61
|
+
v-else
|
|
62
|
+
class="flex items-center gap-1.5 px-2 py-1.5 rounded text-sm text-gray-medium cursor-not-allowed"
|
|
63
|
+
:title="disabledTitle"
|
|
64
|
+
>
|
|
65
|
+
<ResourceIcon
|
|
66
|
+
:resource="r"
|
|
67
|
+
class="size-3.5 shrink-0 opacity-50"
|
|
68
|
+
/>
|
|
69
|
+
<span class="truncate opacity-70">{{ r.title || t('Fichier sans nom') }}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</li>
|
|
72
|
+
</ul>
|
|
73
|
+
<p
|
|
74
|
+
v-else
|
|
75
|
+
class="text-sm text-gray-medium italic mb-0 px-2 py-2"
|
|
76
|
+
>
|
|
77
|
+
{{ t('Aucune ressource correspondante') }}
|
|
78
|
+
</p>
|
|
79
|
+
</PopoverPanel>
|
|
80
|
+
</Popover>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { computed, ref } from 'vue'
|
|
85
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
86
|
+
import { RiArrowDownSLine } from '@remixicon/vue'
|
|
87
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
88
|
+
import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
|
|
89
|
+
import type { Resource } from '../../types/resources'
|
|
90
|
+
|
|
91
|
+
const props = defineProps<{
|
|
92
|
+
resources: Resource[]
|
|
93
|
+
selectedId: string
|
|
94
|
+
searchable?: boolean
|
|
95
|
+
isDisabled?: (resource: Resource) => boolean
|
|
96
|
+
disabledTitle?: string
|
|
97
|
+
}>()
|
|
98
|
+
|
|
99
|
+
const emit = defineEmits<{
|
|
100
|
+
select: [resource: Resource]
|
|
101
|
+
}>()
|
|
102
|
+
|
|
103
|
+
const { t } = useTranslation()
|
|
104
|
+
|
|
105
|
+
const searchQuery = ref('')
|
|
106
|
+
|
|
107
|
+
const filteredResources = computed(() => {
|
|
108
|
+
if (!props.searchable) return props.resources
|
|
109
|
+
const q = searchQuery.value.trim().toLowerCase()
|
|
110
|
+
if (!q) return props.resources
|
|
111
|
+
return props.resources.filter(r => (r.title ?? '').toLowerCase().includes(q))
|
|
112
|
+
})
|
|
113
|
+
</script>
|