@datagouv/components-next 0.2.0 → 1.0.1

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 (155) hide show
  1. package/README.md +1 -1
  2. package/assets/main.css +49 -22
  3. package/dist/Control-BNCDn-8E.js +148 -0
  4. package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-B5lBpOl8.js} +2 -2
  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-Doz1Z0BS.js} +23 -23
  8. package/dist/Map-BdT3i2C4.js +7609 -0
  9. package/dist/MapContainer.client-oiieO8H-.js +105 -0
  10. package/dist/OSM-CamriM9b.js +71 -0
  11. package/dist/PdfPreview.client-CdAhkDFJ.js +14513 -0
  12. package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-B0v8tGJQ.js} +3 -3
  13. package/dist/ScaleLine-BiesrgOv.js +165 -0
  14. package/dist/Swagger.client-CsK65JnG.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-CrjHf74q.js} +17 -17
  19. package/dist/common-C4rDcQpp.js +243 -0
  20. package/dist/components-next.css +1 -1
  21. package/dist/components-next.js +158 -117
  22. package/dist/components.css +1 -1
  23. package/dist/{MapContainer.client-DeSo8EvG.js → index-Bbu9rOHt.js} +4975 -21416
  24. package/dist/leaflet-src-7m1mB8LI.js +6338 -0
  25. package/dist/{main-Dgri3TQL.js → main-CiH8ZmBI.js} +56973 -51462
  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-Bi_bsV6C.js} +1 -1
  29. package/package.json +6 -2
  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 +85 -120
  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/DatasetQuality.vue +23 -16
  46. package/src/components/DatasetQualityInline.vue +13 -17
  47. package/src/components/DatasetQualityScore.vue +12 -15
  48. package/src/components/DatasetQualityTooltipContent.vue +3 -3
  49. package/src/components/DescriptionList.vue +1 -4
  50. package/src/components/DescriptionListDetails.vue +5 -0
  51. package/src/components/DescriptionListTerm.vue +5 -0
  52. package/src/components/DiscussionMessageCard.vue +63 -0
  53. package/src/components/ExtraAccordion.vue +4 -4
  54. package/src/components/Form/BadgeSelect.vue +35 -0
  55. package/src/components/Form/FormatSelect.vue +28 -0
  56. package/src/components/Form/GeozoneSelect.vue +52 -0
  57. package/src/components/Form/GranularitySelect.vue +29 -0
  58. package/src/components/Form/LicenseSelect.vue +30 -0
  59. package/src/components/Form/OrganizationSelect.vue +62 -0
  60. package/src/components/Form/OrganizationTypeSelect.vue +34 -0
  61. package/src/components/Form/ReuseTopicSelect.vue +29 -0
  62. package/src/components/Form/SchemaSelect.vue +30 -0
  63. package/src/components/Form/SearchableSelect.vue +334 -0
  64. package/src/components/Form/SelectGroup.vue +132 -0
  65. package/src/components/Form/TagSelect.vue +38 -0
  66. package/src/components/LeafletMap.vue +31 -0
  67. package/src/components/LicenseBadge.vue +24 -0
  68. package/src/components/LoadingBlock.vue +23 -2
  69. package/src/components/MarkdownViewer.vue +3 -1
  70. package/src/components/ObjectCard.vue +42 -0
  71. package/src/components/ObjectCardBadge.vue +22 -0
  72. package/src/components/ObjectCardHeader.vue +35 -0
  73. package/src/components/ObjectCardOwner.vue +43 -0
  74. package/src/components/ObjectCardShortDescription.vue +28 -0
  75. package/src/components/OrganizationCard.vue +35 -20
  76. package/src/components/OrganizationHorizontalCard.vue +87 -0
  77. package/src/components/OrganizationLogo.vue +1 -1
  78. package/src/components/OrganizationNameWithCertificate.vue +12 -6
  79. package/src/components/OwnerTypeIcon.vue +1 -0
  80. package/src/components/Pagination.vue +1 -1
  81. package/src/components/Placeholder.vue +5 -2
  82. package/src/components/PostCard.vue +62 -0
  83. package/src/components/ProgressBar.vue +31 -0
  84. package/src/components/RadioGroup.vue +32 -0
  85. package/src/components/RadioInput.vue +64 -0
  86. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  87. package/src/components/ResourceAccordion/EditButton.vue +2 -3
  88. package/src/components/ResourceAccordion/JsonPreview.client.vue +3 -3
  89. package/src/components/ResourceAccordion/MapContainer.client.vue +21 -17
  90. package/src/components/ResourceAccordion/Metadata.vue +11 -24
  91. package/src/components/ResourceAccordion/PdfPreview.client.vue +70 -74
  92. package/src/components/ResourceAccordion/Pmtiles.client.vue +2 -2
  93. package/src/components/ResourceAccordion/Preview.vue +2 -2
  94. package/src/components/ResourceAccordion/ResourceAccordion.vue +35 -28
  95. package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
  96. package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
  97. package/src/components/ResourceAccordion/XmlPreview.client.vue +3 -3
  98. package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
  99. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
  100. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +410 -0
  101. package/src/components/ReuseCard.vue +8 -28
  102. package/src/components/ReuseHorizontalCard.vue +80 -0
  103. package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
  104. package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
  105. package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
  106. package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
  107. package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
  108. package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
  109. package/src/components/Search/Filter/ProducerTypeFilter.vue +49 -0
  110. package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
  111. package/src/components/Search/GlobalSearch.vue +707 -0
  112. package/src/components/Search/SearchInput.vue +63 -0
  113. package/src/components/Search/Sidemenu.vue +38 -0
  114. package/src/components/StatBox.vue +5 -5
  115. package/src/components/Tag.vue +30 -0
  116. package/src/components/Toggletip.vue +11 -4
  117. package/src/components/Tooltip.vue +2 -3
  118. package/src/components/TopicCard.vue +134 -0
  119. package/src/components/radioGroupContext.ts +9 -0
  120. package/src/composables/useDebouncedRef.ts +31 -0
  121. package/src/composables/useHasTabularData.ts +15 -0
  122. package/src/composables/useMetrics.ts +4 -3
  123. package/src/composables/useResourceCapabilities.ts +131 -0
  124. package/src/composables/useRouteQueryBoolean.ts +10 -0
  125. package/src/composables/useSelectModelSync.ts +89 -0
  126. package/src/composables/useStableQueryParams.ts +84 -0
  127. package/src/composables/useTranslation.ts +2 -1
  128. package/src/config.ts +4 -0
  129. package/src/functions/api.ts +25 -6
  130. package/src/functions/api.types.ts +5 -3
  131. package/src/functions/datasets.ts +1 -29
  132. package/src/functions/description.ts +33 -0
  133. package/src/functions/helpers.ts +11 -0
  134. package/src/functions/markdown.ts +60 -16
  135. package/src/functions/metrics.ts +33 -0
  136. package/src/functions/organizations.ts +5 -5
  137. package/src/functions/resourceCapabilities.ts +55 -0
  138. package/src/main.ts +96 -7
  139. package/src/types/dataservices.ts +14 -12
  140. package/src/types/datasets.ts +20 -7
  141. package/src/types/discussions.ts +20 -0
  142. package/src/types/licenses.ts +3 -3
  143. package/src/types/organizations.ts +13 -1
  144. package/src/types/owned.ts +4 -2
  145. package/src/types/pages.ts +70 -0
  146. package/src/types/posts.ts +27 -0
  147. package/src/types/resources.ts +16 -0
  148. package/src/types/reuses.ts +14 -5
  149. package/src/types/search.ts +407 -0
  150. package/src/types/users.ts +12 -3
  151. package/dist/PdfPreview.client-COOkEkRA.js +0 -107
  152. package/dist/Swagger.client-CpLgaLg6.js +0 -4
  153. package/dist/pdf-vue3-IkJO65RH.js +0 -273
  154. package/dist/pdf.min-f72cfa08-CdgJTooZ.js +0 -9501
  155. package/src/components/DatasetInformationPanel.vue +0 -211
