@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
  8. package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
  13. package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
  15. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  16. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  17. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  18. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  19. package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
  20. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  21. package/dist/components-next.css +1 -52
  22. package/dist/components-next.js +196 -150
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
  26. package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
  27. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  28. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  29. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  30. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  31. package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +24 -14
  35. package/src/components/AppLink.vue +3 -0
  36. package/src/components/BannerAction.vue +6 -2
  37. package/src/components/Chart/ChartViewer.vue +320 -0
  38. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  39. package/src/components/CopyButton.vue +6 -2
  40. package/src/components/DataserviceCard.vue +24 -5
  41. package/src/components/DataserviceQuality.vue +82 -0
  42. package/src/components/DataserviceQualityInline.vue +31 -0
  43. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  44. package/src/components/DatasetCard.vue +24 -9
  45. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  46. package/src/components/DatasetQuality.vue +11 -11
  47. package/src/components/DatasetQualityInline.vue +2 -2
  48. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  49. package/src/components/DiscussionMessageCard.vue +14 -15
  50. package/src/components/Form/Listbox.vue +101 -0
  51. package/src/components/Form/SchemaSelect.vue +2 -0
  52. package/src/components/FormattedDate.vue +81 -0
  53. package/src/components/InfiniteLoader.vue +53 -0
  54. package/src/components/LoadingBlock.vue +4 -1
  55. package/src/components/ObjectCardHeader.vue +11 -4
  56. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  57. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  58. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  59. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  60. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  61. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  62. package/src/components/OpenApiViewer/openapi.ts +150 -0
  63. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  64. package/src/components/Pagination.vue +25 -62
  65. package/src/components/PostCard.vue +13 -15
  66. package/src/components/RadioInput.vue +7 -2
  67. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  68. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  69. package/src/components/ResourceAccordion/Downloads.vue +172 -0
  70. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  71. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  72. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  73. package/src/components/ResourceAccordion/Metadata.vue +4 -6
  74. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  75. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  76. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  77. package/src/components/ResourceAccordion/Preview.vue +19 -15
  78. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  79. package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
  80. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  81. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  82. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  83. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  84. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  85. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  86. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  87. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
  88. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  89. package/src/components/ResourceListItem.vue +168 -0
  90. package/src/components/ReuseCard.vue +28 -9
  91. package/src/components/ReuseDetails.vue +2 -2
  92. package/src/components/ReuseHorizontalCard.vue +2 -2
  93. package/src/components/SchemaCard.vue +10 -1
  94. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  95. package/src/components/Search/GlobalSearch.vue +229 -123
  96. package/src/components/Search/SearchInput.vue +5 -4
  97. package/src/components/Search/Sidemenu.vue +4 -3
  98. package/src/components/SmallChart.vue +11 -10
  99. package/src/components/StatBox.vue +13 -5
  100. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  101. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  102. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  103. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  104. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  105. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  106. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  107. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  108. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  109. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  110. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  111. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  112. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  113. package/src/components/TabularExplorer/types.ts +84 -0
  114. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  115. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  116. package/src/components/TopicCard.vue +13 -14
  117. package/src/composables/useActiveDescendant.ts +5 -0
  118. package/src/composables/useDatasetResources.ts +164 -0
  119. package/src/composables/useHasTabularData.ts +15 -1
  120. package/src/composables/useMetrics.ts +1 -1
  121. package/src/composables/useResizable.ts +46 -0
  122. package/src/composables/useResourceCapabilities.ts +48 -40
  123. package/src/composables/useSearchFilter.ts +120 -0
  124. package/src/composables/useStableQueryParams.ts +38 -6
  125. package/src/composables/useTabularProfile.ts +70 -0
  126. package/src/composables/useTranslation.ts +24 -9
  127. package/src/config.ts +21 -3
  128. package/src/functions/activities.ts +3 -3
  129. package/src/functions/api.ts +20 -42
  130. package/src/functions/api.types.ts +1 -0
  131. package/src/functions/charts.ts +99 -0
  132. package/src/functions/datasets.ts +0 -17
  133. package/src/functions/dates.ts +6 -7
  134. package/src/functions/helpers.ts +15 -0
  135. package/src/functions/metrics.ts +63 -60
  136. package/src/functions/paginate.ts +46 -0
  137. package/src/functions/resourceCapabilities.ts +34 -0
  138. package/src/functions/resources.ts +136 -1
  139. package/src/functions/schemas.ts +27 -0
  140. package/src/functions/scroll.ts +20 -0
  141. package/src/functions/tabular.ts +214 -0
  142. package/src/functions/tabularApi.ts +96 -11
  143. package/src/main.ts +119 -14
  144. package/src/types/dataservices.ts +17 -0
  145. package/src/types/organizations.ts +10 -0
  146. package/src/types/pages.ts +0 -5
  147. package/src/types/posts.ts +2 -2
  148. package/src/types/reports.ts +5 -1
  149. package/src/types/resources.ts +0 -1
  150. package/src/types/reuses.ts +12 -1
  151. package/src/types/search.ts +63 -1
  152. package/src/types/site.ts +5 -3
  153. package/src/types/ui.ts +2 -0
  154. package/src/types/users.ts +2 -1
  155. package/src/types/visualizations.ts +93 -0
  156. package/assets/swagger-themes/newspaper.css +0 -1670
  157. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  158. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  159. package/dist/Swagger.client-B4y0hXME.js +0 -4
  160. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  161. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  162. package/src/functions/pagination.ts +0 -9
  163. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  164. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  165. /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
