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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CaxMyNif.js} +9 -9
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/ImagePreview.client-CPmCyisP.js +36 -0
  8. package/dist/JsonPreview.client-CKOU-LBs.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-IGYJWP5m.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-csUebU3A.js} +835 -856
  13. package/dist/Pmtiles.client-CLq7UHBg.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DeoHZXW1.js +60 -0
  15. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  16. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  17. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  18. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  19. package/dist/XmlPreview.client-C1toAbQ0.js +34 -0
  20. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  21. package/dist/components-next.css +1 -52
  22. package/dist/components-next.js +193 -149
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-CaUlhl3k.js} +21842 -15810
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-ByYGrf8H.js} +185 -187
  26. package/dist/{main-vqhkVhVS.js → main-DD4ieUAo.js} +41500 -73223
  27. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  28. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  29. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  30. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  31. package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-B1KPK4L4.js} +16 -16
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +3 -2
  35. package/src/components/AppLink.vue +3 -0
  36. package/src/components/BannerAction.vue +6 -2
  37. package/src/components/Chart/ChartViewer.vue +320 -0
  38. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  39. package/src/components/CopyButton.vue +6 -2
  40. package/src/components/DataserviceCard.vue +9 -0
  41. package/src/components/DataserviceQuality.vue +82 -0
  42. package/src/components/DataserviceQualityInline.vue +31 -0
  43. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  44. package/src/components/DatasetCard.vue +9 -4
  45. package/src/components/DatasetQuality.vue +11 -11
  46. package/src/components/DatasetQualityInline.vue +2 -2
  47. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  48. package/src/components/Form/Listbox.vue +101 -0
  49. package/src/components/Form/SchemaSelect.vue +2 -0
  50. package/src/components/InfiniteLoader.vue +53 -0
  51. package/src/components/ObjectCardHeader.vue +11 -4
  52. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  53. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  54. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  55. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  56. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  57. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  58. package/src/components/OpenApiViewer/openapi.ts +150 -0
  59. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  60. package/src/components/Pagination.vue +8 -5
  61. package/src/components/RadioInput.vue +7 -2
  62. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  63. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  64. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  65. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  66. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  67. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  68. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  69. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  70. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  71. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  72. package/src/components/ResourceAccordion/Preview.vue +11 -11
  73. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  74. package/src/components/ResourceAccordion/ResourceAccordion.vue +23 -114
  75. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  76. package/src/components/ResourceExplorer/ResourceExplorer.vue +102 -210
  77. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +74 -0
  78. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +103 -71
  79. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  80. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  81. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  82. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +115 -0
  83. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  84. package/src/components/ResourceListItem.vue +154 -0
  85. package/src/components/ReuseCard.vue +14 -6
  86. package/src/components/ReuseDetails.vue +2 -2
  87. package/src/components/ReuseHorizontalCard.vue +2 -2
  88. package/src/components/SchemaCard.vue +10 -1
  89. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  90. package/src/components/Search/GlobalSearch.vue +219 -117
  91. package/src/components/Search/SearchInput.vue +5 -4
  92. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  93. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  94. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  95. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  96. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  97. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  98. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  99. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  100. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  101. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  102. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  103. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  104. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  105. package/src/components/TabularExplorer/types.ts +81 -0
  106. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  107. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  108. package/src/composables/useActiveDescendant.ts +5 -0
  109. package/src/composables/useDatasetResources.ts +164 -0
  110. package/src/composables/useHasTabularData.ts +15 -1
  111. package/src/composables/useMetrics.ts +1 -1
  112. package/src/composables/useResizable.ts +46 -0
  113. package/src/composables/useResourceCapabilities.ts +47 -21
  114. package/src/composables/useSearchFilter.ts +120 -0
  115. package/src/composables/useStableQueryParams.ts +38 -6
  116. package/src/composables/useTabularProfile.ts +70 -0
  117. package/src/config.ts +21 -3
  118. package/src/functions/activities.ts +3 -3
  119. package/src/functions/api.ts +20 -42
  120. package/src/functions/api.types.ts +1 -0
  121. package/src/functions/charts.ts +99 -0
  122. package/src/functions/datasets.ts +0 -17
  123. package/src/functions/dates.ts +3 -2
  124. package/src/functions/metrics.ts +6 -4
  125. package/src/functions/resources.ts +136 -1
  126. package/src/functions/schemas.ts +27 -0
  127. package/src/functions/tabular.ts +214 -0
  128. package/src/functions/tabularApi.ts +96 -11
  129. package/src/main.ts +117 -14
  130. package/src/types/dataservices.ts +17 -0
  131. package/src/types/organizations.ts +10 -0
  132. package/src/types/pages.ts +0 -5
  133. package/src/types/posts.ts +2 -2
  134. package/src/types/reports.ts +5 -1
  135. package/src/types/resources.ts +0 -1
  136. package/src/types/reuses.ts +12 -1
  137. package/src/types/search.ts +63 -1
  138. package/src/types/site.ts +5 -3
  139. package/src/types/ui.ts +2 -0
  140. package/src/types/users.ts +2 -1
  141. package/src/types/visualizations.ts +93 -0
  142. package/assets/swagger-themes/newspaper.css +0 -1670
  143. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  144. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  145. package/dist/Swagger.client-B4y0hXME.js +0 -4
  146. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  147. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  148. package/src/functions/pagination.ts +0 -9
  149. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  150. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  151. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -1,95 +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
