@datagouv/components-next 0.2.0 → 1.0.0

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 (140) hide show
  1. package/README.md +1 -1
  2. package/assets/main.css +56 -1
  3. package/dist/Control-BNCDn-8E.js +148 -0
  4. package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-Dls5AHTE.js} +1 -1
  5. package/dist/Event-BOgJUhNR.js +738 -0
  6. package/dist/Image-BN-4XkIn.js +247 -0
  7. package/dist/{JsonPreview.client-BMsC5JcY.js → JsonPreview.client-DPDTs433.js} +14 -14
  8. package/dist/Map-BdT3i2C4.js +7609 -0
  9. package/dist/MapContainer.client-BdAzd7bj.js +105 -0
  10. package/dist/OSM-CamriM9b.js +71 -0
  11. package/dist/{PdfPreview.client-COOkEkRA.js → PdfPreview.client-CopqSDyt.js} +3 -3
  12. package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-mF6xaOO_.js} +2 -2
  13. package/dist/ScaleLine-BiesrgOv.js +165 -0
  14. package/dist/Swagger.client-eJ7gpfZA.js +4 -0
  15. package/dist/Tile-DCuqwNOI.js +1206 -0
  16. package/dist/TileImage-CmZf8EdU.js +1067 -0
  17. package/dist/View-DcDc7N2K.js +2858 -0
  18. package/dist/{XmlPreview.client-CAdN0w_Y.js → XmlPreview.client-C0OgBkSq.js} +7 -7
  19. package/dist/common-C4rDcQpp.js +243 -0
  20. package/dist/components-next.css +1 -1
  21. package/dist/components-next.js +153 -117
  22. package/dist/components.css +1 -1
  23. package/dist/{MapContainer.client-DeSo8EvG.js → index-BRGqW8aQ.js} +4975 -21416
  24. package/dist/leaflet-src-7m1mB8LI.js +6338 -0
  25. package/dist/{main-Dgri3TQL.js → main-CNHxAJ8J.js} +56758 -51450
  26. package/dist/proj-CKwYjU38.js +1569 -0
  27. package/dist/tilecoord-YW3qEH_j.js +884 -0
  28. package/dist/{vue3-xml-viewer.common-D6skc_Ai.js → vue3-xml-viewer.common-CmAdQfIy.js} +1 -1
  29. package/package.json +5 -1
  30. package/src/components/ActivityList/ActivityList.vue +6 -2
  31. package/src/components/AppLink.vue +4 -1
  32. package/src/components/Avatar.vue +2 -2
  33. package/src/components/AvatarWithName.vue +8 -4
  34. package/src/components/BouncingDots.vue +21 -0
  35. package/src/components/BrandedButton.vue +2 -0
  36. package/src/components/CopyButton.vue +19 -7
  37. package/src/components/DataserviceCard.vue +83 -118
  38. package/src/components/DatasetCard.vue +110 -171
  39. package/src/components/DatasetInformation/DatasetEmbedSection.vue +43 -0
  40. package/src/components/DatasetInformation/DatasetInformationSection.vue +73 -0
  41. package/src/components/DatasetInformation/DatasetSchemaSection.vue +74 -0
  42. package/src/components/DatasetInformation/DatasetSpatialSection.vue +59 -0
  43. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +45 -0
  44. package/src/components/DatasetInformation/index.ts +5 -0
  45. package/src/components/DatasetQualityTooltipContent.vue +3 -3
  46. package/src/components/DescriptionList.vue +1 -4
  47. package/src/components/DescriptionListDetails.vue +5 -0
  48. package/src/components/DescriptionListTerm.vue +5 -0
  49. package/src/components/DiscussionMessageCard.vue +63 -0
  50. package/src/components/ExtraAccordion.vue +4 -4
  51. package/src/components/Form/BadgeSelect.vue +35 -0
  52. package/src/components/Form/FormatSelect.vue +28 -0
  53. package/src/components/Form/GeozoneSelect.vue +52 -0
  54. package/src/components/Form/GranularitySelect.vue +29 -0
  55. package/src/components/Form/LicenseSelect.vue +30 -0
  56. package/src/components/Form/OrganizationSelect.vue +62 -0
  57. package/src/components/Form/OrganizationTypeSelect.vue +34 -0
  58. package/src/components/Form/ReuseTopicSelect.vue +29 -0
  59. package/src/components/Form/SchemaSelect.vue +30 -0
  60. package/src/components/Form/SearchableSelect.vue +334 -0
  61. package/src/components/Form/SelectGroup.vue +132 -0
  62. package/src/components/Form/TagSelect.vue +38 -0
  63. package/src/components/LeafletMap.vue +31 -0
  64. package/src/components/LicenseBadge.vue +24 -0
  65. package/src/components/LoadingBlock.vue +23 -2
  66. package/src/components/MarkdownViewer.vue +3 -1
  67. package/src/components/ObjectCard.vue +42 -0
  68. package/src/components/ObjectCardBadge.vue +22 -0
  69. package/src/components/ObjectCardHeader.vue +35 -0
  70. package/src/components/ObjectCardOwner.vue +43 -0
  71. package/src/components/ObjectCardShortDescription.vue +28 -0
  72. package/src/components/OrganizationCard.vue +35 -20
  73. package/src/components/OrganizationLogo.vue +1 -1
  74. package/src/components/OrganizationNameWithCertificate.vue +13 -7
  75. package/src/components/OwnerTypeIcon.vue +1 -0
  76. package/src/components/Pagination.vue +1 -1
  77. package/src/components/Placeholder.vue +5 -2
  78. package/src/components/PostCard.vue +62 -0
  79. package/src/components/RadioGroup.vue +32 -0
  80. package/src/components/RadioInput.vue +64 -0
  81. package/src/components/ResourceAccordion/EditButton.vue +2 -3
  82. package/src/components/ResourceAccordion/MapContainer.client.vue +20 -16
  83. package/src/components/ResourceAccordion/Metadata.vue +11 -24
  84. package/src/components/ResourceAccordion/Pmtiles.client.vue +1 -1
  85. package/src/components/ResourceAccordion/Preview.vue +1 -1
  86. package/src/components/ResourceAccordion/ResourceAccordion.vue +30 -20
  87. package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
  88. package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
  89. package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
  90. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
  91. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +361 -0
  92. package/src/components/ReuseCard.vue +8 -28
  93. package/src/components/ReuseHorizontalCard.vue +80 -0
  94. package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
  95. package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
  96. package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
  97. package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
  98. package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
  99. package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
  100. package/src/components/Search/Filter/ProducerTypeFilter.vue +39 -0
  101. package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
  102. package/src/components/Search/GlobalSearch.vue +611 -0
  103. package/src/components/Search/SearchInput.vue +63 -0
  104. package/src/components/Search/Sidemenu.vue +38 -0
  105. package/src/components/StatBox.vue +5 -5
  106. package/src/components/Tag.vue +30 -0
  107. package/src/components/Toggletip.vue +6 -2
  108. package/src/components/Tooltip.vue +2 -3
  109. package/src/components/TopicCard.vue +134 -0
  110. package/src/components/radioGroupContext.ts +9 -0
  111. package/src/composables/useDebouncedRef.ts +31 -0
  112. package/src/composables/useMetrics.ts +4 -3
  113. package/src/composables/useResourceCapabilities.ts +118 -0
  114. package/src/composables/useRouteQueryBoolean.ts +10 -0
  115. package/src/composables/useSelectModelSync.ts +89 -0
  116. package/src/composables/useStableQueryParams.ts +84 -0
  117. package/src/config.ts +4 -0
  118. package/src/functions/api.ts +17 -6
  119. package/src/functions/api.types.ts +4 -2
  120. package/src/functions/datasets.ts +1 -29
  121. package/src/functions/description.ts +33 -0
  122. package/src/functions/helpers.ts +11 -0
  123. package/src/functions/markdown.ts +60 -16
  124. package/src/functions/metrics.ts +33 -0
  125. package/src/functions/organizations.ts +5 -5
  126. package/src/main.ts +89 -7
  127. package/src/types/dataservices.ts +14 -12
  128. package/src/types/datasets.ts +20 -7
  129. package/src/types/discussions.ts +20 -0
  130. package/src/types/licenses.ts +3 -3
  131. package/src/types/organizations.ts +13 -1
  132. package/src/types/owned.ts +4 -2
  133. package/src/types/pages.ts +70 -0
  134. package/src/types/posts.ts +27 -0
  135. package/src/types/resources.ts +6 -0
  136. package/src/types/reuses.ts +14 -5
  137. package/src/types/search.ts +379 -0
  138. package/src/types/users.ts +12 -3
  139. package/dist/Swagger.client-CpLgaLg6.js +0 -4
  140. package/src/components/DatasetInformationPanel.vue +0 -211