- // collapsed option is ignored by the library, thus the override below
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 { useFormatDate } from '../../functions/dates'
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
- {{ formatDate(resource.created_at) }}
96
+ <FormattedDate :date="resource.created_at" />
99
97
  </DescriptionDetails>
100
98
  <DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
101
99
  <DescriptionDetails>
102
- {{ formatDate(resource.last_modified) }}
100
+ <FormattedDate :date="resource.last_modified" />
103
101
  </DescriptionDetails>
104
102
  </dl>
105
103
  <dl
@@ -1,65 +1,68 @@
1
1
  <template>
2
- <div class="text-xs">
3
- <div
4
- v-if="pdfReady"
5
- ref="containerRef"
6
- class="w-full overflow-y-auto max-h-[80vh] space-y-3"
7
- >
8
- <canvas
9
- v-for="page in totalPages"
10
- :key="page"
11
- :ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
12
- class="w-full"
13
- />
14
- </div>
15
- <div
16
- v-else-if="loading"
17
- class="text-gray-medium"
18
- >
19
- {{ t("Chargement de l'aperçu PDF...") }}
20
- </div>
21
- <PreviewUnavailable v-else-if="fileTooLarge">
22
- {{ fileSizeBytes
23
- ? t("Le fichier PDF est trop volumineux pour être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.")
24
- : t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le depuis l'onglet Téléchargements.")
25
- }}
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 depuis l'onglet Téléchargements.") }}
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 depuis l'onglet Téléchargements.") }}
32
- </PreviewUnavailable>
33
- </div>
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 { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
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 PreviewUnavailable from './PreviewUnavailable.vue'
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 fileSizeBytes = computed(() => getResourceFilesize(props.resource))
99
-
100
- const shouldLoadPdf = computed(() => {
101
- const size = fileSizeBytes.value
102
- if (!size) {
103
- // If we don't know the size, don't risk loading a potentially huge file
104
- return false
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
- for (let i = 1; i <= pdfDoc.numPages; i++) {
134
- await renderPage(i)
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="-mx-4"
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
- ref="containerRef"
40
- style="height: 600px;"
41
- />
42
- <div class="fr-px-5v fr-pt-5v">
43
- {{ t("Aperçu de la carte mis à jour le {date}", { date: lastUpdate }) }}
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: { maxZoom: number, centerLon: number, centerLat: number }) => {
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
- style: styleVector as maplibregl.StyleSpecification,
111
- zoom: h.maxZoom - 2,
112
- center: [h.centerLon, h.centerLat],
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.type == 'asc' ? RiArrowUpLine : RiArrowDownLine"
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.type == 'desc' ? t("Trier par ordre croissant") : t("Trier par ordre décroissant") }}</span>
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
- {{ t("Dernière mise à jour de la prévisualisation : {date}", { date: lastUpdate }) }} —
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 { useFormatDate } from '../../functions/dates'
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.length > 0) {
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 == col) {
176
- if (sortConfig.value.type == 'asc') {
177
- sortConfig.value.type = 'desc'
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.type = 'asc'
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
- type: 'asc',
191
+ direction: 'asc',
188
192
  }
189
193
  }
190
194
  else {
191
195
  sortConfig.value.column = col
192
- sortConfig.value.type = 'asc'
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 lastUpdate = computed(() => formatDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined))
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>