@datagouv/components-next 1.3.0 → 1.4.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/dist/{Control-b-43UT2B.js → Control-B9WQxlpJ.js} +10 -10
- package/dist/{Datafair.client-BrjISlaZ.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-py_AAwJY.js → Event-iXDrp-fy.js} +15 -15
- package/dist/{Image-BHQYZWzH.js → Image-mNMBOofs.js} +22 -22
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-DivFAnbN.js → JsonPreview.client-DSl1kiwC.js} +2 -2
- package/dist/{Map-CtP7leHl.js → Map-BfqKMw0S.js} +165 -165
- package/dist/{MapContainer.client-Cq8tNLZZ.js → MapContainer.client-BSfpk_rT.js} +17 -17
- package/dist/{OSM-gdDSIsDA.js → OSM-CcrUhItr.js} +1 -1
- package/dist/{PdfPreview.client-BNBgLsK2.js → PdfPreview.client-BANFeM6K.js} +438 -427
- package/dist/{Pmtiles.client-CQi5Rw8W.js → Pmtiles.client-DH9ZrsZR.js} +6177 -5967
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hG-FhNqy.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-D6rFonpT.js → Tile-CSpn0Ca8.js} +47 -47
- package/dist/{TileImage-cfrEgz5y.js → TileImage-CpbaEFwd.js} +44 -44
- package/dist/{View-vp7vDb9q.js → View-BYBpt-AU.js} +58 -58
- package/dist/{XmlPreview.client-XCsNvnsZ.js → XmlPreview.client-KCB5mwVf.js} +3 -3
- package/dist/{common-C8efyyuc.js → common-BoeTUnDZ.js} +58 -58
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +199 -172
- package/dist/components.css +1 -1
- package/dist/{index-CXUamc8Z.js → index-DHWzX5vK.js} +4668 -4466
- package/dist/{leaflet-src-BRKv9Ksv.js → leaflet-src-pVQfonBI.js} +1 -1
- package/dist/{main-DKu5SOAl.js → main-BDcYkhOc.js} +41684 -33432
- package/dist/{proj-CsjVeojN.js → proj-DxGbobp7.js} +110 -110
- package/dist/{tilecoord-CPcQJnrF.js → tilecoord-DvX7WWiA.js} +94 -94
- package/dist/{vue3-xml-viewer.common-BIaEIFYG.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +28 -27
- package/src/components/ActivityList/ActivityList.vue +21 -12
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +111 -17
- package/src/components/Chart/ChartViewerWrapper.vue +28 -16
- package/src/components/ChartCard.vue +112 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +23 -6
- 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 +17 -6
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -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/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +69 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCard.vue +4 -1
- package/src/components/ObjectCardOwner.vue +13 -1
- package/src/components/Pagination.vue +18 -58
- package/src/components/Placeholder.vue +3 -2
- package/src/components/PostCard.vue +13 -15
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +19 -7
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
- package/src/components/ResourceAccordion/Metadata.vue +21 -5
- package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +27 -18
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +29 -9
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -209
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +136 -183
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +18 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +4 -3
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +12 -8
- package/src/components/Search/SearchInput.vue +1 -1
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +7 -8
- package/src/components/TabularExplorer/TabularCellPopover.vue +19 -3
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +122 -913
- package/src/components/TabularExplorer/TabularFilterContent.vue +33 -25
- package/src/components/TabularExplorer/TabularFilterPopover.vue +7 -7
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +484 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +32 -20
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +15 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +2 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +47 -39
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/charts.ts +32 -1
- package/src/functions/dates.ts +26 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/owned.ts +21 -0
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +83 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +119 -4
- package/src/functions/tabularApi.ts +1 -43
- package/src/main.ts +32 -6
- package/src/types/dataservices.ts +15 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js +0 -61
- /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,116 +1,149 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<aside
|
|
3
|
-
|
|
4
|
-
class="
|
|
3
|
+
class="relative flex shrink-0 flex-col overflow-hidden border-r border-gray-default bg-white"
|
|
4
|
+
:class="resizing ? '' : 'transition-[width] duration-200'"
|
|
5
|
+
:style="{ width: collapsed ? '48px' : `${width}px` }"
|
|
6
|
+
:aria-label="t('Ressources')"
|
|
5
7
|
>
|
|
6
|
-
<div
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
<div
|
|
9
|
+
class="flex h-14 items-center border-b border-gray-default bg-gray-some px-3"
|
|
10
|
+
:class="collapsed ? 'justify-center' : 'justify-between'"
|
|
11
|
+
>
|
|
12
|
+
<span
|
|
13
|
+
v-if="!collapsed"
|
|
14
|
+
class="text-[13px] font-medium text-gray-title"
|
|
15
|
+
>{{ t('Ressources') }}</span>
|
|
10
16
|
<button
|
|
11
17
|
type="button"
|
|
12
|
-
:title="t('Masquer le panneau')"
|
|
13
|
-
class="
|
|
14
|
-
@click="$emit('update:collapsed',
|
|
18
|
+
:title="collapsed ? t('Afficher le panneau des ressources') : t('Masquer le panneau')"
|
|
19
|
+
class="flex size-6 items-center justify-center rounded text-gray-plain hover:bg-gray-100"
|
|
20
|
+
@click="$emit('update:collapsed', !collapsed)"
|
|
15
21
|
>
|
|
16
|
-
<
|
|
22
|
+
<component
|
|
23
|
+
:is="collapsed ? RiSidebarUnfoldLine : RiSidebarFoldLine"
|
|
24
|
+
class="size-5"
|
|
25
|
+
/>
|
|
17
26
|
</button>
|
|
18
27
|
</div>
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
<!-- min-w keeps the content laid out at the sidebar's min width so it doesn't
|
|
30
|
+
reflow (badges wrapping, section titles on 2 lines) while the width animates
|
|
31
|
+
open from the collapsed state — the aside's overflow-hidden clips the rest. -->
|
|
32
|
+
<!-- Extra bottom padding so the last item can scroll clear of the browser's
|
|
33
|
+
hover URL bar (Firefox shows it bottom-left). -->
|
|
34
|
+
<div
|
|
35
|
+
v-if="!collapsed"
|
|
36
|
+
class="flex min-h-0 min-w-[260px] flex-1 flex-col gap-3 overflow-y-auto p-2 pb-16"
|
|
37
|
+
>
|
|
38
|
+
<label class="flex h-8 items-center gap-1 rounded border border-gray-default bg-gray-some px-2">
|
|
39
|
+
<RiSearchLine class="size-3.5 shrink-0 text-gray-medium" />
|
|
40
|
+
<span class="sr-only">{{ t('Rechercher') }}</span>
|
|
41
|
+
<input
|
|
42
|
+
:value="search"
|
|
43
|
+
type="search"
|
|
44
|
+
class="min-w-0 flex-1 bg-transparent text-[13px] outline-none placeholder:text-gray-medium"
|
|
45
|
+
:placeholder="t('Rechercher une ressource')"
|
|
46
|
+
@input="$emit('update:search', ($event.target as HTMLInputElement).value)"
|
|
47
|
+
>
|
|
48
|
+
</label>
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
v-for="group in resources"
|
|
50
|
+
<section
|
|
51
|
+
v-for="group in groups"
|
|
38
52
|
:key="group.type"
|
|
53
|
+
class="space-y-0.5"
|
|
39
54
|
>
|
|
40
|
-
<
|
|
55
|
+
<p class="px-1 py-2 text-[12px] font-medium leading-3 text-gray-medium">
|
|
41
56
|
{{ getResourceLabel(group.type, group.total) }}
|
|
42
|
-
</
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
:class="{
|
|
52
|
-
'font-bold bg-blue-50': resource.id === selectedResourceId,
|
|
53
|
-
}"
|
|
54
|
-
@click="$emit('select', resource)"
|
|
55
|
-
>
|
|
56
|
-
<ResourceIcon
|
|
57
|
-
:resource
|
|
58
|
-
class="size-3.5 shrink-0"
|
|
59
|
-
/>
|
|
60
|
-
<span class="truncate">{{ resource.title || t('Fichier sans nom') }}</span>
|
|
61
|
-
</button>
|
|
62
|
-
</li>
|
|
63
|
-
</ul>
|
|
57
|
+
</p>
|
|
58
|
+
<ResourceListItem
|
|
59
|
+
v-for="resource in group.items"
|
|
60
|
+
:key="resource.id"
|
|
61
|
+
:resource
|
|
62
|
+
:to="resourceTo(resource)"
|
|
63
|
+
:replace
|
|
64
|
+
:selected="resource.id === selectedResourceId"
|
|
65
|
+
/>
|
|
64
66
|
<button
|
|
65
67
|
v-if="group.items.length < group.total"
|
|
66
68
|
type="button"
|
|
67
|
-
|
|
69
|
+
:disabled="loadingType === group.type"
|
|
70
|
+
class="flex w-full items-center gap-1 px-1 py-1 text-left text-[13px] text-blue-default hover:underline disabled:cursor-default disabled:no-underline"
|
|
68
71
|
@click="$emit('load-more', group.type)"
|
|
69
72
|
>
|
|
73
|
+
<RiLoader5Line
|
|
74
|
+
v-if="loadingType === group.type"
|
|
75
|
+
class="size-3.5 shrink-0 animate-spin"
|
|
76
|
+
/>
|
|
70
77
|
{{ t('Charger plus…') }}
|
|
71
78
|
</button>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<!-- The search lives here, so its outcome does too: the viewer only says that
|
|
82
|
+
nothing is selected. -->
|
|
83
|
+
<div
|
|
84
|
+
v-if="search && !groups.length"
|
|
85
|
+
class="flex flex-col gap-2 px-1 py-2"
|
|
86
|
+
>
|
|
87
|
+
<p class="m-0 text-[13px] leading-snug text-gray-medium">
|
|
88
|
+
{{ t('Pas de résultats pour « {q} »', { q: search }) }}
|
|
89
|
+
</p>
|
|
90
|
+
<BrandedButton
|
|
91
|
+
color="secondary"
|
|
92
|
+
size="xs"
|
|
93
|
+
class="w-full"
|
|
94
|
+
@click="$emit('update:search', '')"
|
|
95
|
+
>
|
|
96
|
+
{{ t('Réinitialiser la recherche') }}
|
|
97
|
+
</BrandedButton>
|
|
72
98
|
</div>
|
|
73
99
|
</div>
|
|
74
|
-
</aside>
|
|
75
100
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</
|
|
101
|
+
<!-- Drag the right edge to resize the panel and read longer resource titles.
|
|
102
|
+
A separator rather than a button: it only answers to a drag, and a focusable
|
|
103
|
+
button would announce an action that no key can trigger. Keyboard users fold
|
|
104
|
+
the panel from the header instead. -->
|
|
105
|
+
<div
|
|
106
|
+
v-if="!collapsed"
|
|
107
|
+
role="separator"
|
|
108
|
+
aria-orientation="vertical"
|
|
109
|
+
:title="t('Glisser pour redimensionner')"
|
|
110
|
+
class="absolute right-0 top-0 z-20 h-full w-2 cursor-col-resize touch-none hover:bg-new-primary/10"
|
|
111
|
+
@mousedown="startResize"
|
|
112
|
+
/>
|
|
113
|
+
</aside>
|
|
89
114
|
</template>
|
|
90
115
|
|
|
91
116
|
<script setup lang="ts">
|
|
92
|
-
import {
|
|
93
|
-
import {
|
|
94
|
-
import
|
|
117
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
118
|
+
import { RiSidebarFoldLine, RiSidebarUnfoldLine, RiSearchLine, RiLoader5Line } from '@remixicon/vue'
|
|
119
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
120
|
+
import ResourceListItem from '../ResourceListItem.vue'
|
|
95
121
|
import { getResourceLabel } from '../../functions/resources'
|
|
96
122
|
import { useTranslation } from '../../composables/useTranslation'
|
|
123
|
+
import { useResizable } from '../../composables/useResizable'
|
|
97
124
|
import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
|
|
98
125
|
|
|
99
126
|
const { t } = useTranslation()
|
|
100
127
|
|
|
101
128
|
defineProps<{
|
|
102
|
-
|
|
129
|
+
groups: ResourceGroup[]
|
|
103
130
|
selectedResourceId: string | null
|
|
104
131
|
collapsed: boolean
|
|
105
132
|
search: string
|
|
133
|
+
loadingType: ResourceType | null
|
|
134
|
+
resourceTo: (resource: Resource) => RouteLocationRaw
|
|
135
|
+
replace?: boolean
|
|
106
136
|
}>()
|
|
107
137
|
|
|
108
138
|
defineEmits<{
|
|
109
|
-
'select': [resource: Resource]
|
|
110
139
|
'load-more': [type: ResourceType]
|
|
111
140
|
'update:collapsed': [value: boolean]
|
|
112
141
|
'update:search': [value: string]
|
|
113
142
|
}>()
|
|
114
143
|
|
|
115
|
-
const
|
|
144
|
+
const { width, resizing, startResize } = useResizable({
|
|
145
|
+
initialWidth: 300,
|
|
146
|
+
minWidth: 260,
|
|
147
|
+
maxWidth: 720,
|
|
148
|
+
})
|
|
116
149
|
</script>
|
|
@@ -1,102 +1,24 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<ResourceSelector
|
|
14
|
-
v-if="resources && resources.length > 1"
|
|
15
|
-
:resources
|
|
16
|
-
:selected-id="resource.id"
|
|
17
|
-
@select="emit('select', $event)"
|
|
18
|
-
/>
|
|
19
|
-
<CopyButton
|
|
20
|
-
:label="t('Copier le lien')"
|
|
21
|
-
:copied-label="t('Lien copié !')"
|
|
22
|
-
:text="resourceExternalUrl"
|
|
23
|
-
class="hidden md:inline-flex"
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
26
|
-
<div class="text-gray-medium text-xs flex items-center gap-1 flex-wrap">
|
|
27
|
-
<SchemaBadge :resource />
|
|
28
|
-
<RiSubtractLine
|
|
29
|
-
v-if="resource.schema?.name || resource.schema?.url"
|
|
30
|
-
aria-hidden="true"
|
|
31
|
-
class="size-3 fill-gray-medium"
|
|
32
|
-
/>
|
|
33
|
-
<span>{{ t('mis à jour {date}', { date: formatRelativeIfRecentDate(resource.last_modified) }) }}</span>
|
|
34
|
-
<RiSubtractLine
|
|
35
|
-
aria-hidden="true"
|
|
36
|
-
class="size-3 fill-gray-medium"
|
|
37
|
-
/>
|
|
38
|
-
<template v-if="resource.format">
|
|
39
|
-
<span>
|
|
40
|
-
{{ resource.format.trim().toLowerCase() }}
|
|
41
|
-
<span v-if="resourceFilesize">({{ filesize(resourceFilesize) }})</span>
|
|
42
|
-
</span>
|
|
43
|
-
<RiSubtractLine
|
|
44
|
-
aria-hidden="true"
|
|
45
|
-
class="size-3 fill-gray-medium"
|
|
46
|
-
/>
|
|
47
|
-
</template>
|
|
48
|
-
<span class="inline-flex items-center">
|
|
49
|
-
<RiDownloadLine class="size-3 mr-0.5" />
|
|
50
|
-
{{ summarize(resource.metrics.views) }}
|
|
51
|
-
</span>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
<div class="flex items-center gap-2">
|
|
55
|
-
<BrandedButton
|
|
56
|
-
v-if="isResourceUrl"
|
|
57
|
-
:href="resource.latest"
|
|
58
|
-
:title="t('Lien du fichier - ouvre une nouvelle fenêtre')"
|
|
59
|
-
rel="ugc nofollow noopener"
|
|
60
|
-
new-tab
|
|
61
|
-
size="xs"
|
|
62
|
-
external
|
|
63
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
|
|
64
|
-
>
|
|
65
|
-
{{ t('Visiter') }}
|
|
66
|
-
</BrandedButton>
|
|
67
|
-
<BrandedButton
|
|
68
|
-
v-else-if="ogcService"
|
|
69
|
-
:icon="RiFileCopyLine"
|
|
70
|
-
color="primary"
|
|
71
|
-
size="xs"
|
|
72
|
-
@click="copyResourceUrl"
|
|
73
|
-
>
|
|
74
|
-
{{ t('Copier le lien') }}
|
|
75
|
-
</BrandedButton>
|
|
76
|
-
<BrandedButton
|
|
77
|
-
v-else
|
|
78
|
-
:href="resource.latest"
|
|
79
|
-
rel="ugc nofollow noopener"
|
|
80
|
-
:title="downloadButtonTitle"
|
|
81
|
-
download
|
|
82
|
-
class="matomo_download"
|
|
83
|
-
:icon="unavailable ? RiFileWarningLine : RiDownloadLine"
|
|
84
|
-
size="xs"
|
|
85
|
-
color="primary"
|
|
86
|
-
external
|
|
87
|
-
@click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
|
|
88
|
-
>
|
|
89
|
-
{{ t('Télécharger') }}
|
|
90
|
-
</BrandedButton>
|
|
91
|
-
</div>
|
|
92
|
-
</header>
|
|
2
|
+
<div :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
|
|
3
|
+
<ResourceViewerHeader
|
|
4
|
+
:dataset
|
|
5
|
+
:resource
|
|
6
|
+
:resources
|
|
7
|
+
:resource-to
|
|
8
|
+
:explore-to="exploreTo"
|
|
9
|
+
:resource-external-url="resourceExternalUrl"
|
|
10
|
+
:replace
|
|
11
|
+
:fullscreen
|
|
12
|
+
/>
|
|
93
13
|
|
|
94
|
-
<section class="
|
|
14
|
+
<section :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
|
|
95
15
|
<TabGroup
|
|
96
16
|
size="sm"
|
|
17
|
+
:default-index="defaultTabIndex"
|
|
18
|
+
:class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''"
|
|
97
19
|
@change="switchTab"
|
|
98
20
|
>
|
|
99
|
-
<div class="
|
|
21
|
+
<div class="flex shrink-0 items-center border-b border-gray-default p-2">
|
|
100
22
|
<TabList class="max-w-full overflow-x-auto">
|
|
101
23
|
<Tab
|
|
102
24
|
v-for="tab in tabsOptions"
|
|
@@ -106,13 +28,16 @@
|
|
|
106
28
|
</Tab>
|
|
107
29
|
</TabList>
|
|
108
30
|
</div>
|
|
109
|
-
<TabPanels>
|
|
31
|
+
<TabPanels :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
|
|
110
32
|
<TabPanel
|
|
111
33
|
v-for="tab in tabsOptions"
|
|
112
34
|
:key="tab.key"
|
|
113
|
-
class="
|
|
35
|
+
:class="[tab.key === 'data' || tab.key === 'map' ? '' : 'p-4', fullscreen ? 'flex min-h-0 flex-1 flex-col' : '']"
|
|
114
36
|
>
|
|
115
|
-
<div
|
|
37
|
+
<div
|
|
38
|
+
v-if="tab.key === 'map'"
|
|
39
|
+
:class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : 'h-[600px]'"
|
|
40
|
+
>
|
|
116
41
|
<Pmtiles
|
|
117
42
|
v-if="hasPmtiles"
|
|
118
43
|
:resource="resource"
|
|
@@ -122,47 +47,81 @@
|
|
|
122
47
|
v-if="ogcWms"
|
|
123
48
|
:resource="resource"
|
|
124
49
|
/>
|
|
50
|
+
<PreviewUnavailable v-if="!hasPmtiles && !ogcWms && hasPmtilesError">
|
|
51
|
+
{{ t("La carte n'a pas pu être générée automatiquement pour ce fichier.") }}
|
|
52
|
+
<br>
|
|
53
|
+
<span class="text-gray-medium text-xs">{{ pmtilesError }}</span>
|
|
54
|
+
</PreviewUnavailable>
|
|
125
55
|
</div>
|
|
126
|
-
<div
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
56
|
+
<div
|
|
57
|
+
v-if="tab.key === 'data'"
|
|
58
|
+
:class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''"
|
|
59
|
+
>
|
|
60
|
+
<!-- Wrapped in Suspense so switching to this tab (or loading its data) shows
|
|
61
|
+
the table skeleton instead of a blank gap while TabularExplorer resolves. -->
|
|
62
|
+
<Suspense
|
|
63
|
+
v-if="previewKind === 'tabular'"
|
|
64
|
+
:timeout="200"
|
|
65
|
+
>
|
|
66
|
+
<TabularExplorer :resource-id="resource.id">
|
|
67
|
+
<TabularToolbar class="shrink-0 border-b border-gray-default p-2" />
|
|
68
|
+
<TabularTable :fill="fullscreen" />
|
|
69
|
+
<TabularMobileFilters />
|
|
70
|
+
</TabularExplorer>
|
|
71
|
+
<template #fallback>
|
|
72
|
+
<TabularSkeleton :fill="fullscreen" />
|
|
73
|
+
</template>
|
|
74
|
+
</Suspense>
|
|
75
|
+
|
|
76
|
+
<!-- PDF is a full-bleed visual preview like the table and the map: it
|
|
77
|
+
owns its own reader backdrop, so it sits outside the padded wrapper. -->
|
|
131
78
|
<PdfPreview
|
|
132
|
-
v-else-if="
|
|
79
|
+
v-else-if="previewKind === 'pdf'"
|
|
133
80
|
:resource="resource"
|
|
81
|
+
:fill="fullscreen"
|
|
134
82
|
/>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
83
|
+
|
|
84
|
+
<!-- Text previews stay padded inside the tab panel -->
|
|
85
|
+
<div
|
|
86
|
+
v-else
|
|
87
|
+
class="p-4"
|
|
88
|
+
>
|
|
89
|
+
<JsonPreview
|
|
90
|
+
v-if="previewKind === 'json'"
|
|
91
|
+
:resource="resource"
|
|
92
|
+
/>
|
|
93
|
+
<XmlPreview
|
|
94
|
+
v-else-if="previewKind === 'xml'"
|
|
95
|
+
:resource="resource"
|
|
96
|
+
/>
|
|
97
|
+
<ImagePreview
|
|
98
|
+
v-else-if="previewKind === 'image'"
|
|
99
|
+
:resource="resource"
|
|
100
|
+
/>
|
|
101
|
+
<DatafairPreview
|
|
102
|
+
v-else-if="previewKind === 'datafair'"
|
|
103
|
+
:resource="resource"
|
|
104
|
+
:dataset="dataset"
|
|
105
|
+
/>
|
|
106
|
+
<OpenApiViewer
|
|
107
|
+
v-else-if="previewKind === 'openapi'"
|
|
108
|
+
:url="resource.extras['apidocUrl'] as string"
|
|
109
|
+
/>
|
|
110
|
+
<PreviewUnavailable v-else>
|
|
111
|
+
<!-- "File too large to download" is the only analysis:error value from hydra for now -->
|
|
112
|
+
<template v-if="resource.extras['analysis:error'] === 'File too large to download'">
|
|
113
|
+
{{ t("Ce fichier est trop volumineux pour être analysé et prévisualisé. Téléchargez-le avec le bouton Télécharger.") }}
|
|
114
|
+
</template>
|
|
115
|
+
<template v-else-if="resource.extras['analysis:parsing:error']">
|
|
116
|
+
{{ t("L'analyse de ce fichier a rencontré une erreur, l'aperçu n'est pas disponible. Téléchargez-le avec le bouton Télécharger.") }}
|
|
117
|
+
<br>
|
|
118
|
+
<span class="text-gray-medium text-xs">{{ resource.extras['analysis:parsing:error'] }}</span>
|
|
119
|
+
</template>
|
|
120
|
+
<template v-else>
|
|
121
|
+
{{ t("Ce fichier ne peut pas être prévisualisé. Téléchargez-le avec le bouton Télécharger.") }}
|
|
122
|
+
</template>
|
|
123
|
+
</PreviewUnavailable>
|
|
124
|
+
</div>
|
|
166
125
|
</div>
|
|
167
126
|
<div v-if="tab.key === 'description'">
|
|
168
127
|
<MarkdownViewer
|
|
@@ -179,12 +138,6 @@
|
|
|
179
138
|
<div v-if="tab.key === 'metadata'">
|
|
180
139
|
<Metadata :resource />
|
|
181
140
|
</div>
|
|
182
|
-
<div v-if="tab.key === 'downloads'">
|
|
183
|
-
<Downloads
|
|
184
|
-
:resource="resource"
|
|
185
|
-
:dataset="dataset"
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
141
|
<div v-if="tab.key === 'api'">
|
|
189
142
|
<div class="fr-mb-4w">
|
|
190
143
|
<p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
|
|
@@ -209,13 +162,9 @@
|
|
|
209
162
|
|
|
210
163
|
<script setup lang="ts">
|
|
211
164
|
import { computed, defineAsyncComponent } from 'vue'
|
|
212
|
-
import {
|
|
165
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
213
166
|
import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
|
|
214
|
-
import { toast } from 'vue-sonner'
|
|
215
|
-
import BrandedButton from '../BrandedButton.vue'
|
|
216
|
-
import CopyButton from '../CopyButton.vue'
|
|
217
167
|
import MarkdownViewer from '../MarkdownViewer.vue'
|
|
218
|
-
import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
|
|
219
168
|
import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
|
|
220
169
|
import TabGroup from '../Tabs/TabGroup.vue'
|
|
221
170
|
import TabList from '../Tabs/TabList.vue'
|
|
@@ -223,19 +172,19 @@ import Tab from '../Tabs/Tab.vue'
|
|
|
223
172
|
import TabPanels from '../Tabs/TabPanels.vue'
|
|
224
173
|
import TabPanel from '../Tabs/TabPanel.vue'
|
|
225
174
|
import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
|
|
175
|
+
import TabularToolbar from '../TabularExplorer/TabularToolbar.vue'
|
|
176
|
+
import TabularTable from '../TabularExplorer/TabularTable.vue'
|
|
177
|
+
import TabularMobileFilters from '../TabularExplorer/TabularMobileFilters.vue'
|
|
178
|
+
import TabularSkeleton from '../TabularExplorer/TabularSkeleton.vue'
|
|
226
179
|
import DataStructure from '../ResourceAccordion/DataStructure.vue'
|
|
227
|
-
import Downloads from '../ResourceAccordion/Downloads.vue'
|
|
228
180
|
import Metadata from '../ResourceAccordion/Metadata.vue'
|
|
229
|
-
import
|
|
230
|
-
import ResourceSelector from './ResourceSelector.vue'
|
|
231
|
-
import { filesize, summarize } from '../../functions/helpers'
|
|
232
|
-
import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
|
|
181
|
+
import ResourceViewerHeader from './ResourceViewerHeader.vue'
|
|
233
182
|
import { trackEvent } from '../../functions/matomo'
|
|
234
183
|
import { useComponentsConfig } from '../../config'
|
|
235
|
-
import { useFormatDate } from '../../functions/dates'
|
|
236
184
|
import { useTranslation } from '../../composables/useTranslation'
|
|
237
185
|
import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
|
|
238
186
|
import { provideTabularProfile } from '../../composables/useTabularProfile'
|
|
187
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
239
188
|
import type { Resource } from '../../types/resources'
|
|
240
189
|
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
241
190
|
|
|
@@ -248,6 +197,9 @@ const PdfPreview = defineAsyncComponent(() =>
|
|
|
248
197
|
const XmlPreview = defineAsyncComponent(() =>
|
|
249
198
|
import('../ResourceAccordion/XmlPreview.client.vue'),
|
|
250
199
|
)
|
|
200
|
+
const ImagePreview = defineAsyncComponent(() =>
|
|
201
|
+
import('../ResourceAccordion/ImagePreview.client.vue'),
|
|
202
|
+
)
|
|
251
203
|
const DatafairPreview = defineAsyncComponent(() =>
|
|
252
204
|
import('../ResourceAccordion/Datafair.client.vue'),
|
|
253
205
|
)
|
|
@@ -258,60 +210,61 @@ const Pmtiles = defineAsyncComponent(() =>
|
|
|
258
210
|
import('../ResourceAccordion/Pmtiles.client.vue'),
|
|
259
211
|
)
|
|
260
212
|
|
|
261
|
-
const props = defineProps<{
|
|
213
|
+
const props = withDefaults(defineProps<{
|
|
262
214
|
dataset: Dataset | DatasetV2
|
|
263
215
|
resource: Resource
|
|
264
216
|
resources?: Resource[]
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
217
|
+
resourceTo: (resource: Resource) => RouteLocationRaw
|
|
218
|
+
// When provided (inline mode), shows an "Explorer" button next to the download
|
|
219
|
+
// action that opens the fullscreen explorer on the current resource.
|
|
220
|
+
exploreTo?: (resource: Resource) => string
|
|
221
|
+
// Overrides the "Copier le lien" target.
|
|
222
|
+
resourceExternalUrl?: (resource: Resource) => string
|
|
223
|
+
replace?: boolean
|
|
224
|
+
// Fullscreen mode: make the viewer a flex column so the table fills down to the
|
|
225
|
+
// bottom, and hide the inline download/visit/copy actions — they're shown in the
|
|
226
|
+
// dataset context bar above. Inline mode (dataset page) shows them in the header.
|
|
227
|
+
fullscreen?: boolean
|
|
228
|
+
}>(), {
|
|
229
|
+
fullscreen: false,
|
|
230
|
+
})
|
|
270
231
|
|
|
271
232
|
const { t } = useTranslation()
|
|
272
233
|
const config = useComponentsConfig()
|
|
273
|
-
const
|
|
234
|
+
const route = useRoute()
|
|
235
|
+
const router = useRouter()
|
|
274
236
|
|
|
275
237
|
const {
|
|
238
|
+
previewKind,
|
|
276
239
|
hasTabularData,
|
|
277
240
|
hasPmtiles,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
ogcService,
|
|
241
|
+
hasPmtilesError,
|
|
242
|
+
pmtilesError,
|
|
281
243
|
ogcWms,
|
|
282
|
-
isResourceUrl,
|
|
283
244
|
tabsOptions,
|
|
284
245
|
} = useResourceCapabilities(() => props.resource, () => props.dataset)
|
|
285
246
|
|
|
286
247
|
// Share the tabular profile fetch between TabularExplorer and DataStructure tabs.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
248
|
+
// Only tabular resources have one: asking for the profile of a PDF or an image is a
|
|
249
|
+
// request to the Tabular API that can only fail.
|
|
250
|
+
if (hasTabularData.value) {
|
|
251
|
+
await provideTabularProfile(() => props.resource.id)
|
|
252
|
+
}
|
|
291
253
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
return t('Télécharger le fichier en {format}', { format: format.value })
|
|
254
|
+
// The active tab lives in the URL so a shared link opens on the same one. Read once
|
|
255
|
+
// at mount (TabGroup only takes an initial index), which is enough: switching resource
|
|
256
|
+
// remounts the viewer, and the tab is preserved when the new resource also has it.
|
|
257
|
+
const defaultTabIndex = computed(() => {
|
|
258
|
+
const index = tabsOptions.value.findIndex(option => option.key === route.query.tab)
|
|
259
|
+
return index === -1 ? 0 : index
|
|
300
260
|
})
|
|
301
261
|
|
|
302
|
-
const copyResourceUrl = async () => {
|
|
303
|
-
try {
|
|
304
|
-
await navigator.clipboard.writeText(props.resource.url)
|
|
305
|
-
toast.success(t('Lien copié !'))
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
toast.error(t('Impossible de copier dans le presse-papier'))
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
262
|
const switchTab = (index: number) => {
|
|
313
263
|
const option = tabsOptions.value[index]
|
|
314
264
|
if (!option) return
|
|
265
|
+
// The first tab is the default: drop `tab` rather than writing it in the URL.
|
|
266
|
+
const { tab: _, ...query } = route.query
|
|
267
|
+
router.replace({ query: index === 0 ? query : { ...query, tab: option.key } })
|
|
315
268
|
trackEvent('View resource tab', props.resource.id, option.label)
|
|
316
269
|
if (option.key === 'data') {
|
|
317
270
|
trackEvent('Show preview', props.resource.id)
|