@datagouv/components-next 1.0.2-dev.58 → 1.0.2-dev.59

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.
@@ -1,4 +1,4 @@
1
- import { c as Ke } from "./main-y0HiPYX3.js";
1
+ import { c as Ke } from "./main-jWuVtc9s.js";
2
2
  import We from "vue";
3
3
  function Fe(I, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.58",
3
+ "version": "1.0.2-dev.59",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,7 +12,7 @@
12
12
  >
13
13
  <SearchInput
14
14
  v-model="q"
15
- :placeholder="placeholder || typesMeta[currentType].placeholder"
15
+ :placeholder="placeholder || strategies[currentTypeConfig?.class ?? 'datasets'].placeholder"
16
16
  />
17
17
  </div>
18
18
  <div class="grid grid-cols-12 mt-2 md:mt-5">
@@ -31,13 +31,13 @@
31
31
  >
32
32
  <RadioInput
33
33
  v-for="typeConfig in config"
34
- :key="typeConfig.class"
35
- :value="typeConfig.class"
36
- :count="typesMeta[typeConfig.class].results.value?.total"
37
- :loading="typesMeta[typeConfig.class].status.value === 'pending' || typesMeta[typeConfig.class].status.value === 'idle'"
38
- :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"
39
39
  >
40
- {{ typeConfig.name || typesMeta[typeConfig.class].name }}
40
+ {{ typeConfig.name || strategies[typeConfig.class].name }}
41
41
  </RadioInput>
42
42
  </RadioGroup>
43
43
  </Sidemenu>
@@ -128,7 +128,7 @@
128
128
  v-model="producerType"
129
129
  :facets="getFacets('producer_type')"
130
130
  :loading="searchResultsStatus === 'pending'"
131
- :exclude="currentType === 'organizations' ? ['user'] : []"
131
+ :exclude="currentTypeConfig?.class === 'organizations' ? ['user'] : []"
132
132
  :style="{ order: getOrder('producer_type') }"
133
133
  />
134
134
  <DatasetBadgeFilter
@@ -230,7 +230,7 @@
230
230
  <transition mode="out-in">
231
231
  <LoadingBlock
232
232
  v-slot="{ data: results }"
233
- :status="searchResultsStatus"
233
+ :status="searchResultsStatus!"
234
234
  :data="searchResults"
235
235
  >
236
236
  <div v-if="results && results.data.length">
@@ -240,7 +240,7 @@
240
240
  :key="result.id"
241
241
  class="p-0"
242
242
  >
243
- <template v-if="currentType === 'datasets'">
243
+ <template v-if="currentTypeConfig?.class === 'datasets'">
244
244
  <slot
245
245
  name="dataset"
246
246
  :dataset="result"
@@ -248,7 +248,7 @@
248
248
  <DatasetCard :dataset="(result as Dataset)" />
249
249
  </slot>
250
250
  </template>
251
- <template v-else-if="currentType === 'dataservices'">
251
+ <template v-else-if="currentTypeConfig?.class === 'dataservices'">
252
252
  <slot
253
253
  name="dataservice"
254
254
  :dataservice="result"
@@ -256,7 +256,7 @@
256
256
  <DataserviceCard :dataservice="(result as Dataservice)" />
257
257
  </slot>
258
258
  </template>
259
- <template v-else-if="currentType === 'reuses'">
259
+ <template v-else-if="currentTypeConfig?.class === 'reuses'">
260
260
  <slot
261
261
  name="reuse"
262
262
  :reuse="result"
@@ -264,7 +264,7 @@
264
264
  <ReuseHorizontalCard :reuse="(result as Reuse)" />
265
265
  </slot>
266
266
  </template>
267
- <template v-else-if="currentType === 'organizations'">
267
+ <template v-else-if="currentTypeConfig?.class === 'organizations'">
268
268
  <slot
269
269
  name="organization"
270
270
  :organization="result"
@@ -272,7 +272,7 @@
272
272
  <OrganizationHorizontalCard :organization="(result as Organization)" />
273
273
  </slot>
274
274
  </template>
275
- <template v-else-if="currentType === 'topics'">
275
+ <template v-else-if="currentTypeConfig?.class === 'topics'">
276
276
  <slot
277
277
  name="topic"
278
278
  :topic="result"
@@ -349,7 +349,7 @@
349
349
  </template>
350
350
 
351
351
  <script setup lang="ts">
352
- import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Ref } from 'vue'
352
+ import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
353
353
  import { useRouteQuery } from '@vueuse/router'
