@datagouv/components-next 1.0.2-dev.14 → 1.0.2-dev.140

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 (111) 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-BtQqFBTQ.js → Datafair.client-DXPU1V24.js} +4 -4
  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/JsonPreview.client-D1SnGfWG.js +40 -0
  8. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  9. package/dist/{MapContainer.client-DB6p6KF8.js → MapContainer.client-DlsaEVGx.js} +38 -39
  10. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  11. package/dist/{PdfPreview.client-Dr4VcIur.js → PdfPreview.client-C9OrLR0z.js} +823 -855
  12. package/dist/Pmtiles.client-CyylWfTy.js +26142 -0
  13. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-CtXZOHXI.js +61 -0
  14. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  15. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  16. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  17. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  18. package/dist/XmlPreview.client-DoX7RurH.js +34 -0
  19. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  20. package/dist/components-next.css +1 -52
  21. package/dist/components-next.js +174 -145
  22. package/dist/components.css +2 -2
  23. package/dist/{index-D3rieddI.js → index-Br1sa7wQ.js} +33955 -28030
  24. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-DWG7tmpQ.js} +185 -187
  25. package/dist/main-BU6kP4dE.js +73546 -0
  26. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  27. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  28. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  29. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  30. package/dist/{vue3-xml-viewer.common-CJXShyPB.js → vue3-xml-viewer.common-Cpui-Yi9.js} +16 -16
  31. package/package.json +50 -36
  32. package/src/chart.ts +5 -0
  33. package/src/components/ActivityList/ActivityList.vue +3 -2
  34. package/src/components/Chart/ChartViewer.vue +227 -0
  35. package/src/components/Chart/ChartViewerWrapper.vue +174 -0
  36. package/src/components/DataserviceCard.vue +3 -0
  37. package/src/components/DatasetCard.vue +9 -4
  38. package/src/components/Form/Listbox.vue +101 -0
  39. package/src/components/Form/SearchableSelect.vue +2 -1
  40. package/src/components/InfiniteLoader.vue +53 -0
  41. package/src/components/ObjectCardHeader.vue +11 -4
  42. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  43. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  44. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  45. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  46. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  47. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  48. package/src/components/OpenApiViewer/openapi.ts +150 -0
  49. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  50. package/src/components/Pagination.vue +8 -5
  51. package/src/components/RadioInput.vue +7 -2
  52. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  53. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  54. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  55. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
  56. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  57. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -87
  58. package/src/components/ResourceAccordion/Pmtiles.client.vue +3 -1
  59. package/src/components/ResourceAccordion/Preview.vue +11 -11
  60. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  61. package/src/components/ResourceAccordion/ResourceAccordion.vue +12 -111
  62. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  63. package/src/components/ResourceExplorer/ResourceExplorer.vue +22 -19
  64. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  65. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +50 -148
  66. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  67. package/src/components/ReuseCard.vue +12 -4
  68. package/src/components/Search/GlobalSearch.vue +217 -115
  69. package/src/components/Search/SearchInput.vue +5 -4
  70. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  71. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  72. package/src/components/TabularExplorer/TabularExplorer.vue +1106 -0
  73. package/src/components/TabularExplorer/TabularFilterContent.vue +353 -0
  74. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  75. package/src/components/TabularExplorer/types.ts +86 -0
  76. package/src/composables/useHasTabularData.ts +13 -0
  77. package/src/composables/useMetrics.ts +1 -1
  78. package/src/composables/useResourceCapabilities.ts +1 -1
  79. package/src/composables/useSearchFilter.ts +120 -0
  80. package/src/composables/useStableQueryParams.ts +38 -6
  81. package/src/composables/useTabularProfile.ts +70 -0
  82. package/src/config.ts +20 -3
  83. package/src/functions/activities.ts +3 -3
  84. package/src/functions/api.ts +20 -42
  85. package/src/functions/api.types.ts +1 -0
  86. package/src/functions/charts.ts +79 -0
  87. package/src/functions/datasets.ts +0 -17
  88. package/src/functions/dates.ts +3 -2
  89. package/src/functions/metrics.ts +6 -4
  90. package/src/functions/resources.ts +61 -1
  91. package/src/functions/tabular.ts +171 -0
  92. package/src/functions/tabularApi.ts +96 -11
  93. package/src/main.ts +95 -9
  94. package/src/types/dataservices.ts +2 -0
  95. package/src/types/pages.ts +0 -5
  96. package/src/types/posts.ts +2 -2
  97. package/src/types/reports.ts +5 -1
  98. package/src/types/resources.ts +0 -1
  99. package/src/types/search.ts +63 -1
  100. package/src/types/site.ts +5 -3
  101. package/src/types/ui.ts +2 -0
  102. package/src/types/users.ts +2 -1
  103. package/src/types/visualizations.ts +93 -0
  104. package/assets/swagger-themes/newspaper.css +0 -1670
  105. package/dist/JsonPreview.client-DojrmGyU.js +0 -72
  106. package/dist/Pmtiles.client-DbMjfK00.js +0 -25185
  107. package/dist/Swagger.client-Db_xYKJv.js +0 -4
  108. package/dist/XmlPreview.client-CHxSFhd7.js +0 -64
  109. package/dist/main-D70Xt4a7.js +0 -105854
  110. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  111. package/src/functions/pagination.ts +0 -9
