@datagouv/components-next 1.1.2 → 1.3.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 +7 -6
- package/dist/{Control-ZFh5ta_U.js → Control-b-43UT2B.js} +10 -10
- package/dist/{Datafair.client-8bXp6UeQ.js → Datafair.client-BrjISlaZ.js} +4 -4
- package/dist/{Event-DSQcW7OF.js → Event-py_AAwJY.js} +18 -18
- package/dist/{Image-BijNEG0p.js → Image-BHQYZWzH.js} +23 -23
- package/dist/{JsonPreview.client-BxuoPK_w.js → JsonPreview.client-DivFAnbN.js} +5 -5
- package/dist/{Map-BUtPf5GN.js → Map-CtP7leHl.js} +1313 -1173
- package/dist/{MapContainer.client-CTz0wmJG.js → MapContainer.client-Cq8tNLZZ.js} +17 -17
- package/dist/{OSM-D4MTdBtk.js → OSM-gdDSIsDA.js} +15 -23
- package/dist/{PdfPreview.client-DpVreUSl.js → PdfPreview.client-BNBgLsK2.js} +6 -10
- package/dist/Pmtiles.client-CQi5Rw8W.js +26123 -0
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-stmU5qEB.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js} +2 -2
- package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-hG-FhNqy.js} +2 -2
- package/dist/{Tile-Dcl7oIVu.js → Tile-D6rFonpT.js} +163 -166
- package/dist/{TileImage-BJeHipMX.js → TileImage-cfrEgz5y.js} +131 -119
- package/dist/{View-xp_P_OHw.js → View-vp7vDb9q.js} +169 -119
- package/dist/{XmlPreview.client-DAOs89cR.js → XmlPreview.client-XCsNvnsZ.js} +5 -5
- package/dist/{common-BjQlan3k.js → common-C8efyyuc.js} +49 -49
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +166 -157
- package/dist/components.css +2 -2
- package/dist/{index-JqjPja1u.js → index-CXUamc8Z.js} +6382 -6057
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-BRKv9Ksv.js} +162 -164
- package/dist/{main-D4WQMky0.js → main-DKu5SOAl.js} +48480 -97188
- package/dist/{proj-CsNo9yH1.js → proj-CsjVeojN.js} +322 -306
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-A0fLnBZr.js → tilecoord-CPcQJnrF.js} +270 -261
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-BGsoNyZe.js → vue3-xml-viewer.common-BIaEIFYG.js} +1 -1
- package/package.json +42 -35
- package/src/chart.ts +5 -0
- package/src/components/ActivityList/ActivityList.vue +3 -0
- package/src/components/DatasetCard.vue +6 -4
- package/src/components/ObjectCardHeader.vue +1 -1
- package/src/components/RadioInput.vue +7 -2
- package/src/components/ResourceAccordion/DataStructure.vue +11 -33
- package/src/components/ResourceAccordion/Downloads.vue +160 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +3 -1
- package/src/components/ResourceAccordion/ResourceAccordion.vue +7 -104
- package/src/components/ResourceExplorer/ResourceExplorer.vue +10 -64
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +30 -136
- package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
- package/src/components/Search/GlobalSearch.vue +27 -6
- package/src/components/TabularExplorer/TabularCellPopover.vue +2 -2
- package/src/components/TabularExplorer/TabularExplorer.vue +404 -163
- package/src/components/TabularExplorer/TabularFilterContent.vue +15 -13
- package/src/components/TabularExplorer/TabularFilterPopover.vue +21 -26
- package/src/components/TabularExplorer/types.ts +9 -9
- package/src/composables/useHasTabularData.ts +7 -0
- package/src/composables/useResourceCapabilities.ts +1 -1
- package/src/composables/useSearchFilter.ts +7 -5
- package/src/composables/useStableQueryParams.ts +7 -3
- package/src/composables/useTabularProfile.ts +70 -0
- package/src/functions/activities.ts +3 -3
- package/src/functions/api.ts +13 -7
- package/src/functions/resources.ts +5 -0
- package/src/functions/tabular.ts +103 -1
- package/src/main.ts +12 -6
- package/src/types/resources.ts +0 -1
- package/src/types/search.ts +11 -0
- package/src/types/ui.ts +1 -1
- package/dist/Pmtiles.client-BwmHo3T8.js +0 -25185
|
@@ -26,20 +26,22 @@
|
|
|
26
26
|
<!-- Null filter with progress bar -->
|
|
27
27
|
<div
|
|
28
28
|
v-if="columnProfile && columnProfile.nb_missing_values > 0"
|
|
29
|
-
class="
|
|
29
|
+
class="px-3 py-2 border-b border-black/10 space-y-1.5"
|
|
30
30
|
>
|
|
31
|
-
<
|
|
32
|
-
<span class="
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
<div class="flex items-center gap-2">
|
|
32
|
+
<span class="text-xs text-gray-plain whitespace-nowrap">
|
|
33
|
+
<span class="font-mono tabular-nums">{{ columnProfile.nb_missing_values }}</span>
|
|
34
|
+
null
|
|
35
|
+
<span class="text-gray-low">({{ nullPercent }})</span>
|
|
36
|
+
</span>
|
|
37
|
+
<ProgressBar
|
|
38
|
+
:value="columnProfile.nb_missing_values"
|
|
39
|
+
:max="totalLines"
|
|
40
|
+
bar-class="bg-gray-low"
|
|
41
|
+
class="flex-1 !h-1.5 !min-w-0 !border-0 !bg-gray-default"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="flex items-center justify-end gap-0.5">
|
|
43
45
|
<BrandedButton
|
|
44
46
|
:color="nullFilter === 'only' ? 'primary' : 'tertiary'"
|
|
45
47
|
size="2xs"
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<Popover
|
|
3
3
|
ref="anchor"
|
|
4
|
+
v-slot="{ open }"
|
|
4
5
|
class="relative shrink-0"
|
|
5
6
|
>
|
|
6
|
-
<
|
|
7
|
-
class="p-0.5 rounded focus:outline-none"
|
|
7
|
+
<PopoverButton
|
|
8
|
+
class="p-0.5 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
|
|
8
9
|
:class="hasColumnFilter ? 'bg-primary text-white' : 'hover:bg-gray-100'"
|
|
9
|
-
@click.stop="togglePopover"
|
|
10
10
|
>
|
|
11
|
-
<
|
|
11
|
+
<RiFilterLine
|
|
12
12
|
class="size-3.5"
|
|
13
13
|
aria-hidden="true"
|
|
14
14
|
/>
|
|
15
15
|
<span class="sr-only">{{ t('Filtrer') }} {{ column }}</span>
|
|
16
|
-
</
|
|
16
|
+
</PopoverButton>
|
|
17
17
|
|
|
18
18
|
<ClientOnly>
|
|
19
19
|
<Teleport to="#tooltips">
|
|
20
|
-
<
|
|
21
|
-
v-show="
|
|
20
|
+
<PopoverPanel
|
|
21
|
+
v-show="open"
|
|
22
22
|
ref="panel"
|
|
23
|
+
static
|
|
23
24
|
class="bg-white border border-black/10 rounded-lg shadow-md w-64 absolute z-[800]"
|
|
24
25
|
:style="floatingStyles"
|
|
25
26
|
>
|
|
@@ -50,17 +51,17 @@
|
|
|
50
51
|
:category-badge-styles="categoryBadgeStyles"
|
|
51
52
|
:boolean-counts="booleanCounts"
|
|
52
53
|
/>
|
|
53
|
-
</
|
|
54
|
+
</PopoverPanel>
|
|
54
55
|
</Teleport>
|
|
55
56
|
</ClientOnly>
|
|
56
|
-
</
|
|
57
|
+
</Popover>
|
|
57
58
|
</template>
|
|
58
59
|
|
|
59
60
|
<script setup lang="ts">
|
|
60
|
-
import { computed,
|
|
61
|
+
import { computed, useTemplateRef } from 'vue'
|
|
61
62
|
import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
63
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
64
|
+
import { RiFilterLine, RiCloseLine } from '@remixicon/vue'
|
|
64
65
|
import { useTranslation } from '../../composables/useTranslation'
|
|
65
66
|
import { hasFilterForColumn as _hasFilterForColumn } from '../../functions/tabular'
|
|
66
67
|
import BrandedButton from '../BrandedButton.vue'
|
|
@@ -83,18 +84,12 @@ const filters = defineModel<Record<string, ColumnFilters>>('filters', { default:
|
|
|
83
84
|
|
|
84
85
|
const { t } = useTranslation()
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const anchorRef = useTemplateRef<HTMLElement>('anchor')
|
|
93
|
-
const panelRef = useTemplateRef<HTMLElement>('panel')
|
|
94
|
-
|
|
95
|
-
onClickOutside(panelRef, () => {
|
|
96
|
-
isOpen.value = false
|
|
97
|
-
}, { ignore: [anchorRef] })
|
|
87
|
+
// Headless UI Popover manages open state, Escape-to-close, outside-click,
|
|
88
|
+
// focus handling and ARIA. floating-ui only positions the teleported panel.
|
|
89
|
+
const anchorComponent = useTemplateRef<InstanceType<typeof Popover>>('anchor')
|
|
90
|
+
const panelComponent = useTemplateRef<InstanceType<typeof PopoverPanel>>('panel')
|
|
91
|
+
const anchorEl = computed(() => anchorComponent.value?.$el as HTMLElement | undefined)
|
|
92
|
+
const panelEl = computed(() => panelComponent.value?.$el as HTMLElement | undefined)
|
|
98
93
|
|
|
99
94
|
const hasColumnFilter = computed(() => _hasFilterForColumn(filters.value, props.column))
|
|
100
95
|
|
|
@@ -103,7 +98,7 @@ function clearColumnFilter() {
|
|
|
103
98
|
filters.value = rest
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
const { floatingStyles } = useFloating(
|
|
101
|
+
const { floatingStyles } = useFloating(anchorEl, panelEl, {
|
|
107
102
|
placement: 'bottom-start',
|
|
108
103
|
middleware: [flip(), shift()],
|
|
109
104
|
whileElementsMounted: autoUpdate,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Response from /api/resources/{rid}/data/ */
|
|
2
|
-
export
|
|
2
|
+
export type TabularDataResponse = {
|
|
3
3
|
data: TabularRow[]
|
|
4
4
|
meta: {
|
|
5
5
|
page: number
|
|
@@ -17,11 +17,11 @@ export interface TabularDataResponse {
|
|
|
17
17
|
export type TabularRow = Record<string, unknown> & { __id: number }
|
|
18
18
|
|
|
19
19
|
/** Response from /api/resources/{rid}/profile/ */
|
|
20
|
-
export
|
|
20
|
+
export type TabularProfileResponse = {
|
|
21
21
|
profile: TabularProfile
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export
|
|
24
|
+
export type TabularProfile = {
|
|
25
25
|
header: string[]
|
|
26
26
|
columns: Record<string, TabularColumnInfo>
|
|
27
27
|
formats: Record<string, string[]>
|
|
@@ -38,13 +38,13 @@ export interface TabularProfile {
|
|
|
38
38
|
trailing_columns: number
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export
|
|
41
|
+
export type TabularColumnInfo = {
|
|
42
42
|
score: number
|
|
43
43
|
format: string
|
|
44
44
|
python_type: string
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export
|
|
47
|
+
export type TabularColumnProfile = {
|
|
48
48
|
tops: TabularTopValue[]
|
|
49
49
|
nb_distinct: number
|
|
50
50
|
nb_missing_values: number
|
|
@@ -54,14 +54,14 @@ export interface TabularColumnProfile {
|
|
|
54
54
|
mean?: number
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export
|
|
57
|
+
export type TabularTopValue = {
|
|
58
58
|
value: string
|
|
59
59
|
count: number
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean'
|
|
63
63
|
|
|
64
|
-
export
|
|
64
|
+
export type ColumnFilters = {
|
|
65
65
|
in?: string[]
|
|
66
66
|
exact?: string
|
|
67
67
|
min?: number
|
|
@@ -72,12 +72,12 @@ export interface ColumnFilters {
|
|
|
72
72
|
|
|
73
73
|
export type SortDirection = 'asc' | 'desc'
|
|
74
74
|
|
|
75
|
-
export
|
|
75
|
+
export type SortConfig = {
|
|
76
76
|
column: string
|
|
77
77
|
direction: SortDirection
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export
|
|
80
|
+
export type BadgeStyle = {
|
|
81
81
|
backgroundColor: string
|
|
82
82
|
color: string
|
|
83
83
|
}
|
|
@@ -11,10 +11,17 @@ export const useHasTabularData = () => {
|
|
|
11
11
|
const config = useComponentsConfig()
|
|
12
12
|
|
|
13
13
|
return (resource: Resource) => {
|
|
14
|
+
// Reject resources whose source URL last check failed (>= 400):
|
|
15
|
+
// the tabular-api purges its parquet when the source dies, but
|
|
16
|
+
// `parsing_table` may still be set — leading to a 404 on fetch.
|
|
17
|
+
const checkStatus = resource.extras['check:status']
|
|
18
|
+
const sourceUnreachable = typeof checkStatus === 'number' && checkStatus >= 400
|
|
19
|
+
|
|
14
20
|
return (
|
|
15
21
|
config.tabularApiUrl
|
|
16
22
|
&& resource.extras['analysis:parsing:parsing_table']
|
|
17
23
|
&& !resource.extras['analysis:parsing:error']
|
|
24
|
+
&& !sourceUnreachable
|
|
18
25
|
&& (config.tabularAllowRemote || resource.filetype === 'file')
|
|
19
26
|
)
|
|
20
27
|
}
|
|
@@ -108,7 +108,7 @@ export function useResourceCapabilities(
|
|
|
108
108
|
options.push({ key: 'downloads', label: t('Téléchargements') })
|
|
109
109
|
|
|
110
110
|
if (hasTabularData.value) {
|
|
111
|
-
options.push({ key: '
|
|
111
|
+
options.push({ key: 'api', label: t('API') })
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return options
|
|
@@ -94,10 +94,12 @@ export function useSearchFilter(
|
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
onScopeDispose(() => {
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
97
|
+
// Type-scoped filters (typeKeys set) are cleared by GlobalSearch's
|
|
98
|
+
// watch(currentType) instead, so their URL update batches with the other
|
|
99
|
+
// type-change resets via useRouteQuery rather than racing them with the
|
|
100
|
+
// separate router.replace below. Here we only clear filters that apply to
|
|
101
|
+
// all types: their unmount is a consumer signal unrelated to a type switch,
|
|
102
|
+
// which GlobalSearch has no way to detect.
|
|
101
103
|
//
|
|
102
104
|
// We cannot use `value.value = defaultValue` here because VueUse's
|
|
103
105
|
// useRouteQuery registers its own tryOnScopeDispose cleanup that zeroes
|
|
@@ -105,7 +107,7 @@ export function useSearchFilter(
|
|
|
105
107
|
// The setter then sees `query === v` and early-returns without ever
|
|
106
108
|
// calling router.replace(). Instead we read the live route.query directly
|
|
107
109
|
// (which is router state, not affected by that cleanup) and push the update.
|
|
108
|
-
if (route.query[urlParam] !== undefined) {
|
|
110
|
+
if (normalizedTypeKeys === undefined && route.query[urlParam] !== undefined) {
|
|
109
111
|
const { [urlParam]: _removed, ...restQuery } = route.query
|
|
110
112
|
router.replace({
|
|
111
113
|
query: defaultValue === undefined ? restQuery : { ...restQuery, [urlParam]: String(defaultValue) },
|
|
@@ -72,10 +72,14 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
|
|
|
72
72
|
if (q.value) {
|
|
73
73
|
params.q = q.value
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
const sortToUse = sort.value ?? typeConfig?.defaultSort
|
|
76
|
+
if (sortToUse) {
|
|
76
77
|
const validSortValues = typeConfig?.sortOptions?.map(o => o.value as string) ?? []
|
|
77
|
-
if (validSortValues.includes(
|
|
78
|
-
params.sort =
|
|
78
|
+
if (validSortValues.includes(sortToUse)) {
|
|
79
|
+
params.sort = sortToUse
|
|
80
|
+
}
|
|
81
|
+
else if (import.meta.env.DEV && typeConfig?.defaultSort && typeConfig?.sortOptions && sortToUse === typeConfig.defaultSort) {
|
|
82
|
+
console.warn(`[GlobalSearch] defaultSort "${typeConfig.defaultSort}" is not in sortOptions for "${typeConfig.class}". Valid values: ${validSortValues.join(', ')}`)
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
params.page = page.value
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { computed, inject, provide, toValue, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
2
|
+
import { useComponentsConfig } from '../config'
|
|
3
|
+
import { useFetch } from '../functions/api'
|
|
4
|
+
import type { AsyncDataRequestStatus } from '../functions/api.types'
|
|
5
|
+
import type { TabularProfileResponse } from '../components/TabularExplorer/types'
|
|
6
|
+
|
|
7
|
+
const TABULAR_PROFILE_KEY = Symbol('tabular-profile')
|
|
8
|
+
|
|
9
|
+
export type TabularProfileState = {
|
|
10
|
+
resourceId: Readonly<Ref<string>>
|
|
11
|
+
data: Readonly<Ref<TabularProfileResponse | null>>
|
|
12
|
+
error: Readonly<Ref<unknown | null>>
|
|
13
|
+
status: Readonly<Ref<AsyncDataRequestStatus>>
|
|
14
|
+
refresh: () => Promise<void>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// What is shared through provide/inject: the resourceId (to let descendants
|
|
18
|
+
// check they target the same resource) and the in-flight fetch promise. We
|
|
19
|
+
// share the promise rather than the resolved state because `provide()` must
|
|
20
|
+
// run synchronously during setup — see `provideTabularProfile`.
|
|
21
|
+
type TabularProfileShared = {
|
|
22
|
+
resourceId: Readonly<Ref<string>>
|
|
23
|
+
state: Promise<TabularProfileState>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function createProfileState(resourceId: MaybeRefOrGetter<string>): Promise<TabularProfileState> {
|
|
27
|
+
const config = useComponentsConfig()
|
|
28
|
+
const ridRef = computed(() => toValue(resourceId))
|
|
29
|
+
|
|
30
|
+
// Goes through the package's useFetch, which delegates to the host's
|
|
31
|
+
// customUseFetch (Nuxt useFetch in cdata) so the response is shared
|
|
32
|
+
// between SSR and CSR via the Nuxt payload — avoiding a double fetch.
|
|
33
|
+
const profileUrl = computed(() =>
|
|
34
|
+
ridRef.value ? `${config.tabularApiUrl}/api/resources/${ridRef.value}/profile/` : null,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const { data, error, status, refresh } = await useFetch<TabularProfileResponse>(profileUrl, { raw: true })
|
|
38
|
+
|
|
39
|
+
return { resourceId: ridRef, data, error, status, refresh }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parent: fetch the tabular profile once and share it with descendants
|
|
44
|
+
* (TabularExplorer, DataStructure...) via provide/inject.
|
|
45
|
+
*
|
|
46
|
+
* Not async on purpose: `provide()` only works while the active component
|
|
47
|
+
* instance is set, which is lost after the first `await`. So we kick off the
|
|
48
|
+
* fetch, `provide()` the resulting promise synchronously, then return it for
|
|
49
|
+
* the caller to await.
|
|
50
|
+
*/
|
|
51
|
+
export function provideTabularProfile(resourceId: MaybeRefOrGetter<string>): Promise<TabularProfileState> {
|
|
52
|
+
const ridRef = computed(() => toValue(resourceId))
|
|
53
|
+
const state = createProfileState(resourceId)
|
|
54
|
+
provide<TabularProfileShared>(TABULAR_PROFILE_KEY, { resourceId: ridRef, state })
|
|
55
|
+
return state
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Child: get the tabular profile from an ancestor that called
|
|
60
|
+
* `provideTabularProfile` for the same resourceId. Falls back to
|
|
61
|
+
* fetching on its own if no compatible ancestor is found — preserves
|
|
62
|
+
* standalone usage of TabularExplorer / DataStructure.
|
|
63
|
+
*/
|
|
64
|
+
export async function injectTabularProfile(resourceId: MaybeRefOrGetter<string>): Promise<TabularProfileState> {
|
|
65
|
+
const injected = inject<TabularProfileShared | null>(TABULAR_PROFILE_KEY, null)
|
|
66
|
+
if (injected && injected.resourceId.value === toValue(resourceId)) {
|
|
67
|
+
return await injected.state
|
|
68
|
+
}
|
|
69
|
+
return await createProfileState(resourceId)
|
|
70
|
+
}
|
|
@@ -11,9 +11,9 @@ export function getActivityTranslation(activity: Activity): string {
|
|
|
11
11
|
'dataset:deleted': t('a supprimé le jeu de données'),
|
|
12
12
|
'dataset:discussed': t('a discuté du jeu de données'),
|
|
13
13
|
'dataset:followed': t('suit le jeu de données'),
|
|
14
|
-
'dataset:resource:added': t('a ajouté
|
|
15
|
-
'dataset:resource:updated': t('a mis à jour
|
|
16
|
-
'dataset:resource:deleted': t('a supprimé
|
|
14
|
+
'dataset:resource:added': t('a ajouté la ressource'),
|
|
15
|
+
'dataset:resource:updated': t('a mis à jour la ressource'),
|
|
16
|
+
'dataset:resource:deleted': t('a supprimé la ressource'),
|
|
17
17
|
'dataservice:created': t('a créé le service de données'),
|
|
18
18
|
'dataservice:updated': t('a mis à jour le service de données'),
|
|
19
19
|
'dataservice:deleted': t('a supprimé le service de données'),
|
package/src/functions/api.ts
CHANGED
|
@@ -26,7 +26,7 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
26
26
|
const error: Ref<ErrorT | null> = ref(null)
|
|
27
27
|
const status = ref<AsyncDataRequestStatus>('idle')
|
|
28
28
|
|
|
29
|
-
const execute = async () => {
|
|
29
|
+
const execute = async (signal?: AbortSignal) => {
|
|
30
30
|
const urlValue = toValue(url)
|
|
31
31
|
if (!urlValue) return
|
|
32
32
|
const params = deepToValue(options?.params)
|
|
@@ -36,13 +36,15 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
36
36
|
data.value = isRaw
|
|
37
37
|
// Raw targets another data.gouv service (the Tabular API in TabularExplorer) via its own
|
|
38
38
|
// absolute URL, so it must stay bare ofetch: no datagouv apiBase, no datagouv auth attached.
|
|
39
|
-
? await ofetch<DataT | null>(urlValue, { params: params ?? query })
|
|
39
|
+
? await ofetch<DataT | null>(urlValue, { params: params ?? query, signal })
|
|
40
40
|
// The configured `$fetch` carries baseURL + auth + headers (see the `datagouv` plugin install
|
|
41
41
|
// for the default one). We only forward the URL and params here.
|
|
42
|
-
: await config.$fetch<DataT | null>(urlValue, { baseURL: config.apiBase, params: params ?? query })
|
|
42
|
+
: await config.$fetch<DataT | null>(urlValue, { baseURL: config.apiBase, params: params ?? query, signal })
|
|
43
43
|
status.value = 'success'
|
|
44
44
|
}
|
|
45
45
|
catch (e) {
|
|
46
|
+
// Ignore aborted requests — they're superseded by a newer call, not a real error.
|
|
47
|
+
if (signal?.aborted) return
|
|
46
48
|
error.value = e as ErrorT
|
|
47
49
|
status.value = 'error'
|
|
48
50
|
}
|
|
@@ -51,14 +53,18 @@ export async function useFetch<DataT, ErrorT = never>(
|
|
|
51
53
|
// When server is false, only start watching after mount (client-side only)
|
|
52
54
|
if (options?.server === false) {
|
|
53
55
|
onMounted(() => {
|
|
54
|
-
watchEffect(
|
|
55
|
-
|
|
56
|
+
watchEffect((onCleanup) => {
|
|
57
|
+
const controller = new AbortController()
|
|
58
|
+
onCleanup(() => controller.abort())
|
|
59
|
+
void execute(controller.signal)
|
|
56
60
|
})
|
|
57
61
|
})
|
|
58
62
|
}
|
|
59
63
|
else {
|
|
60
|
-
watchEffect(
|
|
61
|
-
|
|
64
|
+
watchEffect((onCleanup) => {
|
|
65
|
+
const controller = new AbortController()
|
|
66
|
+
onCleanup(() => controller.abort())
|
|
67
|
+
void execute(controller.signal)
|
|
62
68
|
})
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -91,6 +91,11 @@ export function getResourceTitleId(resource: Resource) {
|
|
|
91
91
|
export const RESOURCE_TYPE = readonly(['main', 'documentation', 'update', 'api', 'code', 'other'] as const)
|
|
92
92
|
export type ResourceType = typeof RESOURCE_TYPE[number]
|
|
93
93
|
|
|
94
|
+
// Page size used by ResourceExplorer for its per-type resource fetches.
|
|
95
|
+
// Exported so consumers (e.g. the dataset page's `main` prefetch) can build a
|
|
96
|
+
// byte-for-byte identical cache key and let Nuxt dedupe the request.
|
|
97
|
+
export const RESOURCE_EXPLORER_PAGE_SIZE = 10
|
|
98
|
+
|
|
94
99
|
export const getResourceLabel = (type: ResourceType, count?: number) => {
|
|
95
100
|
const { t } = useTranslation()
|
|
96
101
|
switch (type) {
|
package/src/functions/tabular.ts
CHANGED
|
@@ -4,9 +4,24 @@ import {
|
|
|
4
4
|
RiPriceTag3Line,
|
|
5
5
|
RiText,
|
|
6
6
|
RiCalendarLine,
|
|
7
|
+
RiCalendarEventLine,
|
|
8
|
+
RiTimeLine,
|
|
7
9
|
RiCheckboxLine,
|
|
10
|
+
RiMapPin2Line,
|
|
11
|
+
RiGlobalLine,
|
|
12
|
+
RiFocus3Line,
|
|
13
|
+
RiFingerprintLine,
|
|
14
|
+
RiMailLine,
|
|
15
|
+
RiLinksLine,
|
|
16
|
+
RiPhoneLine,
|
|
17
|
+
RiUserLine,
|
|
18
|
+
RiMoneyEuroCircleLine,
|
|
19
|
+
RiPercentLine,
|
|
20
|
+
RiBracesLine,
|
|
21
|
+
RiCodeLine,
|
|
8
22
|
} from '@remixicon/vue'
|
|
9
23
|
import { useTranslation } from '../composables/useTranslation'
|
|
24
|
+
import type { TranslationFunction } from '../composables/useTranslation'
|
|
10
25
|
import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
|
|
11
26
|
|
|
12
27
|
export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
|
|
@@ -15,7 +30,12 @@ export function hasFilterForColumn(filters: Record<string, ColumnFilters>, colum
|
|
|
15
30
|
return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null)
|
|
16
31
|
}
|
|
17
32
|
|
|
18
|
-
export
|
|
33
|
+
export type TypeDisplay = {
|
|
34
|
+
icon: Component
|
|
35
|
+
label: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, TypeDisplay> {
|
|
19
39
|
return {
|
|
20
40
|
number: { icon: RiHashtag, label: t('Nombre') },
|
|
21
41
|
categorical: { icon: RiPriceTag3Line, label: t('Catégoriel') },
|
|
@@ -25,6 +45,88 @@ export function buildTypeConfig(t: (s: string) => string): Record<ColumnType, {
|
|
|
25
45
|
}
|
|
26
46
|
}
|
|
27
47
|
|
|
48
|
+
// csv-detective formats that carry no semantic meaning beyond the generic
|
|
49
|
+
// display type already resolved by `ColumnType` — these fall back to
|
|
50
|
+
// `buildTypeConfig` (Texte / Nombre / Date / Booléen) in the header.
|
|
51
|
+
export const GENERIC_FORMATS = new Set(['string', 'int', 'float', 'date', 'bool', 'booleen'])
|
|
52
|
+
|
|
53
|
+
// Semantic types detected by csv-detective (see ../../../csv-detective/csv_detective/formats).
|
|
54
|
+
// The list is intentionally exhaustive, but unknown formats are tolerated:
|
|
55
|
+
// `getColumnDisplay` humanizes any format missing from this map.
|
|
56
|
+
export function buildFormatConfig(t: TranslationFunction): Record<string, TypeDisplay> {
|
|
57
|
+
return {
|
|
58
|
+
// Geo — territories & addresses
|
|
59
|
+
adresse: { icon: RiMapPin2Line, label: t('Adresse') },
|
|
60
|
+
commune: { icon: RiMapPin2Line, label: t('Commune') },
|
|
61
|
+
code_commune: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
|
|
62
|
+
code_commune_insee: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
|
|
63
|
+
departement: { icon: RiMapPin2Line, label: t('Département') },
|
|
64
|
+
code_departement: { icon: RiMapPin2Line, label: t('Code département') },
|
|
65
|
+
region: { icon: RiMapPin2Line, label: t('Région') },
|
|
66
|
+
code_region: { icon: RiMapPin2Line, label: t('Code région') },
|
|
67
|
+
code_epci: { icon: RiMapPin2Line, label: t('Code EPCI') },
|
|
68
|
+
code_postal: { icon: RiMapPin2Line, label: t('Code postal') },
|
|
69
|
+
insee_canton: { icon: RiMapPin2Line, label: t('Canton') },
|
|
70
|
+
code_fantoir: { icon: RiMapPin2Line, label: t('Code FANTOIR') },
|
|
71
|
+
id_rnb: { icon: RiMapPin2Line, label: t('Identifiant RNB') },
|
|
72
|
+
// Geo — countries
|
|
73
|
+
pays: { icon: RiGlobalLine, label: t('Pays') },
|
|
74
|
+
iso_country_code_alpha2: { icon: RiGlobalLine, label: t('Code pays (ISO α-2)') },
|
|
75
|
+
iso_country_code_alpha3: { icon: RiGlobalLine, label: t('Code pays (ISO α-3)') },
|
|
76
|
+
iso_country_code_numeric: { icon: RiGlobalLine, label: t('Code pays (ISO num.)') },
|
|
77
|
+
// Geo — coordinates
|
|
78
|
+
latitude_wgs: { icon: RiFocus3Line, label: t('Latitude') },
|
|
79
|
+
latitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Latitude') },
|
|
80
|
+
longitude_wgs: { icon: RiFocus3Line, label: t('Longitude') },
|
|
81
|
+
longitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Longitude') },
|
|
82
|
+
latitude_l93: { icon: RiFocus3Line, label: t('Latitude (Lambert 93)') },
|
|
83
|
+
longitude_l93: { icon: RiFocus3Line, label: t('Longitude (Lambert 93)') },
|
|
84
|
+
latlon_wgs: { icon: RiFocus3Line, label: t('Latitude / longitude') },
|
|
85
|
+
lonlat_wgs: { icon: RiFocus3Line, label: t('Longitude / latitude') },
|
|
86
|
+
// Identifiers
|
|
87
|
+
siren: { icon: RiFingerprintLine, label: t('SIREN') },
|
|
88
|
+
siret: { icon: RiFingerprintLine, label: t('SIRET') },
|
|
89
|
+
code_rna: { icon: RiFingerprintLine, label: t('Identifiant RNA') },
|
|
90
|
+
code_waldec: { icon: RiFingerprintLine, label: t('Identifiant WALDEC') },
|
|
91
|
+
code_import: { icon: RiFingerprintLine, label: t('Code Import') },
|
|
92
|
+
uai: { icon: RiFingerprintLine, label: t('UAI') },
|
|
93
|
+
insee_ape700: { icon: RiFingerprintLine, label: t('Code APE') },
|
|
94
|
+
code_csp_insee: { icon: RiFingerprintLine, label: t('Code CSP') },
|
|
95
|
+
uuid: { icon: RiFingerprintLine, label: t('UUID') },
|
|
96
|
+
mongo_object_id: { icon: RiFingerprintLine, label: t('Identifiant Mongo') },
|
|
97
|
+
// Contact
|
|
98
|
+
email: { icon: RiMailLine, label: t('Email') },
|
|
99
|
+
url: { icon: RiLinksLine, label: t('URL') },
|
|
100
|
+
tel_fr: { icon: RiPhoneLine, label: t('Téléphone') },
|
|
101
|
+
username: { icon: RiUserLine, label: t('Nom d\'utilisateur') },
|
|
102
|
+
// People
|
|
103
|
+
sexe: { icon: RiUserLine, label: t('Sexe') },
|
|
104
|
+
csp_insee: { icon: RiUserLine, label: t('Catégorie socio-pro.') },
|
|
105
|
+
// Quantities
|
|
106
|
+
money: { icon: RiMoneyEuroCircleLine, label: t('Montant') },
|
|
107
|
+
percent: { icon: RiPercentLine, label: t('Pourcentage') },
|
|
108
|
+
// Temporal
|
|
109
|
+
year: { icon: RiCalendarLine, label: t('Année') },
|
|
110
|
+
date_fr: { icon: RiCalendarLine, label: t('Date') },
|
|
111
|
+
jour_de_la_semaine: { icon: RiCalendarEventLine, label: t('Jour de la semaine') },
|
|
112
|
+
mois_de_lannee: { icon: RiCalendarEventLine, label: t('Mois de l\'année') },
|
|
113
|
+
datetime_naive: { icon: RiTimeLine, label: t('Date et heure') },
|
|
114
|
+
datetime_aware: { icon: RiTimeLine, label: t('Date et heure') },
|
|
115
|
+
datetime_rfc822: { icon: RiTimeLine, label: t('Date et heure') },
|
|
116
|
+
// Structured
|
|
117
|
+
json: { icon: RiBracesLine, label: t('JSON') },
|
|
118
|
+
geojson: { icon: RiBracesLine, label: t('GeoJSON') },
|
|
119
|
+
binary: { icon: RiCodeLine, label: t('Binaire') },
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Tolerant fallback for a semantic format we don't explicitly know about:
|
|
124
|
+
// "code_truc_machin" -> "Code truc machin".
|
|
125
|
+
export function humanizeFormat(format: string): string {
|
|
126
|
+
const spaced = format.replace(/_/g, ' ').trim()
|
|
127
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1)
|
|
128
|
+
}
|
|
129
|
+
|
|
28
130
|
export function useFormatTabular() {
|
|
29
131
|
const { locale } = useTranslation()
|
|
30
132
|
|
package/src/main.ts
CHANGED
|
@@ -40,6 +40,9 @@ import BrandedButton from './components/BrandedButton.vue'
|
|
|
40
40
|
import CopyButton from './components/CopyButton.vue'
|
|
41
41
|
import DataserviceCard from './components/DataserviceCard.vue'
|
|
42
42
|
import DatasetCard from './components/DatasetCard.vue'
|
|
43
|
+
import DataStructure from './components/ResourceAccordion/DataStructure.vue'
|
|
44
|
+
import Downloads from './components/ResourceAccordion/Downloads.vue'
|
|
45
|
+
import Metadata from './components/ResourceAccordion/Metadata.vue'
|
|
43
46
|
import DescriptionListTerm from './components/DescriptionListTerm.vue'
|
|
44
47
|
import DescriptionListDetails from './components/DescriptionListDetails.vue'
|
|
45
48
|
import DiscussionMessageCard from './components/DiscussionMessageCard.vue'
|
|
@@ -60,6 +63,7 @@ import LoadingBlock from './components/LoadingBlock.vue'
|
|
|
60
63
|
import MarkdownViewer from './components/MarkdownViewer.vue'
|
|
61
64
|
import OrganizationCard from './components/OrganizationCard.vue'
|
|
62
65
|
import OrganizationHorizontalCard from './components/OrganizationHorizontalCard.vue'
|
|
66
|
+
import ObjectCardOwner from './components/ObjectCardOwner.vue'
|
|
63
67
|
import OrganizationLogo from './components/OrganizationLogo.vue'
|
|
64
68
|
import OrganizationNameWithCertificate from './components/OrganizationNameWithCertificate.vue'
|
|
65
69
|
import OwnerType from './components/OwnerType.vue'
|
|
@@ -73,6 +77,7 @@ import PostCard from './components/PostCard.vue'
|
|
|
73
77
|
import ReadMore from './components/ReadMore.vue'
|
|
74
78
|
import ResourceAccordion from './components/ResourceAccordion/ResourceAccordion.vue'
|
|
75
79
|
import ResourceIcon from './components/ResourceAccordion/ResourceIcon.vue'
|
|
80
|
+
import ResourceSelector from './components/ResourceExplorer/ResourceSelector.vue'
|
|
76
81
|
import ResourceExplorer from './components/ResourceExplorer/ResourceExplorer.vue'
|
|
77
82
|
import ResourceExplorerSidebar from './components/ResourceExplorer/ResourceExplorerSidebar.vue'
|
|
78
83
|
import ResourceExplorerViewer from './components/ResourceExplorer/ResourceExplorerViewer.vue'
|
|
@@ -82,9 +87,6 @@ import ReuseHorizontalCard from './components/ReuseHorizontalCard.vue'
|
|
|
82
87
|
import ReuseDetails from './components/ReuseDetails.vue'
|
|
83
88
|
import SchemaCard from './components/SchemaCard.vue'
|
|
84
89
|
import SimpleBanner from './components/SimpleBanner.vue'
|
|
85
|
-
import SmallChart from './components/SmallChart.vue'
|
|
86
|
-
import ChartViewer from './components/Chart/ChartViewer.vue'
|
|
87
|
-
import ChartViewerWrapper from './components/Chart/ChartViewerWrapper.vue'
|
|
88
90
|
import StatBox from './components/StatBox.vue'
|
|
89
91
|
import Tab from './components/Tabs/Tab.vue'
|
|
90
92
|
import TabGroup from './components/Tabs/TabGroup.vue'
|
|
@@ -115,6 +117,8 @@ export * from './composables/useMetrics'
|
|
|
115
117
|
export * from './composables/useReuseType'
|
|
116
118
|
export * from './composables/useTranslation'
|
|
117
119
|
export * from './composables/useHasTabularData'
|
|
120
|
+
export * from './composables/useResourceCapabilities'
|
|
121
|
+
export * from './composables/useTabularProfile'
|
|
118
122
|
|
|
119
123
|
export * from './functions/activities'
|
|
120
124
|
export * from './functions/datasets'
|
|
@@ -326,6 +330,9 @@ export {
|
|
|
326
330
|
CopyButton,
|
|
327
331
|
DataserviceCard,
|
|
328
332
|
DatasetCard,
|
|
333
|
+
DataStructure,
|
|
334
|
+
Downloads,
|
|
335
|
+
Metadata,
|
|
329
336
|
DatasetInformationSection,
|
|
330
337
|
DatasetTemporalitySection,
|
|
331
338
|
DatasetSpatialSection,
|
|
@@ -365,15 +372,14 @@ export {
|
|
|
365
372
|
ResourceExplorer,
|
|
366
373
|
ResourceExplorerSidebar,
|
|
367
374
|
ResourceExplorerViewer,
|
|
375
|
+
ObjectCardOwner,
|
|
368
376
|
ResourceIcon,
|
|
377
|
+
ResourceSelector,
|
|
369
378
|
ReuseCard,
|
|
370
379
|
ReuseDetails,
|
|
371
380
|
ReuseHorizontalCard,
|
|
372
381
|
SchemaCard,
|
|
373
382
|
SimpleBanner,
|
|
374
|
-
SmallChart,
|
|
375
|
-
ChartViewer,
|
|
376
|
-
ChartViewerWrapper,
|
|
377
383
|
StatBox,
|
|
378
384
|
OpenApiViewer,
|
|
379
385
|
Tab,
|
package/src/types/resources.ts
CHANGED