@@ -0,0 +1,707 @@
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 || typesMeta[currentType].placeholder"
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
+ :exclude="currentType === 'organizations' ? ['user'] : []"
127
+ :style="{ order: getOrder('producer_type') }"
128
+ />
129
+ <DatasetBadgeFilter
130
+ v-if="isEnabled('badge')"
131
+ v-model="badge"
132
+ :facets="getFacets('badge')"
133
+ :loading="searchResultsStatus === 'pending'"
134
+ :style="{ order: getOrder('badge') }"
135
+ />
136
+ <ReuseTypeFilter
137
+ v-if="isEnabled('type')"
138
+ v-model="reuseType"
139
+ :facets="getFacets('type')"
140
+ :loading="searchResultsStatus === 'pending'"
141
+ :style="{ order: getOrder('type') }"
142
+ />
143
+ <slot
144
+ name="filters"
145
+ :is-enabled="isEnabled"
146
+ :get-order="getOrder"
147
+ />
148
+ </BasicAndAdvancedFilters>
149
+ <div
150
+ v-if="hasFilters"
151
+ class="mt-6 text-center"
152
+ >
153
+ <BrandedButton
154
+ color="secondary"
155
+ :icon="RiCloseCircleLine"
156
+ class="w-full justify-center"
157
+ type="button"
158
+ @click="resetFilters"
159
+ >
160
+ {{ t('Réinitialiser les filtres') }}
161
+ </BrandedButton>
162
+ </div>
163
+ </Sidemenu>
164
+ </div>
165
+ </div>
166
+ <section
167
+ ref="results"
168
+ class="col-span-12 mt-4 md:mt-0 search-results"
169
+ :class="showSidebar ? 'md:col-span-8 lg:col-span-9 md:pl-8' : ''"
170
+ >
171
+ <div
172
+ v-if="searchResults?.total"
173
+ class="flex flex-wrap gap-4 items-center justify-between pb-2"
174
+ >
175
+ <p
176
+ class="fr-col-auto my-0"
177
+ role="status"
178
+ >
179
+ {{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
180
+ </p>
181
+ <div class="fr-col-auto fr-grid-row fr-grid-row--middle gap-4">
182
+ <div class="flex items-center">
183
+ <label
184
+ for="sort-search"
185
+ class="fr-col-auto text-sm m-0 mr-2"
186
+ >
187
+ {{ t('Trier par :') }}
188
+ </label>
189
+ <div class="fr-col">
190
+ <select
191
+ id="sort-search"
192
+ v-model="sort"
193
+ class="fr-select text-sm shadow-input-blue!"
194
+ >
195
+ <option :value="undefined">
196
+ {{ t('Pertinence') }}
197
+ </option>
198
+ <option
199
+ v-for="option in allSortOptions"
200
+ :key="option.value"
201
+ :value="option.value"
202
+ :hidden="!activeSortValues.has(option.value)"
203
+ >
204
+ {{ option.label }}
205
+ </option>
206
+ </select>
207
+ </div>
208
+ </div>
209
+ <BrandedButton
210
+ v-if="rssUrl"
211
+ :href="rssUrl"
212
+ :title="t('Flux RSS')"
213
+ color="secondary"
214
+ size="sm"
215
+ :icon="RiRssLine"
216
+ icon-only
217
+ target="_blank"
218
+ />
219
+ </div>
220
+ </div>
221
+ <transition mode="out-in">
222
+ <LoadingBlock
223
+ v-slot="{ data: results }"
224
+ :status="searchResultsStatus"
225
+ :data="searchResults"
226
+ >
227
+ <div v-if="results && results.data.length">
228
+ <ul class="space-y-4 mt-2 p-0 relative z-2 list-none">
229
+ <li
230
+ v-for="result in results.data"
231
+ :key="result.id"
232
+ class="p-0"
233
+ >
234
+ <template v-if="currentType === 'datasets'">
235
+ <slot
236
+ name="dataset"
237
+ :dataset="result"
238
+ >
239
+ <DatasetCard :dataset="(result as Dataset)" />
240
+ </slot>
241
+ </template>
242
+ <template v-else-if="currentType === 'dataservices'">
243
+ <slot
244
+ name="dataservice"
245
+ :dataservice="result"
246
+ >
247
+ <DataserviceCard :dataservice="(result as Dataservice)" />
248
+ </slot>
249
+ </template>
250
+ <template v-else-if="currentType === 'reuses'">
251
+ <slot
252
+ name="reuse"
253
+ :reuse="result"
254
+ >
255
+ <ReuseHorizontalCard :reuse="(result as Reuse)" />
256
+ </slot>
257
+ </template>
258
+ <template v-else-if="currentType === 'organizations'">
259
+ <slot
260
+ name="organization"
261
+ :organization="result"
262
+ >
263
+ <OrganizationHorizontalCard :organization="(result as Organization)" />
264
+ </slot>
265
+ </template>
266
+ </li>
267
+ </ul>
268
+ <Pagination
269
+ v-if="results && results.total > pageSize"
270
+ :page
271
+ :page-size
272
+ :total-results="results.total"
273
+ class="mt-4"
274
+ :link="getLink"
275
+ @change="changePage"
276
+ />
277
+ </div>
278
+ <div
279
+ v-else
280
+ class="mt-4"
281
+ >
282
+ <slot
283
+ name="no-results"
284
+ :has-filters="hasFilters"
285
+ :reset-filters="resetFilters"
286
+ >
287
+ <div class="rounded p-6 flex flex-wrap gap-4 bg-blue-light text-datagouv">
288
+ <div class="flex-none">
289
+ <img
290
+ class="w-20"
291
+ :src="magnifyingGlassSrc"
292
+ alt=""
293
+ >
294
+ </div>
295
+ <div class="flex-1 min-w-48">
296
+ <p class="font-bold mb-2">
297
+ {{ t(`Vous n'avez pas trouvé ce que vous cherchez ?`) }}
298
+ </p>
299
+ <p class="mt-1 mb-3">
300
+ {{ t("Essayez de réinitialiser les filtres pour élargir votre recherche.") }}
301
+ <template v-if="showForumLink">
302
+ <br>
303
+ {{ 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.") }}
304
+ </template>
305
+ </p>
306
+ <div class="flex flex-wrap gap-2">
307
+ <BrandedButton
308
+ color="secondary"
309
+ type="button"
310
+ @click="resetFilters"
311
+ >
312
+ {{ t("Réinitialiser les filtres") }}
313
+ </BrandedButton>
314
+ <BrandedButton
315
+ v-if="showForumLink"
316
+ color="tertiary"
317
+ :href="componentsConfig.forumUrl"
318
+ :icon="RiLightbulbLine"
319
+ keep-margins-even-without-borders
320
+ >
321
+ {{ t("Voir le forum") }}
322
+ </BrandedButton>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </slot>
327
+ </div>
328
+ </LoadingBlock>
329
+ </transition>
330
+ </section>
331
+ </div>
332
+ </form>
333
+ </template>
334
+
335
+ <script setup lang="ts">
336
+ import { computed, watch, useTemplateRef, type Ref } from 'vue'
337
+ import { useRouteQuery } from '@vueuse/router'
338
+ import { RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
339
+ import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
340
+ import { useTranslation } from '../../composables/useTranslation'
341
+ import { useDebouncedRef } from '../../composables/useDebouncedRef'
342
+ import { useStableQueryParams } from '../../composables/useStableQueryParams'
343
+ import { useComponentsConfig } from '../../config'
344
+ import { useFetch } from '../../functions/api'
345
+ import { getLink } from '../../functions/pagination'
346
+ import type { Dataset } from '../../types/datasets'
347
+ import type { Dataservice } from '../../types/dataservices'
348
+ import type { Organization } from '../../types/organizations'
349
+ import type { Reuse } from '../../types/reuses'
350
+ import type { GlobalSearchConfig, SearchType, SortOption, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, OrganizationSearchResponse, FacetItem } from '../../types/search'
351
+ import { getDefaultGlobalSearchConfig } from '../../types/search'
352
+ import BrandedButton from '../BrandedButton.vue'
353
+ import LoadingBlock from '../LoadingBlock.vue'
354
+ import Pagination from '../Pagination.vue'
355
+ import RadioGroup from '../RadioGroup.vue'
356
+ import RadioInput from '../RadioInput.vue'
357
+ import DatasetCard from '../DatasetCard.vue'
358
+ import DataserviceCard from '../DataserviceCard.vue'
359
+ import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
360
+ import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
361
+ import SearchInput from './SearchInput.vue'
362
+ import Sidemenu from './Sidemenu.vue'
363
+ import BasicAndAdvancedFilters from './BasicAndAdvancedFilters.vue'
364
+ import OrganizationSelect from '../Form/OrganizationSelect.vue'
365
+ import OrganizationTypeSelect from '../Form/OrganizationTypeSelect.vue'
366
+ import TagSelect from '../Form/TagSelect.vue'
367
+ import FormatSelect from '../Form/FormatSelect.vue'
368
+ import LicenseSelect from '../Form/LicenseSelect.vue'
369
+ import SchemaSelect from '../Form/SchemaSelect.vue'
370
+ import GeozoneSelect from '../Form/GeozoneSelect.vue'
371
+ import GranularitySelect from '../Form/GranularitySelect.vue'
372
+ import ReuseTopicSelect from '../Form/ReuseTopicSelect.vue'
373
+ import FormatFamilyFilter from './Filter/FormatFamilyFilter.vue'
374
+ import AccessTypeFilter from './Filter/AccessTypeFilter.vue'
375
+ import LastUpdateRangeFilter from './Filter/LastUpdateRangeFilter.vue'
376
+ import ProducerTypeFilter from './Filter/ProducerTypeFilter.vue'
377
+ import DatasetBadgeFilter from './Filter/DatasetBadgeFilter.vue'
378
+ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
379
+
380
+ const props = withDefaults(defineProps<{
381
+ config?: GlobalSearchConfig
382
+ placeholder?: string
383
+ }>(), {
384
+ config: getDefaultGlobalSearchConfig,
385
+ })
386
+
387
+ // 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'
390
+
391
+ const { t } = useTranslation()
392
+ const componentsConfig = useComponentsConfig()
393
+
394
+ // Initial type is used to determine which fetch should be SSR (non-lazy)
395
+ const initialType = currentType.value
396
+
397
+ const currentTypeConfig = computed(() =>
398
+ props.config.find(c => c.class === currentType.value),
399
+ )
400
+
401
+ const activeBasicFilters = computed(() =>
402
+ (currentTypeConfig.value?.basicFilters ?? []) as string[],
403
+ )
404
+
405
+ const activeAdvancedFilters = computed(() =>
406
+ (currentTypeConfig.value?.advancedFilters ?? []) as string[],
407
+ )
408
+
409
+ const activeSortOptions = computed(() =>
410
+ currentTypeConfig.value?.sortOptions ?? [],
411
+ )
412
+
413
+ const activeSortValues = computed(() =>
414
+ new Set(activeSortOptions.value.map(o => o.value as string)),
415
+ )
416
+
417
+ // Deduplicated union of all sort options across all search types.
418
+ // Rendered as hidden <option> elements so the <select> always has a stable
419
+ // intrinsic width regardless of which type is currently active.
420
+ const allSortOptions = computed(() => {
421
+ const seen = new Set<string>()
422
+ return props.config.flatMap(c => (c.sortOptions ?? []) as SortOption<string>[]).filter((o) => {
423
+ if (seen.has(o.value)) return false
424
+ seen.add(o.value)
425
+ return true
426
+ })
427
+ })
428
+
429
+ const activeFilters = computed(() => [
430
+ ...(currentTypeConfig.value?.basicFilters ?? []),
431
+ ...(currentTypeConfig.value?.advancedFilters ?? []),
432
+ ] as string[])
433
+
434
+ const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0)
435
+
436
+ // URL query params
437
+ const q = useRouteQuery<string>('q', '')
438
+ const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsConfig.searchDebounce ?? 300)
439
+ const page = useRouteQuery('page', 1, { transform: Number })
440
+ const sort = useRouteQuery<string | undefined>('sort')
441
+
442
+ // Filter values
443
+ const organizationId = useRouteQuery<string | undefined>('organization')
444
+ const organizationType = useRouteQuery<string | undefined>('organization_badge')
445
+ const tag = useRouteQuery<string | undefined>('tag')
446
+ const format = useRouteQuery<string | undefined>('format')
447
+ const license = useRouteQuery<string | undefined>('license')
448
+ const schema = useRouteQuery<string | undefined>('schema')
449
+ const geozone = useRouteQuery<string | undefined>('geozone')
450
+ const granularity = useRouteQuery<string | undefined>('granularity')
451
+ const badge = useRouteQuery<string | undefined>('badge')
452
+ const topic = useRouteQuery<string | undefined>('topic')
453
+
454
+ // New simple filters
455
+ const formatFamily = useRouteQuery<string | undefined>('format_family')
456
+ const accessType = useRouteQuery<string | undefined>('access_type')
457
+ const lastUpdateRange = useRouteQuery<string | undefined>('last_update_range')
458
+ const producerType = useRouteQuery<string | undefined>('producer_type')
459
+ const reuseType = useRouteQuery<string | undefined>('type')
460
+
461
+ const pageSize = 20
462
+
463
+ // All filter values as a record
464
+ const allFilters: Record<string, Ref<unknown>> = {
465
+ organization: organizationId,
466
+ organization_badge: organizationType,
467
+ tag,
468
+ format,
469
+ license,
470
+ schema,
471
+ geozone,
472
+ granularity,
473
+ badge,
474
+ topic,
475
+ format_family: formatFamily,
476
+ access_type: accessType,
477
+ last_update_range: lastUpdateRange,
478
+ producer_type: producerType,
479
+ type: reuseType,
480
+ }
481
+
482
+ // Reset sort and filters when changing type if they're not valid for the new type
483
+ watch(currentType, () => {
484
+ // Reset sort if not valid
485
+ const validSortValues = activeSortOptions.value.map(o => o.value as string)
486
+ if (sort.value && !validSortValues.includes(sort.value)) {
487
+ sort.value = undefined
488
+ }
489
+
490
+ // Reset filters that are not enabled for the new type
491
+ for (const [filterName, filterRef] of Object.entries(allFilters)) {
492
+ if (filterRef.value !== undefined && !activeFilters.value.includes(filterName)) {
493
+ filterRef.value = undefined
494
+ }
495
+ }
496
+ })
497
+
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
+ // Create stable params for each type
505
+ const stableParamsOptions = {
506
+ allFilters,
507
+ q: qDebounced,
508
+ sort,
509
+ page,
510
+ pageSize,
511
+ }
512
+
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
+ })
529
+
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)
535
+
536
+ // Reset page on filter/sort change
537
+ const filtersForReset = computed(() => ({
538
+ q: qDebounced.value,
539
+ organization: organizationId.value,
540
+ organization_badge: organizationType.value,
541
+ tag: tag.value,
542
+ format: format.value,
543
+ license: license.value,
544
+ schema: schema.value,
545
+ geozone: geozone.value,
546
+ granularity: granularity.value,
547
+ badge: badge.value,
548
+ topic: topic.value,
549
+ format_family: formatFamily.value,
550
+ access_type: accessType.value,
551
+ last_update_range: lastUpdateRange.value,
552
+ producer_type: producerType.value,
553
+ type: reuseType.value,
554
+ }))
555
+
556
+ watch(filtersForReset, () => page.value = 1)
557
+ watch(sort, () => page.value = 1)
558
+
559
+ const hasFilters = computed(() => {
560
+ return q.value
561
+ || organizationId.value
562
+ || organizationType.value
563
+ || tag.value
564
+ || format.value
565
+ || license.value
566
+ || schema.value
567
+ || geozone.value
568
+ || granularity.value
569
+ || badge.value
570
+ || topic.value
571
+ || formatFamily.value
572
+ || accessType.value
573
+ || lastUpdateRange.value
574
+ || producerType.value
575
+ || reuseType.value
576
+ })
577
+
578
+ const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
579
+
580
+ function resetFilters() {
581
+ organizationId.value = undefined
582
+ organizationType.value = undefined
583
+ tag.value = undefined
584
+ format.value = undefined
585
+ license.value = undefined
586
+ schema.value = undefined
587
+ geozone.value = undefined
588
+ granularity.value = undefined
589
+ badge.value = undefined
590
+ topic.value = undefined
591
+ formatFamily.value = undefined
592
+ accessType.value = undefined
593
+ lastUpdateRange.value = undefined
594
+ producerType.value = undefined
595
+ reuseType.value = undefined
596
+ q.value = ''
597
+ flushQ()
598
+ }
599
+
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)
652
+
653
+ // RSS feed URL for datasets
654
+ const rssUrl = computed(() => {
655
+ if (currentType.value !== 'datasets') return null
656
+
657
+ const params = new URLSearchParams()
658
+ const datasetsConfig = props.config.find(c => c.class === 'datasets')
659
+
660
+ // Add hidden filters first
661
+ if (datasetsConfig?.hiddenFilters) {
662
+ for (const hf of datasetsConfig.hiddenFilters) {
663
+ if (hf?.value) params.set(hf.key as string, String(hf.value))
664
+ }
665
+ }
666
+
667
+ // Add active filters
668
+ if (qDebounced.value) params.set('q', qDebounced.value)
669
+ if (organizationId.value) params.set('organization', organizationId.value)
670
+ if (organizationType.value) params.set('organization_badge', organizationType.value)
671
+ if (tag.value) params.set('tag', tag.value)
672
+ if (format.value) params.set('format', format.value)
673
+ if (license.value) params.set('license', license.value)
674
+ if (schema.value) params.set('schema', schema.value)
675
+ if (geozone.value) params.set('geozone', geozone.value)
676
+ if (granularity.value) params.set('granularity', granularity.value)
677
+ if (badge.value) params.set('badge', badge.value)
678
+ if (topic.value) params.set('topic', topic.value)
679
+
680
+ // Add sort if set
681
+ if (sort.value) params.set('sort', sort.value)
682
+
683
+ const queryString = params.toString()
684
+ const basePath = '/api/1/datasets/recent.atom'
685
+ return `${componentsConfig.apiBase}${basePath}${queryString ? '?' + queryString : ''}`
686
+ })
687
+
688
+ // Facets for filters
689
+ const currentFacets = computed(() => searchResults.value?.facets)
690
+
691
+ function getFacets(key: string): FacetItem[] | undefined {
692
+ if (!currentFacets.value) return undefined
693
+ return (currentFacets.value as Record<string, FacetItem[]>)[key]
694
+ }
695
+
696
+ // Scroll handling
697
+ const searchRef = useTemplateRef('search')
698
+
699
+ function scrollToTop() {
700
+ searchRef.value?.scrollIntoView({ behavior: 'smooth' })
701
+ }
702
+
703
+ function changePage(newPage: number) {
704
+ page.value = newPage
705
+ scrollToTop()
706
+ }
707
+ </script>