@datagouv/components-next 1.3.0 → 1.4.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.
- package/dist/{Control-b-43UT2B.js → Control-B9WQxlpJ.js} +10 -10
- package/dist/{Datafair.client-BrjISlaZ.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-py_AAwJY.js → Event-iXDrp-fy.js} +15 -15
- package/dist/{Image-BHQYZWzH.js → Image-mNMBOofs.js} +22 -22
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-DivFAnbN.js → JsonPreview.client-DSl1kiwC.js} +2 -2
- package/dist/{Map-CtP7leHl.js → Map-BfqKMw0S.js} +165 -165
- package/dist/{MapContainer.client-Cq8tNLZZ.js → MapContainer.client-BSfpk_rT.js} +17 -17
- package/dist/{OSM-gdDSIsDA.js → OSM-CcrUhItr.js} +1 -1
- package/dist/{PdfPreview.client-BNBgLsK2.js → PdfPreview.client-BANFeM6K.js} +438 -427
- package/dist/{Pmtiles.client-CQi5Rw8W.js → Pmtiles.client-DH9ZrsZR.js} +6177 -5967
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hG-FhNqy.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-D6rFonpT.js → Tile-CSpn0Ca8.js} +47 -47
- package/dist/{TileImage-cfrEgz5y.js → TileImage-CpbaEFwd.js} +44 -44
- package/dist/{View-vp7vDb9q.js → View-BYBpt-AU.js} +58 -58
- package/dist/{XmlPreview.client-XCsNvnsZ.js → XmlPreview.client-KCB5mwVf.js} +3 -3
- package/dist/{common-C8efyyuc.js → common-BoeTUnDZ.js} +58 -58
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +199 -172
- package/dist/components.css +1 -1
- package/dist/{index-CXUamc8Z.js → index-DHWzX5vK.js} +4668 -4466
- package/dist/{leaflet-src-BRKv9Ksv.js → leaflet-src-pVQfonBI.js} +1 -1
- package/dist/{main-DKu5SOAl.js → main-BDcYkhOc.js} +41684 -33432
- package/dist/{proj-CsjVeojN.js → proj-DxGbobp7.js} +110 -110
- package/dist/{tilecoord-CPcQJnrF.js → tilecoord-DvX7WWiA.js} +94 -94
- package/dist/{vue3-xml-viewer.common-BIaEIFYG.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +28 -27
- package/src/components/ActivityList/ActivityList.vue +21 -12
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +111 -17
- package/src/components/Chart/ChartViewerWrapper.vue +28 -16
- package/src/components/ChartCard.vue +112 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +23 -6
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +17 -6
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +69 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCard.vue +4 -1
- package/src/components/ObjectCardOwner.vue +13 -1
- package/src/components/Pagination.vue +18 -58
- package/src/components/Placeholder.vue +3 -2
- package/src/components/PostCard.vue +13 -15
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +19 -7
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
- package/src/components/ResourceAccordion/Metadata.vue +21 -5
- package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +27 -18
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +29 -9
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -209
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +136 -183
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +18 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +4 -3
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +12 -8
- package/src/components/Search/SearchInput.vue +1 -1
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +7 -8
- package/src/components/TabularExplorer/TabularCellPopover.vue +19 -3
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +122 -913
- package/src/components/TabularExplorer/TabularFilterContent.vue +33 -25
- package/src/components/TabularExplorer/TabularFilterPopover.vue +7 -7
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +484 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +32 -20
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +15 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +2 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +47 -39
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/charts.ts +32 -1
- package/src/functions/dates.ts +26 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/owned.ts +21 -0
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +83 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +119 -4
- package/src/functions/tabularApi.ts +1 -43
- package/src/main.ts +32 -6
- package/src/types/dataservices.ts +15 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js +0 -61
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -90,7 +90,11 @@
|
|
|
90
90
|
@change="changePage"
|
|
91
91
|
/>
|
|
92
92
|
<div class="fr-px-5v">
|
|
93
|
-
|
|
93
|
+
<TranslationT keypath="Dernière mise à jour de la prévisualisation : {date}">
|
|
94
|
+
<template #date>
|
|
95
|
+
<FormattedDate :date="parsingFinishedAt" />
|
|
96
|
+
</template>
|
|
97
|
+
</TranslationT> —
|
|
94
98
|
{{ t('{count} colonnes', columns.length) }} —
|
|
95
99
|
{{ t('Lignes {count}', rowCount) }}
|
|
96
100
|
</div>
|
|
@@ -103,7 +107,8 @@ import { computed, onMounted, ref } from 'vue'
|
|
|
103
107
|
import { RiArrowDownLine, RiArrowUpLine, RiExternalLinkFill } from '@remixicon/vue'
|
|
104
108
|
import Pagination from '../Pagination.vue'
|
|
105
109
|
import { getData, type SortConfig } from '../../functions/tabularApi'
|
|
106
|
-
import
|
|
110
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
111
|
+
import TranslationT from '../TranslationT.vue'
|
|
107
112
|
import { trackEvent } from '../../functions/matomo'
|
|
108
113
|
import type { Resource } from '../../types/resources'
|
|
109
114
|
import { useComponentsConfig } from '../../config'
|
|
@@ -116,7 +121,6 @@ import PreviewLoader from './PreviewLoader.vue'
|
|
|
116
121
|
const props = defineProps<{ resource: Resource }>()
|
|
117
122
|
|
|
118
123
|
const { t } = useTranslation()
|
|
119
|
-
const { formatDate } = useFormatDate()
|
|
120
124
|
|
|
121
125
|
const rows = ref<Array<Record<string, unknown>>>([])
|
|
122
126
|
const columns = ref<Array<string>>([])
|
|
@@ -196,7 +200,7 @@ function sortByField(col: string) {
|
|
|
196
200
|
getTableInfos(currentPage.value, sortConfig.value)
|
|
197
201
|
};
|
|
198
202
|
|
|
199
|
-
const
|
|
203
|
+
const parsingFinishedAt = computed(() => props.resource.extras['analysis:parsing:finished_at'] as string | undefined)
|
|
200
204
|
|
|
201
205
|
onMounted(() => {
|
|
202
206
|
getTableInfos(currentPage.value)
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="text-xs"
|
|
4
|
+
:class="fill ? 'flex min-h-0 flex-1 flex-col' : ''"
|
|
5
|
+
>
|
|
3
6
|
<slot
|
|
4
7
|
v-if="data !== null"
|
|
5
8
|
:data="data"
|
|
6
9
|
/>
|
|
7
|
-
<
|
|
10
|
+
<slot
|
|
8
11
|
v-else-if="loading"
|
|
9
|
-
|
|
12
|
+
name="loading"
|
|
10
13
|
>
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
<div class="text-gray-medium">
|
|
15
|
+
{{ t("Chargement de l'aperçu {fileType}...", { fileType }) }}
|
|
16
|
+
</div>
|
|
17
|
+
</slot>
|
|
13
18
|
<PreviewUnavailable v-else-if="!isSizeAllowed">
|
|
14
19
|
{{ fileSizeBytes
|
|
15
|
-
? t("Le fichier {fileType} est trop volumineux pour être prévisualisé. Téléchargez-le
|
|
16
|
-
: t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le
|
|
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.")
|
|
17
22
|
}}
|
|
18
23
|
</PreviewUnavailable>
|
|
19
24
|
<PreviewUnavailable v-else-if="corsStatus === 'blocked'">
|
|
20
|
-
{{ t("Ce fichier {fileType} ne peut pas être prévisualisé car il est hébergé sur un site distant qui restreint l'accès (CORS). Téléchargez-le
|
|
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 }) }}
|
|
21
26
|
</PreviewUnavailable>
|
|
22
27
|
<PreviewUnavailable v-else-if="error === 'network'">
|
|
23
|
-
{{ t("Ce fichier est hébergé sur un site externe qui ne permet pas la prévisualisation. Téléchargez-le
|
|
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.") }}
|
|
24
29
|
</PreviewUnavailable>
|
|
25
30
|
<PreviewUnavailable v-else-if="error">
|
|
26
|
-
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le
|
|
31
|
+
{{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le pour le consulter.") }}
|
|
27
32
|
</PreviewUnavailable>
|
|
28
33
|
</div>
|
|
29
34
|
</template>
|
|
@@ -40,6 +45,8 @@ const props = defineProps<{
|
|
|
40
45
|
resource: Resource
|
|
41
46
|
maxSize: number | undefined
|
|
42
47
|
load: () => Promise<unknown>
|
|
48
|
+
// Fill the available height (fullscreen) instead of sizing to content.
|
|
49
|
+
fill?: boolean
|
|
43
50
|
}>()
|
|
44
51
|
|
|
45
52
|
const emit = defineEmits<{
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
<CopyButton
|
|
48
48
|
:label="t('Copier le lien')"
|
|
49
49
|
:copied-label="t('Lien copié !')"
|
|
50
|
-
:text="
|
|
50
|
+
:text="externalUrl"
|
|
51
51
|
class="z-2"
|
|
52
52
|
/>
|
|
53
53
|
</div>
|
|
@@ -60,7 +60,17 @@
|
|
|
60
60
|
aria-hidden="true"
|
|
61
61
|
class="size-3 fill-gray-medium"
|
|
62
62
|
/>
|
|
63
|
-
<
|
|
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
|
|
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"
|
|
@@ -296,7 +313,8 @@ import { ref, computed, defineAsyncComponent } from 'vue'
|
|
|
296
313
|
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
|
|
297
314
|
import OrganizationNameWithCertificate from '../OrganizationNameWithCertificate.vue'
|
|
298
315
|
import { filesize, summarize } from '../../functions/helpers'
|
|
299
|
-
import
|
|
316
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
317
|
+
import TranslationT from '../TranslationT.vue'
|
|
300
318
|
import MarkdownViewer from '../MarkdownViewer.vue'
|
|
301
319
|
import type { CommunityResource, Resource } from '../../types/resources'
|
|
302
320
|
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
@@ -309,7 +327,7 @@ import { trackEvent } from '../../functions/matomo'
|
|
|
309
327
|
import CopyButton from '../CopyButton.vue'
|
|
310
328
|
import { useComponentsConfig } from '../../config'
|
|
311
329
|
import { getOwnerName } from '../../functions/owned'
|
|
312
|
-
import { getResourceFormatIcon, getResourceTitleId, detectOgcService,
|
|
330
|
+
import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceFilesize, isImagePreviewFormat, resolveResourceExternalUrl } from '../../functions/resources'
|
|
313
331
|
import BrandedButton from '../BrandedButton.vue'
|
|
314
332
|
import { useTranslation } from '../../composables/useTranslation'
|
|
315
333
|
import { useHasTabularData } from '../../composables/useHasTabularData'
|
|
@@ -331,6 +349,8 @@ const props = withDefaults(defineProps<{
|
|
|
331
349
|
isCommunityResource?: boolean
|
|
332
350
|
resource: Resource | CommunityResource
|
|
333
351
|
canEdit?: boolean
|
|
352
|
+
// Overrides the "Copier le lien" target.
|
|
353
|
+
resourceExternalUrl?: (resource: Resource | CommunityResource) => string
|
|
334
354
|
}>(), {
|
|
335
355
|
expandedOnMount: false,
|
|
336
356
|
isCommunityResource: false,
|
|
@@ -344,18 +364,18 @@ const Pmtiles = defineAsyncComponent(() => import('./Pmtiles.client.vue'))
|
|
|
344
364
|
const JsonPreview = defineAsyncComponent(() => import('./JsonPreview.client.vue'))
|
|
345
365
|
const PdfPreview = defineAsyncComponent(() => import('./PdfPreview.client.vue'))
|
|
346
366
|
const XmlPreview = defineAsyncComponent(() => import('./XmlPreview.client.vue'))
|
|
367
|
+
const ImagePreview = defineAsyncComponent(() => import('./ImagePreview.client.vue'))
|
|
347
368
|
const DatafairPreview = defineAsyncComponent(() => import('./Datafair.client.vue'))
|
|
348
369
|
|
|
349
370
|
const { t } = useTranslation()
|
|
350
|
-
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
351
371
|
const checkTabularData = useHasTabularData()
|
|
352
372
|
|
|
353
373
|
const hasPreview = computed(() => {
|
|
354
|
-
// For JSON, PDF, and
|
|
374
|
+
// For JSON, PDF, XML, and image files, show preview.
|
|
355
375
|
// We cannot check for CORS issues here because we cannot use an async component here.
|
|
356
376
|
// 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.
|
|
357
377
|
const format = props.resource.format?.toLowerCase()
|
|
358
|
-
return format === 'json' || format === 'pdf' || format === 'xml'
|
|
378
|
+
return format === 'json' || format === 'pdf' || format === 'xml' || isImagePreviewFormat(format)
|
|
359
379
|
})
|
|
360
380
|
|
|
361
381
|
const hasTabularData = computed(() => checkTabularData(props.resource))
|
|
@@ -450,7 +470,7 @@ const resourceFilesize = computed(() => getResourceFilesize(props.resource))
|
|
|
450
470
|
const unavailable = availabilityChecked && props.resource.extras['check:available'] === false
|
|
451
471
|
const downloadButtonTitle = unavailable ? t(`Le robot de {certifier} n'a pas pu accéder à ce fichier - Télécharger le fichier en {format}`, { certifier: config.name, format: format.value }) : t(`Télécharger le fichier en {format}`, { format: format.value })
|
|
452
472
|
|
|
453
|
-
const
|
|
473
|
+
const externalUrl = computed(() => resolveResourceExternalUrl(props.dataset, props.resource, props.resourceExternalUrl))
|
|
454
474
|
|
|
455
475
|
const resourceContentId = 'resource-' + props.resource.id
|
|
456
476
|
const resourceHeaderId = 'resource-' + props.resource.id + '-header'
|
|
@@ -1,49 +1,87 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<img
|
|
19
|
-
:src="noResultsImage"
|
|
20
|
-
class="h-20"
|
|
21
|
-
alt=""
|
|
22
|
-
>
|
|
23
|
-
</slot>
|
|
24
|
-
<p class="fr-text--bold fr-my-3v">
|
|
25
|
-
{{ t('Pas de résultats pour « {q} »', { q: search }) }}
|
|
26
|
-
</p>
|
|
27
|
-
<BrandedButton
|
|
28
|
-
color="primary"
|
|
29
|
-
@click="updateSearch('')"
|
|
30
|
-
>
|
|
31
|
-
{{ t('Réinitialiser la recherche') }}
|
|
32
|
-
</BrandedButton>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="hidden md:block">
|
|
2
|
+
<div
|
|
3
|
+
v-if="groups.length || hasAnyResources"
|
|
4
|
+
:class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''"
|
|
5
|
+
>
|
|
6
|
+
<ResourceExplorerHeader
|
|
7
|
+
v-if="fullscreen"
|
|
8
|
+
class="shrink-0"
|
|
9
|
+
:dataset
|
|
10
|
+
:resource="selectedResource"
|
|
11
|
+
:exit-to="exitTo"
|
|
12
|
+
/>
|
|
13
|
+
<div
|
|
14
|
+
class="flex"
|
|
15
|
+
:class="fullscreen ? 'min-h-0 flex-1 overflow-hidden' : 'overflow-hidden rounded border border-gray-default'"
|
|
16
|
+
>
|
|
17
|
+
<div class="hidden md:flex">
|
|
36
18
|
<ResourceExplorerSidebar
|
|
37
|
-
:
|
|
19
|
+
:groups
|
|
38
20
|
:selected-resource-id="selectedResource?.id ?? null"
|
|
39
21
|
:collapsed="sidebarCollapsed"
|
|
40
22
|
:search
|
|
41
|
-
|
|
23
|
+
:loading-type="loadingType"
|
|
24
|
+
:resource-to="resourceTo"
|
|
25
|
+
replace
|
|
42
26
|
@load-more="loadMore"
|
|
43
27
|
@update:collapsed="sidebarCollapsed = $event"
|
|
44
28
|
@update:search="updateSearch($event)"
|
|
45
29
|
/>
|
|
46
30
|
</div>
|
|
31
|
+
<div
|
|
32
|
+
id="resource-explorer-viewer"
|
|
33
|
+
class="flex-1 min-w-0"
|
|
34
|
+
:class="fullscreen ? 'flex flex-col' : ''"
|
|
35
|
+
role="region"
|
|
36
|
+
:aria-label="t('Détail de la ressource')"
|
|
37
|
+
>
|
|
38
|
+
<!-- Suspense shows the skeleton fallback while the (async) viewer resolves
|
|
39
|
+
its data on each resource switch. `timeout` keeps the previous resource
|
|
40
|
+
visible for a beat so a near-instant switch doesn't flash a skeleton. -->
|
|
41
|
+
<Suspense
|
|
42
|
+
v-if="selectedResource"
|
|
43
|
+
:timeout="200"
|
|
44
|
+
>
|
|
45
|
+
<ResourceExplorerViewer
|
|
46
|
+
:key="selectedResource.id"
|
|
47
|
+
:dataset
|
|
48
|
+
:resource="selectedResource"
|
|
49
|
+
:resources="flatResources"
|
|
50
|
+
:resource-to="resourceTo"
|
|
51
|
+
:explore-to="exploreTo"
|
|
52
|
+
:resource-external-url="resourceExternalUrl"
|
|
53
|
+
replace
|
|
54
|
+
:fullscreen
|
|
55
|
+
/>
|
|
56
|
+
<template #fallback>
|
|
57
|
+
<ResourceViewerSkeleton
|
|
58
|
+
:resource="selectedResource"
|
|
59
|
+
:dataset
|
|
60
|
+
:resources="flatResources"
|
|
61
|
+
:resource-to="resourceTo"
|
|
62
|
+
:explore-to="exploreTo"
|
|
63
|
+
:resource-external-url="resourceExternalUrl"
|
|
64
|
+
replace
|
|
65
|
+
:fullscreen
|
|
66
|
+
/>
|
|
67
|
+
</template>
|
|
68
|
+
</Suspense>
|
|
69
|
+
<!-- The selection never falls back to nothing once a resource has been shown,
|
|
70
|
+
so having none means the groups aren't in yet: only `main` is fetched on
|
|
71
|
+
the server, the other types arrive on hydration. -->
|
|
72
|
+
<div
|
|
73
|
+
v-else
|
|
74
|
+
class="animate-pulse-placeholder p-4"
|
|
75
|
+
:class="fullscreen ? 'min-h-0 flex-1' : ''"
|
|
76
|
+
role="status"
|
|
77
|
+
:aria-label="t('Chargement des ressources…')"
|
|
78
|
+
>
|
|
79
|
+
<div
|
|
80
|
+
class="w-full rounded bg-gray-200"
|
|
81
|
+
:class="fullscreen ? 'h-full' : 'h-96'"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
47
85
|
</div>
|
|
48
86
|
</div>
|
|
49
87
|
<div
|
|
@@ -64,194 +102,52 @@
|
|
|
64
102
|
</template>
|
|
65
103
|
|
|
66
104
|
<script setup lang="ts">
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import { useComponentsConfig } from '../../config'
|
|
105
|
+
import { ref } from 'vue'
|
|
106
|
+
import { useRoute } from 'vue-router'
|
|
107
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
71
108
|
import { useTranslation } from '../../composables/useTranslation'
|
|
72
|
-
import {
|
|
73
|
-
import { useFetch } from '../../functions/api'
|
|
74
|
-
import { RESOURCE_EXPLORER_PAGE_SIZE, RESOURCE_TYPE } from '../../functions/resources'
|
|
75
|
-
import type { PaginatedArray } from '../../types/api'
|
|
109
|
+
import { useDatasetResources } from '../../composables/useDatasetResources'
|
|
76
110
|
import type { DatasetV2 } from '../../types/datasets'
|
|
77
|
-
import type { Resource
|
|
111
|
+
import type { Resource } from '../../types/resources'
|
|
78
112
|
import ResourceExplorerSidebar from './ResourceExplorerSidebar.vue'
|
|
79
113
|
import ResourceExplorerViewer from './ResourceExplorerViewer.vue'
|
|
80
|
-
import
|
|
114
|
+
import ResourceExplorerHeader from './ResourceExplorerHeader.vue'
|
|
115
|
+
import ResourceViewerSkeleton from './ResourceViewerSkeleton.vue'
|
|
81
116
|
|
|
82
117
|
const props = withDefaults(defineProps<{
|
|
83
118
|
dataset: DatasetV2
|
|
84
119
|
noResultsImage?: string
|
|
120
|
+
// Fullscreen mode: dataset context bar (org / title / date + download + exit), the
|
|
121
|
+
// viewer fills the height and hides its inline actions (shown in the context bar).
|
|
122
|
+
fullscreen?: boolean
|
|
123
|
+
exitTo?: RouteLocationRaw
|
|
124
|
+
// Inline mode only: link builder for the "Explorer" button in the viewer header
|
|
125
|
+
// that opens the fullscreen explorer on the current resource.
|
|
126
|
+
exploreTo?: (resource: Resource) => string
|
|
127
|
+
// Overrides the "Copier le lien" target.
|
|
128
|
+
resourceExternalUrl?: (resource: Resource) => string
|
|
85
129
|
}>(), {
|
|
86
130
|
noResultsImage: '',
|
|
131
|
+
fullscreen: false,
|
|
87
132
|
})
|
|
88
133
|
|
|
89
134
|
const { t } = useTranslation()
|
|
90
|
-
const
|
|
91
|
-
|
|
135
|
+
const route = useRoute()
|
|
136
|
+
|
|
137
|
+
const {
|
|
138
|
+
groups,
|
|
139
|
+
flatResources,
|
|
140
|
+
hasAnyResources,
|
|
141
|
+
selectedResource,
|
|
142
|
+
loadMore,
|
|
143
|
+
loadingType,
|
|
144
|
+
search,
|
|
145
|
+
updateSearch,
|
|
146
|
+
} = await useDatasetResources(() => props.dataset)
|
|
92
147
|
|
|
93
148
|
const sidebarCollapsed = ref(false)
|
|
94
|
-
const search = ref('')
|
|
95
|
-
const { debounced: searchDebounced, flush } = useDebouncedRef(search, config.searchDebounce ?? 300)
|
|
96
|
-
|
|
97
|
-
const url = computed(() => `/api/2/datasets/${props.dataset.id}/resources/`)
|
|
98
|
-
|
|
99
|
-
// Resource ID from URL query
|
|
100
|
-
const resourceIdQuery = useRouteQuery<string | undefined>('resource_id')
|
|
101
|
-
|
|
102
|
-
// Fetch resources for each type
|
|
103
|
-
const mainParams = computed(() => ({
|
|
104
|
-
type: 'main' as const,
|
|
105
|
-
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
106
|
-
q: searchDebounced.value || undefined,
|
|
107
|
-
}))
|
|
108
|
-
const documentationParams = computed(() => ({
|
|
109
|
-
type: 'documentation' as const,
|
|
110
|
-
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
111
|
-
q: searchDebounced.value || undefined,
|
|
112
|
-
}))
|
|
113
|
-
const updateParams = computed(() => ({
|
|
114
|
-
type: 'update' as const,
|
|
115
|
-
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
116
|
-
q: searchDebounced.value || undefined,
|
|
117
|
-
}))
|
|
118
|
-
const apiParams = computed(() => ({
|
|
119
|
-
type: 'api' as const,
|
|
120
|
-
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
121
|
-
q: searchDebounced.value || undefined,
|
|
122
|
-
}))
|
|
123
|
-
const codeParams = computed(() => ({
|
|
124
|
-
type: 'code' as const,
|
|
125
|
-
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
126
|
-
q: searchDebounced.value || undefined,
|
|
127
|
-
}))
|
|
128
|
-
const otherParams = computed(() => ({
|
|
129
|
-
type: 'other' as const,
|
|
130
|
-
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
131
|
-
q: searchDebounced.value || undefined,
|
|
132
|
-
}))
|
|
133
|
-
|
|
134
|
-
const { data: mainData, status: mainStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: mainParams })
|
|
135
|
-
const { data: documentationData, status: documentationStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: documentationParams, server: false })
|
|
136
|
-
const { data: updateData, status: updateStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: updateParams, server: false })
|
|
137
|
-
const { data: apiData, status: apiStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: apiParams, server: false })
|
|
138
|
-
const { data: codeData, status: codeStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: codeParams, server: false })
|
|
139
|
-
const { data: otherData, status: otherStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: otherParams, server: false })
|
|
140
|
-
|
|
141
|
-
const rawResourcesByTypes = [
|
|
142
|
-
{ data: mainData, status: mainStatus },
|
|
143
|
-
{ data: documentationData, status: documentationStatus },
|
|
144
|
-
{ data: updateData, status: updateStatus },
|
|
145
|
-
{ data: apiData, status: apiStatus },
|
|
146
|
-
{ data: codeData, status: codeStatus },
|
|
147
|
-
{ data: otherData, status: otherStatus },
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
// Evaluated once at setup (before any search) — never changes afterwards
|
|
151
|
-
const hasAnyResources = computed(() => {
|
|
152
|
-
return props.dataset.resources.total > 0
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const extraResourcesByType: Ref<Resource[]>[] = RESOURCE_TYPE.map(() => ref<Resource[]>([]))
|
|
156
|
-
const pageByType: Ref<number>[] = RESOURCE_TYPE.map(() => ref(1))
|
|
157
|
-
|
|
158
|
-
watch(searchDebounced, () => {
|
|
159
|
-
for (let i = 0; i < RESOURCE_TYPE.length; i++) {
|
|
160
|
-
extraResourcesByType[i]!.value = []
|
|
161
|
-
pageByType[i]!.value = 1
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// Separate useFetch for loadMore, initialized at setup time with immediate: false
|
|
166
|
-
// so that it doesn't fetch until execute() is called from the event handler.
|
|
167
|
-
const loadMoreType = ref<ResourceType>('main')
|
|
168
|
-
const loadMorePage = ref(1)
|
|
169
|
-
const loadMoreParams = computed(() => ({
|
|
170
|
-
type: loadMoreType.value,
|
|
171
|
-
page_size: RESOURCE_EXPLORER_PAGE_SIZE,
|
|
172
|
-
page: loadMorePage.value,
|
|
173
|
-
q: searchDebounced.value || undefined,
|
|
174
|
-
}))
|
|
175
|
-
const { data: loadMoreData, execute: executeLoadMore } = await useFetch<PaginatedArray<Resource>>(url, {
|
|
176
|
-
params: loadMoreParams,
|
|
177
|
-
immediate: false,
|
|
178
|
-
watch: false,
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
const loadMore = async (type: ResourceType) => {
|
|
182
|
-
const index = RESOURCE_TYPE.indexOf(type)
|
|
183
|
-
if (index === -1) return
|
|
184
|
-
const pageRef = pageByType[index]!
|
|
185
|
-
const extraRef = extraResourcesByType[index]!
|
|
186
|
-
pageRef.value++
|
|
187
|
-
|
|
188
|
-
loadMoreType.value = type
|
|
189
|
-
loadMorePage.value = pageRef.value
|
|
190
|
-
await executeLoadMore()
|
|
191
|
-
|
|
192
|
-
if (loadMoreData.value) {
|
|
193
|
-
extraRef.value = [...extraRef.value, ...loadMoreData.value.data]
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const allResources = computed<ResourceGroup[]>(() => {
|
|
198
|
-
return RESOURCE_TYPE
|
|
199
|
-
.map((type, index) => {
|
|
200
|
-
const rawData = rawResourcesByTypes[index]!
|
|
201
|
-
const extraData = extraResourcesByType[index]!
|
|
202
|
-
return {
|
|
203
|
-
type: type as ResourceType,
|
|
204
|
-
total: rawData.data.value?.total ?? 0,
|
|
205
|
-
items: [...(rawData.data.value?.data ?? []), ...extraData.value],
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
.filter(group => group.items.length > 0)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
const flatResources = computed(() =>
|
|
212
|
-
allResources.value.flatMap(g => g.items),
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
// Fetch resource by ID if specified in URL (for SSR)
|
|
216
|
-
const initialResourceId = resourceIdQuery.value
|
|
217
|
-
const { data: fetchedResource } = initialResourceId
|
|
218
|
-
? await useFetch<Resource>(`/api/1/datasets/${props.dataset.id}/resources/${initialResourceId}/`)
|
|
219
|
-
: { data: ref(null) }
|
|
220
|
-
|
|
221
|
-
// Initial selection (synchronous for SSR hydration)
|
|
222
|
-
function getInitialResource(): Resource | null {
|
|
223
|
-
const resourceId = resourceIdQuery.value
|
|
224
|
-
if (resourceId) {
|
|
225
|
-
// First check in already loaded resources
|
|
226
|
-
const existing = flatResources.value.find(r => r.id === resourceId)
|
|
227
|
-
if (existing) return existing
|
|
228
|
-
// Use fetched resource if available
|
|
229
|
-
if (fetchedResource.value) return fetchedResource.value
|
|
230
|
-
}
|
|
231
|
-
// Default to first resource
|
|
232
|
-
return flatResources.value[0] ?? null
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const selectedResource = ref<Resource | null>(getInitialResource())
|
|
236
|
-
|
|
237
|
-
function updateSearch(newSearch: string) {
|
|
238
|
-
search.value = newSearch
|
|
239
|
-
flush()
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const selectResource = (resource: Resource) => {
|
|
243
|
-
selectedResource.value = resource
|
|
244
|
-
router.replace({
|
|
245
|
-
query: { ...router.currentRoute.value.query, resource_id: resource.id },
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
149
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (selectedResource.value) return
|
|
252
|
-
const firstResource = flatResources.value[0]
|
|
253
|
-
if (firstResource) {
|
|
254
|
-
selectedResource.value = firstResource
|
|
255
|
-
}
|
|
150
|
+
const resourceTo = (resource: Resource): RouteLocationRaw => ({
|
|
151
|
+
query: { ...route.query, resource_id: resource.id },
|
|
256
152
|
})
|
|
257
153
|
</script>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sticky top-0 z-30 flex h-[58px] shrink-0 items-center justify-between gap-2 border-b border-gray-default bg-gray-some/95 px-3 backdrop-blur-[5px] sm:gap-4">
|
|
3
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
4
|
+
<OrganizationLogo
|
|
5
|
+
v-if="dataset.organization"
|
|
6
|
+
:organization="dataset.organization"
|
|
7
|
+
size-class="size-[34px] shrink-0 rounded-sm border border-gray-default bg-white p-1"
|
|
8
|
+
/>
|
|
9
|
+
<Avatar
|
|
10
|
+
v-else-if="dataset.owner"
|
|
11
|
+
:user="dataset.owner"
|
|
12
|
+
:size="34"
|
|
13
|
+
class="shrink-0"
|
|
14
|
+
/>
|
|
15
|
+
<div class="flex min-w-0 items-center gap-1 text-[13px] leading-[1.4] sm:text-base">
|
|
16
|
+
<span class="min-w-0 truncate text-gray-title">{{ producerName }}</span>
|
|
17
|
+
<span class="shrink-0 text-gray-title">/</span>
|
|
18
|
+
<span class="min-w-0 truncate font-bold text-gray-title">{{ dataset.title }}</span>
|
|
19
|
+
<template v-if="dataset.last_update">
|
|
20
|
+
<span class="hidden shrink-0 text-gray-medium sm:inline">·</span>
|
|
21
|
+
<TranslationT
|
|
22
|
+
class="hidden truncate text-gray-medium sm:inline"
|
|
23
|
+
keypath="mis à jour le {date}"
|
|
24
|
+
>
|
|
25
|
+
<template #date>
|
|
26
|
+
<FormattedDate :date="dataset.last_update" />
|
|
27
|
+
</template>
|
|
28
|
+
</TranslationT>
|
|
29
|
+
</template>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
33
|
+
<ResourceMainAction
|
|
34
|
+
v-if="resource"
|
|
35
|
+
:resource="resource"
|
|
36
|
+
:dataset="dataset"
|
|
37
|
+
/>
|
|
38
|
+
<BrandedButton
|
|
39
|
+
v-if="exitTo"
|
|
40
|
+
:href="exitTo"
|
|
41
|
+
:icon="RiFullscreenExitLine"
|
|
42
|
+
color="secondary"
|
|
43
|
+
size="xs"
|
|
44
|
+
icon-only
|
|
45
|
+
:title="t('Quitter le plein écran')"
|
|
46
|
+
>
|
|
47
|
+
{{ t('Quitter le plein écran') }}
|
|
48
|
+
</BrandedButton>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
import { computed } from 'vue'
|
|
55
|
+
import { RiFullscreenExitLine } from '@remixicon/vue'
|
|
56
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
57
|
+
import Avatar from '../Avatar.vue'
|
|
58
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
59
|
+
import OrganizationLogo from '../OrganizationLogo.vue'
|
|
60
|
+
import ResourceMainAction from './ResourceMainAction.vue'
|
|
61
|
+
import FormattedDate from '../FormattedDate.vue'
|
|
62
|
+
import TranslationT from '../TranslationT.vue'
|
|
63
|
+
import { getOwnerName } from '../../functions/owned'
|
|
64
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
65
|
+
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
66
|
+
import type { Resource } from '../../types/resources'
|
|
67
|
+
|
|
68
|
+
const props = defineProps<{
|
|
69
|
+
dataset: Dataset | DatasetV2
|
|
70
|
+
resource: Resource | null
|
|
71
|
+
exitTo?: RouteLocationRaw
|
|
72
|
+
}>()
|
|
73
|
+
|
|
74
|
+
const { t } = useTranslation()
|
|
75
|
+
|
|
76
|
+
// Plain producer name, matching the prototype: dark text, no link, no certificate
|
|
77
|
+
// badge — unlike ObjectCardOwner which renders a styled link.
|
|
78
|
+
const producerName = computed(() => getOwnerName(props.dataset))
|
|
79
|
+
</script>
|