@datagouv/components-next 1.1.2 → 1.2.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.
Files changed (34) hide show
  1. package/dist/{Datafair.client-8bXp6UeQ.js → Datafair.client-rf4T1IkA.js} +1 -1
  2. package/dist/{JsonPreview.client-BxuoPK_w.js → JsonPreview.client-dzar6iuh.js} +2 -2
  3. package/dist/{MapContainer.client-CTz0wmJG.js → MapContainer.client-D-MoRNhG.js} +2 -2
  4. package/dist/{PdfPreview.client-DpVreUSl.js → PdfPreview.client-DoDYLmJD.js} +2 -2
  5. package/dist/{Pmtiles.client-BwmHo3T8.js → Pmtiles.client-Dzm01Zfm.js} +1 -1
  6. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-stmU5qEB.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js} +1 -1
  7. package/dist/{XmlPreview.client-DAOs89cR.js → XmlPreview.client-cOhwff6P.js} +3 -3
  8. package/dist/components-next.css +1 -1
  9. package/dist/components-next.js +160 -155
  10. package/dist/components.css +1 -1
  11. package/dist/{index-JqjPja1u.js → index-NofRBuyf.js} +1 -1
  12. package/dist/{main-D4WQMky0.js → main-Iz1ZCL6k.js} +41691 -89416
  13. package/dist/{vue3-xml-viewer.common-BGsoNyZe.js → vue3-xml-viewer.common-tVI9uXUz.js} +1 -1
  14. package/package.json +10 -3
  15. package/src/chart.ts +5 -0
  16. package/src/components/ActivityList/ActivityList.vue +3 -0
  17. package/src/components/DatasetCard.vue +6 -4
  18. package/src/components/ObjectCardHeader.vue +1 -1
  19. package/src/components/RadioInput.vue +7 -2
  20. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  21. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  22. package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -102
  23. package/src/components/ResourceExplorer/ResourceExplorer.vue +2 -55
  24. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +26 -135
  25. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  26. package/src/components/Search/GlobalSearch.vue +11 -4
  27. package/src/components/TabularExplorer/TabularExplorer.vue +257 -154
  28. package/src/composables/useHasTabularData.ts +7 -0
  29. package/src/composables/useStableQueryParams.ts +7 -3
  30. package/src/composables/useTabularProfile.ts +70 -0
  31. package/src/functions/activities.ts +3 -3
  32. package/src/main.ts +12 -6
  33. package/src/types/search.ts +11 -0
  34. package/src/types/ui.ts +1 -1
@@ -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
- <Preview
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,128 +180,10 @@
174
180
  <Metadata :resource />
175
181
  </div>
176
182
  <div v-if="tab.key === 'downloads'">
177
- <dl class="fr-pl-0">
178
- <dt
179
- v-if="resource.format === 'url'"
180
- class="font-bold fr-text--sm fr-mb-0"
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
188
  <div v-if="tab.key === 'swagger'">
301
189
  <div class="fr-mb-4w">
@@ -318,7 +206,7 @@
318
206
 
319
207
  <script setup lang="ts">
320
208
  import { computed, defineAsyncComponent } from 'vue'
321
- import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
209
+ import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
322
210
  import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
323
211
  import { toast } from 'vue-sonner'
324
212
  import BrandedButton from '../BrandedButton.vue'
@@ -331,11 +219,12 @@ import TabList from '../Tabs/TabList.vue'
331
219
  import Tab from '../Tabs/Tab.vue'
332
220
  import TabPanels from '../Tabs/TabPanels.vue'
333
221
  import TabPanel from '../Tabs/TabPanel.vue'
334
- import Tooltip from '../Tooltip.vue'
335
- import Preview from '../ResourceAccordion/Preview.vue'
222
+ import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
336
223
  import DataStructure from '../ResourceAccordion/DataStructure.vue'
224
+ import Downloads from '../ResourceAccordion/Downloads.vue'
337
225
  import Metadata from '../ResourceAccordion/Metadata.vue'
338
226
  import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
227
+ import ResourceSelector from './ResourceSelector.vue'
339
228
  import { filesize, summarize } from '../../functions/helpers'
340
229
  import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
