@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,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="fr-search-bar fr-search-bar--lg w-full">
|
|
3
|
+
<label
|
|
4
|
+
class="sr-only"
|
|
5
|
+
:for="id"
|
|
6
|
+
>
|
|
7
|
+
{{ t('Recherche') }}
|
|
8
|
+
</label>
|
|
9
|
+
<input
|
|
10
|
+
:id="id"
|
|
11
|
+
ref="inputRef"
|
|
12
|
+
v-model="q"
|
|
13
|
+
type="search"
|
|
14
|
+
name="q"
|
|
15
|
+
class="input max-h-12 m-0 rounded-tl shadow-input-blue"
|
|
16
|
+
:aria-label="placeholder || t('Rechercher...')"
|
|
17
|
+
:placeholder="placeholder || t('Rechercher...')"
|
|
18
|
+
>
|
|
19
|
+
<BrandedButton
|
|
20
|
+
class="rounded-l-none rounded-br-none rounded-tr min-h-12"
|
|
21
|
+
size="lg"
|
|
22
|
+
color="primary"
|
|
23
|
+
:icon="RiSearchLine"
|
|
24
|
+
icon-only-on-mobile
|
|
25
|
+
type="submit"
|
|
26
|
+
>
|
|
27
|
+
{{ t('Recherche') }}
|
|
28
|
+
</BrandedButton>
|
|
29
|
+
</section>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup lang="ts">
|
|
33
|
+
import { nextTick, onMounted, useId, useTemplateRef } from 'vue'
|
|
34
|
+
import { RiSearchLine } from '@remixicon/vue'
|
|
35
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
36
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
37
|
+
|
|
38
|
+
const q = defineModel<string>({ required: true })
|
|
39
|
+
|
|
40
|
+
withDefaults(defineProps<{
|
|
41
|
+
placeholder?: string
|
|
42
|
+
autoFocus?: boolean
|
|
43
|
+
}>(), {
|
|
44
|
+
autoFocus: true,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const { t } = useTranslation()
|
|
48
|
+
|
|
49
|
+
const id = useId()
|
|
50
|
+
|
|
51
|
+
const input = useTemplateRef<HTMLInputElement>('inputRef')
|
|
52
|
+
|
|
53
|
+
const focus = () => {
|
|
54
|
+
input.value?.focus({
|
|
55
|
+
preventScroll: true,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onMounted(async () => {
|
|
60
|
+
await nextTick()
|
|
61
|
+
focus()
|
|
62
|
+
})
|
|
63
|
+
</script>
|
|
@@ -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>
|
|
@@ -13,11 +13,16 @@
|
|
|
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
|
-
<RiInformationLine
|
|
22
|
+
<RiInformationLine
|
|
23
|
+
class="size-5"
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
/>
|
|
21
26
|
</slot>
|
|
22
27
|
</PopoverButton>
|
|
23
28
|
|
|
@@ -26,9 +31,10 @@
|
|
|
26
31
|
<PopoverPanel
|
|
27
32
|
v-show="open"
|
|
28
33
|
ref="panel"
|
|
29
|
-
class="
|
|
34
|
+
class="drop-shadow bg-white rounded-sm w-96 absolute z-[800]"
|
|
30
35
|
:class="{
|
|
31
36
|
'p-0': noMargin,
|
|
37
|
+
'p-3': !noMargin,
|
|
32
38
|
}"
|
|
33
39
|
:style="floatingStyles"
|
|
34
40
|
static
|
|
@@ -53,6 +59,7 @@ import ValueWatcher from './ValueWatcher.vue'
|
|
|
53
59
|
|
|
54
60
|
withDefaults(defineProps<{
|
|
55
61
|
buttonProps?: object
|
|
62
|
+
buttonClass?: string
|
|
56
63
|
noMargin?: boolean
|
|
57
64
|
styledButton?: boolean
|
|
58
65
|
}>(), {
|
|
@@ -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 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
|
+
}
|
|
@@ -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,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,131 @@
|
|
|
1
|
+
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import { useComponentsConfig } from '../config'
|
|
3
|
+
import { useTranslation } from './useTranslation'
|
|
4
|
+
import { useHasTabularData } from './useHasTabularData'
|
|
5
|
+
import { detectOgcService } from '../functions/resources'
|
|
6
|
+
import { isOrganizationCertified } from '../functions/organizations'
|
|
7
|
+
import type { Resource, WfsMetadata } from '../types/resources'
|
|
8
|
+
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
9
|
+
import { getWfsExportFormats } from '../functions/resourceCapabilities'
|
|
10
|
+
|
|
11
|
+
const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
|
|
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']
|
|
13
|
+
|
|
14
|
+
export function useResourceCapabilities(
|
|
15
|
+
resource: MaybeRefOrGetter<Resource>,
|
|
16
|
+
dataset: MaybeRefOrGetter<Dataset | DatasetV2>,
|
|
17
|
+
) {
|
|
18
|
+
const config = useComponentsConfig()
|
|
19
|
+
const { t } = useTranslation()
|
|
20
|
+
const checkTabularData = useHasTabularData()
|
|
21
|
+
|
|
22
|
+
const hasPreview = computed(() => {
|
|
23
|
+
const format = toValue(resource).format?.toLowerCase()
|
|
24
|
+
return format === 'json' || format === 'pdf' || format === 'xml'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const hasTabularData = computed(() => {
|
|
28
|
+
const r = toValue(resource)
|
|
29
|
+
return checkTabularData(r)
|
|
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 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
|
+
|
|
82
|
+
const isResourceUrl = computed(() => URL_FORMATS.includes(toValue(resource).format))
|
|
83
|
+
|
|
84
|
+
const tabsOptions = computed(() => {
|
|
85
|
+
const r = toValue(resource)
|
|
86
|
+
const options = []
|
|
87
|
+
|
|
88
|
+
if (hasTabularData.value) {
|
|
89
|
+
options.push({ key: 'data', label: t('Données') })
|
|
90
|
+
}
|
|
91
|
+
else if (hasPreview.value || hasDatafairPreview.value || hasOpenAPIPreview.value) {
|
|
92
|
+
options.push({ key: 'data', label: t('Aperçu') })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hasTabularData.value) {
|
|
96
|
+
options.push({ key: 'data-structure', label: t('Structure des données') })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (hasPmtiles.value || ogcWms.value) {
|
|
100
|
+
options.push({ key: 'map', label: t('Carte') })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (r.description) {
|
|
104
|
+
options.push({ key: 'description', label: t('Description') })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
options.push({ key: 'metadata', label: t('Métadonnées') })
|
|
108
|
+
options.push({ key: 'downloads', label: t('Téléchargements') })
|
|
109
|
+
|
|
110
|
+
if (hasTabularData.value) {
|
|
111
|
+
options.push({ key: 'swagger', label: t('Swagger') })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return options
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
hasPreview,
|
|
119
|
+
hasTabularData,
|
|
120
|
+
hasPmtiles,
|
|
121
|
+
hasDatafairPreview,
|
|
122
|
+
hasOpenAPIPreview,
|
|
123
|
+
ogcService,
|
|
124
|
+
ogcWms,
|
|
125
|
+
generatedFormats,
|
|
126
|
+
wfsFormats,
|
|
127
|
+
defaultWfsProjection,
|
|
128
|
+
isResourceUrl,
|
|
129
|
+
tabsOptions,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -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
|
+
}
|