354
354
  import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
355
355
  import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
@@ -359,12 +359,13 @@ import { forEachActiveCustomFilter, isCustomFilterActive, searchFilterContextKey
359
359
  import { useStableQueryParams } from '../../composables/useStableQueryParams'
360
360
  import { useComponentsConfig } from '../../config'
361
361
  import { useFetch } from '../../functions/api'
362
+ import type { AsyncDataRequestStatus } from '../../functions/api.types'
362
363
  import type { Dataset } from '../../types/datasets'
363
364
  import type { Dataservice } from '../../types/dataservices'
364
365
  import type { Organization } from '../../types/organizations'
365
366
  import type { Reuse } from '../../types/reuses'
366
367
  import type { TopicV2 } from '../../types/topics'
367
- import type { GlobalSearchConfig, SearchType, SortOption, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, OrganizationSearchResponse, TopicSearchResponse, FacetItem } from '../../types/search'
368
+ import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SearchTypeConfig, SortOption, FacetItem } from '../../types/search'
368
369
  import { getDefaultGlobalSearchConfig } from '../../types/search'
369
370
  import BrandedButton from '../BrandedButton.vue'
370
371
  import LoadingBlock from '../LoadingBlock.vue'
@@ -404,9 +405,11 @@ const props = withDefaults(defineProps<{
404
405
  hideSearchInput: false,
405
406
  })
406
407
 
408
+ const configKey = (c: SearchTypeConfig) => c.key ?? c.class
409
+
407
410
  // defineModel's default is static and can't depend on props, so we cast and initialize manually
408
- const currentType = defineModel<SearchType>('type') as Ref<SearchType>
409
- 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' })
410
413
 
411
414
  const { t } = useTranslation()
412
415
  const componentsConfig = useComponentsConfig()
@@ -422,7 +425,7 @@ const customFilterStops = new Map<string, () => void>()
422
425
  const initialType = currentType.value
423
426
 
424
427
  const currentTypeConfig = computed(() =>
425
- props.config.find(c => c.class === currentType.value),
428
+ props.config.find(c => configKey(c) === currentType.value),
426
429
  )
427
430
 
428
431
  const activeBasicFilters = computed(() =>
@@ -535,13 +538,6 @@ watch(currentType, () => {
535
538
  }
536
539
  })
537
540
 
538
- // Check which types are enabled
539
- const datasetsEnabled = computed(() => props.config.some(c => c.class === 'datasets'))
540
- const dataservicesEnabled = computed(() => props.config.some(c => c.class === 'dataservices'))
541
- const reusesEnabled = computed(() => props.config.some(c => c.class === 'reuses'))
542
- const organizationsEnabled = computed(() => props.config.some(c => c.class === 'organizations'))
543
- const topicsEnabled = computed(() => props.config.some(c => c.class === 'topics'))
544
-
545
541
  // Create stable params for each type
546
542
  const stableParamsOptions = {
547
543
  allFilters,
@@ -552,33 +548,87 @@ const stableParamsOptions = {
552
548
  pageSize,
553
549
  }
554
550
 
555
- const datasetsParams = useStableQueryParams({
556
- ...stableParamsOptions,
557
- typeConfig: props.config.find(c => c.class === 'datasets'),
558
- })
559
- const dataservicesParams = useStableQueryParams({
560
- ...stableParamsOptions,
561
- typeConfig: props.config.find(c => c.class === 'dataservices'),
562
- })
563
- const reusesParams = useStableQueryParams({
564
- ...stableParamsOptions,
565
- typeConfig: props.config.find(c => c.class === 'reuses'),
566
- })
567
- const organizationsParams = useStableQueryParams({
568
- ...stableParamsOptions,
569
- typeConfig: props.config.find(c => c.class === 'organizations'),
570
- })
571
- const topicsParams = useStableQueryParams({
572
- ...stableParamsOptions,
573
- typeConfig: props.config.find(c => c.class === 'topics'),
574
- })
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
+ }
575
573
 
