@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.
Files changed (39) hide show
  1. package/dist/{ImagePreview.client-Db0MdI5N.js → ImagePreview.client-CSRz7eUF.js} +2 -2
  2. package/dist/{JsonPreview.client-DRCq40Q-.js → JsonPreview.client-wT2hfYLv.js} +2 -2
  3. package/dist/{Map-D17BKcyK.js → Map-QeL96qbQ.js} +1 -1
  4. package/dist/{MapContainer.client-DdPY_Iri.js → MapContainer.client-UC7wQOBj.js} +10 -10
  5. package/dist/{PdfPreview.client-DtKCBgGZ.js → PdfPreview.client-DifxM8Th.js} +2 -2
  6. package/dist/{Pmtiles.client-DSZ_4FXc.js → Pmtiles.client-OyXiX_7g.js} +2104 -2905
  7. package/dist/{PreviewWrapper-DegSQbMM.js → PreviewWrapper-D7yfQyyH.js} +1 -1
  8. package/dist/{XmlPreview.client-D9R_YF5s.js → XmlPreview.client-BkPGZLn6.js} +2 -2
  9. package/dist/components-next.css +1 -1
  10. package/dist/components-next.js +10149 -9989
  11. package/dist/components.css +1 -1
  12. package/dist/{france-Bu5x7PSp.js → france-BNatwStf.js} +1 -1
  13. package/dist/purify.es-CeeOHdER.js +805 -0
  14. package/dist/{resources-BOKIyTJV.js → resources-kuI_FosB.js} +18 -21
  15. package/dist/src-C0XyM3eg.js +68979 -0
  16. package/dist/{vue-D62CcCib.js → vue-D5eMxsCr.js} +78 -74
  17. package/package.json +2 -2
  18. package/src/components/ResourceAccordion/EditButton.vue +1 -4
  19. package/src/components/ResourceAccordion/MapContainer.client.vue +18 -7
  20. package/src/components/ResourceAccordion/ResourceAccordion.vue +1 -6
  21. package/src/components/ResourceExplorer/ResourceExplorer.vue +8 -2
  22. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +140 -90
  23. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +17 -2
  24. package/src/components/ResourceExplorer/ResourceSelector.vue +3 -0
  25. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +3 -5
  26. package/src/components/ResourceIconBadge.vue +28 -0
  27. package/src/components/ResourceListItem.vue +83 -27
  28. package/src/components/TabularExplorer/TabularActiveFilters.vue +94 -54
  29. package/src/components/TabularExplorer/TabularExplorer.vue +58 -18
  30. package/src/components/TabularExplorer/TabularSearchInput.vue +40 -0
  31. package/src/components/TabularExplorer/TabularSkeleton.vue +14 -6
  32. package/src/components/TabularExplorer/TabularTable.vue +12 -5
  33. package/src/components/TabularExplorer/TabularToolbar.vue +7 -4
  34. package/src/components/TabularExplorer/useTabularContext.ts +10 -0
  35. package/src/composables/useResourceCapabilities.ts +5 -0
  36. package/src/functions/resources.ts +0 -4
  37. package/src/main.ts +2 -0
  38. package/src/types/resources.ts +0 -6
  39. package/dist/src-DbFybTVs.js +0 -66090
@@ -4,8 +4,7 @@
4
4
  icon-only
5
5
  :icon="RiPencilLine"
6
6
  color="warning"
7
- :disabled="!!disabledReason"
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
- layerImport._serviceUrlImportInput.value = props.resource.url
102
- layerImport._formContainer.dispatchEvent(new CustomEvent('submit', { cancelable: true }))
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 (!layerImport._getCapResponseWMSLayers) {
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
- // @ts-expect-error no typing from library
120
- const layerInfo = layerImport._getCapResponseWMSLayers.filter(layer => layer.Name == props.resource.title)[0]
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, isGeopfSynced, isImagePreviewFormat, resolveResourceExternalUrl } from '../../functions/resources'
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 class="hidden md:flex">
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
- <aside
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')"
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
- <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'"
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
- <span
13
- v-if="!collapsed"
14
- class="text-[13px] font-medium text-gray-title"
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
- <component
23
- :is="collapsed ? RiSidebarUnfoldLine : RiSidebarFoldLine"
24
- class="size-5"
25
- />
26
- </button>
27
- </div>
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
- <!-- min-w keeps the content laid out at the sidebar's min width so it doesn't
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
- <!-- Extra bottom padding so the last item can scroll clear of the browser's
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
- <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>
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
- <p class="px-1 py-2 text-[12px] font-medium leading-3 text-gray-medium">
56
- {{ getResourceLabel(group.type, group.total) }}
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
- />
66
- <button
67
- v-if="group.items.length < group.total"
68
- type="button"
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"
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
- <RiLoader5Line
74
- v-if="loadingType === group.type"
75
- class="size-3.5 shrink-0 animate-spin"
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
- {{ t('Charger plus…') }}
78
- </button>
79
- </section>
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
- <!-- The search lives here, so its outcome does too: the viewer only says that
114
+ <!-- The search lives here, so its outcome does too: the viewer only says that
82
115
  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', '')"
116
+ <div
117
+ v-if="search && !groups.length"
118
+ class="flex flex-col gap-2 px-1 py-2"
95
119
  >
96
- {{ t('Réinitialiser la recherche') }}
97
- </BrandedButton>
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
- </div>
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-0 top-0 z-20 h-full w-2 cursor-col-resize touch-none hover:bg-new-primary/10"
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
- </aside>
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: 300,
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 :resource-id="resource.id">
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
- <ResourceIcon
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 ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
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>