@datagouv/components-next 1.0.2-dev.57 → 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-5ZJvZtsQ.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.57",
3
+ "version": "1.0.2-dev.59",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -7,7 +7,7 @@
7
7
  <component
8
8
  :is="as"
9
9
  class="mb-0 truncate flex-initial"
10
- :class="[colorClass, { 'text-xs': size === 'xs', 'text-sm': size === 'sm', 'text-base': size === 'base' }]"
10
+ :class="[colorClass, { 'text-xs': size === 'xs', 'text-sm': size === 'sm', 'text-base': size === 'base', 'text-xl sm:text-2xl': size === 'xl' }]"
11
11
  >
12
12
  {{ organization.name }}
13
13
  <small
@@ -24,6 +24,7 @@
24
24
  'size-3': size === 'xs',
25
25
  'size-4': size === 'sm',
26
26
  'size-5': size === 'base',
27
+ 'size-6': size === 'xl',
27
28
  }"
28
29
  :aria-label="t(`L'identité de ce service public est certifiée par {certifier}`, { certifier: config.name })"
29
30
  aria-hidden="true"
@@ -53,7 +54,7 @@ withDefaults(defineProps<{
53
54
  organization: Organization | OrganizationReference
54
55
  showAcronym?: boolean
55
56
  showType?: boolean
56
- size?: 'base' | 'sm' | 'xs'
57
+ size?: 'xl' | 'base' | 'sm' | 'xs'
57
58
  colorClass?: string
58
59
  as?: string
59
60
  }>(), {
@@ -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,13 +31,13 @@
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>
@@ -127,7 +128,7 @@
127
128
  v-model="producerType"
128
129
  :facets="getFacets('producer_type')"
129
130
  :loading="searchResultsStatus === 'pending'"
130
- :exclude="currentType === 'organizations' ? ['user'] : []"
131
+ :exclude="currentTypeConfig?.class === 'organizations' ? ['user'] : []"
131
132
  :style="{ order: getOrder('producer_type') }"
132
133
  />
133
134
  <DatasetBadgeFilter
@@ -229,7 +230,7 @@
229
230
  <transition mode="out-in">
230
231
  <LoadingBlock
231
232
  v-slot="{ data: results }"
232
- :status="searchResultsStatus"
233
+ :status="searchResultsStatus!"
233
234
  :data="searchResults"
234
235
  >
235
236
  <div v-if="results && results.data.length">
@@ -239,7 +240,7 @@
239
240
  :key="result.id"
240
241
  class="p-0"
241
242
  >
242
- <template v-if="currentType === 'datasets'">
243
+ <template v-if="currentTypeConfig?.class === 'datasets'">
243
244
  <slot
244
245
  name="dataset"
245
246
  :dataset="result"
@@ -247,7 +248,7 @@
247
248
  <DatasetCard :dataset="(result as Dataset)" />
248
249
  </slot>
249
250
  </template>
250
- <template v-else-if="currentType === 'dataservices'">
251
+ <template v-else-if="currentTypeConfig?.class === 'dataservices'">
251
252
  <slot
252
253
  name="dataservice"
253
254
  :dataservice="result"
@@ -255,7 +256,7 @@
255
256
  <DataserviceCard :dataservice="(result as Dataservice)" />
256
257
  </slot>
257
258
  </template>
258
- <template v-else-if="currentType === 'reuses'">
259
+ <template v-else-if="currentTypeConfig?.class === 'reuses'">
259
260
  <slot
260
261
  name="reuse"
261
262
  :reuse="result"
@@ -263,7 +264,7 @@
263
264
  <ReuseHorizontalCard :reuse="(result as Reuse)" />
264
265
  </slot>
265
266
  </template>
266
- <template v-else-if="currentType === 'organizations'">
267
+ <template v-else-if="currentTypeConfig?.class === 'organizations'">
267
268
  <slot
268
269
  name="organization"
269
270
  :organization="result"
@@ -271,7 +272,7 @@
271
272
  <OrganizationHorizontalCard :organization="(result as Organization)" />
272
273
  </slot>
273
274
  </template>
274
- <template v-else-if="currentType === 'topics'">
275
+ <template v-else-if="currentTypeConfig?.class === 'topics'">
275
276
  <slot
276
277
  name="topic"
277
278
  :topic="result"
@@ -348,7 +349,7 @@
348
349
  </template>
349
350
 
350
351
  <script setup lang="ts">
351
- 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'
352
353
  import { useRouteQuery } from '@vueuse/router'
353
354
  import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
354
355
  import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
@@ -358,12 +359,13 @@ import { forEachActiveCustomFilter, isCustomFilterActive, searchFilterContextKey
358
359
  import { useStableQueryParams } from '../../composables/useStableQueryParams'
359
360
  import { useComponentsConfig } from '../../config'
360
361
  import { useFetch } from '../../functions/api'
362
+ import type { AsyncDataRequestStatus } from '../../functions/api.types'
361
363
  import type { Dataset } from '../../types/datasets'
362
364
  import type { Dataservice } from '../../types/dataservices'
363
365
  import type { Organization } from '../../types/organizations'
364
366
  import type { Reuse } from '../../types/reuses'
365
367
  import type { TopicV2 } from '../../types/topics'
366
- 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'
367
369
  import { getDefaultGlobalSearchConfig } from '../../types/search'
368
370
  import BrandedButton from '../BrandedButton.vue'
369
371
  import LoadingBlock from '../LoadingBlock.vue'
@@ -397,13 +399,17 @@ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
397
399
  const props = withDefaults(defineProps<{
398
400
  config?: GlobalSearchConfig
399
401
  placeholder?: string
402
+ hideSearchInput?: boolean
400
403
  }>(), {
401
404
  config: getDefaultGlobalSearchConfig,
405
+ hideSearchInput: false,
402
406
  })
403
407
 
408
+ const configKey = (c: SearchTypeConfig) => c.key ?? c.class
409
+
404
410
  // defineModel's default is static and can't depend on props, so we cast and initialize manually
405
- const currentType = defineModel<SearchType>('type') as Ref<SearchType>
406
- 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' })
407
413
 
408
414
  const { t } = useTranslation()
409
415
  const componentsConfig = useComponentsConfig()
@@ -419,7 +425,7 @@ const customFilterStops = new Map<string, () => void>()
419
425
  const initialType = currentType.value
420
426
 
421
427
  const currentTypeConfig = computed(() =>
422
- props.config.find(c => c.class === currentType.value),
428
+ props.config.find(c => configKey(c) === currentType.value),
423
429
  )
424
430
 
425
431
  const activeBasicFilters = computed(() =>
@@ -532,13 +538,6 @@ watch(currentType, () => {
532
538
  }
533
539
  })
534
540
 
535
- // Check which types are enabled
536
- const datasetsEnabled = computed(() => props.config.some(c => c.class === 'datasets'))
537
- const dataservicesEnabled = computed(() => props.config.some(c => c.class === 'dataservices'))
538
- const reusesEnabled = computed(() => props.config.some(c => c.class === 'reuses'))
539
- const organizationsEnabled = computed(() => props.config.some(c => c.class === 'organizations'))
540
- const topicsEnabled = computed(() => props.config.some(c => c.class === 'topics'))
541
-
542
541
  // Create stable params for each type
543
542
  const stableParamsOptions = {
544
543
  allFilters,
@@ -549,33 +548,87 @@ const stableParamsOptions = {
549
548
  pageSize,
550
549
  }
551
550
 
552
- const datasetsParams = useStableQueryParams({
553
- ...stableParamsOptions,
554
- typeConfig: props.config.find(c => c.class === 'datasets'),
555
- })
556
- const dataservicesParams = useStableQueryParams({
557
- ...stableParamsOptions,
558
- typeConfig: props.config.find(c => c.class === 'dataservices'),
559
- })
560
- const reusesParams = useStableQueryParams({
561
- ...stableParamsOptions,
562
- typeConfig: props.config.find(c => c.class === 'reuses'),
563
- })
564
- const organizationsParams = useStableQueryParams({
565
- ...stableParamsOptions,
566
- typeConfig: props.config.find(c => c.class === 'organizations'),
567
- })
568
- const topicsParams = useStableQueryParams({
569
- ...stableParamsOptions,
570
- typeConfig: props.config.find(c => c.class === 'topics'),
571
- })
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
+ }
572
573
 
573
- // URLs that return null when type is not enabled
574
- const datasetsUrl = computed(() => datasetsEnabled.value ? '/api/2/datasets/search/' : null)
575
- const dataservicesUrl = computed(() => dataservicesEnabled.value ? '/api/2/dataservices/search/' : null)
576
- const reusesUrl = computed(() => reusesEnabled.value ? '/api/2/reuses/search/' : null)
577
- const organizationsUrl = computed(() => organizationsEnabled.value ? '/api/2/organizations/search/' : null)
578
- 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
+ }
579
632
 
580
633
  // Reset page on filter/sort change. Custom filters (registered via
581
634
  // useSearchFilter) have their own watchers set up in `provide`, so they're
@@ -648,80 +701,18 @@ function resetFilters() {
648
701
  flushQ()
649
702
  }
650
703
 
651
- // API calls only for enabled types (useFetch skips when URL is null)
652
- // Only the initial type is fetched during SSR, others are client-side only
653
- const { data: datasetsResults, status: datasetsStatus } = await useFetch<DatasetSearchResponse<Dataset>>(
654
- datasetsUrl,
655
- { params: datasetsParams, lazy: true, server: initialType === 'datasets' },
656
- )
657
- const { data: dataservicesResults, status: dataservicesStatus } = await useFetch<DataserviceSearchResponse<Dataservice>>(
658
- dataservicesUrl,
659
- { params: dataservicesParams, lazy: true, server: initialType === 'dataservices' },
660
- )
661
- const { data: reusesResults, status: reusesStatus } = await useFetch<ReuseSearchResponse<Reuse>>(
662
- reusesUrl,
663
- { params: reusesParams, lazy: true, server: initialType === 'reuses' },
664
- )
665
- const { data: organizationsResults, status: organizationsStatus } = await useFetch<OrganizationSearchResponse<Organization>>(
666
- organizationsUrl,
667
- { params: organizationsParams, lazy: true, server: initialType === 'organizations' },
668
- )
669
- const { data: topicsResults, status: topicsStatus } = await useFetch<TopicSearchResponse<TopicV2>>(
670
- topicsUrl,
671
- { params: topicsParams, lazy: true, server: initialType === 'topics' },
672
- )
673
-
674
- const typesMeta = {
675
- datasets: {
676
- icon: RiDatabase2Line,
677
- name: t('Jeux de données'),
678
- placeholder: t('ex. élections présidentielles'),
679
- results: datasetsResults,
680
- status: datasetsStatus,
681
- },
682
- dataservices: {
683
- icon: RiTerminalLine,
684
- name: t('API'),
685
- placeholder: t('ex: SIRENE'),
686
- results: dataservicesResults,
687
- status: dataservicesStatus,
688
- },
689
- reuses: {
690
- icon: RiLineChartLine,
691
- name: t('Réutilisations'),
692
- placeholder: t('Rechercher une réutilisation de données'),
693
- results: reusesResults,
694
- status: reusesStatus,
695
- },
696
- organizations: {
697
- icon: RiBuilding2Line,
698
- name: t('Organisations'),
699
- placeholder: t('Rechercher une organisation'),
700
- results: organizationsResults,
701
- status: organizationsStatus,
702
- },
703
- topics: {
704
- icon: RiBookShelfLine,
705
- name: t('Thématiques'),
706
- placeholder: t('Rechercher une thématique'),
707
- results: topicsResults,
708
- status: topicsStatus,
709
- },
710
- } as const
711
-
712
- const searchResults = computed(() => typesMeta[currentType.value].results.value)
713
- 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)
714
706
 
715
707
  // RSS feed URL for datasets
716
708
  const rssUrl = computed(() => {
717
- if (currentType.value !== 'datasets') return null
709
+ if (currentTypeConfig.value?.class !== 'datasets') return null
718
710
 
719
711
  const params = new URLSearchParams()
720
- const datasetsConfig = props.config.find(c => c.class === 'datasets')
721
712
 
722
713
  // Add hidden filters first
723
- if (datasetsConfig?.hiddenFilters) {
724
- for (const hf of datasetsConfig.hiddenFilters) {
714
+ if (currentTypeConfig.value?.hiddenFilters) {
715
+ for (const hf of currentTypeConfig.value.hiddenFilters) {
725
716
  if (hf?.value) params.set(hf.key as string, String(hf.value))
726
717
  }
727
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>[] = [