576
- // URLs that return null when type is not enabled
577
- const datasetsUrl = computed(() => datasetsEnabled.value ? '/api/2/datasets/search/' : null)
578
- const dataservicesUrl = computed(() => dataservicesEnabled.value ? '/api/2/dataservices/search/' : null)
579
- const reusesUrl = computed(() => reusesEnabled.value ? '/api/2/reuses/search/' : null)
580
- const organizationsUrl = computed(() => organizationsEnabled.value ? '/api/2/organizations/search/' : null)
581
- const topicsUrl = computed(() => topicsEnabled.value ? '/api/2/topics/search/' : null)
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
+ }
624
+
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
+ }
582
632
 
583
633
  // Reset page on filter/sort change. Custom filters (registered via
584
634
  // useSearchFilter) have their own watchers set up in `provide`, so they're
@@ -651,80 +701,18 @@ function resetFilters() {
651
701
  flushQ()
652
702
  }
653
703
 
654
- // API calls only for enabled types (useFetch skips when URL is null)
655
- // Only the initial type is fetched during SSR, others are client-side only
656
- const { data: datasetsResults, status: datasetsStatus } = await useFetch<DatasetSearchResponse<Dataset>>(
657
- datasetsUrl,
658
- { params: datasetsParams, lazy: true, server: initialType === 'datasets' },
659
- )
660
- const { data: dataservicesResults, status: dataservicesStatus } = await useFetch<DataserviceSearchResponse<Dataservice>>(
661
- dataservicesUrl,
662
- { params: dataservicesParams, lazy: true, server: initialType === 'dataservices' },
663
- )
664
- const { data: reusesResults, status: reusesStatus } = await useFetch<ReuseSearchResponse<Reuse>>(
665
- reusesUrl,
666
- { params: reusesParams, lazy: true, server: initialType === 'reuses' },
667
- )
668
- const { data: organizationsResults, status: organizationsStatus } = await useFetch<OrganizationSearchResponse<Organization>>(
669
- organizationsUrl,
670
- { params: organizationsParams, lazy: true, server: initialType === 'organizations' },
671
- )
672
- const { data: topicsResults, status: topicsStatus } = await useFetch<TopicSearchResponse<TopicV2>>(
673
- topicsUrl,
674
- { params: topicsParams, lazy: true, server: initialType === 'topics' },
675
- )
676
-
677
- const typesMeta = {
678
- datasets: {
679
- icon: RiDatabase2Line,
680
- name: t('Jeux de données'),
681
- placeholder: t('ex. élections présidentielles'),
682
- results: datasetsResults,
683
- status: datasetsStatus,
684
- },
685
- dataservices: {
686
- icon: RiTerminalLine,
687
- name: t('API'),
688
- placeholder: t('ex: SIRENE'),
689
- results: dataservicesResults,
690
- status: dataservicesStatus,
691
- },
692
- reuses: {
693
- icon: RiLineChartLine,
694
- name: t('Réutilisations'),
695
- placeholder: t('Rechercher une réutilisation de données'),
696
- results: reusesResults,
697
- status: reusesStatus,
698
- },
699
- organizations: {
700
- icon: RiBuilding2Line,
701
- name: t('Organisations'),
702
- placeholder: t('Rechercher une organisation'),
703
- results: organizationsResults,
704
- status: organizationsStatus,
705
- },
706
- topics: {
707
- icon: RiBookShelfLine,
708
- name: t('Thématiques'),
709
- placeholder: t('Rechercher une thématique'),
710
- results: topicsResults,
711
- status: topicsStatus,
712
- },
713
- } as const
714
-
715
- const searchResults = computed(() => typesMeta[currentType.value].results.value)
716
- 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)
717
706
 
