@datagouv/components-next 1.0.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/assets/main.css +0 -28
- package/dist/{Datafair.client-Dls5AHTE.js → Datafair.client-B5lBpOl8.js} +2 -2
- package/dist/{JsonPreview.client-DPDTs433.js → JsonPreview.client-Doz1Z0BS.js} +16 -16
- package/dist/{MapContainer.client-BdAzd7bj.js → MapContainer.client-oiieO8H-.js} +3 -3
- package/dist/PdfPreview.client-CdAhkDFJ.js +14513 -0
- package/dist/{Pmtiles.client-mF6xaOO_.js → Pmtiles.client-B0v8tGJQ.js} +2 -2
- package/dist/Swagger.client-CsK65JnG.js +4 -0
- package/dist/{XmlPreview.client-C0OgBkSq.js → XmlPreview.client-CrjHf74q.js} +15 -15
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +130 -125
- package/dist/components.css +1 -1
- package/dist/{index-BRGqW8aQ.js → index-Bbu9rOHt.js} +1 -1
- package/dist/{main-CNHxAJ8J.js → main-CiH8ZmBI.js} +22114 -21911
- package/dist/{vue3-xml-viewer.common-CmAdQfIy.js → vue3-xml-viewer.common-Bi_bsV6C.js} +1 -1
- package/package.json +2 -2
- package/src/components/DataserviceCard.vue +3 -3
- package/src/components/DatasetCard.vue +2 -2
- package/src/components/DatasetQuality.vue +23 -16
- package/src/components/DatasetQualityInline.vue +13 -17
- package/src/components/DatasetQualityScore.vue +12 -15
- package/src/components/DiscussionMessageCard.vue +1 -1
- package/src/components/ObjectCard.vue +2 -2
- package/src/components/ObjectCardHeader.vue +1 -1
- package/src/components/OrganizationHorizontalCard.vue +87 -0
- package/src/components/OrganizationNameWithCertificate.vue +1 -1
- package/src/components/ProgressBar.vue +31 -0
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/JsonPreview.client.vue +3 -3
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -1
- package/src/components/ResourceAccordion/PdfPreview.client.vue +70 -74
- package/src/components/ResourceAccordion/Pmtiles.client.vue +1 -1
- package/src/components/ResourceAccordion/Preview.vue +1 -1
- package/src/components/ResourceAccordion/ResourceAccordion.vue +5 -8
- package/src/components/ResourceAccordion/XmlPreview.client.vue +3 -3
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +50 -1
- package/src/components/ReuseHorizontalCard.vue +1 -1
- package/src/components/Search/Filter/ProducerTypeFilter.vue +13 -3
- package/src/components/Search/GlobalSearch.vue +124 -28
- package/src/components/Toggletip.vue +5 -2
- package/src/components/TopicCard.vue +1 -1
- package/src/composables/useHasTabularData.ts +15 -0
- package/src/composables/useResourceCapabilities.ts +18 -5
- package/src/composables/useTranslation.ts +2 -1
- package/src/functions/api.ts +11 -3
- package/src/functions/api.types.ts +1 -1
- package/src/functions/resourceCapabilities.ts +55 -0
- package/src/main.ts +8 -1
- package/src/types/resources.ts +10 -0
- package/src/types/search.ts +29 -1
- package/dist/PdfPreview.client-CopqSDyt.js +0 -107
- package/dist/Swagger.client-eJ7gpfZA.js +0 -4
- package/dist/pdf-vue3-IkJO65RH.js +0 -273
- package/dist/pdf.min-f72cfa08-CdgJTooZ.js +0 -9501
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
rel="ugc nofollow noopener"
|
|
114
114
|
new-tab
|
|
115
115
|
size="xs"
|
|
116
|
+
color="secondary"
|
|
116
117
|
external
|
|
117
118
|
@click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
|
|
118
119
|
>
|
|
@@ -127,7 +128,7 @@
|
|
|
127
128
|
:id="resource.id + '-copy'"
|
|
128
129
|
:data-clipboard-text="resource.url"
|
|
129
130
|
:aria-describedby="resourceTitleId"
|
|
130
|
-
color="
|
|
131
|
+
color="secondary"
|
|
131
132
|
size="xs"
|
|
132
133
|
:icon="RiFileCopyLine"
|
|
133
134
|
>
|
|
@@ -390,6 +391,7 @@ import { getResourceFormatIcon, getResourceTitleId, detectOgcService } from '../
|
|
|
390
391
|
import BrandedButton from '../BrandedButton.vue'
|
|
391
392
|
import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
|
|
392
393
|
import { useTranslation } from '../../composables/useTranslation'
|
|
394
|
+
import { useHasTabularData } from '../../composables/useHasTabularData'
|
|
393
395
|
import Metadata from './Metadata.vue'
|
|
394
396
|
import SchemaBadge from './SchemaBadge.vue'
|
|
395
397
|
import ResourceIcon from './ResourceIcon.vue'
|
|
@@ -426,6 +428,7 @@ const DatafairPreview = defineAsyncComponent(() => import('./Datafair.client.vue
|
|
|
426
428
|
|
|
427
429
|
const { t } = useTranslation()
|
|
428
430
|
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
431
|
+
const checkTabularData = useHasTabularData()
|
|
429
432
|
|
|
430
433
|
const hasPreview = computed(() => {
|
|
431
434
|
// For JSON, PDF, and XML files, show preview.
|
|
@@ -435,13 +438,7 @@ const hasPreview = computed(() => {
|
|
|
435
438
|
return format === 'json' || format === 'pdf' || format === 'xml'
|
|
436
439
|
})
|
|
437
440
|
|
|
438
|
-
const hasTabularData = computed(() =>
|
|
439
|
-
// Determines if we should show the "Données" tab for tabular files AND the "Structure des données" tab (for tabular data structure)
|
|
440
|
-
return config.tabularApiUrl
|
|
441
|
-
&& props.resource.extras['analysis:parsing:parsing_table']
|
|
442
|
-
&& !props.resource.extras['analysis:parsing:error']
|
|
443
|
-
&& (config.tabularAllowRemote || props.resource.filetype === 'file')
|
|
444
|
-
})
|
|
441
|
+
const hasTabularData = computed(() => checkTabularData(props.resource))
|
|
445
442
|
|
|
446
443
|
const hasPmtiles = computed(() => {
|
|
447
444
|
return props.resource.extras['analysis:parsing:pmtiles_url'] || props.resource.format === 'pmtiles'
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
type="warning"
|
|
15
15
|
class="flex items-center space-x-2"
|
|
16
16
|
>
|
|
17
|
-
<RiErrorWarningLine class="
|
|
17
|
+
<RiErrorWarningLine class="shrink-0 size-6" />
|
|
18
18
|
<span>{{ fileSizeBytes
|
|
19
19
|
? t("Fichier XML trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
|
|
20
20
|
: t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
type="warning"
|
|
26
26
|
class="flex items-center space-x-2"
|
|
27
27
|
>
|
|
28
|
-
<RiErrorWarningLine class="
|
|
28
|
+
<RiErrorWarningLine class="shrink-0 size-6" />
|
|
29
29
|
<span>{{ t("Ce fichier XML ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
|
|
30
30
|
</SimpleBanner>
|
|
31
31
|
<SimpleBanner
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
type="warning"
|
|
34
34
|
class="flex items-center space-x-2"
|
|
35
35
|
>
|
|
36
|
-
<RiErrorWarningLine class="
|
|
36
|
+
<RiErrorWarningLine class="shrink-0 size-6" />
|
|
37
37
|
<span>{{ t("Erreur lors du chargement de l'aperçu XML.") }}</span>
|
|
38
38
|
</SimpleBanner>
|
|
39
39
|
</div>
|
|
@@ -228,6 +228,52 @@
|
|
|
228
228
|
/>
|
|
229
229
|
</dd>
|
|
230
230
|
</template>
|
|
231
|
+
<template v-if="wfsFormats.length">
|
|
232
|
+
<dt class="font-bold fr-text--sm fr-mb-0">
|
|
233
|
+
<div class="flex gap-1 items-center">
|
|
234
|
+
{{ t('Formats exportés depuis le service WFS') }}
|
|
235
|
+
<span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
|
|
236
|
+
<Tooltip>
|
|
237
|
+
<RiInformationLine
|
|
238
|
+
class="flex-none size-4"
|
|
239
|
+
:aria-label="t(`Le lien de téléchargement interroge directement le flux WFS distant. Le nombre de features téléchargées peut être limité.`)"
|
|
240
|
+
aria-hidden="true"
|
|
241
|
+
/>
|
|
242
|
+
<template #tooltip>
|
|
243
|
+
<p class="text-sm font-normal mb-0">
|
|
244
|
+
{{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
|
|
245
|
+
</p>
|
|
246
|
+
<p class="text-sm font-normal mb-0">
|
|
247
|
+
{{ t(`Le nombre de features téléchargées peut être limité.`) }}
|
|
248
|
+
</p>
|
|
249
|
+
</template>
|
|
250
|
+
</Tooltip>
|
|
251
|
+
</div>
|
|
252
|
+
</dt>
|
|
253
|
+
<dd
|
|
254
|
+
v-for="wfsFormat in wfsFormats"
|
|
255
|
+
:key="wfsFormat.format"
|
|
256
|
+
class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
|
|
257
|
+
>
|
|
258
|
+
<span>
|
|
259
|
+
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
|
|
260
|
+
<a
|
|
261
|
+
:href="wfsFormat.url"
|
|
262
|
+
class="fr-link"
|
|
263
|
+
rel="ugc nofollow noopener"
|
|
264
|
+
@click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
|
|
265
|
+
>
|
|
266
|
+
<span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
|
|
267
|
+
</a>
|
|
268
|
+
</span>
|
|
269
|
+
<CopyButton
|
|
270
|
+
:label="t('Copier le lien')"
|
|
271
|
+
:copied-label="t('Lien copié !')"
|
|
272
|
+
:text="wfsFormat.url"
|
|
273
|
+
class="relative"
|
|
274
|
+
/>
|
|
275
|
+
</dd>
|
|
276
|
+
</template>
|
|
231
277
|
</dl>
|
|
232
278
|
</div>
|
|
233
279
|
<div v-if="tab.key === 'swagger'">
|
|
@@ -251,7 +297,7 @@
|
|
|
251
297
|
|
|
252
298
|
<script setup lang="ts">
|
|
253
299
|
import { computed, defineAsyncComponent } from 'vue'
|
|
254
|
-
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
|
|
300
|
+
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
|
|
255
301
|
import { toast } from 'vue-sonner'
|
|
256
302
|
import BrandedButton from '../BrandedButton.vue'
|
|
257
303
|
import CopyButton from '../CopyButton.vue'
|
|
@@ -263,6 +309,7 @@ import TabList from '../Tabs/TabList.vue'
|
|
|
263
309
|
import Tab from '../Tabs/Tab.vue'
|
|
264
310
|
import TabPanels from '../Tabs/TabPanels.vue'
|
|
265
311
|
import TabPanel from '../Tabs/TabPanel.vue'
|
|
312
|
+
import Tooltip from '../Tooltip.vue'
|
|
266
313
|
import Preview from '../ResourceAccordion/Preview.vue'
|
|
267
314
|
import DataStructure from '../ResourceAccordion/DataStructure.vue'
|
|
268
315
|
import Metadata from '../ResourceAccordion/Metadata.vue'
|
|
@@ -316,6 +363,8 @@ const {
|
|
|
316
363
|
ogcService,
|
|
317
364
|
ogcWms,
|
|
318
365
|
generatedFormats,
|
|
366
|
+
wfsFormats,
|
|
367
|
+
defaultWfsProjection,
|
|
319
368
|
isResourceUrl,
|
|
320
369
|
tabsOptions,
|
|
321
370
|
} = useResourceCapabilities(() => props.resource, () => props.dataset)
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
<div
|
|
40
40
|
v-if="reuse.organization || reuse.owner"
|
|
41
|
-
class="text-sm
|
|
41
|
+
class="text-sm flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
|
|
42
42
|
>
|
|
43
43
|
<ObjectCardOwner
|
|
44
44
|
:organization="reuse.organization"
|
|
@@ -13,15 +13,19 @@
|
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
15
|
<script setup lang="ts">
|
|
16
|
+
import { computed } from 'vue'
|
|
16
17
|
import type { FacetItem } from '../../../types/search'
|
|
17
18
|
import { useTranslation } from '../../../composables/useTranslation'
|
|
18
19
|
import FilterButtonGroup from './FilterButtonGroup.vue'
|
|
19
20
|
|
|
20
|
-
defineProps<{
|
|
21
|
+
const props = withDefaults(defineProps<{
|
|
21
22
|
modelValue: string | undefined
|
|
22
23
|
facets?: FacetItem[]
|
|
23
24
|
loading?: boolean
|
|
24
|
-
|
|
25
|
+
exclude?: string[]
|
|
26
|
+
}>(), {
|
|
27
|
+
exclude: () => [],
|
|
28
|
+
})
|
|
25
29
|
|
|
26
30
|
const emit = defineEmits<{
|
|
27
31
|
'update:modelValue': [value: string | undefined]
|
|
@@ -29,11 +33,17 @@ const emit = defineEmits<{
|
|
|
29
33
|
|
|
30
34
|
const { t } = useTranslation()
|
|
31
35
|
|
|
32
|
-
const
|
|
36
|
+
const allOptions = [
|
|
33
37
|
{ value: 'public-service', label: t('Service public') },
|
|
34
38
|
{ value: 'local-authority', label: t('Collectivité territoriale') },
|
|
35
39
|
{ value: 'company', label: t('Entreprise') },
|
|
36
40
|
{ value: 'association', label: t('Association') },
|
|
37
41
|
{ value: 'user', label: t('Utilisateur') },
|
|
38
42
|
]
|
|
43
|
+
|
|
44
|
+
const options = computed(() =>
|
|
45
|
+
props.exclude.length > 0
|
|
46
|
+
? allOptions.filter(o => !props.exclude.includes(o.value))
|
|
47
|
+
: allOptions,
|
|
48
|
+
)
|
|
39
49
|
</script>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
>
|
|
12
12
|
<SearchInput
|
|
13
13
|
v-model="q"
|
|
14
|
-
:placeholder="placeholder ||
|
|
14
|
+
:placeholder="placeholder || typesMeta[currentType].placeholder"
|
|
15
15
|
/>
|
|
16
16
|
</div>
|
|
17
17
|
<div class="grid grid-cols-12 mt-2 md:mt-5">
|
|
@@ -123,6 +123,7 @@
|
|
|
123
123
|
v-model="producerType"
|
|
124
124
|
:facets="getFacets('producer_type')"
|
|
125
125
|
:loading="searchResultsStatus === 'pending'"
|
|
126
|
+
:exclude="currentType === 'organizations' ? ['user'] : []"
|
|
126
127
|
:style="{ order: getOrder('producer_type') }"
|
|
127
128
|
/>
|
|
128
129
|
<DatasetBadgeFilter
|
|
@@ -177,31 +178,44 @@
|
|
|
177
178
|
>
|
|
178
179
|
{{ t("{count} résultats | {count} résultat | {count} résultats", searchResults.total) }}
|
|
179
180
|
</p>
|
|
180
|
-
<div class="fr-col-auto fr-grid-row fr-grid-row--middle">
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
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!"
|
|
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"
|
|
192
186
|
>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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!"
|
|
200
194
|
>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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>
|
|
204
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
|
+
/>
|
|
205
219
|
</div>
|
|
206
220
|
</div>
|
|
207
221
|
<transition mode="out-in">
|
|
@@ -241,6 +255,14 @@
|
|
|
241
255
|
<ReuseHorizontalCard :reuse="(result as Reuse)" />
|
|
242
256
|
</slot>
|
|
243
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>
|
|
244
266
|
</li>
|
|
245
267
|
</ul>
|
|
246
268
|
<Pagination
|
|
@@ -294,6 +316,7 @@
|
|
|
294
316
|
color="tertiary"
|
|
295
317
|
:href="componentsConfig.forumUrl"
|
|
296
318
|
:icon="RiLightbulbLine"
|
|
319
|
+
keep-margins-even-without-borders
|
|
297
320
|
>
|
|
298
321
|
{{ t("Voir le forum") }}
|
|
299
322
|
</BrandedButton>
|
|
@@ -312,7 +335,7 @@
|
|
|
312
335
|
<script setup lang="ts">
|
|
313
336
|
import { computed, watch, useTemplateRef, type Ref } from 'vue'
|
|
314
337
|
import { useRouteQuery } from '@vueuse/router'
|
|
315
|
-
import { RiCloseCircleLine, RiDatabase2Line,
|
|
338
|
+
import { RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
316
339
|
import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
|
|
317
340
|
import { useTranslation } from '../../composables/useTranslation'
|
|
318
341
|
import { useDebouncedRef } from '../../composables/useDebouncedRef'
|
|
@@ -322,8 +345,9 @@ import { useFetch } from '../../functions/api'
|
|
|
322
345
|
import { getLink } from '../../functions/pagination'
|
|
323
346
|
import type { Dataset } from '../../types/datasets'
|
|
324
347
|
import type { Dataservice } from '../../types/dataservices'
|
|
348
|
+
import type { Organization } from '../../types/organizations'
|
|
325
349
|
import type { Reuse } from '../../types/reuses'
|
|
326
|
-
import type { GlobalSearchConfig, SearchType, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, FacetItem } from '../../types/search'
|
|
350
|
+
import type { GlobalSearchConfig, SearchType, SortOption, DatasetSearchResponse, DataserviceSearchResponse, ReuseSearchResponse, OrganizationSearchResponse, FacetItem } from '../../types/search'
|
|
327
351
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
328
352
|
import BrandedButton from '../BrandedButton.vue'
|
|
329
353
|
import LoadingBlock from '../LoadingBlock.vue'
|
|
@@ -332,6 +356,7 @@ import RadioGroup from '../RadioGroup.vue'
|
|
|
332
356
|
import RadioInput from '../RadioInput.vue'
|
|
333
357
|
import DatasetCard from '../DatasetCard.vue'
|
|
334
358
|
import DataserviceCard from '../DataserviceCard.vue'
|
|
359
|
+
import OrganizationHorizontalCard from '../OrganizationHorizontalCard.vue'
|
|
335
360
|
import ReuseHorizontalCard from '../ReuseHorizontalCard.vue'
|
|
336
361
|
import SearchInput from './SearchInput.vue'
|
|
337
362
|
import Sidemenu from './Sidemenu.vue'
|
|
@@ -385,6 +410,22 @@ const activeSortOptions = computed(() =>
|
|
|
385
410
|
currentTypeConfig.value?.sortOptions ?? [],
|
|
386
411
|
)
|
|
387
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
|
+
|
|
388
429
|
const activeFilters = computed(() => [
|
|
389
430
|
...(currentTypeConfig.value?.basicFilters ?? []),
|
|
390
431
|
...(currentTypeConfig.value?.advancedFilters ?? []),
|
|
@@ -458,6 +499,7 @@ watch(currentType, () => {
|
|
|
458
499
|
const datasetsEnabled = computed(() => props.config.some(c => c.class === 'datasets'))
|
|
459
500
|
const dataservicesEnabled = computed(() => props.config.some(c => c.class === 'dataservices'))
|
|
460
501
|
const reusesEnabled = computed(() => props.config.some(c => c.class === 'reuses'))
|
|
502
|
+
const organizationsEnabled = computed(() => props.config.some(c => c.class === 'organizations'))
|
|
461
503
|
|
|
462
504
|
// Create stable params for each type
|
|
463
505
|
const stableParamsOptions = {
|
|
@@ -480,11 +522,16 @@ const reusesParams = useStableQueryParams({
|
|
|
480
522
|
...stableParamsOptions,
|
|
481
523
|
typeConfig: props.config.find(c => c.class === 'reuses'),
|
|
482
524
|
})
|
|
525
|
+
const organizationsParams = useStableQueryParams({
|
|
526
|
+
...stableParamsOptions,
|
|
527
|
+
typeConfig: props.config.find(c => c.class === 'organizations'),
|
|
528
|
+
})
|
|
483
529
|
|
|
484
530
|
// URLs that return null when type is not enabled
|
|
485
531
|
const datasetsUrl = computed(() => datasetsEnabled.value ? '/api/2/datasets/search/' : null)
|
|
486
532
|
const dataservicesUrl = computed(() => dataservicesEnabled.value ? '/api/2/dataservices/search/' : null)
|
|
487
533
|
const reusesUrl = computed(() => reusesEnabled.value ? '/api/2/reuses/search/' : null)
|
|
534
|
+
const organizationsUrl = computed(() => organizationsEnabled.value ? '/api/2/organizations/search/' : null)
|
|
488
535
|
|
|
489
536
|
// Reset page on filter/sort change
|
|
490
537
|
const filtersForReset = computed(() => ({
|
|
@@ -528,7 +575,7 @@ const hasFilters = computed(() => {
|
|
|
528
575
|
|| reuseType.value
|
|
529
576
|
})
|
|
530
577
|
|
|
531
|
-
const showForumLink = computed(() => currentType.value === 'datasets' && !!componentsConfig.forumUrl)
|
|
578
|
+
const showForumLink = computed(() => (currentType.value === 'datasets' || currentType.value === 'dataservices') && !!componentsConfig.forumUrl)
|
|
532
579
|
|
|
533
580
|
function resetFilters() {
|
|
534
581
|
organizationId.value = undefined
|
|
@@ -564,31 +611,80 @@ const { data: reusesResults, status: reusesStatus } = await useFetch<ReuseSearch
|
|
|
564
611
|
reusesUrl,
|
|
565
612
|
{ params: reusesParams, lazy: true, server: initialType === 'reuses' },
|
|
566
613
|
)
|
|
614
|
+
const { data: organizationsResults, status: organizationsStatus } = await useFetch<OrganizationSearchResponse<Organization>>(
|
|
615
|
+
organizationsUrl,
|
|
616
|
+
{ params: organizationsParams, lazy: true, server: initialType === 'organizations' },
|
|
617
|
+
)
|
|
567
618
|
|
|
568
619
|
const typesMeta = {
|
|
569
620
|
datasets: {
|
|
570
621
|
icon: RiDatabase2Line,
|
|
571
622
|
name: t('Jeux de données'),
|
|
623
|
+
placeholder: t('ex. élections présidentielles'),
|
|
572
624
|
results: datasetsResults,
|
|
573
625
|
status: datasetsStatus,
|
|
574
626
|
},
|
|
575
627
|
dataservices: {
|
|
576
|
-
icon:
|
|
577
|
-
name: t('
|
|
628
|
+
icon: RiTerminalLine,
|
|
629
|
+
name: t('API'),
|
|
630
|
+
placeholder: t('ex: SIRENE'),
|
|
578
631
|
results: dataservicesResults,
|
|
579
632
|
status: dataservicesStatus,
|
|
580
633
|
},
|
|
581
634
|
reuses: {
|
|
582
635
|
icon: RiLineChartLine,
|
|
583
636
|
name: t('Réutilisations'),
|
|
637
|
+
placeholder: t('Rechercher une réutilisation de données'),
|
|
584
638
|
results: reusesResults,
|
|
585
639
|
status: reusesStatus,
|
|
586
640
|
},
|
|
641
|
+
organizations: {
|
|
642
|
+
icon: RiBuilding2Line,
|
|
643
|
+
name: t('Organisations'),
|
|
644
|
+
placeholder: t('Rechercher une organisation'),
|
|
645
|
+
results: organizationsResults,
|
|
646
|
+
status: organizationsStatus,
|
|
647
|
+
},
|
|
587
648
|
} as const
|
|
588
649
|
|
|
589
650
|
const searchResults = computed(() => typesMeta[currentType.value].results.value)
|
|
590
651
|
const searchResultsStatus = computed(() => typesMeta[currentType.value].status.value)
|
|
591
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
|
+
|
|
592
688
|
// Facets for filters
|
|
593
689
|
const currentFacets = computed(() => searchResults.value?.facets)
|
|
594
690
|
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
/>
|
|
14
14
|
<PopoverButton
|
|
15
15
|
v-bind="buttonProps"
|
|
16
|
-
class="
|
|
17
|
-
|
|
16
|
+
:class="[
|
|
17
|
+
buttonClass ?? 'border-transparent -outline-offset-2 inline-flex items-center justify-center hover:bg-gray-some',
|
|
18
|
+
{ 'w-8 h-8 rounded-full bg-transparent': styledButton && !buttonClass },
|
|
19
|
+
]"
|
|
18
20
|
>
|
|
19
21
|
<slot>
|
|
20
22
|
<RiInformationLine
|
|
@@ -57,6 +59,7 @@ import ValueWatcher from './ValueWatcher.vue'
|
|
|
57
59
|
|
|
58
60
|
withDefaults(defineProps<{
|
|
59
61
|
buttonProps?: object
|
|
62
|
+
buttonClass?: string
|
|
60
63
|
noMargin?: boolean
|
|
61
64
|
styledButton?: boolean
|
|
62
65
|
}>(), {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
<div
|
|
32
32
|
v-if="topic.organization || topic.owner"
|
|
33
|
-
class="text-sm
|
|
33
|
+
class="text-sm flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
|
|
34
34
|
>
|
|
35
35
|
<ObjectCardOwner
|
|
36
36
|
:organization="topic.organization"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useComponentsConfig } from '../config'
|
|
2
|
+
import type { Resource } from '../types/resources'
|
|
3
|
+
|
|
4
|
+
export const useHasTabularData = () => {
|
|
5
|
+
const config = useComponentsConfig()
|
|
6
|
+
|
|
7
|
+
return (resource: Resource) => {
|
|
8
|
+
return (
|
|
9
|
+
config.tabularApiUrl
|
|
10
|
+
&& resource.extras['analysis:parsing:parsing_table']
|
|
11
|
+
&& !resource.extras['analysis:parsing:error']
|
|
12
|
+
&& (config.tabularAllowRemote || resource.filetype === 'file')
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
2
|
import { useComponentsConfig } from '../config'
|
|
3
3
|
import { useTranslation } from './useTranslation'
|
|
4
|
+
import { useHasTabularData } from './useHasTabularData'
|
|
4
5
|
import { detectOgcService } from '../functions/resources'
|
|
5
6
|
import { isOrganizationCertified } from '../functions/organizations'
|
|
6
|
-
import type { Resource } from '../types/resources'
|
|
7
|
+
import type { Resource, WfsMetadata } from '../types/resources'
|
|
7
8
|
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
9
|
+
import { getWfsExportFormats } from '../functions/resourceCapabilities'
|
|
8
10
|
|
|
9
11
|
const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
|
|
10
12
|
const URL_FORMATS = ['url', 'doi', 'www:link', 'www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
|
|
@@ -15,6 +17,7 @@ export function useResourceCapabilities(
|
|
|
15
17
|
) {
|
|
16
18
|
const config = useComponentsConfig()
|
|
17
19
|
const { t } = useTranslation()
|
|
20
|
+
const checkTabularData = useHasTabularData()
|
|
18
21
|
|
|
19
22
|
const hasPreview = computed(() => {
|
|
20
23
|
const format = toValue(resource).format?.toLowerCase()
|
|
@@ -23,10 +26,7 @@ export function useResourceCapabilities(
|
|
|
23
26
|
|
|
24
27
|
const hasTabularData = computed(() => {
|
|
25
28
|
const r = toValue(resource)
|
|
26
|
-
return
|
|
27
|
-
&& r.extras['analysis:parsing:parsing_table']
|
|
28
|
-
&& !r.extras['analysis:parsing:error']
|
|
29
|
-
&& (config.tabularAllowRemote || r.filetype === 'file')
|
|
29
|
+
return checkTabularData(r)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
const hasPmtiles = computed(() => {
|
|
@@ -68,6 +68,17 @@ export function useResourceCapabilities(
|
|
|
68
68
|
return formats
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
const wfsFormats = computed(() => {
|
|
72
|
+
return getWfsExportFormats(toValue(resource))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const defaultWfsProjection = computed<string | null>(() => {
|
|
76
|
+
const r = toValue(resource)
|
|
77
|
+
const wfsMetadata = r.extras['analysis:parsing:ogc_metadata'] as WfsMetadata | null
|
|
78
|
+
if (!wfsMetadata || wfsMetadata.format !== `wfs`) return null
|
|
79
|
+
return wfsMetadata?.detected_layer?.default_crs ?? null
|
|
80
|
+
})
|
|
81
|
+
|
|
71
82
|
const isResourceUrl = computed(() => URL_FORMATS.includes(toValue(resource).format))
|
|
72
83
|
|
|
73
84
|
const tabsOptions = computed(() => {
|
|
@@ -112,6 +123,8 @@ export function useResourceCapabilities(
|
|
|
112
123
|
ogcService,
|
|
113
124
|
ogcWms,
|
|
114
125
|
generatedFormats,
|
|
126
|
+
wfsFormats,
|
|
127
|
+
defaultWfsProjection,
|
|
115
128
|
isResourceUrl,
|
|
116
129
|
tabsOptions,
|
|
117
130
|
}
|
|
@@ -21,7 +21,8 @@ function detectLanguage(): string {
|
|
|
21
21
|
const acceptLanguage = header
|
|
22
22
|
if (acceptLanguage) {
|
|
23
23
|
const primaryLang = acceptLanguage.split(';')[0]!.split(',')[0]!.split('-')[0]!.toLowerCase()
|
|
24
|
-
|
|
24
|
+
// Ignore wildcard * language, that should fallback to client side detection or default language
|
|
25
|
+
if (primaryLang !== '*') return primaryLang
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
catch {
|
package/src/functions/api.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import { ref, toValue, watchEffect, onMounted, type ComputedRef, type Ref } from 'vue'
|
|
1
|
+
import { ref, toValue, watchEffect, onMounted, type ComputedRef, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
2
2
|
import { ofetch } from 'ofetch'
|
|
3
3
|
import { useComponentsConfig } from '../config'
|
|
4
4
|
import { useTranslation } from '../composables/useTranslation'
|
|
5
5
|
import type { AsyncData, AsyncDataRequestStatus, UseFetchOptions } from './api.types'
|
|
6
6
|
|
|
7
|
+
function deepToValue(obj: MaybeRefOrGetter<Record<string, unknown> | undefined>): Record<string, unknown> | undefined {
|
|
8
|
+
const val = toValue(obj)
|
|
9
|
+
if (!val || typeof val !== 'object') return val
|
|
10
|
+
return Object.fromEntries(
|
|
11
|
+
Object.entries(val).map(([k, v]) => [k, toValue(v as MaybeRefOrGetter<unknown>)]),
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export async function useFetch<DataT, ErrorT = never>(
|
|
8
16
|
url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
|
|
9
17
|
options?: UseFetchOptions<DataT>,
|
|
@@ -23,8 +31,8 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
23
31
|
const execute = async () => {
|
|
24
32
|
const urlValue = toValue(url)
|
|
25
33
|
if (!urlValue) return
|
|
26
|
-
const params =
|
|
27
|
-
const query =
|
|
34
|
+
const params = deepToValue(options?.params)
|
|
35
|
+
const query = deepToValue(options?.query)
|
|
28
36
|
status.value = 'pending'
|
|
29
37
|
try {
|
|
30
38
|
data.value = await ofetch<DataT | null>(urlValue, {
|
|
@@ -35,5 +35,5 @@ export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
|
|
|
35
35
|
|
|
36
36
|
export type UseFetchFunction = (<DataT, ErrorT>(
|
|
37
37
|
url: string | Request | Ref<string | Request> | ComputedRef<string | null> | (() => string | Request),
|
|
38
|
-
options?: UseFetchOptions<DataT
|
|
38
|
+
options?: UseFetchOptions<DataT>,
|
|
39
39
|
) => Promise<AsyncData<DataT, ErrorT>>)
|