@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.
Files changed (78) hide show
  1. package/assets/main.css +4 -0
  2. package/dist/Datafair.client-CZBhcl2N.js +30 -0
  3. package/dist/JsonPreview.client-BFXc8Yds.js +40 -0
  4. package/dist/{MapContainer.client-DRkAmdOc.js → MapContainer.client-DXvpJcmy.js} +35 -38
  5. package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-C4rWJaSZ.js} +822 -865
  6. package/dist/{Pmtiles.client-BR7_ldHY.js → Pmtiles.client-ByvF0n7m.js} +574 -579
  7. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-B6mL96Bm.js +61 -0
  8. package/dist/XmlPreview.client-aAATX5FP.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-9wBlcysa.js} +1 -1
  13. package/dist/main-DSV-Ai-o.js +73008 -0
  14. package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-EQMYhxMa.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 +174 -108
  45. package/src/components/Search/SearchInput.vue +3 -3
  46. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  47. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  48. package/src/components/TabularExplorer/TabularExplorer.vue +870 -0
  49. package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
  50. package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
  51. package/src/components/TabularExplorer/types.ts +83 -0
  52. package/src/composables/useResourceCapabilities.ts +1 -1
  53. package/src/composables/useSearchFilter.ts +90 -0
  54. package/src/composables/useStableQueryParams.ts +28 -3
  55. package/src/config.ts +2 -0
  56. package/src/functions/api.ts +34 -33
  57. package/src/functions/api.types.ts +1 -0
  58. package/src/functions/datasets.ts +0 -17
  59. package/src/functions/resources.ts +56 -1
  60. package/src/functions/tabular.ts +60 -0
  61. package/src/functions/tabularApi.ts +4 -6
  62. package/src/main.ts +14 -6
  63. package/src/types/dataservices.ts +2 -0
  64. package/src/types/pages.ts +0 -5
  65. package/src/types/posts.ts +2 -2
  66. package/src/types/reports.ts +3 -0
  67. package/src/types/search.ts +50 -1
  68. package/src/types/site.ts +5 -3
  69. package/src/types/users.ts +0 -1
  70. package/assets/swagger-themes/newspaper.css +0 -1670
  71. package/dist/Datafair.client-E5D6ePRC.js +0 -35
  72. package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
  73. package/dist/Swagger.client-D4-F6yEf.js +0 -4
  74. package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
  75. package/dist/main-B2kXxWRG.js +0 -105833
  76. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  77. package/src/functions/pagination.ts +0 -9
  78. /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="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.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'
@@ -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<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
 
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 showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0)
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
- 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
- })
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
- // 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)
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
- // 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)
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 (currentType.value !== 'datasets') return null
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 (datasetsConfig?.hiddenFilters) {
662
- for (const hf of datasetsConfig.hiddenFilters) {
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 || t('Rechercher...')"
17
- :placeholder="placeholder || t('Rechercher...')"
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>