@datagouv/components-next 1.0.2-dev.2 → 1.0.2-dev.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) 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-CFTmBFoY.js +30 -0
  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-D9N3664r.js +101 -0
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-C6A6ZiKl.js} +835 -871
  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-SrYZwgCT.js → index-B_tPYi5Z.js} +34094 -27864
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
  26. package/dist/main-CVOZp_Nx.js +75398 -0
  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-BRxsqI9j.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
  32. package/package.json +50 -37
  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/Form/SearchableSelect.vue +2 -1
  53. package/src/components/FormattedDate.vue +81 -0
  54. package/src/components/InfiniteLoader.vue +53 -0
  55. package/src/components/LoadingBlock.vue +4 -1
  56. package/src/components/ObjectCardHeader.vue +11 -4
  57. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  58. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  59. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  60. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  61. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  62. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  63. package/src/components/OpenApiViewer/openapi.ts +150 -0
  64. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  65. package/src/components/Pagination.vue +25 -62
  66. package/src/components/PostCard.vue +13 -15
  67. package/src/components/RadioInput.vue +7 -2
  68. package/src/components/ReadMore.vue +1 -1
  69. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  70. package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
  71. package/src/components/ResourceAccordion/Downloads.vue +172 -0
  72. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  73. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
  74. package/src/components/ResourceAccordion/MapContainer.client.vue +6 -19
  75. package/src/components/ResourceAccordion/Metadata.vue +4 -6
  76. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -113
  77. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  78. package/src/components/ResourceAccordion/Pmtiles.client.vue +33 -23
  79. package/src/components/ResourceAccordion/Preview.vue +24 -25
  80. package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
  81. package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
  82. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  83. package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
  84. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
  85. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -199
  86. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  87. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  88. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -280
  89. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  90. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  91. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
  92. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  93. package/src/components/ResourceListItem.vue +168 -0
  94. package/src/components/ReuseCard.vue +28 -9
  95. package/src/components/ReuseDetails.vue +2 -2
  96. package/src/components/ReuseHorizontalCard.vue +2 -2
  97. package/src/components/SchemaCard.vue +10 -1
  98. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  99. package/src/components/Search/GlobalSearch.vue +229 -123
  100. package/src/components/Search/SearchInput.vue +5 -4
  101. package/src/components/Search/Sidemenu.vue +4 -3
  102. package/src/components/SmallChart.vue +11 -10
  103. package/src/components/StatBox.vue +13 -5
  104. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  105. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  106. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  107. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  108. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  109. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  110. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  111. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  112. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  113. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  114. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  115. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  116. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  117. package/src/components/TabularExplorer/types.ts +84 -0
  118. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  119. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  120. package/src/components/TopicCard.vue +13 -14
  121. package/src/composables/useActiveDescendant.ts +5 -0
  122. package/src/composables/useDatasetResources.ts +164 -0
  123. package/src/composables/useHasTabularData.ts +15 -1
  124. package/src/composables/useMetrics.ts +1 -1
  125. package/src/composables/useResizable.ts +46 -0
  126. package/src/composables/useResourceCapabilities.ts +49 -41
  127. package/src/composables/useSearchFilter.ts +120 -0
  128. package/src/composables/useStableQueryParams.ts +38 -6
  129. package/src/composables/useTabularProfile.ts +70 -0
  130. package/src/composables/useTranslation.ts +24 -9
  131. package/src/config.ts +21 -3
  132. package/src/functions/activities.ts +3 -3
  133. package/src/functions/api.ts +20 -42
  134. package/src/functions/api.types.ts +1 -0
  135. package/src/functions/charts.ts +99 -0
  136. package/src/functions/datasets.ts +0 -17
  137. package/src/functions/dates.ts +6 -7
  138. package/src/functions/helpers.ts +15 -0
  139. package/src/functions/metrics.ts +63 -60
  140. package/src/functions/paginate.ts +46 -0
  141. package/src/functions/resourceCapabilities.ts +34 -0
  142. package/src/functions/resources.ts +136 -1
  143. package/src/functions/schemas.ts +27 -0
  144. package/src/functions/scroll.ts +20 -0
  145. package/src/functions/tabular.ts +214 -0
  146. package/src/functions/tabularApi.ts +96 -11
  147. package/src/main.ts +119 -14
  148. package/src/types/dataservices.ts +17 -0
  149. package/src/types/organizations.ts +11 -1
  150. package/src/types/pages.ts +0 -5
  151. package/src/types/posts.ts +2 -2
  152. package/src/types/reports.ts +5 -1
  153. package/src/types/resources.ts +0 -1
  154. package/src/types/reuses.ts +12 -1
  155. package/src/types/search.ts +63 -1
  156. package/src/types/site.ts +5 -3
  157. package/src/types/ui.ts +2 -0
  158. package/src/types/users.ts +2 -1
  159. package/src/types/visualizations.ts +93 -0
  160. package/assets/swagger-themes/newspaper.css +0 -1670
  161. package/dist/Datafair.client-E5D6ePRC.js +0 -35
  162. package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
  163. package/dist/MapContainer.client-DRkAmdOc.js +0 -105
  164. package/dist/Pmtiles.client-BR7_ldHY.js +0 -25190
  165. package/dist/Swagger.client-D4-F6yEf.js +0 -4
  166. package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
  167. package/dist/main-B2kXxWRG.js +0 -105833
  168. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  169. package/src/functions/pagination.ts +0 -9
  170. /package/assets/illustrations/{_microscope.svg → microscope.svg} +0 -0
  171. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  172. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  173. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -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>
