@datagouv/components-next 1.0.2-dev.8 → 1.0.2-dev.81
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/Datafair.client-BzW-ctDf.js +30 -0
- package/dist/JsonPreview.client-BfMSzR07.js +40 -0
- package/dist/{MapContainer.client-DRkAmdOc.js → MapContainer.client-CLs-im9i.js} +35 -38
- package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-C13PQCU_.js} +822 -865
- package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-CL7PXXDl.js} +574 -579
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C6XnsZ-7.js +61 -0
- package/dist/XmlPreview.client-KaENrbbG.js +34 -0
- package/dist/components-next.css +3 -3
- package/dist/components-next.js +166 -148
- package/dist/components.css +1 -1
- package/dist/{index-SrYZwgCT.js → index-C7WVVGgD.js} +1 -1
- package/dist/{main-B2kXxWRG.js → main-K-42Oe8-.js} +91315 -75834
- package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-sHPSE-jD.js} +1 -1
- package/package.json +16 -10
- package/src/components/ActivityList/ActivityList.vue +0 -2
- package/src/components/Chart/ChartViewer.vue +226 -0
- package/src/components/Chart/ChartViewerWrapper.vue +170 -0
- 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/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/ReadMore.vue +1 -1
- package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
- package/src/components/ResourceAccordion/MapContainer.client.vue +7 -11
- package/src/components/ResourceAccordion/Metadata.vue +1 -2
- package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -103
- package/src/components/ResourceAccordion/Pmtiles.client.vue +5 -10
- package/src/components/ResourceAccordion/Preview.vue +16 -21
- package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
- package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
- package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -7
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
- package/src/components/ResourceExplorer/ResourceExplorer.vue +81 -13
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +30 -11
- package/src/components/Search/GlobalSearch.vue +173 -108
- package/src/components/Search/SearchInput.vue +3 -3
- package/src/components/TabularExplorer/TabularCell.vue +51 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +870 -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 +6 -0
- package/src/composables/useResourceCapabilities.ts +1 -1
- package/src/composables/useSearchFilter.ts +118 -0
- package/src/composables/useStableQueryParams.ts +31 -3
- package/src/config.ts +3 -0
- package/src/functions/api.ts +34 -33
- 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/resources.ts +56 -1
- package/src/functions/tabular.ts +60 -0
- package/src/functions/tabularApi.ts +138 -11
- package/src/main.ts +55 -7
- 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 +3 -0
- package/src/types/search.ts +52 -1
- package/src/types/site.ts +5 -3
- package/src/types/users.ts +0 -1
- package/src/types/visualizations.ts +89 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/Datafair.client-E5D6ePRC.js +0 -35
- package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
- package/dist/Swagger.client-D4-F6yEf.js +0 -4
- package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
- /package/assets/illustrations/{_microscope.svg → microscope.svg} +0 -0
|
@@ -5,13 +5,14 @@
|
|
|
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"
|
|
15
16
|
/>
|
|
16
17
|
</div>
|
|
17
18
|
<div class="grid grid-cols-12 mt-2 md:mt-5">
|
|
@@ -30,23 +31,27 @@
|
|
|
30
31
|
>
|
|
31
32
|
<RadioInput
|
|
32
33
|
v-for="typeConfig in config"
|
|
33
|
-
:key="typeConfig
|
|
34
|
-
:value="typeConfig
|
|
35
|
-
:count="
|
|
36
|
-
:loading="
|
|
37
|
-
:icon="
|
|
34
|
+
:key="configKey(typeConfig)"
|
|
35
|
+
:value="configKey(typeConfig)"
|
|
36
|
+
:count="resultsMap[configKey(typeConfig)]?.data.value?.total"
|
|
37
|
+
:loading="resultsMap[configKey(typeConfig)]?.status.value === 'pending' || resultsMap[configKey(typeConfig)]?.status.value === 'idle'"
|
|
38
|
+
:icon="strategies[typeConfig.class].icon"
|
|
38
39
|
>
|
|
39
|
-
{{ typeConfig.name ||
|
|
40
|
+
{{ typeConfig.name || strategies[typeConfig.class].name }}
|
|
40
41
|
</RadioInput>
|
|
41
42
|
</RadioGroup>
|
|
42
43
|
</Sidemenu>
|
|
43
44
|
</div>
|
|
44
45
|
|
|
45
|
-
<div v-if="activeFilters.length > 0">
|
|
46
|
+
<div v-if="activeFilters.length > 0 || $slots['custom-filters-top'] || $slots['custom-filters-bottom']">
|
|
46
47
|
<Sidemenu :button-text="t('Filtres')">
|
|
47
48
|
<template #title>
|
|
48
49
|
{{ t('Filtres') }}
|
|
49
50
|
</template>
|
|
51
|
+
<slot
|
|
52
|
+
name="custom-filters-top"
|
|
53
|
+
:current-type="currentType"
|
|
54
|
+
/>
|
|
50
55
|
<BasicAndAdvancedFilters
|
|
51
56
|
v-slot="{ isEnabled, getOrder }"
|
|
52
57
|
:basic-filters="activeBasicFilters"
|
|
@@ -123,7 +128,7 @@
|
|
|
123
128
|
v-model="producerType"
|
|
124
129
|
:facets="getFacets('producer_type')"
|
|
125
130
|
:loading="searchResultsStatus === 'pending'"
|
|
126
|
-
:exclude="
|
|
131
|
+
:exclude="currentTypeConfig?.class === 'organizations' ? ['user'] : []"
|
|
127
132
|
:style="{ order: getOrder('producer_type') }"
|
|
128
133
|
/>
|
|
129
134
|
<DatasetBadgeFilter
|
|
@@ -146,6 +151,10 @@
|
|
|
146
151
|
:get-order="getOrder"
|
|
147
152
|
/>
|
|
148
153
|
</BasicAndAdvancedFilters>
|
|
154
|
+
<slot
|
|
155
|
+
name="custom-filters-bottom"
|
|
156
|
+
:current-type="currentType"
|
|
157
|
+
/>
|
|
149
158
|
<div
|
|
150
159
|
v-if="hasFilters"
|
|
151
160
|
class="mt-6 text-center"
|
|
@@ -175,6 +184,7 @@
|
|
|
175
184
|
<p
|
|
176
185
|
class="fr-col-auto my-0"
|
|
177
186
|
role="status"
|
|
187
|
+
data-testid="search-result-count"
|
|
178
188
|
>
|
|
179
189
|
{{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
|
|
180
190
|
</p>
|
|
@@ -221,7 +231,7 @@
|
|
|
221
231
|
<transition mode="out-in">
|
|
222
232
|
<LoadingBlock
|
|
223
233
|
v-slot="{ data: results }"
|
|
224
|
-
:status="searchResultsStatus"
|
|
234
|
+
:status="searchResultsStatus!"
|
|
225
235
|
:data="searchResults"
|
|
226
236
|
>
|
|
227
237
|
<div v-if="results && results.data.length">
|
|
@@ -231,7 +241,7 @@
|
|
|
231
241
|
:key="result.id"
|
|
232
242
|
class="p-0"
|
|
233
243
|
>
|
|
234
|
-
<template v-if="
|
|
244
|
+
<template v-if="currentTypeConfig?.class === 'datasets'">
|
|
235
245
|
<slot
|
|
236
246
|
name="dataset"
|
|
237
247
|
:dataset="result"
|
|
@@ -239,7 +249,7 @@
|
|
|
239
249
|
<DatasetCard :dataset="(result as Dataset)" />
|
|
240
250
|
</slot>
|
|
241
251
|
</template>
|
|
242
|
-
<template v-else-if="
|
|
252
|
+
<template v-else-if="currentTypeConfig?.class === 'dataservices'">
|
|
243
253
|
<slot
|
|
244
254
|
name="dataservice"
|
|
245
255
|
:dataservice="result"
|
|
@@ -247,7 +257,7 @@
|
|
|
247
257
|
<DataserviceCard :dataservice="(result as Dataservice)" />
|
|
248
258
|
</slot>
|
|
249
259
|
</template>
|
|
250
|
-
<template v-else-if="
|
|
260
|
+
<template v-else-if="currentTypeConfig?.class === 'reuses'">
|
|
251
261
|
<slot
|
|
252
262
|
name="reuse"
|
|
253
263
|
:reuse="result"
|
|
@@ -255,7 +265,7 @@
|
|
|
255
265
|
<ReuseHorizontalCard :reuse="(result as Reuse)" />
|
|
256
266
|
</slot>
|
|
257
267
|
</template>
|
|
258
|
-
<template v-else-if="
|
|
268
|
+
<template v-else-if="currentTypeConfig?.class === 'organizations'">
|
|
259
269
|
<slot
|
|
260
270
|
name="organization"
|
|
261
271
|
:organization="result"
|
|
@@ -263,6 +273,14 @@
|
|
|
263
273
|
<OrganizationHorizontalCard :organization="(result as Organization)" />
|
|
264
274
|
</slot>
|
|
265
275
|
</template>
|
|
276
|
+
<template v-else-if="currentTypeConfig?.class === 'topics'">
|
|
277
|
+
<slot
|
|
278
|
+
name="topic"
|
|
279
|
+
:topic="result"
|
|
280
|
+
>
|
|
281
|
+
<TopicCard :topic="(result as TopicV2)" />
|
|
282
|
+
</slot>
|
|
283
|
+
</template>
|
|
266
284
|
</li>
|
|
267
285
|
</ul>
|
|
268
286
|
<Pagination
|
|
@@ -271,7 +289,6 @@
|
|
|
271
289
|
:page-size
|
|
272
290
|
:total-results="results.total"
|
|
273
291
|
class="mt-4"
|
|
274
|
-
:link="getLink"
|
|
275
292
|
@change="changePage"
|
|
276
293
|
/>
|
|
277
294
|
</div>
|
|
@@ -333,21 +350,23 @@
|
|
|
333
350
|
</template>
|
|
334
351
|
|
|
335
352
|
<script setup lang="ts">
|
|
336
|
-
import { computed, watch, useTemplateRef, type Ref } from 'vue'
|
|
353
|
+
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
337
354
|
import { useRouteQuery } from '@vueuse/router'
|
|
338
|
-
import { RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
355
|
+
import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
339
356
|
import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
|
|
340
357
|
import { useTranslation } from '../../composables/useTranslation'
|
|
341
358
|
import { useDebouncedRef } from '../../composables/useDebouncedRef'
|
|
359
|
+
import { configKey, forEachActiveCustomFilter, isCustomFilterActive, searchFilterContextKey, type CustomFilterEntry } from '../../composables/useSearchFilter'
|
|
342
360
|
import { useStableQueryParams } from '../../composables/useStableQueryParams'
|
|
343
361
|
import { useComponentsConfig } from '../../config'
|
|
344
362
|
import { useFetch } from '../../functions/api'
|
|
345
|
-
import {
|
|
363
|
+
import type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
346
364
|
import type { Dataset } from '../../types/datasets'
|
|
347
365
|
import type { Dataservice } from '../../types/dataservices'
|
|
348
366
|
import type { Organization } from '../../types/organizations'
|
|
349
367
|
import type { Reuse } from '../../types/reuses'
|
|
350
|
-
import type {
|
|
368
|
+
import type { TopicV2 } from '../../types/topics'
|
|
369
|
+
import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SortOption, FacetItem } from '../../types/search'
|
|
351
370
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
352
371
|
import BrandedButton from '../BrandedButton.vue'
|
|
353
372
|
import LoadingBlock from '../LoadingBlock.vue'
|
|
@@ -358,6 +377,7 @@ import DatasetCard from '../DatasetCard.vue'
|
|
|
358
377
|
import DataserviceCard from '../DataserviceCard.vue'
|
|
359
378
|
import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
|
|
360
379
|
import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
|
|
380
|
+
import TopicCard from '../TopicCard.vue'
|
|
361
381
|
import SearchInput from './SearchInput.vue'
|
|
362
382
|
import Sidemenu from './Sidemenu.vue'
|
|
363
383
|
import BasicAndAdvancedFilters from './BasicAndAdvancedFilters.vue'
|
|
@@ -379,25 +399,43 @@ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
|
|
|
379
399
|
|
|
380
400
|
const props = withDefaults(defineProps<{
|
|
381
401
|
config?: GlobalSearchConfig
|
|
382
|
-
placeholder?: string
|
|
402
|
+
placeholder?: string | null
|
|
403
|
+
hideSearchInput?: boolean
|
|
383
404
|
}>(), {
|
|
384
405
|
config: getDefaultGlobalSearchConfig,
|
|
406
|
+
hideSearchInput: false,
|
|
385
407
|
})
|
|
386
408
|
|
|
387
409
|
// 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]
|
|
410
|
+
const currentType = defineModel<string>('type') as Ref<string>
|
|
411
|
+
if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class: 'datasets' })
|
|
390
412
|
|
|
391
413
|
const { t } = useTranslation()
|
|
392
414
|
const componentsConfig = useComponentsConfig()
|
|
393
415
|
|
|
416
|
+
// Custom filter registry for useSearchFilter composable
|
|
417
|
+
const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
|
|
418
|
+
// Per-filter watch stoppers: each registered filter gets its own watcher so a
|
|
419
|
+
// value change resets page to 1, but registration itself does not (the value
|
|
420
|
+
// came from the URL, not from a user action).
|
|
421
|
+
const customFilterStops = new Map<string, () => void>()
|
|
422
|
+
|
|
394
423
|
// Initial type is used to determine which fetch should be SSR (non-lazy)
|
|
395
424
|
const initialType = currentType.value
|
|
396
425
|
|
|
397
426
|
const currentTypeConfig = computed(() =>
|
|
398
|
-
props.config.find(c => c
|
|
427
|
+
props.config.find(c => configKey(c) === currentType.value),
|
|
399
428
|
)
|
|
400
429
|
|
|
430
|
+
// Precedence: prop → per-type config → strategy default.
|
|
431
|
+
// null at any level means "no placeholder".
|
|
432
|
+
const resolvedPlaceholder = computed(() => {
|
|
433
|
+
if (props.placeholder !== undefined) return props.placeholder ?? ''
|
|
434
|
+
const cfg = currentTypeConfig.value
|
|
435
|
+
if (cfg && 'placeholder' in cfg) return cfg.placeholder ?? ''
|
|
436
|
+
return strategies[cfg?.class ?? 'datasets'].placeholder
|
|
437
|
+
})
|
|
438
|
+
|
|
401
439
|
const activeBasicFilters = computed(() =>
|
|
402
440
|
(currentTypeConfig.value?.basicFilters ?? []) as string[],
|
|
403
441
|
)
|
|
@@ -431,7 +469,8 @@ const activeFilters = computed(() => [
|
|
|
431
469
|
...(currentTypeConfig.value?.advancedFilters ?? []),
|
|
432
470
|
] as string[])
|
|
433
471
|
|
|
434
|
-
const
|
|
472
|
+
const slots = useSlots()
|
|
473
|
+
const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0 || !!slots['custom-filters-top'] || !!slots['custom-filters-bottom'])
|
|
435
474
|
|
|
436
475
|
// URL query params
|
|
437
476
|
const q = useRouteQuery<string>('q', '')
|
|
@@ -439,6 +478,18 @@ const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsCo
|
|
|
439
478
|
const page = useRouteQuery('page', 1, { transform: Number })
|
|
440
479
|
const sort = useRouteQuery<string | undefined>('sort')
|
|
441
480
|
|
|
481
|
+
provide(searchFilterContextKey, {
|
|
482
|
+
register(urlParam, entry) {
|
|
483
|
+
customFilterRegistry.set(urlParam, entry)
|
|
484
|
+
customFilterStops.set(urlParam, watch(entry.ref, () => page.value = 1))
|
|
485
|
+
},
|
|
486
|
+
unregister(urlParam) {
|
|
487
|
+
customFilterStops.get(urlParam)?.()
|
|
488
|
+
customFilterStops.delete(urlParam)
|
|
489
|
+
customFilterRegistry.delete(urlParam)
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
|
|
442
493
|
// Filter values
|
|
443
494
|
const organizationId = useRouteQuery<string | undefined>('organization')
|
|
444
495
|
const organizationType = useRouteQuery<string | undefined>('organization_badge')
|
|
@@ -495,45 +546,102 @@ watch(currentType, () => {
|
|
|
495
546
|
}
|
|
496
547
|
})
|
|
497
548
|
|
|
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
549
|
// Create stable params for each type
|
|
505
550
|
const stableParamsOptions = {
|
|
506
551
|
allFilters,
|
|
552
|
+
customFilterRegistry,
|
|
507
553
|
q: qDebounced,
|
|
508
554
|
sort,
|
|
509
555
|
page,
|
|
510
556
|
pageSize,
|
|
511
557
|
}
|
|
512
558
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
559
|
+
// Discriminated union: each variant carries its own response type so a `class`
|
|
560
|
+
// narrow gives the precise shape of `data.value` (no cast needed).
|
|
561
|
+
type SearchEntry = {
|
|
562
|
+
[K in SearchType]: {
|
|
563
|
+
class: K
|
|
564
|
+
data: Ref<SearchResponseByClass[K] | null>
|
|
565
|
+
status: Ref<AsyncDataRequestStatus>
|
|
566
|
+
}
|
|
567
|
+
}[SearchType]
|
|
568
|
+
|
|
569
|
+
// One strategy per class consolidates everything that varies by class:
|
|
570
|
+
// metadata (icon/name/placeholder), endpoint, and a typed fetch factory.
|
|
571
|
+
type SearchStrategy<C extends SearchType> = {
|
|
572
|
+
url: string
|
|
573
|
+
icon: Component
|
|
574
|
+
name: string
|
|
575
|
+
placeholder: string
|
|
576
|
+
fetch: (
|
|
577
|
+
params: Ref<Record<string, unknown>>,
|
|
578
|
+
server: boolean,
|
|
579
|
+
) => Promise<Extract<SearchEntry, { class: C }>>
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function makeStrategy<C extends SearchType>(
|
|
583
|
+
cls: C,
|
|
584
|
+
meta: Omit<SearchStrategy<C>, 'fetch'>,
|
|
585
|
+
): SearchStrategy<C> {
|
|
586
|
+
return {
|
|
587
|
+
...meta,
|
|
588
|
+
fetch: async (params, server) => {
|
|
589
|
+
const { data, status } = await useFetch<SearchResponseByClass[C]>(
|
|
590
|
+
meta.url,
|
|
591
|
+
{ params, lazy: true, server },
|
|
592
|
+
)
|
|
593
|
+
// Tautologically equivalent to Extract<SearchEntry, { class: C }>, but TS
|
|
594
|
+
// cannot prove it on a generic C, so we assert.
|
|
595
|
+
return { class: cls, data, status } as Extract<SearchEntry, { class: C }>
|
|
596
|
+
},
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const strategies: { [K in SearchType]: SearchStrategy<K> } = {
|
|
601
|
+
datasets: makeStrategy('datasets', {
|
|
602
|
+
url: '/api/2/datasets/search/',
|
|
603
|
+
icon: RiDatabase2Line,
|
|
604
|
+
name: t('Jeux de données'),
|
|
605
|
+
placeholder: t('ex. élections présidentielles'),
|
|
606
|
+
}),
|
|
607
|
+
dataservices: makeStrategy('dataservices', {
|
|
608
|
+
url: '/api/2/dataservices/search/',
|
|
609
|
+
icon: RiTerminalLine,
|
|
610
|
+
name: t('API'),
|
|
611
|
+
placeholder: t('ex: SIRENE'),
|
|
612
|
+
}),
|
|
613
|
+
reuses: makeStrategy('reuses', {
|
|
614
|
+
url: '/api/2/reuses/search/',
|
|
615
|
+
icon: RiLineChartLine,
|
|
616
|
+
name: t('Réutilisations'),
|
|
617
|
+
placeholder: t('Rechercher une réutilisation de données'),
|
|
618
|
+
}),
|
|
619
|
+
organizations: makeStrategy('organizations', {
|
|
620
|
+
url: '/api/2/organizations/search/',
|
|
621
|
+
icon: RiBuilding2Line,
|
|
622
|
+
name: t('Organisations'),
|
|
623
|
+
placeholder: t('Rechercher une organisation'),
|
|
624
|
+
}),
|
|
625
|
+
topics: makeStrategy('topics', {
|
|
626
|
+
url: '/api/2/topics/search/',
|
|
627
|
+
icon: RiBookShelfLine,
|
|
628
|
+
name: t('Thématiques'),
|
|
629
|
+
placeholder: t('Rechercher une thématique'),
|
|
630
|
+
}),
|
|
631
|
+
}
|
|
529
632
|
|
|
530
|
-
//
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
633
|
+
// One params + fetch per config entry, keyed by configKey
|
|
634
|
+
const resultsMap: Record<string, SearchEntry> = {}
|
|
635
|
+
for (const c of props.config) {
|
|
636
|
+
const key = configKey(c)
|
|
637
|
+
const params = useStableQueryParams({ ...stableParamsOptions, typeConfig: c })
|
|
638
|
+
resultsMap[key] = await strategies[c.class].fetch(params, initialType === key)
|
|
639
|
+
}
|
|
535
640
|
|
|
536
|
-
// Reset page on filter/sort change
|
|
641
|
+
// Reset page on filter/sort change. Custom filters (registered via
|
|
642
|
+
// useSearchFilter) have their own watchers set up in `provide`, so they're
|
|
643
|
+
// intentionally excluded here to avoid resetting the page when a filter
|
|
644
|
+
// registers with its URL-derived value.
|
|
537
645
|
const filtersForReset = computed(() => ({
|
|
538
646
|
q: qDebounced.value,
|
|
539
647
|
organization: organizationId.value,
|
|
@@ -573,6 +681,7 @@ const hasFilters = computed(() => {
|
|
|
573
681
|
|| lastUpdateRange.value
|
|
574
682
|
|| producerType.value
|
|
575
683
|
|| reuseType.value
|
|
684
|
+
|| Array.from(customFilterRegistry.values()).some(isCustomFilterActive)
|
|
576
685
|
})
|
|
577
686
|
|
|
578
687
|
const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
|
|
@@ -593,73 +702,25 @@ function resetFilters() {
|
|
|
593
702
|
lastUpdateRange.value = undefined
|
|
594
703
|
producerType.value = undefined
|
|
595
704
|
reuseType.value = undefined
|
|
705
|
+
for (const entry of customFilterRegistry.values()) {
|
|
706
|
+
entry.ref.value = entry.defaultValue
|
|
707
|
+
}
|
|
596
708
|
q.value = ''
|
|
597
709
|
flushQ()
|
|
598
710
|
}
|
|
599
711
|
|
|
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
|
|
649
|
-
|
|
650
|
-
const searchResults = computed(() => typesMeta[currentType.value].results.value)
|
|
651
|
-
const searchResultsStatus = computed(() => typesMeta[currentType.value].status.value)
|
|
712
|
+
const searchResults = computed(() => resultsMap[currentType.value]?.data.value)
|
|
713
|
+
const searchResultsStatus = computed(() => resultsMap[currentType.value]?.status.value)
|
|
652
714
|
|
|
653
715
|
// RSS feed URL for datasets
|
|
654
716
|
const rssUrl = computed(() => {
|
|
655
|
-
if (
|
|
717
|
+
if (currentTypeConfig.value?.class !== 'datasets') return null
|
|
656
718
|
|
|
657
719
|
const params = new URLSearchParams()
|
|
658
|
-
const datasetsConfig = props.config.find(c => c.class === 'datasets')
|
|
659
720
|
|
|
660
721
|
// Add hidden filters first
|
|
661
|
-
if (
|
|
662
|
-
for (const hf of
|
|
722
|
+
if (currentTypeConfig.value?.hiddenFilters) {
|
|
723
|
+
for (const hf of currentTypeConfig.value.hiddenFilters) {
|
|
663
724
|
if (hf?.value) params.set(hf.key as string, String(hf.value))
|
|
664
725
|
}
|
|
665
726
|
}
|
|
@@ -677,6 +738,10 @@ const rssUrl = computed(() => {
|
|
|
677
738
|
if (badge.value) params.set('badge', badge.value)
|
|
678
739
|
if (topic.value) params.set('topic', topic.value)
|
|
679
740
|
|
|
741
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
742
|
+
params.set(apiParam, value)
|
|
743
|
+
}, currentTypeConfig.value ? configKey(currentTypeConfig.value) : undefined)
|
|
744
|
+
|
|
680
745
|
// Add sort if set
|
|
681
746
|
if (sort.value) params.set('sort', sort.value)
|
|
682
747
|
|
|
@@ -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"
|
|
@@ -38,7 +38,7 @@ import BrandedButton from '../BrandedButton.vue'
|
|
|
38
38
|
const q = defineModel<string>({ required: true })
|
|
39
39
|
|
|
40
40
|
withDefaults(defineProps<{
|
|
41
|
-
placeholder?: string
|
|
41
|
+
placeholder?: string | null
|
|
42
42
|
autoFocus?: boolean
|
|
43
43
|
}>(), {
|
|
44
44
|
autoFocus: true,
|
|
@@ -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>
|