@datagouv/components-next 1.0.2-dev.231 → 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 (28) hide show
  1. package/dist/{ImagePreview.client-H7jGf9xS.js → ImagePreview.client-CSRz7eUF.js} +2 -2
  2. package/dist/{JsonPreview.client-CQSCGT6V.js → JsonPreview.client-wT2hfYLv.js} +2 -2
  3. package/dist/{PdfPreview.client-Cz_jnvla.js → PdfPreview.client-DifxM8Th.js} +2 -2
  4. package/dist/{Pmtiles.client-CSoax_YA.js → Pmtiles.client-OyXiX_7g.js} +4 -4
  5. package/dist/{PreviewWrapper-DBlpLgs2.js → PreviewWrapper-D7yfQyyH.js} +1 -1
  6. package/dist/{XmlPreview.client-8vye8TAa.js → XmlPreview.client-BkPGZLn6.js} +2 -2
  7. package/dist/components-next.js +9356 -9191
  8. package/dist/components.css +1 -1
  9. package/dist/{france-Bu5x7PSp.js → france-BNatwStf.js} +1 -1
  10. package/dist/{resources-BBaRfEol.js → resources-kuI_FosB.js} +5 -5
  11. package/dist/{vue-D62CcCib.js → vue-D5eMxsCr.js} +78 -74
  12. package/package.json +1 -1
  13. package/src/components/ResourceExplorer/ResourceExplorer.vue +8 -2
  14. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +140 -90
  15. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +17 -2
  16. package/src/components/ResourceExplorer/ResourceSelector.vue +3 -0
  17. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +3 -5
  18. package/src/components/ResourceIconBadge.vue +28 -0
  19. package/src/components/ResourceListItem.vue +83 -27
  20. package/src/components/TabularExplorer/TabularActiveFilters.vue +94 -54
  21. package/src/components/TabularExplorer/TabularExplorer.vue +58 -18
  22. package/src/components/TabularExplorer/TabularSearchInput.vue +40 -0
  23. package/src/components/TabularExplorer/TabularSkeleton.vue +14 -6
  24. package/src/components/TabularExplorer/TabularTable.vue +12 -5
  25. package/src/components/TabularExplorer/TabularToolbar.vue +7 -4
  26. package/src/components/TabularExplorer/useTabularContext.ts +10 -0
  27. package/src/composables/useResourceCapabilities.ts +5 -0
  28. package/src/main.ts +2 -0
@@ -1,79 +1,119 @@
1
1
  <template>
2
2
  <div
3
- v-if="activeFilters.length > 0 || sort"
4
- class="flex flex-wrap items-center gap-1.5"
3
+ v-if="activeFilters.length > 0 || sort || globalSearch"
4
+ class="flex items-center justify-between gap-2"
5
5
  >
6
- <!-- Sort chip -->
7
- <span
8
- v-if="sort"
9
- class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
10
- >
11
- <component
12
- :is="sort.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
13
- class="size-3.5"
14
- aria-hidden="true"
15
- />
16
- <span class="font-medium">{{ sort.column }}</span>
17
- <span>{{ sort.direction === 'asc' ? t('croissant') : t('décroissant') }}</span>
18
- <button
19
- :aria-label="t('Supprimer le tri')"
20
- class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
21
- @click="sort = null"
6
+ <div class="flex min-w-0 flex-wrap items-center gap-1.5">
7
+ <!-- Sort chip -->
8
+ <span
9
+ v-if="sort"
10
+ class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
22
11
  >
23
- <RiCloseLine
12
+ <component
13
+ :is="sort.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
24
14
  class="size-3.5"
25
15
  aria-hidden="true"
26
16
  />
27
- </button>
28
- </span>
17
+ <span class="font-medium">{{ sort.column }}</span>
18
+ <span>{{ sort.direction === 'asc' ? t('croissant') : t('décroissant') }}</span>
19
+ <button
20
+ :aria-label="t('Supprimer le tri')"
21
+ class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
22
+ @click="sort = null"
23
+ >
24
+ <RiCloseLine
25
+ class="size-3.5"
26
+ aria-hidden="true"
27
+ />
28
+ </button>
29
+ </span>
29
30
 
