@datagouv/components-next 1.0.2-dev.6 → 1.0.2-dev.61
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-CZBhcl2N.js +30 -0
- package/dist/JsonPreview.client-BFXc8Yds.js +40 -0
- package/dist/{MapContainer.client-DRkAmdOc.js → MapContainer.client-DXvpJcmy.js} +35 -38
- package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-C4rWJaSZ.js} +822 -865
- package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-ByvF0n7m.js} +574 -579
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-B6mL96Bm.js +61 -0
- package/dist/XmlPreview.client-aAATX5FP.js +34 -0
- package/dist/components-next.css +3 -3
- package/dist/components-next.js +140 -131
- package/dist/components.css +1 -1
- package/dist/{index-SrYZwgCT.js → index-9wBlcysa.js} +1 -1
- package/dist/main-DSV-Ai-o.js +73008 -0
- package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-EQMYhxMa.js} +1 -1
- package/package.json +14 -9
- package/src/components/ActivityList/ActivityList.vue +0 -2
- 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 +15 -20
- 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 +174 -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/useResourceCapabilities.ts +1 -1
- package/src/composables/useSearchFilter.ts +90 -0
- package/src/composables/useStableQueryParams.ts +28 -3
- package/src/config.ts +2 -0
- package/src/functions/api.ts +34 -33
- package/src/functions/api.types.ts +1 -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 +4 -6
- package/src/main.ts +14 -6
- 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 +50 -1
- package/src/types/site.ts +5 -3
- package/src/types/users.ts +0 -1
- 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/dist/main-B2kXxWRG.js +0 -105833
- 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"
|
|
@@ -221,7 +230,7 @@
|
|
|
221
230
|
<transition mode="out-in">
|
|
222
231
|
<LoadingBlock
|
|
223
232
|
v-slot="{ data: results }"
|
|
224
|
-
:status="searchResultsStatus"
|
|
233
|
+
:status="searchResultsStatus!"
|
|
225
234
|
:data="searchResults"
|
|
226
235
|
>
|
|
227
236
|
<div v-if="results && results.data.length">
|
|
@@ -231,7 +240,7 @@
|
|
|
231
240
|
:key="result.id"
|
|
232
241
|
class="p-0"
|
|
233
242
|
>
|
|
234
|
-
<template v-if="
|
|
243
|
+
<template v-if="currentTypeConfig?.class === 'datasets'">
|
|
235
244
|
<slot
|
|
236
245
|
name="dataset"
|
|
237
246
|
:dataset="result"
|
|
@@ -239,7 +248,7 @@
|
|
|
239
248
|
<DatasetCard :dataset="(result as Dataset)" />
|
|
240
249
|
</slot>
|
|
241
250
|
</template>
|
|
242
|
-
<template v-else-if="
|
|
251
|
+
<template v-else-if="currentTypeConfig?.class === 'dataservices'">
|
|
243
252
|
<slot
|
|
244
253
|
name="dataservice"
|
|
245
254
|
:dataservice="result"
|
|
@@ -247,7 +256,7 @@
|
|
|
247
256
|
<DataserviceCard :dataservice="(result as Dataservice)" />
|
|
248
257
|
</slot>
|
|
249
258
|
</template>
|
|
250
|
-
<template v-else-if="
|
|
259
|
+
<template v-else-if="currentTypeConfig?.class === 'reuses'">
|
|
251
260
|
<slot
|
|
252
261
|
name="reuse"
|
|
253
262
|
:reuse="result"
|
|
@@ -255,7 +264,7 @@
|
|
|
255
264
|
<ReuseHorizontalCard :reuse="(result as Reuse)" />
|
|
256
265
|
</slot>
|
|
257
266
|
</template>
|
|
258
|
-
<template v-else-if="
|
|
267
|
+
<template v-else-if="currentTypeConfig?.class === 'organizations'">
|
|
259
268
|
<slot
|
|
260
269
|
name="organization"
|
|
261
270
|
:organization="result"
|
|
@@ -263,6 +272,14 @@
|
|
|
263
272
|
<OrganizationHorizontalCard :organization="(result as Organization)" />
|
|
264
273
|
</slot>
|
|
265
274
|
</template>
|
|
275
|
+
<template v-else-if="currentTypeConfig?.class === 'topics'">
|
|
276
|
+
<slot
|
|
277
|
+
name="topic"
|
|
278
|
+
:topic="result"
|
|
279
|
+
>
|
|
280
|
+
<TopicCard :topic="(result as TopicV2)" />
|
|
281
|
+
</slot>
|
|
282
|
+
</template>
|
|
266
283
|
</li>
|
|
267
284
|
</ul>
|
|
268
285
|
<Pagination
|
|
@@ -271,7 +288,6 @@
|
|
|
271
288
|
:page-size
|
|
272
289
|
:total-results="results.total"
|
|
273
290
|
class="mt-4"
|
|
274
|
-
:link="getLink"
|
|
275
291
|
@change="changePage"
|
|
276
292
|
/>
|
|
277
293
|
</div>
|
|
@@ -333,21 +349,23 @@
|
|
|
333
349
|
</template>
|
|
334
350
|
|
|
335
351
|
<script setup lang="ts">
|
|
336
|
-
import { computed, watch, useTemplateRef, type Ref } from 'vue'
|
|
352
|
+
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
337
353
|
import { useRouteQuery } from '@vueuse/router'
|
|
338
|
-
import { RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
354
|
+
import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
339
355
|
import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
|
|
340
356
|
import { useTranslation } from '../../composables/useTranslation'
|
|
341
357
|
import { useDebouncedRef } from '../../composables/useDebouncedRef'
|
|
358
|
+
import { forEachActiveCustomFilter, isCustomFilterActive, searchFilterContextKey, type CustomFilterEntry } from '../../composables/useSearchFilter'
|
|
342
359
|
import { useStableQueryParams } from '../../composables/useStableQueryParams'
|
|
343
360
|
import { useComponentsConfig } from '../../config'
|
|
344
361
|
import { useFetch } from '../../functions/api'
|
|
345
|
-
import {
|
|
362
|
+
import type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
346
363
|
import type { Dataset } from '../../types/datasets'
|
|
347
364
|
import type { Dataservice } from '../../types/dataservices'
|
|
348
365
|
import type { Organization } from '../../types/organizations'
|
|
349
366
|
import type { Reuse } from '../../types/reuses'
|
|
350
|
-
import type {
|
|
367
|
+
import type { TopicV2 } from '../../types/topics'
|
|
368
|
+
import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SearchTypeConfig, SortOption, FacetItem } from '../../types/search'
|
|
351
369
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
352
370
|
import BrandedButton from '../BrandedButton.vue'
|
|
353
371
|
import LoadingBlock from '../LoadingBlock.vue'
|
|
@@ -358,6 +376,7 @@ import DatasetCard from '../DatasetCard.vue'
|
|
|
358
376
|
import DataserviceCard from '../DataserviceCard.vue'
|
|
359
377
|
import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
|
|
360
378
|
import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
|
|
379
|
+
import TopicCard from '../TopicCard.vue'
|
|
361
380
|
import SearchInput from './SearchInput.vue'
|
|
362
381
|
import Sidemenu from './Sidemenu.vue'
|
|
363
382
|
import BasicAndAdvancedFilters from './BasicAndAdvancedFilters.vue'
|
|
@@ -379,25 +398,45 @@ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
|
|
|
379
398
|
|
|
380
399
|
const props = withDefaults(defineProps<{
|
|
381
400
|
config?: GlobalSearchConfig
|
|
382
|
-
placeholder?: string
|
|
401
|
+
placeholder?: string | null
|
|
402
|
+
hideSearchInput?: boolean
|
|
383
403
|
}>(), {
|
|
384
404
|
config: getDefaultGlobalSearchConfig,
|
|
405
|
+
hideSearchInput: false,
|
|
385
406
|
})
|
|
386
407
|
|
|
408
|
+
const configKey = (c: SearchTypeConfig) => c.key ?? c.class
|
|
409
|
+
|
|
387
410
|
// 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]
|
|
411
|
+
const currentType = defineModel<string>('type') as Ref<string>
|
|
412
|
+
if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class: 'datasets' })
|
|
390
413
|
|
|
391
414
|
const { t } = useTranslation()
|
|
392
415
|
const componentsConfig = useComponentsConfig()
|
|
393
416
|
|
|
417
|
+
// Custom filter registry for useSearchFilter composable
|
|
418
|
+
const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
|
|
419
|
+
// Per-filter watch stoppers: each registered filter gets its own watcher so a
|
|
420
|
+
// value change resets page to 1, but registration itself does not (the value
|
|
421
|
+
// came from the URL, not from a user action).
|
|
422
|
+
const customFilterStops = new Map<string, () => void>()
|
|
423
|
+
|
|
394
424
|
// Initial type is used to determine which fetch should be SSR (non-lazy)
|
|
395
425
|
const initialType = currentType.value
|
|
396
426
|
|
|
397
427
|
const currentTypeConfig = computed(() =>
|
|
398
|
-
props.config.find(c => c
|
|
428
|
+
props.config.find(c => configKey(c) === currentType.value),
|
|
399
429
|
)
|
|
400
430
|
|
|
431
|
+
// Precedence: prop → per-type config → strategy default.
|
|
432
|
+
// null at any level means "no placeholder".
|
|
433
|
+
const resolvedPlaceholder = computed(() => {
|
|
434
|
+
if (props.placeholder !== undefined) return props.placeholder ?? ''
|
|
435
|
+
const cfg = currentTypeConfig.value
|
|
436
|
+
if (cfg && 'placeholder' in cfg) return cfg.placeholder ?? ''
|
|
437
|
+
return strategies[cfg?.class ?? 'datasets'].placeholder
|
|
438
|
+
})
|
|
439
|
+
|
|
401
440
|
const activeBasicFilters = computed(() =>
|
|
402
441
|
(currentTypeConfig.value?.basicFilters ?? []) as string[],
|
|
403
442
|
)
|
|
@@ -431,7 +470,8 @@ const activeFilters = computed(() => [
|
|
|
431
470
|
...(currentTypeConfig.value?.advancedFilters ?? []),
|
|
432
471
|
] as string[])
|
|
433
472
|
|
|
434
|
-
const
|
|
473
|
+
const slots = useSlots()
|
|
474
|
+
const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0 || !!slots['custom-filters-top'] || !!slots['custom-filters-bottom'])
|
|
435
475
|
|
|
436
476
|
// URL query params
|
|
437
477
|
const q = useRouteQuery<string>('q', '')
|
|
@@ -439,6 +479,18 @@ const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsCo
|
|
|
439
479
|
const page = useRouteQuery('page', 1, { transform: Number })
|
|
440
480
|
const sort = useRouteQuery<string | undefined>('sort')
|
|
441
481
|
|
|
482
|
+
provide(searchFilterContextKey, {
|
|
483
|
+
register(urlParam, entry) {
|
|
484
|
+
customFilterRegistry.set(urlParam, entry)
|
|
485
|
+
customFilterStops.set(urlParam, watch(entry.ref, () => page.value = 1))
|
|
486
|
+
},
|
|
487
|
+
unregister(urlParam) {
|
|
488
|
+
customFilterStops.get(urlParam)?.()
|
|
489
|
+
customFilterStops.delete(urlParam)
|
|
490
|
+
customFilterRegistry.delete(urlParam)
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
|
|
442
494
|
// Filter values
|
|
443
495
|
const organizationId = useRouteQuery<string | undefined>('organization')
|
|
444
496
|
const organizationType = useRouteQuery<string | undefined>('organization_badge')
|
|
@@ -495,45 +547,102 @@ watch(currentType, () => {
|
|
|
495
547
|
}
|
|
496
548
|
})
|
|
497
549
|
|
|
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
550
|
// Create stable params for each type
|
|
505
551
|
const stableParamsOptions = {
|
|
506
552
|
allFilters,
|
|
553
|
+
customFilterRegistry,
|
|
507
554
|
q: qDebounced,
|
|
508
555
|
sort,
|
|
509
556
|
page,
|
|
510
557
|
pageSize,
|
|
511
558
|
}
|
|
512
559
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
560
|
+
// Discriminated union: each variant carries its own response type so a `class`
|
|
561
|
+
// narrow gives the precise shape of `data.value` (no cast needed).
|
|
562
|
+
type SearchEntry = {
|
|
563
|
+
[K in SearchType]: {
|
|
564
|
+
class: K
|
|
565
|
+
data: Ref<SearchResponseByClass[K] | null>
|
|
566
|
+
status: Ref<AsyncDataRequestStatus>
|
|
567
|
+
}
|
|
568
|
+
}[SearchType]
|
|
569
|
+
|
|
570
|
+
// One strategy per class consolidates everything that varies by class:
|
|
571
|
+
// metadata (icon/name/placeholder), endpoint, and a typed fetch factory.
|
|
572
|
+
type SearchStrategy<C extends SearchType> = {
|
|
573
|
+
url: string
|
|
574
|
+
icon: Component
|
|
575
|
+
name: string
|
|
576
|
+
placeholder: string
|
|
577
|
+
fetch: (
|
|
578
|
+
params: Ref<Record<string, unknown>>,
|
|
579
|
+
server: boolean,
|
|
580
|
+
) => Promise<Extract<SearchEntry, { class: C }>>
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function makeStrategy<C extends SearchType>(
|
|
584
|
+
cls: C,
|
|
585
|
+
meta: Omit<SearchStrategy<C>, 'fetch'>,
|
|
586
|
+
): SearchStrategy<C> {
|
|
587
|
+
return {
|
|
588
|
+
...meta,
|
|
589
|
+
fetch: async (params, server) => {
|
|
590
|
+
const { data, status } = await useFetch<SearchResponseByClass[C]>(
|
|
591
|
+
meta.url,
|
|
592
|
+
{ params, lazy: true, server },
|
|
593
|
+
)
|
|
594
|
+
// Tautologically equivalent to Extract<SearchEntry, { class: C }>, but TS
|
|
595
|
+
// cannot prove it on a generic C, so we assert.
|
|
596
|
+
return { class: cls, data, status } as Extract<SearchEntry, { class: C }>
|
|
597
|
+
},
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const strategies: { [K in SearchType]: SearchStrategy<K> } = {
|
|
602
|
+
datasets: makeStrategy('datasets', {
|
|
603
|
+
url: '/api/2/datasets/search/',
|
|
604
|
+
icon: RiDatabase2Line,
|
|
605
|
+
name: t('Jeux de données'),
|
|
606
|
+
placeholder: t('ex. élections présidentielles'),
|
|
607
|
+
}),
|
|
608
|
+
dataservices: makeStrategy('dataservices', {
|
|
609
|
+
url: '/api/2/dataservices/search/',
|
|
610
|
+
icon: RiTerminalLine,
|
|
611
|
+
name: t('API'),
|
|
612
|
+
placeholder: t('ex: SIRENE'),
|
|
613
|
+
}),
|
|
614
|
+
reuses: makeStrategy('reuses', {
|
|
615
|
+
url: '/api/2/reuses/search/',
|
|
616
|
+
icon: RiLineChartLine,
|
|
617
|
+
name: t('Réutilisations'),
|
|
618
|
+
placeholder: t('Rechercher une réutilisation de données'),
|
|
619
|
+
}),
|
|
620
|
+
organizations: makeStrategy('organizations', {
|
|
621
|
+
url: '/api/2/organizations/search/',
|
|
622
|
+
icon: RiBuilding2Line,
|
|
623
|
+
name: t('Organisations'),
|
|
624
|
+
placeholder: t('Rechercher une organisation'),
|
|
625
|
+
}),
|
|
626
|
+
topics: makeStrategy('topics', {
|
|
627
|
+
url: '/api/2/topics/search/',
|
|
628
|
+
icon: RiBookShelfLine,
|
|
629
|
+
name: t('Thématiques'),
|
|
630
|
+
placeholder: t('Rechercher une thématique'),
|
|
631
|
+
}),
|
|
632
|
+
}
|
|
529
633
|
|
|
530
|
-
//
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
634
|
+
// One params + fetch per config entry, keyed by configKey
|
|
635
|
+
const resultsMap: Record<string, SearchEntry> = {}
|
|
636
|
+
for (const c of props.config) {
|
|
637
|
+
const key = configKey(c)
|
|
638
|
+
const params = useStableQueryParams({ ...stableParamsOptions, typeConfig: c })
|
|
639
|
+
resultsMap[key] = await strategies[c.class].fetch(params, initialType === key)
|
|
640
|
+
}
|
|
535
641
|
|
|
536
|
-
// Reset page on filter/sort change
|
|
642
|
+
// Reset page on filter/sort change. Custom filters (registered via
|
|
643
|
+
// useSearchFilter) have their own watchers set up in `provide`, so they're
|
|
644
|
+
// intentionally excluded here to avoid resetting the page when a filter
|
|
645
|
+
// registers with its URL-derived value.
|
|
537
646
|
const filtersForReset = computed(() => ({
|
|
538
647
|
q: qDebounced.value,
|
|
539
648
|
organization: organizationId.value,
|
|
@@ -573,6 +682,7 @@ const hasFilters = computed(() => {
|
|
|
573
682
|
|| lastUpdateRange.value
|
|
574
683
|
|| producerType.value
|
|
575
684
|
|| reuseType.value
|
|
685
|
+
|| Array.from(customFilterRegistry.values()).some(isCustomFilterActive)
|
|
576
686
|
})
|
|
577
687
|
|
|
578
688
|
const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
|
|
@@ -593,73 +703,25 @@ function resetFilters() {
|
|
|
593
703
|
lastUpdateRange.value = undefined
|
|
594
704
|
producerType.value = undefined
|
|
595
705
|
reuseType.value = undefined
|
|
706
|
+
for (const entry of customFilterRegistry.values()) {
|
|
707
|
+
entry.ref.value = entry.defaultValue
|
|
708
|
+
}
|
|
596
709
|
q.value = ''
|
|
597
710
|
flushQ()
|
|
598
711
|
}
|
|
599
712
|
|
|
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)
|
|
713
|
+
const searchResults = computed(() => resultsMap[currentType.value]?.data.value)
|
|
714
|
+
const searchResultsStatus = computed(() => resultsMap[currentType.value]?.status.value)
|
|
652
715
|
|
|
653
716
|
// RSS feed URL for datasets
|
|
654
717
|
const rssUrl = computed(() => {
|
|
655
|
-
if (
|
|
718
|
+
if (currentTypeConfig.value?.class !== 'datasets') return null
|
|
656
719
|
|
|
657
720
|
const params = new URLSearchParams()
|
|
658
|
-
const datasetsConfig = props.config.find(c => c.class === 'datasets')
|
|
659
721
|
|
|
660
722
|
// Add hidden filters first
|
|
661
|
-
if (
|
|
662
|
-
for (const hf of
|
|
723
|
+
if (currentTypeConfig.value?.hiddenFilters) {
|
|
724
|
+
for (const hf of currentTypeConfig.value.hiddenFilters) {
|
|
663
725
|
if (hf?.value) params.set(hf.key as string, String(hf.value))
|
|
664
726
|
}
|
|
665
727
|
}
|
|
@@ -677,6 +739,10 @@ const rssUrl = computed(() => {
|
|
|
677
739
|
if (badge.value) params.set('badge', badge.value)
|
|
678
740
|
if (topic.value) params.set('topic', topic.value)
|
|
679
741
|
|
|
742
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
743
|
+
params.set(apiParam, value)
|
|
744
|
+
})
|
|
745
|
+
|
|
680
746
|
// Add sort if set
|
|
681
747
|
if (sort.value) params.set('sort', sort.value)
|
|
682
748
|
|
|
@@ -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>
|