@datagouv/components-next 1.0.2-dev.8 → 1.0.2-dev.81

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 (83) hide show
  1. package/assets/main.css +4 -0
  2. package/dist/Datafair.client-BzW-ctDf.js +30 -0
  3. package/dist/JsonPreview.client-BfMSzR07.js +40 -0
  4. package/dist/{MapContainer.client-DRkAmdOc.js → MapContainer.client-CLs-im9i.js} +35 -38
  5. package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-C13PQCU_.js} +822 -865
  6. package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-CL7PXXDl.js} +574 -579
  7. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C6XnsZ-7.js +61 -0
  8. package/dist/XmlPreview.client-KaENrbbG.js +34 -0
  9. package/dist/components-next.css +3 -3
  10. package/dist/components-next.js +166 -148
  11. package/dist/components.css +1 -1
  12. package/dist/{index-SrYZwgCT.js → index-C7WVVGgD.js} +1 -1
  13. package/dist/{main-B2kXxWRG.js → main-K-42Oe8-.js} +91315 -75834
  14. package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-sHPSE-jD.js} +1 -1
  15. package/package.json +16 -10
  16. package/src/components/ActivityList/ActivityList.vue +0 -2
  17. package/src/components/Chart/ChartViewer.vue +226 -0
  18. package/src/components/Chart/ChartViewerWrapper.vue +170 -0
  19. package/src/components/Form/Listbox.vue +101 -0
  20. package/src/components/Form/SearchableSelect.vue +2 -1
  21. package/src/components/InfiniteLoader.vue +53 -0
  22. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  23. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  24. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  25. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  26. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  27. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  28. package/src/components/OpenApiViewer/openapi.ts +150 -0
  29. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  30. package/src/components/Pagination.vue +8 -5
  31. package/src/components/ReadMore.vue +1 -1
  32. package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
  33. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
  34. package/src/components/ResourceAccordion/MapContainer.client.vue +7 -11
  35. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  36. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -103
  37. package/src/components/ResourceAccordion/Pmtiles.client.vue +5 -10
  38. package/src/components/ResourceAccordion/Preview.vue +16 -21
  39. package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
  40. package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
  41. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  42. package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -7
  43. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
  44. package/src/components/ResourceExplorer/ResourceExplorer.vue +81 -13
  45. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  46. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +30 -11
  47. package/src/components/Search/GlobalSearch.vue +173 -108
  48. package/src/components/Search/SearchInput.vue +3 -3
  49. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  50. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  51. package/src/components/TabularExplorer/TabularExplorer.vue +870 -0
  52. package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
  53. package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
  54. package/src/components/TabularExplorer/types.ts +83 -0
  55. package/src/composables/useHasTabularData.ts +6 -0
  56. package/src/composables/useResourceCapabilities.ts +1 -1
  57. package/src/composables/useSearchFilter.ts +118 -0
  58. package/src/composables/useStableQueryParams.ts +31 -3
  59. package/src/config.ts +3 -0
  60. package/src/functions/api.ts +34 -33
  61. package/src/functions/api.types.ts +1 -0
  62. package/src/functions/charts.ts +68 -0
  63. package/src/functions/datasets.ts +0 -17
  64. package/src/functions/resources.ts +56 -1
  65. package/src/functions/tabular.ts +60 -0
  66. package/src/functions/tabularApi.ts +138 -11
  67. package/src/main.ts +55 -7
  68. package/src/types/dataservices.ts +2 -0
  69. package/src/types/pages.ts +0 -5
  70. package/src/types/posts.ts +2 -2
  71. package/src/types/reports.ts +3 -0
  72. package/src/types/search.ts +52 -1
  73. package/src/types/site.ts +5 -3
  74. package/src/types/users.ts +0 -1
  75. package/src/types/visualizations.ts +89 -0
  76. package/assets/swagger-themes/newspaper.css +0 -1670
  77. package/dist/Datafair.client-E5D6ePRC.js +0 -35
  78. package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
  79. package/dist/Swagger.client-D4-F6yEf.js +0 -4
  80. package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
  81. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  82. package/src/functions/pagination.ts +0 -9
  83. /package/assets/illustrations/{_microscope.svg → microscope.svg} +0 -0
