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