@datagouv/components-next 1.0.2-dev.6 → 1.0.2-dev.60
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-4zDFMXaE.js +30 -0
- package/dist/JsonPreview.client-DSKs3Wg7.js +40 -0
- package/dist/{MapContainer.client-DRkAmdOc.js → MapContainer.client-Cjwpar3k.js} +35 -38
- package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-6H3KMLOL.js} +822 -865
- package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-BDAAb3_H.js} +574 -579
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BFSXn0mv.js +61 -0
- package/dist/XmlPreview.client-DnL8nMyL.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-7yWP5a5K.js} +1 -1
- package/dist/main-C-0Gkcks.js +73004 -0
- package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-xigir_CG.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 +164 -107
- 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 +45 -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="placeholder ||
|
|
15
|
+
:placeholder="placeholder || strategies[currentTypeConfig?.class ?? 'datasets'].placeholder"
|
|
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'
|
|
@@ -380,22 +399,33 @@ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
|
|
|
380
399
|
const props = withDefaults(defineProps<{
|
|
381
400
|
config?: GlobalSearchConfig
|
|
382
401
|
placeholder?: string
|
|
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
|
|
|
401
431
|
const activeBasicFilters = computed(() =>
|
|
@@ -431,7 +461,8 @@ const activeFilters = computed(() => [
|
|
|
431
461
|
...(currentTypeConfig.value?.advancedFilters ?? []),
|
|
432
462
|
] as string[])
|
|
433
463
|
|
|
434
|
-
const
|
|
464
|
+
const slots = useSlots()
|
|
465
|
+
const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0 || !!slots['custom-filters-top'] || !!slots['custom-filters-bottom'])
|
|
435
466
|
|
|
436
467
|
// URL query params
|
|
437
468
|
const q = useRouteQuery<string>('q', '')
|
|
@@ -439,6 +470,18 @@ const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsCo
|
|
|
439
470
|
const page = useRouteQuery('page', 1, { transform: Number })
|
|
440
471
|
const sort = useRouteQuery<string | undefined>('sort')
|
|
441
472
|
|
|
473
|
+
provide(searchFilterContextKey, {
|
|
474
|
+
register(urlParam, entry) {
|
|
475
|
+
customFilterRegistry.set(urlParam, entry)
|
|
476
|
+
customFilterStops.set(urlParam, watch(entry.ref, () => page.value = 1))
|
|
477
|
+
},
|
|
478
|
+
unregister(urlParam) {
|
|
479
|
+
customFilterStops.get(urlParam)?.()
|
|
480
|
+
customFilterStops.delete(urlParam)
|
|
481
|
+
customFilterRegistry.delete(urlParam)
|
|
482
|
+
},
|
|
483
|
+
})
|
|
484
|
+
|
|
442
485
|
// Filter values
|
|
443
486
|
const organizationId = useRouteQuery<string | undefined>('organization')
|
|
444
487
|
const organizationType = useRouteQuery<string | undefined>('organization_badge')
|
|
@@ -495,45 +538,102 @@ watch(currentType, () => {
|
|
|
495
538
|
}
|
|
496
539
|
})
|
|
497
540
|
|
|
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
541
|
// Create stable params for each type
|
|
505
542
|
const stableParamsOptions = {
|
|
506
543
|
allFilters,
|
|
544
|
+
customFilterRegistry,
|
|
507
545
|
q: qDebounced,
|
|
508
546
|
sort,
|
|
509
547
|
page,
|
|
510
548
|
pageSize,
|
|
511
549
|
}
|
|
512
550
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
551
|
+
// Discriminated union: each variant carries its own response type so a `class`
|
|
552
|
+
// narrow gives the precise shape of `data.value` (no cast needed).
|
|
553
|
+
type SearchEntry = {
|
|
554
|
+
[K in SearchType]: {
|
|
555
|
+
class: K
|
|
556
|
+
data: Ref<SearchResponseByClass[K] | null>
|
|
557
|
+
status: Ref<AsyncDataRequestStatus>
|
|
558
|
+
}
|
|
559
|
+
}[SearchType]
|
|
560
|
+
|
|
561
|
+
// One strategy per class consolidates everything that varies by class:
|
|
562
|
+
// metadata (icon/name/placeholder), endpoint, and a typed fetch factory.
|
|
563
|
+
type SearchStrategy<C extends SearchType> = {
|
|
564
|
+
url: string
|
|
565
|
+
icon: Component
|
|
566
|
+
name: string
|
|
567
|
+
placeholder: string
|
|
568
|
+
fetch: (
|
|
569
|
+
params: Ref<Record<string, unknown>>,
|
|
570
|
+
server: boolean,
|
|
571
|
+
) => Promise<Extract<SearchEntry, { class: C }>>
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function makeStrategy<C extends SearchType>(
|
|
575
|
+
cls: C,
|
|
576
|
+
meta: Omit<SearchStrategy<C>, 'fetch'>,
|
|
577
|
+
): SearchStrategy<C> {
|
|
578
|
+
return {
|
|
579
|
+
...meta,
|
|
580
|
+
fetch: async (params, server) => {
|
|
581
|
+
const { data, status } = await useFetch<SearchResponseByClass[C]>(
|
|
582
|
+
meta.url,
|
|
583
|
+
{ params, lazy: true, server },
|
|
584
|
+
)
|
|
585
|
+
// Tautologically equivalent to Extract<SearchEntry, { class: C }>, but TS
|
|
586
|
+
// cannot prove it on a generic C, so we assert.
|
|
587
|
+
return { class: cls, data, status } as Extract<SearchEntry, { class: C }>
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const strategies: { [K in SearchType]: SearchStrategy<K> } = {
|
|
593
|
+
datasets: makeStrategy('datasets', {
|
|
594
|
+
url: '/api/2/datasets/search/',
|
|
595
|
+
icon: RiDatabase2Line,
|
|
596
|
+
name: t('Jeux de données'),
|
|
597
|
+
placeholder: t('ex. élections présidentielles'),
|
|
598
|
+
}),
|
|
599
|
+
dataservices: makeStrategy('dataservices', {
|
|
600
|
+
url: '/api/2/dataservices/search/',
|
|
601
|
+
icon: RiTerminalLine,
|
|
602
|
+
name: t('API'),
|
|
603
|
+
placeholder: t('ex: SIRENE'),
|
|
604
|
+
}),
|
|
605
|
+
reuses: makeStrategy('reuses', {
|
|
606
|
+
url: '/api/2/reuses/search/',
|
|
607
|
+
icon: RiLineChartLine,
|
|
608
|
+
name: t('Réutilisations'),
|
|
609
|
+
placeholder: t('Rechercher une réutilisation de données'),
|
|
610
|
+
}),
|
|
611
|
+
organizations: makeStrategy('organizations', {
|
|
612
|
+
url: '/api/2/organizations/search/',
|
|
613
|
+
icon: RiBuilding2Line,
|
|
614
|
+
name: t('Organisations'),
|
|
615
|
+
placeholder: t('Rechercher une organisation'),
|
|
616
|
+
}),
|
|
617
|
+
topics: makeStrategy('topics', {
|
|
618
|
+
url: '/api/2/topics/search/',
|
|
619
|
+
icon: RiBookShelfLine,
|
|
620
|
+
name: t('Thématiques'),
|
|
621
|
+
placeholder: t('Rechercher une thématique'),
|
|
622
|
+
}),
|
|
623
|
+
}
|
|
529
624
|
|
|
530
|
-
//
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
625
|
+
// One params + fetch per config entry, keyed by configKey
|
|
626
|
+
const resultsMap: Record<string, SearchEntry> = {}
|
|
627
|
+
for (const c of props.config) {
|
|
628
|
+
const key = configKey(c)
|
|
629
|
+
const params = useStableQueryParams({ ...stableParamsOptions, typeConfig: c })
|
|
630
|
+
resultsMap[key] = await strategies[c.class].fetch(params, initialType === key)
|
|
631
|
+
}
|
|
535
632
|
|
|
536
|
-
// Reset page on filter/sort change
|
|
633
|
+
// Reset page on filter/sort change. Custom filters (registered via
|
|
634
|
+
// useSearchFilter) have their own watchers set up in `provide`, so they're
|
|
635
|
+
// intentionally excluded here to avoid resetting the page when a filter
|
|
636
|
+
// registers with its URL-derived value.
|
|
537
637
|
const filtersForReset = computed(() => ({
|
|
538
638
|
q: qDebounced.value,
|
|
539
639
|
organization: organizationId.value,
|
|
@@ -573,6 +673,7 @@ const hasFilters = computed(() => {
|
|
|
573
673
|
|| lastUpdateRange.value
|
|
574
674
|
|| producerType.value
|
|
575
675
|
|| reuseType.value
|
|
676
|
+
|| Array.from(customFilterRegistry.values()).some(isCustomFilterActive)
|
|
576
677
|
})
|
|
577
678
|
|
|
578
679
|
const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
|
|
@@ -593,73 +694,25 @@ function resetFilters() {
|
|
|
593
694
|
lastUpdateRange.value = undefined
|
|
594
695
|
producerType.value = undefined
|
|
595
696
|
reuseType.value = undefined
|
|
697
|
+
for (const entry of customFilterRegistry.values()) {
|
|
698
|
+
entry.ref.value = entry.defaultValue
|
|
699
|
+
}
|
|
596
700
|
q.value = ''
|
|
597
701
|
flushQ()
|
|
598
702
|
}
|
|
599
703
|
|
|
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)
|
|
704
|
+
const searchResults = computed(() => resultsMap[currentType.value]?.data.value)
|
|
705
|
+
const searchResultsStatus = computed(() => resultsMap[currentType.value]?.status.value)
|
|
652
706
|
|
|
653
707
|
// RSS feed URL for datasets
|
|
654
708
|
const rssUrl = computed(() => {
|
|
655
|
-
if (
|
|
709
|
+
if (currentTypeConfig.value?.class !== 'datasets') return null
|
|
656
710
|
|
|
657
711
|
const params = new URLSearchParams()
|
|
658
|
-
const datasetsConfig = props.config.find(c => c.class === 'datasets')
|
|
659
712
|
|
|
660
713
|
// Add hidden filters first
|
|
661
|
-
if (
|
|
662
|
-
for (const hf of
|
|
714
|
+
if (currentTypeConfig.value?.hiddenFilters) {
|
|
715
|
+
for (const hf of currentTypeConfig.value.hiddenFilters) {
|
|
663
716
|
if (hf?.value) params.set(hf.key as string, String(hf.value))
|
|
664
717
|
}
|
|
665
718
|
}
|
|
@@ -677,6 +730,10 @@ const rssUrl = computed(() => {
|
|
|
677
730
|
if (badge.value) params.set('badge', badge.value)
|
|
678
731
|
if (topic.value) params.set('topic', topic.value)
|
|
679
732
|
|
|
733
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
734
|
+
params.set(apiParam, value)
|
|
735
|
+
})
|
|
736
|
+
|
|
680
737
|
// Add sort if set
|
|
681
738
|
if (sort.value) params.set('sort', sort.value)
|
|
682
739
|
|
|
@@ -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>
|