@datagouv/components-next 1.3.0 → 1.4.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/dist/{Control-b-43UT2B.js → Control-B9WQxlpJ.js} +10 -10
- package/dist/{Datafair.client-BrjISlaZ.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-py_AAwJY.js → Event-iXDrp-fy.js} +15 -15
- package/dist/{Image-BHQYZWzH.js → Image-mNMBOofs.js} +22 -22
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-DivFAnbN.js → JsonPreview.client-DSl1kiwC.js} +2 -2
- package/dist/{Map-CtP7leHl.js → Map-BfqKMw0S.js} +165 -165
- package/dist/{MapContainer.client-Cq8tNLZZ.js → MapContainer.client-BSfpk_rT.js} +17 -17
- package/dist/{OSM-gdDSIsDA.js → OSM-CcrUhItr.js} +1 -1
- package/dist/{PdfPreview.client-BNBgLsK2.js → PdfPreview.client-BANFeM6K.js} +438 -427
- package/dist/{Pmtiles.client-CQi5Rw8W.js → Pmtiles.client-DH9ZrsZR.js} +6177 -5967
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hG-FhNqy.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-D6rFonpT.js → Tile-CSpn0Ca8.js} +47 -47
- package/dist/{TileImage-cfrEgz5y.js → TileImage-CpbaEFwd.js} +44 -44
- package/dist/{View-vp7vDb9q.js → View-BYBpt-AU.js} +58 -58
- package/dist/{XmlPreview.client-XCsNvnsZ.js → XmlPreview.client-KCB5mwVf.js} +3 -3
- package/dist/{common-C8efyyuc.js → common-BoeTUnDZ.js} +58 -58
- package/dist/components-next.css +1 -1
- package/dist/components-next.js +199 -172
- package/dist/components.css +1 -1
- package/dist/{index-CXUamc8Z.js → index-DHWzX5vK.js} +4668 -4466
- package/dist/{leaflet-src-BRKv9Ksv.js → leaflet-src-pVQfonBI.js} +1 -1
- package/dist/{main-DKu5SOAl.js → main-BDcYkhOc.js} +41684 -33432
- package/dist/{proj-CsjVeojN.js → proj-DxGbobp7.js} +110 -110
- package/dist/{tilecoord-CPcQJnrF.js → tilecoord-DvX7WWiA.js} +94 -94
- package/dist/{vue3-xml-viewer.common-BIaEIFYG.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +28 -27
- package/src/components/ActivityList/ActivityList.vue +21 -12
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +111 -17
- package/src/components/Chart/ChartViewerWrapper.vue +28 -16
- package/src/components/ChartCard.vue +112 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +23 -6
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +17 -6
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +69 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCard.vue +4 -1
- package/src/components/ObjectCardOwner.vue +13 -1
- package/src/components/Pagination.vue +18 -58
- package/src/components/Placeholder.vue +3 -2
- package/src/components/PostCard.vue +13 -15
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +19 -7
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
- package/src/components/ResourceAccordion/Metadata.vue +21 -5
- package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +27 -18
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +29 -9
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -209
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +136 -183
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +18 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +4 -3
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +12 -8
- package/src/components/Search/SearchInput.vue +1 -1
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +7 -8
- package/src/components/TabularExplorer/TabularCellPopover.vue +19 -3
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +122 -913
- package/src/components/TabularExplorer/TabularFilterContent.vue +33 -25
- package/src/components/TabularExplorer/TabularFilterPopover.vue +7 -7
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +484 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +32 -20
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +15 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +2 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +47 -39
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/charts.ts +32 -1
- package/src/functions/dates.ts +26 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/owned.ts +21 -0
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +83 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +119 -4
- package/src/functions/tabularApi.ts +1 -43
- package/src/main.ts +32 -6
- package/src/types/dataservices.ts +15 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js +0 -61
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppLink
|
|
3
|
+
ref="row"
|
|
4
|
+
v-bind="$attrs"
|
|
5
|
+
:to
|
|
6
|
+
:replace
|
|
7
|
+
:class="selected ? '[&&]:!bg-gray-200' : '[&&]:hover:!bg-gray-100'"
|
|
8
|
+
class="grid h-7 w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-1 rounded px-1 py-1 text-left !bg-none !no-underline"
|
|
9
|
+
@pointerenter="openOnHover"
|
|
10
|
+
@pointerleave="closeTooltip"
|
|
11
|
+
@focus="show = true"
|
|
12
|
+
@blur="closeTooltip"
|
|
13
|
+
>
|
|
14
|
+
<span
|
|
15
|
+
:class="[iconColor, '[&_svg]:fill-current']"
|
|
16
|
+
class="flex size-5 shrink-0 items-center justify-center rounded-[1px]"
|
|
17
|
+
>
|
|
18
|
+
<component
|
|
19
|
+
:is="iconComponent"
|
|
20
|
+
class="size-4"
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
/>
|
|
23
|
+
</span>
|
|
24
|
+
<div class="flex min-w-0 items-baseline gap-0.5 whitespace-nowrap leading-4">
|
|
25
|
+
<span
|
|
26
|
+
class="truncate text-[13px]"
|
|
27
|
+
:class="selected ? 'font-extrabold text-gray-title' : 'font-medium text-gray-medium'"
|
|
28
|
+
>{{ resource.title || t('Fichier sans nom') }}</span>
|
|
29
|
+
<template v-if="humanFilesize">
|
|
30
|
+
<span class="shrink-0 text-[13px] text-gray-medium">·</span>
|
|
31
|
+
<span class="shrink-0 text-[12px] text-gray-medium">{{ humanFilesize }}</span>
|
|
32
|
+
</template>
|
|
33
|
+
</div>
|
|
34
|
+
<!-- Capped and truncated: an `auto` grid track floors at its content width, so an
|
|
35
|
+
unusually long format (`www:link-1.0-http--samples`) would otherwise squeeze
|
|
36
|
+
the title track to nothing and overflow the fixed-height row. -->
|
|
37
|
+
<span
|
|
38
|
+
v-if="resource.format"
|
|
39
|
+
class="max-w-24 truncate rounded bg-gray-lower px-1.5 py-0.5 text-[12px] uppercase leading-4 text-gray-medium"
|
|
40
|
+
:title="resource.format"
|
|
41
|
+
>{{ resource.format }}</span>
|
|
42
|
+
</AppLink>
|
|
43
|
+
|
|
44
|
+
<!-- Hover card: the row truncates the title, so surface the full name plus the
|
|
45
|
+
same metadata line as the viewer header. Placed beside the row (not below) so
|
|
46
|
+
it doesn't hide the sibling rows we're scanning.
|
|
47
|
+
ClientOnly: Vue hydrates a teleport by scanning the target from its first child,
|
|
48
|
+
so an SSR-rendered teleport to `body` breaks as soon as anything else is prepended
|
|
49
|
+
there — and Nuxt emits the `bodyOpen` head tags before it. The card only ever opens
|
|
50
|
+
on hover, so there is nothing to render on the server anyway. -->
|
|
51
|
+
<ClientOnly>
|
|
52
|
+
<Teleport to="body">
|
|
53
|
+
<div
|
|
54
|
+
v-if="show"
|
|
55
|
+
ref="card"
|
|
56
|
+
role="tooltip"
|
|
57
|
+
class="pointer-events-none z-[80] w-max rounded border border-gray-default bg-white p-2 text-left shadow-[0_2px_4px_rgba(0,0,0,0.04),2px_4px_16px_rgba(0,0,0,0.12)]"
|
|
58
|
+
:style="floatingStyles"
|
|
59
|
+
>
|
|
60
|
+
<span class="block whitespace-nowrap text-[13px] font-medium leading-5 text-gray-title">{{ resource.title || t('Fichier sans nom') }}</span>
|
|
61
|
+
<div class="mt-1 flex items-center gap-1 text-[12px] leading-4 text-gray-medium">
|
|
62
|
+
<TranslationT keypath="mis à jour {date}">
|
|
63
|
+
<template #date>
|
|
64
|
+
<FormattedDate
|
|
65
|
+
:date="resource.last_modified"
|
|
66
|
+
format="relative"
|
|
67
|
+
/>
|
|
68
|
+
</template>
|
|
69
|
+
</TranslationT>
|
|
70
|
+
<template v-if="humanFilesize">
|
|
71
|
+
<span>·</span>
|
|
72
|
+
<span>{{ humanFilesize }}</span>
|
|
73
|
+
</template>
|
|
74
|
+
<template v-if="resource.format">
|
|
75
|
+
<span>·</span>
|
|
76
|
+
<span class="rounded bg-gray-lower px-1.5 py-0.5 uppercase leading-4">{{ resource.format }}</span>
|
|
77
|
+
</template>
|
|
78
|
+
<span>·</span>
|
|
79
|
+
<span class="inline-flex items-center gap-0.5">
|
|
80
|
+
<RiDownloadLine
|
|
81
|
+
class="size-3"
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
/>
|
|
84
|
+
{{ summarize(resource.metrics.views) }}
|
|
85
|
+
</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</Teleport>
|
|
89
|
+
</ClientOnly>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<script setup lang="ts">
|
|
93
|
+
import { computed, ref, useTemplateRef } from 'vue'
|
|
94
|
+
import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue'
|
|
95
|
+
import { useEventListener } from '@vueuse/core'
|
|
96
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
97
|
+
import { RiDownloadLine } from '@remixicon/vue'
|
|
98
|
+
import AppLink from './AppLink.vue'
|
|
99
|
+
import ClientOnly from './ClientOnly.vue'
|
|
100
|
+
import File from './Icons/File.vue'
|
|
101
|
+
import FormattedDate from './FormattedDate.vue'
|
|
102
|
+
import TranslationT from './TranslationT.vue'
|
|
103
|
+
import { getResourceFormatIcon, getResourceIconColor, getResourceFilesize } from '../functions/resources'
|
|
104
|
+
import { filesize, summarize } from '../functions/helpers'
|
|
105
|
+
import { useTranslation } from '../composables/useTranslation'
|
|
106
|
+
import type { Resource } from '../types/resources'
|
|
107
|
+
|
|
108
|
+
// The hover card below is a second root node, so Vue drops fallthrough attributes
|
|
109
|
+
// unless we place them ourselves: without this, a listener bound by the parent
|
|
110
|
+
// (the selector's `@click="close()"`) would silently never fire.
|
|
111
|
+
defineOptions({ inheritAttrs: false })
|
|
112
|
+
|
|
113
|
+
// Shared row for a single resource (sidebar + resource selector). Renders as a
|
|
114
|
+
// navigation link so switching resource is a real link — the URL is the source of
|
|
115
|
+
// truth for the selection.
|
|
116
|
+
const props = withDefaults(defineProps<{
|
|
117
|
+
resource: Resource
|
|
118
|
+
to: RouteLocationRaw
|
|
119
|
+
selected?: boolean
|
|
120
|
+
replace?: boolean
|
|
121
|
+
}>(), {
|
|
122
|
+
selected: false,
|
|
123
|
+
replace: false,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const { t } = useTranslation()
|
|
127
|
+
|
|
128
|
+
// Render the icon directly (not via ResourceIcon which forces a gray color) so the
|
|
129
|
+
// colored badge can tint it through currentColor + [&_svg]:fill-current.
|
|
130
|
+
const iconComponent = computed(() => (props.resource.format ? getResourceFormatIcon(props.resource.format) : null) ?? File)
|
|
131
|
+
const iconColor = computed(() => getResourceIconColor(props.resource.format))
|
|
132
|
+
|
|
133
|
+
const humanFilesize = computed(() => {
|
|
134
|
+
const size = getResourceFilesize(props.resource)
|
|
135
|
+
return size ? filesize(size) : null
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Hover card teleported to <body> so the sidebar's `overflow` doesn't clip it, hence
|
|
139
|
+
// the fixed strategy. `shift` keeps it inside the viewport for a row near an edge, and
|
|
140
|
+
// `autoUpdate` follows the row when its scrollable container moves under the pointer.
|
|
141
|
+
const show = ref(false)
|
|
142
|
+
const row = useTemplateRef<InstanceType<typeof AppLink>>('row')
|
|
143
|
+
const card = useTemplateRef<HTMLElement>('card')
|
|
144
|
+
const rowEl = computed(() => row.value?.$el as HTMLElement | undefined)
|
|
145
|
+
const { floatingStyles } = useFloating(rowEl, card, {
|
|
146
|
+
placement: 'right-start',
|
|
147
|
+
strategy: 'fixed',
|
|
148
|
+
middleware: [offset(16), flip(), shift({ padding: 8 })],
|
|
149
|
+
whileElementsMounted: autoUpdate,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// A tap fires a pointer enter too, so the card would flash on every touch selection
|
|
153
|
+
// in the mobile resource picker. Only a real pointer opens it — keyboard focus still
|
|
154
|
+
// does, through @focus.
|
|
155
|
+
function openOnHover(event: PointerEvent) {
|
|
156
|
+
if (event.pointerType === 'mouse') show.value = true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function closeTooltip() {
|
|
160
|
+
show.value = false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A hover-shown card gets no `mouseleave` when the window loses focus (alt-tab) or
|
|
164
|
+
// the pointer leaves the document, so it would linger on return — close it in those
|
|
165
|
+
// cases too.
|
|
166
|
+
useEventListener(window, 'blur', closeTooltip)
|
|
167
|
+
useEventListener(document, 'mouseleave', closeTooltip)
|
|
168
|
+
</script>
|
|
@@ -21,7 +21,18 @@
|
|
|
21
21
|
:organization-url="organizationUrl"
|
|
22
22
|
/>
|
|
23
23
|
<RiSubtractLine class="size-4 flex-none fill-gray-medium" />
|
|
24
|
-
<
|
|
24
|
+
<TranslationT
|
|
25
|
+
class="block flex-none"
|
|
26
|
+
keypath="publié {date}"
|
|
27
|
+
>
|
|
28
|
+
<template #date>
|
|
29
|
+
<FormattedDate
|
|
30
|
+
:date="reuse.created_at"
|
|
31
|
+
format="relative"
|
|
32
|
+
:options="{ dateStyle: 'medium' }"
|
|
33
|
+
/>
|
|
34
|
+
</template>
|
|
35
|
+
</TranslationT>
|
|
25
36
|
</div>
|
|
26
37
|
<ReuseDetails :reuse />
|
|
27
38
|
</div>
|
|
@@ -67,17 +78,18 @@
|
|
|
67
78
|
import { RiLockLine, RiSubtractLine } from '@remixicon/vue'
|
|
68
79
|
import { computed } from 'vue'
|
|
69
80
|
import type { RouteLocationRaw } from 'vue-router'
|
|
70
|
-
import {
|
|
71
|
-
import type { Reuse } from '../types/reuses'
|
|
81
|
+
import type { Reuse, ReuseV2 } from '../types/reuses'
|
|
72
82
|
import type { TitleTag } from '../types/ui'
|
|
73
83
|
import { useTranslation } from '../composables/useTranslation'
|
|
74
84
|
import AppLink from './AppLink.vue'
|
|
75
85
|
import ObjectCardOwner from './ObjectCardOwner.vue'
|
|
76
86
|
import ReuseDetails from './ReuseDetails.vue'
|
|
77
87
|
import Placeholder from './Placeholder.vue'
|
|
88
|
+
import FormattedDate from './FormattedDate.vue'
|
|
89
|
+
import TranslationT from './TranslationT.vue'
|
|
78
90
|
|
|
79
91
|
const props = withDefaults(defineProps<{
|
|
80
|
-
reuse: Reuse
|
|
92
|
+
reuse: Reuse | ReuseV2
|
|
81
93
|
|
|
82
94
|
/**
|
|
83
95
|
* The reuseUrl is a route location object to allow Vue Router to navigate to the details of a reuse.
|
|
@@ -88,8 +100,9 @@ const props = withDefaults(defineProps<{
|
|
|
88
100
|
/**
|
|
89
101
|
* The organizationUrl is an optional route location object to allow Vue Router to navigate to the details of the organization linked to tha reuse.
|
|
90
102
|
* It is used as a separate prop to allow other sites using the package to define their own organization pages.
|
|
103
|
+
* Pass null instead of a route to render the organization name as plain, non-linked text.
|
|
91
104
|
*/
|
|
92
|
-
organizationUrl?: RouteLocationRaw
|
|
105
|
+
organizationUrl?: RouteLocationRaw | null
|
|
93
106
|
|
|
94
107
|
titleTag?: TitleTag
|
|
95
108
|
}>(), {
|
|
@@ -97,7 +110,6 @@ const props = withDefaults(defineProps<{
|
|
|
97
110
|
})
|
|
98
111
|
|
|
99
112
|
const { t } = useTranslation()
|
|
100
|
-
const { formatRelativeIfRecentDate } = useFormatDate()
|
|
101
113
|
|
|
102
114
|
const reuseUrl = computed(() => props.reuseUrl || props.reuse.page)
|
|
103
115
|
</script>
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
import { RiEyeLine, RiStarLine, RiSubtractLine } from '@remixicon/vue'
|
|
33
33
|
import { useReuseType } from '../composables/useReuseType'
|
|
34
34
|
import { summarize } from '../functions/helpers'
|
|
35
|
-
import type { Reuse } from '../types/reuses'
|
|
35
|
+
import type { Reuse, ReuseV2 } from '../types/reuses'
|
|
36
36
|
import { useTranslation } from '../composables/useTranslation'
|
|
37
37
|
|
|
38
38
|
const props = defineProps<{
|
|
39
|
-
reuse: Reuse
|
|
39
|
+
reuse: Reuse | ReuseV2
|
|
40
40
|
}>()
|
|
41
41
|
|
|
42
42
|
const { t } = useTranslation()
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<script setup lang="ts">
|
|
61
61
|
import { RiArchiveLine, RiLineChartLine, RiLockLine } from '@remixicon/vue'
|
|
62
62
|
import type { RouteLocationRaw } from 'vue-router'
|
|
63
|
-
import type { Reuse } from '../types/reuses'
|
|
63
|
+
import type { Reuse, ReuseV2 } from '../types/reuses'
|
|
64
64
|
import { useTranslation } from '../composables/useTranslation'
|
|
65
65
|
import ReuseDetails from './ReuseDetails.vue'
|
|
66
66
|
import Placeholder from './Placeholder.vue'
|
|
@@ -71,9 +71,10 @@ import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
|
|
|
71
71
|
import ObjectCardBadge from './ObjectCardBadge.vue'
|
|
72
72
|
|
|
73
73
|
defineProps<{
|
|
74
|
-
reuse: Reuse
|
|
74
|
+
reuse: Reuse | ReuseV2
|
|
75
75
|
reuseUrl?: RouteLocationRaw
|
|
76
|
-
|
|
76
|
+
// undefined falls back to the organization's data.gouv.fr page; pass null to render plain, non-linked text
|
|
77
|
+
organizationUrl?: RouteLocationRaw | null
|
|
77
78
|
}>()
|
|
78
79
|
|
|
79
80
|
const { t } = useTranslation()
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
+
:id
|
|
3
4
|
class="p-4 relative border bg-white hover:bg-gray-some"
|
|
4
|
-
:class="{
|
|
5
|
+
:class="{
|
|
6
|
+
'border-gray-default': !selected && !active,
|
|
7
|
+
'border-datagouv': selected && !active,
|
|
8
|
+
'border-blue-outline': active,
|
|
9
|
+
}"
|
|
5
10
|
:role="selectable ? 'option' : undefined"
|
|
11
|
+
:tabindex="selectable ? -1 : undefined"
|
|
6
12
|
:aria-selected="selectable && selected"
|
|
7
13
|
>
|
|
8
14
|
<h3 class="text-base font-bold mb-0 flex items-center gap-1">
|
|
@@ -73,7 +79,10 @@ import { useComponentsConfig } from '../config'
|
|
|
73
79
|
|
|
74
80
|
defineProps<{
|
|
75
81
|
schema: RegisteredSchema
|
|
82
|
+
id?: string
|
|
76
83
|
selectable?: boolean
|
|
84
|
+
// Highlighted by the keyboard inside the listbox, which is not the same as chosen
|
|
85
|
+
active?: boolean
|
|
77
86
|
selected?: boolean
|
|
78
87
|
showLinks?: boolean
|
|
79
88
|
}>()
|
|
@@ -31,7 +31,7 @@ const { t } = useTranslation()
|
|
|
31
31
|
|
|
32
32
|
const options = [
|
|
33
33
|
{ value: 'open', label: t('Téléchargement libre') },
|
|
34
|
-
{ value: 'open_with_account', label: t('Ouvert
|
|
35
|
-
{ value: 'restricted', label: t('
|
|
34
|
+
{ value: 'open_with_account', label: t('Ouvert avec un compte') },
|
|
35
|
+
{ value: 'restricted', label: t('Accès restreint') },
|
|
36
36
|
]
|
|
37
37
|
</script>
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
>
|
|
7
7
|
<div
|
|
8
8
|
v-if="!hideSearchInput"
|
|
9
|
-
ref="search"
|
|
10
9
|
class="flex flex-wrap items-center justify-between"
|
|
11
10
|
data-cy="search"
|
|
12
11
|
>
|
|
@@ -228,6 +227,7 @@
|
|
|
228
227
|
size="sm"
|
|
229
228
|
:icon="RiRssLine"
|
|
230
229
|
icon-only
|
|
230
|
+
external
|
|
231
231
|
target="_blank"
|
|
232
232
|
/>
|
|
233
233
|
</div>
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
name="reuse"
|
|
267
267
|
:reuse="result"
|
|
268
268
|
>
|
|
269
|
-
<ReuseHorizontalCard :reuse="(result as
|
|
269
|
+
<ReuseHorizontalCard :reuse="(result as ReuseV2)" />
|
|
270
270
|
</slot>
|
|
271
271
|
</template>
|
|
272
272
|
<template v-else-if="currentTypeConfig?.class === 'organizations'">
|
|
@@ -356,6 +356,7 @@
|
|
|
356
356
|
<script setup lang="ts">
|
|
357
357
|
import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
|
|
358
358
|
import { useRouteQuery } from '@vueuse/router'
|
|
359
|
+
import { useRoute } from 'vue-router'
|
|
359
360
|
import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
|
|
360
361
|
import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
|
|
361
362
|
import { useTranslation } from '../../composables/useTranslation'
|
|
@@ -364,11 +365,12 @@ import { configKey, forEachActiveCustomFilter, isCustomFilterActive, searchFilte
|
|
|
364
365
|
import { useStableQueryParams } from '../../composables/useStableQueryParams'
|
|
365
366
|
import { useComponentsConfig } from '../../config'
|
|
366
367
|
import { useFetch } from '../../functions/api'
|
|
368
|
+
import { scrollToBlockTop } from '../../functions/scroll'
|
|
367
369
|
import type { AsyncDataRequestStatus } from '../../functions/api.types'
|
|
368
370
|
import type { Dataset } from '../../types/datasets'
|
|
369
371
|
import type { Dataservice } from '../../types/dataservices'
|
|
370
372
|
import type { Organization } from '../../types/organizations'
|
|
371
|
-
import type {
|
|
373
|
+
import type { ReuseV2 } from '../../types/reuses'
|
|
372
374
|
import type { TopicV2 } from '../../types/topics'
|
|
373
375
|
import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SortOption, FacetItem } from '../../types/search'
|
|
374
376
|
import { getDefaultGlobalSearchConfig } from '../../types/search'
|
|
@@ -422,6 +424,7 @@ if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class
|
|
|
422
424
|
|
|
423
425
|
const { t } = useTranslation()
|
|
424
426
|
const componentsConfig = useComponentsConfig()
|
|
427
|
+
const route = useRoute()
|
|
425
428
|
|
|
426
429
|
// Custom filter registry for useSearchFilter composable
|
|
427
430
|
const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
|
|
@@ -796,14 +799,15 @@ function getFacets(key: string): FacetItem[] | undefined {
|
|
|
796
799
|
}
|
|
797
800
|
|
|
798
801
|
// Scroll handling
|
|
799
|
-
const
|
|
802
|
+
const resultsRef = useTemplateRef('results')
|
|
800
803
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
+
// Every criteria lives in the URL, custom filters included, so watching the
|
|
805
|
+
// query covers them all: whenever the result list is replaced, bring its top
|
|
806
|
+
// back into view rather than leaving the reader in the middle of a list they
|
|
807
|
+
// have not seen yet.
|
|
808
|
+
watch(() => route.query, () => scrollToBlockTop(resultsRef.value))
|
|
804
809
|
|
|
805
810
|
function changePage(newPage: number) {
|
|
806
811
|
page.value = newPage
|
|
807
|
-
scrollToTop()
|
|
808
812
|
}
|
|
809
813
|
</script>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
v-model="q"
|
|
13
13
|
type="search"
|
|
14
14
|
name="q"
|
|
15
|
-
class="input max-h-12 m-0 rounded-tl shadow-input-blue"
|
|
15
|
+
class="input flex-1 max-h-12 m-0 rounded-tl shadow-input-blue"
|
|
16
16
|
:aria-label="placeholder === null ? t('Rechercher...') : placeholder ?? t('Rechercher...')"
|
|
17
17
|
:placeholder="placeholder === null ? undefined : placeholder ?? t('Rechercher...')"
|
|
18
18
|
>
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
:class="{ 'rotate-180': open }"
|
|
12
12
|
/>
|
|
13
13
|
</button>
|
|
14
|
-
|
|
14
|
+
<!-- Collapsing with CSS rather than `v-if` on a JS media query: the server has no
|
|
15
|
+
viewport, so it always renders the desktop state and a narrow client would drop
|
|
16
|
+
the whole panel during hydration. -->
|
|
17
|
+
<div :class="{ 'hidden md:block': !open }">
|
|
15
18
|
<p
|
|
16
19
|
:id="titleId"
|
|
17
20
|
class="text-sm font-bold leading-tight mb-6 hidden md:block"
|
|
@@ -26,13 +29,11 @@
|
|
|
26
29
|
<script setup lang="ts">
|
|
27
30
|
import { ref, useId } from 'vue'
|
|
28
31
|
import { RiArrowDownSLine } from '@remixicon/vue'
|
|
29
|
-
import { useMediaQuery } from '@vueuse/core'
|
|
30
32
|
|
|
31
33
|
defineProps<{
|
|
32
34
|
buttonText: string
|
|
33
35
|
}>()
|
|
34
36
|
|
|
35
37
|
const titleId = useId()
|
|
36
|
-
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
37
38
|
const open = ref(false)
|
|
38
39
|
</script>
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
class="flex flex-wrap justify-between"
|
|
12
12
|
>
|
|
13
13
|
<p class="text-[0.625rem] m-0 text-gray-medium">
|
|
14
|
-
|
|
14
|
+
<FormattedDate
|
|
15
|
+
:date="startDate"
|
|
16
|
+
:options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
|
|
17
|
+
/>
|
|
15
18
|
</p>
|
|
16
19
|
<p class="text-[0.625rem] m-0 text-gray-medium">
|
|
17
|
-
|
|
20
|
+
<FormattedDate
|
|
21
|
+
:date="endDate"
|
|
22
|
+
:options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
|
|
23
|
+
/>
|
|
18
24
|
</p>
|
|
19
25
|
</div>
|
|
20
26
|
</div>
|
|
@@ -24,6 +30,7 @@
|
|
|
24
30
|
import type { ChartOptions, ScriptableLineSegmentContext } from 'chart.js'
|
|
25
31
|
import Chart from 'chart.js/auto'
|
|
26
32
|
import { computed, onMounted, ref, useTemplateRef, watchEffect } from 'vue'
|
|
33
|
+
import FormattedDate from './FormattedDate.vue'
|
|
27
34
|
|
|
28
35
|
const LIGHT_COLOR = '#B6CFFB'
|
|
29
36
|
const COLOR = '#3558A2'
|
|
@@ -107,14 +114,8 @@ const additionalDatasetConfig = computed<{
|
|
|
107
114
|
return {}
|
|
108
115
|
})
|
|
109
116
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getFullYear().toString().slice(-2)}`
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const startDate = computed(() => months.value.length ? getMonthYear(months.value[0]!) : null)
|
|
117
|
-
const endDate = computed(() => months.value.length ? getMonthYear(months.value[months.value.length - 1]!) : null)
|
|
117
|
+
const startDate = computed(() => months.value[0] ?? null)
|
|
118
|
+
const endDate = computed(() => months.value[months.value.length - 1] ?? null)
|
|
118
119
|
|
|
119
120
|
const OPTIONS = {
|
|
120
121
|
// @ts-expect-error animation can be `true` but the typing is not expecting it
|
|
@@ -65,14 +65,20 @@
|
|
|
65
65
|
class="mt-1 mb-0 text-xs"
|
|
66
66
|
>
|
|
67
67
|
{{ t("depuis ") }}
|
|
68
|
-
|
|
68
|
+
<FormattedDate
|
|
69
|
+
:date="since"
|
|
70
|
+
:options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
|
|
71
|
+
/>
|
|
69
72
|
</p>
|
|
70
73
|
<p class="mt-1 mb-0 text-xs text-success-darkest">
|
|
71
74
|
<strong>
|
|
72
75
|
+ {{ summarize(lastValue, 2) }}
|
|
73
76
|
</strong>
|
|
74
77
|
{{ t(" en ") }}
|
|
75
|
-
|
|
78
|
+
<FormattedDate
|
|
79
|
+
:date="lastMonth"
|
|
80
|
+
:options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
|
|
81
|
+
/>
|
|
76
82
|
</p>
|
|
77
83
|
</template>
|
|
78
84
|
</div>
|
|
@@ -146,7 +152,10 @@
|
|
|
146
152
|
+ {{ summarize(lastValue, 2) }}
|
|
147
153
|
</strong>
|
|
148
154
|
{{ t(" en ") }}
|
|
149
|
-
|
|
155
|
+
<FormattedDate
|
|
156
|
+
:date="lastMonth"
|
|
157
|
+
:options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
|
|
158
|
+
/>
|
|
150
159
|
</p>
|
|
151
160
|
</div>
|
|
152
161
|
</template>
|
|
@@ -154,7 +163,7 @@
|
|
|
154
163
|
<script setup lang="ts">
|
|
155
164
|
import { computed } from 'vue'
|
|
156
165
|
import { ContentLoader } from 'vue-content-loader'
|
|
157
|
-
import
|
|
166
|
+
import FormattedDate from './FormattedDate.vue'
|
|
158
167
|
import { summarize } from '../functions/helpers'
|
|
159
168
|
import { useTranslation } from '../composables/useTranslation'
|
|
160
169
|
import SmallChart from './SmallChart.vue'
|
|
@@ -169,7 +178,6 @@ const props = defineProps<{
|
|
|
169
178
|
}>()
|
|
170
179
|
|
|
171
180
|
const { t } = useTranslation()
|
|
172
|
-
const { formatDate } = useFormatDate()
|
|
173
181
|
|
|
174
182
|
const months = computed(() => props.data ? Object.keys(props.data) : [])
|
|
175
183
|
const values = computed(() => props.data ? Object.values(props.data) : [])
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="activeFilters.length > 0 || sort"
|
|
4
|
+
class="flex flex-wrap items-center gap-1.5"
|
|
5
|
+
>
|
|
6
|
+
<!-- Sort chip -->
|
|
7
|
+
<span
|
|
8
|
+
v-if="sort"
|
|
9
|
+
class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
|
|
10
|
+
>
|
|
11
|
+
<component
|
|
12
|
+
:is="sort.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
|
|
13
|
+
class="size-3.5"
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
/>
|
|
16
|
+
<span class="font-medium">{{ sort.column }}</span>
|
|
17
|
+
<span>{{ sort.direction === 'asc' ? t('croissant') : t('décroissant') }}</span>
|
|
18
|
+
<button
|
|
19
|
+
:aria-label="t('Supprimer le tri')"
|
|
20
|
+
class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
|
|
21
|
+
@click="sort = null"
|
|
22
|
+
>
|
|
23
|
+
<RiCloseLine
|
|
24
|
+
class="size-3.5"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
/>
|
|
27
|
+
</button>
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
<!-- Filter chips -->
|
|
31
|
+
<span
|
|
32
|
+
v-for="af in activeFilters"
|
|
33
|
+
:key="af.column"
|
|
34
|
+
:data-testid="`active-filter-${af.column}`"
|
|
35
|
+
class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
|
|
36
|
+
>
|
|
37
|
+
<component
|
|
38
|
+
:is="getColumnDisplay(af.column).icon"
|
|
39
|
+
class="size-3.5"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
/>
|
|
42
|
+
<span class="font-medium">{{ af.column }}</span>
|
|
43
|
+
<span>{{ af.label }}</span>
|
|
44
|
+
<button
|
|
45
|
+
:aria-label="t('Supprimer ce filtre')"
|
|
46
|
+
class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
|
|
47
|
+
@click="removeFilter(af.column)"
|
|
48
|
+
>
|
|
49
|
+
<RiCloseLine
|
|
50
|
+
class="size-3.5"
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
/>
|
|
53
|
+
</button>
|
|
54
|
+
</span>
|
|
55
|
+
|
|
56
|
+
<!-- Clear all -->
|
|
57
|
+
<BrandedButton
|
|
58
|
+
v-if="withClear"
|
|
59
|
+
color="tertiary"
|
|
60
|
+
size="2xs"
|
|
61
|
+
:icon="RiCloseLine"
|
|
62
|
+
@click="clearAllFilters(); sort = null"
|
|
63
|
+
>
|
|
64
|
+
{{ t('Tout effacer') }}
|
|
65
|
+
</BrandedButton>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script setup lang="ts">
|
|
70
|
+
import { RiArrowDownLine, RiArrowUpLine, RiCloseLine } from '@remixicon/vue'
|
|
71
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
72
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
73
|
+
import { useTabularContext } from './useTabularContext'
|
|
74
|
+
|
|
75
|
+
defineProps<{ withClear?: boolean }>()
|
|
76
|
+
|
|
77
|
+
const { t } = useTranslation()
|
|
78
|
+
const { activeFilters, sort, removeFilter, clearAllFilters, getColumnDisplay } = useTabularContext()
|
|
79
|
+
</script>
|
|
@@ -17,16 +17,15 @@
|
|
|
17
17
|
</span>
|
|
18
18
|
<span
|
|
19
19
|
v-else-if="columnType === 'categorical'"
|
|
20
|
-
class="inline-block rounded
|
|
21
|
-
:style="categoryBadgeStyle ? { backgroundColor: categoryBadgeStyle.backgroundColor, color: categoryBadgeStyle.color } : undefined"
|
|
20
|
+
class="inline-block max-w-full truncate rounded bg-gray-lower px-2 py-0.5 text-xs text-gray-plain"
|
|
22
21
|
>{{ value }}</span>
|
|
23
22
|
<span
|
|
24
|
-
v-else-if="columnType === 'number'"
|
|
25
|
-
:class="compact ? 'font-mono tabular-nums text-xs
|
|
26
|
-
>{{ formatNumber(value) }}</span>
|
|
23
|
+
v-else-if="columnType === 'number' || columnType === 'year'"
|
|
24
|
+
:class="compact ? 'font-mono tabular-nums text-xs' : ''"
|
|
25
|
+
>{{ columnType === 'year' || noNumberFormat ? value : formatNumber(value) }}</span>
|
|
27
26
|
<span
|
|
28
27
|
v-else-if="columnType === 'date'"
|
|
29
|
-
:class="compact ? 'font-mono tabular-nums text-xs
|
|
28
|
+
:class="compact ? 'font-mono tabular-nums text-xs' : ''"
|
|
30
29
|
>{{ formatCellDate(value) }}</span>
|
|
31
30
|
<span
|
|
32
31
|
v-else
|
|
@@ -37,13 +36,13 @@
|
|
|
37
36
|
<script setup lang="ts">
|
|
38
37
|
import { useTranslation } from '../../composables/useTranslation'
|
|
39
38
|
import { useFormatTabular, isTruthy } from '../../functions/tabular'
|
|
40
|
-
import type { ColumnType
|
|
39
|
+
import type { ColumnType } from './types'
|
|
41
40
|
|
|
42
41
|
defineProps<{
|
|
43
42
|
value: unknown
|
|
44
43
|
columnType: ColumnType
|
|
45
|
-
categoryBadgeStyle?: BadgeStyle
|
|
46
44
|
compact?: boolean
|
|
45
|
+
noNumberFormat?: boolean
|
|
47
46
|
}>()
|
|
48
47
|
|
|
49
48
|
const { t } = useTranslation()
|