@@ -0,0 +1,361 @@
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>
80
+
81
+ <section class="pb-4">
82
+ <TabGroup
83
+ size="sm"
84
+ @change="switchTab"
85
+ >
86
+ <div class="pl-4 pr-4 pb-4">
87
+ <TabList class="max-w-full overflow-x-auto">
88
+ <Tab
89
+ v-for="tab in tabsOptions"
90
+ :key="tab.key"
91
+ >
92
+ {{ tab.label }}
93
+ </Tab>
94
+ </TabList>
95
+ </div>
96
+ <TabPanels>
97
+ <TabPanel
98
+ v-for="tab in tabsOptions"
99
+ :key="tab.key"
100
+ class="px-4"
101
+ >
102
+ <div v-if="tab.key === 'map'">
103
+ <Pmtiles
104
+ v-if="hasPmtiles"
105
+ :resource="resource"
106
+ :dataset="dataset"
107
+ />
108
+ <MapContainer
109
+ v-if="ogcWms"
110
+ :resource="resource"
111
+ />
112
+ </div>
113
+ <div v-if="tab.key === 'data'">
114
+ <JsonPreview
115
+ v-if="resource.format && resource.format.toLowerCase() === 'json'"
116
+ :resource="resource"
117
+ />
118
+ <PdfPreview
119
+ v-else-if="resource.format && resource.format.toLowerCase() === 'pdf'"
120
+ :resource="resource"
121
+ />
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
136
+ v-else
137
+ :resource="resource"
138
+ />
139
+ </div>
140
+ <div v-if="tab.key === 'description'">
141
+ <MarkdownViewer
142
+ :content="resource.description || ''"
143
+ size="sm"
144
+ />
145
+ </div>
146
+ <div v-if="tab.key === 'data-structure'">
147
+ <DataStructure
148
+ v-if="hasTabularData"
149
+ :resource="resource"
150
+ />
151
+ </div>
152
+ <div v-if="tab.key === 'metadata'">
153
+ <Metadata :resource />
154
+ </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
+ </dl>
232
+ </div>
233
+ <div v-if="tab.key === 'swagger'">
234
+ <div class="fr-mb-4w">
235
+ <p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
236
+ <p>{{ t("- Si le fichier est modifié, l'API sera mise à jour et sa structure pourra changer.") }}</p>
237
+ <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
238
+ <p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
239
+ </div>
240
+ <Swagger
241
+ v-if="hasTabularData"
242
+ :url="`${config.tabularApiUrl}/api/resources/${resource.id}/swagger/`"
243
+ />
244
+ </div>
245
+ </TabPanel>
246
+ </TabPanels>
247
+ </TabGroup>
248
+ </section>
249
+ </div>
250
+ </template>
251
+
252
+ <script setup lang="ts">
253
+ import { computed, defineAsyncComponent } from 'vue'
254
+ import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
255
+ import { toast } from 'vue-sonner'
256
+ import BrandedButton from '../BrandedButton.vue'
257
+ import CopyButton from '../CopyButton.vue'
258
+ import MarkdownViewer from '../MarkdownViewer.vue'
259
+ import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
260
+ import Swagger from '../ResourceAccordion/Swagger.client.vue'
261
+ import TabGroup from '../Tabs/TabGroup.vue'
262
+ import TabList from '../Tabs/TabList.vue'
263
+ import Tab from '../Tabs/Tab.vue'
264
+ import TabPanels from '../Tabs/TabPanels.vue'
265
+ import TabPanel from '../Tabs/TabPanel.vue'
266
+ import Preview from '../ResourceAccordion/Preview.vue'
267
+ import DataStructure from '../ResourceAccordion/DataStructure.vue'
268
+ import Metadata from '../ResourceAccordion/Metadata.vue'
269
+ import { filesize, summarize } from '../../functions/helpers'
270
+ import { getResourceFormatIcon } from '../../functions/resources'
271
+ import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
272
+ import { trackEvent } from '../../functions/matomo'
273
+ import { useComponentsConfig } from '../../config'
274
+ import { useFormatDate } from '../../functions/dates'
275
+ import { useTranslation } from '../../composables/useTranslation'
276
+ import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
277
+ import type { Resource } from '../../types/resources'
278
+ import type { Dataset, DatasetV2 } from '../../types/datasets'
279
+
280
+ const JsonPreview = defineAsyncComponent(() =>
281
+ import('../ResourceAccordion/JsonPreview.client.vue'),
282
+ )
283
+ const PdfPreview = defineAsyncComponent(() =>
284
+ import('../ResourceAccordion/PdfPreview.client.vue'),
285
+ )
286
+ const XmlPreview = defineAsyncComponent(() =>
287
+ import('../ResourceAccordion/XmlPreview.client.vue'),
288
+ )
289
+ const DatafairPreview = defineAsyncComponent(() =>
290
+ import('../ResourceAccordion/Datafair.client.vue'),
291
+ )
292
+ const MapContainer = defineAsyncComponent(() =>
293
+ import('../ResourceAccordion/MapContainer.client.vue'),
294
+ )
295
+ const Pmtiles = defineAsyncComponent(() =>
296
+ import('../ResourceAccordion/Pmtiles.client.vue'),
297
+ )
298
+ const SwaggerClient = defineAsyncComponent(() =>
299
+ import('../ResourceAccordion/Swagger.client.vue'),
300
+ )
301
+
302
+ const props = defineProps<{
303
+ dataset: Dataset | DatasetV2
304
+ resource: Resource
305
+ }>()
306
+
307
+ const { t } = useTranslation()
308
+ const config = useComponentsConfig()
309
+ const { formatRelativeIfRecentDate } = useFormatDate()
310
+
311
+ const {
312
+ hasTabularData,
313
+ hasPmtiles,
314
+ hasDatafairPreview,
315
+ hasOpenAPIPreview,
316
+ ogcService,
317
+ ogcWms,
318
+ generatedFormats,
319
+ isResourceUrl,
320
+ tabsOptions,
321
+ } = useResourceCapabilities(() => props.resource, () => props.dataset)
322
+
323
+ const resourceFilesize = computed(() => getResourceFilesize(props.resource))
324
+ const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
325
+
326
+ const format = computed(() => getResourceFormatIcon(props.resource.format) ? props.resource.format : 'Fichier')
327
+ const availabilityChecked = computed(() => props.resource.extras && 'check:available' in props.resource.extras)
328
+ const unavailable = computed(() => availabilityChecked.value && props.resource.extras['check:available'] === false)
329
+ const downloadButtonTitle = computed(() => {
330
+ if (unavailable.value) {
331
+ 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 })
332
+ }
333
+ return t('Télécharger le fichier en {format}', { format: format.value })
334
+ })
335
+
336
+ const conversionsLastUpdate = computed(() =>
337
+ formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
338
+ )
339
+
340
+ const copyResourceUrl = async () => {
341
+ try {
342
+ await navigator.clipboard.writeText(props.resource.url)
343
+ toast.success(t('Lien copié !'))
344
+ }
345
+ catch {
346
+ toast.error(t('Impossible de copier dans le presse-papier'))
347
+ }
348
+ }
349
+
350
+ const switchTab = (index: number) => {
351
+ const option = tabsOptions.value[index]
352
+ if (!option) return
353
+ trackEvent('View resource tab', props.resource.id, option.label)
354
+ if (option.key === 'data') {
355
+ trackEvent('Show preview', props.resource.id)
356
+ }
357
+ if (option.key === 'data-structure') {
358
+ trackEvent('Show data structure', props.resource.id)
359
+ }
360
+ }
361
+ </script>
@@ -11,32 +11,15 @@
11
11
  </AppLink>
