@datagouv/components-next 1.0.2-dev.230 → 1.0.2-dev.232
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/{ImagePreview.client-Db0MdI5N.js → ImagePreview.client-CSRz7eUF.js} +2 -2
- package/dist/{JsonPreview.client-DRCq40Q-.js → JsonPreview.client-wT2hfYLv.js} +2 -2
- package/dist/{Map-D17BKcyK.js → Map-QeL96qbQ.js} +1 -1
- package/dist/{MapContainer.client-DdPY_Iri.js → MapContainer.client-UC7wQOBj.js} +10 -10
- package/dist/{PdfPreview.client-DtKCBgGZ.js → PdfPreview.client-DifxM8Th.js} +2 -2
- package/dist/{Pmtiles.client-DSZ_4FXc.js → Pmtiles.client-OyXiX_7g.js} +2104 -2905
- package/dist/{PreviewWrapper-DegSQbMM.js → PreviewWrapper-D7yfQyyH.js} +1 -1
- package/dist/{XmlPreview.client-D9R_YF5s.js → XmlPreview.client-BkPGZLn6.js} +2 -2
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +10149 -9989
- package/dist/components.css +1 -1
- package/dist/{france-Bu5x7PSp.js → france-BNatwStf.js} +1 -1
- package/dist/purify.es-CeeOHdER.js +805 -0
- package/dist/{resources-BOKIyTJV.js → resources-kuI_FosB.js} +18 -21
- package/dist/src-C0XyM3eg.js +68979 -0
- package/dist/{vue-D62CcCib.js → vue-D5eMxsCr.js} +78 -74
- package/package.json +2 -2
- package/src/components/ResourceAccordion/EditButton.vue +1 -4
- package/src/components/ResourceAccordion/MapContainer.client.vue +18 -7
- package/src/components/ResourceAccordion/ResourceAccordion.vue +1 -6
- package/src/components/ResourceExplorer/ResourceExplorer.vue +8 -2
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +140 -90
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +17 -2
- package/src/components/ResourceExplorer/ResourceSelector.vue +3 -0
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +3 -5
- package/src/components/ResourceIconBadge.vue +28 -0
- package/src/components/ResourceListItem.vue +83 -27
- package/src/components/TabularExplorer/TabularActiveFilters.vue +94 -54
- package/src/components/TabularExplorer/TabularExplorer.vue +58 -18
- package/src/components/TabularExplorer/TabularSearchInput.vue +40 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +14 -6
- package/src/components/TabularExplorer/TabularTable.vue +12 -5
- package/src/components/TabularExplorer/TabularToolbar.vue +7 -4
- package/src/components/TabularExplorer/useTabularContext.ts +10 -0
- package/src/composables/useResourceCapabilities.ts +5 -0
- package/src/functions/resources.ts +0 -4
- package/src/main.ts +2 -0
- package/src/types/resources.ts +0 -6
- package/dist/src-DbFybTVs.js +0 -66090
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
icon-only
|
|
5
5
|
:icon="RiPencilLine"
|
|
6
6
|
color="warning"
|
|
7
|
-
:
|
|
8
|
-
:title="disabledReason ?? t('Éditer le fichier')"
|
|
7
|
+
:title="t('Éditer le fichier')"
|
|
9
8
|
data-testid="edit-button"
|
|
10
9
|
/>
|
|
11
10
|
</template>
|
|
@@ -20,12 +19,10 @@ type Props = {
|
|
|
20
19
|
datasetId: string
|
|
21
20
|
isCommunityResource?: boolean
|
|
22
21
|
resourceId: string
|
|
23
|
-
disabledReason?: string | null
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
27
25
|
isCommunityResource: false,
|
|
28
|
-
disabledReason: null,
|
|
29
26
|
})
|
|
30
27
|
|
|
31
28
|
const { t } = useTranslation()
|
|
@@ -15,6 +15,15 @@ import PreviewUnavailable from './PreviewUnavailable.vue'
|
|
|
15
15
|
import type { Resource } from '../../types/resources'
|
|
16
16
|
import { useTranslation } from '../../composables/useTranslation'
|
|
17
17
|
|
|
18
|
+
type WmsCapabilitiesLayer = { Name: string }
|
|
19
|
+
|
|
20
|
+
type LayerImportInternals = {
|
|
21
|
+
_serviceUrlImportInput: HTMLInputElement
|
|
22
|
+
_formContainer: HTMLFormElement
|
|
23
|
+
_getCapResponseWMSLayers: Array<WmsCapabilitiesLayer> | null
|
|
24
|
+
_addGetCapWMSLayer: (layerInfo: WmsCapabilitiesLayer | undefined) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
const props = defineProps<{ resource: Resource }>()
|
|
19
28
|
|
|
20
29
|
const { t } = useTranslation()
|
|
@@ -38,7 +47,6 @@ async function displayMap() {
|
|
|
38
47
|
import('ol/control/ScaleLine'),
|
|
39
48
|
import('ol/layer/Tile'),
|
|
40
49
|
import('ol/source/OSM'),
|
|
41
|
-
// @ts-expect-error no types provided
|
|
42
50
|
import('geopf-extensions-openlayers'),
|
|
43
51
|
])
|
|
44
52
|
|
|
@@ -94,12 +102,16 @@ async function displayMap() {
|
|
|
94
102
|
map.addControl(attributions)
|
|
95
103
|
|
|
96
104
|
const layerImport = new LayerImport({
|
|
105
|
+
// @ts-expect-error `position` is handled by the base control but missing from the published types
|
|
97
106
|
position: 'bottom-left',
|
|
98
107
|
listable: true,
|
|
99
108
|
layerTypes: ['WMS'],
|
|
100
109
|
})
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
// LayerImport has no public API to load a service URL: we fill and submit its form,
|
|
111
|
+
// then add the resource's layer once the GetCapabilities response is parsed.
|
|
112
|
+
const layerImportInternals = layerImport as unknown as LayerImportInternals
|
|
113
|
+
layerImportInternals._serviceUrlImportInput.value = props.resource.url
|
|
114
|
+
layerImportInternals._formContainer.dispatchEvent(new CustomEvent('submit', { cancelable: true }))
|
|
103
115
|
|
|
104
116
|
map.addControl(layerImport)
|
|
105
117
|
|
|
@@ -108,7 +120,7 @@ async function displayMap() {
|
|
|
108
120
|
const waitTimeout = 500
|
|
109
121
|
let retry = 20
|
|
110
122
|
function showLayer() {
|
|
111
|
-
if (!
|
|
123
|
+
if (!layerImportInternals._getCapResponseWMSLayers) {
|
|
112
124
|
retry--
|
|
113
125
|
if (retry > 0)
|
|
114
126
|
setTimeout(showLayer, waitTimeout)
|
|
@@ -116,9 +128,8 @@ async function displayMap() {
|
|
|
116
128
|
hasError.value = true
|
|
117
129
|
}
|
|
118
130
|
else {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
layerImport._addGetCapWMSLayer(layerInfo)
|
|
131
|
+
const layerInfo = layerImportInternals._getCapResponseWMSLayers.filter(layer => layer.Name == props.resource.title)[0]
|
|
132
|
+
layerImportInternals._addGetCapWMSLayer(layerInfo)
|
|
122
133
|
}
|
|
123
134
|
}
|
|
124
135
|
setTimeout(showLayer, waitTimeout)
|
|
@@ -173,7 +173,6 @@
|
|
|
173
173
|
:dataset-id="dataset.id"
|
|
174
174
|
:resource-id="resource.id"
|
|
175
175
|
:is-community-resource="isCommunityResource"
|
|
176
|
-
:disabled-reason="editDisabledReason"
|
|
177
176
|
size="xs"
|
|
178
177
|
/>
|
|
179
178
|
</p>
|
|
@@ -328,7 +327,7 @@ import { trackEvent } from '../../functions/matomo'
|
|
|
328
327
|
import CopyButton from '../CopyButton.vue'
|
|
329
328
|
import { useComponentsConfig } from '../../config'
|
|
330
329
|
import { getOwnerName } from '../../functions/owned'
|
|
331
|
-
import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceFilesize,
|
|
330
|
+
import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceFilesize, isImagePreviewFormat, resolveResourceExternalUrl } from '../../functions/resources'
|
|
332
331
|
import BrandedButton from '../BrandedButton.vue'
|
|
333
332
|
import { useTranslation } from '../../composables/useTranslation'
|
|
334
333
|
import { useHasTabularData } from '../../composables/useHasTabularData'
|
|
@@ -385,10 +384,6 @@ const hasPmtiles = computed(() => {
|
|
|
385
384
|
return props.resource.extras['analysis:parsing:pmtiles_url'] || props.resource.format === 'pmtiles'
|
|
386
385
|
})
|
|
387
386
|
|
|
388
|
-
const editDisabledReason = computed(() => {
|
|
389
|
-
return isGeopfSynced(props.resource) ? t('Vous ne pouvez pas modifier cette ressource car elle est synchronisée avec cartes.gouv.fr') : null
|
|
390
|
-
})
|
|
391
|
-
|
|
392
387
|
const hasDatafairPreview = computed(() => {
|
|
393
388
|
// Checks if there are the corresponding extras for a datafair preview.
|
|
394
389
|
// Limited only to datasets published by certified organizations since it will load an iframe.
|
|
@@ -14,8 +14,13 @@
|
|
|
14
14
|
class="flex"
|
|
15
15
|
:class="fullscreen ? 'min-h-0 flex-1 overflow-hidden' : 'overflow-hidden rounded border border-gray-default'"
|
|
16
16
|
>
|
|
17
|
-
<div
|
|
17
|
+
<div
|
|
18
|
+
class="hidden md:flex"
|
|
19
|
+
:class="resourceListExpanded ? 'flex-1' : ''"
|
|
20
|
+
>
|
|
18
21
|
<ResourceExplorerSidebar
|
|
22
|
+
v-model:expanded="resourceListExpanded"
|
|
23
|
+
:dataset
|
|
19
24
|
:groups
|
|
20
25
|
:selected-resource-id="selectedResource?.id ?? null"
|
|
21
26
|
:collapsed="sidebarCollapsed"
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
<div
|
|
32
37
|
id="resource-explorer-viewer"
|
|
33
38
|
class="flex-1 min-w-0"
|
|
34
|
-
:class="fullscreen ? 'flex flex-col' : ''"
|
|
39
|
+
:class="[fullscreen ? 'flex flex-col' : '', resourceListExpanded ? 'md:hidden' : '']"
|
|
35
40
|
role="region"
|
|
36
41
|
:aria-label="t('Détail de la ressource')"
|
|
37
42
|
>
|
|
@@ -154,6 +159,7 @@ const {
|
|
|
154
159
|
watch(selectedResource, resource => emit('select', resource), { immediate: true })
|
|
155
160
|
|
|
156
161
|
const sidebarCollapsed = ref(false)
|
|
162
|
+
const resourceListExpanded = ref(false)
|
|
157
163
|
|
|
158
164
|
const resourceTo = (resource: Resource): RouteLocationRaw => ({
|
|
159
165
|
query: { ...route.query, resource_id: resource.id },
|
|
@@ -1,134 +1,183 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
:
|
|
2
|
+
<!-- The resize grip sits outside the aside: it straddles the border, and the
|
|
3
|
+
aside's overflow-hidden would clip its outer half. -->
|
|
4
|
+
<div
|
|
5
|
+
class="relative flex"
|
|
6
|
+
:class="expanded ? 'flex-1' : 'shrink-0'"
|
|
7
7
|
>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
<!-- Expanded, the list takes the viewer's place (the explorer hides the viewer). -->
|
|
9
|
+
<aside
|
|
10
|
+
class="flex flex-col overflow-hidden bg-white"
|
|
11
|
+
:class="[
|
|
12
|
+
expanded ? 'flex-1' : 'shrink-0 border-r border-gray-default',
|
|
13
|
+
resizing ? '' : 'transition-[width] duration-200',
|
|
14
|
+
]"
|
|
15
|
+
:style="expanded ? undefined : { width: collapsed ? '48px' : `${width}px` }"
|
|
16
|
+
:aria-label="t('Ressources')"
|
|
11
17
|
>
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
class="
|
|
15
|
-
>{{ t('Ressources') }}</span>
|
|
16
|
-
<button
|
|
17
|
-
type="button"
|
|
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)"
|
|
18
|
+
<div
|
|
19
|
+
class="flex h-14 items-center border-b border-gray-default bg-gray-some px-3"
|
|
20
|
+
:class="collapsed ? 'justify-center' : 'justify-between'"
|
|
21
21
|
>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
class="
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
<span
|
|
23
|
+
v-if="!collapsed"
|
|
24
|
+
class="text-[14px] font-medium text-gray-title"
|
|
25
|
+
>{{ t('Ressources') }}</span>
|
|
26
|
+
<span class="flex items-center gap-1">
|
|
27
|
+
<button
|
|
28
|
+
v-if="!collapsed"
|
|
29
|
+
type="button"
|
|
30
|
+
class="inline-flex h-7 items-center gap-1 rounded px-2 text-[13px] font-medium text-gray-title underline-offset-4 hover:bg-gray-100 hover:underline"
|
|
31
|
+
@click="$emit('update:expanded', !expanded)"
|
|
32
|
+
>
|
|
33
|
+
<template v-if="expanded">
|
|
34
|
+
<RiArrowLeftLine
|
|
35
|
+
class="size-3.5"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
/>
|
|
38
|
+
{{ t('Revenir à l’explorateur') }}
|
|
39
|
+
</template>
|
|
40
|
+
<template v-else>
|
|
41
|
+
{{ t('Tout afficher') }}
|
|
42
|
+
</template>
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
v-if="!expanded"
|
|
46
|
+
type="button"
|
|
47
|
+
:title="collapsed ? t('Afficher le panneau des ressources') : t('Masquer le panneau')"
|
|
48
|
+
class="flex size-6 items-center justify-center rounded text-gray-plain hover:bg-gray-100"
|
|
49
|
+
@click="$emit('update:collapsed', !collapsed)"
|
|
50
|
+
>
|
|
51
|
+
<component
|
|
52
|
+
:is="collapsed ? RiSidebarUnfoldLine : RiSidebarFoldLine"
|
|
53
|
+
class="size-5"
|
|
54
|
+
/>
|
|
55
|
+
</button>
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
28
58
|
|
|
29
|
-
|
|
59
|
+
<!-- min-w keeps the content laid out at the sidebar's min width so it doesn't
|
|
30
60
|
reflow (badges wrapping, section titles on 2 lines) while the width animates
|
|
31
61
|
open from the collapsed state — the aside's overflow-hidden clips the rest. -->
|
|
32
|
-
|
|
62
|
+
<!-- Extra bottom padding so the last item can scroll clear of the browser's
|
|
33
63
|
hover URL bar (Firefox shows it bottom-left). -->
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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>
|
|
49
|
-
|
|
50
|
-
<section
|
|
51
|
-
v-for="group in groups"
|
|
52
|
-
:key="group.type"
|
|
53
|
-
class="space-y-0.5"
|
|
64
|
+
<div
|
|
65
|
+
v-if="!collapsed"
|
|
66
|
+
class="flex min-h-0 min-w-[260px] flex-1 flex-col gap-3 overflow-y-auto p-2 pb-16"
|
|
54
67
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
class="
|
|
71
|
-
@click="$emit('load-more', group.type)"
|
|
68
|
+
<label class="flex h-8 items-center gap-1 rounded border border-gray-default bg-gray-some px-2">
|
|
69
|
+
<RiSearchLine class="size-3.5 shrink-0 text-gray-medium" />
|
|
70
|
+
<span class="sr-only">{{ t('Rechercher') }}</span>
|
|
71
|
+
<input
|
|
72
|
+
:value="search"
|
|
73
|
+
type="search"
|
|
74
|
+
class="min-w-0 flex-1 bg-transparent text-[14px] outline-none placeholder:text-gray-medium"
|
|
75
|
+
:placeholder="t('Rechercher une ressource')"
|
|
76
|
+
@input="$emit('update:search', ($event.target as HTMLInputElement).value)"
|
|
77
|
+
>
|
|
78
|
+
</label>
|
|
79
|
+
|
|
80
|
+
<section
|
|
81
|
+
v-for="group in groups"
|
|
82
|
+
:key="group.type"
|
|
83
|
+
class="space-y-0.5"
|
|
72
84
|
>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
<p class="px-1 py-2 text-[13px] font-medium leading-4 text-gray-medium">
|
|
86
|
+
{{ getResourceLabel(group.type, group.total) }}
|
|
87
|
+
</p>
|
|
88
|
+
<ResourceListItem
|
|
89
|
+
v-for="resource in group.items"
|
|
90
|
+
:key="resource.id"
|
|
91
|
+
:resource
|
|
92
|
+
:dataset
|
|
93
|
+
:to="resourceTo(resource)"
|
|
94
|
+
:replace
|
|
95
|
+
:selected="resource.id === selectedResourceId"
|
|
96
|
+
:expanded
|
|
97
|
+
@click="$emit('update:expanded', false)"
|
|
76
98
|
/>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
<button
|
|
100
|
+
v-if="group.items.length < group.total"
|
|
101
|
+
type="button"
|
|
102
|
+
:disabled="loadingType === group.type"
|
|
103
|
+
class="flex w-full items-center gap-1 px-1 py-1 text-left text-[14px] text-blue-default hover:underline disabled:cursor-default disabled:no-underline"
|
|
104
|
+
@click="$emit('load-more', group.type)"
|
|
105
|
+
>
|
|
106
|
+
<RiLoader5Line
|
|
107
|
+
v-if="loadingType === group.type"
|
|
108
|
+
class="size-3.5 shrink-0 animate-spin"
|
|
109
|
+
/>
|
|
110
|
+
{{ t('Charger plus…') }}
|
|
111
|
+
</button>
|
|
112
|
+
</section>
|
|
80
113
|
|
|
81
|
-
|
|
114
|
+
<!-- The search lives here, so its outcome does too: the viewer only says that
|
|
82
115
|
nothing is selected. -->
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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', '')"
|
|
116
|
+
<div
|
|
117
|
+
v-if="search && !groups.length"
|
|
118
|
+
class="flex flex-col gap-2 px-1 py-2"
|
|
95
119
|
>
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
<p class="m-0 text-[14px] leading-snug text-gray-medium">
|
|
121
|
+
{{ t('Pas de résultats pour « {q} »', { q: search }) }}
|
|
122
|
+
</p>
|
|
123
|
+
<BrandedButton
|
|
124
|
+
color="secondary"
|
|
125
|
+
size="xs"
|
|
126
|
+
class="w-full"
|
|
127
|
+
@click="$emit('update:search', '')"
|
|
128
|
+
>
|
|
129
|
+
{{ t('Réinitialiser la recherche') }}
|
|
130
|
+
</BrandedButton>
|
|
131
|
+
</div>
|
|
98
132
|
</div>
|
|
99
|
-
</
|
|
133
|
+
</aside>
|
|
100
134
|
|
|
101
135
|
<!-- Drag the right edge to resize the panel and read longer resource titles.
|
|
102
136
|
A separator rather than a button: it only answers to a drag, and a focusable
|
|
103
137
|
button would announce an action that no key can trigger. Keyboard users fold
|
|
104
138
|
the panel from the header instead. -->
|
|
105
139
|
<div
|
|
106
|
-
v-if="!collapsed"
|
|
140
|
+
v-if="!collapsed && !expanded"
|
|
107
141
|
role="separator"
|
|
108
142
|
aria-orientation="vertical"
|
|
109
143
|
:title="t('Glisser pour redimensionner')"
|
|
110
|
-
class="absolute right-
|
|
144
|
+
class="group absolute -right-1.5 top-0 z-20 flex h-full w-3 cursor-col-resize touch-none justify-center hover:bg-new-primary/10"
|
|
111
145
|
@mousedown="startResize"
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
>
|
|
147
|
+
<!-- Grip centered on the header so the resize affordance is visible at rest. -->
|
|
148
|
+
<span
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
class="mt-3 flex h-8 w-2 flex-col items-center justify-center gap-0.5 rounded-full border border-gray-default bg-white shadow-sm transition-colors group-hover:border-new-primary"
|
|
151
|
+
>
|
|
152
|
+
<span
|
|
153
|
+
v-for="dot in 3"
|
|
154
|
+
:key="dot"
|
|
155
|
+
class="size-0.5 rounded-full bg-gray-medium group-hover:bg-new-primary"
|
|
156
|
+
/>
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
114
160
|
</template>
|
|
115
161
|
|
|
116
162
|
<script setup lang="ts">
|
|
117
163
|
import type { RouteLocationRaw } from 'vue-router'
|
|
118
|
-
import { RiSidebarFoldLine, RiSidebarUnfoldLine, RiSearchLine, RiLoader5Line } from '@remixicon/vue'
|
|
164
|
+
import { RiArrowLeftLine, RiSidebarFoldLine, RiSidebarUnfoldLine, RiSearchLine, RiLoader5Line } from '@remixicon/vue'
|
|
119
165
|
import BrandedButton from '../BrandedButton.vue'
|
|
120
166
|
import ResourceListItem from '../ResourceListItem.vue'
|
|
121
167
|
import { getResourceLabel } from '../../functions/resources'
|
|
122
168
|
import { useTranslation } from '../../composables/useTranslation'
|
|
123
169
|
import { useResizable } from '../../composables/useResizable'
|
|
170
|
+
import type { DatasetV2 } from '../../types/datasets'
|
|
124
171
|
import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
|
|
125
172
|
|
|
126
173
|
const { t } = useTranslation()
|
|
127
174
|
|
|
128
175
|
defineProps<{
|
|
176
|
+
dataset: DatasetV2
|
|
129
177
|
groups: ResourceGroup[]
|
|
130
178
|
selectedResourceId: string | null
|
|
131
179
|
collapsed: boolean
|
|
180
|
+
expanded: boolean
|
|
132
181
|
search: string
|
|
133
182
|
loadingType: ResourceType | null
|
|
134
183
|
resourceTo: (resource: Resource) => RouteLocationRaw
|
|
@@ -138,11 +187,12 @@ defineProps<{
|
|
|
138
187
|
defineEmits<{
|
|
139
188
|
'load-more': [type: ResourceType]
|
|
140
189
|
'update:collapsed': [value: boolean]
|
|
190
|
+
'update:expanded': [value: boolean]
|
|
141
191
|
'update:search': [value: string]
|
|
142
192
|
}>()
|
|
143
193
|
|
|
144
194
|
const { width, resizing, startResize } = useResizable({
|
|
145
|
-
initialWidth:
|
|
195
|
+
initialWidth: 340,
|
|
146
196
|
minWidth: 260,
|
|
147
197
|
maxWidth: 720,
|
|
148
198
|
})
|
|
@@ -63,8 +63,17 @@
|
|
|
63
63
|
v-if="previewKind === 'tabular'"
|
|
64
64
|
:timeout="200"
|
|
65
65
|
>
|
|
66
|
-
<TabularExplorer
|
|
66
|
+
<TabularExplorer
|
|
67
|
+
v-model:global-search="search"
|
|
68
|
+
:resource-id="resource.id"
|
|
69
|
+
>
|
|
67
70
|
<TabularToolbar class="shrink-0 border-b border-gray-default p-2" />
|
|
71
|
+
<div class="hidden shrink-0 md:block">
|
|
72
|
+
<TabularActiveFilters
|
|
73
|
+
with-clear
|
|
74
|
+
class="border-b border-gray-default p-2"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
68
77
|
<TabularTable :fill="fullscreen" />
|
|
69
78
|
<TabularMobileFilters />
|
|
70
79
|
</TabularExplorer>
|
|
@@ -161,7 +170,7 @@
|
|
|
161
170
|
</template>
|
|
162
171
|
|
|
163
172
|
<script setup lang="ts">
|
|
164
|
-
import { computed, defineAsyncComponent } from 'vue'
|
|
173
|
+
import { computed, defineAsyncComponent, ref } from 'vue'
|
|
165
174
|
import { useRoute, useRouter } from 'vue-router'
|
|
166
175
|
import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
|
|
167
176
|
import MarkdownViewer from '../MarkdownViewer.vue'
|
|
@@ -171,6 +180,7 @@ import TabList from '../Tabs/TabList.vue'
|
|
|
171
180
|
import Tab from '../Tabs/Tab.vue'
|
|
172
181
|
import TabPanels from '../Tabs/TabPanels.vue'
|
|
173
182
|
import TabPanel from '../Tabs/TabPanel.vue'
|
|
183
|
+
import TabularActiveFilters from '../TabularExplorer/TabularActiveFilters.vue'
|
|
174
184
|
import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
|
|
175
185
|
import TabularToolbar from '../TabularExplorer/TabularToolbar.vue'
|
|
176
186
|
import TabularTable from '../TabularExplorer/TabularTable.vue'
|
|
@@ -251,6 +261,11 @@ if (hasTabularData.value) {
|
|
|
251
261
|
await provideTabularProfile(() => props.resource.id)
|
|
252
262
|
}
|
|
253
263
|
|
|
264
|
+
// Kept here as well as in the table: the viewer outlives the data tab, so the search
|
|
265
|
+
// survives a trip to another tab, and it resets with the resource since switching
|
|
266
|
+
// resource remounts the viewer.
|
|
267
|
+
const search = ref('')
|
|
268
|
+
|
|
254
269
|
// The active tab lives in the URL so a shared link opens on the same one. Read once
|
|
255
270
|
// at mount (TabGroup only takes an initial index), which is enough: switching resource
|
|
256
271
|
// remounts the viewer, and the tab is preserved when the new resource also has it.
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
>
|
|
22
22
|
<ResourceListItem
|
|
23
23
|
:resource="r"
|
|
24
|
+
:dataset
|
|
24
25
|
:to="resourceTo(r)"
|
|
25
26
|
:replace
|
|
26
27
|
:selected="r.id === selectedId"
|
|
@@ -38,9 +39,11 @@ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
|
38
39
|
import { RiArrowDownSLine } from '@remixicon/vue'
|
|
39
40
|
import { useTranslation } from '../../composables/useTranslation'
|
|
40
41
|
import ResourceListItem from '../ResourceListItem.vue'
|
|
42
|
+
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
41
43
|
import type { Resource } from '../../types/resources'
|
|
42
44
|
|
|
43
45
|
defineProps<{
|
|
46
|
+
dataset: Dataset | DatasetV2
|
|
44
47
|
resources: Resource[]
|
|
45
48
|
selectedId: string
|
|
46
49
|
resourceTo: (resource: Resource) => RouteLocationRaw
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<header class="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-gray-default bg-gray-some px-3">
|
|
3
3
|
<div class="flex min-w-0 items-center gap-1.5 overflow-hidden text-[13px] text-gray-medium">
|
|
4
|
-
<
|
|
5
|
-
:resource
|
|
6
|
-
class="size-4 shrink-0"
|
|
7
|
-
/>
|
|
4
|
+
<ResourceIconBadge :resource />
|
|
8
5
|
<span
|
|
9
6
|
class="min-w-0 truncate font-medium text-gray-title"
|
|
10
7
|
:title="resource.title || t('Fichier sans nom')"
|
|
11
8
|
>{{ resource.title || t('Fichier sans nom') }}</span>
|
|
12
9
|
<ResourceSelector
|
|
13
10
|
v-if="resources && resources.length > 1 && resourceTo"
|
|
11
|
+
:dataset
|
|
14
12
|
:resources
|
|
15
13
|
:selected-id="resource.id"
|
|
16
14
|
:resource-to
|
|
@@ -87,7 +85,7 @@ import { computed } from 'vue'
|
|
|
87
85
|
import { RiDownloadLine, RiFullscreenLine } from '@remixicon/vue'
|
|
88
86
|
import BrandedButton from '../BrandedButton.vue'
|
|
89
87
|
import CopyButton from '../CopyButton.vue'
|
|
90
|
-
import
|
|
88
|
+
import ResourceIconBadge from '../ResourceIconBadge.vue'
|
|
91
89
|
import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
|
|
92
90
|
import ResourceSelector from './ResourceSelector.vue'
|
|
93
91
|
import ResourceMainAction from './ResourceMainAction.vue'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
:class="[iconColor, '[&_svg]:fill-current']"
|
|
4
|
+
class="flex size-5 shrink-0 items-center justify-center rounded-[1px]"
|
|
5
|
+
>
|
|
6
|
+
<component
|
|
7
|
+
:is="iconComponent"
|
|
8
|
+
class="size-4"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
/>
|
|
11
|
+
</span>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { computed } from 'vue'
|
|
16
|
+
import File from './Icons/File.vue'
|
|
17
|
+
import { getResourceFormatIcon, getResourceIconColor } from '../functions/resources'
|
|
18
|
+
import type { Resource } from '../types/resources'
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
resource: Resource
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
// Render the icon directly (not via ResourceIcon which forces a gray color) so the
|
|
25
|
+
// colored badge can tint it through currentColor + [&_svg]:fill-current.
|
|
26
|
+
const iconComponent = computed(() => (props.resource.format ? getResourceFormatIcon(props.resource.format) : null) ?? File)
|
|
27
|
+
const iconColor = computed(() => getResourceIconColor(props.resource.format))
|
|
28
|
+
</script>
|