@datagouv/components-next 1.0.2-dev.52 → 1.0.2-dev.54

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.
Files changed (41) hide show
  1. package/assets/main.css +4 -0
  2. package/dist/{Datafair.client-CBE0uebV.js → Datafair.client-qm_JoZUL.js} +1 -1
  3. package/dist/{JsonPreview.client-B-kw70ep.js → JsonPreview.client-BpovqdDN.js} +2 -2
  4. package/dist/{MapContainer.client-dUUInlHa.js → MapContainer.client-6Y5RJxtw.js} +2 -2
  5. package/dist/{PdfPreview.client-Y2qTpKFd.js → PdfPreview.client-Drv5EwJe.js} +2 -2
  6. package/dist/{Pmtiles.client-BOPJ59Cq.js → Pmtiles.client-B3dUb4iS.js} +1 -1
  7. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-DtgGpKmM.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-BmRAxeK4.js} +1 -1
  8. package/dist/{XmlPreview.client-CtW1m4O8.js → XmlPreview.client-CXF1N-AI.js} +3 -3
  9. package/dist/components-next.css +1 -1
  10. package/dist/components-next.js +144 -145
  11. package/dist/components.css +1 -1
  12. package/dist/{index-B8siUkxs.js → index-lCAbcwQm.js} +1 -1
  13. package/dist/{main-DaWGX8hL.js → main-5ZJvZtsQ.js} +31672 -52962
  14. package/dist/{vue3-xml-viewer.common-S9Mg9vGE.js → vue3-xml-viewer.common-X_gxbf2s.js} +1 -1
  15. package/package.json +1 -3
  16. package/src/components/InfiniteLoader.vue +53 -0
  17. package/src/components/ResourceAccordion/Preview.vue +11 -11
  18. package/src/components/ResourceAccordion/ResourceAccordion.vue +1 -1
  19. package/src/components/ResourceExplorer/ResourceExplorer.vue +60 -3
  20. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  21. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +4 -3
  22. package/src/components/Search/GlobalSearch.vue +45 -4
  23. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  24. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  25. package/src/components/TabularExplorer/TabularExplorer.vue +870 -0
  26. package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
  27. package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
  28. package/src/components/TabularExplorer/types.ts +83 -0
  29. package/src/composables/useHasTabularData.ts +1 -9
  30. package/src/composables/useSearchFilter.ts +90 -0
  31. package/src/composables/useStableQueryParams.ts +28 -3
  32. package/src/functions/api.ts +34 -33
  33. package/src/functions/api.types.ts +1 -0
  34. package/src/functions/tabular.ts +60 -0
  35. package/src/functions/tabularApi.ts +9 -137
  36. package/src/main.ts +9 -27
  37. package/src/components/Chart/ChartViewer.vue +0 -147
  38. package/src/components/Chart/ChartViewerWrapper.vue +0 -224
  39. package/src/components/Form/Listbox.vue +0 -101
  40. package/src/functions/charts.ts +0 -68
  41. package/src/types/visualizations.ts +0 -89