@@ -1,48 +1,34 @@
1
1
  <template>
2
- <div class="text-xs">
2
+ <PreviewWrapper
3
+ v-slot="{ data }"
4
+ file-type="PDF"
5
+ :resource="resource"
6
+ :max-size="config.maxPdfPreviewByteSize"
7
+ :load="load"
8
+ @loaded="renderAllPages"
9
+ >
3
10
  <div
4
- v-if="pdfReady"
5
11
  ref="containerRef"
6
12
  class="w-full overflow-y-auto max-h-[80vh] space-y-3"
7
13
  >
8
14
  <canvas
9
- v-for="page in totalPages"
15
+ v-for="page in (data as PDFDocumentProxy).numPages"
10
16
  :key="page"
11
17
  :ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
12
18
  class="w-full"
13
19
  />
14
20
  </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>
21
+ </PreviewWrapper>
34
22
  </template>
35
23
 
36
24
  <script setup lang="ts">
37
- import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
25
+ import { onBeforeUnmount, ref } from 'vue'
38
26
  import * as pdfjsLib from 'pdfjs-dist'
39
27
  import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
40
28
  import type { PDFDocumentProxy } from 'pdfjs-dist'
41
- import PreviewUnavailable from './PreviewUnavailable.vue'
29
+ import PreviewWrapper from './PreviewWrapper.vue'
42
30
  import { useComponentsConfig } from '../../config'
43
31
  import type { Resource } from '../../types/resources'
44
- import { useTranslation } from '../../composables/useTranslation'
45
- import { getResourceFilesize } from '../../functions/datasets'
46
32
 
47
33
  pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
48
34
 
@@ -51,15 +37,8 @@ const props = defineProps<{
51
37
  }>()
52
38
 
53
39
  const config = useComponentsConfig()
54
- const { t } = useTranslation()
55
40
 
56
41
  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
42
  let pdfDoc: PDFDocumentProxy | null = null
64
43
  const canvasRefs = new Map<number, HTMLCanvasElement>()
65
44
 
@@ -95,64 +74,22 @@ async function renderPage(pageNum: number) {
95
74
  }).promise
96
75
  }
97
76
 
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()
77
+ const load = async () => {
78
+ const loadingTask = pdfjsLib.getDocument({
79
+ url: props.resource.url,
80
+ isEvalSupported: false,
81
+ })
82
+ pdfDoc = await loadingTask.promise
83
+ return pdfDoc
84
+ }
132
85
 
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
86
+ const renderAllPages = async () => {
87
+ if (!pdfDoc) return
88
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
89
+ await renderPage(i)
149
90
  }
150
91
  }
151
92
 
