@datagouv/components-next 0.2.0 → 1.0.0

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 (140) hide show
  1. package/README.md +1 -1
  2. package/assets/main.css +56 -1
  3. package/dist/Control-BNCDn-8E.js +148 -0
  4. package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-Dls5AHTE.js} +1 -1
  5. package/dist/Event-BOgJUhNR.js +738 -0
  6. package/dist/Image-BN-4XkIn.js +247 -0
  7. package/dist/{JsonPreview.client-BMsC5JcY.js → JsonPreview.client-DPDTs433.js} +14 -14
  8. package/dist/Map-BdT3i2C4.js +7609 -0
  9. package/dist/MapContainer.client-BdAzd7bj.js +105 -0
  10. package/dist/OSM-CamriM9b.js +71 -0
  11. package/dist/{PdfPreview.client-COOkEkRA.js → PdfPreview.client-CopqSDyt.js} +3 -3
  12. package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-mF6xaOO_.js} +2 -2
  13. package/dist/ScaleLine-BiesrgOv.js +165 -0
  14. package/dist/Swagger.client-eJ7gpfZA.js +4 -0
  15. package/dist/Tile-DCuqwNOI.js +1206 -0
  16. package/dist/TileImage-CmZf8EdU.js +1067 -0
  17. package/dist/View-DcDc7N2K.js +2858 -0
  18. package/dist/{XmlPreview.client-CAdN0w_Y.js → XmlPreview.client-C0OgBkSq.js} +7 -7
  19. package/dist/common-C4rDcQpp.js +243 -0
  20. package/dist/components-next.css +1 -1
  21. package/dist/components-next.js +153 -117
  22. package/dist/components.css +1 -1
  23. package/dist/{MapContainer.client-DeSo8EvG.js → index-BRGqW8aQ.js} +4975 -21416
  24. package/dist/leaflet-src-7m1mB8LI.js +6338 -0
  25. package/dist/{main-Dgri3TQL.js → main-CNHxAJ8J.js} +56758 -51450
  26. package/dist/proj-CKwYjU38.js +1569 -0
  27. package/dist/tilecoord-YW3qEH_j.js +884 -0
  28. package/dist/{vue3-xml-viewer.common-D6skc_Ai.js → vue3-xml-viewer.common-CmAdQfIy.js} +1 -1
  29. package/package.json +5 -1
  30. package/src/components/ActivityList/ActivityList.vue +6 -2
  31. package/src/components/AppLink.vue +4 -1
  32. package/src/components/Avatar.vue +2 -2
  33. package/src/components/AvatarWithName.vue +8 -4
  34. package/src/components/BouncingDots.vue +21 -0
  35. package/src/components/BrandedButton.vue +2 -0
  36. package/src/components/CopyButton.vue +19 -7
  37. package/src/components/DataserviceCard.vue +83 -118
  38. package/src/components/DatasetCard.vue +110 -171
  39. package/src/components/DatasetInformation/DatasetEmbedSection.vue +43 -0
  40. package/src/components/DatasetInformation/DatasetInformationSection.vue +73 -0
  41. package/src/components/DatasetInformation/DatasetSchemaSection.vue +74 -0
  42. package/src/components/DatasetInformation/DatasetSpatialSection.vue +59 -0
  43. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +45 -0
  44. package/src/components/DatasetInformation/index.ts +5 -0
  45. package/src/components/DatasetQualityTooltipContent.vue +3 -3
  46. package/src/components/DescriptionList.vue +1 -4
  47. package/src/components/DescriptionListDetails.vue +5 -0
  48. package/src/components/DescriptionListTerm.vue +5 -0
  49. package/src/components/DiscussionMessageCard.vue +63 -0
  50. package/src/components/ExtraAccordion.vue +4 -4
  51. package/src/components/Form/BadgeSelect.vue +35 -0
  52. package/src/components/Form/FormatSelect.vue +28 -0
  53. package/src/components/Form/GeozoneSelect.vue +52 -0
  54. package/src/components/Form/GranularitySelect.vue +29 -0
  55. package/src/components/Form/LicenseSelect.vue +30 -0
  56. package/src/components/Form/OrganizationSelect.vue +62 -0
  57. package/src/components/Form/OrganizationTypeSelect.vue +34 -0
  58. package/src/components/Form/ReuseTopicSelect.vue +29 -0
  59. package/src/components/Form/SchemaSelect.vue +30 -0
  60. package/src/components/Form/SearchableSelect.vue +334 -0
  61. package/src/components/Form/SelectGroup.vue +132 -0
  62. package/src/components/Form/TagSelect.vue +38 -0
  63. package/src/components/LeafletMap.vue +31 -0
  64. package/src/components/LicenseBadge.vue +24 -0
  65. package/src/components/LoadingBlock.vue +23 -2
  66. package/src/components/MarkdownViewer.vue +3 -1
  67. package/src/components/ObjectCard.vue +42 -0
  68. package/src/components/ObjectCardBadge.vue +22 -0
  69. package/src/components/ObjectCardHeader.vue +35 -0
  70. package/src/components/ObjectCardOwner.vue +43 -0
  71. package/src/components/ObjectCardShortDescription.vue +28 -0
  72. package/src/components/OrganizationCard.vue +35 -20
  73. package/src/components/OrganizationLogo.vue +1 -1
  74. package/src/components/OrganizationNameWithCertificate.vue +13 -7
  75. package/src/components/OwnerTypeIcon.vue +1 -0
  76. package/src/components/Pagination.vue +1 -1
  77. package/src/components/Placeholder.vue +5 -2
  78. package/src/components/PostCard.vue +62 -0
  79. package/src/components/RadioGroup.vue +32 -0
  80. package/src/components/RadioInput.vue +64 -0
  81. package/src/components/ResourceAccordion/EditButton.vue +2 -3
  82. package/src/components/ResourceAccordion/MapContainer.client.vue +20 -16
  83. package/src/components/ResourceAccordion/Metadata.vue +11 -24
  84. package/src/components/ResourceAccordion/Pmtiles.client.vue +1 -1
  85. package/src/components/ResourceAccordion/Preview.vue +1 -1
  86. package/src/components/ResourceAccordion/ResourceAccordion.vue +30 -20
  87. package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
  88. package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
  89. package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
  90. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
  91. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +361 -0
  92. package/src/components/ReuseCard.vue +8 -28
  93. package/src/components/ReuseHorizontalCard.vue +80 -0
  94. package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
  95. package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
  96. package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
  97. package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
  98. package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
  99. package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
  100. package/src/components/Search/Filter/ProducerTypeFilter.vue +39 -0
  101. package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
  102. package/src/components/Search/GlobalSearch.vue +611 -0
  103. package/src/components/Search/SearchInput.vue +63 -0
  104. package/src/components/Search/Sidemenu.vue +38 -0
  105. package/src/components/StatBox.vue +5 -5
  106. package/src/components/Tag.vue +30 -0
  107. package/src/components/Toggletip.vue +6 -2
  108. package/src/components/Tooltip.vue +2 -3
  109. package/src/components/TopicCard.vue +134 -0
  110. package/src/components/radioGroupContext.ts +9 -0
  111. package/src/composables/useDebouncedRef.ts +31 -0
  112. package/src/composables/useMetrics.ts +4 -3
  113. package/src/composables/useResourceCapabilities.ts +118 -0
  114. package/src/composables/useRouteQueryBoolean.ts +10 -0
  115. package/src/composables/useSelectModelSync.ts +89 -0
  116. package/src/composables/useStableQueryParams.ts +84 -0
  117. package/src/config.ts +4 -0
  118. package/src/functions/api.ts +17 -6
  119. package/src/functions/api.types.ts +4 -2
  120. package/src/functions/datasets.ts +1 -29
  121. package/src/functions/description.ts +33 -0
  122. package/src/functions/helpers.ts +11 -0
  123. package/src/functions/markdown.ts +60 -16
  124. package/src/functions/metrics.ts +33 -0
  125. package/src/functions/organizations.ts +5 -5
  126. package/src/main.ts +89 -7
  127. package/src/types/dataservices.ts +14 -12
  128. package/src/types/datasets.ts +20 -7
  129. package/src/types/discussions.ts +20 -0
  130. package/src/types/licenses.ts +3 -3
  131. package/src/types/organizations.ts +13 -1
  132. package/src/types/owned.ts +4 -2
  133. package/src/types/pages.ts +70 -0
  134. package/src/types/posts.ts +27 -0
  135. package/src/types/resources.ts +6 -0
  136. package/src/types/reuses.ts +14 -5
  137. package/src/types/search.ts +379 -0
  138. package/src/types/users.ts +12 -3
  139. package/dist/Swagger.client-CpLgaLg6.js +0 -4
  140. package/src/components/DatasetInformationPanel.vue +0 -211
