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