152
- onMounted(() => {
153
- loadPdf()
154
- })
155
-
156
93
  onBeforeUnmount(() => {
157
94
  pdfDoc?.destroy()
158
95
  })
@@ -107,7 +107,9 @@ async function displayMap() {
107
107
  p.getHeader().then((h: { maxZoom: number, centerLon: number, centerLat: number }) => {
108
108
  const map = new maplibregl.Map({
109
109
  container: container.value!,
110
- style: styleVector as maplibregl.StyleSpecification,
110
+ // JSON import is inferred too loosely (e.g. center as number[]) to match
111
+ // maplibre's strict StyleSpecification, so cast through unknown.
112
+ style: styleVector as unknown as maplibregl.StyleSpecification,
111
113
  zoom: h.maxZoom - 2,
112
114
  center: [h.centerLon, h.centerLat],
113
115
  })
@@ -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,82 @@
1
+ <template>
2
+ <div class="text-xs">
3
+ <slot
4
+ v-if="data !== null"
5
+ :data="data"
6
+ />
7
+ <div
8
+ v-else-if="loading"
9
+ class="text-gray-medium"
10
+ >
11
+ {{ t("Chargement de l'aperçu {fileType}...", { fileType }) }}
12
+ </div>
13
+ <PreviewUnavailable v-else-if="!isSizeAllowed">
14
+ {{ fileSizeBytes
15
+ ? t("Le fichier {fileType} est trop volumineux pour être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.", { fileType })
16
+ : t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le depuis l'onglet Téléchargements.")
17
+ }}
18
+ </PreviewUnavailable>
19
+ <PreviewUnavailable v-else-if="corsStatus === 'blocked'">
20
+ {{ 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 depuis l'onglet Téléchargements.", { fileType }) }}
21
+ </PreviewUnavailable>
22
+ <PreviewUnavailable v-else-if="error === 'network'">
23
+ {{ 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.") }}
24
+ </PreviewUnavailable>
25
+ <PreviewUnavailable v-else-if="error">
26
+ {{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le depuis l'onglet Téléchargements.") }}
27
+ </PreviewUnavailable>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { computed, nextTick, onMounted, ref } from 'vue'
33
+ import PreviewUnavailable from './PreviewUnavailable.vue'
34
+ import type { Resource } from '../../types/resources'
35
+ import { getResourceFilesize, getResourceCorsStatus } from '../../functions/resources'
36
+ import { useTranslation } from '../../composables/useTranslation'
37
+
38
+ const props = defineProps<{
39
+ fileType: string
40
+ resource: Resource
41
+ maxSize: number | undefined
42
+ load: () => Promise<unknown>
43
+ }>()
44
+
45
+ const emit = defineEmits<{
46
+ loaded: []
47
+ }>()
48
+
49
+ const { t } = useTranslation()
50
+
51
+ const data = ref<unknown>(null)
52
+ const loading = ref(false)
53
+ const error = ref<'network' | 'generic' | null>(null)
54
+
55
+ const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
56
+ const corsStatus = computed(() => getResourceCorsStatus(props.resource))
57
+
58
+ const isSizeAllowed = computed(() => {
59
+ const size = fileSizeBytes.value
60
+ const max = props.maxSize
61
+ if (!size || !max) return false
62
+ return size <= max
63
+ })
64
+
65
+ onMounted(async () => {
66
+ if (!isSizeAllowed.value || corsStatus.value === 'blocked') return
67
+
68
+ loading.value = true
69
+ try {
70
+ data.value = await props.load()
71
+ await nextTick()
72
+ emit('loaded')
73
+ }
74
+ catch (err) {
75
+ console.error('Error loading preview:', err)
76
+ error.value = err instanceof TypeError ? 'network' : 'generic'
77
+ }
78
+ finally {
79
+ loading.value = false
80
+ }
81
+ })
82
+ </script>
@@ -56,7 +56,7 @@
56
56
  :resource
57
57
  />
58
58
  <RiSubtractLine
59
- v-if="resource.schema"
59
+ v-if="resource.schema?.name || resource.schema?.url"
60
60
  aria-hidden="true"
61
61
  class="size-3 fill-gray-medium"
62
62
  />
@@ -231,7 +231,7 @@
231
231
  :dataset="dataset"
232
232
  />
233
233
  <!-- Show Datafair embedded preview (koumoul) -->
234
- <SwaggerClient
234
+ <OpenApiViewer
235
235
  v-else-if="hasOpenAPIPreview"
236
236
  :url="resource.extras['apidocUrl'] as string"
237
237
  />
@@ -265,91 +265,13 @@
265
265
  <div
266
266
  v-if="tab.key === 'downloads'"
267
267
  >
268
- <dl class="fr-pl-0">
269
- <dt
270
- v-if="resource.format === 'url'"
271
- class="font-bold fr-text--sm fr-mb-0"
272
- >
273
- {{ t("URL d'origine") }}
274
- </dt>
275
- <dt
276
- v-else
277
- class="font-bold fr-text--sm fr-mb-0"
278
- >
279
- {{ t('Format original') }}
280
- </dt>
281
- <dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
282
- <span v-if="resource.format === 'url'">
283
- <a
284
- :href="resource.latest"
285
- class="fr-link no-icon-after"
286
- rel="ugc nofollow noopener"
287
- target="_blank"
288
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
289
- >
290
- <component
291
- :is="config.textClamp"
292
- v-if="config && config.textClamp"
293
- :auto-resize="true"
294
- :max-lines="1"
295
- :text="resource.url"
296
- >
297
- <template #after>
298
- <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm" />
299
- </template>
300
- </component>
301
- </a>
302
- </span>
303
- <span v-else>
304
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
305
- <a
306
- :href="resource.latest"
307
- class="fr-link"
308
- rel="ugc nofollow noopener"
309
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
310
- >
311
- <span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
312
- </a>
313
- </span>
314
- <CopyButton
315
- :label="t('Copier le lien')"
316
- :copied-label="t('Lien copié !')"
317
- :text="resource.latest"
318
- class="relative"
319
- />
320
- </dd>
321
- <template v-if="generatedFormats.length">
322
- <dt class="font-bold fr-text--sm fr-mb-0">
323
- {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
324
- </dt>
325
- <dd
326
- v-for="generatedFormat in generatedFormats"
327
- :key="generatedFormat.format"
328
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
329
- >
330
- <span>
331
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
332
- <a
333
- :href="generatedFormat.url"
334
- class="fr-link"
335
- rel="ugc nofollow noopener"
336
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
337
- >
338
- <span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
339
- </a>
340
- </span>
341
- <CopyButton
342
- :label="t('Copier le lien')"
343
- :copied-label="t('Lien copié !')"
344
- :text="generatedFormat.url"
345
- class="relative"
346
- />
347
- </dd>
348
- </template>
349
- </dl>
268
+ <Downloads
269
+ :resource="resource"
270
+ :dataset="dataset"
271
+ />
350
272
  </div>
351
273
  <div
352
- v-if="tab.key === 'swagger'"
274
+ v-if="tab.key === 'api'"
353
275
  >
354
276
  <div class="fr-mb-4w">
355
277
  <p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
@@ -357,7 +279,7 @@
357
279
  <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
358
280
  <p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
359
281
  </div>
360
- <Swagger
282
+ <OpenApiViewer
361
283
  v-if="hasTabularData"
362
284
  :url="`${config.tabularApiUrl}/api/resources/${props.resource.id}/swagger/`"
363
285
  />
@@ -387,9 +309,8 @@ import { trackEvent } from '../../functions/matomo'
387
309
  import CopyButton from '../CopyButton.vue'
388
310
  import { useComponentsConfig } from '../../config'
389
311
  import { getOwnerName } from '../../functions/owned'
390
- import { getResourceFormatIcon, getResourceTitleId, detectOgcService } from '../../functions/resources'
312
+ import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
391
313
  import BrandedButton from '../BrandedButton.vue'
392
- import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
393
314
  import { useTranslation } from '../../composables/useTranslation'
394
315
  import { useHasTabularData } from '../../composables/useHasTabularData'
395
316
  import Metadata from './Metadata.vue'
@@ -397,11 +318,11 @@ import SchemaBadge from './SchemaBadge.vue'
397
318
  import ResourceIcon from './ResourceIcon.vue'
398
319
  import EditButton from './EditButton.vue'
399
320
  import DataStructure from './DataStructure.vue'
321
+ import Downloads from './Downloads.vue'
400
322
  import Preview from './Preview.vue'
401
323
  import { isOrganizationCertified } from '../../functions/organizations'
402
- import SwaggerClient from './Swagger.client.vue'
324
+ import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
403
325
 
404
- const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
405
326
  const URL_FORMATS = ['url', 'doi', 'www:link', ' www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
406
327
 
407
328
  const props = withDefaults(defineProps<{
@@ -418,7 +339,6 @@ const props = withDefaults(defineProps<{
418
339
 
419
340
  const config = useComponentsConfig()
420
341
 
421
- const Swagger = defineAsyncComponent(() => import('./Swagger.client.vue'))
422
342
  const MapContainer = defineAsyncComponent(() => import('./MapContainer.client.vue'))
423
343
  const Pmtiles = defineAsyncComponent(() => import('./Pmtiles.client.vue'))
424
344
  const JsonPreview = defineAsyncComponent(() => import('./JsonPreview.client.vue'))
@@ -462,24 +382,6 @@ const ogcService = computed(() => detectOgcService(props.resource))
462
382
 
463
383
  const ogcWms = computed(() => ogcService.value === 'wms')
464
384
 
465
- const generatedFormats = computed(() => {
466
- const formats = GENERATED_FORMATS
467
- .filter(format => `analysis:parsing:${format}_url` in props.resource.extras)
468
- .map(format => ({
469
- url: props.resource.extras[`analysis:parsing:${format}_url`] as string,
470
- size: props.resource.extras[`analysis:parsing:${format}_size`] as number | undefined,
471
- format: format,
472
- }))
473
- if ('analysis:parsing:parsing_table' in props.resource.extras) {
474
- formats.push({
475
- url: `${config.tabularApiUrl}/api/resources/${props.resource.id}/data/json/`,
476
- size: undefined,
477
- format: 'json',
478
- })
479
- }
480
- return formats
481
- })
482
-
483
385
  const open = ref(props.expandedOnMount)
484
386
  const toggle = () => {
485
387
  open.value = !open.value
@@ -515,7 +417,7 @@ const tabsOptions = computed(() => {
515
417
  options.push({ key: 'downloads', label: t('Téléchargements') })
516
418
 
517
419
  if (hasTabularData.value) {
518
- options.push({ key: 'swagger', label: t('Swagger') })
420
+ options.push({ key: 'api', label: t('API') })
519
421
  }
520
422
 
521
423
  return options
@@ -542,7 +444,6 @@ const communityResource = computed<CommunityResource | null>(() => {
542
444
  const owner = computed(() => communityResource.value ? getOwnerName(communityResource.value) : null)
543
445
 
544
446
  const lastUpdate = props.resource.last_modified
545
- const conversionsLastUpdate = computed(() => formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined))
546
447
  const availabilityChecked = props.resource.extras && 'check:available' in props.resource.extras
547
448
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
548
449
 
@@ -1,42 +1,24 @@
1
1
  <template>
2
- <div class="fr-text--xs">
3
- <div v-if="xmlData">
4
- <XmlViewer :xml="xmlData" />
5
- </div>
6
- <div
7
- v-else-if="loading"
8
- class="text-gray-medium"
9
- >
10
- {{ t("Chargement de l'aperçu XML...") }}
11
- </div>
12
- <PreviewUnavailable v-else-if="fileTooLarge">
13
- {{ fileSizeBytes
14
- ? t("Le fichier XML est trop volumineux pour être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.")
15
- : t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le depuis l'onglet Téléchargements.")
16
- }}
17
- </PreviewUnavailable>
18
- <PreviewUnavailable v-else-if="error === 'network'">
19
- {{ 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.") }}
20
- </PreviewUnavailable>
21
- <PreviewUnavailable v-else-if="error">
22
- {{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le depuis l'onglet Téléchargements.") }}
23
- </PreviewUnavailable>
24
- </div>
2
+ <PreviewWrapper
3
+ v-slot="{ data }"
4
+ file-type="XML"
5
+ :resource="resource"
6
+ :max-size="config.maxXmlPreviewCharSize"
7
+ :load="load"
8
+ >
9
+ <XmlViewer :xml="(data as string)" />
10
+ </PreviewWrapper>
25
11
  </template>
26
12
 
27
13
  <script setup lang="ts">
28
- import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
14
+ import { defineAsyncComponent } from 'vue'
29
15
  import { useComponentsConfig } from '../../config'
30
- import PreviewUnavailable from './PreviewUnavailable.vue'
16
+ import PreviewWrapper from './PreviewWrapper.vue'
31
17
  import type { Resource } from '../../types/resources'
32
- import { useTranslation } from '../../composables/useTranslation'
33
18
  import '../../types/vue3-xml-viewer.d'
34
- import { getResourceFilesize } from '../../main'
35
19
 
36
20
  const XmlViewer = defineAsyncComponent(() =>
37
- import('vue3-xml-viewer').then((module) => {
38
- return module.default || module.XmlViewer
39
- }),
21
+ import('vue3-xml-viewer').then(module => module.default || module.XmlViewer),
40
22
  )
41
23
 
42
24
  const props = defineProps<{
@@ -44,74 +26,10 @@ const props = defineProps<{
44
26
  }>()
45
27
 
46
28
  const config = useComponentsConfig()
47
- const { t } = useTranslation()
48
29
 
49
- const xmlData = ref<string | null>(null)
50
- const loading = ref(false)
51
- const error = ref<string | null>(null)
52
- const fileTooLarge = ref(false)
53
-
54
- const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
55
-
56
- const shouldLoadXml = computed(() => {
57
- const size = fileSizeBytes.value
58
- if (!size) {
59
- // If we don't know the size, don't risk loading a potentially huge file
60
- return false
61
- }
62
-
63
- // Check if maxXmlPreviewCharSize is configured
64
- if (!config.maxXmlPreviewCharSize) {
65
- // If no limit is set, don't load unknown files
66
- return false
67
- }
68
-
69
- // Convert maxXmlPreviewCharSize from characters to bytes (rough estimate)
70
- // Assuming average 1 byte per character for XML
71
- const maxByteSize = config.maxXmlPreviewCharSize
72
-
73
- return size <= maxByteSize
74
- })
75
-
76
- const fetchXmlData = async () => {
77
- // Check if file is too large or size is unknown before making the request
78
- if (!shouldLoadXml.value) {
79
- fileTooLarge.value = true
80
- return
81
- }
82
-
83
- loading.value = true
84
- error.value = null
85
-
86
- try {
87
- const response = await fetch(props.resource.url)
88
- // const response = await fetch('/test-data.xml') // For testing locally without CORS issues
89
- if (!response.ok) {
90
- throw new Error(`HTTP error! status: ${response.status}`)
91
- }
92
- const data = await response.text()
93
-
94
- // Use the XML data as string - let the XML viewer handle large files
95
- xmlData.value = data
96
- }
97
- catch (err) {
98
- console.error('Error loading XML:', err)
99
-
100
- if (err instanceof TypeError) {
101
- error.value = 'network'
102
- }
103
- else {
104
- error.value = 'generic'
105
- }
106
-
107
- xmlData.value = null
108
- }
109
- finally {
110
- loading.value = false
111
- }
30
+ const load = async () => {
31
+ const response = await fetch(props.resource.url)
32
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
33
+ return response.text()
112
34
  }
113
-
114
- onMounted(() => {
115
- fetchXmlData()
116
- })
117
35
  </script>