@datagouv/components-next 1.0.2-dev.97 → 1.0.2-dev.98

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 (25) hide show
  1. package/dist/{Datafair.client-8bXp6UeQ.js → Datafair.client-C2j760M5.js} +1 -1
  2. package/dist/{JsonPreview.client-BxuoPK_w.js → JsonPreview.client-PFfBR4J8.js} +2 -2
  3. package/dist/{MapContainer.client-CTz0wmJG.js → MapContainer.client-CGrS2baS.js} +2 -2
  4. package/dist/{PdfPreview.client-DpVreUSl.js → PdfPreview.client-DU36UBGQ.js} +2 -2
  5. package/dist/{Pmtiles.client-BwmHo3T8.js → Pmtiles.client-DuTezcn5.js} +1 -1
  6. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-stmU5qEB.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-ProPRqX6.js} +1 -1
  7. package/dist/{XmlPreview.client-DAOs89cR.js → XmlPreview.client-Bcq2Ye14.js} +3 -3
  8. package/dist/components-next.css +1 -1
  9. package/dist/components-next.js +169 -161
  10. package/dist/components.css +1 -1
  11. package/dist/{index-JqjPja1u.js → index-BJ-zwAF5.js} +1 -1
  12. package/dist/{main-D4WQMky0.js → main-TqHFAOCi.js} +73370 -73298
  13. package/dist/{vue3-xml-viewer.common-BGsoNyZe.js → vue3-xml-viewer.common-BnJTx_B7.js} +1 -1
  14. package/package.json +1 -1
  15. package/src/components/DatasetCard.vue +6 -4
  16. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  17. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  18. package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -102
  19. package/src/components/ResourceExplorer/ResourceExplorer.vue +2 -55
  20. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +26 -135
  21. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  22. package/src/components/TabularExplorer/TabularExplorer.vue +257 -154
  23. package/src/composables/useHasTabularData.ts +7 -0
  24. package/src/composables/useTabularProfile.ts +70 -0
  25. package/src/main.ts +12 -0
@@ -1,4 +1,4 @@
1
- import { c as Ke } from "./main-D4WQMky0.js";
1
+ import { c as Ke } from "./main-TqHFAOCi.js";
2
2
  import We from "vue";
