@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.200
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-CFTmBFoY.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-BvDM3qjE.js +36 -0
- package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
- package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
- package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
- package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.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-DFQ1T-LN.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 +196 -150
- package/dist/components.css +2 -2
- package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
- package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
- 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-CcF3Xd23.js} +1 -1
- package/package.json +47 -38
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +24 -14
- 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 +24 -5
- 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 +24 -9
- 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/Listbox.vue +101 -0
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +81 -0
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/LoadingBlock.vue +4 -1
- 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 +25 -62
- package/src/components/PostCard.vue +13 -15
- 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 +172 -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 +4 -6
- 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 +19 -15
- package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- 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 +143 -291
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +28 -9
- 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 +229 -123
- package/src/components/Search/SearchInput.vue +5 -4
- 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 +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 +84 -0
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +13 -14
- 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 +6 -7
- 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/scroll.ts +20 -0
- package/src/functions/tabular.ts +214 -0
- package/src/functions/tabularApi.ts +96 -11
- package/src/main.ts +119 -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
|
@@ -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>
|
|
@@ -4,12 +4,11 @@ import type { Resource } from '../../types/resources'
|
|
|
4
4
|
import CopyButton from '../CopyButton.vue'
|
|
5
5
|
import DescriptionDetails from '../DescriptionDetails.vue'
|
|
6
6
|
import DescriptionTerm from '../DescriptionTerm.vue'
|
|
7
|
-
import
|
|
7
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
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
|
|
@@ -20,7 +19,6 @@ const resourceTitleId = computed(() => getResourceTitleId(props.resource))
|
|
|
20
19
|
const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
21
20
|
|
|
22
21
|
const { t } = useTranslation()
|
|
23
|
-
const { formatDate } = useFormatDate()
|
|
24
22
|
</script>
|
|
25
23
|
|
|
26
24
|
<template>
|
|
@@ -95,11 +93,11 @@ const { formatDate } = useFormatDate()
|
|
|
95
93
|
>
|
|
96
94
|
<DescriptionTerm>{{ t('Créée le') }}</DescriptionTerm>
|
|
97
95
|
<DescriptionDetails>
|
|
98
|
-
|
|
96
|
+
<FormattedDate :date="resource.created_at" />
|
|
99
97
|
</DescriptionDetails>
|
|
100
98
|
<DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
|
|
101
99
|
<DescriptionDetails>
|
|
102
|
-
|
|
100
|
+
<FormattedDate :date="resource.last_modified" />
|
|
103
101
|
</DescriptionDetails>
|
|
104
102
|
</dl>
|
|
105
103
|
<dl
|
|
@@ -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>
|
|
@@ -90,7 +90,11 @@
|
|
|
90
90
|
@change="changePage"
|
|
91
91
|
/>
|
|
92
92
|
<div class="fr-px-5v">
|
|
93
|
-
|
|
93
|
+
<TranslationT keypath="Dernière mise à jour de la prévisualisation : {date}">
|
|
94
|
+
<template #date>
|
|
95
|
+
<FormattedDate :date="parsingFinishedAt" />
|
|
96
|
+
</template>
|
|
97
|
+
</TranslationT> —
|
|
94
98
|
{{ t('{count} colonnes', columns.length) }} —
|
|
95
99
|
{{ t('Lignes {count}', rowCount) }}
|
|
96
100
|
</div>
|
|
@@ -103,7 +107,8 @@ import { computed, onMounted, ref } from 'vue'
|
|
|
103
107
|
import { RiArrowDownLine, RiArrowUpLine, RiExternalLinkFill } from '@remixicon/vue'
|
|
104
108
|
import Pagination from '../Pagination.vue'
|
|
105
109
|
import { getData, type SortConfig } from '../../functions/tabularApi'
|
|
106
|
-
import
|
|
110
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
111
|
+
import TranslationT from '../TranslationT.vue'
|
|
107
112
|
import { trackEvent } from '../../functions/matomo'
|
|
108
113
|
import type { Resource } from '../../types/resources'
|
|
109
114
|
import { useComponentsConfig } from '../../config'
|
|
@@ -116,13 +121,12 @@ import PreviewLoader from './PreviewLoader.vue'
|
|
|
116
121
|
const props = defineProps<{ resource: Resource }>()
|
|
117
122
|
|
|
118
123
|
const { t } = useTranslation()
|
|
119
|
-
const { formatDate } = useFormatDate()
|
|
120
124
|
|
|
121
125
|
const rows = ref<Array<Record<string, unknown>>>([])
|
|
122
126
|
const columns = ref<Array<string>>([])
|
|
123
127
|
const loading = ref(true)
|
|
124
128
|
const hasError = ref(false)
|
|
125
|
-
const sortConfig = ref<SortConfig>(null)
|
|
129
|
+
const sortConfig = ref<SortConfig | null>(null)
|
|
126
130
|
const rowCount = ref(0)
|
|
127
131
|
const config = useComponentsConfig()
|
|
128
132
|
const pageSize = computed(() => config.tabularApiPageSize || 15)
|
|
@@ -138,11 +142,11 @@ function isSortedBy(col: string) {
|
|
|
138
142
|
/**
|
|
139
143
|
* Retrieve preview necessary infos
|
|
140
144
|
*/
|
|
141
|
-
async function getTableInfos(page: number, sortConfig?: SortConfig) {
|
|
145
|
+
async function getTableInfos(page: number, sortConfig?: SortConfig | null) {
|
|
142
146
|
try {
|
|
143
147
|
// Check that this function return wanted data
|
|
144
148
|
const response = await getData(config, props.resource.id, page, sortConfig)
|
|
145
|
-
if ('data' in response && response.data && response.data
|
|
149
|
+
if ('data' in response && response.data && 0 in response.data) {
|
|
146
150
|
// Update existing rows
|
|
147
151
|
rows.value = response.data
|
|
148
152
|
columns.value = Object.keys(response.data[0]).filter(item => item !== '__id')
|
|
@@ -172,31 +176,31 @@ function changePage(page: number) {
|
|
|
172
176
|
* Sort by a specific column
|
|
173
177
|
*/
|
|
174
178
|
function sortByField(col: string) {
|
|
175
|
-
if (sortConfig.value && sortConfig.value.column
|
|
176
|
-
if (sortConfig.value.
|
|
177
|
-
sortConfig.value.
|
|
179
|
+
if (sortConfig.value && sortConfig.value.column === col) {
|
|
180
|
+
if (sortConfig.value.direction === 'asc') {
|
|
181
|
+
sortConfig.value.direction = 'desc'
|
|
178
182
|
}
|
|
179
183
|
else {
|
|
180
|
-
sortConfig.value.
|
|
184
|
+
sortConfig.value.direction = 'asc'
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
else {
|
|
184
188
|
if (!sortConfig.value) {
|
|
185
189
|
sortConfig.value = {
|
|
186
190
|
column: col,
|
|
187
|
-
|
|
191
|
+
direction: 'asc',
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
else {
|
|
191
195
|
sortConfig.value.column = col
|
|
192
|
-
sortConfig.value.
|
|
196
|
+
sortConfig.value.direction = 'asc'
|
|
193
197
|
}
|
|
194
198
|
}
|
|
195
199
|
currentPage.value = 1
|
|
196
200
|
getTableInfos(currentPage.value, sortConfig.value)
|
|
197
201
|
};
|
|
198
202
|
|
|
199
|
-
const
|
|
203
|
+
const parsingFinishedAt = computed(() => props.resource.extras['analysis:parsing:finished_at'] as string | undefined)
|
|
200
204
|
|
|
201
205
|
onMounted(() => {
|
|
202
206
|
getTableInfos(currentPage.value)
|
|
@@ -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>
|