12
12
  </h3>
13
13
  <div class="order-3 text-sm m-0 text-gray-medium">
14
- <p class="text-sm mb-0 flex items-center">
15
- <span
16
- v-if="reuse.organization"
17
- class="relative block truncate break-all z-[2] flex-initial"
18
- >
19
- <AppLink
20
- v-if="organizationUrl"
21
- class="link overflow-hidden"
22
- :to="organizationUrl"
23
- >
24
- <OrganizationNameWithCertificate
25
- :organization="reuse.organization"
26
- />
27
- </AppLink>
28
- <OrganizationNameWithCertificate
29
- v-else
30
- :organization="reuse.organization"
31
- />
32
- </span>
33
- <span
34
- v-else-if="ownerName"
35
- class="mr-1 truncate"
36
- >{{ ownerName }}</span>
14
+ <div class="text-sm mb-0 flex items-center">
15
+ <ObjectCardOwner
16
+ :organization="reuse.organization"
17
+ :owner="reuse.owner"
18
+ :organization-url="organizationUrl"
19
+ />
37
20
  <RiSubtractLine class="size-4 flex-none fill-gray-medium" />
38
21
  <span class="block flex-none">{{ t('publié {date}', { date: formatRelativeIfRecentDate(reuse.created_at, { dateStyle: 'medium' }) }) }}</span>