- <SchemaBadge :resource />
21
- <RiSubtractLine
22
- v-if="resource.schema"
23
- aria-hidden="true"
24
- class="size-3 fill-gray-medium"
25
- />
26
- <span>{{ t('mis à jour {date}', { date: formatRelativeIfRecentDate(resource.last_modified) }) }}</span>
27
- <RiSubtractLine
28
- aria-hidden="true"
29
- class="size-3 fill-gray-medium"
30
- />
31
- <template v-if="resource.format">
32
- <span>
33
- {{ resource.format.trim().toLowerCase() }}
34
- <span v-if="resourceFilesize">({{ filesize(resourceFilesize) }})</span>
35
- </span>
36
- <RiSubtractLine
37
- aria-hidden="true"
38
- class="size-3 fill-gray-medium"
39
- />
40
- </template>
41
- <span class="inline-flex items-center">
42
- <RiDownloadLine class="size-3 mr-0.5" />
43
- {{ summarize(resource.metrics.views) }}
44
- </span>
45
- </div>
46
- </div>
47
- <div class="flex items-center gap-2">
48
- <BrandedButton
49
- v-if="isResourceUrl"
50
- :href="resource.latest"
51
- :title="t('Lien du fichier - ouvre une nouvelle fenêtre')"
52
- rel="ugc nofollow noopener"
53
- new-tab
54
- size="xs"
55
- external
56
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
57
- >
58
- {{ t('Visiter') }}
59
- </BrandedButton>
60
- <BrandedButton
61
- v-else-if="ogcService"
62
- :icon="RiFileCopyLine"
63
- color="primary"
64
- size="xs"
65
- @click="copyResourceUrl"
66
- >
67
- {{ t('Copier le lien') }}
68
- </BrandedButton>
69
- <BrandedButton
70
- v-else
71
- :href="resource.latest"
72
- rel="ugc nofollow noopener"
73
- :title="downloadButtonTitle"
74
- download
75
- class="matomo_download"
76
- :icon="unavailable ? RiFileWarningLine : RiDownloadLine"
77
- size="xs"
78
- color="primary"
79
- external
80
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
81
- >
82
- {{ t('Télécharger') }}
83
- </BrandedButton>
84
- </div>
85
- </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
+ />
86
12
 
87
- <section class="pb-4">
13
+ <section :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
88
14
  <TabGroup
89
15
  size="sm"
16
+ :default-index="defaultTabIndex"
17
+ :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''"
90
18
  @change="switchTab"
91
19
  >
