@datagouv/components-next 1.0.2-dev.11 → 1.0.2-dev.111
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/assets/main.css +4 -0
- package/dist/{Control-DuZJdKV_.js → Control-ZFh5ta_U.js} +1 -1
- package/dist/{Datafair.client-8haHXl47.js → Datafair.client-CKB2P_X1.js} +1 -1
- package/dist/{Event--kp8kMdJ.js → Event-DSQcW7OF.js} +24 -24
- package/dist/{Image-34hvypZI.js → Image-BijNEG0p.js} +6 -6
- package/dist/JsonPreview.client-Bx11-jfT.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BUtPf5GN.js} +756 -756
- package/dist/{MapContainer.client-l6HuXTHR.js → MapContainer.client-CdZSeT_L.js} +37 -38
- package/dist/{OSM-s40W6sQ2.js → OSM-D4MTdBtk.js} +2 -2
- package/dist/{PdfPreview.client-4OueK-2Z.js → PdfPreview.client-Bh9lP-qU.js} +822 -850
- package/dist/{Pmtiles.client-4j3VTYkz.js → Pmtiles.client-Bi46wN14.js} +1 -1
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BnC7vWGP.js +61 -0
- package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-hJQIqcZm.js} +2 -2
- package/dist/{Tile-DbNFNPfU.js → Tile-Dcl7oIVu.js} +35 -35
- package/dist/{TileImage-BsXBxMtq.js → TileImage-BJeHipMX.js} +4 -4
- package/dist/{View-BR92hTWP.js → View-xp_P_OHw.js} +412 -401
- package/dist/XmlPreview.client-oFAOv828.js +34 -0
- package/dist/{common-PJfpC179.js → common-BjQlan3k.js} +36 -36
- package/dist/components-next.css +6 -6
- package/dist/components-next.js +165 -142
- package/dist/components.css +1 -1
- package/dist/{index-CVTIoZQ0.js → index-CxCuKQ81.js} +32886 -27183
- package/dist/main-CQ9ZQG7n.js +73607 -0
- package/dist/{proj-DsetBcW7.js → proj-CsNo9yH1.js} +532 -512
- package/dist/{tilecoord-Db24Px13.js → tilecoord-A0fLnBZr.js} +28 -28
- package/dist/{vue3-xml-viewer.common-CWer_T5-.js → vue3-xml-viewer.common-B9qp90K_.js} +1 -1
- package/package.json +25 -11
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +3 -2
- package/src/components/Chart/ChartViewer.vue +226 -0
- package/src/components/Chart/ChartViewerWrapper.vue +170 -0
- package/src/components/DataserviceCard.vue +3 -0
- package/src/components/DatasetCard.vue +9 -4
- package/src/components/Form/Listbox.vue +101 -0
- package/src/components/Form/SearchableSelect.vue +2 -1
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/ObjectCardHeader.vue +11 -4
- package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
- package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
- package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
- package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
- package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
- package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
- package/src/components/OpenApiViewer/openapi.ts +150 -0
- package/src/components/OrganizationNameWithCertificate.vue +3 -2
- package/src/components/Pagination.vue +8 -5
- package/src/components/RadioInput.vue +7 -2
- package/src/components/ReadMore.vue +1 -1
- package/src/components/ResourceAccordion/DataStructure.vue +11 -33
- package/src/components/ResourceAccordion/Downloads.vue +160 -0
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
- package/src/components/ResourceAccordion/Metadata.vue +1 -2
- package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -87
- package/src/components/ResourceAccordion/Preview.vue +11 -11
- package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +11 -110
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- package/src/components/ResourceExplorer/ResourceExplorer.vue +14 -10
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +50 -148
- package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
- package/src/components/ReuseCard.vue +12 -4
- package/src/components/Search/GlobalSearch.vue +201 -113
- package/src/components/Search/SearchInput.vue +5 -4
- package/src/components/TabularExplorer/TabularCell.vue +51 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +973 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
- package/src/components/TabularExplorer/types.ts +83 -0
- package/src/composables/useHasTabularData.ts +13 -0
- package/src/composables/useMetrics.ts +1 -1
- package/src/composables/useResourceCapabilities.ts +1 -1
- package/src/composables/useSearchFilter.ts +118 -0
- package/src/composables/useStableQueryParams.ts +38 -6
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/config.ts +20 -3
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +9 -37
- package/src/functions/api.types.ts +1 -0
- package/src/functions/charts.ts +68 -0
- package/src/functions/datasets.ts +0 -17
- package/src/functions/metrics.ts +6 -4
- package/src/functions/resources.ts +56 -1
- package/src/functions/tabular.ts +60 -0
- package/src/functions/tabularApi.ts +138 -11
- package/src/main.ts +90 -9
- package/src/types/dataservices.ts +2 -0
- package/src/types/pages.ts +0 -5
- package/src/types/posts.ts +2 -2
- package/src/types/reports.ts +5 -1
- package/src/types/search.ts +63 -1
- package/src/types/site.ts +5 -3
- package/src/types/ui.ts +2 -0
- package/src/types/users.ts +2 -1
- package/src/types/visualizations.ts +89 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/JsonPreview.client-D53pj9Cw.js +0 -72
- package/dist/Swagger.client-DPBmsH9q.js +0 -4
- package/dist/XmlPreview.client-XElkoA4F.js +0 -64
- package/dist/main-BbT-LUXy.js +0 -105854
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
@submit.prevent
|
|
6
6
|
>
|
|
7
7
|
<div
|
|
8
|
+
v-if="!hideSearchInput"
|
|
8
9
|
ref="search"
|
|
9
10
|
class="flex flex-wrap items-center justify-between"
|
|
10
11
|
data-cy="search"
|
|
11
12
|
>
|
|
12
13
|
<SearchInput
|
|
13
14
|
v-model="q"
|
|
14
|
-
:placeholder="
|
|
15
|
+
:placeholder="resolvedPlaceholder"
|
|
16
|
+
:auto-focus
|
|
15
17
|
/>
|
|
16
18
|
</div>
|
|
17
19
|
<div class="grid grid-cols-12 mt-2 md:mt-5">
|
|
@@ -30,23 +32,27 @@
|
|
|
30
32
|
>
|
|
31
33
|
<RadioInput
|
|
32
34
|
v-for="typeConfig in config"
|
|
33
|
-
:key="typeConfig
|
|
34
|
-
:value="typeConfig
|
|
35
|
-
:count="
|
|
36
|
-
:loading="
|
|
37
|
-
:icon="
|
|
35
|
+
:key="configKey(typeConfig)"
|
|
36
|
+
:value="configKey(typeConfig)"
|
|
37
|
+
:count="resultsMap[configKey(typeConfig)]?.data.value?.total"
|
|
38
|
+
:loading="resultsMap[configKey(typeConfig)]?.status.value === 'pending' || resultsMap[configKey(typeConfig)]?.status.value === 'idle'"
|
|
39
|
+
:icon="typeConfig.icon ?? strategies[typeConfig.class].icon"
|
|
38
40
|
>
|
|
39
|
-
{{ typeConfig.name ||
|
|
41
|
+
{{ typeConfig.name || strategies[typeConfig.class].name }}
|
|
40
42
|
</RadioInput>
|
|
41
43
|
</RadioGroup>
|
|
42
44
|
</Sidemenu>
|
|
43
45
|
</div>
|
|
44
46
|
|
|
45
|
-
<div v-if="activeFilters.length > 0">
|
|
47
|
+
<div v-if="activeFilters.length > 0 || $slots['custom-filters-top'] || $slots['custom-filters-bottom']">
|
|
46
48
|
<Sidemenu :button-text="t('Filtres')">
|
|
47
49
|
<template #title>
|
|
48
50
|
{{ t('Filtres') }}
|
|
49
51
|
</template>
|
|
52
|
+
<slot
|
|
53
|
+
name="custom-filters-top"
|
|
54
|
+
:current-type="currentType"
|
|
55
|
+
/>
|
|
50
56
|
<BasicAndAdvancedFilters
|
|
51
57
|
v-slot="{ isEnabled, getOrder }"
|
|
52
58
|
:basic-filters="activeBasicFilters"
|
|
@@ -123,7 +129,7 @@
|
|
|
123
129
|
v-model="producerType"
|
|
124
130
|
:facets="getFacets('producer_type')"
|
|
125
131
|
:loading="searchResultsStatus === 'pending'"
|
|
126
|
-
:exclude="
|
|
132
|
+
:exclude="currentTypeConfig?.class === 'organizations' ? ['user'] : []"
|
|
127
133
|
:style="{ order: getOrder('producer_type') }"
|
|
128
134
|
/>
|
|
129
135
|
<DatasetBadgeFilter
|
|
@@ -146,6 +152,10 @@
|
|
|
146
152
|
:get-order="getOrder"
|
|
147
153
|
/>
|
|
148
154
|
</BasicAndAdvancedFilters>
|
|
155
|
+
<slot
|
|
156
|
+
name="custom-filters-bottom"
|
|
157
|
+
:current-type="currentType"
|
|
158
|
+
/>
|
|
149
159
|
<div
|
|
150
160
|
v-if="hasFilters"
|
|
151
161
|
class="mt-6 text-center"
|
|
@@ -175,6 +185,7 @@
|
|
|
175
185
|
<p
|
|
176
186
|
class="fr-col-auto my-0"
|
|
177
187
|
role="status"
|
|
188
|
+
data-testid="search-result-count"
|
|
178
189
|
>
|
|
179
190
|
{{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
|
|
180
191
|
</p>
|
|
@@ -189,10 +200,13 @@
|
|
|
189
200
|
<div class="fr-col">
|
|
190
201
|
<select
|
|
191
202
|
id="sort-search"
|
|
192
|
-
v-model="
|
|
203
|
+
v-model="effectiveSort"
|
|
193
204
|
class="fr-select text-sm shadow-input-blue!"
|
|
194
205
|
>
|
|
195
|
-
<option
|
|
206
|
+
<option
|
|
207
|
+
v-if="!currentTypeConfig?.defaultSort"
|
|
208
|
+
:value="undefined"
|
|
209
|
+
>
|
|
196
210
|
{{ t('Pertinence') }}
|
|
197
211
|
</option>
|
|
198
212
|
<option
|
|
@@ -221,7 +235,7 @@
|
|
|
221
235
|
<transition mode="out-in">
|
|
222
236
|
<LoadingBlock
|
|
223
237
|
v-slot="{ data: results }"
|
|
224
|
-
:status="searchResultsStatus"
|
|
238
|
+
:status="searchResultsStatus!"
|
|
225
239
|
:data="searchResults"
|
|
226
240
|
>
|
|
227
241
|
<div v-if="results && results.data.length">
|
|
@@ -231,7 +245,7 @@
|
|
|
231
245
|
:key="result.id"
|
|
232
246
|
class="p-0"
|
|
233
247
|
>
|
|
234
|
-
<template v-if="
|
|
248
|
+
<template v-if="currentTypeConfig?.class === 'datasets'">
|
|
235
249
|
<slot
|
|
236
250
|
name="dataset"
|
|
237
251
|
:dataset="result"
|
|
@@ -239,7 +253,7 @@
|
|
|
239
253
|
<DatasetCard :dataset="(result as Dataset)" />
|
|
240
254
|
</slot>
|
|
241
255
|
</template>
|
|
242
|
-
<template v-else-if="
|
|
256
|
+
<template v-else-if="currentTypeConfig?.class === 'dataservices'">
|
|
243
257
|
<slot
|
|
244
258
|
name="dataservice"
|
|
245
259
|
:dataservice="result"
|
|
@@ -247,7 +261,7 @@
|
|
|
247
261
|
<DataserviceCard :dataservice="(result as Dataservice)" />
|
|
248
262
|
</slot>
|
|
249
263
|
</template>
|
|
250
|
-
<template v-else-if="
|
|
264
|
+
<template v-else-if="currentTypeConfig?.class === 'reuses'">
|
|
251
265
|
<slot
|
|
252
266
|
name="reuse"
|
|
253
267
|
:reuse="result"
|
|
@@ -255,7 +269,7 @@
|
|
|
255
269
|
<ReuseHorizontalCard :reuse="(result as Reuse)" />
|
|
256
270
|
</slot>
|
|
257
271
|
</template>
|
|
258
|
-
<template v-else-if="
|
|
272
|
+
<template v-else-if="currentTypeConfig?.class === 'organizations'">
|
|
259
273
|
<slot
|
|
260
274
|
name="organization"
|
|
261
275
|
:organization="result"
|
|
@@ -263,6 +277,14 @@
|
|
|
263
277
|
<OrganizationHorizontalCard :organization="(result as Organization)" />
|
|
264
278
|
</slot>
|
|
265
279
|
</template>
|
|
280
|
+
<template v-else-if="currentTypeConfig?.class === 'topics'">
|
|
281
|
+
<slot
|
|
282
|
+
name="topic"
|
|
283
|
+
:topic="result"
|
|
284
|
+
>
|
|
285
|
+
<TopicCard :topic="(result as TopicV2)" />
|
|
286
|
+
</slot>
|
|
287
|
+
</template>
|
|
266
288
|
</li>
|
|
267
289
|
</ul>
|
|
268
290
|
<Pagination
|
|
@@ -271,7 +293,6 @@
|
|
|
271
293
|
:page-size
|
|
272
294
|
:total-results="results.total"
|
|
273
295
|
class="mt-4"
|
|
274
|
-
:link="getLink"
|
|
275
296
|
@change="changePage"
|
|
276
297
|
/>
|
|
277
298
|
</div>
|
|
@@ -333,21 +354,23 @@
|
|
|
333
354
|
</template>
|
|
334
355
|
|
|
335
356
|
<script setup lang="ts">
|
|
336
|
-
import { computed, watch, useTemplateRef, type Ref } from 'vue'
|
|
357
|
+
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
337
358
|
import { useRouteQuery } from '@vueuse/router'
|
|
338
|
-
import { RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
359
|
+
import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
339
360
|
import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
|
|
340
361
|
import { useTranslation } from '../../composables/useTranslation'
|
|
341
362
|
import { useDebouncedRef } from '../../composables/useDebouncedRef'
|
|
363
|
+
import { configKey, forEachActiveCustomFilter, isCustomFilterActive, searchFilterContextKey, type CustomFilterEntry } from '../../composables/useSearchFilter'
|
|
342
364
|
import { useStableQueryParams } from '../../composables/useStableQueryParams'
|
|
343
365
|
import { useComponentsConfig } from '../../config'
|
|
344
366
|
import { useFetch } from '../../functions/api'
|
|
345
|
-
import {
|
|
367
|
+
import type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
346
368
|
import type { Dataset } from '../../types/datasets'
|
|
347
369
|
import type { Dataservice } from '../../types/dataservices'
|
|
348
370
|
import type { Organization } from '../../types/organizations'
|
|
349
371
|
import type { Reuse } from '../../types/reuses'
|
|
350
|
-
import type {
|
|
372
|
+
import type { TopicV2 } from '../../types/topics'
|
|
373
|
+
import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SortOption, FacetItem } from '../../types/search'
|
|
351
374
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
352
375
|
import BrandedButton from '../BrandedButton.vue'
|
|
353
376
|
import LoadingBlock from '../LoadingBlock.vue'
|
|
@@ -358,6 +381,7 @@ import DatasetCard from '../DatasetCard.vue'
|
|
|
358
381
|
import DataserviceCard from '../DataserviceCard.vue'
|
|
359
382
|
import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
|
|
360
383
|
import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
|
|
384
|
+
import TopicCard from '../TopicCard.vue'
|
|
361
385
|
import SearchInput from './SearchInput.vue'
|
|
362
386
|
import Sidemenu from './Sidemenu.vue'
|
|
363
387
|
import BasicAndAdvancedFilters from './BasicAndAdvancedFilters.vue'
|
|
@@ -379,25 +403,49 @@ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
|
|
|
379
403
|
|
|
380
404
|
const props = withDefaults(defineProps<{
|
|
381
405
|
config?: GlobalSearchConfig
|
|
382
|
-
placeholder?: string
|
|
406
|
+
placeholder?: string | null
|
|
407
|
+
hideSearchInput?: boolean
|
|
408
|
+
autoFocus?: boolean
|
|
383
409
|
}>(), {
|
|
384
410
|
config: getDefaultGlobalSearchConfig,
|
|
411
|
+
hideSearchInput: false,
|
|
412
|
+
autoFocus: true,
|
|
385
413
|
})
|
|
386
414
|
|
|
415
|
+
const emit = defineEmits<{
|
|
416
|
+
resultsCount: [total: number]
|
|
417
|
+
}>()
|
|
418
|
+
|
|
387
419
|
// defineModel's default is static and can't depend on props, so we cast and initialize manually
|
|
388
|
-
const currentType = defineModel<
|
|
389
|
-
if (!currentType.value) currentType.value = props.config[0]
|
|
420
|
+
const currentType = defineModel<string>('type') as Ref<string>
|
|
421
|
+
if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class: 'datasets' })
|
|
390
422
|
|
|
391
423
|
const { t } = useTranslation()
|
|
392
424
|
const componentsConfig = useComponentsConfig()
|
|
393
425
|
|
|
426
|
+
// Custom filter registry for useSearchFilter composable
|
|
427
|
+
const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
|
|
428
|
+
// Per-filter watch stoppers: each registered filter gets its own watcher so a
|
|
429
|
+
// value change resets page to 1, but registration itself does not (the value
|
|
430
|
+
// came from the URL, not from a user action).
|
|
431
|
+
const customFilterStops = new Map<string, () => void>()
|
|
432
|
+
|
|
394
433
|
// Initial type is used to determine which fetch should be SSR (non-lazy)
|
|
395
434
|
const initialType = currentType.value
|
|
396
435
|
|
|
397
436
|
const currentTypeConfig = computed(() =>
|
|
398
|
-
props.config.find(c => c
|
|
437
|
+
props.config.find(c => configKey(c) === currentType.value),
|
|
399
438
|
)
|
|
400
439
|
|
|
440
|
+
// Precedence: prop → per-type config → strategy default.
|
|
441
|
+
// null at any level means "no placeholder".
|
|
442
|
+
const resolvedPlaceholder = computed(() => {
|
|
443
|
+
if (props.placeholder !== undefined) return props.placeholder ?? ''
|
|
444
|
+
const cfg = currentTypeConfig.value
|
|
445
|
+
if (cfg && 'placeholder' in cfg) return cfg.placeholder ?? ''
|
|
446
|
+
return strategies[cfg?.class ?? 'datasets'].placeholder
|
|
447
|
+
})
|
|
448
|
+
|
|
401
449
|
const activeBasicFilters = computed(() =>
|
|
402
450
|
(currentTypeConfig.value?.basicFilters ?? []) as string[],
|
|
403
451
|
)
|
|
@@ -431,13 +479,35 @@ const activeFilters = computed(() => [
|
|
|
431
479
|
...(currentTypeConfig.value?.advancedFilters ?? []),
|
|
432
480
|
] as string[])
|
|
433
481
|
|
|
434
|
-
const
|
|
482
|
+
const slots = useSlots()
|
|
483
|
+
const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0 || !!slots['custom-filters-top'] || !!slots['custom-filters-bottom'])
|
|
435
484
|
|
|
436
485
|
// URL query params
|
|
437
486
|
const q = useRouteQuery<string>('q', '')
|
|
438
487
|
const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsConfig.searchDebounce ?? 300)
|
|
488
|
+
// When the search input is hidden, the parent owns the input and is expected
|
|
489
|
+
// to debounce user typing itself (otherwise typing would land in the URL
|
|
490
|
+
// instantly via v-model and stack two debounces). Bypass the internal debounce
|
|
491
|
+
// so URL-driven q changes hit the fetch params immediately.
|
|
492
|
+
const qForParams = computed(() => props.hideSearchInput ? q.value : qDebounced.value)
|
|
439
493
|
const page = useRouteQuery('page', 1, { transform: Number })
|
|
440
494
|
const sort = useRouteQuery<string | undefined>('sort')
|
|
495
|
+
const effectiveSort = computed({
|
|
496
|
+
get: () => sort.value ?? currentTypeConfig.value?.defaultSort,
|
|
497
|
+
set: (value) => { sort.value = value },
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
provide(searchFilterContextKey, {
|
|
501
|
+
register(urlParam, entry) {
|
|
502
|
+
customFilterRegistry.set(urlParam, entry)
|
|
503
|
+
customFilterStops.set(urlParam, watch(entry.ref, () => page.value = 1))
|
|
504
|
+
},
|
|
505
|
+
unregister(urlParam) {
|
|
506
|
+
customFilterStops.get(urlParam)?.()
|
|
507
|
+
customFilterStops.delete(urlParam)
|
|
508
|
+
customFilterRegistry.delete(urlParam)
|
|
509
|
+
},
|
|
510
|
+
})
|
|
441
511
|
|
|
442
512
|
// Filter values
|
|
443
513
|
const organizationId = useRouteQuery<string | undefined>('organization')
|
|
@@ -495,47 +565,104 @@ watch(currentType, () => {
|
|
|
495
565
|
}
|
|
496
566
|
})
|
|
497
567
|
|
|
498
|
-
// Check which types are enabled
|
|
499
|
-
const datasetsEnabled = computed(() => props.config.some(c => c.class === 'datasets'))
|
|
500
|
-
const dataservicesEnabled = computed(() => props.config.some(c => c.class === 'dataservices'))
|
|
501
|
-
const reusesEnabled = computed(() => props.config.some(c => c.class === 'reuses'))
|
|
502
|
-
const organizationsEnabled = computed(() => props.config.some(c => c.class === 'organizations'))
|
|
503
|
-
|
|
504
568
|
// Create stable params for each type
|
|
505
569
|
const stableParamsOptions = {
|
|
506
570
|
allFilters,
|
|
507
|
-
|
|
571
|
+
customFilterRegistry,
|
|
572
|
+
q: qForParams,
|
|
508
573
|
sort,
|
|
509
574
|
page,
|
|
510
575
|
pageSize,
|
|
511
576
|
}
|
|
512
577
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
578
|
+
// Discriminated union: each variant carries its own response type so a `class`
|
|
579
|
+
// narrow gives the precise shape of `data.value` (no cast needed).
|
|
580
|
+
type SearchEntry = {
|
|
581
|
+
[K in SearchType]: {
|
|
582
|
+
class: K
|
|
583
|
+
data: Ref<SearchResponseByClass[K] | null>
|
|
584
|
+
status: Ref<AsyncDataRequestStatus>
|
|
585
|
+
}
|
|
586
|
+
}[SearchType]
|
|
587
|
+
|
|
588
|
+
// One strategy per class consolidates everything that varies by class:
|
|
589
|
+
// metadata (icon/name/placeholder), endpoint, and a typed fetch factory.
|
|
590
|
+
type SearchStrategy<C extends SearchType> = {
|
|
591
|
+
url: string
|
|
592
|
+
icon: Component
|
|
593
|
+
name: string
|
|
594
|
+
placeholder: string
|
|
595
|
+
fetch: (
|
|
596
|
+
params: Ref<Record<string, unknown>>,
|
|
597
|
+
server: boolean,
|
|
598
|
+
) => Promise<Extract<SearchEntry, { class: C }>>
|
|
599
|
+
}
|
|
529
600
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
601
|
+
function makeStrategy<C extends SearchType>(
|
|
602
|
+
cls: C,
|
|
603
|
+
meta: Omit<SearchStrategy<C>, 'fetch'>,
|
|
604
|
+
): SearchStrategy<C> {
|
|
605
|
+
return {
|
|
606
|
+
...meta,
|
|
607
|
+
fetch: async (params, server) => {
|
|
608
|
+
const { data, status } = await useFetch<SearchResponseByClass[C]>(
|
|
609
|
+
meta.url,
|
|
610
|
+
{ params, lazy: true, server },
|
|
611
|
+
)
|
|
612
|
+
// Tautologically equivalent to Extract<SearchEntry, { class: C }>, but TS
|
|
613
|
+
// cannot prove it on a generic C, so we assert.
|
|
614
|
+
return { class: cls, data, status } as Extract<SearchEntry, { class: C }>
|
|
615
|
+
},
|
|
616
|
+
}
|
|
617
|
+
}
|
|
535
618
|
|
|
536
|
-
|
|
619
|
+
const strategies: { [K in SearchType]: SearchStrategy<K> } = {
|
|
620
|
+
datasets: makeStrategy('datasets', {
|
|
621
|
+
url: '/api/2/datasets/search/',
|
|
622
|
+
icon: RiDatabase2Line,
|
|
623
|
+
name: t('Jeux de données'),
|
|
624
|
+
placeholder: t('ex. élections présidentielles'),
|
|
625
|
+
}),
|
|
626
|
+
dataservices: makeStrategy('dataservices', {
|
|
627
|
+
url: '/api/2/dataservices/search/',
|
|
628
|
+
icon: RiTerminalLine,
|
|
629
|
+
name: t('API'),
|
|
630
|
+
placeholder: t('ex: SIRENE'),
|
|
631
|
+
}),
|
|
632
|
+
reuses: makeStrategy('reuses', {
|
|
633
|
+
url: '/api/2/reuses/search/',
|
|
634
|
+
icon: RiLineChartLine,
|
|
635
|
+
name: t('Réutilisations'),
|
|
636
|
+
placeholder: t('Rechercher une réutilisation de données'),
|
|
637
|
+
}),
|
|
638
|
+
organizations: makeStrategy('organizations', {
|
|
639
|
+
url: '/api/2/organizations/search/',
|
|
640
|
+
icon: RiBuilding2Line,
|
|
641
|
+
name: t('Organisations'),
|
|
642
|
+
placeholder: t('Rechercher une organisation'),
|
|
643
|
+
}),
|
|
644
|
+
topics: makeStrategy('topics', {
|
|
645
|
+
url: '/api/2/topics/search/',
|
|
646
|
+
icon: RiBookShelfLine,
|
|
647
|
+
name: t('Thématiques'),
|
|
648
|
+
placeholder: t('Rechercher une thématique'),
|
|
649
|
+
}),
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// One params + fetch per config entry, keyed by configKey
|
|
653
|
+
const resultsMap: Record<string, SearchEntry> = {}
|
|
654
|
+
for (const c of props.config) {
|
|
655
|
+
const key = configKey(c)
|
|
656
|
+
const params = useStableQueryParams({ ...stableParamsOptions, typeConfig: c })
|
|
657
|
+
resultsMap[key] = await strategies[c.class].fetch(params, initialType === key)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Reset page on filter/sort change. Custom filters (registered via
|
|
661
|
+
// useSearchFilter) have their own watchers set up in `provide`, so they're
|
|
662
|
+
// intentionally excluded here to avoid resetting the page when a filter
|
|
663
|
+
// registers with its URL-derived value.
|
|
537
664
|
const filtersForReset = computed(() => ({
|
|
538
|
-
q:
|
|
665
|
+
q: qForParams.value,
|
|
539
666
|
organization: organizationId.value,
|
|
540
667
|
organization_badge: organizationType.value,
|
|
541
668
|
tag: tag.value,
|
|
@@ -573,6 +700,7 @@ const hasFilters = computed(() => {
|
|
|
573
700
|
|| lastUpdateRange.value
|
|
574
701
|
|| producerType.value
|
|
575
702
|
|| reuseType.value
|
|
703
|
+
|| Array.from(customFilterRegistry.values()).some(isCustomFilterActive)
|
|
576
704
|
})
|
|
577
705
|
|
|
578
706
|
const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
|
|
@@ -593,79 +721,35 @@ function resetFilters() {
|
|
|
593
721
|
lastUpdateRange.value = undefined
|
|
594
722
|
producerType.value = undefined
|
|
595
723
|
reuseType.value = undefined
|
|
724
|
+
for (const entry of customFilterRegistry.values()) {
|
|
725
|
+
entry.ref.value = entry.defaultValue
|
|
726
|
+
}
|
|
596
727
|
q.value = ''
|
|
597
728
|
flushQ()
|
|
598
729
|
}
|
|
599
730
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const { data: datasetsResults, status: datasetsStatus } = await useFetch<DatasetSearchResponse<Dataset>>(
|
|
603
|
-
datasetsUrl,
|
|
604
|
-
{ params: datasetsParams, lazy: true, server: initialType === 'datasets' },
|
|
605
|
-
)
|
|
606
|
-
const { data: dataservicesResults, status: dataservicesStatus } = await useFetch<DataserviceSearchResponse<Dataservice>>(
|
|
607
|
-
dataservicesUrl,
|
|
608
|
-
{ params: dataservicesParams, lazy: true, server: initialType === 'dataservices' },
|
|
609
|
-
)
|
|
610
|
-
const { data: reusesResults, status: reusesStatus } = await useFetch<ReuseSearchResponse<Reuse>>(
|
|
611
|
-
reusesUrl,
|
|
612
|
-
{ params: reusesParams, lazy: true, server: initialType === 'reuses' },
|
|
613
|
-
)
|
|
614
|
-
const { data: organizationsResults, status: organizationsStatus } = await useFetch<OrganizationSearchResponse<Organization>>(
|
|
615
|
-
organizationsUrl,
|
|
616
|
-
{ params: organizationsParams, lazy: true, server: initialType === 'organizations' },
|
|
617
|
-
)
|
|
618
|
-
|
|
619
|
-
const typesMeta = {
|
|
620
|
-
datasets: {
|
|
621
|
-
icon: RiDatabase2Line,
|
|
622
|
-
name: t('Jeux de données'),
|
|
623
|
-
placeholder: t('ex. élections présidentielles'),
|
|
624
|
-
results: datasetsResults,
|
|
625
|
-
status: datasetsStatus,
|
|
626
|
-
},
|
|
627
|
-
dataservices: {
|
|
628
|
-
icon: RiTerminalLine,
|
|
629
|
-
name: t('API'),
|
|
630
|
-
placeholder: t('ex: SIRENE'),
|
|
631
|
-
results: dataservicesResults,
|
|
632
|
-
status: dataservicesStatus,
|
|
633
|
-
},
|
|
634
|
-
reuses: {
|
|
635
|
-
icon: RiLineChartLine,
|
|
636
|
-
name: t('Réutilisations'),
|
|
637
|
-
placeholder: t('Rechercher une réutilisation de données'),
|
|
638
|
-
results: reusesResults,
|
|
639
|
-
status: reusesStatus,
|
|
640
|
-
},
|
|
641
|
-
organizations: {
|
|
642
|
-
icon: RiBuilding2Line,
|
|
643
|
-
name: t('Organisations'),
|
|
644
|
-
placeholder: t('Rechercher une organisation'),
|
|
645
|
-
results: organizationsResults,
|
|
646
|
-
status: organizationsStatus,
|
|
647
|
-
},
|
|
648
|
-
} as const
|
|
731
|
+
const searchResults = computed(() => resultsMap[currentType.value]?.data.value)
|
|
732
|
+
const searchResultsStatus = computed(() => resultsMap[currentType.value]?.status.value)
|
|
649
733
|
|
|
650
|
-
|
|
651
|
-
|
|
734
|
+
watch(searchResults, (results) => {
|
|
735
|
+
if (results) emit('resultsCount', results.total)
|
|
736
|
+
}, { immediate: true })
|
|
652
737
|
|
|
653
738
|
// RSS feed URL for datasets
|
|
654
739
|
const rssUrl = computed(() => {
|
|
655
|
-
if (
|
|
740
|
+
if (currentTypeConfig.value?.class !== 'datasets') return null
|
|
656
741
|
|
|
657
742
|
const params = new URLSearchParams()
|
|
658
|
-
const datasetsConfig = props.config.find(c => c.class === 'datasets')
|
|
659
743
|
|
|
660
744
|
// Add hidden filters first
|
|
661
|
-
if (
|
|
662
|
-
for (const hf of
|
|
745
|
+
if (currentTypeConfig.value?.hiddenFilters) {
|
|
746
|
+
for (const hf of currentTypeConfig.value.hiddenFilters) {
|
|
663
747
|
if (hf?.value) params.set(hf.key as string, String(hf.value))
|
|
664
748
|
}
|
|
665
749
|
}
|
|
666
750
|
|
|
667
751
|
// Add active filters
|
|
668
|
-
if (
|
|
752
|
+
if (qForParams.value) params.set('q', qForParams.value)
|
|
669
753
|
if (organizationId.value) params.set('organization', organizationId.value)
|
|
670
754
|
if (organizationType.value) params.set('organization_badge', organizationType.value)
|
|
671
755
|
if (tag.value) params.set('tag', tag.value)
|
|
@@ -677,8 +761,12 @@ const rssUrl = computed(() => {
|
|
|
677
761
|
if (badge.value) params.set('badge', badge.value)
|
|
678
762
|
if (topic.value) params.set('topic', topic.value)
|
|
679
763
|
|
|
764
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
765
|
+
params.set(apiParam, value)
|
|
766
|
+
}, currentTypeConfig.value ? configKey(currentTypeConfig.value) : undefined)
|
|
767
|
+
|
|
680
768
|
// Add sort if set
|
|
681
|
-
if (
|
|
769
|
+
if (effectiveSort.value) params.set('sort', effectiveSort.value)
|
|
682
770
|
|
|
683
771
|
const queryString = params.toString()
|
|
684
772
|
const basePath = '/api/1/datasets/recent.atom'
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
type="search"
|
|
14
14
|
name="q"
|
|
15
15
|
class="input max-h-12 m-0 rounded-tl shadow-input-blue"
|
|
16
|
-
:aria-label="placeholder
|
|
17
|
-
:placeholder="placeholder
|
|
16
|
+
:aria-label="placeholder === null ? t('Rechercher...') : placeholder ?? t('Rechercher...')"
|
|
17
|
+
:placeholder="placeholder === null ? undefined : placeholder ?? t('Rechercher...')"
|
|
18
18
|
>
|
|
19
19
|
<BrandedButton
|
|
20
20
|
class="rounded-l-none rounded-br-none rounded-tr min-h-12"
|
|
@@ -37,8 +37,8 @@ import BrandedButton from '../BrandedButton.vue'
|
|
|
37
37
|
|
|
38
38
|
const q = defineModel<string>({ required: true })
|
|
39
39
|
|
|
40
|
-
withDefaults(defineProps<{
|
|
41
|
-
placeholder?: string
|
|
40
|
+
const props = withDefaults(defineProps<{
|
|
41
|
+
placeholder?: string | null
|
|
42
42
|
autoFocus?: boolean
|
|
43
43
|
}>(), {
|
|
44
44
|
autoFocus: true,
|
|
@@ -57,6 +57,7 @@ const focus = () => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
onMounted(async () => {
|
|
60
|
+
if (!props.autoFocus) return
|
|
60
61
|
await nextTick()
|
|
61
62
|
focus()
|
|
62
63
|
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
v-if="value == null || value === ''"
|
|
4
|
+
class="font-[Inconsolata,monospace] text-gray-low italic"
|
|
5
|
+
:class="compact ? 'text-xs' : 'text-sm'"
|
|
6
|
+
>null</span>
|
|
7
|
+
<span
|
|
8
|
+
v-else-if="columnType === 'boolean'"
|
|
9
|
+
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs"
|
|
10
|
+
:class="isTruthy(value) ? 'bg-new-success-light text-new-success' : 'bg-new-warning-light text-new-error'"
|
|
11
|
+
>
|
|
12
|
+
<span
|
|
13
|
+
class="size-2 rounded-full"
|
|
14
|
+
:class="isTruthy(value) ? 'bg-new-success' : 'bg-new-error'"
|
|
15
|
+
/>
|
|
16
|
+
{{ isTruthy(value) ? t('Vrai') : t('Faux') }}
|
|
17
|
+
</span>
|
|
18
|
+
<span
|
|
19
|
+
v-else-if="columnType === 'categorical'"
|
|
20
|
+
class="inline-block rounded font-medium px-2 py-0.5 text-xs max-w-full truncate"
|
|
21
|
+
:style="categoryBadgeStyle ? { backgroundColor: categoryBadgeStyle.backgroundColor, color: categoryBadgeStyle.color } : undefined"
|
|
22
|
+
>{{ value }}</span>
|
|
23
|
+
<span
|
|
24
|
+
v-else-if="columnType === 'number'"
|
|
25
|
+
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
26
|
+
>{{ formatNumber(value) }}</span>
|
|
27
|
+
<span
|
|
28
|
+
v-else-if="columnType === 'date'"
|
|
29
|
+
:class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
|
|
30
|
+
>{{ formatCellDate(value) }}</span>
|
|
31
|
+
<span
|
|
32
|
+
v-else
|
|
33
|
+
class="text-gray-title truncate block text-xs"
|
|
34
|
+
>{{ typeof value === 'object' ? JSON.stringify(value) : value }}</span>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
39
|
+
import { useFormatTabular, isTruthy } from '../../functions/tabular'
|
|
40
|
+
import type { ColumnType, BadgeStyle } from './types'
|
|
41
|
+
|
|
42
|
+
defineProps<{
|
|
43
|
+
value: unknown
|
|
44
|
+
columnType: ColumnType
|
|
45
|
+
categoryBadgeStyle?: BadgeStyle
|
|
46
|
+
compact?: boolean
|
|
47
|
+
}>()
|
|
48
|
+
|
|
49
|
+
const { t } = useTranslation()
|
|
50
|
+
const { formatNumber, formatCellDate } = useFormatTabular()
|
|
51
|
+
</script>
|