@datagouv/components-next 1.2.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/README.md +7 -6
- package/dist/{Control-ZFh5ta_U.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-DSQcW7OF.js → Event-iXDrp-fy.js} +11 -11
- package/dist/{Image-BijNEG0p.js → Image-mNMBOofs.js} +5 -5
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DSl1kiwC.js} +5 -5
- package/dist/{Map-BUtPf5GN.js → Map-BfqKMw0S.js} +1306 -1166
- package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-BSfpk_rT.js} +15 -15
- package/dist/{OSM-D4MTdBtk.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BANFeM6K.js} +441 -434
- package/dist/Pmtiles.client-DH9ZrsZR.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-Dcl7oIVu.js → Tile-CSpn0Ca8.js} +153 -156
- package/dist/{TileImage-BJeHipMX.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-xp_P_OHw.js → View-BYBpt-AU.js} +167 -117
- package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-KCB5mwVf.js} +5 -5
- package/dist/{common-BjQlan3k.js → common-BoeTUnDZ.js} +26 -26
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +201 -170
- package/dist/components.css +2 -2
- package/dist/{index-NofRBuyf.js → index-DHWzX5vK.js} +5999 -5472
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-pVQfonBI.js} +162 -164
- package/dist/{main-Iz1ZCL6k.js → main-BDcYkhOc.js} +36671 -29402
- package/dist/{proj-CsNo9yH1.js → proj-DxGbobp7.js} +312 -296
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-A0fLnBZr.js → tilecoord-DvX7WWiA.js} +264 -255
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-tVI9uXUz.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +40 -39
- 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 +30 -19
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +31 -11
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +140 -184
- 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 +28 -10
- 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 +21 -5
- 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 +127 -780
- package/src/components/TabularExplorer/TabularFilterContent.vue +48 -38
- package/src/components/TabularExplorer/TabularFilterPopover.vue +27 -32
- 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 +40 -28
- 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 +48 -40
- package/src/composables/useSearchFilter.ts +7 -5
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/api.ts +13 -7
- 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 +88 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +222 -5
- 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/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.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,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,18 +138,15 @@
|
|
|
179
138
|
<div v-if="tab.key === 'metadata'">
|
|
180
139
|
<Metadata :resource />
|
|
181
140
|
</div>
|
|
182
|
-
<div v-if="tab.key === '
|
|
183
|
-
<Downloads
|
|
184
|
-
:resource="resource"
|
|
185
|
-
:dataset="dataset"
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
|
-
<div v-if="tab.key === 'swagger'">
|
|
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>
|
|
191
144
|
<p>{{ t("- Si le fichier est modifié, l'API sera mise à jour et sa structure pourra changer.") }}</p>
|
|
192
145
|
<p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
|
|
193
146
|
<p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
|
|
147
|
+
<p v-if="config.tabularApiUrl">
|
|
148
|
+
{{ t("L'URL de base de l'API est {url}", { url: config.tabularApiUrl }) }}
|
|
149
|
+
</p>
|
|
194
150
|
</div>
|
|
195
151
|
<OpenApiViewer
|
|
196
152
|
v-if="hasTabularData"
|
|
@@ -206,13 +162,9 @@
|
|
|
206
162
|
|
|
207
163
|
<script setup lang="ts">
|
|
208
164
|
import { computed, defineAsyncComponent } from 'vue'
|
|
209
|
-
import {
|
|
165
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
210
166
|
import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
|
|
211
|
-
import { toast } from 'vue-sonner'
|
|
212
|
-
import BrandedButton from '../BrandedButton.vue'
|
|
213
|
-
import CopyButton from '../CopyButton.vue'
|
|
214
167
|
import MarkdownViewer from '../MarkdownViewer.vue'
|
|
215
|
-
import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
|
|
216
168
|
import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
|
|
217
169
|
import TabGroup from '../Tabs/TabGroup.vue'
|
|
218
170
|
import TabList from '../Tabs/TabList.vue'
|
|
@@ -220,19 +172,19 @@ import Tab from '../Tabs/Tab.vue'
|
|
|
220
172
|
import TabPanels from '../Tabs/TabPanels.vue'
|
|
221
173
|
import TabPanel from '../Tabs/TabPanel.vue'
|
|
222
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'
|
|
223
179
|
import DataStructure from '../ResourceAccordion/DataStructure.vue'
|
|
224
|
-
import Downloads from '../ResourceAccordion/Downloads.vue'
|
|
225
180
|
import Metadata from '../ResourceAccordion/Metadata.vue'
|
|
226
|
-
import
|
|
227
|
-
import ResourceSelector from './ResourceSelector.vue'
|
|
228
|
-
import { filesize, summarize } from '../../functions/helpers'
|
|
229
|
-
import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
|
|
181
|
+
import ResourceViewerHeader from './ResourceViewerHeader.vue'
|
|
230
182
|
import { trackEvent } from '../../functions/matomo'
|
|
231
183
|
import { useComponentsConfig } from '../../config'
|
|
232
|
-
import { useFormatDate } from '../../functions/dates'
|
|
233
184
|
import { useTranslation } from '../../composables/useTranslation'
|
|
234
185
|
import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
|
|
235
186
|
import { provideTabularProfile } from '../../composables/useTabularProfile'
|
|
187
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
236
188
|
import type { Resource } from '../../types/resources'
|
|
237
189
|
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
238
190
|
|
|
@@ -245,6 +197,9 @@ const PdfPreview = defineAsyncComponent(() =>
|
|
|
245
197
|
const XmlPreview = defineAsyncComponent(() =>
|
|
246
198
|
import('../ResourceAccordion/XmlPreview.client.vue'),
|
|
247
199
|
)
|
|
200
|
+
const ImagePreview = defineAsyncComponent(() =>
|
|
201
|
+
import('../ResourceAccordion/ImagePreview.client.vue'),
|
|
202
|
+
)
|
|
248
203
|
const DatafairPreview = defineAsyncComponent(() =>
|
|
249
204
|
import('../ResourceAccordion/Datafair.client.vue'),
|
|
250
205
|
)
|
|
@@ -255,60 +210,61 @@ const Pmtiles = defineAsyncComponent(() =>
|
|
|
255
210
|
import('../ResourceAccordion/Pmtiles.client.vue'),
|
|
256
211
|
)
|
|
257
212
|
|
|
258
|
-
const props = defineProps<{
|
|
213
|
+
const props = withDefaults(defineProps<{
|
|
259
214
|
dataset: Dataset | DatasetV2
|
|
260
215
|
resource: Resource
|
|
261
216
|
resources?: Resource[]
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
})
|
|
267
231
|
|
|
268
232
|
const { t } = useTranslation()
|
|
269
233
|
const config = useComponentsConfig()
|
|
270
|
-
const
|
|
234
|
+
const route = useRoute()
|
|
235
|
+
const router = useRouter()
|
|
271
236
|
|
|
272
237
|
const {
|
|
238
|
+
previewKind,
|
|
273
239
|
hasTabularData,
|
|
274
240
|
hasPmtiles,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
ogcService,
|
|
241
|
+
hasPmtilesError,
|
|
242
|
+
pmtilesError,
|
|
278
243
|
ogcWms,
|
|
279
|
-
isResourceUrl,
|
|
280
244
|
tabsOptions,
|
|
281
245
|
} = useResourceCapabilities(() => props.resource, () => props.dataset)
|
|
282
246
|
|
|
283
247
|
// Share the tabular profile fetch between TabularExplorer and DataStructure tabs.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
253
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
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
|
|
297
260
|
})
|
|
298
261
|
|
|
299
|
-
const copyResourceUrl = async () => {
|
|
300
|
-
try {
|
|
301
|
-
await navigator.clipboard.writeText(props.resource.url)
|
|
302
|
-
toast.success(t('Lien copié !'))
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
toast.error(t('Impossible de copier dans le presse-papier'))
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
262
|
const switchTab = (index: number) => {
|
|
310
263
|
const option = tabsOptions.value[index]
|
|
311
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 } })
|
|
312
268
|
trackEvent('View resource tab', props.resource.id, option.label)
|
|
313
269
|
if (option.key === 'data') {
|
|
314
270
|
trackEvent('Show preview', props.resource.id)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- A resource that is only a link is visited, not downloaded. -->
|
|
3
|
+
<BrandedButton
|
|
4
|
+
v-if="isResourceUrl"
|
|
5
|
+
:href="resource.latest"
|
|
6
|
+
:title="t('Lien du fichier - ouvre une nouvelle fenêtre')"
|
|
7
|
+
rel="ugc nofollow noopener"
|
|
8
|
+
new-tab
|
|
9
|
+
size="xs"
|
|
10
|
+
external
|
|
11
|
+
@click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
|
|
12
|
+
>
|
|
13
|
+
{{ t('Visiter') }}
|
|
14
|
+
</BrandedButton>
|
|
15
|
+
|
|
16
|
+
<!-- An OGC endpoint with nothing to export: its URL is the only useful thing to
|
|
17
|
+
hand over, to paste into a GIS. -->
|
|
18
|
+
<BrandedButton
|
|
19
|
+
v-else-if="!groups.length"
|
|
20
|
+
:icon="RiFileCopyLine"
|
|
21
|
+
color="primary"
|
|
22
|
+
size="xs"
|
|
23
|
+
@click="copyServiceUrl"
|
|
24
|
+
>
|
|
25
|
+
{{ t('Copier le lien') }}
|
|
26
|
+
</BrandedButton>
|
|
27
|
+
|
|
28
|
+
<!-- A single downloadable format needs no menu: a plain download button links
|
|
29
|
+
straight to it, matching the previous behaviour. -->
|
|
30
|
+
<BrandedButton
|
|
31
|
+
v-else-if="singleDownload"
|
|
32
|
+
:href="singleDownload.url"
|
|
33
|
+
:icon="RiDownloadLine"
|
|
34
|
+
rel="ugc nofollow noopener"
|
|
35
|
+
download
|
|
36
|
+
external
|
|
37
|
+
class="matomo_download"
|
|
38
|
+
size="xs"
|
|
39
|
+
color="primary"
|
|
40
|
+
@click="trackDownload(singleDownload.format)"
|
|
41
|
+
>
|
|
42
|
+
{{ t('Télécharger') }}
|
|
43
|
+
</BrandedButton>
|
|
44
|
+
|
|
45
|
+
<Popover
|
|
46
|
+
v-else
|
|
47
|
+
ref="anchor"
|
|
48
|
+
class="relative"
|
|
49
|
+
>
|
|
50
|
+
<PopoverButton
|
|
51
|
+
:as="BrandedButton"
|
|
52
|
+
:icon="RiDownloadLine"
|
|
53
|
+
size="xs"
|
|
54
|
+
color="primary"
|
|
55
|
+
aria-haspopup="menu"
|
|
56
|
+
>
|
|
57
|
+
{{ t('Télécharger') }}
|
|
58
|
+
</PopoverButton>
|
|
59
|
+
|
|
60
|
+
<ClientOnly>
|
|
61
|
+
<Teleport to="#tooltips">
|
|
62
|
+
<PopoverPanel
|
|
63
|
+
v-slot="{ close }"
|
|
64
|
+
ref="panel"
|
|
65
|
+
class="absolute z-[800] w-72 overflow-hidden rounded border border-gray-default bg-white shadow-lg"
|
|
66
|
+
:style="floatingStyles"
|
|
67
|
+
>
|
|
68
|
+
<div
|
|
69
|
+
v-for="group in groups"
|
|
70
|
+
:key="group.title"
|
|
71
|
+
>
|
|
72
|
+
<div class="border-b border-gray-default bg-gray-some px-2 py-1.5 text-[12px] font-bold uppercase leading-tight text-gray-title">
|
|
73
|
+
{{ group.title }}
|
|
74
|
+
</div>
|
|
75
|
+
<div
|
|
76
|
+
v-for="item in group.items"
|
|
77
|
+
:key="item.url"
|
|
78
|
+
class="group/dl flex items-center border-b border-gray-default last:border-b-0 hover:bg-gray-some"
|
|
79
|
+
>
|
|
80
|
+
<a
|
|
81
|
+
:href="item.url"
|
|
82
|
+
class="matomo_download flex min-w-0 flex-1 items-center gap-2 py-2 pl-2 pr-1 text-[14px] leading-tight text-gray-title !bg-none !no-underline"
|
|
83
|
+
rel="ugc nofollow noopener"
|
|
84
|
+
download
|
|
85
|
+
@click="trackDownload(item.format); close()"
|
|
86
|
+
>
|
|
87
|
+
<span class="min-w-0 flex-1 truncate">{{ t('Format {format}', { format: item.format }) }}</span>
|
|
88
|
+
<span
|
|
89
|
+
v-if="item.size"
|
|
90
|
+
class="shrink-0 rounded bg-gray-lower px-1 text-[13px] tabular-nums text-gray-medium"
|
|
91
|
+
>{{ filesize(item.size) }}</span>
|
|
92
|
+
</a>
|
|
93
|
+
<CopyButton
|
|
94
|
+
:label="t('Copier le lien')"
|
|
95
|
+
:copied-label="t('Lien copié !')"
|
|
96
|
+
:text="item.url"
|
|
97
|
+
icon-only
|
|
98
|
+
class="mr-1.5 shrink-0 opacity-0 transition-opacity focus-visible:opacity-100 group-hover/dl:opacity-100"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- The exports above are files; the service itself is consumed live by a GIS,
|
|
104
|
+
so its URL stays reachable next to them. -->
|
|
105
|
+
<div v-if="ogcService">
|
|
106
|
+
<div class="border-y border-gray-default bg-gray-some px-2 py-1.5 text-[12px] font-bold uppercase leading-tight text-gray-title">
|
|
107
|
+
{{ t('Service') }}
|
|
108
|
+
</div>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
class="flex w-full items-center gap-2 px-2 py-2 text-left text-[14px] leading-tight text-gray-title hover:bg-gray-some"
|
|
112
|
+
@click="copyServiceUrl(); close()"
|
|
113
|
+
>
|
|
114
|
+
<RiFileCopyLine
|
|
115
|
+
class="size-4 shrink-0 text-gray-medium"
|
|
116
|
+
aria-hidden="true"
|
|
117
|
+
/>
|
|
118
|
+
{{ t('Copier le lien du service') }}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</PopoverPanel>
|
|
122
|
+
</Teleport>
|
|
123
|
+
</ClientOnly>
|
|
124
|
+
</Popover>
|
|
125
|
+
</template>
|
|
126
|
+
|
|
127
|
+
<script setup lang="ts">
|
|
128
|
+
import { computed, useTemplateRef } from 'vue'
|
|
129
|
+
import { autoUpdate, flip, shift, useFloating } from '@floating-ui/vue'
|
|
130
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
131
|
+
import { RiDownloadLine, RiFileCopyLine } from '@remixicon/vue'
|
|
132
|
+
import { toast } from 'vue-sonner'
|
|
133
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
134
|
+
import CopyButton from '../CopyButton.vue'
|
|
135
|
+
import ClientOnly from '../ClientOnly.vue'
|
|
136
|
+
import { filesize } from '../../functions/helpers'
|
|
137
|
+
import { getResourceFilesize } from '../../functions/resources'
|
|
138
|
+
import { trackEvent } from '../../functions/matomo'
|
|
139
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
140
|
+
import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
|
|
141
|
+
import type { Resource } from '../../types/resources'
|
|
142
|
+
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
143
|
+
|
|
144
|
+
type DownloadItem = { url: string, format: string, size?: number }
|
|
145
|
+
type DownloadGroup = { title: string, items: DownloadItem[] }
|
|
146
|
+
|
|
147
|
+
// The one action a resource offers, whatever its kind: the two explorer headers mount
|
|
148
|
+
// this so the triage lives in a single place instead of drifting between them.
|
|
149
|
+
const props = defineProps<{
|
|
150
|
+
resource: Resource
|
|
151
|
+
dataset: Dataset | DatasetV2
|
|
152
|
+
}>()
|
|
153
|
+
|
|
154
|
+
const { t } = useTranslation()
|
|
155
|
+
|
|
156
|
+
const {
|
|
157
|
+
generatedFormats,
|
|
158
|
+
wfsFormats,
|
|
159
|
+
defaultWfsProjection,
|
|
160
|
+
isResourceUrl,
|
|
161
|
+
ogcService,
|
|
162
|
+
} = useResourceCapabilities(() => props.resource, () => props.dataset)
|
|
163
|
+
|
|
164
|
+
const groups = computed<DownloadGroup[]>(() => {
|
|
165
|
+
const result: DownloadGroup[] = []
|
|
166
|
+
|
|
167
|
+
// An OGC endpoint is not a file: `latest` points at its GetCapabilities, which is
|
|
168
|
+
// nothing a visitor wants to download. Only its exports below are.
|
|
169
|
+
if (!ogcService.value) {
|
|
170
|
+
result.push({
|
|
171
|
+
title: t('Format original'),
|
|
172
|
+
items: [{
|
|
173
|
+
url: props.resource.latest,
|
|
174
|
+
format: props.resource.format,
|
|
175
|
+
size: getResourceFilesize(props.resource) ?? undefined,
|
|
176
|
+
}],
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (generatedFormats.value.length) {
|
|
181
|
+
result.push({
|
|
182
|
+
title: t('Formats générés automatiquement'),
|
|
183
|
+
items: generatedFormats.value.map(format => ({
|
|
184
|
+
url: format.url,
|
|
185
|
+
format: format.format,
|
|
186
|
+
size: format.size,
|
|
187
|
+
})),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (wfsFormats.value.length) {
|
|
192
|
+
const projection = defaultWfsProjection.value
|
|
193
|
+
result.push({
|
|
194
|
+
title: projection
|
|
195
|
+
? t('Formats exportés depuis le service WFS (projection {crs})', { crs: projection })
|
|
196
|
+
: t('Formats exportés depuis le service WFS'),
|
|
197
|
+
items: wfsFormats.value.map(format => ({
|
|
198
|
+
url: format.url,
|
|
199
|
+
format: format.format,
|
|
200
|
+
})),
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// A single downloadable format needs no menu: expose it directly for the plain button.
|
|
208
|
+
// Never for an OGC service, whose menu also carries the link to the service itself.
|
|
209
|
+
const singleDownload = computed<DownloadItem | null>(() => {
|
|
210
|
+
if (ogcService.value) return null
|
|
211
|
+
if (groups.value.length !== 1) return null
|
|
212
|
+
const items = groups.value[0]!.items
|
|
213
|
+
return items.length === 1 ? items[0]! : null
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const trackDownload = (format: string) => {
|
|
217
|
+
trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${format}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const copyServiceUrl = async () => {
|
|
221
|
+
try {
|
|
222
|
+
await navigator.clipboard.writeText(props.resource.url)
|
|
223
|
+
toast.success(t('Lien copié !'))
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
toast.error(t('Impossible de copier dans le presse-papier'))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The panel is teleported, so it is positioned against its trigger with floating-ui.
|
|
231
|
+
const anchor = useTemplateRef<InstanceType<typeof Popover>>('anchor')
|
|
232
|
+
const panel = useTemplateRef<InstanceType<typeof PopoverPanel>>('panel')
|
|
233
|
+
const anchorEl = computed(() => anchor.value?.$el as HTMLElement | undefined)
|
|
234
|
+
const panelEl = computed(() => panel.value?.$el as HTMLElement | undefined)
|
|
235
|
+
const { floatingStyles } = useFloating(anchorEl, panelEl, {
|
|
236
|
+
placement: 'bottom-end',
|
|
237
|
+
middleware: [flip(), shift({ padding: 8 })],
|
|
238
|
+
whileElementsMounted: autoUpdate,
|
|
239
|
+
})
|
|
240
|
+
</script>
|