@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/assets/main.css +4 -0
- package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
- package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
- package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
- package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
- package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
- package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
- package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
- package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
- package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
- package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
- package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
- package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +196 -150
- package/dist/components.css +2 -2
- package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
- package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
- package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
- package/package.json +47 -38
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +24 -14
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +320 -0
- package/src/components/Chart/ChartViewerWrapper.vue +182 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +24 -5
- 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 +24 -9
- 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/Listbox.vue +101 -0
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +81 -0
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCardHeader.vue +11 -4
- package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
- package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
- package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
- package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
- package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
- package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
- package/src/components/OpenApiViewer/openapi.ts +150 -0
- package/src/components/OrganizationNameWithCertificate.vue +3 -2
- package/src/components/Pagination.vue +25 -62
- package/src/components/PostCard.vue +13 -15
- package/src/components/RadioInput.vue +7 -2
- package/src/components/ResourceAccordion/DataStructure.vue +11 -33
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +172 -0
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
- package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
- package/src/components/ResourceAccordion/Metadata.vue +4 -6
- package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
- package/src/components/ResourceAccordion/Preview.vue +19 -15
- package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +28 -9
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +2 -2
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +229 -123
- package/src/components/Search/SearchInput.vue +5 -4
- 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 +50 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
- 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 +481 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +84 -0
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +13 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +15 -1
- package/src/composables/useMetrics.ts +1 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +48 -40
- package/src/composables/useSearchFilter.ts +120 -0
- package/src/composables/useStableQueryParams.ts +38 -6
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +21 -3
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +20 -42
- package/src/functions/api.types.ts +1 -0
- package/src/functions/charts.ts +99 -0
- package/src/functions/datasets.ts +0 -17
- package/src/functions/dates.ts +6 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +63 -60
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +136 -1
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +214 -0
- package/src/functions/tabularApi.ts +96 -11
- package/src/main.ts +119 -14
- package/src/types/dataservices.ts +17 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/pages.ts +0 -5
- package/src/types/posts.ts +2 -2
- package/src/types/reports.ts +5 -1
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +63 -1
- package/src/types/site.ts +5 -3
- package/src/types/ui.ts +2 -0
- package/src/types/users.ts +2 -1
- package/src/types/visualizations.ts +93 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
- package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
- package/dist/Swagger.client-B4y0hXME.js +0 -4
- package/dist/XmlPreview.client-LETEfNBH.js +0 -64
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
- /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
|
@@ -1,45 +1,85 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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">
|
|
18
|
+
<ResourceExplorerSidebar
|
|
19
|
+
:groups
|
|
20
|
+
:selected-resource-id="selectedResource?.id ?? null"
|
|
21
|
+
:collapsed="sidebarCollapsed"
|
|
22
|
+
:search
|
|
23
|
+
:loading-type="loadingType"
|
|
24
|
+
:resource-to="resourceTo"
|
|
25
|
+
replace
|
|
26
|
+
@load-more="loadMore"
|
|
27
|
+
@update:collapsed="sidebarCollapsed = $event"
|
|
28
|
+
@update:search="updateSearch($event)"
|
|
10
29
|
/>
|
|
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
|
+
replace
|
|
53
|
+
:fullscreen
|
|
54
|
+
/>
|
|
55
|
+
<template #fallback>
|
|
56
|
+
<ResourceViewerSkeleton
|
|
57
|
+
:resource="selectedResource"
|
|
58
|
+
:dataset
|
|
59
|
+
:resources="flatResources"
|
|
60
|
+
:resource-to="resourceTo"
|
|
61
|
+
:explore-to="exploreTo"
|
|
62
|
+
replace
|
|
63
|
+
:fullscreen
|
|
64
|
+
/>
|
|
65
|
+
</template>
|
|
66
|
+
</Suspense>
|
|
67
|
+
<!-- The selection never falls back to nothing once a resource has been shown,
|
|
68
|
+
so having none means the groups aren't in yet: only `main` is fetched on
|
|
69
|
+
the server, the other types arrive on hydration. -->
|
|
11
70
|
<div
|
|
12
|
-
v-else
|
|
13
|
-
class="
|
|
71
|
+
v-else
|
|
72
|
+
class="animate-pulse-placeholder p-4"
|
|
73
|
+
:class="fullscreen ? 'min-h-0 flex-1' : ''"
|
|
74
|
+
role="status"
|
|
75
|
+
:aria-label="t('Chargement des ressources…')"
|
|
14
76
|
>
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
alt=""
|
|
20
|
-
>
|
|
21
|
-
</slot>
|
|
22
|
-
<p class="fr-text--bold fr-my-3v">
|
|
23
|
-
{{ t('Pas de résultats pour « {q} »', { q: search }) }}
|
|
24
|
-
</p>
|
|
25
|
-
<BrandedButton
|
|
26
|
-
color="primary"
|
|
27
|
-
@click="updateSearch('')"
|
|
28
|
-
>
|
|
29
|
-
{{ t('Réinitialiser la recherche') }}
|
|
30
|
-
</BrandedButton>
|
|
77
|
+
<div
|
|
78
|
+
class="w-full rounded bg-gray-200"
|
|
79
|
+
:class="fullscreen ? 'h-full' : 'h-96'"
|
|
80
|
+
/>
|
|
31
81
|
</div>
|
|
32
82
|
</div>
|
|
33
|
-
<ResourceExplorerSidebar
|
|
34
|
-
:resources="allResources"
|
|
35
|
-
:selected-resource-id="selectedResource?.id ?? null"
|
|
36
|
-
:collapsed="sidebarCollapsed"
|
|
37
|
-
:search
|
|
38
|
-
@select="selectResource"
|
|
39
|
-
@load-more="loadMore"
|
|
40
|
-
@update:collapsed="sidebarCollapsed = $event"
|
|
41
|
-
@update:search="updateSearch($event)"
|
|
42
|
-
/>
|
|
43
83
|
</div>
|
|
44
84
|
</div>
|
|
45
85
|
<div
|
|
@@ -60,195 +100,50 @@
|
|
|
60
100
|
</template>
|
|
61
101
|
|
|
62
102
|
<script setup lang="ts">
|
|
63
|
-
import {
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
import { useComponentsConfig } from '../../config'
|
|
103
|
+
import { ref } from 'vue'
|
|
104
|
+
import { useRoute } from 'vue-router'
|
|
105
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
67
106
|
import { useTranslation } from '../../composables/useTranslation'
|
|
68
|
-
import {
|
|
69
|
-
import { useFetch } from '../../functions/api'
|
|
70
|
-
import { RESOURCE_TYPE } from '../../functions/resources'
|
|
71
|
-
import type { PaginatedArray } from '../../types/api'
|
|
107
|
+
import { useDatasetResources } from '../../composables/useDatasetResources'
|
|
72
108
|
import type { DatasetV2 } from '../../types/datasets'
|
|
73
|
-
import type { Resource
|
|
109
|
+
import type { Resource } from '../../types/resources'
|
|
74
110
|
import ResourceExplorerSidebar from './ResourceExplorerSidebar.vue'
|
|
75
111
|
import ResourceExplorerViewer from './ResourceExplorerViewer.vue'
|
|
76
|
-
import
|
|
112
|
+
import ResourceExplorerHeader from './ResourceExplorerHeader.vue'
|
|
113
|
+
import ResourceViewerSkeleton from './ResourceViewerSkeleton.vue'
|
|
77
114
|
|
|
78
115
|
const props = withDefaults(defineProps<{
|
|
79
116
|
dataset: DatasetV2
|
|
80
117
|
noResultsImage?: string
|
|
118
|
+
// Fullscreen mode: dataset context bar (org / title / date + download + exit), the
|
|
119
|
+
// viewer fills the height and hides its inline actions (shown in the context bar).
|
|
120
|
+
fullscreen?: boolean
|
|
121
|
+
exitTo?: RouteLocationRaw
|
|
122
|
+
// Inline mode only: link builder for the "Explorer" button in the viewer header
|
|
123
|
+
// that opens the fullscreen explorer on the current resource.
|
|
124
|
+
exploreTo?: (resource: Resource) => string
|
|
81
125
|
}>(), {
|
|
82
126
|
noResultsImage: '',
|
|
127
|
+
fullscreen: false,
|
|
83
128
|
})
|
|
84
129
|
|
|
85
130
|
const { t } = useTranslation()
|
|
86
|
-
const
|
|
87
|
-
|
|
131
|
+
const route = useRoute()
|
|
132
|
+
|
|
133
|
+
const {
|
|
134
|
+
groups,
|
|
135
|
+
flatResources,
|
|
136
|
+
hasAnyResources,
|
|
137
|
+
selectedResource,
|
|
138
|
+
loadMore,
|
|
139
|
+
loadingType,
|
|
140
|
+
search,
|
|
141
|
+
updateSearch,
|
|
142
|
+
} = await useDatasetResources(() => props.dataset)
|
|
88
143
|
|
|
89
144
|
const sidebarCollapsed = ref(false)
|
|
90
|
-
const search = ref('')
|
|
91
|
-
const { debounced: searchDebounced, flush } = useDebouncedRef(search, config.searchDebounce ?? 300)
|
|
92
|
-
|
|
93
|
-
const PAGE_SIZE = 10
|
|
94
|
-
const url = computed(() => `/api/2/datasets/${props.dataset.id}/resources/`)
|
|
95
|
-
|
|
96
|
-
// Resource ID from URL query
|
|
97
|
-
const resourceIdQuery = useRouteQuery<string | undefined>('resource_id')
|
|
98
|
-
|
|
99
|
-
// Fetch resources for each type
|
|
100
|
-
const mainParams = computed(() => ({
|
|
101
|
-
type: 'main' as const,
|
|
102
|
-
page_size: PAGE_SIZE,
|
|
103
|
-
q: searchDebounced.value || undefined,
|
|
104
|
-
}))
|
|
105
|
-
const documentationParams = computed(() => ({
|
|
106
|
-
type: 'documentation' as const,
|
|
107
|
-
page_size: PAGE_SIZE,
|
|
108
|
-
q: searchDebounced.value || undefined,
|
|
109
|
-
}))
|
|
110
|
-
const updateParams = computed(() => ({
|
|
111
|
-
type: 'update' as const,
|
|
112
|
-
page_size: PAGE_SIZE,
|
|
113
|
-
q: searchDebounced.value || undefined,
|
|
114
|
-
}))
|
|
115
|
-
const apiParams = computed(() => ({
|
|
116
|
-
type: 'api' as const,
|
|
117
|
-
page_size: PAGE_SIZE,
|
|
118
|
-
q: searchDebounced.value || undefined,
|
|
119
|
-
}))
|
|
120
|
-
const codeParams = computed(() => ({
|
|
121
|
-
type: 'code' as const,
|
|
122
|
-
page_size: PAGE_SIZE,
|
|
123
|
-
q: searchDebounced.value || undefined,
|
|
124
|
-
}))
|
|
125
|
-
const otherParams = computed(() => ({
|
|
126
|
-
type: 'other' as const,
|
|
127
|
-
page_size: PAGE_SIZE,
|
|
128
|
-
q: searchDebounced.value || undefined,
|
|
129
|
-
}))
|
|
130
|
-
|
|
131
|
-
const { data: mainData, status: mainStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: mainParams })
|
|
132
|
-
const { data: documentationData, status: documentationStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: documentationParams, server: false })
|
|
133
|
-
const { data: updateData, status: updateStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: updateParams, server: false })
|
|
134
|
-
const { data: apiData, status: apiStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: apiParams, server: false })
|
|
135
|
-
const { data: codeData, status: codeStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: codeParams, server: false })
|
|
136
|
-
const { data: otherData, status: otherStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: otherParams, server: false })
|
|
137
|
-
|
|
138
|
-
const rawResourcesByTypes = [
|
|
139
|
-
{ data: mainData, status: mainStatus },
|
|
140
|
-
{ data: documentationData, status: documentationStatus },
|
|
141
|
-
{ data: updateData, status: updateStatus },
|
|
142
|
-
{ data: apiData, status: apiStatus },
|
|
143
|
-
{ data: codeData, status: codeStatus },
|
|
144
|
-
{ data: otherData, status: otherStatus },
|
|
145
|
-
]
|
|
146
|
-
|
|
147
|
-
// Evaluated once at setup (before any search) — never changes afterwards
|
|
148
|
-
const hasAnyResources = computed(() => {
|
|
149
|
-
return props.dataset.resources.total > 0
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
const extraResourcesByType: Ref<Resource[]>[] = RESOURCE_TYPE.map(() => ref<Resource[]>([]))
|
|
153
|
-
const pageByType: Ref<number>[] = RESOURCE_TYPE.map(() => ref(1))
|
|
154
|
-
|
|
155
|
-
watch(searchDebounced, () => {
|
|
156
|
-
for (let i = 0; i < RESOURCE_TYPE.length; i++) {
|
|
157
|
-
extraResourcesByType[i]!.value = []
|
|
158
|
-
pageByType[i]!.value = 1
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// Separate useFetch for loadMore, initialized at setup time with immediate: false
|
|
163
|
-
// so that it doesn't fetch until execute() is called from the event handler.
|
|
164
|
-
const loadMoreType = ref<ResourceType>('main')
|
|
165
|
-
const loadMorePage = ref(1)
|
|
166
|
-
const loadMoreParams = computed(() => ({
|
|
167
|
-
type: loadMoreType.value,
|
|
168
|
-
page_size: PAGE_SIZE,
|
|
169
|
-
page: loadMorePage.value,
|
|
170
|
-
q: searchDebounced.value || undefined,
|
|
171
|
-
}))
|
|
172
|
-
const { data: loadMoreData, execute: executeLoadMore } = await useFetch<PaginatedArray<Resource>>(url, {
|
|
173
|
-
params: loadMoreParams,
|
|
174
|
-
immediate: false,
|
|
175
|
-
watch: false,
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
const loadMore = async (type: ResourceType) => {
|
|
179
|
-
const index = RESOURCE_TYPE.indexOf(type)
|
|
180
|
-
if (index === -1) return
|
|
181
|
-
const pageRef = pageByType[index]!
|
|
182
|
-
const extraRef = extraResourcesByType[index]!
|
|
183
|
-
pageRef.value++
|
|
184
|
-
|
|
185
|
-
loadMoreType.value = type
|
|
186
|
-
loadMorePage.value = pageRef.value
|
|
187
|
-
await executeLoadMore()
|
|
188
|
-
|
|
189
|
-
if (loadMoreData.value) {
|
|
190
|
-
extraRef.value = [...extraRef.value, ...loadMoreData.value.data]
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const allResources = computed<ResourceGroup[]>(() => {
|
|
195
|
-
return RESOURCE_TYPE
|
|
196
|
-
.map((type, index) => {
|
|
197
|
-
const rawData = rawResourcesByTypes[index]!
|
|
198
|
-
const extraData = extraResourcesByType[index]!
|
|
199
|
-
return {
|
|
200
|
-
type: type as ResourceType,
|
|
201
|
-
total: rawData.data.value?.total ?? 0,
|
|
202
|
-
items: [...(rawData.data.value?.data ?? []), ...extraData.value],
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
.filter(group => group.items.length > 0)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
const flatResources = computed(() =>
|
|
209
|
-
allResources.value.flatMap(g => g.items),
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
// Fetch resource by ID if specified in URL (for SSR)
|
|
213
|
-
const initialResourceId = resourceIdQuery.value
|
|
214
|
-
const { data: fetchedResource } = initialResourceId
|
|
215
|
-
? await useFetch<Resource>(`/api/1/datasets/${props.dataset.id}/resources/${initialResourceId}/`)
|
|
216
|
-
: { data: ref(null) }
|
|
217
|
-
|
|
218
|
-
// Initial selection (synchronous for SSR hydration)
|
|
219
|
-
function getInitialResource(): Resource | null {
|
|
220
|
-
const resourceId = resourceIdQuery.value
|
|
221
|
-
if (resourceId) {
|
|
222
|
-
// First check in already loaded resources
|
|
223
|
-
const existing = flatResources.value.find(r => r.id === resourceId)
|
|
224
|
-
if (existing) return existing
|
|
225
|
-
// Use fetched resource if available
|
|
226
|
-
if (fetchedResource.value) return fetchedResource.value
|
|
227
|
-
}
|
|
228
|
-
// Default to first resource
|
|
229
|
-
return flatResources.value[0] ?? null
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const selectedResource = ref<Resource | null>(getInitialResource())
|
|
233
|
-
|
|
234
|
-
function updateSearch(newSearch: string) {
|
|
235
|
-
search.value = newSearch
|
|
236
|
-
flush()
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const selectResource = (resource: Resource) => {
|
|
240
|
-
selectedResource.value = resource
|
|
241
|
-
router.replace({
|
|
242
|
-
query: { ...router.currentRoute.value.query, resource_id: resource.id },
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
145
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (selectedResource.value) return
|
|
249
|
-
const firstResource = flatResources.value[0]
|
|
250
|
-
if (firstResource) {
|
|
251
|
-
selectedResource.value = firstResource
|
|
252
|
-
}
|
|
146
|
+
const resourceTo = (resource: Resource): RouteLocationRaw => ({
|
|
147
|
+
query: { ...route.query, resource_id: resource.id },
|
|
253
148
|
})
|
|
254
149
|
</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>
|
|
@@ -1,116 +1,149 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<aside
|
|
3
|
-
|
|
4
|
-
class="
|
|
3
|
+
class="relative flex shrink-0 flex-col overflow-hidden border-r border-gray-default bg-white"
|
|
4
|
+
:class="resizing ? '' : 'transition-[width] duration-200'"
|
|
5
|
+
:style="{ width: collapsed ? '48px' : `${width}px` }"
|
|
6
|
+
:aria-label="t('Ressources')"
|
|
5
7
|
>
|
|
6
|
-
<div
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
<div
|
|
9
|
+
class="flex h-14 items-center border-b border-gray-default bg-gray-some px-3"
|
|
10
|
+
:class="collapsed ? 'justify-center' : 'justify-between'"
|
|
11
|
+
>
|
|
12
|
+
<span
|
|
13
|
+
v-if="!collapsed"
|
|
14
|
+
class="text-[13px] font-medium text-gray-title"
|
|
15
|
+
>{{ t('Ressources') }}</span>
|
|
10
16
|
<button
|
|
11
17
|
type="button"
|
|
12
|
-
:title="t('Masquer le panneau')"
|
|
13
|
-
class="
|
|
14
|
-
@click="$emit('update:collapsed',
|
|
18
|
+
:title="collapsed ? t('Afficher le panneau des ressources') : t('Masquer le panneau')"
|
|
19
|
+
class="flex size-6 items-center justify-center rounded text-gray-plain hover:bg-gray-100"
|
|
20
|
+
@click="$emit('update:collapsed', !collapsed)"
|
|
15
21
|
>
|
|
16
|
-
<
|
|
22
|
+
<component
|
|
23
|
+
:is="collapsed ? RiSidebarUnfoldLine : RiSidebarFoldLine"
|
|
24
|
+
class="size-5"
|
|
25
|
+
/>
|
|
17
26
|
</button>
|
|
18
27
|
</div>
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
<!-- min-w keeps the content laid out at the sidebar's min width so it doesn't
|
|
30
|
+
reflow (badges wrapping, section titles on 2 lines) while the width animates
|
|
31
|
+
open from the collapsed state — the aside's overflow-hidden clips the rest. -->
|
|
32
|
+
<!-- Extra bottom padding so the last item can scroll clear of the browser's
|
|
33
|
+
hover URL bar (Firefox shows it bottom-left). -->
|
|
34
|
+
<div
|
|
35
|
+
v-if="!collapsed"
|
|
36
|
+
class="flex min-h-0 min-w-[260px] flex-1 flex-col gap-3 overflow-y-auto p-2 pb-16"
|
|
37
|
+
>
|
|
38
|
+
<label class="flex h-8 items-center gap-1 rounded border border-gray-default bg-gray-some px-2">
|
|
39
|
+
<RiSearchLine class="size-3.5 shrink-0 text-gray-medium" />
|
|
40
|
+
<span class="sr-only">{{ t('Rechercher') }}</span>
|
|
41
|
+
<input
|
|
42
|
+
:value="search"
|
|
43
|
+
type="search"
|
|
44
|
+
class="min-w-0 flex-1 bg-transparent text-[13px] outline-none placeholder:text-gray-medium"
|
|
45
|
+
:placeholder="t('Rechercher une ressource')"
|
|
46
|
+
@input="$emit('update:search', ($event.target as HTMLInputElement).value)"
|
|
47
|
+
>
|
|
48
|
+
</label>
|
|
34
49
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
v-for="group in resources"
|
|
50
|
+
<section
|
|
51
|
+
v-for="group in groups"
|
|
38
52
|
:key="group.type"
|
|
53
|
+
class="space-y-0.5"
|
|
39
54
|
>
|
|
40
|
-
<
|
|
55
|
+
<p class="px-1 py-2 text-[12px] font-medium leading-3 text-gray-medium">
|
|
41
56
|
{{ getResourceLabel(group.type, group.total) }}
|
|
42
|
-
</
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
:class="{
|
|
52
|
-
'font-bold bg-blue-50': resource.id === selectedResourceId,
|
|
53
|
-
}"
|
|
54
|
-
@click="$emit('select', resource)"
|
|
55
|
-
>
|
|
56
|
-
<ResourceIcon
|
|
57
|
-
:resource
|
|
58
|
-
class="size-3.5 shrink-0"
|
|
59
|
-
/>
|
|
60
|
-
<span class="truncate">{{ resource.title || t('Fichier sans nom') }}</span>
|
|
61
|
-
</button>
|
|
62
|
-
</li>
|
|
63
|
-
</ul>
|
|
57
|
+
</p>
|
|
58
|
+
<ResourceListItem
|
|
59
|
+
v-for="resource in group.items"
|
|
60
|
+
:key="resource.id"
|
|
61
|
+
:resource
|
|
62
|
+
:to="resourceTo(resource)"
|
|
63
|
+
:replace
|
|
64
|
+
:selected="resource.id === selectedResourceId"
|
|
65
|
+
/>
|
|
64
66
|
<button
|
|
65
67
|
v-if="group.items.length < group.total"
|
|
66
68
|
type="button"
|
|
67
|
-
|
|
69
|
+
:disabled="loadingType === group.type"
|
|
70
|
+
class="flex w-full items-center gap-1 px-1 py-1 text-left text-[13px] text-blue-default hover:underline disabled:cursor-default disabled:no-underline"
|
|
68
71
|
@click="$emit('load-more', group.type)"
|
|
69
72
|
>
|
|
73
|
+
<RiLoader5Line
|
|
74
|
+
v-if="loadingType === group.type"
|
|
75
|
+
class="size-3.5 shrink-0 animate-spin"
|
|
76
|
+
/>
|
|
70
77
|
{{ t('Charger plus…') }}
|
|
71
78
|
</button>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<!-- The search lives here, so its outcome does too: the viewer only says that
|
|
82
|
+
nothing is selected. -->
|
|
83
|
+
<div
|
|
84
|
+
v-if="search && !groups.length"
|
|
85
|
+
class="flex flex-col gap-2 px-1 py-2"
|
|
86
|
+
>
|
|
87
|
+
<p class="m-0 text-[13px] leading-snug text-gray-medium">
|
|
88
|
+
{{ t('Pas de résultats pour « {q} »', { q: search }) }}
|
|
89
|
+
</p>
|
|
90
|
+
<BrandedButton
|
|
91
|
+
color="secondary"
|
|
92
|
+
size="xs"
|
|
93
|
+
class="w-full"
|
|
94
|
+
@click="$emit('update:search', '')"
|
|
95
|
+
>
|
|
96
|
+
{{ t('Réinitialiser la recherche') }}
|
|
97
|
+
</BrandedButton>
|
|
72
98
|
</div>
|
|
73
99
|
</div>
|
|
74
|
-
</aside>
|
|
75
100
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
</
|
|
101
|
+
<!-- Drag the right edge to resize the panel and read longer resource titles.
|
|
102
|
+
A separator rather than a button: it only answers to a drag, and a focusable
|
|
103
|
+
button would announce an action that no key can trigger. Keyboard users fold
|
|
104
|
+
the panel from the header instead. -->
|
|
105
|
+
<div
|
|
106
|
+
v-if="!collapsed"
|
|
107
|
+
role="separator"
|
|
108
|
+
aria-orientation="vertical"
|
|
109
|
+
:title="t('Glisser pour redimensionner')"
|
|
110
|
+
class="absolute right-0 top-0 z-20 h-full w-2 cursor-col-resize touch-none hover:bg-new-primary/10"
|
|
111
|
+
@mousedown="startResize"
|
|
112
|
+
/>
|
|
113
|
+
</aside>
|
|
89
114
|
</template>
|
|
90
115
|
|
|
91
116
|
<script setup lang="ts">
|
|
92
|
-
import {
|
|
93
|
-
import {
|
|
94
|
-
import
|
|
117
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
118
|
+
import { RiSidebarFoldLine, RiSidebarUnfoldLine, RiSearchLine, RiLoader5Line } from '@remixicon/vue'
|
|
119
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
120
|
+
import ResourceListItem from '../ResourceListItem.vue'
|
|
95
121
|
import { getResourceLabel } from '../../functions/resources'
|
|
96
122
|
import { useTranslation } from '../../composables/useTranslation'
|
|
123
|
+
import { useResizable } from '../../composables/useResizable'
|
|
97
124
|
import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
|
|
98
125
|
|
|
99
126
|
const { t } = useTranslation()
|
|
100
127
|
|
|
101
128
|
defineProps<{
|
|
102
|
-
|
|
129
|
+
groups: ResourceGroup[]
|
|
103
130
|
selectedResourceId: string | null
|
|
104
131
|
collapsed: boolean
|
|
105
132
|
search: string
|
|
133
|
+
loadingType: ResourceType | null
|
|
134
|
+
resourceTo: (resource: Resource) => RouteLocationRaw
|
|
135
|
+
replace?: boolean
|
|
106
136
|
}>()
|
|
107
137
|
|
|
108
138
|
defineEmits<{
|
|
109
|
-
'select': [resource: Resource]
|
|
110
139
|
'load-more': [type: ResourceType]
|
|
111
140
|
'update:collapsed': [value: boolean]
|
|
112
141
|
'update:search': [value: string]
|
|
113
142
|
}>()
|
|
114
143
|
|
|
115
|
-
const
|
|
144
|
+
const { width, resizing, startResize } = useResizable({
|
|
145
|
+
initialWidth: 300,
|
|
146
|
+
minWidth: 260,
|
|
147
|
+
maxWidth: 720,
|
|
148
|
+
})
|
|
116
149
|
</script>
|