@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,89 +1,23 @@
1
1
  <template>
2
- <div class="border border-gray-default">
3
- <header class="p-4 flex flex-wrap md:flex-nowrap gap-4 items-center justify-between">
4
- <div>
5
- <div class="flex items-center mb-1">
6
- <h3 class="m-0 flex items-baseline text-base font-bold leading-tight">
7
- <ResourceIcon
8
- :resource
9
- class="size-3.5 mr-1"
10
- />
11
- <span class="line-clamp-2">{{ resource.title || t('Fichier sans nom') }}</span>
12
- </h3>
13
- <CopyButton
14
- :label="t('Copier le lien')"
15
- :copied-label="t('Lien copié !')"
16
- :text="resourceExternalUrl"
17
- />
18
- </div>
19
- <div class="text-gray-medium text-xs flex items-center gap-1">
20
- <span>{{ t('mis à jour {date}', { date: formatRelativeIfRecentDate(resource.last_modified) }) }}</span>
21
- <RiSubtractLine
22
- aria-hidden="true"
23
- class="size-3 fill-gray-medium"
24
- />
25
- <template v-if="resource.format">
26
- <span>
27
- {{ resource.format.trim().toLowerCase() }}
28
- <span v-if="resourceFilesize">({{ filesize(resourceFilesize) }})</span>
29
- </span>
30
- <RiSubtractLine
31
- aria-hidden="true"
32
- class="size-3 fill-gray-medium"
33
- />
34
- </template>
35
- <span class="inline-flex items-center">
36
- <RiDownloadLine class="size-3 mr-0.5" />
37
- {{ summarize(resource.metrics.views) }}
38
- </span>
39
- </div>
40
- </div>
41
- <div class="flex items-center gap-2">
42
- <BrandedButton
43
- v-if="isResourceUrl"
44
- :href="resource.latest"
45
- :title="t('Lien du fichier - ouvre une nouvelle fenêtre')"
46
- rel="ugc nofollow noopener"
47
- new-tab
48
- size="xs"
49
- external
50
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
51
- >
52
- {{ t('Visiter') }}
53
- </BrandedButton>
54
- <BrandedButton
55
- v-else-if="ogcService"
56
- :icon="RiFileCopyLine"
57
- color="primary"
58
- size="xs"
59
- @click="copyResourceUrl"
60
- >
61
- {{ t('Copier le lien') }}
62
- </BrandedButton>
63
- <BrandedButton
64
- v-else
65
- :href="resource.latest"
66
- rel="ugc nofollow noopener"
67
- :title="downloadButtonTitle"
68
- download
69
- class="matomo_download"
70
- :icon="unavailable ? RiFileWarningLine : RiDownloadLine"
71
- size="xs"
72
- color="primary"
73
- external
74
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
75
- >
76
- {{ t('Télécharger') }}
77
- </BrandedButton>
78
- </div>
79
- </header>
2
+ <div :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
3
+ <ResourceViewerHeader
4
+ :dataset
5
+ :resource
6
+ :resources
7
+ :resource-to
8
+ :explore-to="exploreTo"
9
+ :replace
10
+ :fullscreen
11
+ />
80
12
 
81
- <section class="pb-4">
13
+ <section :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
82
14
  <TabGroup
83
15
  size="sm"
16
+ :default-index="defaultTabIndex"
17
+ :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''"
84
18
  @change="switchTab"
85
19
  >
86
- <div class="pl-4 pr-4 pb-4">
20
+ <div class="flex shrink-0 items-center border-b border-gray-default p-2">
87
21
  <TabList class="max-w-full overflow-x-auto">
88
22
  <Tab
89
23
  v-for="tab in tabsOptions"
@@ -93,13 +27,16 @@
93
27
  </Tab>
94
28
  </TabList>
95
29
  </div>
96
- <TabPanels>
30
+ <TabPanels :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
97
31
  <TabPanel
98
32
  v-for="tab in tabsOptions"
99
33
  :key="tab.key"
100
- class="px-4"
34
+ :class="[tab.key === 'data' || tab.key === 'map' ? '' : 'p-4', fullscreen ? 'flex min-h-0 flex-1 flex-col' : '']"
101
35
  >