@@ -0,0 +1,611 @@
1
+ <template>
2
+ <form
3
+ class="group/form"
4
+ data-input-color="blue"
5
+ @submit.prevent
6
+ >
7
+ <div
8
+ ref="search"
9
+ class="flex flex-wrap items-center justify-between"
10
+ data-cy="search"
11
+ >
12
+ <SearchInput
13
+ v-model="q"
14
+ :placeholder="placeholder || t('Ex : élection présidentielle 2022')"
15
+ />
16
+ </div>
17
+ <div class="grid grid-cols-12 mt-2 md:mt-5">
18
+ <div
19
+ v-if="showSidebar"
20
+ class="col-span-12 md:col-span-4 lg:col-span-3 md:space-y-8"
21
+ >
22
+ <div v-if="config.length > 1">
23
+ <Sidemenu :button-text="t('Type')">
24
+ <template #title>
25
+ {{ t('Type') }}
26
+ </template>
27
+ <RadioGroup
28
+ v-model="currentType"
29
+ name="search-type"
30
+ >
31
+ <RadioInput
32
+ 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"
38
+ >
39
+ {{ typeConfig.name || typesMeta[typeConfig.class].name }}
40
+ </RadioInput>
41
+ </RadioGroup>
42
+ </Sidemenu>
43
+ </div>
44
+
45
+ <div v-if="activeFilters.length > 0">
46
+ <Sidemenu :button-text="t('Filtres')">
47
+ <template #title>
48
+ {{ t('Filtres') }}
49
+ </template>
50
+ <BasicAndAdvancedFilters
51
+ v-slot="{ isEnabled, getOrder }"
52
+ :basic-filters="activeBasicFilters"
53
+ :advanced-filters="activeAdvancedFilters"
54
+ >
55
+ <OrganizationSelect
56
+ v-if="isEnabled('organization')"
57
+ v-model:id="organizationId"
58
+ :style="{ order: getOrder('organization') }"
59
+ />
60
+ <OrganizationTypeSelect
61
+ v-if="isEnabled('organization_badge')"
62
+ v-model="organizationType"
63
+ :style="{ order: getOrder('organization_badge') }"
64
+ />
65
+ <TagSelect
66
+ v-if="isEnabled('tag')"
67
+ v-model:id="tag"
68
+ :style="{ order: getOrder('tag') }"
69
+ />
70
+ <FormatSelect
71
+ v-if="isEnabled('format')"
72
+ v-model:id="format"
73
+ :style="{ order: getOrder('format') }"
74
+ />
75
+ <LicenseSelect
76
+ v-if="isEnabled('license')"
77
+ v-model:id="license"
78
+ :style="{ order: getOrder('license') }"
79
+ />
80
+ <SchemaSelect
81
+ v-if="isEnabled('schema')"
82
+ v-model:id="schema"
83
+ :style="{ order: getOrder('schema') }"
84
+ />
85
+ <GeozoneSelect
86
+ v-if="isEnabled('geozone')"
87
+ v-model:id="geozone"
88
+ :style="{ order: getOrder('geozone') }"
89
+ />
90
+ <GranularitySelect
91
+ v-if="isEnabled('granularity')"
92
+ v-model:id="granularity"
93
+ :style="{ order: getOrder('granularity') }"
94
+ />
95
+ <ReuseTopicSelect
96
+ v-if="isEnabled('topic')"
97
+ v-model:id="topic"
98
+ :style="{ order: getOrder('topic') }"
99
+ />
100
+ <FormatFamilyFilter
101
+ v-if="isEnabled('format_family')"
102
+ v-model="formatFamily"
103
+ :facets="getFacets('format_family')"
104
+ :loading="searchResultsStatus === 'pending'"
105
+ :style="{ order: getOrder('format_family') }"
106
+ />
107
+ <AccessTypeFilter
108
+ v-if="isEnabled('access_type')"
109
+ v-model="accessType"
110
+ :facets="getFacets('access_type')"
111
+ :loading="searchResultsStatus === 'pending'"
112
+ :style="{ order: getOrder('access_type') }"
113
+ />
114
+ <LastUpdateRangeFilter
115
+ v-if="isEnabled('last_update_range')"
116
+ v-model="lastUpdateRange"
117
+ :facets="getFacets('last_update')"
118
+ :loading="searchResultsStatus === 'pending'"
119
+ :style="{ order: getOrder('last_update_range') }"
120
+ />
121
+ <ProducerTypeFilter
122
+ v-if="isEnabled('producer_type')"
123
+ v-model="producerType"
124
+ :facets="getFacets('producer_type')"
125
+ :loading="searchResultsStatus === 'pending'"
126
+ :style="{ order: getOrder('producer_type') }"
127
+ />
128
+ <DatasetBadgeFilter
129
+ v-if="isEnabled('badge')"
130
+ v-model="badge"
131
+ :facets="getFacets('badge')"
132
+ :loading="searchResultsStatus === 'pending'"
133
+ :style="{ order: getOrder('badge') }"
134
+ />
135
+ <ReuseTypeFilter
136
+ v-if="isEnabled('type')"
137
+ v-model="reuseType"
138
+ :facets="getFacets('type')"
139
+ :loading="searchResultsStatus === 'pending'"
140
+ :style="{ order: getOrder('type') }"
141
+ />
142
+ <slot
143
+ name="filters"
144
+ :is-enabled="isEnabled"
145
+ :get-order="getOrder"
146
+ />
147
+ </BasicAndAdvancedFilters>
148
+ <div
149
+ v-if="hasFilters"
150
+ class="mt-6 text-center"
151
+ >
152
+ <BrandedButton
153
+ color="secondary"
154
+ :icon="RiCloseCircleLine"
155
+ class="w-full justify-center"
156
+ type="button"
157
+ @click="resetFilters"
158
+ >
159
+ {{ t('Réinitialiser les filtres') }}
160
+ </BrandedButton>
161
+ </div>
162
+ </Sidemenu>
163
+ </div>
164
+ </div>
165
+ <section
166
+ ref="results"
167
+ class="col-span-12 mt-4 md:mt-0 search-results"
168
+ :class="showSidebar ? 'md:col-span-8 lg:col-span-9 md:pl-8' : ''"
169
+ >
170
+ <div
171
+ v-if="searchResults?.total"
172
+ class="flex flex-wrap gap-4 items-center justify-between pb-2"
173
+ >
174
+ <p
175
+ class="fr-col-auto my-0"
176
+ role="status"
177
+ >
178
+ {{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
179
+ </p>
180
+ <div class="fr-col-auto fr-grid-row fr-grid-row--middle">
181
+ <label
182
+ for="sort-search"
183
+ class="fr-col-auto text-sm m-0 mr-2"
184
+ >
185
+ {{ t('Trier par :') }}
186
+ </label>
187
+ <div class="fr-col">
188
+ <select
189
+ id="sort-search"
190
+ v-model="sort"
191
+ class="fr-select text-sm shadow-input-blue!"
192
+ >
193
+ <option :value="undefined">
194
+ {{ t('Pertinence') }}
195
+ </option>
196
+ <option
197
+ v-for="option in activeSortOptions"
198
+ :key="option.value"
199
+ :value="option.value"
200
+ >
201
+ {{ option.label }}
202
+ </option>
203
+ </select>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ <transition mode="out-in">
208
+ <LoadingBlock
209
+ v-slot="{ data: results }"
210
+ :status="searchResultsStatus"
211
+ :data="searchResults"
212
+ >
213
+ <div v-if="results && results.data.length">
214
+ <ul class="space-y-4 mt-2 p-0 relative z-2 list-none">
215
+ <li
216
+ v-for="result in results.data"
217
+ :key="result.id"
218
+ class="p-0"
219
+ >
220
+ <template v-if="currentType === 'datasets'">
221
+ <slot
222
+ name="dataset"
223
+ :dataset="result"
224
+ >
225
+ <DatasetCard :dataset="(result as Dataset)" />
226
+ </slot>
227
+ </template>
228
+ <template v-else-if="currentType === 'dataservices'">
229
+ <slot
230
+ name="dataservice"
231
+ :dataservice="result"
232
+ >
233
+ <DataserviceCard :dataservice="(result as Dataservice)" />
234
+ </slot>
235
+ </template>
236
+ <template v-else-if="currentType === 'reuses'">
237
+ <slot
238
+ name="reuse"
239
+ :reuse="result"
240
+ >
241
+ <ReuseHorizontalCard :reuse="(result as Reuse)" />
242
+ </slot>
243
+ </template>
244
+ </li>
245
+ </ul>
246
+ <Pagination
247
+ v-if="results && results.total > pageSize"
248
+ :page
249
+ :page-size
250
+ :total-results="results.total"
251
+ class="mt-4"
252
+ :link="getLink"
253
+ @change="changePage"
254
+ />
255
+ </div>
256
+ <div
257
+ v-else
258
+ class="mt-4"
259
+ >
260
+ <slot
261
+ name="no-results"
262
+ :has-filters="hasFilters"
263
+ :reset-filters="resetFilters"
264
+ >
265
+ <div class="rounded p-6 flex flex-wrap gap-4 bg-blue-light text-datagouv">
266
+ <div class="flex-none">
267
+ <img
268
+ class="w-20"
269
+ :src="magnifyingGlassSrc"
270
+ alt=""
271
+ >
272
+ </div>
273
+ <div class="flex-1 min-w-48">
274
+ <p class="font-bold mb-2">
275
+ {{ t(`Vous n'avez pas trouvé ce que vous cherchez ?`) }}
276
+ </p>
277
+ <p class="mt-1 mb-3">
278
+ {{ t("Essayez de réinitialiser les filtres pour élargir votre recherche.") }}
279
+ <template v-if="showForumLink">
280
+ <br>
281
+ {{ t("Vous pouvez aussi regarder les demandes en cours et soumettre la vôtre sur notre forum dédié à la recherche et à l'ouverture de données.") }}
282
+ </template>
283
+ </p>
284
+ <div class="flex flex-wrap gap-2">
285
+ <BrandedButton
286
+ color="secondary"
287
+ type="button"
288
+ @click="resetFilters"
289
+ >
290
+ {{ t("Réinitialiser les filtres") }}
291
+ </BrandedButton>
292
+ <BrandedButton
293
+ v-if="showForumLink"
294
+ color="tertiary"
295
+ :href="componentsConfig.forumUrl"
296
+ :icon="RiLightbulbLine"
297
+ >
298
+ {{ t("Voir le forum") }}
299
+ </BrandedButton>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </slot>
304
+ </div>
305
+ </LoadingBlock>
306
+ </transition>
307
+ </section>
308
+ </div>
309
+ </form>
310
+ </template>
311
+
312
+ <script setup lang="ts">
313
+ import { computed, watch, useTemplateRef, type Ref } from 'vue'
314
+ import { useRouteQuery } from '@vueuse/router'
315
+ import { RiCloseCircleLine, RiDatabase2Line, RiRobot2Line, RiLineChartLine, RiLightbulbLine } from '@remixicon/vue'
316
+ import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
317
+ import { useTranslation } from '../../composables/useTranslation'
318
+ import { useDebouncedRef } from '../../composables/useDebouncedRef'
319
+ import { useStableQueryParams } from '../../composables/useStableQueryParams'
320
+ import { useComponentsConfig } from '../../config'
321
+ import { useFetch } from '../../functions/api'
322
+ import { getLink } from '../../functions/pagination'
323
+ import type { Dataset } from '../../types/datasets'
324
+ import type { Dataservice } from '../../types/dataservices'
325
+ import type { Reuse } from '../../types/reuses'
326
+ import type { GlobalSearchConfig, SearchType, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, FacetItem } from '../../types/search'
327
+ import { getDefaultGlobalSearchConfig } from '../../types/search'
328
+ import BrandedButton from '../BrandedButton.vue'
329
+ import LoadingBlock from '../LoadingBlock.vue'
330
+ import Pagination from '../Pagination.vue'
331
+ import RadioGroup from '../RadioGroup.vue'
332
+ import RadioInput from '../RadioInput.vue'
333
+ import DatasetCard from '../DatasetCard.vue'
334
+ import DataserviceCard from '../DataserviceCard.vue'
335
+ import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
336
+ import SearchInput from './SearchInput.vue'
337
+ import Sidemenu from './Sidemenu.vue'
338
+ import BasicAndAdvancedFilters from './BasicAndAdvancedFilters.vue'
339
+ import OrganizationSelect from '../Form/OrganizationSelect.vue'
340
+ import OrganizationTypeSelect from '../Form/OrganizationTypeSelect.vue'
341
+ import TagSelect from '../Form/TagSelect.vue'
342
+ import FormatSelect from '../Form/FormatSelect.vue'
343
+ import LicenseSelect from '../Form/LicenseSelect.vue'
344
+ import SchemaSelect from '../Form/SchemaSelect.vue'
345
+ import GeozoneSelect from '../Form/GeozoneSelect.vue'
346
+ import GranularitySelect from '../Form/GranularitySelect.vue'
347
+ import ReuseTopicSelect from '../Form/ReuseTopicSelect.vue'
348
+ import FormatFamilyFilter from './Filter/FormatFamilyFilter.vue'
349
+ import AccessTypeFilter from './Filter/AccessTypeFilter.vue'
350
+ import LastUpdateRangeFilter from './Filter/LastUpdateRangeFilter.vue'
351
+ import ProducerTypeFilter from './Filter/ProducerTypeFilter.vue'
352
+ import DatasetBadgeFilter from './Filter/DatasetBadgeFilter.vue'
353
+ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
354
+
355
+ const props = withDefaults(defineProps<{
356
+ config?: GlobalSearchConfig
357
+ placeholder?: string
358
+ }>(), {
359
+ config: getDefaultGlobalSearchConfig,
360
+ })
361
+
362
+ // defineModel's default is static and can't depend on props, so we cast and initialize manually
363
+ const currentType = defineModel<SearchType>('type') as Ref<SearchType>
364
+ if (!currentType.value) currentType.value = props.config[0]?.class ?? 'datasets'
365
+
366
+ const { t } = useTranslation()
367
+ const componentsConfig = useComponentsConfig()
368
+
369
+ // Initial type is used to determine which fetch should be SSR (non-lazy)
370
+ const initialType = currentType.value
371
+
372
+ const currentTypeConfig = computed(() =>
373
+ props.config.find(c => c.class === currentType.value),
374
+ )
375
+
376
+ const activeBasicFilters = computed(() =>
377
+ (currentTypeConfig.value?.basicFilters ?? []) as string[],
378
+ )
379
+
380
+ const activeAdvancedFilters = computed(() =>
381
+ (currentTypeConfig.value?.advancedFilters ?? []) as string[],
382
+ )
383
+
384
+ const activeSortOptions = computed(() =>
385
+ currentTypeConfig.value?.sortOptions ?? [],
386
+ )
387
+
388
+ const activeFilters = computed(() => [
389
+ ...(currentTypeConfig.value?.basicFilters ?? []),
390
+ ...(currentTypeConfig.value?.advancedFilters ?? []),
391
+ ] as string[])
392
+
393
+ const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0)
394
+
395
+ // URL query params
396
+ const q = useRouteQuery<string>('q', '')
397
+ const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsConfig.searchDebounce ?? 300)
398
+ const page = useRouteQuery('page', 1, { transform: Number })
399
+ const sort = useRouteQuery<string | undefined>('sort')
400
+
401
+ // Filter values
402
+ const organizationId = useRouteQuery<string | undefined>('organization')
403
+ const organizationType = useRouteQuery<string | undefined>('organization_badge')
404
+ const tag = useRouteQuery<string | undefined>('tag')
405
+ const format = useRouteQuery<string | undefined>('format')
406
+ const license = useRouteQuery<string | undefined>('license')
407
+ const schema = useRouteQuery<string | undefined>('schema')
408
+ const geozone = useRouteQuery<string | undefined>('geozone')
409
+ const granularity = useRouteQuery<string | undefined>('granularity')
410
+ const badge = useRouteQuery<string | undefined>('badge')
411
+ const topic = useRouteQuery<string | undefined>('topic')
412
+
413
+ // New simple filters
414
+ const formatFamily = useRouteQuery<string | undefined>('format_family')
415
+ const accessType = useRouteQuery<string | undefined>('access_type')
416
+ const lastUpdateRange = useRouteQuery<string | undefined>('last_update_range')
417
+ const producerType = useRouteQuery<string | undefined>('producer_type')
418
+ const reuseType = useRouteQuery<string | undefined>('type')
419
+
420
+ const pageSize = 20
421
+
422
+ // All filter values as a record
423
+ const allFilters: Record<string, Ref<unknown>> = {
424
+ organization: organizationId,
425
+ organization_badge: organizationType,
426
+ tag,
427
+ format,
428
+ license,
429
+ schema,
430
+ geozone,
431
+ granularity,
432
+ badge,
433
+ topic,
434
+ format_family: formatFamily,
435
+ access_type: accessType,
436
+ last_update_range: lastUpdateRange,
437
+ producer_type: producerType,
438
+ type: reuseType,
439
+ }
440
+
441
+ // Reset sort and filters when changing type if they're not valid for the new type
442
+ watch(currentType, () => {
443
+ // Reset sort if not valid
444
+ const validSortValues = activeSortOptions.value.map(o => o.value as string)
445
+ if (sort.value && !validSortValues.includes(sort.value)) {
446
+ sort.value = undefined
447
+ }
448
+
449
+ // Reset filters that are not enabled for the new type
450
+ for (const [filterName, filterRef] of Object.entries(allFilters)) {
451
+ if (filterRef.value !== undefined && !activeFilters.value.includes(filterName)) {
452
+ filterRef.value = undefined
453
+ }
454
+ }
455
+ })
456
+
457
+ // Check which types are enabled
458
+ const datasetsEnabled = computed(() => props.config.some(c => c.class === 'datasets'))
459
+ const dataservicesEnabled = computed(() => props.config.some(c => c.class === 'dataservices'))
460
+ const reusesEnabled = computed(() => props.config.some(c => c.class === 'reuses'))
461
+
462
+ // Create stable params for each type
463
+ const stableParamsOptions = {
464
+ allFilters,
465
+ q: qDebounced,
466
+ sort,
467
+ page,
468
+ pageSize,
469
+ }
470
+
471
+ const datasetsParams = useStableQueryParams({
472
+ ...stableParamsOptions,
473
+ typeConfig: props.config.find(c => c.class === 'datasets'),
474
+ })
475
+ const dataservicesParams = useStableQueryParams({
476
+ ...stableParamsOptions,
477
+ typeConfig: props.config.find(c => c.class === 'dataservices'),
478
+ })
479
+ const reusesParams = useStableQueryParams({
480
+ ...stableParamsOptions,
481
+ typeConfig: props.config.find(c => c.class === 'reuses'),
482
+ })
483
+
484
+ // URLs that return null when type is not enabled
485
+ const datasetsUrl = computed(() => datasetsEnabled.value ? '/api/2/datasets/search/' : null)
486
+ const dataservicesUrl = computed(() => dataservicesEnabled.value ? '/api/2/dataservices/search/' : null)
487
+ const reusesUrl = computed(() => reusesEnabled.value ? '/api/2/reuses/search/' : null)
488
+
489
+ // Reset page on filter/sort change
490
+ const filtersForReset = computed(() => ({
491
+ q: qDebounced.value,
492
+ organization: organizationId.value,
493
+ organization_badge: organizationType.value,
494
+ tag: tag.value,
495
+ format: format.value,
496
+ license: license.value,
497
+ schema: schema.value,
498
+ geozone: geozone.value,
499
+ granularity: granularity.value,
500
+ badge: badge.value,
501
+ topic: topic.value,
502
+ format_family: formatFamily.value,
503
+ access_type: accessType.value,
504
+ last_update_range: lastUpdateRange.value,
505
+ producer_type: producerType.value,
506
+ type: reuseType.value,
507
+ }))
508
+
509
+ watch(filtersForReset, () => page.value = 1)
510
+ watch(sort, () => page.value = 1)
511
+
512
+ const hasFilters = computed(() => {
513
+ return q.value
514
+ || organizationId.value
515
+ || organizationType.value
516
+ || tag.value
517
+ || format.value
518
+ || license.value
519
+ || schema.value
520
+ || geozone.value
521
+ || granularity.value
522
+ || badge.value
523
+ || topic.value
524
+ || formatFamily.value
525
+ || accessType.value
526
+ || lastUpdateRange.value
527
+ || producerType.value
528
+ || reuseType.value
529
+ })
530
+
531
+ const showForumLink = computed(() => currentType.value === 'datasets' && !!componentsConfig.forumUrl)
532
+
533
+ function resetFilters() {
534
+ organizationId.value = undefined
535
+ organizationType.value = undefined
536
+ tag.value = undefined
537
+ format.value = undefined
538
+ license.value = undefined
539
+ schema.value = undefined
540
+ geozone.value = undefined
541
+ granularity.value = undefined
542
+ badge.value = undefined
543
+ topic.value = undefined
544
+ formatFamily.value = undefined
545
+ accessType.value = undefined
546
+ lastUpdateRange.value = undefined
547
+ producerType.value = undefined
548
+ reuseType.value = undefined
549
+ q.value = ''
550
+ flushQ()
551
+ }
552
+
553
+ // API calls only for enabled types (useFetch skips when URL is null)
554
+ // Only the initial type is fetched during SSR, others are client-side only
555
+ const { data: datasetsResults, status: datasetsStatus } = await useFetch<DatasetSearchResponse<Dataset>>(
556
+ datasetsUrl,
557
+ { params: datasetsParams, lazy: true, server: initialType === 'datasets' },
558
+ )
559
+ const { data: dataservicesResults, status: dataservicesStatus } = await useFetch<DataserviceSearchResponse<Dataservice>>(
560
+ dataservicesUrl,
561
+ { params: dataservicesParams, lazy: true, server: initialType === 'dataservices' },
562
+ )
563
+ const { data: reusesResults, status: reusesStatus } = await useFetch<ReuseSearchResponse<Reuse>>(
564
+ reusesUrl,
565
+ { params: reusesParams, lazy: true, server: initialType === 'reuses' },
566
+ )
567
+
568
+ const typesMeta = {
569
+ datasets: {
570
+ icon: RiDatabase2Line,
571
+ name: t('Jeux de données'),
572
+ results: datasetsResults,
573
+ status: datasetsStatus,
574
+ },
575
+ dataservices: {
576
+ icon: RiRobot2Line,
577
+ name: t('APIs'),
578
+ results: dataservicesResults,
579
+ status: dataservicesStatus,
580
+ },
581
+ reuses: {
582
+ icon: RiLineChartLine,
583
+ name: t('Réutilisations'),
584
+ results: reusesResults,
585
+ status: reusesStatus,
586
+ },
587
+ } as const
588
+
589
+ const searchResults = computed(() => typesMeta[currentType.value].results.value)
590
+ const searchResultsStatus = computed(() => typesMeta[currentType.value].status.value)
591
+
592
+ // Facets for filters
593
+ const currentFacets = computed(() => searchResults.value?.facets)
594
+
595
+ function getFacets(key: string): FacetItem[] | undefined {
596
+ if (!currentFacets.value) return undefined
597
+ return (currentFacets.value as Record<string, FacetItem[]>)[key]
598
+ }
599
+
600
+ // Scroll handling
601
+ const searchRef = useTemplateRef('search')
602
+
603
+ function scrollToTop() {
604
+ searchRef.value?.scrollIntoView({ behavior: 'smooth' })
605
+ }
606
+
607
+ function changePage(newPage: number) {
608
+ page.value = newPage
609
+ scrollToTop()
610
+ }
611
+ </script>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <section class="fr-search-bar fr-search-bar--lg w-full">
3
+ <label
4
+ class="sr-only"
5
+ :for="id"
6
+ >
7
+ {{ t('Recherche') }}
8
+ </label>
9
+ <input
10
+ :id="id"
11
+ ref="inputRef"
12
+ v-model="q"
13
+ type="search"
14
+ name="q"
15
+ class="input max-h-12 m-0 rounded-tl shadow-input-blue"
16
+ :aria-label="placeholder || t('Rechercher...')"
17
+ :placeholder="placeholder || t('Rechercher...')"
18
+ >
19
+ <BrandedButton
20
+ class="rounded-l-none rounded-br-none rounded-tr min-h-12"
21
+ size="lg"
22
+ color="primary"
23
+ :icon="RiSearchLine"
24
+ icon-only-on-mobile
25
+ type="submit"
26
+ >
27
+ {{ t('Recherche') }}
28
+ </BrandedButton>
29
+ </section>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { nextTick, onMounted, useId, useTemplateRef } from 'vue'
34
+ import { RiSearchLine } from '@remixicon/vue'
35
+ import { useTranslation } from '../../composables/useTranslation'
36
+ import BrandedButton from '../BrandedButton.vue'
37
+
38
+ const q = defineModel<string>({ required: true })
39
+
40
+ withDefaults(defineProps<{
41
+ placeholder?: string
42
+ autoFocus?: boolean
43
+ }>(), {
44
+ autoFocus: true,
45
+ })
46
+
47
+ const { t } = useTranslation()
48
+
49
+ const id = useId()
50
+
51
+ const input = useTemplateRef<HTMLInputElement>('inputRef')
52
+
53
+ const focus = () => {
54
+ input.value?.focus({
55
+ preventScroll: true,
56
+ })
57
+ }
58
+
59
+ onMounted(async () => {
60
+ await nextTick()
61
+ focus()
62
+ })
63
+ </script>