30
- <!-- Filter chips -->
31
- <span
32
- v-for="af in activeFilters"
33
- :key="af.column"
34
- :data-testid="`active-filter-${af.column}`"
35
- class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
36
- >
37
- <component
38
- :is="getColumnDisplay(af.column).icon"
39
- class="size-3.5"
40
- aria-hidden="true"
41
- />
42
- <span class="font-medium">{{ af.column }}</span>
43
- <span>{{ af.label }}</span>
44
- <button
45
- :aria-label="t('Supprimer ce filtre')"
46
- class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
47
- @click="removeFilter(af.column)"
31
+ <!-- Global search chip -->
32
+ <span
33
+ v-if="globalSearch"
34
+ class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
48
35
  >
49
- <RiCloseLine
36
+ <RiSearchLine
50
37
  class="size-3.5"
51
38
  aria-hidden="true"
52
39
  />
53
- </button>
54
- </span>
40
+ <span class="font-medium">{{ t('Toutes les colonnes') }}</span>
41
+ <span>{{ t('contient') }} "{{ globalSearch }}"</span>
42
+ <button
43
+ :aria-label="t('Supprimer la recherche')"
44
+ class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
45
+ @click="globalSearch = ''"
46
+ >
47
+ <RiCloseLine
48
+ class="size-3.5"
49
+ aria-hidden="true"
50
+ />
51
+ </button>
52
+ </span>
55
53
 
56
- <!-- Clear all -->
57
- <BrandedButton
58
- v-if="withClear"
59
- color="tertiary"
60
- size="2xs"
61
- :icon="RiCloseLine"
62
- @click="clearAllFilters(); sort = null"
63
- >
64
- {{ t('Tout effacer') }}
65
- </BrandedButton>
54
+ <!-- Filter chips -->
55
+ <span
56
+ v-for="af in activeFilters"
57
+ :key="af.column"
58
+ :data-testid="`active-filter-${af.column}`"
59
+ class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
60
+ >
61
+ <component
62
+ :is="getColumnDisplay(af.column).icon"
63
+ class="size-3.5"
64
+ aria-hidden="true"
65
+ />
66
+ <span class="font-medium">{{ af.column }}</span>
67
+ <span>{{ af.label }}</span>
68
+ <button
69
+ :aria-label="t('Supprimer ce filtre')"
70
+ class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
71
+ @click="removeFilter(af.column)"
72
+ >
73
+ <RiCloseLine
74
+ class="size-3.5"
75
+ aria-hidden="true"
76
+ />
77
+ </button>
78
+ </span>
79
+ </div>
80
+
81
+ <div class="flex shrink-0 items-center gap-1">
82
+ <!-- A sort alone narrows nothing: the file would be the whole resource, which
83
+ the resource's own download already gives. And with no matching row, the
84
+ API answers an empty file, without even the header. -->
85
+ <BrandedButton
86
+ v-if="(activeFilters.length > 0 || globalSearch) && tableData?.meta.total"
87
+ color="tertiary"
88
+ size="2xs"
89
+ :icon="RiDownloadLine"
90
+ :href="filteredDownloadUrl"
91
+ >
92
+ {{ t('Télécharger les données filtrées') }}
93
+ </BrandedButton>
94
+ <!-- Clear all -->
95
+ <BrandedButton
96
+ v-if="withClear"
97
+ color="tertiary"
98
+ size="2xs"
99
+ :icon="RiCloseLine"
100
+ @click="clearAllFilters(); sort = null"
101
+ >
102
+ {{ t('Tout effacer') }}
103
+ </BrandedButton>
104
+ </div>
66
105
  </div>
67
106
  </template>
68
107
 
69
108
  <script setup lang="ts">