718
707
  // RSS feed URL for datasets
719
708
  const rssUrl = computed(() => {
720
- if (currentType.value !== 'datasets') return null
709
+ if (currentTypeConfig.value?.class !== 'datasets') return null
721
710
 
722
711
  const params = new URLSearchParams()
723
- const datasetsConfig = props.config.find(c => c.class === 'datasets')
724
712
 
725
713
  // Add hidden filters first
726
- if (datasetsConfig?.hiddenFilters) {
727
- for (const hf of datasetsConfig.hiddenFilters) {
714
+ if (currentTypeConfig.value?.hiddenFilters) {
715
+ for (const hf of currentTypeConfig.value.hiddenFilters) {
728
716
  if (hf?.value) params.set(hf.key as string, String(hf.value))
729
717
  }
730
718
  }
@@ -1,5 +1,10 @@
1
1
  import type { PaginatedArray } from './api'
2
2
  import type { AccessType } from './access_types'
3
+ import type { Dataset } from './datasets'
4
+ import type { Dataservice } from './dataservices'
5
+ import type { Organization } from './organizations'
6
+ import type { Reuse } from './reuses'
7
+ import type { TopicV2 } from './topics'
3
8
  import type {
4
9
  CERTIFIED,
5
10
  PUBLIC_SERVICE,
@@ -291,6 +296,7 @@ export type SortOption<Sort extends string> = {
291
296
 
292
297
  export type DatasetSearchConfig = {
293
298
  class: 'datasets'
299
+ key?: string
294
300
  name?: string
295
301
  hiddenFilters?: HiddenFilter<DatasetSearchFilters>[]
296
302
  basicFilters?: (keyof DatasetSearchFilters)[]
@@ -300,6 +306,7 @@ export type DatasetSearchConfig = {
300
306
 
301
307
  export type DataserviceSearchConfig = {
302
308
  class: 'dataservices'
309
+ key?: string
303
310
  name?: string
304
311
  hiddenFilters?: HiddenFilter<DataserviceSearchFilters>[]
305
312
  basicFilters?: (keyof DataserviceSearchFilters)[]
@@ -309,6 +316,7 @@ export type DataserviceSearchConfig = {
309
316
 
310
317
  export type ReuseSearchConfig = {
311
318
  class: 'reuses'
319
+ key?: string
312
320
  name?: string
313
321
  hiddenFilters?: HiddenFilter<ReuseSearchFilters>[]
314
322
  basicFilters?: (keyof ReuseSearchFilters)[]
@@ -318,6 +326,7 @@ export type ReuseSearchConfig = {
318
326
 
319
327
  export type OrganizationSearchConfig = {
320
328
  class: 'organizations'
329
+ key?: string
321
330
  name?: string
322
331
  hiddenFilters?: HiddenFilter<OrganizationSearchFilters>[]
323
332
  basicFilters?: (keyof OrganizationSearchFilters)[]
@@ -327,6 +336,7 @@ export type OrganizationSearchConfig = {
327
336
 
328
337
  export type TopicSearchConfig = {
329
338
  class: 'topics'
339
+ key?: string
330
340
  name?: string
331
341
  hiddenFilters?: HiddenFilter<TopicSearchFilters>[]
332
342
  basicFilters?: (keyof TopicSearchFilters)[]
@@ -340,6 +350,15 @@ export type SearchType = SearchTypeConfig['class']
340
350
 
341
351
  export type GlobalSearchConfig = SearchTypeConfig[]
342
352
 
353
+ // Maps each search class to its concrete response shape.
354
+ export type SearchResponseByClass = {
355
+ datasets: DatasetSearchResponse<Dataset>
356
+ dataservices: DataserviceSearchResponse<Dataservice>
357
+ reuses: ReuseSearchResponse<Reuse>
358
+ organizations: OrganizationSearchResponse<Organization>
359
+ topics: TopicSearchResponse<TopicV2>
360
+ }
361
+
343
362
  // Helper functions for default configs
344
363
 
345
364
  export const defaultDatasetSortOptions: SortOption<DatasetSearchSort>[] = [