102
- <div v-if="tab.key === 'map'">
36
+ <div
37
+ v-if="tab.key === 'map'"
38
+ :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : 'h-[600px]'"
39
+ >
103
40
  <Pmtiles
104
41
  v-if="hasPmtiles"
105
42
  :resource="resource"
@@ -109,33 +46,81 @@
109
46
  v-if="ogcWms"
110
47
  :resource="resource"
111
48
  />
49
+ <PreviewUnavailable v-if="!hasPmtiles && !ogcWms && hasPmtilesError">
50
+ {{ t("La carte n'a pas pu être générée automatiquement pour ce fichier.") }}
51
+ <br>
52
+ <span class="text-gray-medium text-xs">{{ pmtilesError }}</span>
53
+ </PreviewUnavailable>
112
54
  </div>
113
- <div v-if="tab.key === 'data'">
114
- <JsonPreview
115
- v-if="resource.format && resource.format.toLowerCase() === 'json'"
116
- :resource="resource"
117
- />
55
+ <div
56
+ v-if="tab.key === 'data'"
57
+ :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''"
58
+ >
59
+ <!-- Wrapped in Suspense so switching to this tab (or loading its data) shows
60
+ the table skeleton instead of a blank gap while TabularExplorer resolves. -->
61
+ <Suspense
62
+ v-if="previewKind === 'tabular'"
63
+ :timeout="200"
64
+ >
65
+ <TabularExplorer :resource-id="resource.id">
66
+ <TabularToolbar class="shrink-0 border-b border-gray-default p-2" />
67
+ <TabularTable :fill="fullscreen" />
68
+ <TabularMobileFilters />
69
+ </TabularExplorer>
70
+ <template #fallback>
71
+ <TabularSkeleton :fill="fullscreen" />
72
+ </template>
73
+ </Suspense>
74
+
75
+ <!-- PDF is a full-bleed visual preview like the table and the map: it
76
+ owns its own reader backdrop, so it sits outside the padded wrapper. -->
118
77
  <PdfPreview
119
- v-else-if="resource.format && resource.format.toLowerCase() === 'pdf'"
78
+ v-else-if="previewKind === 'pdf'"
120
79
  :resource="resource"
80
+ :fill="fullscreen"
121
81
  />
122
- <XmlPreview
123
- v-else-if="resource.format && resource.format.toLowerCase() === 'xml'"
124
- :resource="resource"
125
- />
126
- <DatafairPreview
127
- v-else-if="hasDatafairPreview"
128
- :resource="resource"
129
- :dataset="dataset"
130
- />
131
- <SwaggerClient
132
- v-else-if="hasOpenAPIPreview"
133
- :url="resource.extras['apidocUrl'] as string"
134
- />
135
- <Preview
82
+
83
+ <!-- Text previews stay padded inside the tab panel -->
84
+ <div
136
85
  v-else
137
- :resource="resource"
138
- />
86
+ class="p-4"
87
+ >
88
+ <JsonPreview
89
+ v-if="previewKind === 'json'"
90
+ :resource="resource"
91
+ />
92
+ <XmlPreview
93
+ v-else-if="previewKind === 'xml'"
94
+ :resource="resource"
95
+ />
96
+ <ImagePreview
97
+ v-else-if="previewKind === 'image'"
98
+ :resource="resource"
99
+ />
100
+ <DatafairPreview
101
+ v-else-if="previewKind === 'datafair'"
102
+ :resource="resource"
103
+ :dataset="dataset"
104
+ />
105
+ <OpenApiViewer
106
+ v-else-if="previewKind === 'openapi'"
107
+ :url="resource.extras['apidocUrl'] as string"
108
+ />
109
+ <PreviewUnavailable v-else>
110
+ <!-- "File too large to download" is the only analysis:error value from hydra for now -->
111
+ <template v-if="resource.extras['analysis:error'] === 'File too large to download'">
112
+ {{ t("Ce fichier est trop volumineux pour être analysé et prévisualisé. Téléchargez-le avec le bouton Télécharger.") }}
113
+ </template>
114
+ <template v-else-if="resource.extras['analysis:parsing:error']">
115
+ {{ t("L'analyse de ce fichier a rencontré une erreur, l'aperçu n'est pas disponible. Téléchargez-le avec le bouton Télécharger.") }}
116
+ <br>
117
+ <span class="text-gray-medium text-xs">{{ resource.extras['analysis:parsing:error'] }}</span>
118
+ </template>
119
+ <template v-else>
120
+ {{ t("Ce fichier ne peut pas être prévisualisé. Téléchargez-le avec le bouton Télécharger.") }}
121
+ </template>
122
+ </PreviewUnavailable>
123
+ </div>
139
124
  </div>
