@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.
Files changed (77) hide show
  1. package/assets/main.css +4 -0
  2. package/dist/Datafair.client-4zDFMXaE.js +30 -0
  3. package/dist/JsonPreview.client-DSKs3Wg7.js +40 -0
  4. package/dist/{MapContainer.client-DRkAmdOc.js → MapContainer.client-Cjwpar3k.js} +35 -38
  5. package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-6H3KMLOL.js} +822 -865
  6. package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-BDAAb3_H.js} +574 -579
  7. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BFSXn0mv.js +61 -0
  8. package/dist/XmlPreview.client-DnL8nMyL.js +34 -0
  9. package/dist/components-next.css +3 -3
  10. package/dist/components-next.js +140 -131
  11. package/dist/components.css +1 -1
  12. package/dist/{index-SrYZwgCT.js → index-7yWP5a5K.js} +1 -1
  13. package/dist/main-C-0Gkcks.js +73004 -0
  14. package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-xigir_CG.js} +1 -1
  15. package/package.json +14 -9
  16. package/src/components/ActivityList/ActivityList.vue +0 -2
  17. package/src/components/Form/SearchableSelect.vue +2 -1
  18. package/src/components/InfiniteLoader.vue +53 -0
  19. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  20. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  21. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  22. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  23. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  24. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  25. package/src/components/OpenApiViewer/openapi.ts +150 -0
  26. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  27. package/src/components/Pagination.vue +8 -5
  28. package/src/components/ReadMore.vue +1 -1
  29. package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
  30. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
  31. package/src/components/ResourceAccordion/MapContainer.client.vue +7 -11
  32. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  33. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -103
  34. package/src/components/ResourceAccordion/Pmtiles.client.vue +5 -10
  35. package/src/components/ResourceAccordion/Preview.vue +15 -20
  36. package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
  37. package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
  38. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  39. package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -7
  40. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
  41. package/src/components/ResourceExplorer/ResourceExplorer.vue +81 -13
  42. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  43. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +30 -11
  44. package/src/components/Search/GlobalSearch.vue +164 -107
  45. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  46. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  47. package/src/components/TabularExplorer/TabularExplorer.vue +870 -0
  48. package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
  49. package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
  50. package/src/components/TabularExplorer/types.ts +83 -0
  51. package/src/composables/useResourceCapabilities.ts +1 -1
  52. package/src/composables/useSearchFilter.ts +90 -0
  53. package/src/composables/useStableQueryParams.ts +28 -3
  54. package/src/config.ts +2 -0
  55. package/src/functions/api.ts +34 -33
  56. package/src/functions/api.types.ts +1 -0
  57. package/src/functions/datasets.ts +0 -17
  58. package/src/functions/resources.ts +56 -1
  59. package/src/functions/tabular.ts +60 -0
  60. package/src/functions/tabularApi.ts +4 -6
  61. package/src/main.ts +14 -6
  62. package/src/types/dataservices.ts +2 -0
  63. package/src/types/pages.ts +0 -5
  64. package/src/types/posts.ts +2 -2
  65. package/src/types/reports.ts +3 -0
  66. package/src/types/search.ts +45 -1
  67. package/src/types/site.ts +5 -3
  68. package/src/types/users.ts +0 -1
  69. package/assets/swagger-themes/newspaper.css +0 -1670
  70. package/dist/Datafair.client-E5D6ePRC.js +0 -35
  71. package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
  72. package/dist/Swagger.client-D4-F6yEf.js +0 -4
  73. package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
  74. package/dist/main-B2kXxWRG.js +0 -105833
  75. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  76. package/src/functions/pagination.ts +0 -9
  77. /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 || typesMeta[currentType].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.class"
34
- :value="typeConfig.class"
35
- :count="typesMeta[typeConfig.class].results.value?.total"
36
- :loading="typesMeta[typeConfig.class].status.value === 'pending' || typesMeta[typeConfig.class].status.value === 'idle'"
37
- :icon="typesMeta[typeConfig.class].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 || typesMeta[typeConfig.class].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="currentType === 'organizations' ? ['user'] : []"
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="currentType === 'datasets'">
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="currentType === 'dataservices'">
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="currentType === 'reuses'">
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="currentType === 'organizations'">
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 { getLink } from '../../functions/pagination'
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 { GlobalSearchConfig, SearchType, SortOption, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, OrganizationSearchResponse, FacetItem } from '../../types/search'
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<SearchType>('type') as Ref<SearchType>
389
- if (!currentType.value) currentType.value = props.config[0]?.class ?? 'datasets'
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.class === currentType.value),
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 showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0)
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
- const datasetsParams = useStableQueryParams({
514
- ...stableParamsOptions,
515
- typeConfig: props.config.find(c => c.class === 'datasets'),
516
- })
517
- const dataservicesParams = useStableQueryParams({
518
- ...stableParamsOptions,
519
- typeConfig: props.config.find(c => c.class === 'dataservices'),
520
- })
521
- const reusesParams = useStableQueryParams({
522
- ...stableParamsOptions,
523
- typeConfig: props.config.find(c => c.class === 'reuses'),
524
- })
525
- const organizationsParams = useStableQueryParams({
526
- ...stableParamsOptions,
527
- typeConfig: props.config.find(c => c.class === 'organizations'),
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
- // URLs that return null when type is not enabled
531
- const datasetsUrl = computed(() => datasetsEnabled.value ? '/api/2/datasets/search/' : null)
532
- const dataservicesUrl = computed(() => dataservicesEnabled.value ? '/api/2/dataservices/search/' : null)
533
- const reusesUrl = computed(() => reusesEnabled.value ? '/api/2/reuses/search/' : null)
534
- const organizationsUrl = computed(() => organizationsEnabled.value ? '/api/2/organizations/search/' : null)
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
- // API calls only for enabled types (useFetch skips when URL is null)
601
- // Only the initial type is fetched during SSR, others are client-side only
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 (currentType.value !== 'datasets') return null
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 (datasetsConfig?.hiddenFilters) {
662
- for (const hf of datasetsConfig.hiddenFilters) {
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>