@@ -1,64 +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
- <SimpleBanner
22
- v-else-if="fileTooLarge"
23
- type="warning"
24
- class="flex items-center space-x-2"
25
- >
26
- <RiErrorWarningLine class="flex-none size-6" />
27
- <span>{{ fileSizeBytes
28
- ? t("Fichier PDF trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
29
- : t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
30
- }}</span>
31
- </SimpleBanner>
32
- <SimpleBanner
33
- v-else-if="error === 'network'"
34
- type="warning"
35
- class="flex items-center space-x-2"
36
- >
37
- <RiErrorWarningLine class="flex-none size-6" />
38
- <span>{{ t("Ce fichier PDF ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
39
- </SimpleBanner>
40
- <SimpleBanner
41
- v-else-if="error"
42
- type="warning"
43
- class="flex items-center space-x-2"
44
- >
45
- <RiErrorWarningLine class="flex-none size-6" />
46
- <span>{{ t("Erreur lors du chargement de l'aperçu PDF. Pour consulter le fichier, téléchargez-le depuis l'onglet Téléchargements.") }}</span>
47
- </SimpleBanner>
48
- </div>
21
+ </PreviewWrapper>
49
22
  </template>
50
23
 
51
24
  <script setup lang="ts">
52
- import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
53
- import { RiErrorWarningLine } from '@remixicon/vue'
25
+ import { onBeforeUnmount, ref } from 'vue'
54
26
  import * as pdfjsLib from 'pdfjs-dist'
55
27
  import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
56
28
  import type { PDFDocumentProxy } from 'pdfjs-dist'
57
- import SimpleBanner from '../SimpleBanner.vue'
29
+ import PreviewWrapper from './PreviewWrapper.vue'
58
30
  import { useComponentsConfig } from '../../config'
59
31
  import type { Resource } from '../../types/resources'
60
- import { useTranslation } from '../../composables/useTranslation'
61
- import { getResourceFilesize } from '../../functions/datasets'
62
32
 
63
33
  pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
64
34
 
@@ -67,15 +37,8 @@ const props = defineProps<{
67
37
  }>()
68
38
 
69
39
  const config = useComponentsConfig()
70
- const { t } = useTranslation()
71
40
 
72
41
  const containerRef = ref<HTMLElement | null>(null)
73
- const pdfReady = ref(false)
74
- const loading = ref(false)
75
- const error = ref<string | null>(null)
76
- const fileTooLarge = ref(false)
77
- const totalPages = ref(0)
78
-
79
42
  let pdfDoc: PDFDocumentProxy | null = null
80
43
  const canvasRefs = new Map<number, HTMLCanvasElement>()
81
44
 
@@ -111,64 +74,22 @@ async function renderPage(pageNum: number) {
111
74
  }).promise
112
75
  }
113
76
 
114
- const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
115
-
116
- const shouldLoadPdf = computed(() => {
117
- const size = fileSizeBytes.value
118
- if (!size) {
119
- // If we don't know the size, don't risk loading a potentially huge file
120
- return false
121
- }
122
-
123
- // Use maxPdfPreviewByteSize from config, fallback to 10 MB if not set
124
- const maxByteSize = config.maxPdfPreviewByteSize ?? 10_000_000
125
- return size <= maxByteSize
126
- })
127
-
128
- const loadPdf = async () => {
129
- if (!shouldLoadPdf.value) {
130
- fileTooLarge.value = true
131
- return
132
- }
133
-
134
- loading.value = true
135
- error.value = null
136
-
137
- try {
138
- const loadingTask = pdfjsLib.getDocument({
139
- url: props.resource.url,
140
- isEvalSupported: false,
141
- })
142
-
143
- pdfDoc = await loadingTask.promise
144
- totalPages.value = pdfDoc.numPages
145
- pdfReady.value = true
146
-
147
- 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
+ }
148
85
 
149
- for (let i = 1; i <= pdfDoc.numPages; i++) {
150
- await renderPage(i)
151
- }
152
- }
153
- catch (err) {
154
- console.error('Error loading PDF:', err)
155
-
156
- if (err instanceof TypeError) {
157
- error.value = 'network'
158
- }
159
- else {
160
- error.value = 'generic'
161
- }
162
- }
163
- finally {
164
- loading.value = false
86
+ const renderAllPages = async () => {
87
+ if (!pdfDoc) return
88
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
89
+ await renderPage(i)
165
90
  }
166
91
  }
167
92
 
168
- onMounted(() => {
169
- loadPdf()
170
- })
171
-
172
93
  onBeforeUnmount(() => {
173
94
  pdfDoc?.destroy()
174
95
  })
@@ -1,13 +1,8 @@
1
1
  <template>
2
2
  <div>
3
- <SimpleBanner
4
- v-if="hasError"
5
- type="warning"
6
- class="flex items-center space-x-2"
7
- >
8
- <RiErrorWarningLine class="shrink-0 size-6" />
9
- <span>{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}</span>
10
- </SimpleBanner>
3
+ <PreviewUnavailable v-if="hasError">
4
+ {{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}
5
+ </PreviewUnavailable>
11
6
  <div
12
7
  v-else
13
8
  class="-mx-4"
@@ -53,7 +48,7 @@
53
48
 
54
49
  <script setup lang="ts">
55
50
  import { computed, onMounted, ref, useTemplateRef } from 'vue'
56
- import { RiErrorWarningLine, RiExternalLinkFill } from '@remixicon/vue'
51
+ import { RiExternalLinkFill } from '@remixicon/vue'
57
52
  import { Protocol, PMTiles } from 'pmtiles'
58
53
  import maplibregl from 'maplibre-gl'
59
54
  import DOMPurify from 'dompurify'
@@ -64,7 +59,7 @@ import type { Resource } from '../../types/resources'
64
59
  import type { Dataset, DatasetV2 } from '../../types/datasets'
65
60
  import BrandedButton from '../BrandedButton.vue'
66
61
  import styleVector from '../../../assets/json/vector.json'
67
- import SimpleBanner from '../SimpleBanner.vue'
62
+ import PreviewUnavailable from './PreviewUnavailable.vue'
68
63
  import { useTranslation } from '../../composables/useTranslation'
69
64
  import franceSvg from './france.svg?raw'
70
65
  import { getOwnerName, getOwnerPage } from '../../functions/owned'
@@ -1,13 +1,8 @@
1
1
  <template>
2
2
  <div>
3
- <SimpleBanner
4
- v-if="hasError"
5
- type="warning"
6
- class="flex items-center space-x-2"
7
- >
8
- <RiErrorWarningLine class="shrink-0 size-6" />
9
- <span>{{ t("L'aperçu de ce fichier n'a pas pu être chargé.") }}</span>
10
- </SimpleBanner>
3
+ <PreviewUnavailable v-if="hasError">
4
+ {{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le depuis l'onglet Téléchargements.") }}
5
+ </PreviewUnavailable>
11
6
  <PreviewLoader v-else-if="loading" />
12
7
  <div
13
8
  v-else
@@ -53,7 +48,7 @@
53
48
  >
54
49
  <BrandedButton
55
50
  color="tertiary"
56
- :icon="isSortedBy(col) && sortConfig && sortConfig.type == 'asc' ? RiArrowUpLine : RiArrowDownLine"
51
+ :icon="isSortedBy(col) && sortConfig && sortConfig.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
57
52
  icon-right
58
53
  size="xs"
59
54
  @click="sortByField(col)"
@@ -61,7 +56,7 @@
61
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… -->
62
57
  <span class="relative">
63
58
  {{ col }}
64
- <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>
65
60
  </span>
66
61
  </BrandedButton>
67
62
  </th>
@@ -105,7 +100,7 @@
105
100
 
106
101
  <script setup lang="ts">
107
102
  import { computed, onMounted, ref } from 'vue'
108
- import { RiArrowDownLine, RiArrowUpLine, RiErrorWarningLine, RiExternalLinkFill } from '@remixicon/vue'
103
+ import { RiArrowDownLine, RiArrowUpLine, RiExternalLinkFill } from '@remixicon/vue'
109
104
  import Pagination from '../Pagination.vue'
110
105
  import { getData, type SortConfig } from '../../functions/tabularApi'
111
106
  import { useFormatDate } from '../../functions/dates'
@@ -113,7 +108,7 @@ import { trackEvent } from '../../functions/matomo'
113
108
  import type { Resource } from '../../types/resources'
114
109
  import { useComponentsConfig } from '../../config'
115
110
  import BrandedButton from '../BrandedButton.vue'
116
- import SimpleBanner from '../SimpleBanner.vue'
111
+ import PreviewUnavailable from './PreviewUnavailable.vue'
117
112
  import { useTranslation } from '../../composables/useTranslation'
118
113
  import franceSvg from './france.svg?raw'
119
114
  import PreviewLoader from './PreviewLoader.vue'
@@ -127,7 +122,7 @@ const rows = ref<Array<Record<string, unknown>>>([])
127
122
  const columns = ref<Array<string>>([])
128
123
  const loading = ref(true)
129
124
  const hasError = ref(false)
130
- const sortConfig = ref<SortConfig>(null)
125
+ const sortConfig = ref<SortConfig | null>(null)
131
126
  const rowCount = ref(0)
132
127
  const config = useComponentsConfig()
133
128
  const pageSize = computed(() => config.tabularApiPageSize || 15)
@@ -143,11 +138,11 @@ function isSortedBy(col: string) {
143
138
  /**
144
139
  * Retrieve preview necessary infos
145
140
  */
146
- async function getTableInfos(page: number, sortConfig?: SortConfig) {
141
+ async function getTableInfos(page: number, sortConfig?: SortConfig | null) {
147
142
  try {
148
143
  // Check that this function return wanted data
149
144
  const response = await getData(config, props.resource.id, page, sortConfig)
150
- if ('data' in response && response.data && response.data.length > 0) {
145
+ if ('data' in response && response.data && 0 in response.data) {
151
146
  // Update existing rows
152
147
  rows.value = response.data
153
148
  columns.value = Object.keys(response.data[0]).filter(item => item !== '__id')
@@ -177,24 +172,24 @@ function changePage(page: number) {
177
172
  * Sort by a specific column
178
173
  */
179
174
  function sortByField(col: string) {
180
- if (sortConfig.value && sortConfig.value.column == col) {
181
- if (sortConfig.value.type == 'asc') {
182
- sortConfig.value.type = 'desc'
175
+ if (sortConfig.value && sortConfig.value.column === col) {
176
+ if (sortConfig.value.direction === 'asc') {
177
+ sortConfig.value.direction = 'desc'
183
178
  }
184
179
  else {
185
- sortConfig.value.type = 'asc'
180
+ sortConfig.value.direction = 'asc'
186
181
  }
187
182
  }
188
183
  else {
189
184
  if (!sortConfig.value) {
190
185
  sortConfig.value = {
191
186
  column: col,
192
- type: 'asc',
187
+ direction: 'asc',
193
188
  }
194
189
  }
195
190
  else {
196
191
  sortConfig.value.column = col
197
- sortConfig.value.type = 'asc'
192
+ sortConfig.value.direction = 'asc'
198
193
  }
199
194
  }
200
195
  currentPage.value = 1
@@ -1,7 +1,6 @@
1
1
  <template>
2
2
  <ContentLoader
3
- :width="1124"
4
- :height="300"
3
+ viewBox="0 0 1124 300"
5
4
  :speed="2"
6
5
  primary-color="#f3f3f3"
7
6
  secondary-color="#ecebeb"
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <div class="flex flex-col items-center py-12">
3
+ <img
4
+ :src="microscopeSrc"
5
+ class="h-20 mb-3"
6
+ alt=""
7
+ >
8
+ <p class="fr-text--bold mb-1">
9
+ {{ t("Aucun aperçu disponible") }}
10
+ </p>
11
+ <p class="text-sm text-gray-medium mb-0 text-center max-w-lg">
12
+ <slot />
13
+ </p>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { useTranslation } from '../../composables/useTranslation'
19
+ import microscopeSrc from '../../../assets/illustrations/microscope.svg?url'
20
+
21
+ const { t } = useTranslation()
22
+ </script>
@@ -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
  />
@@ -357,7 +357,7 @@
357
357
  <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
358
358
  <p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
359
359
  </div>
360
- <Swagger
360
+ <OpenApiViewer
361
361
  v-if="hasTabularData"
362
362
  :url="`${config.tabularApiUrl}/api/resources/${props.resource.id}/swagger/`"
363
363
  />
@@ -387,9 +387,8 @@ import { trackEvent } from '../../functions/matomo'
387
387
  import CopyButton from '../CopyButton.vue'
388
388
  import { useComponentsConfig } from '../../config'
389
389
  import { getOwnerName } from '../../functions/owned'
390
- import { getResourceFormatIcon, getResourceTitleId, detectOgcService } from '../../functions/resources'
390
+ import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
391
391
  import BrandedButton from '../BrandedButton.vue'
392
- import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
393
392
  import { useTranslation } from '../../composables/useTranslation'
394
393
  import { useHasTabularData } from '../../composables/useHasTabularData'
395
394
  import Metadata from './Metadata.vue'
@@ -399,7 +398,7 @@ import EditButton from './EditButton.vue'
399
398
  import DataStructure from './DataStructure.vue'
400
399
  import Preview from './Preview.vue'
401
400
  import { isOrganizationCertified } from '../../functions/organizations'
402
- import SwaggerClient from './Swagger.client.vue'
401
+ import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
403
402
 
404
403
  const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
405
404
  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']
@@ -418,7 +417,6 @@ const props = withDefaults(defineProps<{
418
417
 
419
418
  const config = useComponentsConfig()
420
419
 
421
- const Swagger = defineAsyncComponent(() => import('./Swagger.client.vue'))
422
420
  const MapContainer = defineAsyncComponent(() => import('./MapContainer.client.vue'))
423
421
  const Pmtiles = defineAsyncComponent(() => import('./Pmtiles.client.vue'))
424
422
  const JsonPreview = defineAsyncComponent(() => import('./JsonPreview.client.vue'))
@@ -1,59 +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
- <SimpleBanner
13
- v-else-if="fileTooLarge"
14
- type="warning"
15
- class="flex items-center space-x-2"
16
- >
17
- <RiErrorWarningLine class="shrink-0 size-6" />
18
- <span>{{ fileSizeBytes
19
- ? t("Fichier XML trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
20
- : t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
21
- }}</span>
22
- </SimpleBanner>
23
- <SimpleBanner
24
- v-else-if="error === 'network'"
25
- type="warning"
26
- class="flex items-center space-x-2"
27
- >
28
- <RiErrorWarningLine class="shrink-0 size-6" />
29
- <span>{{ t("Ce fichier XML ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
30
- </SimpleBanner>
31
- <SimpleBanner
32
- v-else-if="error"
33
- type="warning"
34
- class="flex items-center space-x-2"
35
- >
36
- <RiErrorWarningLine class="shrink-0 size-6" />
37
- <span>{{ t("Erreur lors du chargement de l'aperçu XML.") }}</span>
38
- </SimpleBanner>
39
- </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>
40
11
  </template>
41
12
 
42
13
  <script setup lang="ts">
43
- import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
44
- import { RiErrorWarningLine } from '@remixicon/vue'
45
-
14
+ import { defineAsyncComponent } from 'vue'
46
15
  import { useComponentsConfig } from '../../config'
47
- import SimpleBanner from '../SimpleBanner.vue'
16
+ import PreviewWrapper from './PreviewWrapper.vue'
48
17
  import type { Resource } from '../../types/resources'
49
- import { useTranslation } from '../../composables/useTranslation'
50
18
  import '../../types/vue3-xml-viewer.d'
51
- import { getResourceFilesize } from '../../main'
52
19
 
53
20
  const XmlViewer = defineAsyncComponent(() =>
54
- import('vue3-xml-viewer').then((module) => {
55
- return module.default || module.XmlViewer
56
- }),
21
+ import('vue3-xml-viewer').then(module => module.default || module.XmlViewer),
57
22
  )
58
23
 
59
24
  const props = defineProps<{
@@ -61,74 +26,10 @@ const props = defineProps<{
61
26
  }>()
62
27
 
63
28
  const config = useComponentsConfig()
64
- const { t } = useTranslation()
65
-
66
- const xmlData = ref<string | null>(null)
67
- const loading = ref(false)
68
- const error = ref<string | null>(null)
69
- const fileTooLarge = ref(false)
70
-
71
- const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
72
-
73
- const shouldLoadXml = computed(() => {
74
- const size = fileSizeBytes.value
75
- if (!size) {
76
- // If we don't know the size, don't risk loading a potentially huge file
77
- return false
78
- }
79
-
80
- // Check if maxXmlPreviewCharSize is configured
81
- if (!config.maxXmlPreviewCharSize) {
82
- // If no limit is set, don't load unknown files
83
- return false
84
- }
85
-
86
- // Convert maxXmlPreviewCharSize from characters to bytes (rough estimate)
87
- // Assuming average 1 byte per character for XML
88
- const maxByteSize = config.maxXmlPreviewCharSize
89
-
90
- return size <= maxByteSize
91
- })
92
29
 
93
- const fetchXmlData = async () => {
94
- // Check if file is too large or size is unknown before making the request
95
- if (!shouldLoadXml.value) {
96
- fileTooLarge.value = true
97
- return
98
- }
99
-
100
- loading.value = true
101
- error.value = null
102
-
103
- try {
104
- const response = await fetch(props.resource.url)
105
- // const response = await fetch('/test-data.xml') // For testing locally without CORS issues
106
- if (!response.ok) {
107
- throw new Error(`HTTP error! status: ${response.status}`)
108
- }
109
- const data = await response.text()
110
-
111
- // Use the XML data as string - let the XML viewer handle large files
112
- xmlData.value = data
113
- }
114
- catch (err) {
115
- console.error('Error loading XML:', err)
116
-
117
- if (err instanceof TypeError) {
118
- error.value = 'network'
119
- }
120
- else {
121
- error.value = 'generic'
122
- }
123
-
124
- xmlData.value = null
125
- }
126
- finally {
127
- loading.value = false
128
- }
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()
129
34
  }
130
-
131
- onMounted(() => {
132
- fetchXmlData()
133
- })
134
35
  </script>