92
- <div class="pl-4 pr-4 pb-4">
20
+ <div class="flex shrink-0 items-center border-b border-gray-default p-2">
93
21
  <TabList class="max-w-full overflow-x-auto">
94
22
  <Tab
95
23
  v-for="tab in tabsOptions"
@@ -99,13 +27,16 @@
99
27
  </Tab>
100
28
  </TabList>
101
29
  </div>
102
- <TabPanels>
30
+ <TabPanels :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
103
31
  <TabPanel
104
32
  v-for="tab in tabsOptions"
105
33
  :key="tab.key"
106
- class="px-4"
34
+ :class="[tab.key === 'data' || tab.key === 'map' ? '' : 'p-4', fullscreen ? 'flex min-h-0 flex-1 flex-col' : '']"
107
35
  >
108
- <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
+ >
109
40
  <Pmtiles
110
41
  v-if="hasPmtiles"
111
42
  :resource="resource"
@@ -115,36 +46,81 @@
115
46
  v-if="ogcWms"
116
47
  :resource="resource"
117
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>
118
54
  </div>
119
- <div v-if="tab.key === 'data'">
120
- <JsonPreview
121
- v-if="resource.format && resource.format.toLowerCase() === 'json'"
122
- :resource="resource"
123
- />
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. -->
124
77
  <PdfPreview
125
- v-else-if="resource.format && resource.format.toLowerCase() === 'pdf'"
126
- :resource="resource"
127
- />
128
- <XmlPreview
129
- v-else-if="resource.format && resource.format.toLowerCase() === 'xml'"
130
- :resource="resource"
131
- />
132
- <DatafairPreview
133
- v-else-if="hasDatafairPreview"
134
- :resource="resource"
135
- :dataset="dataset"
136
- />
137
- <SwaggerClient
138
- v-else-if="hasOpenAPIPreview"
139
- :url="resource.extras['apidocUrl'] as string"
140
- />
141
- <Preview
142
- v-else-if="hasTabularData"
78
+ v-else-if="previewKind === 'pdf'"
143
79
  :resource="resource"
80
+ :fill="fullscreen"
144
81
  />
145
- <PreviewUnavailable v-else>
146
- {{ t("Ce fichier ne peut pas être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.") }}
147
- </PreviewUnavailable>
82
+
83
+ <!-- Text previews stay padded inside the tab panel -->
84
+ <div
85
+ v-else
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>
148
124
  </div>
149
125
  <div v-if="tab.key === 'description'">
150
126
  <MarkdownViewer
@@ -161,138 +137,17 @@
161
137
  <div v-if="tab.key === 'metadata'">
162
138
  <Metadata :resource />
163
139
  </div>
