@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.
- package/README.md +1 -1
- package/assets/main.css +56 -1
- package/dist/Control-BNCDn-8E.js +148 -0
- package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-Dls5AHTE.js} +1 -1
- package/dist/Event-BOgJUhNR.js +738 -0
- package/dist/Image-BN-4XkIn.js +247 -0
- package/dist/{JsonPreview.client-BMsC5JcY.js → JsonPreview.client-DPDTs433.js} +14 -14
- package/dist/Map-BdT3i2C4.js +7609 -0
- package/dist/MapContainer.client-BdAzd7bj.js +105 -0
- package/dist/OSM-CamriM9b.js +71 -0
- package/dist/{PdfPreview.client-COOkEkRA.js → PdfPreview.client-CopqSDyt.js} +3 -3
- package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-mF6xaOO_.js} +2 -2
- package/dist/ScaleLine-BiesrgOv.js +165 -0
- package/dist/Swagger.client-eJ7gpfZA.js +4 -0
- package/dist/Tile-DCuqwNOI.js +1206 -0
- package/dist/TileImage-CmZf8EdU.js +1067 -0
- package/dist/View-DcDc7N2K.js +2858 -0
- package/dist/{XmlPreview.client-CAdN0w_Y.js → XmlPreview.client-C0OgBkSq.js} +7 -7
- package/dist/common-C4rDcQpp.js +243 -0
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +153 -117
- package/dist/components.css +1 -1
- package/dist/{MapContainer.client-DeSo8EvG.js → index-BRGqW8aQ.js} +4975 -21416
- package/dist/leaflet-src-7m1mB8LI.js +6338 -0
- package/dist/{main-Dgri3TQL.js → main-CNHxAJ8J.js} +56758 -51450
- package/dist/proj-CKwYjU38.js +1569 -0
- package/dist/tilecoord-YW3qEH_j.js +884 -0
- package/dist/{vue3-xml-viewer.common-D6skc_Ai.js → vue3-xml-viewer.common-CmAdQfIy.js} +1 -1
- package/package.json +5 -1
- package/src/components/ActivityList/ActivityList.vue +6 -2
- package/src/components/AppLink.vue +4 -1
- package/src/components/Avatar.vue +2 -2
- package/src/components/AvatarWithName.vue +8 -4
- package/src/components/BouncingDots.vue +21 -0
- package/src/components/BrandedButton.vue +2 -0
- package/src/components/CopyButton.vue +19 -7
- package/src/components/DataserviceCard.vue +83 -118
- package/src/components/DatasetCard.vue +110 -171
- package/src/components/DatasetInformation/DatasetEmbedSection.vue +43 -0
- package/src/components/DatasetInformation/DatasetInformationSection.vue +73 -0
- package/src/components/DatasetInformation/DatasetSchemaSection.vue +74 -0
- package/src/components/DatasetInformation/DatasetSpatialSection.vue +59 -0
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +45 -0
- package/src/components/DatasetInformation/index.ts +5 -0
- package/src/components/DatasetQualityTooltipContent.vue +3 -3
- package/src/components/DescriptionList.vue +1 -4
- package/src/components/DescriptionListDetails.vue +5 -0
- package/src/components/DescriptionListTerm.vue +5 -0
- package/src/components/DiscussionMessageCard.vue +63 -0
- package/src/components/ExtraAccordion.vue +4 -4
- package/src/components/Form/BadgeSelect.vue +35 -0
- package/src/components/Form/FormatSelect.vue +28 -0
- package/src/components/Form/GeozoneSelect.vue +52 -0
- package/src/components/Form/GranularitySelect.vue +29 -0
- package/src/components/Form/LicenseSelect.vue +30 -0
- package/src/components/Form/OrganizationSelect.vue +62 -0
- package/src/components/Form/OrganizationTypeSelect.vue +34 -0
- package/src/components/Form/ReuseTopicSelect.vue +29 -0
- package/src/components/Form/SchemaSelect.vue +30 -0
- package/src/components/Form/SearchableSelect.vue +334 -0
- package/src/components/Form/SelectGroup.vue +132 -0
- package/src/components/Form/TagSelect.vue +38 -0
- package/src/components/LeafletMap.vue +31 -0
- package/src/components/LicenseBadge.vue +24 -0
- package/src/components/LoadingBlock.vue +23 -2
- package/src/components/MarkdownViewer.vue +3 -1
- package/src/components/ObjectCard.vue +42 -0
- package/src/components/ObjectCardBadge.vue +22 -0
- package/src/components/ObjectCardHeader.vue +35 -0
- package/src/components/ObjectCardOwner.vue +43 -0
- package/src/components/ObjectCardShortDescription.vue +28 -0
- package/src/components/OrganizationCard.vue +35 -20
- package/src/components/OrganizationLogo.vue +1 -1
- package/src/components/OrganizationNameWithCertificate.vue +13 -7
- package/src/components/OwnerTypeIcon.vue +1 -0
- package/src/components/Pagination.vue +1 -1
- package/src/components/Placeholder.vue +5 -2
- package/src/components/PostCard.vue +62 -0
- package/src/components/RadioGroup.vue +32 -0
- package/src/components/RadioInput.vue +64 -0
- package/src/components/ResourceAccordion/EditButton.vue +2 -3
- package/src/components/ResourceAccordion/MapContainer.client.vue +20 -16
- package/src/components/ResourceAccordion/Metadata.vue +11 -24
- package/src/components/ResourceAccordion/Pmtiles.client.vue +1 -1
- package/src/components/ResourceAccordion/Preview.vue +1 -1
- package/src/components/ResourceAccordion/ResourceAccordion.vue +30 -20
- package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
- package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
- package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +361 -0
- package/src/components/ReuseCard.vue +8 -28
- package/src/components/ReuseHorizontalCard.vue +80 -0
- package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
- package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
- package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
- package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
- package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
- package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
- package/src/components/Search/Filter/ProducerTypeFilter.vue +39 -0
- package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
- package/src/components/Search/GlobalSearch.vue +611 -0
- package/src/components/Search/SearchInput.vue +63 -0
- package/src/components/Search/Sidemenu.vue +38 -0
- package/src/components/StatBox.vue +5 -5
- package/src/components/Tag.vue +30 -0
- package/src/components/Toggletip.vue +6 -2
- package/src/components/Tooltip.vue +2 -3
- package/src/components/TopicCard.vue +134 -0
- package/src/components/radioGroupContext.ts +9 -0
- package/src/composables/useDebouncedRef.ts +31 -0
- package/src/composables/useMetrics.ts +4 -3
- package/src/composables/useResourceCapabilities.ts +118 -0
- package/src/composables/useRouteQueryBoolean.ts +10 -0
- package/src/composables/useSelectModelSync.ts +89 -0
- package/src/composables/useStableQueryParams.ts +84 -0
- package/src/config.ts +4 -0
- package/src/functions/api.ts +17 -6
- package/src/functions/api.types.ts +4 -2
- package/src/functions/datasets.ts +1 -29
- package/src/functions/description.ts +33 -0
- package/src/functions/helpers.ts +11 -0
- package/src/functions/markdown.ts +60 -16
- package/src/functions/metrics.ts +33 -0
- package/src/functions/organizations.ts +5 -5
- package/src/main.ts +89 -7
- package/src/types/dataservices.ts +14 -12
- package/src/types/datasets.ts +20 -7
- package/src/types/discussions.ts +20 -0
- package/src/types/licenses.ts +3 -3
- package/src/types/organizations.ts +13 -1
- package/src/types/owned.ts +4 -2
- package/src/types/pages.ts +70 -0
- package/src/types/posts.ts +27 -0
- package/src/types/resources.ts +6 -0
- package/src/types/reuses.ts +14 -5
- package/src/types/search.ts +379 -0
- package/src/types/users.ts +12 -3
- package/dist/Swagger.client-CpLgaLg6.js +0 -4
- package/src/components/DatasetInformationPanel.vue +0 -211
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
>
|
|
10
10
|
<div>
|
|
11
11
|
<div class="flex items-center fr-mb-1v">
|
|
12
|
-
<
|
|
12
|
+
<h3
|
|
13
13
|
:id="resourceTitleId"
|
|
14
14
|
class="fr-m-0"
|
|
15
15
|
>
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
/></span>
|
|
44
44
|
<span class="absolute inset-0 z-1" />
|
|
45
45
|
</button>
|
|
46
|
-
</
|
|
46
|
+
</h3>
|
|
47
47
|
<CopyButton
|
|
48
48
|
:label="t('Copier le lien')"
|
|
49
49
|
:copied-label="t('Lien copié !')"
|
|
@@ -51,22 +51,33 @@
|
|
|
51
51
|
class="z-2"
|
|
52
52
|
/>
|
|
53
53
|
</div>
|
|
54
|
-
<div class="text-gray-medium subheaders-infos">
|
|
54
|
+
<div class="text-gray-medium subheaders-infos flex items-center gap-1">
|
|
55
55
|
<SchemaBadge
|
|
56
56
|
:resource
|
|
57
|
-
class="dash-after"
|
|
58
57
|
/>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class="
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
<RiSubtractLine
|
|
59
|
+
v-if="resource.schema"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
class="size-3 fill-gray-medium"
|
|
62
|
+
/>
|
|
63
|
+
<span class="text-xs mb-0">{{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(lastUpdate) }) }}</span>
|
|
64
|
+
<RiSubtractLine
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
class="size-3 fill-gray-medium"
|
|
67
|
+
/>
|
|
68
|
+
<template v-if="resource.format">
|
|
69
|
+
<span class="text-xs mb-0">
|
|
70
|
+
<span class="hidden sm:inline">{{ t("Format") }}</span>
|
|
71
|
+
{{ resource.format.trim().toLowerCase() }}
|
|
72
|
+
<span v-if="resourceFilesize">({{ filesize(resourceFilesize) }})</span>
|
|
73
|
+
</span>
|
|
74
|
+
<RiSubtractLine
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
class="size-3 fill-gray-medium"
|
|
77
|
+
/>
|
|
78
|
+
</template>
|
|
68
79
|
<span
|
|
69
|
-
class="inline-flex items-center
|
|
80
|
+
class="inline-flex items-center text-xs mb-0"
|
|
70
81
|
:aria-label="t('{n} téléchargements', resource.metrics.views)"
|
|
71
82
|
>
|
|
72
83
|
<span class="fr-icon-download-line fr-icon--xs fr-mr-1v" />
|
|
@@ -132,14 +143,15 @@
|
|
|
132
143
|
rel="ugc nofollow noopener"
|
|
133
144
|
:title="downloadButtonTitle"
|
|
134
145
|
download
|
|
135
|
-
class="relative
|
|
146
|
+
class="relative matomo_download z-2"
|
|
136
147
|
:icon="unavailable ? RiFileWarningLine : RiDownloadLine"
|
|
137
148
|
size="xs"
|
|
149
|
+
color="secondary"
|
|
138
150
|
:aria-describedby="resourceTitleId"
|
|
139
151
|
external
|
|
140
152
|
@click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
|
|
141
153
|
>
|
|
142
|
-
|
|
154
|
+
{{ t('Télécharger') }}
|
|
143
155
|
</BrandedButton>
|
|
144
156
|
</p>
|
|
145
157
|
<p
|
|
@@ -358,7 +370,7 @@
|
|
|
358
370
|
|
|
359
371
|
<script setup lang="ts">
|
|
360
372
|
import { ref, computed, defineAsyncComponent } from 'vue'
|
|
361
|
-
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine } from '@remixicon/vue'
|
|
373
|
+
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
|
|
362
374
|
import OrganizationNameWithCertificate from '../OrganizationNameWithCertificate.vue'
|
|
363
375
|
import { filesize, summarize } from '../../functions/helpers'
|
|
364
376
|
import { useFormatDate } from '../../functions/dates'
|
|
@@ -595,9 +607,7 @@ article {
|
|
|
595
607
|
article header .subheaders-infos .hidden.show-on-small {
|
|
596
608
|
display: inline !important;
|
|
597
609
|
}
|
|
598
|
-
|
|
599
|
-
content: ''
|
|
600
|
-
} */
|
|
610
|
+
*/
|
|
601
611
|
|
|
602
612
|
/* article .fr-pl-4v fr-pr-4v {
|
|
603
613
|
padding: 0.75rem !important;
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
>
|
|
10
10
|
<RiInformationLine class="size-4" />
|
|
11
11
|
<template #toggletip="{ close }">
|
|
12
|
-
<div class="flex justify-between border-
|
|
12
|
+
<div class="flex justify-between border-b">
|
|
13
13
|
<h5 class="fr-text--sm fr-my-0 fr-p-2v">{{ t("Schéma de données") }}</h5>
|
|
14
14
|
<button
|
|
15
15
|
type="button"
|
|
16
16
|
:title="t('Fermer')"
|
|
17
|
-
class="border-
|
|
17
|
+
class="border-l close-button flex items-center justify-center"
|
|
18
18
|
@click="close"
|
|
19
19
|
>×</button>
|
|
20
20
|
</div>
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="allResources.length || hasAnyResources">
|
|
3
|
+
<div class="flex gap-6">
|
|
4
|
+
<div class="flex-1 min-w-0">
|
|
5
|
+
<ResourceExplorerViewer
|
|
6
|
+
v-if="selectedResource && allResources.length"
|
|
7
|
+
:key="selectedResource.id"
|
|
8
|
+
:dataset
|
|
9
|
+
:resource="selectedResource"
|
|
10
|
+
/>
|
|
11
|
+
<div
|
|
12
|
+
v-else-if="search"
|
|
13
|
+
class="flex flex-col items-center py-12"
|
|
14
|
+
>
|
|
15
|
+
<slot name="no-results-image">
|
|
16
|
+
<img
|
|
17
|
+
:src="noResultsImage"
|
|
18
|
+
class="h-20"
|
|
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>
|
|
31
|
+
</div>
|
|
32
|
+
</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
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div
|
|
46
|
+
v-else
|
|
47
|
+
class="flex flex-col items-center py-12"
|
|
48
|
+
>
|
|
49
|
+
<slot name="empty-image">
|
|
50
|
+
<img
|
|
51
|
+
:src="noResultsImage"
|
|
52
|
+
class="h-20"
|
|
53
|
+
alt=""
|
|
54
|
+
>
|
|
55
|
+
</slot>
|
|
56
|
+
<p class="fr-text--bold fr-my-3v">
|
|
57
|
+
{{ t('Ce jeu de données ne contient aucune ressource.') }}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { computed, ref, watch, type Ref } from 'vue'
|
|
64
|
+
import { useRouter } from 'vue-router'
|
|
65
|
+
import { useRouteQuery } from '@vueuse/router'
|
|
66
|
+
import { useComponentsConfig } from '../../config'
|
|
67
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
68
|
+
import { useDebouncedRef } from '../../composables/useDebouncedRef'
|
|
69
|
+
import { useFetch } from '../../functions/api'
|
|
70
|
+
import { RESOURCE_TYPE } from '../../functions/resources'
|
|
71
|
+
import type { PaginatedArray } from '../../types/api'
|
|
72
|
+
import type { DatasetV2 } from '../../types/datasets'
|
|
73
|
+
import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
|
|
74
|
+
import ResourceExplorerSidebar from './ResourceExplorerSidebar.vue'
|
|
75
|
+
import ResourceExplorerViewer from './ResourceExplorerViewer.vue'
|
|
76
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
77
|
+
|
|
78
|
+
const props = withDefaults(defineProps<{
|
|
79
|
+
dataset: DatasetV2
|
|
80
|
+
noResultsImage?: string
|
|
81
|
+
}>(), {
|
|
82
|
+
noResultsImage: '',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const { t } = useTranslation()
|
|
86
|
+
const router = useRouter()
|
|
87
|
+
const config = useComponentsConfig()
|
|
88
|
+
|
|
89
|
+
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
|
+
const loadMore = async (type: ResourceType) => {
|
|
163
|
+
const index = RESOURCE_TYPE.indexOf(type)
|
|
164
|
+
if (index === -1) return
|
|
165
|
+
const pageRef = pageByType[index]!
|
|
166
|
+
const extraRef = extraResourcesByType[index]!
|
|
167
|
+
pageRef.value++
|
|
168
|
+
|
|
169
|
+
const { data } = await useFetch<PaginatedArray<Resource>>(url, {
|
|
170
|
+
params: {
|
|
171
|
+
type,
|
|
172
|
+
page_size: PAGE_SIZE,
|
|
173
|
+
page: pageRef.value,
|
|
174
|
+
q: searchDebounced.value || undefined,
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if (data.value) {
|
|
179
|
+
extraRef.value = [...extraRef.value, ...data.value.data]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const allResources = computed<ResourceGroup[]>(() => {
|
|
184
|
+
return RESOURCE_TYPE
|
|
185
|
+
.map((type, index) => {
|
|
186
|
+
const rawData = rawResourcesByTypes[index]!
|
|
187
|
+
const extraData = extraResourcesByType[index]!
|
|
188
|
+
return {
|
|
189
|
+
type: type as ResourceType,
|
|
190
|
+
total: rawData.data.value?.total ?? 0,
|
|
191
|
+
items: [...(rawData.data.value?.data ?? []), ...extraData.value],
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
.filter(group => group.items.length > 0)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const flatResources = computed(() =>
|
|
198
|
+
allResources.value.flatMap(g => g.items),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// Fetch resource by ID if specified in URL (for SSR)
|
|
202
|
+
const initialResourceId = resourceIdQuery.value
|
|
203
|
+
const { data: fetchedResource } = initialResourceId
|
|
204
|
+
? await useFetch<Resource>(`/api/1/datasets/${props.dataset.id}/resources/${initialResourceId}/`)
|
|
205
|
+
: { data: ref(null) }
|
|
206
|
+
|
|
207
|
+
// Initial selection (synchronous for SSR hydration)
|
|
208
|
+
function getInitialResource(): Resource | null {
|
|
209
|
+
const resourceId = resourceIdQuery.value
|
|
210
|
+
if (resourceId) {
|
|
211
|
+
// First check in already loaded resources
|
|
212
|
+
const existing = flatResources.value.find(r => r.id === resourceId)
|
|
213
|
+
if (existing) return existing
|
|
214
|
+
// Use fetched resource if available
|
|
215
|
+
if (fetchedResource.value) return fetchedResource.value
|
|
216
|
+
}
|
|
217
|
+
// Default to first resource
|
|
218
|
+
return flatResources.value[0] ?? null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const selectedResource = ref<Resource | null>(getInitialResource())
|
|
222
|
+
|
|
223
|
+
function updateSearch(newSearch: string) {
|
|
224
|
+
search.value = newSearch
|
|
225
|
+
flush()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const selectResource = (resource: Resource) => {
|
|
229
|
+
selectedResource.value = resource
|
|
230
|
+
router.replace({
|
|
231
|
+
query: { ...router.currentRoute.value.query, resource_id: resource.id },
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Update selection when resources change (e.g., after client-side fetch completes)
|
|
236
|
+
watch(flatResources, () => {
|
|
237
|
+
if (selectedResource.value) return
|
|
238
|
+
const firstResource = flatResources.value[0]
|
|
239
|
+
if (firstResource) {
|
|
240
|
+
selectedResource.value = firstResource
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
</script>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside
|
|
3
|
+
v-if="!collapsed"
|
|
4
|
+
class="w-72 shrink-0 pl-4"
|
|
5
|
+
>
|
|
6
|
+
<div class="flex items-center justify-between mb-3">
|
|
7
|
+
<h3 class="text-sm font-bold uppercase mb-0">
|
|
8
|
+
{{ t('Ressources') }}
|
|
9
|
+
</h3>
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
:title="t('Masquer le panneau')"
|
|
13
|
+
class="p-1 hover:bg-gray-100 rounded"
|
|
14
|
+
@click="$emit('update:collapsed', true)"
|
|
15
|
+
>
|
|
16
|
+
<RiArrowRightSLine class="size-5" />
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="mb-3">
|
|
21
|
+
<label
|
|
22
|
+
:for="searchId"
|
|
23
|
+
class="sr-only"
|
|
24
|
+
>{{ t('Rechercher') }}</label>
|
|
25
|
+
<input
|
|
26
|
+
:id="searchId"
|
|
27
|
+
:value="search"
|
|
28
|
+
type="search"
|
|
29
|
+
class="w-full border border-gray-default rounded px-2.5 py-1.5 text-sm"
|
|
30
|
+
:placeholder="t('Rechercher')"
|
|
31
|
+
@input="$emit('update:search', ($event.target as HTMLInputElement).value)"
|
|
32
|
+
>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="space-y-4 overflow-y-auto">
|
|
36
|
+
<div
|
|
37
|
+
v-for="group in resources"
|
|
38
|
+
:key="group.type"
|
|
39
|
+
>
|
|
40
|
+
<div class="text-xs text-gray-plain font-bold uppercase mb-1.5 border-b border-gray-default pb-1">
|
|
41
|
+
{{ getResourceLabel(group.type, group.total) }}
|
|
42
|
+
</div>
|
|
43
|
+
<ul class="list-none p-0 m-0 space-y-0.5">
|
|
44
|
+
<li
|
|
45
|
+
v-for="resource in group.items"
|
|
46
|
+
:key="resource.id"
|
|
47
|
+
>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="w-full text-left px-2 py-1.5 rounded text-sm flex items-center gap-1.5 hover:bg-gray-100"
|
|
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>
|
|
64
|
+
<button
|
|
65
|
+
v-if="group.items.length < group.total"
|
|
66
|
+
type="button"
|
|
67
|
+
class="w-full text-left px-2 py-1.5 text-sm text-blue-default hover:underline"
|
|
68
|
+
@click="$emit('load-more', group.type)"
|
|
69
|
+
>
|
|
70
|
+
{{ t('Charger plus…') }}
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</aside>
|
|
75
|
+
|
|
76
|
+
<div
|
|
77
|
+
v-else
|
|
78
|
+
class="shrink-0 flex items-start pt-1"
|
|
79
|
+
>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
:title="t('Afficher le panneau des ressources')"
|
|
83
|
+
class="p-1 hover:bg-gray-100 rounded border border-gray-default"
|
|
84
|
+
@click="$emit('update:collapsed', false)"
|
|
85
|
+
>
|
|
86
|
+
<RiArrowLeftSLine class="size-5" />
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { useId } from 'vue'
|
|
93
|
+
import { RiArrowRightSLine, RiArrowLeftSLine } from '@remixicon/vue'
|
|
94
|
+
import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
|
|
95
|
+
import { getResourceLabel } from '../../functions/resources'
|
|
96
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
97
|
+
import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
|
|
98
|
+
|
|
99
|
+
const { t } = useTranslation()
|
|
100
|
+
|
|
101
|
+
defineProps<{
|
|
102
|
+
resources: ResourceGroup[]
|
|
103
|
+
selectedResourceId: string | null
|
|
104
|
+
collapsed: boolean
|
|
105
|
+
search: string
|
|
106
|
+
}>()
|
|
107
|
+
|
|
108
|
+
defineEmits<{
|
|
109
|
+
'select': [resource: Resource]
|
|
110
|
+
'load-more': [type: ResourceType]
|
|
111
|
+
'update:collapsed': [value: boolean]
|
|
112
|
+
'update:search': [value: string]
|
|
113
|
+
}>()
|
|
114
|
+
|
|
115
|
+
const searchId = useId()
|
|
116
|
+
</script>
|