@@ -95,7 +90,11 @@
95
90
  @change="changePage"
96
91
  />
97
92
  <div class="fr-px-5v">
98
- {{ 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> —
99
98
  {{ t('{count} colonnes', columns.length) }} —
100
99
  {{ t('Lignes {count}', rowCount) }}
101
100
  </div>
@@ -105,15 +104,16 @@
105
104
 
106
105
  <script setup lang="ts">
107
106
  import { computed, onMounted, ref } from 'vue'
108
- import { RiArrowDownLine, RiArrowUpLine, RiErrorWarningLine, RiExternalLinkFill } from '@remixicon/vue'
107
+ import { RiArrowDownLine, RiArrowUpLine, RiExternalLinkFill } from '@remixicon/vue'
109
108
  import Pagination from '../Pagination.vue'
110
109
  import { getData, type SortConfig } from '../../functions/tabularApi'
111
- import { useFormatDate } from '../../functions/dates'
110
+ import FormattedDate from '../FormattedDate.vue'
111
+ import TranslationT from '../TranslationT.vue'
112
112
  import { trackEvent } from '../../functions/matomo'
113
113
  import type { Resource } from '../../types/resources'
114
114
  import { useComponentsConfig } from '../../config'
115
115
  import BrandedButton from '../BrandedButton.vue'
116
- import SimpleBanner from '../SimpleBanner.vue'
116
+ import PreviewUnavailable from './PreviewUnavailable.vue'
117
117
  import { useTranslation } from '../../composables/useTranslation'
118
118
  import franceSvg from './france.svg?raw'
119
119
  import PreviewLoader from './PreviewLoader.vue'
@@ -121,13 +121,12 @@ import PreviewLoader from './PreviewLoader.vue'
121
121
  const props = defineProps<{ resource: Resource }>()
122
122
 
123
123
  const { t } = useTranslation()
124
- const { formatDate } = useFormatDate()
125
124
 
126
125
  const rows = ref<Array<Record<string, unknown>>>([])
127
126
  const columns = ref<Array<string>>([])
128
127
  const loading = ref(true)
129
128
  const hasError = ref(false)
130
- const sortConfig = ref<SortConfig>(null)
129
+ const sortConfig = ref<SortConfig | null>(null)
131
130
  const rowCount = ref(0)
132
131
  const config = useComponentsConfig()
133
132
  const pageSize = computed(() => config.tabularApiPageSize || 15)
@@ -143,11 +142,11 @@ function isSortedBy(col: string) {
143
142
  /**
144
143
  * Retrieve preview necessary infos
145
144
  */
146
- async function getTableInfos(page: number, sortConfig?: SortConfig) {
145
+ async function getTableInfos(page: number, sortConfig?: SortConfig | null) {
147
146
  try {
148
147
  // Check that this function return wanted data
149
148
  const response = await getData(config, props.resource.id, page, sortConfig)
150
- if ('data' in response && response.data && response.data.length > 0) {
149
+ if ('data' in response && response.data && 0 in response.data) {
151
150
  // Update existing rows
152
151
  rows.value = response.data
153
152
  columns.value = Object.keys(response.data[0]).filter(item => item !== '__id')
@@ -177,31 +176,31 @@ function changePage(page: number) {
177
176
  * Sort by a specific column
178
177
  */
179
178
  function sortByField(col: string) {
180
- if (sortConfig.value && sortConfig.value.column == col) {
181
- if (sortConfig.value.type == 'asc') {
182
- sortConfig.value.type = 'desc'
179
+ if (sortConfig.value && sortConfig.value.column === col) {
180
+ if (sortConfig.value.direction === 'asc') {
181
+ sortConfig.value.direction = 'desc'
183
182
  }
184
183
  else {
185
- sortConfig.value.type = 'asc'
184
+ sortConfig.value.direction = 'asc'
186
185
  }
187
186
  }
188
187
  else {
189
188
  if (!sortConfig.value) {
190
189
  sortConfig.value = {
191
190
  column: col,
192
- type: 'asc',
191
+ direction: 'asc',
193
192
  }
194
193
  }
195
194
  else {
196
195
  sortConfig.value.column = col
197
- sortConfig.value.type = 'asc'
196
+ sortConfig.value.direction = 'asc'
198
197
  }
199
198
  }
200
199
  currentPage.value = 1
201
200
  getTableInfos(currentPage.value, sortConfig.value)
202
201
  };
203
202
 
204
- 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)
205
204
 
206
205
  onMounted(() => {
207
206
  getTableInfos(currentPage.value)
@@ -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,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>
@@ -56,11 +56,21 @@
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
  />
63
- <span class="text-xs mb-0">{{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(lastUpdate) }) }}</span>
63
+ <TranslationT
64
+ class="text-xs mb-0"
65
+ keypath="Mis à jour {date}"
66
+ >
67
+ <template #date>
68
+ <FormattedDate
69
+ :date="lastUpdate"
70
+ format="relative"
71
+ />
72
+ </template>
73
+ </TranslationT>
64
74
  <RiSubtractLine
65
75
  aria-hidden="true"
66
76
  class="size-3 fill-gray-medium"
@@ -197,7 +207,10 @@
197
207
  :key="tab.key"
198
208
  class="px-4"
199
209
  >
200
- <div v-if="tab.key === 'map'">
210
+ <div
211
+ v-if="tab.key === 'map'"
212
+ class="-mx-4 h-[600px]"
213
+ >
201
214
  <Pmtiles
202
215
  v-if="hasPmtiles"
203
216
  :resource="resource"
@@ -224,6 +237,10 @@
224
237
  v-else-if="resource.format && resource.format.toLowerCase() === 'xml'"
225
238
  :resource="resource"
226
239
  />
240
+ <ImagePreview
241
+ v-else-if="isImagePreviewFormat(resource.format)"
242
+ :resource="resource"
243
+ />
227
244
  <!-- Show Datafair embedded preview (koumoul) -->
228
245
  <DatafairPreview
229
246
  v-else-if="hasDatafairPreview"
@@ -231,7 +248,7 @@
231
248
  :dataset="dataset"
232
249
  />
233
250
  <!-- Show Datafair embedded preview (koumoul) -->
234
- <SwaggerClient
251
+ <OpenApiViewer
235
252
  v-else-if="hasOpenAPIPreview"
236
253
  :url="resource.extras['apidocUrl'] as string"
237
254
  />
@@ -265,91 +282,13 @@
265
282
  <div
266
283
  v-if="tab.key === 'downloads'"
267
284
  >
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>
285
+ <Downloads
286
+ :resource="resource"
287
+ :dataset="dataset"
288
+ />
350
289
  </div>
351
290
  <div
352
- v-if="tab.key === 'swagger'"
291
+ v-if="tab.key === 'api'"
353
292
  >
354
293
  <div class="fr-mb-4w">
355
294
  <p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
@@ -357,7 +296,7 @@
357
296
  <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
358
297
  <p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
359
298
  </div>
360
- <Swagger
299
+ <OpenApiViewer
361
300
  v-if="hasTabularData"
362
301
  :url="`${config.tabularApiUrl}/api/resources/${props.resource.id}/swagger/`"
363
302
  />
@@ -374,7 +313,8 @@ import { ref, computed, defineAsyncComponent } from 'vue'
374
313
  import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
375
314
  import OrganizationNameWithCertificate from '../OrganizationNameWithCertificate.vue'
376
315
  import { filesize, summarize } from '../../functions/helpers'
377
- import { useFormatDate } from '../../functions/dates'
316
+ import FormattedDate from '../FormattedDate.vue'
317
+ import TranslationT from '../TranslationT.vue'
378
318
  import MarkdownViewer from '../MarkdownViewer.vue'
379
319
  import type { CommunityResource, Resource } from '../../types/resources'
380
320
  import type { Dataset, DatasetV2 } from '../../types/datasets'
@@ -387,9 +327,8 @@ import { trackEvent } from '../../functions/matomo'
387
327
  import CopyButton from '../CopyButton.vue'
388
328
  import { useComponentsConfig } from '../../config'
389
329
  import { getOwnerName } from '../../functions/owned'
390
- import { getResourceFormatIcon, getResourceTitleId, detectOgcService } from '../../functions/resources'
330
+ import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize, isImagePreviewFormat } from '../../functions/resources'
391
331
  import BrandedButton from '../BrandedButton.vue'
392
- import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
393
332
  import { useTranslation } from '../../composables/useTranslation'
394
333
  import { useHasTabularData } from '../../composables/useHasTabularData'
395
334
  import Metadata from './Metadata.vue'
@@ -397,11 +336,11 @@ import SchemaBadge from './SchemaBadge.vue'
397
336
  import ResourceIcon from './ResourceIcon.vue'
398
337
  import EditButton from './EditButton.vue'
399
338
  import DataStructure from './DataStructure.vue'
339
+ import Downloads from './Downloads.vue'
400
340
  import Preview from './Preview.vue'
401
341
  import { isOrganizationCertified } from '../../functions/organizations'
402
- import SwaggerClient from './Swagger.client.vue'
342
+ import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
403
343
 
404
- const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
405
344
  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
345
 
407
346
  const props = withDefaults(defineProps<{
@@ -418,24 +357,23 @@ const props = withDefaults(defineProps<{
418
357
 
419
358
  const config = useComponentsConfig()
420
359
 
421
- const Swagger = defineAsyncComponent(() => import('./Swagger.client.vue'))
422
360
  const MapContainer = defineAsyncComponent(() => import('./MapContainer.client.vue'))
423
361
  const Pmtiles = defineAsyncComponent(() => import('./Pmtiles.client.vue'))
424
362
  const JsonPreview = defineAsyncComponent(() => import('./JsonPreview.client.vue'))
425
363
  const PdfPreview = defineAsyncComponent(() => import('./PdfPreview.client.vue'))
426
364
  const XmlPreview = defineAsyncComponent(() => import('./XmlPreview.client.vue'))
365
+ const ImagePreview = defineAsyncComponent(() => import('./ImagePreview.client.vue'))
427
366
  const DatafairPreview = defineAsyncComponent(() => import('./Datafair.client.vue'))
428
367
 
429
368
  const { t } = useTranslation()
430
- const { formatRelativeIfRecentDate } = useFormatDate()
431
369
  const checkTabularData = useHasTabularData()
432
370
 
433
371
  const hasPreview = computed(() => {
434
- // For JSON, PDF, and XML files, show preview.
372
+ // For JSON, PDF, XML, and image files, show preview.
435
373
  // We cannot check for CORS issues here because we cannot use an async component here.
436
374
  // If there is a CORS issue when fetching the file for preview, it will be managed and displayed as an error banner by the preview component.
437
375
  const format = props.resource.format?.toLowerCase()
438
- return format === 'json' || format === 'pdf' || format === 'xml'
376
+ return format === 'json' || format === 'pdf' || format === 'xml' || isImagePreviewFormat(format)
439
377
  })
440
378
 
441
379
  const hasTabularData = computed(() => checkTabularData(props.resource))
@@ -462,24 +400,6 @@ const ogcService = computed(() => detectOgcService(props.resource))
462
400
 
463
401
  const ogcWms = computed(() => ogcService.value === 'wms')
464
402
 
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
403
  const open = ref(props.expandedOnMount)
484
404
  const toggle = () => {
485
405
  open.value = !open.value
@@ -515,7 +435,7 @@ const tabsOptions = computed(() => {
515
435
  options.push({ key: 'downloads', label: t('Téléchargements') })
516
436
 
517
437
  if (hasTabularData.value) {
518
- options.push({ key: 'swagger', label: t('Swagger') })
438
+ options.push({ key: 'api', label: t('API') })
519
439
  }
520
440
 
521
441
  return options
@@ -542,7 +462,6 @@ const communityResource = computed<CommunityResource | null>(() => {
542
462
  const owner = computed(() => communityResource.value ? getOwnerName(communityResource.value) : null)
543
463
 
544
464
  const lastUpdate = props.resource.last_modified
545
- const conversionsLastUpdate = computed(() => formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined))
546
465
  const availabilityChecked = props.resource.extras && 'check:available' in props.resource.extras
547
466
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
548
467
 
@@ -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>