70
- import { RiArrowDownLine, RiArrowUpLine, RiCloseLine } from '@remixicon/vue'
109
+ import { RiArrowDownLine, RiArrowUpLine, RiCloseLine, RiDownloadLine, RiSearchLine } from '@remixicon/vue'
71
110
  import BrandedButton from '../BrandedButton.vue'
72
111
  import { useTranslation } from '../../composables/useTranslation'
73
112
  import { useTabularContext } from './useTabularContext'
74
113
 
114
+ // Framing (borders, padding) belongs to the call site, like the toolbar's.
75
115
  defineProps<{ withClear?: boolean }>()
76
116
 
77
117
  const { t } = useTranslation()
78
- const { activeFilters, sort, removeFilter, clearAllFilters, getColumnDisplay } = useTabularContext()
118
+ const { tableData, activeFilters, sort, globalSearch, removeFilter, clearAllFilters, getColumnDisplay, filteredDownloadUrl } = useTabularContext()
79
119
  </script>
@@ -8,11 +8,16 @@
8
8
 
9
9
  <!-- Same skeleton as the Suspense fallback above us: in both cases the slot isn't
10
10
  rendered yet, so its toolbar is a placeholder too. -->
11
- <TabularSkeleton v-else-if="previewLoading" />
11
+ <TabularSkeleton
12
+ v-else-if="previewLoading"
13
+ :search-input
14
+ />
12
15
 
13
16
  <!-- Loaded: the consumer composes the parts (toolbar, table, mobile sheet) from
14
- the provided context, so it controls the framing and layout. -->
15
- <template v-else-if="tableData && profileData">
17
+ the provided context, so it controls the framing and layout. Once loaded, it
18
+ stays: a failed search or filter only empties the rows, so the controls that
19
+ caused it remain there to undo it. -->
20
+ <template v-else-if="(tableData || loadedOnce) && profileData">
16
21
  <slot />
17
22
  </template>
18
23
  </div>
