@datagouv/components-next 1.0.2-dev.18 → 1.0.2-dev.181

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 (151) 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-CaxMyNif.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-CPmCyisP.js +36 -0
  8. package/dist/JsonPreview.client-CKOU-LBs.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-IGYJWP5m.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-csUebU3A.js} +835 -856
  13. package/dist/Pmtiles.client-CLq7UHBg.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DeoHZXW1.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-C1toAbQ0.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 +193 -149
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-CaUlhl3k.js} +21842 -15810
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-ByYGrf8H.js} +185 -187
  26. package/dist/{main-vqhkVhVS.js → main-DD4ieUAo.js} +41500 -73223
  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-B1KPK4L4.js} +16 -16
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +3 -2
  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 +9 -0
  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 +9 -4
  45. package/src/components/DatasetQuality.vue +11 -11
  46. package/src/components/DatasetQualityInline.vue +2 -2
  47. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  48. package/src/components/Form/Listbox.vue +101 -0
  49. package/src/components/Form/SchemaSelect.vue +2 -0
  50. package/src/components/InfiniteLoader.vue +53 -0
  51. package/src/components/ObjectCardHeader.vue +11 -4
  52. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  53. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  54. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  55. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  56. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  57. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  58. package/src/components/OpenApiViewer/openapi.ts +150 -0
  59. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  60. package/src/components/Pagination.vue +8 -5
  61. package/src/components/RadioInput.vue +7 -2
  62. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  63. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  64. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  65. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  66. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  67. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  68. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  69. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  70. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  71. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  72. package/src/components/ResourceAccordion/Preview.vue +11 -11
  73. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  74. package/src/components/ResourceAccordion/ResourceAccordion.vue +23 -114
  75. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  76. package/src/components/ResourceExplorer/ResourceExplorer.vue +102 -210
  77. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +74 -0
  78. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +103 -71
  79. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  80. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  81. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  82. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +115 -0
  83. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  84. package/src/components/ResourceListItem.vue +154 -0
  85. package/src/components/ReuseCard.vue +14 -6
  86. package/src/components/ReuseDetails.vue +2 -2
  87. package/src/components/ReuseHorizontalCard.vue +2 -2
  88. package/src/components/SchemaCard.vue +10 -1
  89. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  90. package/src/components/Search/GlobalSearch.vue +219 -117
  91. package/src/components/Search/SearchInput.vue +5 -4
  92. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  93. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  94. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  95. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  96. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  97. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  98. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  99. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  100. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  101. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  102. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  103. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  104. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  105. package/src/components/TabularExplorer/types.ts +81 -0
  106. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  107. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  108. package/src/composables/useActiveDescendant.ts +5 -0
  109. package/src/composables/useDatasetResources.ts +164 -0
  110. package/src/composables/useHasTabularData.ts +15 -1
  111. package/src/composables/useMetrics.ts +1 -1
  112. package/src/composables/useResizable.ts +46 -0
  113. package/src/composables/useResourceCapabilities.ts +47 -21
  114. package/src/composables/useSearchFilter.ts +120 -0
  115. package/src/composables/useStableQueryParams.ts +38 -6
  116. package/src/composables/useTabularProfile.ts +70 -0
  117. package/src/config.ts +21 -3
  118. package/src/functions/activities.ts +3 -3
  119. package/src/functions/api.ts +20 -42
  120. package/src/functions/api.types.ts +1 -0
  121. package/src/functions/charts.ts +99 -0
  122. package/src/functions/datasets.ts +0 -17
  123. package/src/functions/dates.ts +3 -2
  124. package/src/functions/metrics.ts +6 -4
  125. package/src/functions/resources.ts +136 -1
  126. package/src/functions/schemas.ts +27 -0
  127. package/src/functions/tabular.ts +214 -0
  128. package/src/functions/tabularApi.ts +96 -11
  129. package/src/main.ts +117 -14
  130. package/src/types/dataservices.ts +17 -0
  131. package/src/types/organizations.ts +10 -0
  132. package/src/types/pages.ts +0 -5
  133. package/src/types/posts.ts +2 -2
  134. package/src/types/reports.ts +5 -1
  135. package/src/types/resources.ts +0 -1
  136. package/src/types/reuses.ts +12 -1
  137. package/src/types/search.ts +63 -1
  138. package/src/types/site.ts +5 -3
  139. package/src/types/ui.ts +2 -0
  140. package/src/types/users.ts +2 -1
  141. package/src/types/visualizations.ts +93 -0
  142. package/assets/swagger-themes/newspaper.css +0 -1670
  143. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  144. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  145. package/dist/Swagger.client-B4y0hXME.js +0 -4
  146. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  147. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  148. package/src/functions/pagination.ts +0 -9
  149. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  150. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  151. /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>
@@ -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
- <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>
@@ -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.length > 0) {
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 == col) {
176
- if (sortConfig.value.type == 'asc') {
177
- sortConfig.value.type = 'desc'
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.type = 'asc'
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
- type: 'asc',
187
+ direction: 'asc',
188
188
  }
189
189
  }
190
190
  else {
191
191
  sortConfig.value.column = col
192
- sortConfig.value.type = 'asc'
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>