341
230
  import { trackEvent } from '../../functions/matomo'
@@ -343,6 +232,7 @@ import { useComponentsConfig } from '../../config'
343
232
  import { useFormatDate } from '../../functions/dates'
344
233
  import { useTranslation } from '../../composables/useTranslation'
345
234
  import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
235
+ import { provideTabularProfile } from '../../composables/useTabularProfile'
346
236
  import type { Resource } from '../../types/resources'
347
237
  import type { Dataset, DatasetV2 } from '../../types/datasets'
348
238
 
@@ -368,6 +258,11 @@ const Pmtiles = defineAsyncComponent(() =>
368
258
  const props = defineProps<{
369
259
  dataset: Dataset | DatasetV2
370
260
  resource: Resource
261
+ resources?: Resource[]
262
+ }>()
263
+
264
+ const emit = defineEmits<{
265
+ select: [resource: Resource]
371
266
  }>()
372
267
 
373
268
  const { t } = useTranslation()
@@ -381,13 +276,13 @@ const {
381
276
  hasOpenAPIPreview,
382
277
  ogcService,
383
278
  ogcWms,
384
- generatedFormats,
385
- wfsFormats,
386
- defaultWfsProjection,
387
279
  isResourceUrl,
388
280
  tabsOptions,
389
281
  } = useResourceCapabilities(() => props.resource, () => props.dataset)
390
282
 
283
+ // Share the tabular profile fetch between TabularExplorer and DataStructure tabs.
284
+ await provideTabularProfile(() => props.resource.id)
285
+
391
286
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
392
287
  const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
393
288
 
@@ -401,10 +296,6 @@ const downloadButtonTitle = computed(() => {
401
296
  return t('Télécharger le fichier en {format}', { format: format.value })
402
297
  })
403
298
 
404
- const conversionsLastUpdate = computed(() =>
405
- formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
406
- )
407
-
408
299
  const copyResourceUrl = async () => {
409
300
  try {
410
301
  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>
@@ -36,7 +36,7 @@
36
36
  :value="configKey(typeConfig)"
37
37
  :count="resultsMap[configKey(typeConfig)]?.data.value?.total"
38
38
  :loading="resultsMap[configKey(typeConfig)]?.status.value === 'pending' || resultsMap[configKey(typeConfig)]?.status.value === 'idle'"
39
- :icon="strategies[typeConfig.class].icon"
39
+ :icon="typeConfig.icon ?? strategies[typeConfig.class].icon"
40
40
  >
41
41
  {{ typeConfig.name || strategies[typeConfig.class].name }}
42
42
  </RadioInput>
@@ -200,10 +200,13 @@
200
200
  <div class="fr-col">
201
201
  <select
202
202
  id="sort-search"
203
- v-model="sort"
203
+ v-model="effectiveSort"
204
204
  class="fr-select text-sm shadow-input-blue!"
205
205
  >
206
- <option :value="undefined">
206
+ <option
207
+ v-if="!currentTypeConfig?.defaultSort"
208
+ :value="undefined"
209
+ >
207
210
  {{ t('Pertinence') }}
208
211
  </option>
209
212
  <option
@@ -489,6 +492,10 @@ const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsCo
489
492
  const qForParams = computed(() => props.hideSearchInput ? q.value : qDebounced.value)
490
493
  const page = useRouteQuery('page', 1, { transform: Number })
491
494
  const sort = useRouteQuery<string | undefined>('sort')
495
+ const effectiveSort = computed({
496
+ get: () => sort.value ?? currentTypeConfig.value?.defaultSort,
497
+ set: (value) => { sort.value = value },
498
+ })
492
499
 
493
500
  provide(searchFilterContextKey, {
494
501
  register(urlParam, entry) {
@@ -759,7 +766,7 @@ const rssUrl = computed(() => {
759
766
  }, currentTypeConfig.value ? configKey(currentTypeConfig.value) : undefined)
760
767
 
761
768
  // Add sort if set
762
- if (sort.value) params.set('sort', sort.value)
769
+ if (effectiveSort.value) params.set('sort', effectiveSort.value)
763
770
 
764
771
  const queryString = params.toString()
765
772
  const basePath = '/api/1/datasets/recent.atom'