39
- </p>
22
+ </div>
40
23
  <ReuseDetails :reuse />
41
24
  </div>
42
25
  </div>
@@ -82,11 +65,10 @@ import { RiLockLine, RiSubtractLine } from '@remixicon/vue'
82
65
  import { computed } from 'vue'
83
66
  import type { RouteLocationRaw } from 'vue-router'
84
67
  import { useFormatDate } from '../functions/dates'
85
- import { getOwnerName } from '../functions/owned'
86
68
  import type { Reuse } from '../types/reuses'
87
69
  import { useTranslation } from '../composables/useTranslation'
88
70
  import AppLink from './AppLink.vue'
89
- import OrganizationNameWithCertificate from './OrganizationNameWithCertificate.vue'
71
+ import ObjectCardOwner from './ObjectCardOwner.vue'
90
72
  import ReuseDetails from './ReuseDetails.vue'
91
73
  import Placeholder from './Placeholder.vue'
92
74
 
@@ -109,7 +91,5 @@ const props = defineProps<{
109
91
  const { t } = useTranslation()
110
92
  const { formatRelativeIfRecentDate } = useFormatDate()
111
93
 
112
- const ownerName = computed(() => getOwnerName(props.reuse))
113
-
114
94
  const reuseUrl = computed(() => props.reuseUrl || props.reuse.page)
115
95
  </script>
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <ObjectCard media-size="lg">
3
+ <template #badge>
4
+ <ObjectCardBadge
5
+ v-if="reuse.private"
6
+ :icon="RiLockLine"
7
+ >
8
+ {{ t('Brouillon') }}
9
+ </ObjectCardBadge>
10
+ <ObjectCardBadge
11
+ v-else-if="reuse.archived"
12
+ :icon="RiArchiveLine"
13
+ >
14
+ {{ t('Archivé') }}
15
+ </ObjectCardBadge>
16
+ </template>
17
+
18
+ <template #media>
19
+ <img
20
+ v-if="reuse.image"
21
+ :src="reuse.image"
22
+ class="w-full h-full object-cover"
23
+ :alt="reuse.title"
24
+ >
25
+ <Placeholder
26
+ v-else
27
+ type="Reuse"
28
+ class="w-full h-full"
29
+ />
30
+ </template>
31
+
32
+ <ObjectCardHeader
33
+ :icon="RiLineChartLine"
34
+ :url="reuseUrl || reuse.page"
35
+ >
36
+ {{ reuse.title }}
37
+ </ObjectCardHeader>
38
+
39
+ <div
40
+ v-if="reuse.organization || reuse.owner"
41
+ class="text-sm m-0 flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
42
+ >
43
+ <ObjectCardOwner
44
+ :organization="reuse.organization"
45
+ :owner="reuse.owner"
46
+ :organization-url="organizationUrl"
47
+ />
48
+ </div>
49
+
50
+ <div class="mx-0 -mb-1 flex flex-wrap items-center text-sm text-gray-medium mt-1">
51
+ <ReuseDetails :reuse />
52
+ </div>
53
+
54
+ <ObjectCardShortDescription :text="reuse.description" />
55
+
56
+ <slot />
57
+ </ObjectCard>
58
+ </template>
59
+
60
+ <script setup lang="ts">
61
+ import { RiArchiveLine, RiLineChartLine, RiLockLine } from '@remixicon/vue'
62
+ import type { RouteLocationRaw } from 'vue-router'
63
+ import type { Reuse } from '../types/reuses'
64
+ import { useTranslation } from '../composables/useTranslation'
65
+ import ReuseDetails from './ReuseDetails.vue'
66
+ import Placeholder from './Placeholder.vue'
67
+ import ObjectCard from './ObjectCard.vue'
68
+ import ObjectCardHeader from './ObjectCardHeader.vue'
69
+ import ObjectCardOwner from './ObjectCardOwner.vue'
70
+ import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
71
+ import ObjectCardBadge from './ObjectCardBadge.vue'
72
+
73
+ defineProps<{
74
+ reuse: Reuse
75
+ reuseUrl?: RouteLocationRaw
76
+ organizationUrl?: RouteLocationRaw
77
+ }>()
78
+
79
+ const { t } = useTranslation()
80
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <!-- [&_.fr-input-group]:!mb-0 disables DSFR margin-bottom since we use gap for spacing -->
3
+ <div class="flex flex-col gap-4 [&_.fr-input-group]:!mb-0">
4
+ <slot
5
+ :is-enabled="isBasicFilter"
6
+ :get-order="getBasicOrder"
7
+ />
8
+ </div>
9
+
10
+ <Disclosure
11
+ v-if="advancedFilters.length"
12
+ v-slot="{ open }"
13
+ as="div"
14
+ class="mt-4"
15
+ >
16
+ <DisclosureButton class="flex w-[calc(100%+2rem)] items-center justify-between -mx-4 px-4 py-3 font-bold md:w-full md:mx-0 md:px-0 md:text-sm md:leading-tight md:mb-2">
17
+ {{ t('Filtres avancés') }}
18
+ <RiArrowDownSLine
19
+ class="size-4 transition-transform"
20
+ :class="{ 'rotate-180': open }"
21
+ />
22
+ </DisclosureButton>
23
+ <DisclosurePanel class="flex flex-col gap-4 mt-4 [&_.fr-input-group]:!mb-0">
24
+ <slot
25
+ :is-enabled="isAdvancedFilter"
26
+ :get-order="getAdvancedOrder"
27
+ />
28
+ </DisclosurePanel>
29
+ </Disclosure>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
34
+ import { RiArrowDownSLine } from '@remixicon/vue'
35
+ import { useTranslation } from '../../composables/useTranslation'
36
+
37
+ const props = defineProps<{
38
+ basicFilters: string[]
39
+ advancedFilters: string[]
40
+ }>()
41
+
42
+ const { t } = useTranslation()
43
+
44
+ const isBasicFilter = (name: string) => props.basicFilters.includes(name)
45
+ const isAdvancedFilter = (name: string) => props.advancedFilters.includes(name)
46
+
47
+ const getBasicOrder = (name: string) => props.basicFilters.indexOf(name)
48
+ const getAdvancedOrder = (name: string) => props.advancedFilters.indexOf(name)
49
+ </script>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <FilterButtonGroup
3
+ :model-value="modelValue"
4
+ :options="options"
5
+ :label="t(`Modalités d'accès`)"
6
+ :all-label="t('Toutes')"
7
+ :facets="facets"
8
+ :loading="loading"
9
+ name="access_type"
10
+ highlight-active
11
+ @update:model-value="emit('update:modelValue', $event)"
12
+ />
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import type { FacetItem } from '../../../types/search'
17
+ import { useTranslation } from '../../../composables/useTranslation'
18
+ import FilterButtonGroup from './FilterButtonGroup.vue'
19
+
20
+ defineProps<{
21
+ modelValue: string | undefined
22
+ facets?: FacetItem[]
23
+ loading?: boolean
24
+ }>()
25
+
26
+ const emit = defineEmits<{
27
+ 'update:modelValue': [value: string | undefined]
28
+ }>()
29
+
30
+ const { t } = useTranslation()
31
+
32
+ const options = [
33
+ { value: 'open', label: t('Téléchargement libre') },
34
+ { value: 'open_with_account', label: t('Ouvert sous condition') },
35
+ { value: 'restricted', label: t('Accessible sous habilitation') },
36
+ ]
37
+ </script>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <FilterButtonGroup
3
+ :model-value="modelValue"
4
+ :options="options"
5
+ :label="t('Label de donnée')"
6
+ :all-label="t('Tous')"
7
+ :facets="facets"
8
+ :loading="loading"
9
+ name="badge"
10
+ highlight-active
11
+ @update:model-value="emit('update:modelValue', $event)"
12
+ />
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { computed } from 'vue'
17
+ import type { FacetItem } from '../../../types/search'
18
+ import { useFetch } from '../../../functions/api'
19
+ import { useTranslation } from '../../../composables/useTranslation'
20
+ import FilterButtonGroup from './FilterButtonGroup.vue'
21
+
22
+ defineProps<{
23
+ modelValue: string | undefined
24
+ facets?: FacetItem[]
25
+ loading?: boolean
26
+ }>()
27
+
28
+ const emit = defineEmits<{
29
+ 'update:modelValue': [value: string | undefined]
30
+ }>()
31
+
32
+ const { t } = useTranslation()
33
+
34
+ const { data: badgesRecord } = await useFetch<Record<string, string>>('/api/1/datasets/badges/', { lazy: true })
35
+
36
+ const options = computed(() => {
37
+ if (!badgesRecord.value) return []
38
+ return Object.entries(badgesRecord.value).map(([value, label]) => ({ value, label }))
39
+ })
40
+ </script>