@datagouv/components-next 1.0.2-dev.19 → 1.0.2-dev.191
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-fxOwwaND.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-XSR9THWR.js +36 -0
- package/dist/JsonPreview.client-CU1FWFIy.js +40 -0
- package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
- package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-D0-Dn7jq.js} +41 -42
- package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-B7eDgq8y.js} +835 -856
- package/dist/Pmtiles.client-CCpvggkZ.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BzhNjYoY.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-CyjpNh5i.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 +194 -149
- package/dist/components.css +2 -2
- package/dist/{index-BFQX7Ccz.js → index-BBnxH-Nj.js} +21842 -15810
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-Be0Ohp2k.js} +185 -187
- package/dist/{main-vqhkVhVS.js → main-C2kCZkPD.js} +41515 -73245
- 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-__BZsi8i.js} +16 -16
- package/package.json +47 -38
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +3 -2
- 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 +9 -0
- 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 +9 -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/Form/Listbox.vue +101 -0
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/InfiniteLoader.vue +53 -0
- 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 +23 -58
- 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 +160 -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 +1 -2
- 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 +11 -11
- package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
- package/src/components/ResourceAccordion/ResourceAccordion.vue +23 -114
- package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
- package/src/components/ResourceExplorer/ResourceExplorer.vue +102 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +74 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +103 -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 +115 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
- package/src/components/ResourceListItem.vue +161 -0
- package/src/components/ReuseCard.vue +14 -6
- 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 +220 -117
- package/src/components/Search/SearchInput.vue +5 -4
- package/src/components/Search/Sidemenu.vue +4 -3
- 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 +81 -0
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- 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 +3 -2
- 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/tabular.ts +214 -0
- package/src/functions/tabularApi.ts +96 -11
- package/src/main.ts +117 -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,15 @@
|
|
|
5
5
|
@submit.prevent
|
|
6
6
|
>
|
|
7
7
|
<div
|
|
8
|
+
v-if="!hideSearchInput"
|
|
8
9
|
ref="search"
|
|
9
10
|
class="flex flex-wrap items-center justify-between"
|
|
10
11
|
data-cy="search"
|
|
11
12
|
>
|
|
12
13
|
<SearchInput
|
|
13
14
|
v-model="q"
|
|
14
|
-
:placeholder="
|
|
15
|
+
:placeholder="resolvedPlaceholder"
|
|
16
|
+
:auto-focus
|
|
15
17
|
/>
|
|
16
18
|
</div>
|
|
17
19
|
<div class="grid grid-cols-12 mt-2 md:mt-5">
|
|
@@ -30,23 +32,27 @@
|
|
|
30
32
|
>
|
|
31
33
|
<RadioInput
|
|
32
34
|
v-for="typeConfig in config"
|
|
33
|
-
:key="typeConfig
|
|
34
|
-
:value="typeConfig
|
|
35
|
-
:count="
|
|
36
|
-
:loading="
|
|
37
|
-
:icon="
|
|
35
|
+
:key="configKey(typeConfig)"
|
|
36
|
+
:value="configKey(typeConfig)"
|
|
37
|
+
:count="resultsMap[configKey(typeConfig)]?.data.value?.total"
|
|
38
|
+
:loading="resultsMap[configKey(typeConfig)]?.status.value === 'pending' || resultsMap[configKey(typeConfig)]?.status.value === 'idle'"
|
|
39
|
+
:icon="typeConfig.icon ?? strategies[typeConfig.class].icon"
|
|
38
40
|
>
|
|
39
|
-
{{ typeConfig.name ||
|
|
41
|
+
{{ typeConfig.name || strategies[typeConfig.class].name }}
|
|
40
42
|
</RadioInput>
|
|
41
43
|
</RadioGroup>
|
|
42
44
|
</Sidemenu>
|
|
43
45
|
</div>
|
|
44
46
|
|
|
45
|
-
<div v-if="activeFilters.length > 0">
|
|
47
|
+
<div v-if="activeFilters.length > 0 || $slots['custom-filters-top'] || $slots['custom-filters-bottom']">
|
|
46
48
|
<Sidemenu :button-text="t('Filtres')">
|
|
47
49
|
<template #title>
|
|
48
50
|
{{ t('Filtres') }}
|
|
49
51
|
</template>
|
|
52
|
+
<slot
|
|
53
|
+
name="custom-filters-top"
|
|
54
|
+
:current-type="currentType"
|
|
55
|
+
/>
|
|
50
56
|
<BasicAndAdvancedFilters
|
|
51
57
|
v-slot="{ isEnabled, getOrder }"
|
|
52
58
|
:basic-filters="activeBasicFilters"
|
|
@@ -123,7 +129,7 @@
|
|
|
123
129
|
v-model="producerType"
|
|
124
130
|
:facets="getFacets('producer_type')"
|
|
125
131
|
:loading="searchResultsStatus === 'pending'"
|
|
126
|
-
:exclude="
|
|
132
|
+
:exclude="currentTypeConfig?.class === 'organizations' ? ['user'] : []"
|
|
127
133
|
:style="{ order: getOrder('producer_type') }"
|
|
128
134
|
/>
|
|
129
135
|
<DatasetBadgeFilter
|
|
@@ -146,6 +152,10 @@
|
|
|
146
152
|
:get-order="getOrder"
|
|
147
153
|
/>
|
|
148
154
|
</BasicAndAdvancedFilters>
|
|
155
|
+
<slot
|
|
156
|
+
name="custom-filters-bottom"
|
|
157
|
+
:current-type="currentType"
|
|
158
|
+
/>
|
|
149
159
|
<div
|
|
150
160
|
v-if="hasFilters"
|
|
151
161
|
class="mt-6 text-center"
|
|
@@ -175,6 +185,7 @@
|
|
|
175
185
|
<p
|
|
176
186
|
class="fr-col-auto my-0"
|
|
177
187
|
role="status"
|
|
188
|
+
data-testid="search-result-count"
|
|
178
189
|
>
|
|
179
190
|
{{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
|
|
180
191
|
</p>
|
|
@@ -189,10 +200,13 @@
|
|
|
189
200
|
<div class="fr-col">
|
|
190
201
|
<select
|
|
191
202
|
id="sort-search"
|
|
192
|
-
v-model="
|
|
203
|
+
v-model="effectiveSort"
|
|
193
204
|
class="fr-select text-sm shadow-input-blue!"
|
|
194
205
|
>
|
|
195
|
-
<option
|
|
206
|
+
<option
|
|
207
|
+
v-if="!currentTypeConfig?.defaultSort"
|
|
208
|
+
:value="undefined"
|
|
209
|
+
>
|
|
196
210
|
{{ t('Pertinence') }}
|
|
197
211
|
</option>
|
|
198
212
|
<option
|
|
@@ -214,6 +228,7 @@
|
|
|
214
228
|
size="sm"
|
|
215
229
|
:icon="RiRssLine"
|
|
216
230
|
icon-only
|
|
231
|
+
external
|
|
217
232
|
target="_blank"
|
|
218
233
|
/>
|
|
219
234
|
</div>
|
|
@@ -221,7 +236,7 @@
|
|
|
221
236
|
<transition mode="out-in">
|
|
222
237
|
<LoadingBlock
|
|
223
238
|
v-slot="{ data: results }"
|
|
224
|
-
:status="searchResultsStatus"
|
|
239
|
+
:status="searchResultsStatus!"
|
|
225
240
|
:data="searchResults"
|
|
226
241
|
>
|
|
227
242
|
<div v-if="results && results.data.length">
|
|
@@ -231,7 +246,7 @@
|
|
|
231
246
|
:key="result.id"
|
|
232
247
|
class="p-0"
|
|
233
248
|
>
|
|
234
|
-
<template v-if="
|
|
249
|
+
<template v-if="currentTypeConfig?.class === 'datasets'">
|
|
235
250
|
<slot
|
|
236
251
|
name="dataset"
|
|
237
252
|
:dataset="result"
|
|
@@ -239,7 +254,7 @@
|
|
|
239
254
|
<DatasetCard :dataset="(result as Dataset)" />
|
|
240
255
|
</slot>
|
|
241
256
|
</template>
|
|
242
|
-
<template v-else-if="
|
|
257
|
+
<template v-else-if="currentTypeConfig?.class === 'dataservices'">
|
|
243
258
|
<slot
|
|
244
259
|
name="dataservice"
|
|
245
260
|
:dataservice="result"
|
|
@@ -247,15 +262,15 @@
|
|
|
247
262
|
<DataserviceCard :dataservice="(result as Dataservice)" />
|
|
248
263
|
</slot>
|
|
249
264
|
</template>
|
|
250
|
-
<template v-else-if="
|
|
265
|
+
<template v-else-if="currentTypeConfig?.class === 'reuses'">
|
|
251
266
|
<slot
|
|
252
267
|
name="reuse"
|
|
253
268
|
:reuse="result"
|
|
254
269
|
>
|
|
255
|
-
<ReuseHorizontalCard :reuse="(result as
|
|
270
|
+
<ReuseHorizontalCard :reuse="(result as ReuseV2)" />
|
|
256
271
|
</slot>
|
|
257
272
|
</template>
|
|
258
|
-
<template v-else-if="
|
|
273
|
+
<template v-else-if="currentTypeConfig?.class === 'organizations'">
|
|
259
274
|
<slot
|
|
260
275
|
name="organization"
|
|
261
276
|
:organization="result"
|
|
@@ -263,6 +278,14 @@
|
|
|
263
278
|
<OrganizationHorizontalCard :organization="(result as Organization)" />
|
|
264
279
|
</slot>
|
|
265
280
|
</template>
|
|
281
|
+
<template v-else-if="currentTypeConfig?.class === 'topics'">
|
|
282
|
+
<slot
|
|
283
|
+
name="topic"
|
|
284
|
+
:topic="result"
|
|
285
|
+
>
|
|
286
|
+
<TopicCard :topic="(result as TopicV2)" />
|
|
287
|
+
</slot>
|
|
288
|
+
</template>
|
|
266
289
|
</li>
|
|
267
290
|
</ul>
|
|
268
291
|
<Pagination
|
|
@@ -271,7 +294,6 @@
|
|
|
271
294
|
:page-size
|
|
272
295
|
:total-results="results.total"
|
|
273
296
|
class="mt-4"
|
|
274
|
-
:link="getLink"
|
|
275
297
|
@change="changePage"
|
|
276
298
|
/>
|
|
277
299
|
</div>
|
|
@@ -333,21 +355,23 @@
|
|
|
333
355
|
</template>
|
|
334
356
|
|
|
335
357
|
<script setup lang="ts">
|
|
336
|
-
import { computed, watch, useTemplateRef, type Ref } from 'vue'
|
|
358
|
+
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
337
359
|
import { useRouteQuery } from '@vueuse/router'
|
|
338
|
-
import { RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
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 type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
346
369
|
import type { Dataset } from '../../types/datasets'
|
|
347
370
|
import type { Dataservice } from '../../types/dataservices'
|
|
348
371
|
import type { Organization } from '../../types/organizations'
|
|
349
|
-
import type {
|
|
350
|
-
import type {
|
|
372
|
+
import type { ReuseV2 } from '../../types/reuses'
|
|
373
|
+
import type { TopicV2 } from '../../types/topics'
|
|
374
|
+
import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SortOption, FacetItem } from '../../types/search'
|
|
351
375
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
352
376
|
import BrandedButton from '../BrandedButton.vue'
|
|
353
377
|
import LoadingBlock from '../LoadingBlock.vue'
|
|
@@ -358,6 +382,7 @@ import DatasetCard from '../DatasetCard.vue'
|
|
|
358
382
|
import DataserviceCard from '../DataserviceCard.vue'
|
|
359
383
|
import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
|
|
360
384
|
import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
|
|
385
|
+
import TopicCard from '../TopicCard.vue'
|
|
361
386
|
import SearchInput from './SearchInput.vue'
|
|
362
387
|
import Sidemenu from './Sidemenu.vue'
|
|
363
388
|
import BasicAndAdvancedFilters from './BasicAndAdvancedFilters.vue'
|
|
@@ -379,25 +404,49 @@ import ReuseTypeFilter from './Filter/ReuseTypeFilter.vue'
|
|
|
379
404
|
|
|
380
405
|
const props = withDefaults(defineProps<{
|
|
381
406
|
config?: GlobalSearchConfig
|
|
382
|
-
placeholder?: string
|
|
407
|
+
placeholder?: string | null
|
|
408
|
+
hideSearchInput?: boolean
|
|
409
|
+
autoFocus?: boolean
|
|
383
410
|
}>(), {
|
|
384
411
|
config: getDefaultGlobalSearchConfig,
|
|
412
|
+
hideSearchInput: false,
|
|
413
|
+
autoFocus: true,
|
|
385
414
|
})
|
|
386
415
|
|
|
416
|
+
const emit = defineEmits<{
|
|
417
|
+
resultsCount: [total: number]
|
|
418
|
+
}>()
|
|
419
|
+
|
|
387
420
|
// 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]
|
|
421
|
+
const currentType = defineModel<string>('type') as Ref<string>
|
|
422
|
+
if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class: 'datasets' })
|
|
390
423
|
|
|
391
424
|
const { t } = useTranslation()
|
|
392
425
|
const componentsConfig = useComponentsConfig()
|
|
393
426
|
|
|
427
|
+
// Custom filter registry for useSearchFilter composable
|
|
428
|
+
const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
|
|
429
|
+
// Per-filter watch stoppers: each registered filter gets its own watcher so a
|
|
430
|
+
// value change resets page to 1, but registration itself does not (the value
|
|
431
|
+
// came from the URL, not from a user action).
|
|
432
|
+
const customFilterStops = new Map<string, () => void>()
|
|
433
|
+
|
|
394
434
|
// Initial type is used to determine which fetch should be SSR (non-lazy)
|
|
395
435
|
const initialType = currentType.value
|
|
396
436
|
|
|
397
437
|
const currentTypeConfig = computed(() =>
|
|
398
|
-
props.config.find(c => c
|
|
438
|
+
props.config.find(c => configKey(c) === currentType.value),
|
|
399
439
|
)
|
|
400
440
|
|
|
441
|
+
// Precedence: prop → per-type config → strategy default.
|
|
442
|
+
// null at any level means "no placeholder".
|
|
443
|
+
const resolvedPlaceholder = computed(() => {
|
|
444
|
+
if (props.placeholder !== undefined) return props.placeholder ?? ''
|
|
445
|
+
const cfg = currentTypeConfig.value
|
|
446
|
+
if (cfg && 'placeholder' in cfg) return cfg.placeholder ?? ''
|
|
447
|
+
return strategies[cfg?.class ?? 'datasets'].placeholder
|
|
448
|
+
})
|
|
449
|
+
|
|
401
450
|
const activeBasicFilters = computed(() =>
|
|
402
451
|
(currentTypeConfig.value?.basicFilters ?? []) as string[],
|
|
403
452
|
)
|
|
@@ -431,13 +480,35 @@ const activeFilters = computed(() => [
|
|
|
431
480
|
...(currentTypeConfig.value?.advancedFilters ?? []),
|
|
432
481
|
] as string[])
|
|
433
482
|
|
|
434
|
-
const
|
|
483
|
+
const slots = useSlots()
|
|
484
|
+
const showSidebar = computed(() => props.config.length > 1 || activeFilters.value.length > 0 || !!slots['custom-filters-top'] || !!slots['custom-filters-bottom'])
|
|
435
485
|
|
|
436
486
|
// URL query params
|
|
437
487
|
const q = useRouteQuery<string>('q', '')
|
|
438
488
|
const { debounced: qDebounced, flush: flushQ } = useDebouncedRef(q, componentsConfig.searchDebounce ?? 300)
|
|
489
|
+
// When the search input is hidden, the parent owns the input and is expected
|
|
490
|
+
// to debounce user typing itself (otherwise typing would land in the URL
|
|
491
|
+
// instantly via v-model and stack two debounces). Bypass the internal debounce
|
|
492
|
+
// so URL-driven q changes hit the fetch params immediately.
|
|
493
|
+
const qForParams = computed(() => props.hideSearchInput ? q.value : qDebounced.value)
|
|
439
494
|
const page = useRouteQuery('page', 1, { transform: Number })
|
|
440
495
|
const sort = useRouteQuery<string | undefined>('sort')
|
|
496
|
+
const effectiveSort = computed({
|
|
497
|
+
get: () => sort.value ?? currentTypeConfig.value?.defaultSort,
|
|
498
|
+
set: (value) => { sort.value = value },
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
provide(searchFilterContextKey, {
|
|
502
|
+
register(urlParam, entry) {
|
|
503
|
+
customFilterRegistry.set(urlParam, entry)
|
|
504
|
+
customFilterStops.set(urlParam, watch(entry.ref, () => page.value = 1))
|
|
505
|
+
},
|
|
506
|
+
unregister(urlParam) {
|
|
507
|
+
customFilterStops.get(urlParam)?.()
|
|
508
|
+
customFilterStops.delete(urlParam)
|
|
509
|
+
customFilterRegistry.delete(urlParam)
|
|
510
|
+
},
|
|
511
|
+
})
|
|
441
512
|
|
|
442
513
|
// Filter values
|
|
443
514
|
const organizationId = useRouteQuery<string | undefined>('organization')
|
|
@@ -479,63 +550,134 @@ const allFilters: Record<string, Ref<unknown>> = {
|
|
|
479
550
|
type: reuseType,
|
|
480
551
|
}
|
|
481
552
|
|
|
482
|
-
// Reset sort and filters when changing type
|
|
553
|
+
// Reset page, sort and filters when changing type. Every reset below goes
|
|
554
|
+
// through useRouteQuery, so VueUse coalesces them into a single router.replace
|
|
555
|
+
// (its shared _queriesQueue flushes once on nextTick). Custom filters are cleared
|
|
556
|
+
// here too, rather than in useSearchFilter's onunmount, so their URL change joins
|
|
557
|
+
// that same batch instead of racing it with a separate router.replace.
|
|
483
558
|
watch(currentType, () => {
|
|
559
|
+
// page=1 is the default and is dropped from the URL, so only reset when needed.
|
|
560
|
+
if (page.value !== 1) page.value = 1
|
|
561
|
+
|
|
484
562
|
// Reset sort if not valid
|
|
485
563
|
const validSortValues = activeSortOptions.value.map(o => o.value as string)
|
|
486
564
|
if (sort.value && !validSortValues.includes(sort.value)) {
|
|
487
565
|
sort.value = undefined
|
|
488
566
|
}
|
|
489
567
|
|
|
490
|
-
// Reset filters that are not enabled for the new type
|
|
568
|
+
// Reset built-in filters that are not enabled for the new type
|
|
491
569
|
for (const [filterName, filterRef] of Object.entries(allFilters)) {
|
|
492
570
|
if (filterRef.value !== undefined && !activeFilters.value.includes(filterName)) {
|
|
493
571
|
filterRef.value = undefined
|
|
494
572
|
}
|
|
495
573
|
}
|
|
496
|
-
})
|
|
497
574
|
|
|
498
|
-
//
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
575
|
+
// Reset type-scoped custom filters that don't apply to the new type
|
|
576
|
+
for (const entry of customFilterRegistry.values()) {
|
|
577
|
+
if (entry.typeKeys && !entry.typeKeys.includes(currentType.value) && isCustomFilterActive(entry)) {
|
|
578
|
+
entry.ref.value = entry.defaultValue
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
})
|
|
503
582
|
|
|
504
583
|
// Create stable params for each type
|
|
505
584
|
const stableParamsOptions = {
|
|
506
585
|
allFilters,
|
|
507
|
-
|
|
586
|
+
customFilterRegistry,
|
|
587
|
+
q: qForParams,
|
|
508
588
|
sort,
|
|
509
589
|
page,
|
|
510
590
|
pageSize,
|
|
511
591
|
}
|
|
512
592
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
593
|
+
// Discriminated union: each variant carries its own response type so a `class`
|
|
594
|
+
// narrow gives the precise shape of `data.value` (no cast needed).
|
|
595
|
+
type SearchEntry = {
|
|
596
|
+
[K in SearchType]: {
|
|
597
|
+
class: K
|
|
598
|
+
data: Ref<SearchResponseByClass[K] | null>
|
|
599
|
+
status: Ref<AsyncDataRequestStatus>
|
|
600
|
+
}
|
|
601
|
+
}[SearchType]
|
|
602
|
+
|
|
603
|
+
// One strategy per class consolidates everything that varies by class:
|
|
604
|
+
// metadata (icon/name/placeholder), endpoint, and a typed fetch factory.
|
|
605
|
+
type SearchStrategy<C extends SearchType> = {
|
|
606
|
+
url: string
|
|
607
|
+
icon: Component
|
|
608
|
+
name: string
|
|
609
|
+
placeholder: string
|
|
610
|
+
fetch: (
|
|
611
|
+
params: Ref<Record<string, unknown>>,
|
|
612
|
+
server: boolean,
|
|
613
|
+
) => Promise<Extract<SearchEntry, { class: C }>>
|
|
614
|
+
}
|
|
529
615
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
616
|
+
function makeStrategy<C extends SearchType>(
|
|
617
|
+
cls: C,
|
|
618
|
+
meta: Omit<SearchStrategy<C>, 'fetch'>,
|
|
619
|
+
): SearchStrategy<C> {
|
|
620
|
+
return {
|
|
621
|
+
...meta,
|
|
622
|
+
fetch: async (params, server) => {
|
|
623
|
+
const { data, status } = await useFetch<SearchResponseByClass[C]>(
|
|
624
|
+
meta.url,
|
|
625
|
+
{ params, lazy: true, server },
|
|
626
|
+
)
|
|
627
|
+
// Tautologically equivalent to Extract<SearchEntry, { class: C }>, but TS
|
|
628
|
+
// cannot prove it on a generic C, so we assert.
|
|
629
|
+
return { class: cls, data, status } as Extract<SearchEntry, { class: C }>
|
|
630
|
+
},
|
|
631
|
+
}
|
|
632
|
+
}
|
|
535
633
|
|
|
536
|
-
|
|
634
|
+
const strategies: { [K in SearchType]: SearchStrategy<K> } = {
|
|
635
|
+
datasets: makeStrategy('datasets', {
|
|
636
|
+
url: '/api/2/datasets/search/',
|
|
637
|
+
icon: RiDatabase2Line,
|
|
638
|
+
name: t('Jeux de données'),
|
|
639
|
+
placeholder: t('ex. élections présidentielles'),
|
|
640
|
+
}),
|
|
641
|
+
dataservices: makeStrategy('dataservices', {
|
|
642
|
+
url: '/api/2/dataservices/search/',
|
|
643
|
+
icon: RiTerminalLine,
|
|
644
|
+
name: t('API'),
|
|
645
|
+
placeholder: t('ex: SIRENE'),
|
|
646
|
+
}),
|
|
647
|
+
reuses: makeStrategy('reuses', {
|
|
648
|
+
url: '/api/2/reuses/search/',
|
|
649
|
+
icon: RiLineChartLine,
|
|
650
|
+
name: t('Réutilisations'),
|
|
651
|
+
placeholder: t('Rechercher une réutilisation de données'),
|
|
652
|
+
}),
|
|
653
|
+
organizations: makeStrategy('organizations', {
|
|
654
|
+
url: '/api/2/organizations/search/',
|
|
655
|
+
icon: RiBuilding2Line,
|
|
656
|
+
name: t('Organisations'),
|
|
657
|
+
placeholder: t('Rechercher une organisation'),
|
|
658
|
+
}),
|
|
659
|
+
topics: makeStrategy('topics', {
|
|
660
|
+
url: '/api/2/topics/search/',
|
|
661
|
+
icon: RiBookShelfLine,
|
|
662
|
+
name: t('Thématiques'),
|
|
663
|
+
placeholder: t('Rechercher une thématique'),
|
|
664
|
+
}),
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// One params + fetch per config entry, keyed by configKey
|
|
668
|
+
const resultsMap: Record<string, SearchEntry> = {}
|
|
669
|
+
for (const c of props.config) {
|
|
670
|
+
const key = configKey(c)
|
|
671
|
+
const params = useStableQueryParams({ ...stableParamsOptions, typeConfig: c })
|
|
672
|
+
resultsMap[key] = await strategies[c.class].fetch(params, initialType === key)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Reset page on filter/sort change. Custom filters (registered via
|
|
676
|
+
// useSearchFilter) have their own watchers set up in `provide`, so they're
|
|
677
|
+
// intentionally excluded here to avoid resetting the page when a filter
|
|
678
|
+
// registers with its URL-derived value.
|
|
537
679
|
const filtersForReset = computed(() => ({
|
|
538
|
-
q:
|
|
680
|
+
q: qForParams.value,
|
|
539
681
|
organization: organizationId.value,
|
|
540
682
|
organization_badge: organizationType.value,
|
|
541
683
|
tag: tag.value,
|
|
@@ -573,6 +715,7 @@ const hasFilters = computed(() => {
|
|
|
573
715
|
|| lastUpdateRange.value
|
|
574
716
|
|| producerType.value
|
|
575
717
|
|| reuseType.value
|
|
718
|
+
|| Array.from(customFilterRegistry.values()).some(isCustomFilterActive)
|
|
576
719
|
})
|
|
577
720
|
|
|
578
721
|
const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
|
|
@@ -593,79 +736,35 @@ function resetFilters() {
|
|
|
593
736
|
lastUpdateRange.value = undefined
|
|
594
737
|
producerType.value = undefined
|
|
595
738
|
reuseType.value = undefined
|
|
739
|
+
for (const entry of customFilterRegistry.values()) {
|
|
740
|
+
entry.ref.value = entry.defaultValue
|
|
741
|
+
}
|
|
596
742
|
q.value = ''
|
|
597
743
|
flushQ()
|
|
598
744
|
}
|
|
599
745
|
|
|
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
|
-
)
|
|
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
|
|
746
|
+
const searchResults = computed(() => resultsMap[currentType.value]?.data.value)
|
|
747
|
+
const searchResultsStatus = computed(() => resultsMap[currentType.value]?.status.value)
|
|
649
748
|
|
|
650
|
-
|
|
651
|
-
|
|
749
|
+
watch(searchResults, (results) => {
|
|
750
|
+
if (results) emit('resultsCount', results.total)
|
|
751
|
+
}, { immediate: true })
|
|
652
752
|
|
|
653
753
|
// RSS feed URL for datasets
|
|
654
754
|
const rssUrl = computed(() => {
|
|
655
|
-
if (
|
|
755
|
+
if (currentTypeConfig.value?.class !== 'datasets') return null
|
|
656
756
|
|
|
657
757
|
const params = new URLSearchParams()
|
|
658
|
-
const datasetsConfig = props.config.find(c => c.class === 'datasets')
|
|
659
758
|
|
|
660
759
|
// Add hidden filters first
|
|
661
|
-
if (
|
|
662
|
-
for (const hf of
|
|
760
|
+
if (currentTypeConfig.value?.hiddenFilters) {
|
|
761
|
+
for (const hf of currentTypeConfig.value.hiddenFilters) {
|
|
663
762
|
if (hf?.value) params.set(hf.key as string, String(hf.value))
|
|
664
763
|
}
|
|
665
764
|
}
|
|
666
765
|
|
|
667
766
|
// Add active filters
|
|
668
|
-
if (
|
|
767
|
+
if (qForParams.value) params.set('q', qForParams.value)
|
|
669
768
|
if (organizationId.value) params.set('organization', organizationId.value)
|
|
670
769
|
if (organizationType.value) params.set('organization_badge', organizationType.value)
|
|
671
770
|
if (tag.value) params.set('tag', tag.value)
|
|
@@ -677,8 +776,12 @@ const rssUrl = computed(() => {
|
|
|
677
776
|
if (badge.value) params.set('badge', badge.value)
|
|
678
777
|
if (topic.value) params.set('topic', topic.value)
|
|
679
778
|
|
|
779
|
+
forEachActiveCustomFilter(customFilterRegistry, (apiParam, value) => {
|
|
780
|
+
params.set(apiParam, value)
|
|
781
|
+
}, currentTypeConfig.value ? configKey(currentTypeConfig.value) : undefined)
|
|
782
|
+
|
|
680
783
|
// Add sort if set
|
|
681
|
-
if (
|
|
784
|
+
if (effectiveSort.value) params.set('sort', effectiveSort.value)
|
|
682
785
|
|
|
683
786
|
const queryString = params.toString()
|
|
684
787
|
const basePath = '/api/1/datasets/recent.atom'
|
|
@@ -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>
|