@@ -32,15 +37,11 @@ import type { TabularDataResponse, TabularRow, SortConfig, ColumnFilters, DateFi
32
37
  import { provideTabularContext, type ActiveFilter } from './useTabularContext'
33
38
  import { useColumnMetadata } from './useColumnMetadata'
34
39
 
35
- const props = defineProps<{
40
+ const props = withDefaults(defineProps<{
36
41
  resourceId: string
37
- // When set, searches across multiple columns using the Tabular API's or(...)
38
- // parameter. Text and categorical columns get a __contains filter; number
39
- // columns get a __exact filter (since __contains is not supported for numbers
40
- // by the API). Year, date and boolean columns are excluded.
41
- // Note: combined via AND with any existing column-specific `contains` filters,
42
- // so it acts as an additional narrowing constraint, not a replacement.
43
- globalSearch?: string
42
+ // Whether the toolbar shows its search field. A page with a search of its own
43
+ // hides it and drives the search through `v-model:global-search` instead.
44
+ searchInput?: boolean
44
45
  // Filters seeded on mount, e.g. { 'Administration': { contains: 'Ministère' } }.
45
46
  // The explorer owns them afterwards: later changes to this prop are ignored,
46
47
  // so pass a fresh instance (or remount) to reset them.
@@ -49,7 +50,18 @@ const props = defineProps<{
49
50
  // Same ownership rule as `initialFilters`: the explorer owns it afterwards,
50
51
  // and the user can drop it from the active-sort chip.
51
52
  initialSort?: SortConfig
52
- }>()
53
+ }>(), {
54
+ searchInput: true,
55
+ })
56
+
57
+ // Searches across multiple columns using the Tabular API's or(...) parameter. Text
58
+ // and categorical columns get a __contains filter; number columns get a __exact
59
+ // filter (since __contains is not supported for numbers by the API). Year, date and
60
+ // boolean columns are excluded.
61
+ // Combined via AND with any column-specific `contains` filter, so it acts as an
62
+ // additional narrowing constraint, not a replacement.
63
+ // Owned here like the filters; a parent binds it to drive or keep it.
64
+ const globalSearch = defineModel<string>('globalSearch', { default: '' })
53
65
 
54
66
  const { t } = useTranslation()
55
67
  const config = useComponentsConfig()
@@ -85,8 +97,10 @@ const filters = ref<Record<string, ColumnFilters>>({ ...props.initialFilters })
85
97
 
86
98
  const PAGE_SIZE = 50
87
99
 
88
- const dataQuery = computed(() => {
89
- const q: Record<string, string | number> = { page: 1, page_size: PAGE_SIZE }
100
+ // What the table shows (sort, filters, search), apart from its pagination: the
101
+ // filtered download sends the very same params, so the file can't drift from the table.
102
+ const tableQuery = computed(() => {
103
+ const q: Record<string, string | number> = {}
90
104
  if (sort.value) {
91
105
  q[`${sort.value.column}__sort`] = sort.value.direction
92
106
  }
@@ -116,21 +130,28 @@ const dataQuery = computed(() => {
116
130
  Object.assign(q, buildDateFilterParams(col, filter.date))
117
131
  }
118
132
  }
119
- if (props.globalSearch && profileData.value?.profile) {
120
- const conditions = buildGlobalSearchConditions(allColumns.value, getColumnType, props.globalSearch)
133
+ if (globalSearch.value && profileData.value?.profile) {
134
+ const conditions = buildGlobalSearchConditions(allColumns.value, getColumnType, globalSearch.value)
121
135
  q.or = '(' + conditions.join(',') + ')'
122
136
  }
123
137
  return q
124
138
  })
125
139
 
140
+ const dataQuery = computed(() => ({ page: 1, page_size: PAGE_SIZE, ...tableQuery.value }))
141
+
126
142
  const { data: tableData, error, status: dataStatus } = await useFetch<TabularDataResponse>(dataUrl, { raw: true, query: dataQuery })
127
143
 
144
+ // A failed request clears `tableData`: telling a resource that can't be previewed
145
+ // from a search or filter the API rejects needs to know whether it ever loaded.
146
+ const loadedOnce = ref(!!tableData.value)
147
+
128
148
  // The component renders nothing useful until the profile is available
129
149
  // (allColumns is derived from it). Surface a clear loading / error state
130
150
  // so we don't end up with an empty table + a spinner running forever.
131
151
  const profileLoading = computed(() => !profileData.value && (profileStatus.value === 'idle' || profileStatus.value === 'pending'))
132
- const previewError = computed(() => error.value || profileError.value)
133
- const previewLoading = computed(() => !previewError.value && (!tableData.value || profileLoading.value))
152
+ const previewError = computed(() => profileError.value || (!loadedOnce.value && error.value))
153
+ const queryFailed = computed(() => loadedOnce.value && !!error.value)
154
+ const previewLoading = computed(() => !previewError.value && (profileLoading.value || (!tableData.value && !loadedOnce.value)))
134
155
  // A search / filter / sort change refetches while the previous rows stay on
135
156
  // screen: without a signal, the table looks unchanged for several seconds.
136
157
  const isRefreshing = computed(() => dataStatus.value === 'pending' && !previewLoading.value)
@@ -145,10 +166,15 @@ const generation = ref(0)
145
166
  watch(() => tableData.value, (data) => {
146
167
  generation.value++
147
168
  if (data) {
169
+ loadedOnce.value = true
148
170
  allRows.value = [...data.data]
149
171
  currentPage.value = 1
150
172
  hasMore.value = data.data.length < data.meta.total
151
173
  }
174
+ else if (queryFailed.value) {
175
+ allRows.value = []
176
+ hasMore.value = false
177
+ }
152
178
  }, { immediate: true })
153
179
 
154
180
  async function loadNextPage() {
@@ -275,8 +301,18 @@ function removeFilter(column: string) {
275
301
  filters.value = rest
276
302
  }
277
303
 
304
+ // The Tabular API's CSV export takes the params of the data endpoint and ignores the
305
+ // pagination: the file holds every filtered row, not just the pages loaded on screen.
306
+ // All the columns, not only the displayed ones: the API splits `columns` on every
307
+ // comma, so a column whose name holds one can't be requested by name.
308
+ const filteredDownloadUrl = computed(() => {
309
+ const params = new URLSearchParams(Object.entries(tableQuery.value).map(([key, value]) => [key, String(value)]))
310
+ return `${config.tabularApiUrl}/api/resources/${props.resourceId}/data/csv/?${params}`
311
+ })
312
+
278
313
  function clearAllFilters() {
279
314
  filters.value = {}
315
+ globalSearch.value = ''
280
316
  }
281
317
 
282
318
  function hasFilterForColumn(col: string): boolean {
@@ -300,6 +336,10 @@ provideTabularContext({
300
336
  activeFilters,
301
337
  removeFilter,
302
338
  clearAllFilters,
339
+ globalSearch,
340
+ searchInput: computed(() => props.searchInput),
341
+ queryFailed,
342
+ filteredDownloadUrl,
303
343
  hasFilterForColumn,
304
344
  allColumns,
305
345
  visibleColumns,
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <label class="flex h-8 w-[clamp(160px,22vw,220px)] min-w-0 shrink-0 items-center gap-1 rounded border border-gray-default bg-gray-some px-2">
3
+ <RiSearchLine
4
+ class="size-3.5 shrink-0 text-gray-medium"
5
+ aria-hidden="true"
6
+ />
7
+ <input
8
+ v-model="text"
9
+ type="search"
10
+ class="min-w-0 flex-1 bg-transparent text-[13px] outline-none placeholder:text-gray-medium [&::-webkit-search-cancel-button]:appearance-none"
11
+ :aria-label="t('Rechercher une valeur')"
12
+ :placeholder="t('Rechercher une valeur')"
13
+ >
14
+ </label>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { ref, watch } from 'vue'
19
+ import { RiSearchLine } from '@remixicon/vue'
20
+ import { useComponentsConfig } from '../../config'
21
+ import { useDebouncedRef } from '../../composables/useDebouncedRef'
22
+ import { useTranslation } from '../../composables/useTranslation'
23
+ import { useTabularContext } from './useTabularContext'
24
+
25
+ const { t } = useTranslation()
26
+ const config = useComponentsConfig()
27
+ const { globalSearch } = useTabularContext()
28
+
29
+ // Only typing is debounced: each settled value is a request to the Tabular API.
30
+ const text = ref(globalSearch.value)
31
+ const { debounced } = useDebouncedRef(text, config.searchDebounce ?? 300)
32
+ watch(debounced, (value) => {
33
+ globalSearch.value = value.trim()
34
+ })
35
+
36
+ // Cleared from elsewhere (its chip, "Tout effacer"): the field follows.
37
+ watch(globalSearch, (value) => {
38
+ if (value !== text.value.trim()) text.value = value
39
+ })
40
+ </script>
@@ -1,9 +1,13 @@
1
1
  <template>
2
- <!-- Toolbar row — mirrors TabularToolbar: the mobile filter button on the left (it
3
- only shows below md), and the columns menu (h-6 button, which drives the row
4
- height) plus the rows info on the right. The active-filters area stays empty
5
- until the user filters, so nothing stands in for it. -->
2
+ <!-- Toolbar row — mirrors TabularToolbar: the search field and the mobile filter
3
+ button (it only shows below md) on the left, the columns menu plus the rows info
4
+ on the right. The active-filters line stays empty until the user filters, so
5
+ nothing stands in for it. -->
6
6
  <div class="animate-pulse-placeholder flex shrink-0 items-center gap-2 border-b border-gray-default p-2">
7
+ <div
8
+ v-if="searchInput"
9
+ class="h-8 w-[clamp(160px,22vw,220px)] shrink-0 rounded bg-gray-200"
10
+ />
7
11
  <div class="h-6 w-24 rounded bg-gray-200 md:hidden" />
8
12
  <div class="flex flex-1 items-center justify-end gap-4">
9
13
  <div class="h-6 w-24 rounded bg-gray-200" />
@@ -83,10 +87,14 @@
83
87
  <script setup lang="ts">
84
88
  import { useTranslation } from '../../composables/useTranslation'
85
89
 
86
- defineProps<{
90
+ withDefaults(defineProps<{
87
91
  // Fill the available height (fullscreen) instead of the default capped height.
88
92
  fill?: boolean
89
- }>()
93
+ // Same as the explorer's: whether the toolbar has its search field.
94
+ searchInput?: boolean
95
+ }>(), {
96
+ searchInput: true,
97
+ })
90
98
 
91
99
  const { t } = useTranslation()
92
100
  </script>
@@ -113,11 +113,12 @@
113
113
  class="py-16 text-center"
114
114
  >
115
115
  <div class="flex flex-col items-center gap-2">
116
- <RiSearchLine
116
+ <component
117
+ :is="queryFailed ? RiErrorWarningLine : RiSearchLine"
117
118
  class="size-8 text-gray-low"
118
119
  aria-hidden="true"
119
120
  />
120
- <span class="text-sm text-gray-low">{{ t('Aucun résultat trouvé.') }}</span>
121
+ <span class="text-sm text-gray-low">{{ emptyMessage }}</span>
121
122
  </div>
122
123
  </td>
123
124
  </tr>
@@ -175,11 +176,12 @@
175
176
  class="py-16 text-center"
176
177
  >
177
178
  <div class="flex flex-col items-center gap-2">
178
- <RiSearchLine
179
+ <component
180
+ :is="queryFailed ? RiErrorWarningLine : RiSearchLine"
179
181
  class="size-8 text-gray-low"
180
182
  aria-hidden="true"
181
183
  />
182
- <span class="text-sm text-gray-low">{{ t('Aucun résultat trouvé.') }}</span>
184
+ <span class="text-sm text-gray-low">{{ emptyMessage }}</span>
183
185
  </div>
184
186
  </div>
185
187
  <div
@@ -256,7 +258,7 @@
256
258
  <script setup lang="ts">
257
259
  import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
258
260
  import { useElementSize, useScroll } from '@vueuse/core'
259
- import { RiArrowDownLine, RiArrowDownSLine, RiArrowUpLine, RiSearchLine } from '@remixicon/vue'
261
+ import { RiArrowDownLine, RiArrowDownSLine, RiArrowUpLine, RiErrorWarningLine, RiSearchLine } from '@remixicon/vue'
260
262
  import AppLink from '../AppLink.vue'
261
263
  import BrandedButton from '../BrandedButton.vue'
262
264
  import InfiniteLoader from '../InfiniteLoader.vue'
@@ -300,8 +302,13 @@ const {
300
302
  getColumnDisplay,
301
303
  getNullPercent,
302
304
  getBooleanCounts,
305
+ queryFailed,
303
306
  } = useTabularContext()
304
307
 
308
+ const emptyMessage = computed(() => queryFailed.value
309
+ ? t('Les données n\'ont pas pu être chargées avec cette recherche ou ces filtres.')
310
+ : t('Aucun résultat trouvé.'))
311
+
305
312
  function getRowHref(row: TabularRow): string | undefined {
306
313
  return props.rowHref?.href(row)
307
314
  }
@@ -1,10 +1,8 @@
1
1
  <template>
2
2
  <div class="flex items-center gap-2">
3
3
  <div class="flex min-w-0 flex-1 items-center gap-1.5">
4
+ <TabularSearchInput v-if="searchInput" />
4
5
  <TabularMobileFilterButton class="md:hidden" />
5
- <div class="hidden md:block">
6
- <TabularActiveFilters with-clear />
7
- </div>
8
6
  </div>
9
7
  <div class="flex shrink-0 items-center gap-4">
10
8
  <TabularColumnsMenu />
@@ -14,12 +12,17 @@
14
12
  </template>
15
13
 
16
14
  <script setup lang="ts">
17
- import TabularActiveFilters from './TabularActiveFilters.vue'
18
15
  import TabularColumnsMenu from './TabularColumnsMenu.vue'
19
16
  import TabularMobileFilterButton from './TabularMobileFilterButton.vue'
20
17
  import TabularRowsInfo from './TabularRowsInfo.vue'
18
+ import TabularSearchInput from './TabularSearchInput.vue'
19
+ import { useTabularContext } from './useTabularContext'
21
20
 
22
21
  // Framing (borders, padding, spacing) belongs to the call site: this toolbar sits in a
23
22
  // bordered panel in the resource viewer and in the page flow on the standalone explore
24
23
  // pages. Single root element, so the caller's `class` merges onto it.
24
+ // The active filters are not in here: they get a line of their own, placed by the
25
+ // call site under the toolbar (<TabularActiveFilters>).
26
+
27
+ const { searchInput } = useTabularContext()
25
28
  </script>
@@ -42,7 +42,17 @@ export type TabularContext = {
42
42
  filters: Ref<Record<string, ColumnFilters>>
43
43
  activeFilters: ComputedRef<ActiveFilter[]>
44
44
  removeFilter: (column: string) => void
45
+ // Also empties the global search.
45
46
  clearAllFilters: () => void
47
+
48
+ // Global search across all columns, and whether the toolbar shows its field
49
+ globalSearch: Ref<string>
50
+ searchInput: ComputedRef<boolean>
51
+ // The last request was rejected after a first successful load: the controls stay,
52
+ // the rows are replaced by the error.
53
+ queryFailed: ComputedRef<boolean>
54
+ // CSV export of the rows matching the filters and search
55
+ filteredDownloadUrl: ComputedRef<string>
46
56
  hasFilterForColumn: (col: string) => boolean
47
57
 
48
58
  // Columns visibility
@@ -82,6 +82,10 @@ export function useResourceCapabilities(
82
82
  return 'unavailable'
83
83
  })
84
84
 
85
+ // Whether opening the resource shows its content (a preview or a map), not just
86
+ // its metadata.
87
+ const hasPreview = computed(() => previewKind.value !== 'unavailable' || !!hasPmtiles.value || ogcWms.value)
88
+
85
89
  const tabsOptions = computed(() => {
86
90
  const r = toValue(resource)
87
91
  const options = []
@@ -122,6 +126,7 @@ export function useResourceCapabilities(
122
126
 
123
127
  return {
124
128
  previewKind,
129
+ hasPreview,
125
130
  hasTabularData,
126
131
  hasPmtiles,
127
132
  hasPmtilesError,
package/src/main.ts CHANGED
@@ -109,6 +109,7 @@ import SearchableSelect from './components/Form/SearchableSelect.vue'
109
109
  import SelectGroup from './components/Form/SelectGroup.vue'
110
110
  import Listbox from './components/Form/Listbox.vue'
111
111
  import InfiniteLoader from './components/InfiniteLoader.vue'
112
+ import TabularActiveFilters from './components/TabularExplorer/TabularActiveFilters.vue'
112
113
  import TabularExplorer from './components/TabularExplorer/TabularExplorer.vue'
113
114
  import TabularMobileFilters from './components/TabularExplorer/TabularMobileFilters.vue'
114
115
  import TabularTable from './components/TabularExplorer/TabularTable.vue'
@@ -422,6 +423,7 @@ export {
422
423
  SelectGroup,
423
424
  Listbox,
424
425
  InfiniteLoader,
426
+ TabularActiveFilters,
425
427
  TabularExplorer,
426
428
  TabularMobileFilters,
427
429
  TabularTable,