@@ -1,224 +0,0 @@
1
- <template>
2
- <LoadingBlock
3
- :status="status"
4
- :data="series"
5
- >
6
- <template #default="{ data }">
7
- <ChartViewer
8
- :chart="chart"
9
- :series="data"
10
- />
11
- </template>
12
- <template #error>
13
- <div class="text-center py-8 text-gray-500">
14
- Une erreur est survenue lors du chargement des données du graphique.
15
- </div>
16
- </template>
17
- </LoadingBlock>
18
- </template>
19
-
20
- <script setup lang="ts">
21
- import { reactive, ref, watch } from 'vue'
22
- import ChartViewer from './ChartViewer.vue'
23
- import LoadingBlock from '../../components/LoadingBlock.vue'
24
- import { useComponentsConfig } from '../../config'
25
- import { fetchTabularData, useGetProfile, type TabularDataResponse, type TabularProfileResponse } from '../../functions/tabularApi'
26
- import type { Chart, ChartForApi } from '../../types/visualizations'
27
-
28
- const props = defineProps<{
29
- chart: Chart | ChartForApi
30
- loadAllPages?: boolean
31
- }>()
32
-
33
- const emit = defineEmits<{
34
- columns: [columns: Record<string, Array<string>>]
35
- }>()
36
-
37
- const config = useComponentsConfig()
38
- const getProfile = useGetProfile()
39
-
40
- // Loading and error states
41
- const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle')
42
- const error = ref<Error | null>(null)
43
-
44
- // Dataset source for the chart
45
- const series = reactive<{
46
- data: Record<string, Array<Record<string, unknown>>>
47
- columns: Record<string, Array<string>>
48
- page: Record<string, number>
49
- hasNextPage: Record<string, boolean>
50
- }>({
51
- data: {},
52
- columns: {},
53
- page: {},
54
- hasNextPage: {},
55
- })
56
-
57
- async function fetchSeriesProfile() {
58
- status.value = 'pending'
59
- error.value = null
60
-
61
- try {
62
- if (props.chart.series.length === 0) {
63
- status.value = 'success'
64
- series.data = {}
65
- series.columns = {}
66
- return
67
- }
68
-
69
- // Fetch data for all series in parallel
70
- const fetchPromises = props.chart.series.map(async (serie) => {
71
- if (!serie.resource_id) return
72
- return {
73
- id: serie.resource_id,
74
- profile: await getProfile(serie.resource_id),
75
- }
76
- }).filter(Boolean) as Array<Promise<{
77
- id: string
78
- profile: TabularProfileResponse
79
- }>>
80
-
81
- const results = (await Promise.allSettled(fetchPromises))
82
- .filter(r => r.status === 'fulfilled')
83
- .map(r => r.value)
84
- series.columns = Object.fromEntries(results.map(result => [
85
- result.id,
86
- result.profile.profile.header,
87
- ]))
88
- status.value = 'success'
89
- }
90
- catch (err) {
91
- error.value = err instanceof Error ? err : new Error('Failed to fetch series profile')
92
- status.value = 'error'
93
- console.log(err)
94
- series.columns = {}
95
- }
96
- }
97
-
98
- async function loadMorePages() {
99
- for (const serie of props.chart.series) {
100
- const xColumn = serie.column_x_name_override ?? props.chart.x_axis.column_x
101
- const resourceId = serie.resource_id
102
- if (!xColumn || !resourceId || !serie.column_y) continue
103
-
104
- // Check if there's more data to load for this series
105
- if (!series.hasNextPage[resourceId]) {
106
- continue
107
- }
108
-
109
- // Calculate the next page to fetch
110
- const nextPage = (series.page[resourceId] || 0) + 1
111
-
112
- const response = await fetchTabularData(config, {
113
- columns: serie.aggregate_y ? undefined : [xColumn, serie.column_y],
114
- resourceId,
115
- page: nextPage,
116
- pageSize: 100,
117
- groupBy: xColumn,
118
- aggregation: serie.column_y && serie.aggregate_y
119
- ? {
120
- column: serie.column_y,
121
- type: serie.aggregate_y,
122
- }
123
- : undefined,
124
- filters: serie.filters ?? undefined,
125
- })
126
-
127
- // Update the page tracker
128
- series.page[resourceId] = nextPage
129
-
130
- if (!series.data[resourceId]) {
131
- series.data[resourceId] = []
132
- }
133
- series.data[resourceId] = [...series.data[resourceId], ...response.data]
134
-
135
- // Update hasNextPage based on the API response
136
- series.hasNextPage[resourceId] = !!response.links.next
137
- }
138
- }
139
-
140
- async function fetchSeriesData() {
141
- status.value = 'pending'
142
- error.value = null
143
-
144
- try {
145
- if (props.chart.series.length === 0 || !props.chart.x_axis.column_x) {
146
- status.value = 'success'
147
- series.data = {}
148
- return
149
- }
150
-
151
- const fetchPromises = props.chart.series.map(async (serie) => {
152
- const xColumn = serie.column_x_name_override ?? props.chart.x_axis.column_x
153
-
154
- if (!xColumn || !serie.resource_id || !serie.column_y) return
155
- return {
156
- id: serie.resource_id,
157
- data: await fetchTabularData(config, {
158
- columns: serie.aggregate_y ? undefined : [xColumn, serie.column_y],
159
- resourceId: serie.resource_id,
160
- page: 1,
161
- pageSize: 100,
162
- groupBy: xColumn,
163
- aggregation: serie.column_y && serie.aggregate_y
164
- ? {
165
- column: serie.column_y,
166
- type: serie.aggregate_y,
167
- }
168
- : undefined,
169
- filters: serie.filters ?? undefined,
170
- }),
171
- }
172
- }).filter(Boolean) as Array<Promise<{
173
- id: string
174
- data: TabularDataResponse
175
- }>>
176
-
177
- const results = (await Promise.allSettled(fetchPromises))
178
- .filter(r => r.status === 'fulfilled')
179
- .map(r => r.value)
180
- // Transform data into echarts format
181
- series.data = Object.fromEntries(results.map(result => [
182
- result.id,
183
- result.data.data,
184
- ]))
185
-
186
- // Reset page tracking for each series to 0 (page 1 has been loaded)
187
- // Also initialize hasNextPage based on the response
188
- for (const result of results) {
189
- const serie = props.chart.series.find(s => s.resource_id === result.id)
190
- if (serie) {
191
- series.page[result.id] = 0
192
- series.hasNextPage[result.id] = !!result.data.links.next
193
- }
194
- }
195
-
196
- // If loadAllPages is true, fetch the next page (loadMorePages can be called again for more)
197
- // This allows progressive loading - first page loads quickly, then more can be loaded on demand
198
- if (props.loadAllPages) {
199
- await loadMorePages()
200
- }
201
-
202
- status.value = 'success'
203
- }
204
- catch (err) {
205
- error.value = err instanceof Error ? err : new Error('Failed to fetch series data')
206
- status.value = 'error'
207
- series.data = {}
208
- }
209
- }
210
-
211
- // Watch for changes in the chart or its series
212
- watch(() => props.chart.series, async () => {
213
- await fetchSeriesProfile()
214
- }, { immediate: true, deep: true })
215
-
216
- // Watch for changes in the chart or its series
217
- watch([() => props.chart.series, () => props.chart.x_axis.column_x], async () => {
218
- await fetchSeriesData()
219
- }, { immediate: true, deep: true })
220
-
221
- watch(() => series.columns, () => {
222
- emit('columns', series.columns)
223
- })
224
- </script>
@@ -1,101 +0,0 @@
1
- <template>
2
- <Listbox v-model="model">
3
- <div class="relative min-w-0">
4
- <div
5
- ref="floatingReference"
6
- class="relative w-full cursor-default overflow-hidden bg-white text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm"
7
- >
8
- <ListboxButton class="input shadow-input text-sm flex items-center gap-2">
9
- <slot name="button">
10
- <div class="w-full flex items-center justify-between gap-2">
11
- <div
12
- class="truncate"
13
- :class="{ 'text-new-disabled-text': isDisabled(model) }"
14
- >
15
- {{ displayValue(model) }}
16
- </div>
17
- <RiArrowDownSLine class="flex-none size-4 justify-self-end" />
18
- </div>
19
- </slot>
20
- </ListboxButton>
21
- </div>
22
-
23
- <ListboxOptions
24
- ref="popover"
25
- :style="floatingStyles"
26
- class="z-10 mt-1 absolute max-h-60 min-w-80 w-full overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm pl-0"
27
- >
28
- <ListboxOption
29
- v-for="option in options"
30
- :key="getOptionId(toValue(option))"
31
- v-slot="{ active, selected }"
32
- as="template"
33
- :value="option"
34
- >
35
- <li
36
- class="relative cursor-default select-none py-2 pr-4 list-none flex items-center gap-2 text-gray-900"
37
- :class="{
38
- 'bg-gray-lower': active && !isDisabled(toValue(option)),
39
- 'text-new-disabled-text': isDisabled(toValue(option)),
40
- 'pl-2': selected,
41
- 'pl-6': !selected,
42
- }"
43
- >
44
- <div class="flex items-center justify-center aspect-square">
45
- <RiCheckLine
46
- v-if="selected"
47
- class="size-4"
48
- :class="isDisabled(toValue(option)) ?' text-new-disabled-text' : 'text-new-primary'"
49
- />
50
- </div>
51
- <slot
52
- name="option"
53
- v-bind="{ option, active }"
54
- >
55
- {{ displayValue(option) }}
56
- </slot>
57
- </li>
58
- </ListboxOption>
59
- </ListboxOptions>
60
- </div>
61
- </Listbox>
62
- </template>
63
-
64
- <script setup lang="ts" generic="T extends string | number | object">
65
- import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/vue'
66
- import { useFloating, autoUpdate, autoPlacement } from '@floating-ui/vue'
67
- import { toValue, useTemplateRef } from 'vue'
68
- import { RiArrowDownSLine, RiCheckLine } from '@remixicon/vue'
69
-
70
- withDefaults(defineProps<{
71
- options?: Array<T>
72
- getOptionId?: (option: T) => string | number
73
- displayValue: (option: T | null) => string
74
- isDisabled?: (option: T | null) => boolean
75
- }>(), {
76
- getOptionId: (option: T): string | number => {
77
- if (typeof option === 'string') return option
78
- if (typeof option === 'number') return option
79
- if (typeof option === 'object' && 'id' in option) return option.id as string
80
-
81
- throw new Error('Please set getOptionId()')
82
- },
83
- isDisabled: (option: T | null): boolean => {
84
- if (option && typeof option === 'object' && 'disabled' in option) return option.disabled as boolean
85
-
86
- return false
87
- },
88
- })
89
-
90
- const model = defineModel<T | null>({ required: true })
91
-
92
- const referenceRef = useTemplateRef('floatingReference')
93
- const floatingRef = useTemplateRef<InstanceType<typeof ListboxOptions>>('popover')
94
- const { floatingStyles } = useFloating(referenceRef, floatingRef, {
95
- middleware: [autoPlacement({
96
- allowedPlacements: ['bottom-start', 'bottom', 'bottom-end'],
97
- crossAxis: true,
98
- })],
99
- whileElementsMounted: autoUpdate,
100
- })
101
- </script>
@@ -1,68 +0,0 @@
1
- import type { Chart, ChartForm, ChartForApi, Filter } from '../types/visualizations'
2
-
3
- export function toChartForm(chart: Chart) {
4
- const seriesFilter = chart.series[0]?.filters as Filter | null
5
-
6
- return {
7
- title: chart.title,
8
- description: chart.description,
9
- private: chart.private,
10
- owned: chart.organization ? { organization: chart.organization.id, owner: null } : { owner: chart.owner.id, organization: null },
11
- x_axis: {
12
- column_x: chart.x_axis.column_x,
13
- type: chart.x_axis.type,
14
- sort_combined: chart.x_axis.sort_x_by && chart.x_axis.sort_x_direction
15
- ? `${chart.x_axis.sort_x_by}-${chart.x_axis.sort_x_direction}`
16
- : '',
17
- },
18
- y_axis: {
19
- label: chart.y_axis.label || '',
20
- min: chart.y_axis.min,
21
- max: chart.y_axis.max,
22
- unit: chart.y_axis.unit || '',
23
- unit_position: chart.y_axis.unit_position || 'suffix',
24
- },
25
- series: chart.series.map(serie => ({
26
- ...serie,
27
- aggregate_y: serie.aggregate_y || '',
28
- })),
29
- extras: chart.extras,
30
- chart_type: chart.series[0] ? chart.series[0].type : null,
31
- filter: seriesFilter ?? null,
32
- } satisfies ChartForm
33
- }
34
-
35
- export function toChartApi(chartForm: ChartForm): ChartForApi {
36
- const xAxis = {
37
- column_x: chartForm.x_axis.column_x,
38
- type: chartForm.x_axis.type,
39
- sort_x_by: chartForm.x_axis.sort_combined
40
- ? (chartForm.x_axis.sort_combined.split('-')[0] as 'axis_x' | 'axis_y')
41
- : null,
42
- sort_x_direction: chartForm.x_axis.sort_combined
43
- ? (chartForm.x_axis.sort_combined.split('-')[1] as 'asc' | 'desc')
44
- : null,
45
- }
46
-
47
- return {
48
- ...chartForm.owned,
49
- title: chartForm.title,
50
- description: chartForm.description,
51
- private: chartForm.private,
52
- x_axis: xAxis,
53
- y_axis: {
54
- label: chartForm.y_axis.label ?? null,
55
- min: chartForm.y_axis.min ?? null,
56
- max: chartForm.y_axis.max ?? null,
57
- unit: chartForm.y_axis.unit ?? null,
58
- unit_position: chartForm.y_axis.unit_position ?? null,
59
- },
60
- series: chartForm.series.map((serie, index) => ({
61
- ...serie,
62
- type: index === 0 && chartForm.chart_type ? chartForm.chart_type : serie.type,
63
- aggregate_y: serie.aggregate_y || null,
64
- filters: serie.filters || (index === 0 && chartForm.filter ? chartForm.filter : null),
65
- })),
66
- extras: chartForm.extras,
67
- }
68
- }
@@ -1,89 +0,0 @@
1
- import type { TabularAggregateType } from '../functions/tabularApi'
2
- import type { Owned, OwnedWithId } from './owned'
3
-
4
- export type FilterCondition = 'exact' | 'differs' | 'is_null' | 'is_not_null' | 'greater' | 'less' | 'strictly_greater' | 'strictly_less'
5
- export type Filter = {
6
- _cls: 'Filter'
7
- column: string
8
- condition: FilterCondition
9
- value: string | null
10
- }
11
-
12
- export type AndFilters = {
13
- _cls: 'AndFilters'
14
- filters: Array<Filter | AndFilters>
15
- }
16
-
17
- export type GenericFilter = Filter | AndFilters
18
-
19
- export type XAxisType = 'discrete' | 'continuous'
20
-
21
- export type XAxisSortBy = 'axis_x' | 'axis_y'
22
-
23
- export type SortDirection = 'asc' | 'desc'
24
-
25
- export type XAxis = {
26
- column_x: string
27
- sort_x_by: XAxisSortBy | null
28
- sort_x_direction: SortDirection | null
29
- type: XAxisType
30
- }
31
-
32
- export type CombinedSort = '' | 'axis_x-asc' | 'axis_x-desc' | 'axis_y-asc' | 'axis_y-desc'
33
-
34
- export type XAxisForm = Omit<XAxis, 'sort_x_by' | 'sort_x_direction'> & { sort_combined: CombinedSort }
35
-
36
- export type UnitPosition = 'prefix' | 'suffix'
37
-
38
- export type YAxis = {
39
- min: number | null
40
- max: number | null
41
- label: string | null
42
- unit: string | null
43
- unit_position: UnitPosition | null
44
- }
45
-
46
- export type DataSeriesType = 'line' | 'histogram'
47
-
48
- export type DataSeries = {
49
- type: DataSeriesType
50
- column_y: string
51
- aggregate_y: TabularAggregateType | null
52
- resource_id: string
53
- column_x_name_override: string | null
54
- filters: GenericFilter | null
55
- }
56
-
57
- // Type for form where aggregate_y can be empty string for select binding
58
- export type DataSeriesForm = Omit<DataSeries, 'aggregate_y'> & { aggregate_y: TabularAggregateType | '' | null }
59
-
60
- export type Chart = Owned & {
61
- id: string
62
- title: string
63
- slug: string
64
- description: string
65
- private: boolean
66
- created_at: string
67
- last_modified: string
68
- deleted_at: string | null
69
- uri: string
70
- page: string
71
- x_axis: XAxis
72
- y_axis: YAxis
73
- series: Array<DataSeries>
74
- extras: Record<string, unknown>
75
- permissions: { delete: boolean, edit: boolean, read: boolean }
76
- metrics: {
77
- views: number
78
- }
79
- }
80
-
81
- export type ChartForApi = OwnedWithId & Pick<Chart, 'title' | 'description' | 'private' | 'x_axis' | 'y_axis' | 'series' | 'extras'>
82
-
83
- export type ChartForm = Omit<ChartForApi, 'x_axis' | 'series' | 'owner' | 'organization'> & {
84
- owned: OwnedWithId
85
- x_axis: XAxisForm
86
- series: Array<DataSeriesForm>
87
- chart_type?: DataSeriesType | null
88
- filter: GenericFilter | null
89
- }