@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,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav :aria-labelledby="titleId">
|
|
3
|
+
<button
|
|
4
|
+
class="flex w-[calc(100%+2rem)] items-center justify-between -mx-4 px-4 py-3 font-bold md:hidden"
|
|
5
|
+
:aria-expanded="open"
|
|
6
|
+
@click="open = !open"
|
|
7
|
+
>
|
|
8
|
+
{{ buttonText }}
|
|
9
|
+
<RiArrowDownSLine
|
|
10
|
+
class="size-4 transition-transform"
|
|
11
|
+
:class="{ 'rotate-180': open }"
|
|
12
|
+
/>
|
|
13
|
+
</button>
|
|
14
|
+
<div v-if="open || !isMobile">
|
|
15
|
+
<p
|
|
16
|
+
:id="titleId"
|
|
17
|
+
class="text-sm font-bold leading-tight mb-6 hidden md:block"
|
|
18
|
+
>
|
|
19
|
+
<slot name="title" />
|
|
20
|
+
</p>
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
</nav>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import { ref, useId } from 'vue'
|
|
28
|
+
import { RiArrowDownSLine } from '@remixicon/vue'
|
|
29
|
+
import { useMediaQuery } from '@vueuse/core'
|
|
30
|
+
|
|
31
|
+
defineProps<{
|
|
32
|
+
buttonText: string
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const titleId = useId()
|
|
36
|
+
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
37
|
+
const open = ref(false)
|
|
38
|
+
</script>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div v-if="size === 'sm'">
|
|
3
|
-
<
|
|
3
|
+
<p class="text-sm font-bold m-0">
|
|
4
4
|
{{ title }}
|
|
5
|
-
</
|
|
5
|
+
</p>
|
|
6
6
|
<div class="flex flex-wrap items-center">
|
|
7
7
|
<ContentLoader
|
|
8
8
|
v-if="summary === null"
|
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
'text-gray-medium': !changesThisYear && !summary,
|
|
83
83
|
}"
|
|
84
84
|
>
|
|
85
|
-
<
|
|
85
|
+
<p class="text-sm m-0">
|
|
86
86
|
{{ title }}
|
|
87
|
-
</
|
|
87
|
+
</p>
|
|
88
88
|
<div class="flex flex-wrap items-center">
|
|
89
89
|
<ContentLoader
|
|
90
90
|
v-if="summary === null"
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
</div>
|
|
141
141
|
<p
|
|
142
142
|
v-if="lastValue && lastMonth"
|
|
143
|
-
class="mt-2 font-normal
|
|
143
|
+
class="mt-2 font-normal normal-case fr-badge fr-badge--no-icon fr-badge--success"
|
|
144
144
|
>
|
|
145
145
|
<strong class="mr-1">
|
|
146
146
|
+ {{ summarize(lastValue, 2) }}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
class="inline-flex items-center space-x-1 text-xs rounded-xl px-2 py-0.5"
|
|
4
|
+
:class="classes"
|
|
5
|
+
>
|
|
6
|
+
<component
|
|
7
|
+
:is="icon"
|
|
8
|
+
v-if="icon"
|
|
9
|
+
class="size-3"
|
|
10
|
+
/>
|
|
11
|
+
<span class="-mt-px">
|
|
12
|
+
<slot />
|
|
13
|
+
</span>
|
|
14
|
+
</span>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { computed, type Component } from 'vue'
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
icon?: Component
|
|
22
|
+
type: 'secondary'
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const classes = computed(() => {
|
|
26
|
+
return {
|
|
27
|
+
secondary: 'text-gray-title bg-gray-lower',
|
|
28
|
+
}[props.type]
|
|
29
|
+
})
|
|
30
|
+
</script>
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
:class="{ 'w-8 h-8 rounded-full bg-transparent': styledButton }"
|
|
18
18
|
>
|
|
19
19
|
<slot>
|
|
20
|
-
<RiInformationLine
|
|
20
|
+
<RiInformationLine
|
|
21
|
+
class="size-5"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
/>
|
|
21
24
|
</slot>
|
|
22
25
|
</PopoverButton>
|
|
23
26
|
|
|
@@ -26,9 +29,10 @@
|
|
|
26
29
|
<PopoverPanel
|
|
27
30
|
v-show="open"
|
|
28
31
|
ref="panel"
|
|
29
|
-
class="
|
|
32
|
+
class="drop-shadow bg-white rounded-sm w-96 absolute z-[800]"
|
|
30
33
|
:class="{
|
|
31
34
|
'p-0': noMargin,
|
|
35
|
+
'p-3': !noMargin,
|
|
32
36
|
}"
|
|
33
37
|
:style="floatingStyles"
|
|
34
38
|
static
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ObjectCard>
|
|
3
|
+
<template
|
|
4
|
+
v-if="showLogo"
|
|
5
|
+
#media
|
|
6
|
+
>
|
|
7
|
+
<OrganizationLogo
|
|
8
|
+
v-if="topic.organization"
|
|
9
|
+
:organization="topic.organization"
|
|
10
|
+
size-class="size-12"
|
|
11
|
+
/>
|
|
12
|
+
<Avatar
|
|
13
|
+
v-else-if="topic.owner"
|
|
14
|
+
:user="topic.owner"
|
|
15
|
+
:size="48"
|
|
16
|
+
/>
|
|
17
|
+
<Placeholder
|
|
18
|
+
v-else
|
|
19
|
+
type="Topic"
|
|
20
|
+
class="size-12"
|
|
21
|
+
/>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<ObjectCardHeader
|
|
25
|
+
:icon="RiBookShelfLine"
|
|
26
|
+
:url="topicUrl || topic.page || '#'"
|
|
27
|
+
>
|
|
28
|
+
{{ topic.name }}
|
|
29
|
+
</ObjectCardHeader>
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
v-if="topic.organization || topic.owner"
|
|
33
|
+
class="text-sm m-0 flex flex-wrap md:flex-nowrap gap-y-1 items-center truncate"
|
|
34
|
+
>
|
|
35
|
+
<ObjectCardOwner
|
|
36
|
+
:organization="topic.organization"
|
|
37
|
+
:owner="topic.owner"
|
|
38
|
+
/>
|
|
39
|
+
<RiSubtractLine
|
|
40
|
+
v-if="(topic.organization || topic.owner) && topic.last_modified"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
class="hidden md:block size-4 flex-none fill-gray-medium"
|
|
43
|
+
/>
|
|
44
|
+
<div
|
|
45
|
+
v-if="topic.last_modified"
|
|
46
|
+
class="w-full md:w-auto text-gray-medium whitespace-nowrap"
|
|
47
|
+
>
|
|
48
|
+
{{ t('Mis à jour {date}', { date: formatDate(topic.last_modified) }) }}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<ObjectCardShortDescription :text="topic.description" />
|
|
53
|
+
|
|
54
|
+
<div
|
|
55
|
+
v-if="showStats && (topic.nb_datasets || topic.nb_dataservices || topic.nb_reuses)"
|
|
56
|
+
class="flex items-center gap-4 mt-2 text-xs text-gray-medium"
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
v-if="topic.nb_datasets"
|
|
60
|
+
class="flex items-center gap-1"
|
|
61
|
+
>
|
|
62
|
+
<RiDatabase2Line
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
class="size-4"
|
|
65
|
+
/>
|
|
66
|
+
<span>{{ topic.nb_datasets }}</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div
|
|
70
|
+
v-if="topic.nb_dataservices"
|
|
71
|
+
class="flex items-center gap-1"
|
|
72
|
+
>
|
|
73
|
+
<RiTerminalLine
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
class="size-4"
|
|
76
|
+
/>
|
|
77
|
+
<span>{{ topic.nb_dataservices }}</span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div
|
|
81
|
+
v-if="topic.nb_reuses"
|
|
82
|
+
class="flex items-center gap-1"
|
|
83
|
+
>
|
|
84
|
+
<RiLineChartLine
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
class="size-4"
|
|
87
|
+
/>
|
|
88
|
+
<span>{{ topic.nb_reuses }}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<slot />
|
|
93
|
+
</ObjectCard>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script setup lang="ts">
|
|
97
|
+
import { RiBookShelfLine, RiDatabase2Line, RiLineChartLine, RiSubtractLine, RiTerminalLine } from '@remixicon/vue'
|
|
98
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
99
|
+
import type { TopicV2 } from '../types/topics'
|
|
100
|
+
import { useFormatDate } from '../functions/dates'
|
|
101
|
+
import { useTranslation } from '../composables/useTranslation'
|
|
102
|
+
import ObjectCardHeader from './ObjectCardHeader.vue'
|
|
103
|
+
import ObjectCardOwner from './ObjectCardOwner.vue'
|
|
104
|
+
import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
|
|
105
|
+
import OrganizationLogo from './OrganizationLogo.vue'
|
|
106
|
+
import Avatar from './Avatar.vue'
|
|
107
|
+
import Placeholder from './Placeholder.vue'
|
|
108
|
+
import ObjectCard from './ObjectCard.vue'
|
|
109
|
+
|
|
110
|
+
type TopicWithStats = TopicV2 & {
|
|
111
|
+
nb_datasets?: number
|
|
112
|
+
nb_dataservices?: number
|
|
113
|
+
nb_reuses?: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
withDefaults(defineProps<{
|
|
117
|
+
topic: TopicWithStats
|
|
118
|
+
topicUrl?: RouteLocationRaw
|
|
119
|
+
showLogo?: boolean
|
|
120
|
+
showStats?: boolean
|
|
121
|
+
}>(), {
|
|
122
|
+
showLogo: true,
|
|
123
|
+
showStats: true,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const { t } = useTranslation()
|
|
127
|
+
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
128
|
+
|
|
129
|
+
const formatDate = (dateString: string) => {
|
|
130
|
+
return formatRelativeIfRecentDate(dateString, {
|
|
131
|
+
dateStyle: 'long',
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ref, readonly, watch, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Like VueUse's refDebounced but with a flush() method to force immediate sync.
|
|
5
|
+
* Useful when resetting filters - no need to wait for debounce delay.
|
|
6
|
+
*/
|
|
7
|
+
export function useDebouncedRef<T>(source: Ref<T>, delay: number) {
|
|
8
|
+
const debounced = ref(source.value) as Ref<T>
|
|
9
|
+
const debouncedReadonly = readonly(debounced)
|
|
10
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
11
|
+
|
|
12
|
+
watch(source, () => {
|
|
13
|
+
if (timeoutId) {
|
|
14
|
+
clearTimeout(timeoutId)
|
|
15
|
+
}
|
|
16
|
+
timeoutId = setTimeout(() => {
|
|
17
|
+
debounced.value = source.value
|
|
18
|
+
timeoutId = null
|
|
19
|
+
}, delay)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function flush() {
|
|
23
|
+
if (timeoutId) {
|
|
24
|
+
clearTimeout(timeoutId)
|
|
25
|
+
timeoutId = null
|
|
26
|
+
}
|
|
27
|
+
debounced.value = source.value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { debounced: debouncedReadonly, flush }
|
|
31
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useComponentsConfig } from '../config'
|
|
2
|
-
import { getOrganizationMetrics, getDatasetMetrics, getDataserviceMetrics, getReuseMetrics } from '../functions/metrics'
|
|
2
|
+
import { getOrganizationMetrics, getDatasetMetrics, getDataserviceMetrics, getReuseMetrics, createDatasetsForOrganizationMetricsUrl } from '../functions/metrics'
|
|
3
3
|
|
|
4
4
|
export function useMetrics() {
|
|
5
5
|
const config = useComponentsConfig()
|
|
6
6
|
|
|
7
|
-
if (!config.metricsApiUrl) {
|
|
8
|
-
throw new Error('metricsApiUrl
|
|
7
|
+
if (!config.metricsApiUrl || !config.apiBase) {
|
|
8
|
+
throw new Error('metricsApiUrl and apiBase must be configured in @datagouv/components-next')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
return {
|
|
@@ -13,5 +13,6 @@ export function useMetrics() {
|
|
|
13
13
|
getDatasetMetrics: (datasetId: string) => getDatasetMetrics(datasetId, config.metricsApiUrl!),
|
|
14
14
|
getDataserviceMetrics: (dataserviceId: string) => getDataserviceMetrics(dataserviceId, config.metricsApiUrl!),
|
|
15
15
|
getReuseMetrics: (reuseId: string) => getReuseMetrics(reuseId, config.metricsApiUrl!),
|
|
16
|
+
createDatasetsForOrganizationMetricsUrl: (organizationId: string) => createDatasetsForOrganizationMetricsUrl(organizationId, config.metricsApiUrl!, config.apiBase),
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import { useComponentsConfig } from '../config'
|
|
3
|
+
import { useTranslation } from './useTranslation'
|
|
4
|
+
import { detectOgcService } from '../functions/resources'
|
|
5
|
+
import { isOrganizationCertified } from '../functions/organizations'
|
|
6
|
+
import type { Resource } from '../types/resources'
|
|
7
|
+
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
8
|
+
|
|
9
|
+
const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
|
|
10
|
+
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']
|
|
11
|
+
|
|
12
|
+
export function useResourceCapabilities(
|
|
13
|
+
resource: MaybeRefOrGetter<Resource>,
|
|
14
|
+
dataset: MaybeRefOrGetter<Dataset | DatasetV2>,
|
|
15
|
+
) {
|
|
16
|
+
const config = useComponentsConfig()
|
|
17
|
+
const { t } = useTranslation()
|
|
18
|
+
|
|
19
|
+
const hasPreview = computed(() => {
|
|
20
|
+
const format = toValue(resource).format?.toLowerCase()
|
|
21
|
+
return format === 'json' || format === 'pdf' || format === 'xml'
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const hasTabularData = computed(() => {
|
|
25
|
+
const r = toValue(resource)
|
|
26
|
+
return config.tabularApiUrl
|
|
27
|
+
&& r.extras['analysis:parsing:parsing_table']
|
|
28
|
+
&& !r.extras['analysis:parsing:error']
|
|
29
|
+
&& (config.tabularAllowRemote || r.filetype === 'file')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const hasPmtiles = computed(() => {
|
|
33
|
+
const r = toValue(resource)
|
|
34
|
+
return r.extras['analysis:parsing:pmtiles_url'] || r.format === 'pmtiles'
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const hasDatafairPreview = computed(() => {
|
|
38
|
+
const d = toValue(dataset)
|
|
39
|
+
const r = toValue(resource)
|
|
40
|
+
return isOrganizationCertified(d.organization) && r.extras['datafairEmbed']
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const hasOpenAPIPreview = computed(() => {
|
|
44
|
+
const d = toValue(dataset)
|
|
45
|
+
const r = toValue(resource)
|
|
46
|
+
return isOrganizationCertified(d.organization) && r.extras['apidocUrl']
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const ogcService = computed(() => detectOgcService(toValue(resource)))
|
|
50
|
+
const ogcWms = computed(() => ogcService.value === 'wms')
|
|
51
|
+
|
|
52
|
+
const generatedFormats = computed(() => {
|
|
53
|
+
const r = toValue(resource)
|
|
54
|
+
const formats = GENERATED_FORMATS
|
|
55
|
+
.filter(format => `analysis:parsing:${format}_url` in r.extras)
|
|
56
|
+
.map(format => ({
|
|
57
|
+
url: r.extras[`analysis:parsing:${format}_url`] as string,
|
|
58
|
+
size: r.extras[`analysis:parsing:${format}_size`] as number | undefined,
|
|
59
|
+
format,
|
|
60
|
+
}))
|
|
61
|
+
if ('analysis:parsing:parsing_table' in r.extras) {
|
|
62
|
+
formats.push({
|
|
63
|
+
url: `${config.tabularApiUrl}/api/resources/${r.id}/data/json/`,
|
|
64
|
+
size: undefined,
|
|
65
|
+
format: 'json',
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
return formats
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const isResourceUrl = computed(() => URL_FORMATS.includes(toValue(resource).format))
|
|
72
|
+
|
|
73
|
+
const tabsOptions = computed(() => {
|
|
74
|
+
const r = toValue(resource)
|
|
75
|
+
const options = []
|
|
76
|
+
|
|
77
|
+
if (hasTabularData.value) {
|
|
78
|
+
options.push({ key: 'data', label: t('Données') })
|
|
79
|
+
}
|
|
80
|
+
else if (hasPreview.value || hasDatafairPreview.value || hasOpenAPIPreview.value) {
|
|
81
|
+
options.push({ key: 'data', label: t('Aperçu') })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hasTabularData.value) {
|
|
85
|
+
options.push({ key: 'data-structure', label: t('Structure des données') })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (hasPmtiles.value || ogcWms.value) {
|
|
89
|
+
options.push({ key: 'map', label: t('Carte') })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (r.description) {
|
|
93
|
+
options.push({ key: 'description', label: t('Description') })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
options.push({ key: 'metadata', label: t('Métadonnées') })
|
|
97
|
+
options.push({ key: 'downloads', label: t('Téléchargements') })
|
|
98
|
+
|
|
99
|
+
if (hasTabularData.value) {
|
|
100
|
+
options.push({ key: 'swagger', label: t('Swagger') })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return options
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
hasPreview,
|
|
108
|
+
hasTabularData,
|
|
109
|
+
hasPmtiles,
|
|
110
|
+
hasDatafairPreview,
|
|
111
|
+
hasOpenAPIPreview,
|
|
112
|
+
ogcService,
|
|
113
|
+
ogcWms,
|
|
114
|
+
generatedFormats,
|
|
115
|
+
isResourceUrl,
|
|
116
|
+
tabsOptions,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useRouteQuery } from '@vueuse/router'
|
|
2
|
+
|
|
3
|
+
export function useRouteQueryBoolean(name: string) {
|
|
4
|
+
return useRouteQuery<string | undefined, boolean | undefined>(name, undefined, {
|
|
5
|
+
transform: {
|
|
6
|
+
get: v => v === 'true' ? true : v === 'false' ? false : undefined,
|
|
7
|
+
set: v => v === undefined ? undefined : String(v),
|
|
8
|
+
},
|
|
9
|
+
})
|
|
10
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ref, watch, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Syncs a model (full object) with an id (identifier only) bidirectionally.
|
|
5
|
+
* Used in *Select components where the parent binds via v-model:id but internally we need the full object.
|
|
6
|
+
*/
|
|
7
|
+
export function useSelectModelSync<T>(options: {
|
|
8
|
+
model: Ref<T | null>
|
|
9
|
+
id: Ref<string | undefined>
|
|
10
|
+
items: Ref<T[] | null | undefined>
|
|
11
|
+
getId: (item: T) => string
|
|
12
|
+
}) {
|
|
13
|
+
const { model, id, items, getId } = options
|
|
14
|
+
|
|
15
|
+
watch(model, (newModel) => {
|
|
16
|
+
id.value = newModel ? getId(newModel) : undefined
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
watch([id, items], ([newId]) => {
|
|
20
|
+
if (!newId) {
|
|
21
|
+
model.value = null
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (model.value && getId(model.value) === newId) return
|
|
25
|
+
model.value = items.value?.find(item => getId(item) === newId) ?? null
|
|
26
|
+
}, { immediate: true })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Simplified sync for string-based selects where model and id are both strings.
|
|
31
|
+
*/
|
|
32
|
+
export function useStringSelectSync(options: {
|
|
33
|
+
model: Ref<string | null>
|
|
34
|
+
id: Ref<string | undefined>
|
|
35
|
+
}) {
|
|
36
|
+
const { model, id } = options
|
|
37
|
+
|
|
38
|
+
watch(model, (newModel) => {
|
|
39
|
+
id.value = newModel ?? undefined
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
watch(id, (newId) => {
|
|
43
|
+
if (!newId) {
|
|
44
|
+
model.value = null
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
if (model.value === newId) return
|
|
48
|
+
model.value = newId
|
|
49
|
+
}, { immediate: true })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sync with async fetch when id changes and model needs to be fetched.
|
|
54
|
+
* Returns a `fetching` ref for loading state.
|
|
55
|
+
*/
|
|
56
|
+
export function useAsyncSelectModelSync<T>(options: {
|
|
57
|
+
model: Ref<T | null>
|
|
58
|
+
id: Ref<string | undefined>
|
|
59
|
+
getId: (item: T) => string
|
|
60
|
+
fetchById: (id: string) => Promise<T | null>
|
|
61
|
+
}) {
|
|
62
|
+
const { model, id, getId, fetchById } = options
|
|
63
|
+
const fetching = ref(false)
|
|
64
|
+
|
|
65
|
+
watch(model, (newModel) => {
|
|
66
|
+
id.value = newModel ? getId(newModel) : undefined
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
watch(id, async (newId) => {
|
|
70
|
+
if (!newId) {
|
|
71
|
+
model.value = null
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (model.value && getId(model.value) === newId) return
|
|
75
|
+
|
|
76
|
+
fetching.value = true
|
|
77
|
+
try {
|
|
78
|
+
model.value = await fetchById(newId)
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
model.value = null
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
fetching.value = false
|
|
85
|
+
}
|
|
86
|
+
}, { immediate: true })
|
|
87
|
+
|
|
88
|
+
return { fetching }
|
|
89
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ref, watch, type Ref } from 'vue'
|
|
2
|
+
import type { SearchTypeConfig } from '../types/search'
|
|
3
|
+
|
|
4
|
+
type FilterRefs = Record<string, Ref<unknown>>
|
|
5
|
+
|
|
6
|
+
interface StableQueryParamsOptions {
|
|
7
|
+
typeConfig: SearchTypeConfig | undefined
|
|
8
|
+
allFilters: FilterRefs
|
|
9
|
+
q: Ref<string>
|
|
10
|
+
sort: Ref<string | undefined>
|
|
11
|
+
page: Ref<number>
|
|
12
|
+
pageSize: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a stable ref for query params that only updates when content actually changes.
|
|
17
|
+
* Applies hiddenFilters first, then user filters (which can override hiddenFilters).
|
|
18
|
+
*/
|
|
19
|
+
export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
20
|
+
const { typeConfig, allFilters, q, sort, page, pageSize } = options
|
|
21
|
+
const stableParams = ref<Record<string, unknown>>({})
|
|
22
|
+
|
|
23
|
+
const buildParams = () => {
|
|
24
|
+
const params: Record<string, unknown> = {}
|
|
25
|
+
|
|
26
|
+
// 1. Apply hiddenFilters first (can be overridden by user filters)
|
|
27
|
+
if (typeConfig?.hiddenFilters) {
|
|
28
|
+
for (const hf of typeConfig.hiddenFilters) {
|
|
29
|
+
if (hf) {
|
|
30
|
+
params[hf.key as string] = hf.value
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Get enabled filters for this type
|
|
36
|
+
const enabledFilters = [
|
|
37
|
+
...(typeConfig?.basicFilters ?? []),
|
|
38
|
+
...(typeConfig?.advancedFilters ?? []),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
// 3. Apply user filter values (only enabled ones)
|
|
42
|
+
// Skip undefined/null/empty values so they're not sent to the API
|
|
43
|
+
for (const filterName of enabledFilters) {
|
|
44
|
+
const filterRef = allFilters[filterName as string]
|
|
45
|
+
if (filterRef) {
|
|
46
|
+
const value = filterRef.value
|
|
47
|
+
if (value !== undefined && value !== '' && value !== null) {
|
|
48
|
+
params[filterName as string] = value
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 4. Always include q, sort (if valid for this type), page, page_size
|
|
54
|
+
if (q.value) {
|
|
55
|
+
params.q = q.value
|
|
56
|
+
}
|
|
57
|
+
if (sort.value) {
|
|
58
|
+
const validSortValues = typeConfig?.sortOptions?.map(o => o.value as string) ?? []
|
|
59
|
+
if (validSortValues.includes(sort.value)) {
|
|
60
|
+
params.sort = sort.value
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
params.page = page.value
|
|
64
|
+
params.page_size = pageSize
|
|
65
|
+
|
|
66
|
+
return params
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Watch all dependencies and update only if content changed
|
|
70
|
+
watch(
|
|
71
|
+
[q, sort, page, ...Object.values(allFilters)],
|
|
72
|
+
() => {
|
|
73
|
+
const newParams = buildParams()
|
|
74
|
+
// JSON.stringify comparison is safe here because buildParams() builds the object deterministically
|
|
75
|
+
// (keys are always added in the same order), avoiding the key ordering edge case.
|
|
76
|
+
if (JSON.stringify(newParams) !== JSON.stringify(stableParams.value)) {
|
|
77
|
+
stableParams.value = newParams
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{ immediate: true },
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return stableParams
|
|
84
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type PluginConfig = {
|
|
|
15
15
|
metricsApiUrl?: string
|
|
16
16
|
schemaValidataUrl?: string
|
|
17
17
|
schemaDocumentationUrl?: string
|
|
18
|
+
schemasSiteUrl?: string
|
|
19
|
+
schemasSiteName?: string
|
|
18
20
|
tabularApiUrl?: string
|
|
19
21
|
tabularApiPageSize?: number
|
|
20
22
|
tabularAllowRemote?: boolean
|
|
@@ -27,6 +29,8 @@ export type PluginConfig = {
|
|
|
27
29
|
textClamp?: string | Component | null
|
|
28
30
|
appLink?: Component | null
|
|
29
31
|
clientOnly?: Component | null
|
|
32
|
+
searchDebounce?: number
|
|
33
|
+
forumUrl?: string
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export const configKey = Symbol() as InjectionKey<PluginConfig>
|