@datagouv/components-next 1.0.2-dev.20 → 1.0.2-dev.200
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.
- package/README.md +7 -6
- package/assets/main.css +4 -0
- package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
- package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
- package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
- package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
- package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
- package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
- package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
- package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
- package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
- package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
- package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
- package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +196 -150
- package/dist/components.css +2 -2
- package/dist/{index-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
- package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
- package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
- package/package.json +47 -38
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +24 -14
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +320 -0
- package/src/components/Chart/ChartViewerWrapper.vue +182 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +24 -5
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +24 -9
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/Listbox.vue +101 -0
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +81 -0
- package/src/components/InfiniteLoader.vue +53 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCardHeader.vue +11 -4
- package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
- package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
- package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
- package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
- package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
- package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
- package/src/components/OpenApiViewer/openapi.ts +150 -0
- package/src/components/OrganizationNameWithCertificate.vue +3 -2
- package/src/components/Pagination.vue +25 -62
- package/src/components/PostCard.vue +13 -15
- package/src/components/RadioInput.vue +7 -2
- package/src/components/ResourceAccordion/DataStructure.vue +11 -33
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +172 -0
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
- package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
- package/src/components/ResourceAccordion/Metadata.vue +4 -6
- package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
- package/src/components/ResourceAccordion/Preview.vue +19 -15
- package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +28 -9
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +2 -2
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +229 -123
- package/src/components/Search/SearchInput.vue +5 -4
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +50 -0
- package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
- package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
- package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +481 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +84 -0
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +13 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +15 -1
- package/src/composables/useMetrics.ts +1 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +48 -40
- package/src/composables/useSearchFilter.ts +120 -0
- package/src/composables/useStableQueryParams.ts +38 -6
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +21 -3
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +20 -42
- package/src/functions/api.types.ts +1 -0
- package/src/functions/charts.ts +99 -0
- package/src/functions/datasets.ts +0 -17
- package/src/functions/dates.ts +6 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +63 -60
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +136 -1
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +214 -0
- package/src/functions/tabularApi.ts +96 -11
- package/src/main.ts +119 -14
- package/src/types/dataservices.ts +17 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/pages.ts +0 -5
- package/src/types/posts.ts +2 -2
- package/src/types/reports.ts +5 -1
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +63 -1
- package/src/types/site.ts +5 -3
- package/src/types/ui.ts +2 -0
- package/src/types/users.ts +2 -1
- package/src/types/visualizations.ts +93 -0
- package/assets/swagger-themes/newspaper.css +0 -1670
- package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
- package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
- package/dist/Swagger.client-B4y0hXME.js +0 -4
- package/dist/XmlPreview.client-LETEfNBH.js +0 -64
- package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
- package/src/functions/pagination.ts +0 -9
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
@submit.prevent
|
|
6
6
|
>
|
|
7
7
|
<div
|
|
8
|
-
|
|
8
|
+
v-if="!hideSearchInput"
|
|
9
9
|
class="flex flex-wrap items-center justify-between"
|
|
10
10
|
data-cy="search"
|
|
11
11
|
>
|
|
12
12
|
<SearchInput
|
|
13
13
|
v-model="q"
|
|
14
|
-
:placeholder="
|
|
14
|
+
:placeholder="resolvedPlaceholder"
|
|
15
|
+
:auto-focus
|
|
15
16
|
/>
|
|
16
17
|
</div>
|
|
17
18
|
<div class="grid grid-cols-12 mt-2 md:mt-5">
|
|
@@ -30,23 +31,27 @@
|
|
|
30
31
|
>
|
|
31
32
|
<RadioInput
|
|
32
33
|
v-for="typeConfig in config"
|
|
33
|
-
:key="typeConfig
|
|
34
|
-
:value="typeConfig
|
|
35
|
-
:count="
|
|
36
|
-
:loading="
|
|
37
|
-
: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="typeConfig.icon ?? strategies[typeConfig.class].icon"
|
|
38
39
|
>
|
|
39
|
-
{{ typeConfig.name ||
|
|
40
|
+
{{ typeConfig.name || strategies[typeConfig.class].name }}
|
|
40
41
|
</RadioInput>
|
|
41
42
|
</RadioGroup>
|
|
42
43
|
</Sidemenu>
|
|
43
44
|
</div>
|
|
44
45
|
|
|
45
|
-
<div v-if="activeFilters.length > 0">
|
|
46
|
+
<div v-if="activeFilters.length > 0 || $slots['custom-filters-top'] || $slots['custom-filters-bottom']">
|
|
46
47
|
<Sidemenu :button-text="t('Filtres')">
|
|
47
48
|
<template #title>
|
|
48
49
|
{{ t('Filtres') }}
|
|
49
50
|
</template>
|
|
51
|
+
<slot
|
|
52
|
+
name="custom-filters-top"
|
|
53
|
+
:current-type="currentType"
|
|
54
|
+
/>
|
|
50
55
|
<BasicAndAdvancedFilters
|
|
51
56
|
v-slot="{ isEnabled, getOrder }"
|
|
52
57
|
:basic-filters="activeBasicFilters"
|
|
@@ -123,7 +128,7 @@
|
|
|
123
128
|
v-model="producerType"
|
|
124
129
|
:facets="getFacets('producer_type')"
|
|
125
130
|
:loading="searchResultsStatus === 'pending'"
|
|
126
|
-
:exclude="
|
|
131
|
+
:exclude="currentTypeConfig?.class === 'organizations' ? ['user'] : []"
|
|
127
132
|
:style="{ order: getOrder('producer_type') }"
|
|
128
133
|
/>
|
|
129
134
|
<DatasetBadgeFilter
|
|
@@ -146,6 +151,10 @@
|
|
|
146
151
|
:get-order="getOrder"
|
|
147
152
|
/>
|
|
148
153
|
</BasicAndAdvancedFilters>
|
|
154
|
+
<slot
|
|
155
|
+
name="custom-filters-bottom"
|
|
156
|
+
:current-type="currentType"
|
|
157
|
+
/>
|
|
149
158
|
<div
|
|
150
159
|
v-if="hasFilters"
|
|
151
160
|
class="mt-6 text-center"
|
|
@@ -175,6 +184,7 @@
|
|
|
175
184
|
<p
|
|
176
185
|
class="fr-col-auto my-0"
|
|
177
186
|
role="status"
|
|
187
|
+
data-testid="search-result-count"
|
|
178
188
|
>
|
|
179
189
|
{{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
|
|
180
190
|
</p>
|
|
@@ -189,10 +199,13 @@
|
|
|
189
199
|
<div class="fr-col">
|
|
190
200
|
<select
|
|
191
201
|
id="sort-search"
|
|
192
|
-
v-model="
|
|
202
|
+
v-model="effectiveSort"
|
|
193
203
|
class="fr-select text-sm shadow-input-blue!"
|
|
194
204
|
>
|
|
195
|
-
<option
|
|
205
|
+
<option
|
|
206
|
+
v-if="!currentTypeConfig?.defaultSort"
|
|
207
|
+
:value="undefined"
|
|
208
|
+
>
|
|
196
209
|
{{ t('Pertinence') }}
|
|
197
210
|
</option>
|
|
198
211
|
<option
|
|
@@ -214,6 +227,7 @@
|
|
|
214
227
|
size="sm"
|
|
215
228
|
:icon="RiRssLine"
|
|
216
229
|
icon-only
|
|
230
|
+
external
|
|
217
231
|
target="_blank"
|
|
218
232
|
/>
|
|
219
233
|
</div>
|
|
@@ -221,7 +235,7 @@
|
|
|
221
235
|
<transition mode="out-in">
|
|
222
236
|
<LoadingBlock
|
|
223
237
|
v-slot="{ data: results }"
|
|
224
|
-
:status="searchResultsStatus"
|
|
238
|
+
:status="searchResultsStatus!"
|
|
225
239
|
:data="searchResults"
|
|
226
240
|
>
|
|
227
241
|
<div v-if="results && results.data.length">
|
|
@@ -231,7 +245,7 @@
|
|
|
231
245
|
:key="result.id"
|
|
232
246
|
class="p-0"
|
|
233
247
|
>
|
|
234
|
-
<template v-if="
|
|
248
|
+
<template v-if="currentTypeConfig?.class === 'datasets'">
|
|
235
249
|
<slot
|
|
236
250
|
name="dataset"
|
|
237
251
|
:dataset="result"
|
|
@@ -239,7 +253,7 @@
|
|
|
239
253
|
<DatasetCard :dataset="(result as Dataset)" />
|
|
240
254
|
</slot>
|
|
241
255
|
</template>
|
|
242
|
-
<template v-else-if="
|
|
256
|
+
<template v-else-if="currentTypeConfig?.class === 'dataservices'">
|
|
243
257
|
<slot
|
|
244
258
|
name="dataservice"
|
|
245
259
|
:dataservice="result"
|
|
@@ -247,15 +261,15 @@
|
|
|
247
261
|
<DataserviceCard :dataservice="(result as Dataservice)" />
|
|
248
262
|
</slot>
|
|
249
263
|
</template>
|
|
250
|
-
<template v-else-if="
|
|
264
|
+
<template v-else-if="currentTypeConfig?.class === 'reuses'">
|
|
251
265
|
<slot
|
|
252
266
|
name="reuse"
|
|
253
267
|
:reuse="result"
|
|
254
268
|
>
|
|
255
|
-
<ReuseHorizontalCard :reuse="(result as
|
|
269
|
+
<ReuseHorizontalCard :reuse="(result as ReuseV2)" />
|
|
256
270
|
</slot>
|
|
257
271
|
</template>
|
|
258
|
-
<template v-else-if="
|
|
272
|
+
<template v-else-if="currentTypeConfig?.class === 'organizations'">
|
|
259
273
|
<slot
|
|
260
274
|
name="organization"
|
|
261
275
|
:organization="result"
|
|
@@ -263,6 +277,14 @@
|
|
|
263
277
|
<OrganizationHorizontalCard :organization="(result as Organization)" />
|
|
264
278
|
</slot>
|
|
265
279
|
</template>
|
|
280
|
+
<template v-else-if="currentTypeConfig?.class === 'topics'">
|
|
281
|
+
<slot
|
|
282
|
+
name="topic"
|
|
283
|
+
:topic="result"
|
|
284
|
+
>
|
|
285
|
+
<TopicCard :topic="(result as TopicV2)" />
|
|
286
|
+
</slot>
|
|
287
|
+
</template>
|
|
266
288
|
</li>
|
|
267
289
|
</ul>
|
|
268
290
|
<Pagination
|
|
@@ -271,7 +293,6 @@
|
|
|
271
293
|
:page-size
|
|
272
294
|
:total-results="results.total"
|
|
273
295
|
class="mt-4"
|
|
274
|
-
:link="getLink"
|
|
275
296
|
@change="changePage"
|
|
276
297
|
/>
|
|
277
298
|
</div>
|
|
@@ -333,21 +354,25 @@
|
|
|
333
354
|
</template>
|
|
334
355
|
|
|
335
356
|
<script setup lang="ts">
|
|
336
|
-
import { computed, watch, useTemplateRef, type Ref } from 'vue'
|
|
357
|
+
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
337
358
|
import { useRouteQuery } from '@vueuse/router'
|
|
338
|
-
import {
|
|
359
|
+
import { useRoute } from 'vue-router'
|
|
360
|
+
import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
339
361
|
import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
|
|
340
362
|
import { useTranslation } from '../../composables/useTranslation'
|
|
341
363
|
import { useDebouncedRef } from '../../composables/useDebouncedRef'
|
|
364
|
+
import { configKey, forEachActiveCustomFilter, isCustomFilterActive, searchFilterContextKey, type CustomFilterEntry } from '../../composables/useSearchFilter'
|
|
342
365
|
import { useStableQueryParams } from '../../composables/useStableQueryParams'
|
|
343
366
|
import { useComponentsConfig } from '../../config'
|
|
344
367
|
import { useFetch } from '../../functions/api'
|
|
345
|
-
import {
|
|
368
|
+
import { scrollToBlockTop } from '../../functions/scroll'
|
|
369
|
+
import type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
346
370
|
import type { Dataset } from '../../types/datasets'
|
|
347
371
|
import type { Dataservice } from '../../types/dataservices'
|
|
348
372
|
import type { Organization } from '../../types/organizations'
|
|
349
|
-
import type {
|
|
350
|
-
import type {
|
|
373
|
+
import type { ReuseV2 } from '../../types/reuses'
|
|
374
|
+
import type { TopicV2 } from '../../types/topics'
|
|
375
|
+
import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SortOption, FacetItem } from '../../types/search'
|
|
351
376
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
352
377
|
import BrandedButton from '../BrandedButton.vue'
|
|
353
378
|
import LoadingBlock from '../LoadingBlock.vue'
|
|
@@ -358,6 +383,7 @@ import DatasetCard from '../DatasetCard.vue'
|
|
|
358
383
|
import DataserviceCard from '../DataserviceCard.vue'
|
|
359
384
|
import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
|
|
360
385
|
import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
|
|
386
|
+
import TopicCard from '../TopicCard.vue'
|
|
361
387
|
import SearchInput from './SearchInput.vue'
|
|
362
388
|
import Sidemenu from './Sidemenu.vue'
|
|
363
389
|
import BasicAndAdvancedFilters from './BasicAndAdvancedFilters.vue'
|
|
@@ -379,25 +405,50 @@ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
|
|
|
379
405
|
|
|
380
406
|
const props = withDefaults(defineProps<{
|
|
381
407
|
config?: GlobalSearchConfig
|
|
382
|
-
placeholder?: string
|
|
408
|
+
placeholder?: string | null
|
|
409
|
+
hideSearchInput?: boolean
|
|
410
|
+
autoFocus?: boolean
|
|
383
411
|
}>(), {
|
|
384
412
|
config: getDefaultGlobalSearchConfig,
|
|
413
|
+
hideSearchInput: false,
|
|
414
|
+
autoFocus: true,
|
|
385
415
|
})
|
|
386
416
|
|
|
417
|
+
const emit = defineEmits<{
|
|
418
|
+
resultsCount: [total: number]
|
|
419
|
+
}>()
|
|
420
|
+
|
|
387
421
|
// defineModel's default is static and can't depend on props, so we cast and initialize manually
|
|
388
|
-
const currentType = defineModel<
|
|
389
|
-
if (!currentType.value) currentType.value = props.config[0]
|
|
422
|
+
const currentType = defineModel<string>('type') as Ref<string>
|
|
423
|
+
if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class: 'datasets' })
|
|
390
424
|
|
|
391
425
|
const { t } = useTranslation()
|
|
392
426
|
const componentsConfig = useComponentsConfig()
|
|
427
|
+
const route = useRoute()
|
|
428
|
+
|
|
429
|
+
// Custom filter registry for useSearchFilter composable
|
|
430
|
+
const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
|
|
431
|
+
// Per-filter watch stoppers: each registered filter gets its own watcher so a
|
|
432
|
+
// value change resets page to 1, but registration itself does not (the value
|
|
433
|
+
// came from the URL, not from a user action).
|
|
434
|
+
const customFilterStops = new Map<string, () => void>()
|
|
393
435
|
|
|
394
436
|
// Initial type is used to determine which fetch should be SSR (non-lazy)
|
|
395
437
|
const initialType = currentType.value
|
|
396
438
|
|
|
397
439
|
const currentTypeConfig = computed(() =>
|
|
398
|
-
props.config.find(c => c
|
|
440
|
+
props.config.find(c => configKey(c) === currentType.value),
|
|
399
441
|
)
|
|
400
442
|
|
|
443
|
+
// Precedence: prop → per-type config → strategy default.
|
|
444
|
+
// null at any level means "no placeholder".
|
|
445
|
+
const resolvedPlaceholder = computed(() => {
|
|
446
|
+
if (props.placeholder !== undefined) return props.placeholder ?? ''
|
|
447
|
+
const cfg = currentTypeConfig.value
|
|
448
|
+
if (cfg && 'placeholder' in cfg) return cfg.placeholder ?? ''
|
|
449
|
+
return strategies[cfg?.class ?? 'datasets'].placeholder
|
|
450
|
+
})
|
|
451
|
+
|
|
401
452
|
const activeBasicFilters = computed(() =>
|
|
402
453
|
(currentTypeConfig.value?.basicFilters ?? []) as string[],
|
|
403
454
|
)
|
|
@@ -431,13 +482,35 @@ const activeFilters = computed(() => [
|
|
|
431
482
|
...(currentTypeConfig.value?.advancedFilters ?? []),
|
|
432
483
|
] as string[])
|
|
433
484
|
|
|
434
|
-
const
|
|
485
|
+
const slots = useSlots()
|
|
486
|
+
const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0 || !!slots['custom-filters-top'] || !!slots['custom-filters-bottom'])
|
|
435
487
|
|
|
436
488
|
// URL query params
|
|
437
489
|
const q = useRouteQuery<string>('q', '')
|
|
438
490
|
const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsConfig.searchDebounce ?? 300)
|
|
491
|
+
// When the search input is hidden, the parent owns the input and is expected
|
|
492
|
+
// to debounce user typing itself (otherwise typing would land in the URL
|
|
493
|
+
// instantly via v-model and stack two debounces). Bypass the internal debounce
|
|
494
|
+
// so URL-driven q changes hit the fetch params immediately.
|
|
495
|
+
const qForParams = computed(() => props.hideSearchInput ? q.value : qDebounced.value)
|
|
439
496
|
const page = useRouteQuery('page', 1, { transform: Number })
|
|
440
497
|
const sort = useRouteQuery<string | undefined>('sort')
|
|
498
|
+
const effectiveSort = computed({
|
|
499
|
+
get: () => sort.value ?? currentTypeConfig.value?.defaultSort,
|
|
500
|
+
set: (value) => { sort.value = value },
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
provide(searchFilterContextKey, {
|
|
504
|
+
register(urlParam, entry) {
|
|
505
|
+
customFilterRegistry.set(urlParam, entry)
|
|
506
|
+
customFilterStops.set(urlParam, watch(entry.ref, () => page.value = 1))
|
|
507
|
+
},
|
|
508
|
+
unregister(urlParam) {
|
|
509
|
+
customFilterStops.get(urlParam)?.()
|
|
510
|
+
customFilterStops.delete(urlParam)
|
|
511
|
+
customFilterRegistry.delete(urlParam)
|
|
512
|
+
},
|
|
513
|
+
})
|
|
441
514
|
|
|
442
515
|
// Filter values
|
|
443
516
|
const organizationId = useRouteQuery<string | undefined>('organization')
|
|
@@ -479,63 +552,134 @@ const allFilters: Record<string, Ref<unknown>> = {
|
|
|
479
552
|
type: reuseType,
|
|
480
553
|
}
|
|
481
554
|
|
|
482
|
-
// Reset sort and filters when changing type
|
|
555
|
+
// Reset page, sort and filters when changing type. Every reset below goes
|
|
556
|
+
// through useRouteQuery, so VueUse coalesces them into a single router.replace
|
|
557
|
+
// (its shared _queriesQueue flushes once on nextTick). Custom filters are cleared
|
|
558
|
+
// here too, rather than in useSearchFilter's onunmount, so their URL change joins
|
|
559
|
+
// that same batch instead of racing it with a separate router.replace.
|
|
483
560
|
watch(currentType, () => {
|
|
561
|
+
// page=1 is the default and is dropped from the URL, so only reset when needed.
|
|
562
|
+
if (page.value !== 1) page.value = 1
|
|
563
|
+
|
|
484
564
|
// Reset sort if not valid
|
|
485
565
|
const validSortValues = activeSortOptions.value.map(o => o.value as string)
|
|
486
566
|
if (sort.value && !validSortValues.includes(sort.value)) {
|
|
487
567
|
sort.value = undefined
|
|
488
568
|
}
|
|
489
569
|
|
|
490
|
-
// Reset filters that are not enabled for the new type
|
|
570
|
+
// Reset built-in filters that are not enabled for the new type
|
|
491
571
|
for (const [filterName, filterRef] of Object.entries(allFilters)) {
|
|
492
572
|
if (filterRef.value !== undefined && !activeFilters.value.includes(filterName)) {
|
|
493
573
|
filterRef.value = undefined
|
|
494
574
|
}
|
|
495
575
|
}
|
|
496
|
-
})
|
|
497
576
|
|
|
498
|
-
//
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
577
|
+
// Reset type-scoped custom filters that don't apply to the new type
|
|
578
|
+
for (const entry of customFilterRegistry.values()) {
|
|
579
|
+
if (entry.typeKeys && !entry.typeKeys.includes(currentType.value) && isCustomFilterActive(entry)) {
|
|
580
|
+
entry.ref.value = entry.defaultValue
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
})
|
|
503
584
|
|
|
504
585
|
// Create stable params for each type
|
|
505
586
|
const stableParamsOptions = {
|
|
506
587
|
allFilters,
|
|
507
|
-
|
|
588
|
+
customFilterRegistry,
|
|
589
|
+
q: qForParams,
|
|
508
590
|
sort,
|
|
509
591
|
page,
|
|
510
592
|
pageSize,
|
|
511
593
|
}
|
|
512
594
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
595
|
+
// Discriminated union: each variant carries its own response type so a `class`
|
|
596
|
+
// narrow gives the precise shape of `data.value` (no cast needed).
|
|
597
|
+
type SearchEntry = {
|
|
598
|
+
[K in SearchType]: {
|
|
599
|
+
class: K
|
|
600
|
+
data: Ref<SearchResponseByClass[K] | null>
|
|
601
|
+
status: Ref<AsyncDataRequestStatus>
|
|
602
|
+
}
|
|
603
|
+
}[SearchType]
|
|
604
|
+
|
|
605
|
+
// One strategy per class consolidates everything that varies by class:
|
|
606
|
+
// metadata (icon/name/placeholder), endpoint, and a typed fetch factory.
|
|
607
|
+
type SearchStrategy<C extends SearchType> = {
|
|
608
|
+
url: string
|
|
609
|
+
icon: Component
|
|
610
|
+
name: string
|
|
611
|
+
placeholder: string
|
|
612
|
+
fetch: (
|
|
613
|
+
params: Ref<Record<string, unknown>>,
|
|
614
|
+
server: boolean,
|
|
615
|
+
) => Promise<Extract<SearchEntry, { class: C }>>
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function makeStrategy<C extends SearchType>(
|
|
619
|
+
cls: C,
|
|
620
|
+
meta: Omit<SearchStrategy<C>, 'fetch'>,
|
|
621
|
+
): SearchStrategy<C> {
|
|
622
|
+
return {
|
|
623
|
+
...meta,
|
|
624
|
+
fetch: async (params, server) => {
|
|
625
|
+
const { data, status } = await useFetch<SearchResponseByClass[C]>(
|
|
626
|
+
meta.url,
|
|
627
|
+
{ params, lazy: true, server },
|
|
628
|
+
)
|
|
629
|
+
// Tautologically equivalent to Extract<SearchEntry, { class: C }>, but TS
|
|
630
|
+
// cannot prove it on a generic C, so we assert.
|
|
631
|
+
return { class: cls, data, status } as Extract<SearchEntry, { class: C }>
|
|
632
|
+
},
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const strategies: { [K in SearchType]: SearchStrategy<K> } = {
|
|
637
|
+
datasets: makeStrategy('datasets', {
|
|
638
|
+
url: '/api/2/datasets/search/',
|
|
639
|
+
icon: RiDatabase2Line,
|
|
640
|
+
name: t('Jeux de données'),
|
|
641
|
+
placeholder: t('ex. élections présidentielles'),
|
|
642
|
+
}),
|
|
643
|
+
dataservices: makeStrategy('dataservices', {
|
|
644
|
+
url: '/api/2/dataservices/search/',
|
|
645
|
+
icon: RiTerminalLine,
|
|
646
|
+
name: t('API'),
|
|
647
|
+
placeholder: t('ex: SIRENE'),
|
|
648
|
+
}),
|
|
649
|
+
reuses: makeStrategy('reuses', {
|
|
650
|
+
url: '/api/2/reuses/search/',
|
|
651
|
+
icon: RiLineChartLine,
|
|
652
|
+
name: t('Réutilisations'),
|
|
653
|
+
placeholder: t('Rechercher une réutilisation de données'),
|
|
654
|
+
}),
|
|
655
|
+
organizations: makeStrategy('organizations', {
|
|
656
|
+
url: '/api/2/organizations/search/',
|
|
657
|
+
icon: RiBuilding2Line,
|
|
658
|
+
name: t('Organisations'),
|
|
659
|
+
placeholder: t('Rechercher une organisation'),
|
|
660
|
+
}),
|
|
661
|
+
topics: makeStrategy('topics', {
|
|
662
|
+
url: '/api/2/topics/search/',
|
|
663
|
+
icon: RiBookShelfLine,
|
|
664
|
+
name: t('Thématiques'),
|
|
665
|
+
placeholder: t('Rechercher une thématique'),
|
|
666
|
+
}),
|
|
667
|
+
}
|
|
529
668
|
|
|
530
|
-
//
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
669
|
+
// One params + fetch per config entry, keyed by configKey
|
|
670
|
+
const resultsMap: Record<string, SearchEntry> = {}
|
|
671
|
+
for (const c of props.config) {
|
|
672
|
+
const key = configKey(c)
|
|
673
|
+
const params = useStableQueryParams({ ...stableParamsOptions, typeConfig: c })
|
|
674
|
+
resultsMap[key] = await strategies[c.class].fetch(params, initialType === key)
|
|
675
|
+
}
|
|
535
676
|
|
|
536
|
-
// Reset page on filter/sort change
|
|
677
|
+
// Reset page on filter/sort change. Custom filters (registered via
|
|
678
|
+
// useSearchFilter) have their own watchers set up in `provide`, so they're
|
|
679
|
+
// intentionally excluded here to avoid resetting the page when a filter
|
|
680
|
+
// registers with its URL-derived value.
|
|
537
681
|
const filtersForReset = computed(() => ({
|
|
538
|
-
q:
|
|
682
|
+
q: qForParams.value,
|
|
539
683
|
organization: organizationId.value,
|
|
540
684
|
organization_badge: organizationType.value,
|
|
541
685
|
tag: tag.value,
|
|
@@ -573,6 +717,7 @@ const hasFilters = computed(() => {
|
|
|
573
717
|
|| lastUpdateRange.value
|
|
574
718
|
|| producerType.value
|
|
575
719
|
|| reuseType.value
|
|
720
|
+
|| Array.from(customFilterRegistry.values()).some(isCustomFilterActive)
|
|
576
721
|
})
|
|
577
722
|
|
|
578
723
|
const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
|
|
@@ -593,79 +738,35 @@ function resetFilters() {
|
|
|
593
738
|
lastUpdateRange.value = undefined
|
|
594
739
|
producerType.value = undefined
|
|
595
740
|
reuseType.value = undefined
|
|
741
|
+
for (const entry of customFilterRegistry.values()) {
|
|
742
|
+
entry.ref.value = entry.defaultValue
|
|
743
|
+
}
|
|
596
744
|
q.value = ''
|
|
597
745
|
flushQ()
|
|
598
746
|
}
|
|
599
747
|
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
)
|
|
748
|
+
const searchResults = computed(() => resultsMap[currentType.value]?.data.value)
|
|
749
|
+
const searchResultsStatus = computed(() => resultsMap[currentType.value]?.status.value)
|
|
618
750
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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)
|
|
751
|
+
watch(searchResults, (results) => {
|
|
752
|
+
if (results) emit('resultsCount', results.total)
|
|
753
|
+
}, { immediate: true })
|
|
652
754
|
|
|
653
755
|
// RSS feed URL for datasets
|
|
654
756
|
const rssUrl = computed(() => {
|
|
655
|
-
if (
|
|
757
|
+
if (currentTypeConfig.value?.class !== 'datasets') return null
|
|
656
758
|
|
|
657
759
|
const params = new URLSearchParams()
|
|
658
|
-
const datasetsConfig = props.config.find(c => c.class === 'datasets')
|
|
659
760
|
|
|
660
761
|
// Add hidden filters first
|
|
661
|
-
if (
|
|
662
|
-
for (const hf of
|
|
762
|
+
if (currentTypeConfig.value?.hiddenFilters) {
|
|
763
|
+
for (const hf of currentTypeConfig.value.hiddenFilters) {
|
|
663
764
|
if (hf?.value) params.set(hf.key as string, String(hf.value))
|
|
664
765
|
}
|
|
665
766
|
}
|
|
666
767
|
|
|
667
768
|
// Add active filters
|
|
668
|
-
if (
|
|
769
|
+
if (qForParams.value) params.set('q', qForParams.value)
|
|
669
770
|
if (organizationId.value) params.set('organization', organizationId.value)
|
|
670
771
|
if (organizationType.value) params.set('organization_badge', organizationType.value)
|
|
671
772
|
if (tag.value) params.set('tag', tag.value)
|
|
@@ -677,8 +778,12 @@ const rssUrl = computed(() => {
|
|
|
677
778
|
if (badge.value) params.set('badge', badge.value)
|
|
678
779
|
if (topic.value) params.set('topic', topic.value)
|
|
679
780
|
|
|
781
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
782
|
+
params.set(apiParam, value)
|
|
783
|
+
}, currentTypeConfig.value ? configKey(currentTypeConfig.value) : undefined)
|
|
784
|
+
|
|
680
785
|
// Add sort if set
|
|
681
|
-
if (
|
|
786
|
+
if (effectiveSort.value) params.set('sort', effectiveSort.value)
|
|
682
787
|
|
|
683
788
|
const queryString = params.toString()
|
|
684
789
|
const basePath = '/api/1/datasets/recent.atom'
|
|
@@ -694,14 +799,15 @@ function getFacets(key: string): FacetItem[] | undefined {
|
|
|
694
799
|
}
|
|
695
800
|
|
|
696
801
|
// Scroll handling
|
|
697
|
-
const
|
|
802
|
+
const resultsRef = useTemplateRef('results')
|
|
698
803
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
804
|
+
// Every criteria lives in the URL, custom filters included, so watching the
|
|
805
|
+
// query covers them all: whenever the result list is replaced, bring its top
|
|
806
|
+
// back into view rather than leaving the reader in the middle of a list they
|
|
807
|
+
// have not seen yet.
|
|
808
|
+
watch(() => route.query, () => scrollToBlockTop(resultsRef.value))
|
|
702
809
|
|
|
703
810
|
function changePage(newPage: number) {
|
|
704
811
|
page.value = newPage
|
|
705
|
-
scrollToTop()
|
|
706
812
|
}
|
|
707
813
|
</script>
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
type="search"
|
|
14
14
|
name="q"
|
|
15
15
|
class="input max-h-12 m-0 rounded-tl shadow-input-blue"
|
|
16
|
-
:aria-label="placeholder
|
|
17
|
-
:placeholder="placeholder
|
|
16
|
+
:aria-label="placeholder === null ? t('Rechercher...') : placeholder ?? t('Rechercher...')"
|
|
17
|
+
:placeholder="placeholder === null ? undefined : placeholder ?? t('Rechercher...')"
|
|
18
18
|
>
|
|
19
19
|
<BrandedButton
|
|
20
20
|
class="rounded-l-none rounded-br-none rounded-tr min-h-12"
|
|
@@ -37,8 +37,8 @@ import BrandedButton from '../BrandedButton.vue'
|
|
|
37
37
|
|
|
38
38
|
const q = defineModel<string>({ required: true })
|
|
39
39
|
|
|
40
|
-
withDefaults(defineProps<{
|
|
41
|
-
placeholder?: string
|
|
40
|
+
const props = withDefaults(defineProps<{
|
|
41
|
+
placeholder?: string | null
|
|
42
42
|
autoFocus?: boolean
|
|
43
43
|
}>(), {
|
|
44
44
|
autoFocus: true,
|
|
@@ -57,6 +57,7 @@ const focus = () => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
onMounted(async () => {
|
|
60
|
+
if (!props.autoFocus) return
|
|
60
61
|
await nextTick()
|
|
61
62
|
focus()
|
|
62
63
|
})
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
:class="{ 'rotate-180': open }"
|
|
12
12
|
/>
|
|
13
13
|
</button>
|
|
14
|
-
|
|
14
|
+
<!-- Collapsing with CSS rather than `v-if` on a JS media query: the server has no
|
|
15
|
+
viewport, so it always renders the desktop state and a narrow client would drop
|
|
16
|
+
the whole panel during hydration. -->
|
|
17
|
+
<div :class="{ 'hidden md:block': !open }">
|
|
15
18
|
<p
|
|
16
19
|
:id="titleId"
|
|
17
20
|
class="text-sm font-bold leading-tight mb-6 hidden md:block"
|
|
@@ -26,13 +29,11 @@
|
|
|
26
29
|
<script setup lang="ts">
|
|
27
30
|
import { ref, useId } from 'vue'
|
|
28
31
|
import { RiArrowDownSLine } from '@remixicon/vue'
|
|
29
|
-
import { useMediaQuery } from '@vueuse/core'
|
|
30
32
|
|
|
31
33
|
defineProps<{
|
|
32
34
|
buttonText: string
|
|
33
35
|
}>()
|
|
34
36
|
|
|
35
37
|
const titleId = useId()
|
|
36
|
-
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
37
38
|
const open = ref(false)
|
|
38
39
|
</script>
|