140
125
  <div v-if="tab.key === 'description'">
141
126
  <MarkdownViewer
@@ -152,138 +137,17 @@
152
137
  <div v-if="tab.key === 'metadata'">
153
138
  <Metadata :resource />
154
139
  </div>
155
- <div v-if="tab.key === 'downloads'">
156
- <dl class="fr-pl-0">
157
- <dt
158
- v-if="resource.format === 'url'"
159
- class="font-bold fr-text--sm fr-mb-0"
160
- >
161
- {{ t("URL d'origine") }}
162
- </dt>
163
- <dt
164
- v-else
165
- class="font-bold fr-text--sm fr-mb-0"
166
- >
167
- {{ t('Format original') }}
168
- </dt>
169
- <dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
170
- <span
171
- v-if="resource.format === 'url'"
172
- class="inline-flex items-center max-w-full"
173
- >
174
- <a
175
- :href="resource.latest"
176
- class="fr-link no-icon-after truncate"
177
- rel="ugc nofollow noopener"
178
- target="_blank"
179
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
180
- >
181
- {{ resource.url }}
182
- </a>
183
- <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm shrink-0" />
184
- </span>
185
- <span v-else>
186
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
187
- <a
188
- :href="resource.latest"
189
- class="fr-link"
190
- rel="ugc nofollow noopener"
191
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
192
- >
193
- <span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
194
- </a>
195
- </span>
196
- <CopyButton
197
- :label="t('Copier le lien')"
198
- :copied-label="t('Lien copié !')"
199
- :text="resource.latest"
200
- class="relative"
201
- />
202
- </dd>
203
- <template v-if="generatedFormats.length">
204
- <dt class="font-bold fr-text--sm fr-mb-0">
205
- {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
206
- </dt>
207
- <dd
208
- v-for="generatedFormat in generatedFormats"
209
- :key="generatedFormat.format"
210
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
211
- >
212
- <span>
213
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
214
- <a
215
- :href="generatedFormat.url"
216
- class="fr-link"
217
- rel="ugc nofollow noopener"
218
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
219
- >
220
- <span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
221
- </a>
222
- </span>
223
- <CopyButton
224
- :label="t('Copier le lien')"
225
- :copied-label="t('Lien copié !')"
226
- :text="generatedFormat.url"
227
- class="relative"
228
- />
229
- </dd>
230
- </template>
231
- <template v-if="wfsFormats.length">
232
- <dt class="font-bold fr-text--sm fr-mb-0">
233
- <div class="flex gap-1 items-center">
234
- {{ t('Formats exportés depuis le service WFS') }}
235
- <span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
236
- <Tooltip>
237
- <RiInformationLine
238
- class="flex-none size-4"
239
- :aria-label="t(`Le lien de téléchargement interroge directement le flux WFS distant. Le nombre de features téléchargées peut être limité.`)"
240
- aria-hidden="true"
241
- />
242
- <template #tooltip>
243
- <p class="text-sm font-normal mb-0">
244
- {{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
245
- </p>
246
- <p class="text-sm font-normal mb-0">
247
- {{ t(`Le nombre de features téléchargées peut être limité.`) }}
248
- </p>
249
- </template>
250
- </Tooltip>
251
- </div>
252
- </dt>
253
- <dd
254
- v-for="wfsFormat in wfsFormats"
255
- :key="wfsFormat.format"
256
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
257
- >
258
- <span>
259
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
260
- <a
261
- :href="wfsFormat.url"
262
- class="fr-link"
263
- rel="ugc nofollow noopener"
264
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
265
- >
266
- <span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
267
- </a>
268
- </span>
269
- <CopyButton
270
- :label="t('Copier le lien')"
271
- :copied-label="t('Lien copié !')"
272
- :text="wfsFormat.url"
273
- class="relative"
274
- />
275
- </dd>
276
- </template>
277
- </dl>
278
- </div>
279
- <div v-if="tab.key === 'swagger'">
140
+ <div v-if="tab.key === 'api'">
280
141
  <div class="fr-mb-4w">
281
142
  <p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
282
143
  <p>{{ t("- Si le fichier est modifié, l'API sera mise à jour et sa structure pourra changer.") }}</p>
283
144
  <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
284
145
  <p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
146
+ <p v-if="config.tabularApiUrl">
147
+ {{ t("L'URL de base de l'API est {url}", { url: config.tabularApiUrl }) }}
148
+ </p>
285
149
  </div>
286
- <Swagger
150
+ <OpenApiViewer
287
151
  v-if="hasTabularData"
288
152
  :url="`${config.tabularApiUrl}/api/resources/${resource.id}/swagger/`"
289
153
  />
@@ -297,30 +161,29 @@
297
161
 
298
162
  <script setup lang="ts">
299
163
  import { computed, defineAsyncComponent } from 'vue'
300
- import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
301
- import { toast } from 'vue-sonner'
302
- import BrandedButton from '../BrandedButton.vue'
303
- import CopyButton from '../CopyButton.vue'
164
+ import { useRoute, useRouter } from 'vue-router'
165
+ import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
304
166
  import MarkdownViewer from '../MarkdownViewer.vue'
305
- import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
306
- import Swagger from '../ResourceAccordion/Swagger.client.vue'
167
+ import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
307
168
  import TabGroup from '../Tabs/TabGroup.vue'
308
169
  import TabList from '../Tabs/TabList.vue'
309
170
  import Tab from '../Tabs/Tab.vue'
310
171
  import TabPanels from '../Tabs/TabPanels.vue'
311
172
  import TabPanel from '../Tabs/TabPanel.vue'
312
- import Tooltip from '../Tooltip.vue'
313
- import Preview from '../ResourceAccordion/Preview.vue'
173
+ import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
174
+ import TabularToolbar from '../TabularExplorer/TabularToolbar.vue'
175
+ import TabularTable from '../TabularExplorer/TabularTable.vue'
176
+ import TabularMobileFilters from '../TabularExplorer/TabularMobileFilters.vue'
177
+ import TabularSkeleton from '../TabularExplorer/TabularSkeleton.vue'
314
178
  import DataStructure from '../ResourceAccordion/DataStructure.vue'
315
179
  import Metadata from '../ResourceAccordion/Metadata.vue'
316
- import { filesize, summarize } from '../../functions/helpers'
317
- import { getResourceFormatIcon } from '../../functions/resources'
318
- import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
180
+ import ResourceViewerHeader from './ResourceViewerHeader.vue'
319
181
  import { trackEvent } from '../../functions/matomo'
320
182
  import { useComponentsConfig } from '../../config'
321
- import { useFormatDate } from '../../functions/dates'
322
183
  import { useTranslation } from '../../composables/useTranslation'
323
184
  import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
185
+ import { provideTabularProfile } from '../../composables/useTabularProfile'
186
+ import type { RouteLocationRaw } from 'vue-router'
324
187
  import type { Resource } from '../../types/resources'
325
188
  import type { Dataset, DatasetV2 } from '../../types/datasets'
326
189
 
@@ -333,6 +196,9 @@ const PdfPreview = defineAsyncComponent(() =>
333
196
  const XmlPreview = defineAsyncComponent(() =>
334
197
  import('../ResourceAccordion/XmlPreview.client.vue'),
335
198
  )
199
+ const ImagePreview = defineAsyncComponent(() =>
200
+ import('../ResourceAccordion/ImagePreview.client.vue'),
201
+ )
336
202
  const DatafairPreview = defineAsyncComponent(() =>
337
203
  import('../ResourceAccordion/Datafair.client.vue'),
338
204
  )
@@ -342,63 +208,60 @@ const MapContainer = defineAsyncComponent(() =>
342
208
  const Pmtiles = defineAsyncComponent(() =>
343
209
  import('../ResourceAccordion/Pmtiles.client.vue'),
344
210
  )
345
- const SwaggerClient = defineAsyncComponent(() =>
346
- import('../ResourceAccordion/Swagger.client.vue'),
347
- )
348
211
 
349
- const props = defineProps<{
212
+ const props = withDefaults(defineProps<{
350
213
  dataset: Dataset | DatasetV2
351
214
  resource: Resource
352
- }>()
215
+ resources?: Resource[]
216
+ resourceTo: (resource: Resource) => RouteLocationRaw
217
+ // When provided (inline mode), shows an "Explorer" button next to the download
218
+ // action that opens the fullscreen explorer on the current resource.
219
+ exploreTo?: (resource: Resource) => string
220
+ replace?: boolean
221
+ // Fullscreen mode: make the viewer a flex column so the table fills down to the
222
+ // bottom, and hide the inline download/visit/copy actions — they're shown in the
223
+ // dataset context bar above. Inline mode (dataset page) shows them in the header.
224
+ fullscreen?: boolean
225
+ }>(), {
226
+ fullscreen: false,
227
+ })
353
228
 
354
229
  const { t } = useTranslation()
355
230
  const config = useComponentsConfig()
356
- const { formatRelativeIfRecentDate } = useFormatDate()
231
+ const route = useRoute()
232
+ const router = useRouter()
357
233
 
358
234
  const {
235
+ previewKind,
359
236
  hasTabularData,
360
237
  hasPmtiles,
361
- hasDatafairPreview,
362
- hasOpenAPIPreview,
363
- ogcService,
238
+ hasPmtilesError,
239
+ pmtilesError,
364
240
  ogcWms,
365
- generatedFormats,
366
- wfsFormats,
367
- defaultWfsProjection,
368
- isResourceUrl,
369
241
  tabsOptions,
370
242
  } = useResourceCapabilities(() => props.resource, () => props.dataset)
371
243
 
372
- const resourceFilesize = computed(() => getResourceFilesize(props.resource))
373
- const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
244
+ // Share the tabular profile fetch between TabularExplorer and DataStructure tabs.
245
+ // Only tabular resources have one: asking for the profile of a PDF or an image is a
246
+ // request to the Tabular API that can only fail.
247
+ if (hasTabularData.value) {
248
+ await provideTabularProfile(() => props.resource.id)
249
+ }
374
250
 
375
- const format = computed(() => getResourceFormatIcon(props.resource.format) ? props.resource.format : 'Fichier')
376
- const availabilityChecked = computed(() => props.resource.extras && 'check:available' in props.resource.extras)
377
- const unavailable = computed(() => availabilityChecked.value && props.resource.extras['check:available'] === false)
378
- const downloadButtonTitle = computed(() => {
379
- if (unavailable.value) {
380
- return t('Le robot de {platform} n\'a pas pu accéder à ce fichier - Télécharger le fichier en {format}', { platform: config.name, format: format.value })
381
- }
382
- return t('Télécharger le fichier en {format}', { format: format.value })
251
+ // The active tab lives in the URL so a shared link opens on the same one. Read once
252
+ // at mount (TabGroup only takes an initial index), which is enough: switching resource
253
+ // remounts the viewer, and the tab is preserved when the new resource also has it.
254
+ const defaultTabIndex = computed(() => {
255
+ const index = tabsOptions.value.findIndex(option => option.key === route.query.tab)
256
+ return index === -1 ? 0 : index
383
257
  })
384
258
 
385
- const conversionsLastUpdate = computed(() =>
386
- formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
387
- )
388
-
389
- const copyResourceUrl = async () => {
390
- try {
391
- await navigator.clipboard.writeText(props.resource.url)
392
- toast.success(t('Lien copié !'))
393
- }
394
- catch {
395
- toast.error(t('Impossible de copier dans le presse-papier'))
396
- }
397
- }
398
-
399
259
  const switchTab = (index: number) => {
400
260
  const option = tabsOptions.value[index]
401
261
  if (!option) return
262
+ // The first tab is the default: drop `tab` rather than writing it in the URL.
263
+ const { tab: _, ...query } = route.query
264
+ router.replace({ query: index === 0 ? query : { ...query, tab: option.key } })
402
265
  trackEvent('View resource tab', props.resource.id, option.label)
403
266
  if (option.key === 'data') {
404
267
  trackEvent('Show preview', props.resource.id)