@datagouv/components-next 1.0.2-dev.19 → 1.0.2-dev.191
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/assets/main.css +4 -0
- package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-fxOwwaND.js} +9 -9
- package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
- package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
- package/dist/ImagePreview.client-XSR9THWR.js +36 -0
- package/dist/JsonPreview.client-CU1FWFIy.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
- package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D0-Dn7jq.js} +41 -42
- package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-B7eDgq8y.js} +835 -856
- package/dist/Pmtiles.client-CCpvggkZ.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BzhNjYoY.js +60 -0
- package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
- package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
- package/dist/XmlPreview.client-CyjpNh5i.js +34 -0
- package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +194 -149
- package/dist/components.css +2 -2
- package/dist/{index-BFQX7Ccz.js → index-BBnxH-Nj.js} +21842 -15810
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-Be0Ohp2k.js} +185 -187
- package/dist/{main-vqhkVhVS.js → main-C2kCZkPD.js} +41515 -73245
- package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-__BZsi8i.js} +16 -16
- package/package.json +47 -38
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +3 -2
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +320 -0
- package/src/components/Chart/ChartViewerWrapper.vue +182 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +9 -0
- 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 +9 -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/Form/Listbox.vue +101 -0
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/ObjectCardHeader.vue +11 -4
- package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
- package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
- package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
- package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
- package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
- package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
- package/src/components/OpenApiViewer/openapi.ts +150 -0
- package/src/components/OrganizationNameWithCertificate.vue +3 -2
- package/src/components/Pagination.vue +23 -58
- package/src/components/RadioInput.vue +7 -2
- package/src/components/ResourceAccordion/DataStructure.vue +11 -33
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +160 -0
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
- package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
- package/src/components/ResourceAccordion/Metadata.vue +1 -2
- package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
- package/src/components/ResourceAccordion/Preview.vue +11 -11
- package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +23 -114
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- package/src/components/ResourceExplorer/ResourceExplorer.vue +102 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +74 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +103 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +115 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
- package/src/components/ResourceListItem.vue +161 -0
- package/src/components/ReuseCard.vue +14 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +2 -2
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +220 -117
- package/src/components/Search/SearchInput.vue +5 -4
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +50 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
- 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 +481 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +81 -0
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +15 -1
- package/src/composables/useMetrics.ts +1 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +48 -40
- package/src/composables/useSearchFilter.ts +120 -0
- package/src/composables/useStableQueryParams.ts +38 -6
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +21 -3
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +20 -42
- package/src/functions/api.types.ts +1 -0
- package/src/functions/charts.ts +99 -0
- package/src/functions/datasets.ts +0 -17
- package/src/functions/dates.ts +3 -2
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +63 -60
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +136 -1
- package/src/functions/schemas.ts +27 -0
- package/src/functions/tabular.ts +214 -0
- package/src/functions/tabularApi.ts +96 -11
- package/src/main.ts +117 -14
- package/src/types/dataservices.ts +17 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/pages.ts +0 -5
- package/src/types/posts.ts +2 -2
- package/src/types/reports.ts +5 -1
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +63 -1
- package/src/types/site.ts +5 -3
- package/src/types/ui.ts +2 -0
- package/src/types/users.ts +2 -1
- package/src/types/visualizations.ts +93 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
- package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
- package/dist/Swagger.client-B4y0hXME.js +0 -4
- package/dist/XmlPreview.client-LETEfNBH.js +0 -64
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
- /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,48 +1,31 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</div>
|
|
20
|
-
<PreviewUnavailable v-else-if="fileTooLarge">
|
|
21
|
-
{{ fileSizeBytes
|
|
22
|
-
? t("Le fichier JSON est trop volumineux pour être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.")
|
|
23
|
-
: t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le depuis l'onglet Téléchargements.")
|
|
24
|
-
}}
|
|
25
|
-
</PreviewUnavailable>
|
|
26
|
-
<PreviewUnavailable v-else-if="error === 'network'">
|
|
27
|
-
{{ t("Ce fichier est hébergé sur un site externe qui ne permet pas la prévisualisation. Téléchargez-le depuis l'onglet Téléchargements.") }}
|
|
28
|
-
</PreviewUnavailable>
|
|
29
|
-
<PreviewUnavailable v-else-if="error">
|
|
30
|
-
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le depuis l'onglet Téléchargements.") }}
|
|
31
|
-
</PreviewUnavailable>
|
|
32
|
-
</div>
|
|
2
|
+
<PreviewWrapper
|
|
3
|
+
v-slot="{ data }"
|
|
4
|
+
file-type="JSON"
|
|
5
|
+
:resource="resource"
|
|
6
|
+
:max-size="config.maxJsonPreviewCharSize"
|
|
7
|
+
:load="load"
|
|
8
|
+
>
|
|
9
|
+
<JsonViewer
|
|
10
|
+
:value="data"
|
|
11
|
+
boxed
|
|
12
|
+
sort
|
|
13
|
+
theme="light"
|
|
14
|
+
:max-depth="3"
|
|
15
|
+
:expand-depth="2"
|
|
16
|
+
:indent-width="2"
|
|
17
|
+
/>
|
|
18
|
+
</PreviewWrapper>
|
|
33
19
|
</template>
|
|
34
20
|
|
|
35
21
|
<script setup lang="ts">
|
|
36
|
-
import {
|
|
22
|
+
import { defineAsyncComponent } from 'vue'
|
|
37
23
|
import { useComponentsConfig } from '../../config'
|
|
38
|
-
import
|
|
24
|
+
import PreviewWrapper from './PreviewWrapper.vue'
|
|
39
25
|
import type { Resource } from '../../types/resources'
|
|
40
|
-
import { useTranslation } from '../../composables/useTranslation'
|
|
41
|
-
import { getResourceFilesize } from '../../functions/datasets'
|
|
42
26
|
|
|
43
27
|
const JsonViewer = defineAsyncComponent(() =>
|
|
44
28
|
import('vue3-json-viewer').then((module) => {
|
|
45
|
-
// Import CSS when component loads
|
|
46
29
|
import('vue3-json-viewer/dist/vue3-json-viewer.css')
|
|
47
30
|
return module.JsonViewer
|
|
48
31
|
}),
|
|
@@ -53,74 +36,10 @@ const props = defineProps<{
|
|
|
53
36
|
}>()
|
|
54
37
|
|
|
55
38
|
const config = useComponentsConfig()
|
|
56
|
-
const { t } = useTranslation()
|
|
57
39
|
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
|
|
64
|
-
|
|
65
|
-
const shouldLoadJson = computed(() => {
|
|
66
|
-
const size = fileSizeBytes.value
|
|
67
|
-
if (!size) {
|
|
68
|
-
// If we don't know the size, don't risk loading a potentially huge file
|
|
69
|
-
return false
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Check if maxJsonPreviewCharSize is configured
|
|
73
|
-
if (!config.maxJsonPreviewCharSize) {
|
|
74
|
-
// If no limit is set, don't load unknown files
|
|
75
|
-
return false
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Convert maxJsonPreviewCharSize from characters to bytes (rough estimate)
|
|
79
|
-
// Assuming average 1 byte per character for JSON
|
|
80
|
-
const maxByteSize = config.maxJsonPreviewCharSize
|
|
81
|
-
|
|
82
|
-
return size <= maxByteSize
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
const fetchJsonData = async () => {
|
|
86
|
-
// Check if file is too large or size is unknown before making the request
|
|
87
|
-
if (!shouldLoadJson.value) {
|
|
88
|
-
fileTooLarge.value = true
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
loading.value = true
|
|
93
|
-
error.value = null
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const response = await fetch(props.resource.url)
|
|
97
|
-
// const response = await fetch('/test-data.json') // For testing locally without CORS issues
|
|
98
|
-
if (!response.ok) {
|
|
99
|
-
throw new Error(`HTTP error! status: ${response.status}`)
|
|
100
|
-
}
|
|
101
|
-
const data = await response.json()
|
|
102
|
-
|
|
103
|
-
// Use the original data directly - let the JSON viewer handle large files
|
|
104
|
-
jsonData.value = data
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
console.error('Error loading JSON:', err)
|
|
108
|
-
|
|
109
|
-
if (err instanceof TypeError) {
|
|
110
|
-
error.value = 'network'
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
error.value = 'generic'
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
jsonData.value = null
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
loading.value = false
|
|
120
|
-
}
|
|
40
|
+
const load = async () => {
|
|
41
|
+
const response = await fetch(props.resource.url)
|
|
42
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
|
43
|
+
return response.json()
|
|
121
44
|
}
|
|
122
|
-
|
|
123
|
-
onMounted(() => {
|
|
124
|
-
fetchJsonData()
|
|
125
|
-
})
|
|
126
45
|
</script>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
</PreviewUnavailable>
|
|
5
5
|
<div
|
|
6
6
|
v-else
|
|
7
|
-
id="map"
|
|
8
7
|
ref="mapRef"
|
|
8
|
+
class="size-full"
|
|
9
9
|
/>
|
|
10
10
|
</template>
|
|
11
11
|
|
|
@@ -89,10 +89,8 @@ async function displayMap() {
|
|
|
89
89
|
|
|
90
90
|
const attributions = new GeoportalAttribution({
|
|
91
91
|
position: 'bottom-right',
|
|
92
|
-
|
|
93
|
-
// see https://github.com/IGNF/geopf-extensions-openlayers/issues/497
|
|
92
|
+
collapsed: false,
|
|
94
93
|
})
|
|
95
|
-
attributions.setCollapsed(false)
|
|
96
94
|
map.addControl(attributions)
|
|
97
95
|
|
|
98
96
|
const layerImport = new LayerImport({
|
|
@@ -130,10 +128,3 @@ onMounted(() => {
|
|
|
130
128
|
displayMap()
|
|
131
129
|
})
|
|
132
130
|
</script>
|
|
133
|
-
|
|
134
|
-
<style>
|
|
135
|
-
#map {
|
|
136
|
-
width: 100%;
|
|
137
|
-
height: 500px;
|
|
138
|
-
}
|
|
139
|
-
</style>
|
|
@@ -7,9 +7,8 @@ import DescriptionTerm from '../DescriptionTerm.vue'
|
|
|
7
7
|
import { useFormatDate } from '../../functions/dates'
|
|
8
8
|
import { filesize } from '../../functions/helpers'
|
|
9
9
|
import ExtraAccordion from '../ExtraAccordion.vue'
|
|
10
|
-
import { getResourceTitleId, getResourceLabel } from '../../functions/resources'
|
|
10
|
+
import { getResourceTitleId, getResourceLabel, getResourceFilesize } from '../../functions/resources'
|
|
11
11
|
import { useTranslation } from '../../composables/useTranslation'
|
|
12
|
-
import { getResourceFilesize } from '../../functions/datasets'
|
|
13
12
|
|
|
14
13
|
const props = defineProps<{
|
|
15
14
|
resource: Resource
|
|
@@ -1,65 +1,68 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/>
|
|
14
|
-
</
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
2
|
+
<PreviewWrapper
|
|
3
|
+
file-type="PDF"
|
|
4
|
+
:resource="resource"
|
|
5
|
+
:max-size="config.maxPdfPreviewByteSize"
|
|
6
|
+
:load="load"
|
|
7
|
+
:fill="fill"
|
|
8
|
+
@loaded="renderAllPages"
|
|
9
|
+
>
|
|
10
|
+
<!-- While pdfjs downloads/parses the document, show the A4 skeleton instead of a
|
|
11
|
+
bare "loading" line — it matches the reader framing below seamlessly. -->
|
|
12
|
+
<template #loading>
|
|
13
|
+
<PdfSkeleton :fill="fill" />
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<!-- Cap the page width (A4 never needs more) and center on a light backdrop so
|
|
17
|
+
it reads like a document reader. The width cap applies in both modes since
|
|
18
|
+
the render width follows the container; `fill` only drives the height
|
|
19
|
+
(fullscreen fills down, inline stays capped). The scroll container is a plain
|
|
20
|
+
block (not a flex) so the trailing `pb` is reliably scrollable — flex + overflow
|
|
21
|
+
drops the end padding of flex items. -->
|
|
22
|
+
<template #default="{ data }">
|
|
23
|
+
<div
|
|
24
|
+
class="overflow-y-auto bg-gray-100"
|
|
25
|
+
:class="fill ? 'min-h-0 flex-1' : 'max-h-[80vh]'"
|
|
26
|
+
>
|
|
27
|
+
<div class="flex justify-center p-4 pb-16">
|
|
28
|
+
<div
|
|
29
|
+
ref="containerRef"
|
|
30
|
+
class="w-[800px] max-w-full space-y-3"
|
|
31
|
+
>
|
|
32
|
+
<canvas
|
|
33
|
+
v-for="page in (data as PDFDocumentProxy).numPages"
|
|
34
|
+
:key="page"
|
|
35
|
+
:ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
|
|
36
|
+
class="w-full bg-white shadow-sm"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
</PreviewWrapper>
|
|
34
43
|
</template>
|
|
35
44
|
|
|
36
45
|
<script setup lang="ts">
|
|
37
|
-
import {
|
|
46
|
+
import { onBeforeUnmount, ref } from 'vue'
|
|
38
47
|
import * as pdfjsLib from 'pdfjs-dist'
|
|
39
48
|
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
|
40
49
|
import type { PDFDocumentProxy } from 'pdfjs-dist'
|
|
41
|
-
import
|
|
50
|
+
import PreviewWrapper from './PreviewWrapper.vue'
|
|
51
|
+
import PdfSkeleton from './PdfSkeleton.vue'
|
|
42
52
|
import { useComponentsConfig } from '../../config'
|
|
43
53
|
import type { Resource } from '../../types/resources'
|
|
44
|
-
import { useTranslation } from '../../composables/useTranslation'
|
|
45
|
-
import { getResourceFilesize } from '../../functions/datasets'
|
|
46
54
|
|
|
47
55
|
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
|
|
48
56
|
|
|
49
57
|
const props = defineProps<{
|
|
50
58
|
resource: Resource
|
|
59
|
+
// Fill the available height (fullscreen) instead of the default capped height.
|
|
60
|
+
fill?: boolean
|
|
51
61
|
}>()
|
|
52
62
|
|
|
53
63
|
const config = useComponentsConfig()
|
|
54
|
-
const { t } = useTranslation()
|
|
55
64
|
|
|
56
65
|
const containerRef = ref<HTMLElement | null>(null)
|
|
57
|
-
const pdfReady = ref(false)
|
|
58
|
-
const loading = ref(false)
|
|
59
|
-
const error = ref<string | null>(null)
|
|
60
|
-
const fileTooLarge = ref(false)
|
|
61
|
-
const totalPages = ref(0)
|
|
62
|
-
|
|
63
66
|
let pdfDoc: PDFDocumentProxy | null = null
|
|
64
67
|
const canvasRefs = new Map<number, HTMLCanvasElement>()
|
|
65
68
|
|
|
@@ -95,64 +98,22 @@ async function renderPage(pageNum: number) {
|
|
|
95
98
|
}).promise
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Use maxPdfPreviewByteSize from config, fallback to 10 MB if not set
|
|
108
|
-
const maxByteSize = config.maxPdfPreviewByteSize ?? 10_000_000
|
|
109
|
-
return size <= maxByteSize
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const loadPdf = async () => {
|
|
113
|
-
if (!shouldLoadPdf.value) {
|
|
114
|
-
fileTooLarge.value = true
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
loading.value = true
|
|
119
|
-
error.value = null
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const loadingTask = pdfjsLib.getDocument({
|
|
123
|
-
url: props.resource.url,
|
|
124
|
-
isEvalSupported: false,
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
pdfDoc = await loadingTask.promise
|
|
128
|
-
totalPages.value = pdfDoc.numPages
|
|
129
|
-
pdfReady.value = true
|
|
130
|
-
|
|
131
|
-
await nextTick()
|
|
101
|
+
const load = async () => {
|
|
102
|
+
const loadingTask = pdfjsLib.getDocument({
|
|
103
|
+
url: props.resource.url,
|
|
104
|
+
isEvalSupported: false,
|
|
105
|
+
})
|
|
106
|
+
pdfDoc = await loadingTask.promise
|
|
107
|
+
return pdfDoc
|
|
108
|
+
}
|
|
132
109
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
catch (err) {
|
|
138
|
-
console.error('Error loading PDF:', err)
|
|
139
|
-
|
|
140
|
-
if (err instanceof TypeError) {
|
|
141
|
-
error.value = 'network'
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
error.value = 'generic'
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
finally {
|
|
148
|
-
loading.value = false
|
|
110
|
+
const renderAllPages = async () => {
|
|
111
|
+
if (!pdfDoc) return
|
|
112
|
+
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
|
113
|
+
await renderPage(i)
|
|
149
114
|
}
|
|
150
115
|
}
|
|
151
116
|
|
|
152
|
-
onMounted(() => {
|
|
153
|
-
loadPdf()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
117
|
onBeforeUnmount(() => {
|
|
157
118
|
pdfDoc?.destroy()
|
|
158
119
|
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- An A4 sheet on the reader backdrop with a few pulsing text lines, matching the
|
|
3
|
+
real PdfPreview framing (centered, capped ~800px, gray backdrop). -->
|
|
4
|
+
<div
|
|
5
|
+
class="overflow-hidden bg-gray-100"
|
|
6
|
+
:class="fill ? 'min-h-0 flex-1' : 'max-h-[80vh]'"
|
|
7
|
+
role="status"
|
|
8
|
+
:aria-label="t('Chargement de l\'aperçu…')"
|
|
9
|
+
>
|
|
10
|
+
<div class="flex justify-center p-4">
|
|
11
|
+
<div class="aspect-[210/297] w-[800px] max-w-full bg-white p-10 shadow-sm">
|
|
12
|
+
<div class="animate-pulse-placeholder space-y-6">
|
|
13
|
+
<div class="space-y-3">
|
|
14
|
+
<div class="h-5 w-1/2 rounded bg-gray-200" />
|
|
15
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
16
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
17
|
+
<div class="h-3 w-5/6 rounded bg-gray-200" />
|
|
18
|
+
</div>
|
|
19
|
+
<div class="space-y-3">
|
|
20
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
21
|
+
<div class="h-3 w-11/12 rounded bg-gray-200" />
|
|
22
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
23
|
+
<div class="h-3 w-3/4 rounded bg-gray-200" />
|
|
24
|
+
</div>
|
|
25
|
+
<div class="space-y-3">
|
|
26
|
+
<div class="h-3 w-full rounded bg-gray-200" />
|
|
27
|
+
<div class="h-3 w-5/6 rounded bg-gray-200" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
37
|
+
|
|
38
|
+
defineProps<{
|
|
39
|
+
// Fill the available height (fullscreen) instead of the default capped height.
|
|
40
|
+
fill?: boolean
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
const { t } = useTranslation()
|
|
44
|
+
</script>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="flex h-full flex-col">
|
|
3
3
|
<PreviewUnavailable v-if="hasError">
|
|
4
4
|
{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}
|
|
5
5
|
</PreviewUnavailable>
|
|
6
6
|
<div
|
|
7
7
|
v-else
|
|
8
|
-
class="-
|
|
8
|
+
class="flex min-h-0 flex-1 flex-col"
|
|
9
9
|
>
|
|
10
10
|
<div
|
|
11
11
|
v-if="pmtilesViewerUrl"
|
|
12
|
-
class="bg-blue-100 text-datagouv fr-hidden fr-unhidden-md p-4"
|
|
12
|
+
class="bg-blue-100 text-datagouv fr-hidden fr-unhidden-md p-4 shrink-0"
|
|
13
13
|
>
|
|
14
14
|
<div class="fr-grid-row fr-grid-row--middle fr-grid-row--gutters">
|
|
15
15
|
<div
|
|
@@ -35,12 +35,17 @@
|
|
|
35
35
|
</p>
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
|
-
<div
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
<div class="relative min-h-0 flex-1">
|
|
39
|
+
<div
|
|
40
|
+
ref="containerRef"
|
|
41
|
+
class="size-full"
|
|
42
|
+
/>
|
|
43
|
+
<!-- Freshness caption overlaid inside the map (bottom-left, the corner left
|
|
44
|
+
free by the navigation/geolocate controls top-right and the attribution
|
|
45
|
+
bottom-right) so the map itself can use the whole height. -->
|
|
46
|
+
<div class="pointer-events-none absolute bottom-2 left-2 z-10 rounded bg-white/90 px-2 py-1 text-[12px] leading-4 text-gray-medium shadow-sm">
|
|
47
|
+
{{ t("Aperçu de la carte mis à jour le {date}", { date: lastUpdate }) }}
|
|
48
|
+
</div>
|
|
44
49
|
</div>
|
|
45
50
|
</div>
|
|
46
51
|
</div>
|
|
@@ -104,12 +109,22 @@ async function displayMap() {
|
|
|
104
109
|
const p = new PMTiles(pmtilesUrl.value)
|
|
105
110
|
protocol.add(p)
|
|
106
111
|
|
|
107
|
-
p.getHeader().then((h: {
|
|
112
|
+
p.getHeader().then((h: { minLon: number, minLat: number, maxLon: number, maxLat: number, centerZoom: number, centerLon: number, centerLat: number }) => {
|
|
113
|
+
// Frame the data's bounding box (carried by the archive header) so we land on
|
|
114
|
+
// the data. Fall back to the header center when the bbox is degenerate — an
|
|
115
|
+
// archive with a single feature, or bounds left unset (all zeroes).
|
|
116
|
+
const hasBounds = h.minLon < h.maxLon && h.minLat < h.maxLat
|
|
108
117
|
const map = new maplibregl.Map({
|
|
109
118
|
container: container.value!,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
// JSON import is inferred too loosely (e.g. center as number[]) to match
|
|
120
|
+
// maplibre's strict StyleSpecification, so cast through unknown.
|
|
121
|
+
style: styleVector as unknown as maplibregl.StyleSpecification,
|
|
122
|
+
...(hasBounds
|
|
123
|
+
? {
|
|
124
|
+
bounds: [[h.minLon, h.minLat], [h.maxLon, h.maxLat]] as [[number, number], [number, number]],
|
|
125
|
+
fitBoundsOptions: { padding: 24 },
|
|
126
|
+
}
|
|
127
|
+
: { center: [h.centerLon, h.centerLat] as [number, number], zoom: h.centerZoom }),
|
|
113
128
|
})
|
|
114
129
|
map.addControl(new maplibregl.NavigationControl())
|
|
115
130
|
map.addControl(
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
>
|
|
49
49
|
<BrandedButton
|
|
50
50
|
color="tertiary"
|
|
51
|
-
:icon="isSortedBy(col) && sortConfig && sortConfig.
|
|
51
|
+
:icon="isSortedBy(col) && sortConfig && sortConfig.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
|
|
52
52
|
icon-right
|
|
53
53
|
size="xs"
|
|
54
54
|
@click="sortByField(col)"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
<!-- There is a weird bug with `sr-only`, I needed to add a relative parent to avoid full page x scrolling into the void… -->
|
|
57
57
|
<span class="relative">
|
|
58
58
|
{{ col }}
|
|
59
|
-
<span class="sr-only">{{ sortConfig && sortConfig.
|
|
59
|
+
<span class="sr-only">{{ sortConfig && sortConfig.direction === 'desc' ? t("Trier par ordre croissant") : t("Trier par ordre décroissant") }}</span>
|
|
60
60
|
</span>
|
|
61
61
|
</BrandedButton>
|
|
62
62
|
</th>
|
|
@@ -122,7 +122,7 @@ const rows = ref<Array<Record<string, unknown>>>([])
|
|
|
122
122
|
const columns = ref<Array<string>>([])
|
|
123
123
|
const loading = ref(true)
|
|
124
124
|
const hasError = ref(false)
|
|
125
|
-
const sortConfig = ref<SortConfig>(null)
|
|
125
|
+
const sortConfig = ref<SortConfig | null>(null)
|
|
126
126
|
const rowCount = ref(0)
|
|
127
127
|
const config = useComponentsConfig()
|
|
128
128
|
const pageSize = computed(() => config.tabularApiPageSize || 15)
|
|
@@ -138,11 +138,11 @@ function isSortedBy(col: string) {
|
|
|
138
138
|
/**
|
|
139
139
|
* Retrieve preview necessary infos
|
|
140
140
|
*/
|
|
141
|
-
async function getTableInfos(page: number, sortConfig?: SortConfig) {
|
|
141
|
+
async function getTableInfos(page: number, sortConfig?: SortConfig | null) {
|
|
142
142
|
try {
|
|
143
143
|
// Check that this function return wanted data
|
|
144
144
|
const response = await getData(config, props.resource.id, page, sortConfig)
|
|
145
|
-
if ('data' in response && response.data && response.data
|
|
145
|
+
if ('data' in response && response.data && 0 in response.data) {
|
|
146
146
|
// Update existing rows
|
|
147
147
|
rows.value = response.data
|
|
148
148
|
columns.value = Object.keys(response.data[0]).filter(item => item !== '__id')
|
|
@@ -172,24 +172,24 @@ function changePage(page: number) {
|
|
|
172
172
|
* Sort by a specific column
|
|
173
173
|
*/
|
|
174
174
|
function sortByField(col: string) {
|
|
175
|
-
if (sortConfig.value && sortConfig.value.column
|
|
176
|
-
if (sortConfig.value.
|
|
177
|
-
sortConfig.value.
|
|
175
|
+
if (sortConfig.value && sortConfig.value.column === col) {
|
|
176
|
+
if (sortConfig.value.direction === 'asc') {
|
|
177
|
+
sortConfig.value.direction = 'desc'
|
|
178
178
|
}
|
|
179
179
|
else {
|
|
180
|
-
sortConfig.value.
|
|
180
|
+
sortConfig.value.direction = 'asc'
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
else {
|
|
184
184
|
if (!sortConfig.value) {
|
|
185
185
|
sortConfig.value = {
|
|
186
186
|
column: col,
|
|
187
|
-
|
|
187
|
+
direction: 'asc',
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
else {
|
|
191
191
|
sortConfig.value.column = col
|
|
192
|
-
sortConfig.value.
|
|
192
|
+
sortConfig.value.direction = 'asc'
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
currentPage.value = 1
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="text-xs"
|
|
4
|
+
:class="fill ? 'flex min-h-0 flex-1 flex-col' : ''"
|
|
5
|
+
>
|
|
6
|
+
<slot
|
|
7
|
+
v-if="data !== null"
|
|
8
|
+
:data="data"
|
|
9
|
+
/>
|
|
10
|
+
<slot
|
|
11
|
+
v-else-if="loading"
|
|
12
|
+
name="loading"
|
|
13
|
+
>
|
|
14
|
+
<div class="text-gray-medium">
|
|
15
|
+
{{ t("Chargement de l'aperçu {fileType}...", { fileType }) }}
|
|
16
|
+
</div>
|
|
17
|
+
</slot>
|
|
18
|
+
<PreviewUnavailable v-else-if="!isSizeAllowed">
|
|
19
|
+
{{ fileSizeBytes
|
|
20
|
+
? t("Le fichier {fileType} est trop volumineux pour être prévisualisé. Téléchargez-le pour le consulter.", { fileType })
|
|
21
|
+
: t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le pour le consulter.")
|
|
22
|
+
}}
|
|
23
|
+
</PreviewUnavailable>
|
|
24
|
+
<PreviewUnavailable v-else-if="corsStatus === 'blocked'">
|
|
25
|
+
{{ t("Ce fichier {fileType} ne peut pas être prévisualisé car il est hébergé sur un site distant qui restreint l'accès (CORS). Téléchargez-le pour le consulter.", { fileType }) }}
|
|
26
|
+
</PreviewUnavailable>
|
|
27
|
+
<PreviewUnavailable v-else-if="error === 'network'">
|
|
28
|
+
{{ t("Ce fichier est hébergé sur un site externe qui ne permet pas la prévisualisation. Téléchargez-le pour le consulter.") }}
|
|
29
|
+
</PreviewUnavailable>
|
|
30
|
+
<PreviewUnavailable v-else-if="error">
|
|
31
|
+
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le pour le consulter.") }}
|
|
32
|
+
</PreviewUnavailable>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { computed, nextTick, onMounted, ref } from 'vue'
|
|
38
|
+
import PreviewUnavailable from './PreviewUnavailable.vue'
|
|
39
|
+
import type { Resource } from '../../types/resources'
|
|
40
|
+
import { getResourceFilesize, getResourceCorsStatus } from '../../functions/resources'
|
|
41
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
42
|
+
|
|
43
|
+
const props = defineProps<{
|
|
44
|
+
fileType: string
|
|
45
|
+
resource: Resource
|
|
46
|
+
maxSize: number | undefined
|
|
47
|
+
load: () => Promise<unknown>
|
|
48
|
+
// Fill the available height (fullscreen) instead of sizing to content.
|
|
49
|
+
fill?: boolean
|
|
50
|
+
}>()
|
|
51
|
+
|
|
52
|
+
const emit = defineEmits<{
|
|
53
|
+
loaded: []
|
|
54
|
+
}>()
|
|
55
|
+
|
|
56
|
+
const { t } = useTranslation()
|
|
57
|
+
|
|
58
|
+
const data = ref<unknown>(null)
|
|
59
|
+
const loading = ref(false)
|
|
60
|
+
const error = ref<'network' | 'generic' | null>(null)
|
|
61
|
+
|
|
62
|
+
const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
|
|
63
|
+
const corsStatus = computed(() => getResourceCorsStatus(props.resource))
|
|
64
|
+
|
|
65
|
+
const isSizeAllowed = computed(() => {
|
|
66
|
+
const size = fileSizeBytes.value
|
|
67
|
+
const max = props.maxSize
|
|
68
|
+
if (!size || !max) return false
|
|
69
|
+
return size <= max
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
onMounted(async () => {
|
|
73
|
+
if (!isSizeAllowed.value || corsStatus.value === 'blocked') return
|
|
74
|
+
|
|
75
|
+
loading.value = true
|
|
76
|
+
try {
|
|
77
|
+
data.value = await props.load()
|
|
78
|
+
await nextTick()
|
|
79
|
+
emit('loaded')
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error('Error loading preview:', err)
|
|
83
|
+
error.value = err instanceof TypeError ? 'network' : 'generic'
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
loading.value = false
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
</script>
|