3
3
  function Fe(I, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.97",
3
+ "version": "1.0.2-dev.98",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -107,10 +107,12 @@
107
107
  </p>
108
108
  </div>
109
109
  </div>
110
- <ObjectCardShortDescription
111
- v-if="showDescriptionShort"
112
- :text="getDescriptionShort(props.dataset)"
113
- />
110
+ <slot>
111
+ <ObjectCardShortDescription
112
+ v-if="showDescriptionShort"
113
+ :text="getDescriptionShort(props.dataset)"
114
+ />
115
+ </slot>
114
116
  </ObjectCard>
115
117
  </template>
116
118
 
@@ -49,45 +49,23 @@
49
49
  </template>
50
50
 
51
51
  <script setup lang="ts">
52
- import { onMounted, ref } from 'vue'
52
+ import { computed } from 'vue'
53
53
  import type { Resource } from '../../types/resources'
54
- import { useGetProfile } from '../../functions/tabularApi'
55
54
  import { useTranslation } from '../../composables/useTranslation'
55
+ import { injectTabularProfile } from '../../composables/useTabularProfile'
56
56
  import PreviewLoader from './PreviewLoader.vue'
57
57
 
58
58
  const props = defineProps<{ resource: Resource }>()
59
- const getProfile = useGetProfile()
60
59
  const { t } = useTranslation()
61
60
 
62
- type ColumnInfo = {
63
- score: number
64
- format: string
65
- python_type: string
66
- }
61
+ // Profile is shared with sibling components (e.g. TabularExplorer) via
62
+ // `provideTabularProfile` in the parent. Falls back to a local fetch
63
+ // when no parent provides it (standalone usage).
64
+ const { data: profileData, status } = await injectTabularProfile(() => props.resource.id)
67
65
 
68
- const columns = ref<Array<string>>([])
69
- const columnsInfo = ref<Record<string, ColumnInfo>>({})
70
- const loading = ref(true)
71
- const hasError = ref(false)
72
- const hasColumnInfo = ref(false)
73
-
74
- onMounted(async () => {
75
- try {
76
- const response = await getProfile(props.resource.id) // Assurez-vous que cette fonction retourne bien les données attendues
77
- if ('profile' in response && response.profile) {
78
- columns.value = Object.keys(response.profile.columns)
79
- columnsInfo.value = response.profile.columns
80
- hasColumnInfo.value = true
81
- loading.value = false
82
- }
83
- else {
84
- hasError.value = true
85
- loading.value = false
86
- }
87
- }
88
- catch {
89
- hasError.value = true
90
- loading.value = false
91
- }
92
- })
66
+ const loading = computed(() => status.value === 'idle' || status.value === 'pending')
67
+ const hasError = computed(() => status.value === 'error')
68
+ const hasColumnInfo = computed(() => !!profileData.value?.profile?.columns)
69
+ const columns = computed(() => profileData.value?.profile ? Object.keys(profileData.value.profile.columns) : [])
70
+ const columnsInfo = computed(() => profileData.value?.profile?.columns ?? {})
93
71
  </script>
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <dl class="fr-pl-0">
3
+ <dt
4
+ v-if="resource.format === 'url'"
5
+ class="font-bold fr-text--sm fr-mb-0"
6
+ >
7
+ {{ t("URL d'origine") }}
8
+ </dt>
9
+ <dt
10
+ v-else
11
+ class="font-bold fr-text--sm fr-mb-0"
12
+ >
13
+ {{ t('Format original') }}
14
+ </dt>
15
+ <dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
16
+ <span
17
+ v-if="resource.format === 'url'"
18
+ class="inline-flex items-center max-w-full"
19
+ >
20
+ <a
21
+ :href="resource.latest"
22
+ class="fr-link no-icon-after truncate"
23
+ rel="ugc nofollow noopener"
24
+ target="_blank"
25
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
26
+ >
27
+ {{ resource.url }}
28
+ </a>
29
+ <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm shrink-0" />
30
+ </span>
31
+ <span v-else>
32
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
33
+ <a
34
+ :href="resource.latest"
35
+ class="fr-link"
36
+ rel="ugc nofollow noopener"
37
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
38
+ >
39
+ <span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
40
+ </a>
41
+ </span>
42
+ <CopyButton
43
+ :label="t('Copier le lien')"
44
+ :copied-label="t('Lien copié !')"
45
+ :text="resource.latest"
46
+ class="relative"
47
+ />
48
+ </dd>
49
+ <template v-if="generatedFormats.length">
50
+ <dt class="font-bold fr-text--sm fr-mb-0">
51
+ {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
52
+ </dt>
53
+ <dd
54
+ v-for="generatedFormat in generatedFormats"
55
+ :key="generatedFormat.format"
56
+ class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
57
+ >
58
+ <span>
59
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
60
+ <a
61
+ :href="generatedFormat.url"
62
+ class="fr-link"
63
+ rel="ugc nofollow noopener"
64
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
65
+ >
66
+ <span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
67
+ </a>
68
+ </span>
69
+ <CopyButton
70
+ :label="t('Copier le lien')"
71
+ :copied-label="t('Lien copié !')"
72
+ :text="generatedFormat.url"
73
+ class="relative"
74
+ />
75
+ </dd>
76
+ </template>
77
+ <template v-if="wfsFormats.length">
78
+ <dt class="font-bold fr-text--sm fr-mb-0">
79
+ <div class="flex gap-1 items-center">
80
+ {{ t('Formats exportés depuis le service WFS') }}
81
+ <span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
82
+ <Tooltip>
83
+ <RiInformationLine
84
+ class="flex-none size-4"
85
+ :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é.`)"
86
+ aria-hidden="true"
87
+ />
88
+ <template #tooltip>
89
+ <p class="text-sm font-normal mb-0">
90
+ {{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
91
+ </p>
92
+ <p class="text-sm font-normal mb-0">
93
+ {{ t(`Le nombre de features téléchargées peut être limité.`) }}
94
+ </p>
95
+ </template>
96
+ </Tooltip>
97
+ </div>
98
+ </dt>
99
+ <dd
100
+ v-for="wfsFormat in wfsFormats"
101
+ :key="wfsFormat.format"
102
+ class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
103
+ >
104
+ <span>
105
+ <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
106
+ <a
107
+ :href="wfsFormat.url"
108
+ class="fr-link"
109
+ rel="ugc nofollow noopener"
110
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
111
+ >
112
+ <span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
113
+ </a>
114
+ </span>
115
+ <CopyButton
116
+ :label="t('Copier le lien')"
117
+ :copied-label="t('Lien copié !')"
118
+ :text="wfsFormat.url"
119
+ class="relative"
120
+ />
121
+ </dd>
122
+ </template>
123
+ </dl>
124
+ </template>
125
+
126
+ <script setup lang="ts">
127
+ import { computed } from 'vue'
128
+ import { RiInformationLine } from '@remixicon/vue'
129
+ import CopyButton from '../CopyButton.vue'
130
+ import Tooltip from '../Tooltip.vue'
131
+ import { filesize } from '../../functions/helpers'
132
+ import { getResourceFilesize } from '../../functions/resources'
133
+ import { trackEvent } from '../../functions/matomo'
134
+ import { useComponentsConfig } from '../../config'
135
+ import { useFormatDate } from '../../functions/dates'
136
+ import { useTranslation } from '../../composables/useTranslation'
137
+ import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
138
+ import type { Resource } from '../../types/resources'
139
+ import type { Dataset, DatasetV2 } from '../../types/datasets'
140
+
141
+ const props = defineProps<{
142
+ resource: Resource
143
+ dataset: Dataset | DatasetV2
144
+ }>()
145
+
146
+ const { t } = useTranslation()
147
+ const config = useComponentsConfig()
148
+ const { formatRelativeIfRecentDate } = useFormatDate()
149
+
150
+ const { generatedFormats, wfsFormats, defaultWfsProjection } = useResourceCapabilities(
151
+ () => props.resource,
152
+ () => props.dataset,
153
+ )
154
+
155
+ const resourceFilesize = computed(() => getResourceFilesize(props.resource))
156
+
157
+ const conversionsLastUpdate = computed(() =>
158
+ formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
159
+ )
160
+ </script>
@@ -265,88 +265,10 @@
265
265
  <div
266
266
  v-if="tab.key === 'downloads'"
267
267
  >
268
- <dl class="fr-pl-0">
269
- <dt
270
- v-if="resource.format === 'url'"
271
- class="font-bold fr-text--sm fr-mb-0"
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
274
  v-if="tab.key === 'swagger'"
@@ -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
@@ -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
@@ -107,7 +77,6 @@ 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<{
@@ -246,21 +215,6 @@ const flatResources = computed(() =>
246
215
 
247
216
  // Fetch resource by ID if specified in URL (for SSR)
248
217
  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
218
  const { data: fetchedResource } = initialResourceId
265
219
  ? await useFetch<Resource>(`/api/1/datasets/${props.dataset.id}/resources/${initialResourceId}/`)
266
220
  : { data: ref(null) }
@@ -288,7 +242,6 @@ function updateSearch(newSearch: string) {
288
242
 
289
243
  const selectResource = (resource: Resource) => {
290
244
  selectedResource.value = resource
291
- mobileSidebarOpen.value = false
292
245
  router.replace({
293
246
  query: { ...router.currentRoute.value.query, resource_id: resource.id },
294
247
  })
@@ -303,9 +256,3 @@ watch(flatResources, () => {
303
256
  }
304
257
  })
305
258
  </script>
306
-
307
- <style>
308
- html:has(dialog.mobile-sidebar[open]) {
309
- overflow: hidden;
310
- }
311
- </style>