@datagouv/components-next 1.0.2-dev.224 → 1.0.2-dev.226
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/{Datafair.client-DCH6pKT-.js → Datafair.client-DAVOjeA5.js} +1 -1
- package/dist/{ImagePreview.client-4V1oQ9HT.js → ImagePreview.client-BVq2fe2D.js} +1 -1
- package/dist/{JsonPreview.client-HLeSx8cB.js → JsonPreview.client-D7JQGx8Y.js} +2 -2
- package/dist/{MapContainer.client-Nrff3qMB.js → MapContainer.client-9VZAoTyE.js} +2 -2
- package/dist/{PdfPreview.client-aBObZjHC.js → PdfPreview.client-CMmfJ0bo.js} +2 -2
- package/dist/{Pmtiles.client-BZQJsJvC.js → Pmtiles.client-CmM3_3V2.js} +1 -1
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-S7RhJ98Q.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-DQjjYC78.js} +1 -1
- package/dist/{XmlPreview.client-DKlkBMFL.js → XmlPreview.client-C8eEKIK1.js} +3 -3
- package/dist/components-next.js +90 -88
- package/dist/{index-izN9k0Xe.js → index-CZCR7b2x.js} +1 -1
- package/dist/{leaflet-src-Cjar1xgD.js → leaflet-src-CzCD_D2E.js} +1 -1
- package/dist/{main-Cv5Nde60.js → main-DGD6P2pt.js} +10425 -10414
- package/dist/{vue3-xml-viewer.common-DhlRLVze.js → vue3-xml-viewer.common-DvtdBvwr.js} +1 -1
- package/package.json +1 -1
- package/src/components/RadioInput.vue +1 -0
- package/src/components/Search/GlobalSearch.vue +50 -17
- package/src/composables/useStableQueryParams.ts +26 -15
- package/src/functions/helpers.ts +10 -0
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
:key="configKey(typeConfig)"
|
|
35
35
|
:value="configKey(typeConfig)"
|
|
36
36
|
:count="resultsMap[configKey(typeConfig)]?.data.value?.total"
|
|
37
|
-
:loading="resultsMap[configKey(typeConfig)]?.
|
|
37
|
+
:loading="resultsMap[configKey(typeConfig)]?.countsLoading.value"
|
|
38
38
|
:icon="typeConfig.icon ?? strategies[typeConfig.class].icon"
|
|
39
39
|
>
|
|
40
40
|
{{ typeConfig.name || strategies[typeConfig.class].name }}
|
|
@@ -106,28 +106,28 @@
|
|
|
106
106
|
v-if="isEnabled('format_family')"
|
|
107
107
|
v-model="formatFamily"
|
|
108
108
|
:facets="getFacets('format_family')"
|
|
109
|
-
:loading="
|
|
109
|
+
:loading="facetsLoading"
|
|
110
110
|
:style="{ order: getOrder('format_family') }"
|
|
111
111
|
/>
|
|
112
112
|
<AccessTypeFilter
|
|
113
113
|
v-if="isEnabled('access_type')"
|
|
114
114
|
v-model="accessType"
|
|
115
115
|
:facets="getFacets('access_type')"
|
|
116
|
-
:loading="
|
|
116
|
+
:loading="facetsLoading"
|
|
117
117
|
:style="{ order: getOrder('access_type') }"
|
|
118
118
|
/>
|
|
119
119
|
<LastUpdateRangeFilter
|
|
120
120
|
v-if="isEnabled('last_update_range')"
|
|
121
121
|
v-model="lastUpdateRange"
|
|
122
122
|
:facets="getFacets('last_update')"
|
|
123
|
-
:loading="
|
|
123
|
+
:loading="facetsLoading"
|
|
124
124
|
:style="{ order: getOrder('last_update_range') }"
|
|
125
125
|
/>
|
|
126
126
|
<ProducerTypeFilter
|
|
127
127
|
v-if="isEnabled('producer_type')"
|
|
128
128
|
v-model="producerType"
|
|
129
129
|
:facets="getFacets('producer_type')"
|
|
130
|
-
:loading="
|
|
130
|
+
:loading="facetsLoading"
|
|
131
131
|
:exclude="currentTypeConfig?.class === 'organizations' ? ['user'] : []"
|
|
132
132
|
:style="{ order: getOrder('producer_type') }"
|
|
133
133
|
/>
|
|
@@ -135,14 +135,14 @@
|
|
|
135
135
|
v-if="isEnabled('badge')"
|
|
136
136
|
v-model="badge"
|
|
137
137
|
:facets="getFacets('badge')"
|
|
138
|
-
:loading="
|
|
138
|
+
:loading="facetsLoading"
|
|
139
139
|
:style="{ order: getOrder('badge') }"
|
|
140
140
|
/>
|
|
141
141
|
<ReuseTypeFilter
|
|
142
142
|
v-if="isEnabled('type')"
|
|
143
143
|
v-model="reuseType"
|
|
144
144
|
:facets="getFacets('type')"
|
|
145
|
-
:loading="
|
|
145
|
+
:loading="facetsLoading"
|
|
146
146
|
:style="{ order: getOrder('type') }"
|
|
147
147
|
/>
|
|
148
148
|
<slot
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
</template>
|
|
355
355
|
|
|
356
356
|
<script setup lang="ts">
|
|
357
|
-
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
357
|
+
import { computed, provide, ref, shallowReactive, useSlots, watch, useTemplateRef, type Component, type ComputedRef, type Ref } from 'vue'
|
|
358
358
|
import { useRouteQuery } from '@vueuse/router'
|
|
359
359
|
import { useRoute } from 'vue-router'
|
|
360
360
|
import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
@@ -365,6 +365,7 @@ import { configKey, forEachActiveCustomFilter, isCustomFilterActive, searchFilte
|
|
|
365
365
|
import { useStableQueryParams } from '../../composables/useStableQueryParams'
|
|
366
366
|
import { useComponentsConfig } from '../../config'
|
|
367
367
|
import { useFetch } from '../../functions/api'
|
|
368
|
+
import { isEqualExcept } from '../../functions/helpers'
|
|
368
369
|
import { scrollToBlockTop } from '../../functions/scroll'
|
|
369
370
|
import type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
370
371
|
import type { Dataset } from '../../types/datasets'
|
|
@@ -590,15 +591,21 @@ const stableParamsOptions = {
|
|
|
590
591
|
sort,
|
|
591
592
|
page,
|
|
592
593
|
pageSize,
|
|
594
|
+
currentType,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Refs produced by the fetch, before the counts loading state is attached.
|
|
598
|
+
type SearchResultRefs<C extends SearchType> = {
|
|
599
|
+
class: C
|
|
600
|
+
data: Ref<SearchResponseByClass[C] | null>
|
|
601
|
+
status: Ref<AsyncDataRequestStatus>
|
|
593
602
|
}
|
|
594
603
|
|
|
595
604
|
// Discriminated union: each variant carries its own response type so a `class`
|
|
596
605
|
// narrow gives the precise shape of `data.value` (no cast needed).
|
|
597
606
|
type SearchEntry = {
|
|
598
|
-
[K in SearchType]: {
|
|
599
|
-
|
|
600
|
-
data: Ref<SearchResponseByClass[K] | null>
|
|
601
|
-
status: Ref<AsyncDataRequestStatus>
|
|
607
|
+
[K in SearchType]: SearchResultRefs<K> & {
|
|
608
|
+
countsLoading: ComputedRef<boolean>
|
|
602
609
|
}
|
|
603
610
|
}[SearchType]
|
|
604
611
|
|
|
@@ -612,7 +619,7 @@ type SearchStrategy<C extends SearchType> = {
|
|
|
612
619
|
fetch: (
|
|
613
620
|
params: Ref<Record<string, unknown>>,
|
|
614
621
|
server: boolean,
|
|
615
|
-
) => Promise<
|
|
622
|
+
) => Promise<SearchResultRefs<C>>
|
|
616
623
|
}
|
|
617
624
|
|
|
618
625
|
function makeStrategy<C extends SearchType>(
|
|
@@ -626,9 +633,9 @@ function makeStrategy<C extends SearchType>(
|
|
|
626
633
|
meta.url,
|
|
627
634
|
{ params, lazy: true, server },
|
|
628
635
|
)
|
|
629
|
-
// Tautologically equivalent to
|
|
630
|
-
//
|
|
631
|
-
return { class: cls, data, status } as
|
|
636
|
+
// Tautologically equivalent to SearchResultRefs<C>, but TS cannot prove
|
|
637
|
+
// it on a generic C, so we assert.
|
|
638
|
+
return { class: cls, data, status } as SearchResultRefs<C>
|
|
632
639
|
},
|
|
633
640
|
}
|
|
634
641
|
}
|
|
@@ -666,12 +673,37 @@ const strategies: { [K in SearchType]: SearchStrategy<K> } = {
|
|
|
666
673
|
}),
|
|
667
674
|
}
|
|
668
675
|
|
|
676
|
+
// `sort`, `page` and `page_size` change which results come back, never how many
|
|
677
|
+
// there are nor how the API aggregates them: a refetch that touches only those
|
|
678
|
+
// cannot change a single count we display.
|
|
679
|
+
const COUNT_INVARIANT_PARAMS = ['sort', 'page', 'page_size']
|
|
680
|
+
|
|
669
681
|
// One params + fetch per config entry, keyed by configKey
|
|
670
682
|
const resultsMap: Record<string, SearchEntry> = {}
|
|
671
683
|
for (const c of props.config) {
|
|
672
684
|
const key = configKey(c)
|
|
673
685
|
const params = useStableQueryParams({ ...stableParamsOptions, typeConfig: c })
|
|
674
|
-
|
|
686
|
+
const result = await strategies[c.class].fetch(params, initialType === key)
|
|
687
|
+
|
|
688
|
+
// A reload that only moves sort or pagination leaves every count where it is,
|
|
689
|
+
// so it must not replace them with loading dots. The comparison is against the
|
|
690
|
+
// params the displayed counts came from, so that two changes in a row cannot
|
|
691
|
+
// clear the loading state owed to the first one. `data` already holds a value
|
|
692
|
+
// for the type rendered on the server; no applied params means the initial
|
|
693
|
+
// load, which does need its loading state.
|
|
694
|
+
const appliedParams = ref(result.data.value ? params.value : null)
|
|
695
|
+
watch(result.data, () => {
|
|
696
|
+
appliedParams.value = params.value
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
const countsCanChange = computed(() => !appliedParams.value
|
|
700
|
+
|| !isEqualExcept(params.value, appliedParams.value, COUNT_INVARIANT_PARAMS))
|
|
701
|
+
|
|
702
|
+
const countsLoading = computed(() => countsCanChange.value
|
|
703
|
+
&& (result.status.value === 'pending' || result.status.value === 'idle'))
|
|
704
|
+
|
|
705
|
+
// Same generic-C limitation as in makeStrategy: the union is narrowed by key.
|
|
706
|
+
resultsMap[key] = { ...result, countsLoading } as SearchEntry
|
|
675
707
|
}
|
|
676
708
|
|
|
677
709
|
// Reset page on filter/sort change. Custom filters (registered via
|
|
@@ -792,6 +824,7 @@ const rssUrl = computed(() => {
|
|
|
792
824
|
|
|
793
825
|
// Facets for filters
|
|
794
826
|
const currentFacets = computed(() => searchResults.value?.facets)
|
|
827
|
+
const facetsLoading = computed(() => resultsMap[currentType.value]?.countsLoading.value ?? false)
|
|
795
828
|
|
|
796
829
|
function getFacets(key: string): FacetItem[] | undefined {
|
|
797
830
|
if (!currentFacets.value) return undefined
|
|
@@ -12,18 +12,23 @@ interface StableQueryParamsOptions {
|
|
|
12
12
|
sort: Ref<string | undefined>
|
|
13
13
|
page: Ref<number>
|
|
14
14
|
pageSize: number
|
|
15
|
+
currentType: Ref<string>
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Creates a stable ref for query params that only updates when content actually changes.
|
|
19
20
|
* Applies hiddenFilters first, then user filters (which can override hiddenFilters).
|
|
21
|
+
* Sort and pagination are only sent for the displayed type, so changing them
|
|
22
|
+
* refetches that type alone.
|
|
20
23
|
*/
|
|
21
24
|
export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
22
|
-
const { typeConfig, allFilters, customFilterRegistry, q, sort, page, pageSize } = options
|
|
25
|
+
const { typeConfig, allFilters, customFilterRegistry, q, sort, page, pageSize, currentType } = options
|
|
23
26
|
const stableParams = ref<Record<string, unknown>>({})
|
|
24
27
|
|
|
25
28
|
const buildParams = () => {
|
|
26
29
|
const params: Record<string, unknown> = {}
|
|
30
|
+
const typeKey = typeConfig ? configKey(typeConfig) : undefined
|
|
31
|
+
const isDisplayedType = typeKey === undefined || typeKey === currentType.value
|
|
27
32
|
|
|
28
33
|
// 1. Apply hiddenFilters first (can be overridden by user filters)
|
|
29
34
|
if (typeConfig?.hiddenFilters) {
|
|
@@ -57,7 +62,6 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
57
62
|
// combines with an existing built-in value instead of overwriting it.
|
|
58
63
|
// Pass the current type key so filters scoped to specific types are excluded
|
|
59
64
|
// from background fetches for other types.
|
|
60
|
-
const currentTypeKey = typeConfig ? configKey(typeConfig) : undefined
|
|
61
65
|
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
62
66
|
const existing = params[apiParam]
|
|
63
67
|
if (existing === undefined) {
|
|
@@ -66,24 +70,31 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
66
70
|
else {
|
|
67
71
|
params[apiParam] = Array.isArray(existing) ? [...existing, value] : [existing, value]
|
|
68
72
|
}
|
|
69
|
-
},
|
|
73
|
+
}, typeKey)
|
|
70
74
|
|
|
71
|
-
// 4. Always include q
|
|
75
|
+
// 4. Always include q
|
|
72
76
|
if (q.value) {
|
|
73
77
|
params.q = q.value
|
|
74
78
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
|
|
80
|
+
// 5. Sort and pagination only apply to the displayed type. The other tabs
|
|
81
|
+
// are fetched for their `total` alone, which is invariant under sort and
|
|
82
|
+
// page, so sending those there would refetch every tab on each sort or
|
|
83
|
+
// page change. They ask for a single result for the same reason.
|
|
84
|
+
if (isDisplayedType) {
|
|
85
|
+
const sortToUse = sort.value ?? typeConfig?.defaultSort
|
|
86
|
+
if (sortToUse) {
|
|
87
|
+
const validSortValues = typeConfig?.sortOptions?.map(o => o.value as string) ?? []
|
|
88
|
+
if (validSortValues.includes(sortToUse)) {
|
|
89
|
+
params.sort = sortToUse
|
|
90
|
+
}
|
|
91
|
+
else if (import.meta.env.DEV && typeConfig?.defaultSort && typeConfig?.sortOptions && sortToUse === typeConfig.defaultSort) {
|
|
92
|
+
console.warn(`[GlobalSearch] defaultSort "${typeConfig.defaultSort}" is not in sortOptions for "${typeConfig.class}". Valid values: ${validSortValues.join(', ')}`)
|
|
93
|
+
}
|
|
83
94
|
}
|
|
95
|
+
params.page = page.value
|
|
84
96
|
}
|
|
85
|
-
params.
|
|
86
|
-
params.page_size = pageSize
|
|
97
|
+
params.page_size = isDisplayedType ? pageSize : 1
|
|
87
98
|
|
|
88
99
|
return params
|
|
89
100
|
}
|
|
@@ -100,7 +111,7 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
100
111
|
|
|
101
112
|
// Watch all dependencies and update only if content changed
|
|
102
113
|
watch(
|
|
103
|
-
[q, sort, page, ...Object.values(allFilters), customFilterValues],
|
|
114
|
+
[q, sort, page, currentType, ...Object.values(allFilters), customFilterValues],
|
|
104
115
|
() => {
|
|
105
116
|
const newParams = buildParams()
|
|
106
117
|
// JSON.stringify comparison is safe here because buildParams() builds the object deterministically
|
package/src/functions/helpers.ts
CHANGED
|
@@ -53,6 +53,16 @@ export const isSafeHttpUrl = (value: unknown): value is string => {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export const removeKeys = <T extends Record<string, unknown>>(object: T, keys: Array<string>): Partial<T> => {
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
Object.entries(object).filter(([key]) => !keys.includes(key)),
|
|
59
|
+
) as Partial<T>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const isEqualExcept = <T extends Record<string, unknown>>(a: T, b: T, keys: Array<string>): boolean => {
|
|
63
|
+
return JSON.stringify(removeKeys(a, keys)) === JSON.stringify(removeKeys(b, keys))
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
export const escapeCsvValue = (value: string | number | null | undefined): string => {
|
|
57
67
|
if (value === null || value === undefined || value === '') {
|
|
58
68
|
return ''
|