164
- <div v-if="tab.key === 'downloads'">
165
- <dl class="fr-pl-0">
166
- <dt
167
- v-if="resource.format === 'url'"
168
- class="font-bold fr-text--sm fr-mb-0"
169
- >
170
- {{ t("URL d'origine") }}
171
- </dt>
172
- <dt
173
- v-else
174
- class="font-bold fr-text--sm fr-mb-0"
175
- >
176
- {{ t('Format original') }}
177
- </dt>
178
- <dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
179
- <span
180
- v-if="resource.format === 'url'"
181
- class="inline-flex items-center max-w-full"
182
- >
183
- <a
184
- :href="resource.latest"
185
- class="fr-link no-icon-after truncate"
186
- rel="ugc nofollow noopener"
187
- target="_blank"
188
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
189
- >
190
- {{ resource.url }}
191
- </a>
192
- <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm shrink-0" />
193
- </span>
194
- <span v-else>
195
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
196
- <a
197
- :href="resource.latest"
198
- class="fr-link"
199
- rel="ugc nofollow noopener"
200
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
201
- >
202
- <span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
203
- </a>
204
- </span>
205
- <CopyButton
206
- :label="t('Copier le lien')"
207
- :copied-label="t('Lien copié !')"
208
- :text="resource.latest"
209
- class="relative"
210
- />
211
- </dd>
212
- <template v-if="generatedFormats.length">
213
- <dt class="font-bold fr-text--sm fr-mb-0">
214
- {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
215
- </dt>
216
- <dd
217
- v-for="generatedFormat in generatedFormats"
218
- :key="generatedFormat.format"
219
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
220
- >
221
- <span>
222
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
223
- <a
224
- :href="generatedFormat.url"
225
- class="fr-link"
226
- rel="ugc nofollow noopener"
227
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
228
- >
229
- <span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
230
- </a>
231
- </span>
232
- <CopyButton
233
- :label="t('Copier le lien')"
234
- :copied-label="t('Lien copié !')"
235
- :text="generatedFormat.url"
236
- class="relative"
237
- />
238
- </dd>
239
- </template>
240
- <template v-if="wfsFormats.length">
241
- <dt class="font-bold fr-text--sm fr-mb-0">
242
- <div class="flex gap-1 items-center">
243
- {{ t('Formats exportés depuis le service WFS') }}
244
- <span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
245
- <Tooltip>
246
- <RiInformationLine
247
- class="flex-none size-4"
248
- :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é.`)"
249
- aria-hidden="true"
250
- />
251
- <template #tooltip>
252
- <p class="text-sm font-normal mb-0">
253
- {{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
254
- </p>
255
- <p class="text-sm font-normal mb-0">
256
- {{ t(`Le nombre de features téléchargées peut être limité.`) }}
257
- </p>
258
- </template>
259
- </Tooltip>
260
- </div>
261
- </dt>
262
- <dd
263
- v-for="wfsFormat in wfsFormats"
264
- :key="wfsFormat.format"
265
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
266
- >
267
- <span>
268
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
269
- <a
270
- :href="wfsFormat.url"
271
- class="fr-link"
272
- rel="ugc nofollow noopener"
273
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
274
- >
275
- <span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
276
- </a>
277
- </span>
278
- <CopyButton
279
- :label="t('Copier le lien')"
280
- :copied-label="t('Lien copié !')"
281
- :text="wfsFormat.url"
282
- class="relative"
283
- />
284
- </dd>
285
- </template>
286
- </dl>
287
- </div>
288
- <div v-if="tab.key === 'swagger'">
140
+ <div v-if="tab.key === 'api'">
289
141
  <div class="fr-mb-4w">
290
142
  <p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
291
143
  <p>{{ t("- Si le fichier est modifié, l'API sera mise à jour et sa structure pourra changer.") }}</p>
292
144
  <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
293
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>
294
149
  </div>
295
- <Swagger
150
+ <OpenApiViewer
296
151
  v-if="hasTabularData"
297
152
  :url="`${config.tabularApiUrl}/api/resources/${resource.id}/swagger/`"
298
153
  />
@@ -306,32 +161,29 @@
306
161
 
307
162
  <script setup lang="ts">
308
163
  import { computed, defineAsyncComponent } from 'vue'
309
- import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
164
+ import { useRoute, useRouter } from 'vue-router'
310
165
  import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
311
- import { toast } from 'vue-sonner'
312
- import BrandedButton from '../BrandedButton.vue'
313
- import CopyButton from '../CopyButton.vue'
314
166
  import MarkdownViewer from '../MarkdownViewer.vue'
315
- import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
316
- import Swagger from '../ResourceAccordion/Swagger.client.vue'
167
+ import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
317
168
  import TabGroup from '../Tabs/TabGroup.vue'
318
169
  import TabList from '../Tabs/TabList.vue'
319
170
  import Tab from '../Tabs/Tab.vue'
320
171
  import TabPanels from '../Tabs/TabPanels.vue'
321
172
  import TabPanel from '../Tabs/TabPanel.vue'
322
- import Tooltip from '../Tooltip.vue'
323
- 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'
324
178
  import DataStructure from '../ResourceAccordion/DataStructure.vue'
325
179
  import Metadata from '../ResourceAccordion/Metadata.vue'
326
- import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
327
- import { filesize, summarize } from '../../functions/helpers'
328
- import { getResourceFormatIcon } from '../../functions/resources'
329
- import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
180
+ import ResourceViewerHeader from './ResourceViewerHeader.vue'
330
181
  import { trackEvent } from '../../functions/matomo'
331
182
  import { useComponentsConfig } from '../../config'
332
- import { useFormatDate } from '../../functions/dates'
333
183
  import { useTranslation } from '../../composables/useTranslation'
334
184
  import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
185
+ import { provideTabularProfile } from '../../composables/useTabularProfile'
186
+ import type { RouteLocationRaw } from 'vue-router'
335
187
  import type { Resource } from '../../types/resources'
336
188
  import type { Dataset, DatasetV2 } from '../../types/datasets'
337
189
 
@@ -344,6 +196,9 @@ const PdfPreview = defineAsyncComponent(() =>
344
196
  const XmlPreview = defineAsyncComponent(() =>
345
197
  import('../ResourceAccordion/XmlPreview.client.vue'),
346
198
  )
199
+ const ImagePreview = defineAsyncComponent(() =>
200
+ import('../ResourceAccordion/ImagePreview.client.vue'),
201
+ )
347
202
  const DatafairPreview = defineAsyncComponent(() =>
348
203
  import('../ResourceAccordion/Datafair.client.vue'),
349
204
  )
@@ -353,63 +208,60 @@ const MapContainer = defineAsyncComponent(() =>
353
208
  const Pmtiles = defineAsyncComponent(() =>
354
209
  import('../ResourceAccordion/Pmtiles.client.vue'),
355
210
  )
356
- const SwaggerClient = defineAsyncComponent(() =>
357
- import('../ResourceAccordion/Swagger.client.vue'),
358
- )
359
211
 
360
- const props = defineProps<{
212
+ const props = withDefaults(defineProps<{
361
213
  dataset: Dataset | DatasetV2
362
214
  resource: Resource
363
- }>()
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
+ })
364
228
 
365
229
  const { t } = useTranslation()
366
230
  const config = useComponentsConfig()
367
- const { formatRelativeIfRecentDate } = useFormatDate()
231
+ const route = useRoute()
232
+ const router = useRouter()
368
233
 
369
234
  const {
235
+ previewKind,
370
236
  hasTabularData,
371
237
  hasPmtiles,
372
- hasDatafairPreview,
373
- hasOpenAPIPreview,
374
- ogcService,
238
+ hasPmtilesError,
239
+ pmtilesError,
375
240
  ogcWms,
376
- generatedFormats,
377
- wfsFormats,
378
- defaultWfsProjection,
379
- isResourceUrl,
380
241
  tabsOptions,
381
242
  } = useResourceCapabilities(() => props.resource, () => props.dataset)
382
243
 
383
- const resourceFilesize = computed(() => getResourceFilesize(props.resource))
384
- 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
+ }
385
250
 
386
- const format = computed(() => getResourceFormatIcon(props.resource.format) ? props.resource.format : 'Fichier')
387
- const availabilityChecked = computed(() => props.resource.extras && 'check:available' in props.resource.extras)
388
- const unavailable = computed(() => availabilityChecked.value && props.resource.extras['check:available'] === false)
389
- const downloadButtonTitle = computed(() => {
390
- if (unavailable.value) {
391
- 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 })
392
- }
393
- 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
394
257
  })
395
258
 
396
- const conversionsLastUpdate = computed(() =>
397
- formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
398
- )
399
-
400
- const copyResourceUrl = async () => {
401
- try {
402
- await navigator.clipboard.writeText(props.resource.url)
403
- toast.success(t('Lien copié !'))
404
- }
405
- catch {
406
- toast.error(t('Impossible de copier dans le presse-papier'))
407
- }
408
- }
409
-
410
259
  const switchTab = (index: number) => {
411
260
  const option = tabsOptions.value[index]
412
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 } })
413
265
  trackEvent('View resource tab', props.resource.id, option.label)
414
266
  if (option.key === 'data') {
415
267
  trackEvent('Show preview', props.resource.id)