@datagouv/components-next 1.0.2-dev.18 → 1.0.2-dev.181

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 (151) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/{Datafair.client-Ds3VQrRi.js → Datafair.client-CaxMyNif.js} +9 -9
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/ImagePreview.client-CPmCyisP.js +36 -0
  8. package/dist/JsonPreview.client-CKOU-LBs.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/{MapContainer.client-B2BijMx5.js → MapContainer.client-IGYJWP5m.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-csUebU3A.js} +835 -856
  13. package/dist/Pmtiles.client-CLq7UHBg.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DeoHZXW1.js +60 -0
  15. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  16. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  17. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  18. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  19. package/dist/XmlPreview.client-C1toAbQ0.js +34 -0
  20. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  21. package/dist/components-next.css +1 -52
  22. package/dist/components-next.js +193 -149
  23. package/dist/components.css +2 -2
  24. package/dist/{index-BFQX7Ccz.js → index-CaUlhl3k.js} +21842 -15810
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-ByYGrf8H.js} +185 -187
  26. package/dist/{main-vqhkVhVS.js → main-DD4ieUAo.js} +41500 -73223
  27. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  28. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  29. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  30. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  31. package/dist/{vue3-xml-viewer.common-BcNJo35o.js → vue3-xml-viewer.common-B1KPK4L4.js} +16 -16
  32. package/package.json +47 -38
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +3 -2
  35. package/src/components/AppLink.vue +3 -0
  36. package/src/components/BannerAction.vue +6 -2
  37. package/src/components/Chart/ChartViewer.vue +320 -0
  38. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  39. package/src/components/CopyButton.vue +6 -2
  40. package/src/components/DataserviceCard.vue +9 -0
  41. package/src/components/DataserviceQuality.vue +82 -0
  42. package/src/components/DataserviceQualityInline.vue +31 -0
  43. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  44. package/src/components/DatasetCard.vue +9 -4
  45. package/src/components/DatasetQuality.vue +11 -11
  46. package/src/components/DatasetQualityInline.vue +2 -2
  47. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  48. package/src/components/Form/Listbox.vue +101 -0
  49. package/src/components/Form/SchemaSelect.vue +2 -0
  50. package/src/components/InfiniteLoader.vue +53 -0
  51. package/src/components/ObjectCardHeader.vue +11 -4
  52. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  53. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  54. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  55. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  56. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  57. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  58. package/src/components/OpenApiViewer/openapi.ts +150 -0
  59. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  60. package/src/components/Pagination.vue +8 -5
  61. package/src/components/RadioInput.vue +7 -2
  62. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  63. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  64. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  65. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  66. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  67. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  68. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  69. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  70. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  71. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  72. package/src/components/ResourceAccordion/Preview.vue +11 -11
  73. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  74. package/src/components/ResourceAccordion/ResourceAccordion.vue +23 -114
  75. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  76. package/src/components/ResourceExplorer/ResourceExplorer.vue +102 -210
  77. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +74 -0
  78. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +103 -71
  79. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  80. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  81. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  82. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +115 -0
  83. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  84. package/src/components/ResourceListItem.vue +154 -0
  85. package/src/components/ReuseCard.vue +14 -6
  86. package/src/components/ReuseDetails.vue +2 -2
  87. package/src/components/ReuseHorizontalCard.vue +2 -2
  88. package/src/components/SchemaCard.vue +10 -1
  89. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  90. package/src/components/Search/GlobalSearch.vue +219 -117
  91. package/src/components/Search/SearchInput.vue +5 -4
  92. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  93. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  94. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  95. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  96. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  97. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  98. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  99. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  100. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  101. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  102. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  103. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  104. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  105. package/src/components/TabularExplorer/types.ts +81 -0
  106. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  107. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  108. package/src/composables/useActiveDescendant.ts +5 -0
  109. package/src/composables/useDatasetResources.ts +164 -0
  110. package/src/composables/useHasTabularData.ts +15 -1
  111. package/src/composables/useMetrics.ts +1 -1
  112. package/src/composables/useResizable.ts +46 -0
  113. package/src/composables/useResourceCapabilities.ts +47 -21
  114. package/src/composables/useSearchFilter.ts +120 -0
  115. package/src/composables/useStableQueryParams.ts +38 -6
  116. package/src/composables/useTabularProfile.ts +70 -0
  117. package/src/config.ts +21 -3
  118. package/src/functions/activities.ts +3 -3
  119. package/src/functions/api.ts +20 -42
  120. package/src/functions/api.types.ts +1 -0
  121. package/src/functions/charts.ts +99 -0
  122. package/src/functions/datasets.ts +0 -17
  123. package/src/functions/dates.ts +3 -2
  124. package/src/functions/metrics.ts +6 -4
  125. package/src/functions/resources.ts +136 -1
  126. package/src/functions/schemas.ts +27 -0
  127. package/src/functions/tabular.ts +214 -0
  128. package/src/functions/tabularApi.ts +96 -11
  129. package/src/main.ts +117 -14
  130. package/src/types/dataservices.ts +17 -0
  131. package/src/types/organizations.ts +10 -0
  132. package/src/types/pages.ts +0 -5
  133. package/src/types/posts.ts +2 -2
  134. package/src/types/reports.ts +5 -1
  135. package/src/types/resources.ts +0 -1
  136. package/src/types/reuses.ts +12 -1
  137. package/src/types/search.ts +63 -1
  138. package/src/types/site.ts +5 -3
  139. package/src/types/ui.ts +2 -0
  140. package/src/types/users.ts +2 -1
  141. package/src/types/visualizations.ts +93 -0
  142. package/assets/swagger-themes/newspaper.css +0 -1670
  143. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  144. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  145. package/dist/Swagger.client-B4y0hXME.js +0 -4
  146. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  147. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  148. package/src/functions/pagination.ts +0 -9
  149. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  150. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  151. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -0,0 +1,320 @@
1
+ <template>
2
+ <div
3
+ ref="chartContainer"
4
+ class="w-full min-h-96"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { format, use, type ComposeOption } from 'echarts/core'
10
+ import { CanvasRenderer } from 'echarts/renderers'
11
+ import { LineChart, BarChart, type BarSeriesOption, type LineSeriesOption } from 'echarts/charts'
12
+ import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent } from 'echarts/components'
13
+ import { init, type ECharts as EChartsType } from 'echarts'
14
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
15
+ import { summarize } from '../../functions/helpers'
16
+ import { useFormatDate } from '../../functions/dates'
17
+ import type { Chart, ColumnDefinition, ColumnsDefinition, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
18
+ import { useTranslation } from '../../composables/useTranslation'
19
+
20
+ use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])
21
+
22
+ const props = defineProps<{
23
+ chart: Chart | ChartForApi
24
+ series: {
25
+ data: Record<string, Array<Record<string, unknown>>>
26
+ columns: ColumnsDefinition
27
+ }
28
+ }>()
29
+
30
+ const { locale } = useTranslation()
31
+ const { formatDate } = useFormatDate()
32
+ const chartContainer = ref<HTMLElement | null>(null)
33
+ let echartsInstance: EChartsType | null = null
34
+
35
+ function capture(): string | null {
36
+ return echartsInstance?.getDataURL() ?? null
37
+ }
38
+
39
+ defineExpose({ capture })
40
+
41
+ // ECharts has a dedicated 'time' axis type for continuous date values.
42
+ function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' | 'time' {
43
+ if (!xAxis) return 'category'
44
+ if (xAxis.type !== 'continuous') return 'category'
45
+ return isDateAxis.value ? 'time' : 'value'
46
+ }
47
+
48
+ function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
49
+ return (value: number) => {
50
+ const v = summarize(value)
51
+ if (!yAxis.unit) return v
52
+ if (yAxis.unit_position === 'prefix') return `${yAxis.unit} ${v}`
53
+ return `${v} ${yAxis.unit}`
54
+ }
55
+ }
56
+
57
+ function buildXAxisFormatter(column: ColumnDefinition | null, options?: Intl.DateTimeFormatOptions): ((value: string | number | Date) => string) | undefined {
58
+ if (column?.type !== 'date') return undefined
59
+ return (value: string | number | Date) => {
60
+ if (value === null || value === undefined) return ''
61
+ const date = value instanceof Date ? value : new Date(value)
62
+ if (Number.isNaN(date.getTime())) return String(value)
63
+ return formatDate(date, options ?? { dateStyle: 'short' })
64
+ }
65
+ }
66
+
67
+ function parseXValueAsDate(row: Record<string, unknown>, xColumn: string): Record<string, unknown> {
68
+ const value = row[xColumn]
69
+ if (value === null || value === undefined || value instanceof Date) return row
70
+ if (typeof value === 'string') {
71
+ const date = new Date(value)
72
+ if (!Number.isNaN(date.getTime())) {
73
+ return { ...row, [xColumn]: date }
74
+ }
75
+ }
76
+ return row
77
+ }
78
+
79
+ const xAxisColumn = computed(() => {
80
+ const columnName = props.chart.x_axis.column_x
81
+ if (!columnName) return null
82
+ for (const columns of Object.values(props.series.columns)) {
83
+ const column = columns.find(c => c.name === columnName)
84
+ if (column) return column
85
+ }
86
+ return null
87
+ })
88
+
89
+ const isDateAxis = computed(() => xAxisColumn.value?.type === 'date')
90
+
91
+ const xAxisBounds = computed(() => {
92
+ const columnName = props.chart.x_axis.column_x
93
+ if (!columnName) return { min: undefined, max: undefined }
94
+
95
+ let min: number | undefined
96
+ let max: number | undefined
97
+
98
+ for (const s of props.chart.series) {
99
+ const xColumn = s.column_x_name_override ?? columnName
100
+ const data = props.series.data[s.resource_id]
101
+ if (!data) continue
102
+
103
+ for (const row of data) {
104
+ const value = row[xColumn]
105
+ if (value === null || value === undefined) continue
106
+
107
+ let numericValue: number | undefined
108
+ if (value instanceof Date) {
109
+ numericValue = value.getTime()
110
+ }
111
+ else if (isDateAxis.value && typeof value === 'string') {
112
+ numericValue = new Date(value).getTime()
113
+ }
114
+ else {
115
+ numericValue = Number(value)
116
+ }
117
+
118
+ if (numericValue === undefined || Number.isNaN(numericValue)) continue
119
+
120
+ if (min === undefined || numericValue < min) min = numericValue
121
+ if (max === undefined || numericValue > max) max = numericValue
122
+ }
123
+ }
124
+
125
+ return { min, max }
126
+ })
127
+
128
+ const echartsOption = computed(() => {
129
+ const seriesCount = props.chart.series.length
130
+ if (!props.chart.series || seriesCount === 0) return
131
+
132
+ const seriesArray = props.chart.series.map((s) => {
133
+ const xColumn = s.column_x_name_override ?? props.chart.x_axis.column_x
134
+ const yColumn = s.aggregate_y ? `${s.column_y}__${s.aggregate_y}` : s.column_y
135
+ const resourceId = s.resource_id
136
+
137
+ const resourceColumns = props.series.columns[resourceId]
138
+ if (!xColumn || !yColumn || !resourceId || !s.type || !props.series.data[resourceId] || !resourceColumns) {
139
+ return null
140
+ }
141
+
142
+ const rawData = props.series.data[resourceId]
143
+ const sortedData = isDateAxis.value
144
+ ? rawData.map(row => parseXValueAsDate(row, xColumn))
145
+ : [...rawData]
146
+ const sortBy = props.chart.x_axis.sort_x_by ?? (isDateAxis.value ? 'axis_x' : null)
147
+ const sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'
148
+
149
+ if (sortBy && sortDirection && props.chart.x_axis.column_x) {
150
+ const sortKey = sortBy === 'axis_y' ? yColumn : xColumn
151
+ sortedData.sort((a, b) => {
152
+ const valA = a[sortKey]
153
+ const valB = b[sortKey]
154
+
155
+ const aNullish = valA === null || valA === undefined
156
+ const bNullish = valB === null || valB === undefined
157
+ if (aNullish && bNullish) return 0
158
+ if (aNullish) return sortDirection === 'asc' ? -1 : 1
159
+ if (bNullish) return sortDirection === 'asc' ? 1 : -1
160
+
161
+ if (valA instanceof Date && valB instanceof Date) {
162
+ const timeA = valA.getTime()
163
+ const timeB = valB.getTime()
164
+ if (timeA < timeB) return sortDirection === 'asc' ? -1 : 1
165
+ if (timeA > timeB) return sortDirection === 'asc' ? 1 : -1
166
+ return 0
167
+ }
168
+
169
+ if (typeof valA === 'boolean' && typeof valB === 'boolean') {
170
+ if (valA === valB) return 0
171
+ // false comes before true in ascending order
172
+ if (valB) return sortDirection === 'asc' ? -1 : 1
173
+ return sortDirection === 'asc' ? 1 : -1
174
+ }
175
+
176
+ const numA = Number(valA)
177
+ const numB = Number(valB)
178
+ const bothNumeric = !isNaN(numA) && !isNaN(numB)
179
+
180
+ if (bothNumeric) {
181
+ if (numA < numB) return sortDirection === 'asc' ? -1 : 1
182
+ if (numA > numB) return sortDirection === 'asc' ? 1 : -1
183
+ return 0
184
+ }
185
+
186
+ const strA = String(valA)
187
+ const strB = String(valB)
188
+ return strA.localeCompare(strB, locale, {
189
+ sensitivity: 'base',
190
+ numeric: true,
191
+ }) * (sortDirection === 'asc' ? 1 : -1)
192
+ })
193
+ }
194
+
195
+ return {
196
+ series: {
197
+ type: s.type === 'histogram' ? 'bar' : 'line',
198
+ dimensions: s.aggregate_y ? [xColumn, yColumn] : resourceColumns.map(c => c.name),
199
+ name: yColumn,
200
+ encode: {
201
+ x: xColumn,
202
+ y: yColumn,
203
+ },
204
+ } as LineSeriesOption | BarSeriesOption,
205
+ data: {
206
+ source: sortedData,
207
+ dimensions: s.aggregate_y ? [xColumn, yColumn] : resourceColumns.map(c => c.name),
208
+ },
209
+ }
210
+ })
211
+
212
+ const seriesData = {
213
+ series: [] as Array<LineSeriesOption | BarSeriesOption>,
214
+ data: [] as Array<Record<string, unknown>>,
215
+ }
216
+
217
+ for (const curr of seriesArray) {
218
+ if (!curr) continue
219
+ seriesData.series.push(curr.series)
220
+ seriesData.data.push(curr.data)
221
+ }
222
+
223
+ const xAxisType = mapXAxisType(props.chart.x_axis)
224
+
225
+ return {
226
+ dataset: [...seriesData.data],
227
+ textStyle: {
228
+ fontFamily: 'Marianne, arial, sans-serif',
229
+ },
230
+ color: '#000091',
231
+ tooltip: {
232
+ trigger: 'axis' as const,
233
+ formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
234
+ let tooltip = ''
235
+ const valueFormatter = new Intl.NumberFormat(locale)
236
+ const xAxisFormatter = buildXAxisFormatter(xAxisColumn.value, { dateStyle: 'medium' })
237
+ for (const param of params) {
238
+ const seriesName = param.seriesName
239
+ const axisLabel = xAxisFormatter ? xAxisFormatter(param.axisValueLabel) : param.axisValueLabel
240
+ tooltip += `${format.encodeHTML(String(axisLabel))}: <strong>${valueFormatter.format(Number(param.value[seriesName]))}</strong><br>`
241
+ }
242
+ return tooltip
243
+ },
244
+ },
245
+ legend: {
246
+ show: false,
247
+ bottom: 0,
248
+ },
249
+ grid: {
250
+ top: 60,
251
+ bottom: 40,
252
+ left: 20,
253
+ right: 20,
254
+ containLabel: true,
255
+ },
256
+ xAxis: {
257
+ type: xAxisType,
258
+ name: (props.chart.x_axis as XAxis).column_x,
259
+ min: xAxisType !== 'category' ? xAxisColumn.value?.min ?? xAxisBounds.value.min ?? undefined : undefined,
260
+ max: xAxisType !== 'category' ? xAxisColumn.value?.max ?? xAxisBounds.value.max ?? undefined : undefined,
261
+ axisLabel: xAxisType === 'time'
262
+ ? { formatter: buildXAxisFormatter(xAxisColumn.value) }
263
+ : undefined,
264
+ },
265
+ yAxis: {
266
+ type: 'value' as const,
267
+ name: props.chart.y_axis.label ?? undefined,
268
+ min: props.chart.y_axis.min ?? undefined,
269
+ max: props.chart.y_axis.max ?? undefined,
270
+ axisLabel: {
271
+ formatter: buildYAxisFormatter(props.chart.y_axis),
272
+ },
273
+ },
274
+ series: seriesData.series,
275
+ } satisfies ComposeOption<
276
+ | BarSeriesOption
277
+ | LineSeriesOption
278
+ >
279
+ })
280
+
281
+ onMounted(() => {
282
+ if (chartContainer.value) {
283
+ echartsInstance = init(chartContainer.value)
284
+ updateChart()
285
+ }
286
+ })
287
+
288
+ onUnmounted(() => {
289
+ if (echartsInstance) {
290
+ echartsInstance.dispose()
291
+ echartsInstance = null
292
+ }
293
+ })
294
+
295
+ watch(() => props.chart, updateChart, { deep: true })
296
+
297
+ watch(() => props.series, updateChart, { deep: true })
298
+
299
+ function updateChart() {
300
+ if (!echartsInstance) return
301
+ const option = echartsOption.value
302
+ if (option) {
303
+ echartsInstance.setOption(option, { notMerge: true })
304
+ }
305
+ }
306
+
307
+ const handleResize = () => {
308
+ if (echartsInstance) {
309
+ echartsInstance.resize()
310
+ }
311
+ }
312
+
313
+ onMounted(() => {
314
+ window.addEventListener('resize', handleResize)
315
+ })
316
+
317
+ onUnmounted(() => {
318
+ window.removeEventListener('resize', handleResize)
319
+ })
320
+ </script>
@@ -0,0 +1,182 @@
1
+ <template>
2
+ <LoadingBlock
3
+ :status="status"
4
+ :data="series"
5
+ >
6
+ <template #default="{ data }">
7
+ <ChartViewer
8
+ ref="chartViewerRef"
9
+ :chart="chart"
10
+ :series="data"
11
+ />
12
+ </template>
13
+ <template #error>
14
+ <div class="text-center py-8 text-gray-500">
15
+ {{ t('Une erreur est survenue lors du chargement des données du graphique.') }}
16
+ </div>
17
+ </template>
18
+ </LoadingBlock>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { computed, reactive, ref, watch } from 'vue'
23
+ import ChartViewer from './ChartViewer.vue'
24
+ import LoadingBlock from '../../components/LoadingBlock.vue'
25
+ import { useComponentsConfig } from '../../config'
26
+ import { fetchTabularData, useGetProfile } from '../../functions/tabularApi'
27
+ import type { Chart, ChartForApi, ColumnsDefinition } from '../../types/visualizations'
28
+ import { useTranslation } from '../../composables/useTranslation'
29
+ import { watchDebounced } from '@vueuse/shared'
30
+ import { buildColumnsFromProfile } from '../../functions/charts'
31
+
32
+ const props = defineProps<{
33
+ chart: Chart | ChartForApi
34
+ }>()
35
+
36
+ const { t } = useTranslation()
37
+ const chartViewerRef = ref<InstanceType<typeof ChartViewer> | null>(null)
38
+
39
+ function capture(): string | null {
40
+ return chartViewerRef.value?.capture() ?? null
41
+ }
42
+
43
+ defineExpose({ capture })
44
+ const config = useComponentsConfig()
45
+ const getProfile = useGetProfile()
46
+
47
+ const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle')
48
+ const error = ref<Error | null>(null)
49
+
50
+ const pendingOperations = ref(0)
51
+
52
+ const series = reactive<{
53
+ data: Record<string, Array<Record<string, unknown>>>
54
+ columns: ColumnsDefinition
55
+ }>({
56
+ data: {},
57
+ columns: {},
58
+ })
59
+
60
+ const resourceIds = computed(() => {
61
+ return props.chart.series
62
+ .filter(s => s.resource_id)
63
+ .map(s => s.resource_id)
64
+ })
65
+
66
+ async function fetchSeriesProfile() {
67
+ pendingOperations.value++
68
+ status.value = 'pending'
69
+ error.value = null
70
+
71
+ try {
72
+ if (props.chart.series.length === 0) {
73
+ status.value = 'success'
74
+ series.data = {}
75
+ series.columns = {}
76
+ return
77
+ }
78
+
79
+ const fetchPromises = props.chart.series
80
+ .filter(serie => serie.resource_id)
81
+ .map(async (serie) => {
82
+ return {
83
+ id: serie.resource_id,
84
+ profile: await getProfile(serie.resource_id),
85
+ }
86
+ })
87
+
88
+ const results = (await Promise.allSettled(fetchPromises))
89
+ .filter(r => r.status === 'fulfilled')
90
+ .map(r => r.value)
91
+ series.columns = Object.fromEntries(results.map(result => [
92
+ result.id,
93
+ buildColumnsFromProfile(result.profile),
94
+ ]))
95
+ }
96
+ catch (err) {
97
+ error.value = err instanceof Error ? err : new Error('Failed to fetch series profile')
98
+ status.value = 'error'
99
+ console.error(err)
100
+ series.columns = {}
101
+ series.data = {}
102
+ }
103
+ finally {
104
+ pendingOperations.value--
105
+ }
106
+ }
107
+
108
+ async function fetchSeriesData() {
109
+ pendingOperations.value++
110
+ status.value = 'pending'
111
+ error.value = null
112
+
113
+ try {
114
+ if (props.chart.series.length === 0 || !props.chart.x_axis.column_x) {
115
+ status.value = 'success'
116
+ series.data = {}
117
+ return
118
+ }
119
+
120
+ const fetchPromises = props.chart.series
121
+ .filter((serie) => {
122
+ const xColumn = serie.column_x_name_override ?? props.chart.x_axis.column_x
123
+ return xColumn && serie.resource_id && serie.column_y
124
+ })
125
+ .map(async (serie) => {
126
+ const xColumn = serie.column_x_name_override ?? props.chart.x_axis.column_x
127
+ return {
128
+ id: serie.resource_id,
129
+ data: await fetchTabularData(config, {
130
+ columns: serie.aggregate_y ? undefined : [xColumn, serie.column_y],
131
+ resourceId: serie.resource_id,
132
+ page: 1,
133
+ pageSize: 100,
134
+ groupBy: xColumn,
135
+ aggregation: serie.column_y && serie.aggregate_y
136
+ ? {
137
+ column: serie.column_y,
138
+ type: serie.aggregate_y,
139
+ }
140
+ : undefined,
141
+ filters: serie.filters ?? undefined,
142
+ }),
143
+ }
144
+ })
145
+
146
+ const results = (await Promise.allSettled(fetchPromises))
147
+ .filter(r => r.status === 'fulfilled')
148
+ .map(r => r.value)
149
+ series.data = Object.fromEntries(results.map(result => [
150
+ result.id,
151
+ result.data.data,
152
+ ]))
153
+ }
154
+ catch (err) {
155
+ error.value = err instanceof Error ? err : new Error('Failed to fetch series data')
156
+ status.value = 'error'
157
+ series.data = {}
158
+ }
159
+ finally {
160
+ pendingOperations.value--
161
+ }
162
+ }
163
+
164
+ watch(resourceIds, async () => {
165
+ await fetchSeriesProfile()
166
+ }, { immediate: true })
167
+
168
+ watchDebounced([
169
+ () => props.chart.series,
170
+ () => props.chart.x_axis.column_x,
171
+ ], async () => {
172
+ await fetchSeriesData()
173
+ }, { debounce: config.searchDebounce ?? 300, immediate: true, deep: true })
174
+
175
+ watch(pendingOperations, (count) => {
176
+ if (count === 0) {
177
+ if (error.value === null && status.value === 'pending') {
178
+ status.value = 'success'
179
+ }
180
+ }
181
+ })
182
+ </script>
@@ -17,7 +17,7 @@
17
17
  />
18
18
  <span
19
19
  class="copy-label"
20
- :class="{ 'sr-only': hideLabel }"
20
+ :class="{ 'sr-only': hideLabel || iconOnly }"
21
21
  >{{ copiedLabel }}</span>
22
22
  </span>
23
23
  <span
@@ -32,7 +32,7 @@
32
32
  />
33
33
  <span
34
34
  class="copy-link copy-label"
35
- :class="{ 'sr-only': hideLabel }"
35
+ :class="{ 'sr-only': hideLabel || iconOnly }"
36
36
  >{{ label }}</span>
37
37
  </span>
38
38
  </button>
@@ -49,9 +49,13 @@ const props = withDefaults(defineProps<{
49
49
  label: string
50
50
  copiedLabel: string
51
51
  hideLabel?: boolean
52
+ // Like hideLabel but keeps the default icon and has no bordered box — just the
53
+ // plain copy icon with an sr-only label.
54
+ iconOnly?: boolean
52
55
  reverse?: boolean
53
56
  }>(), {
54
57
  hideLabel: false,
58
+ iconOnly: false,
55
59
  reverse: false,
56
60
  })
57
61
 
@@ -53,6 +53,7 @@
53
53
  <ObjectCardHeader
54
54
  :icon="isTabularApi ? RiSparklingLine : RiTerminalLine"
55
55
  :url="dataserviceUrl || dataservice.self_web_url"
56
+ :title-tag="titleTag"
56
57
  >
57
58
  {{ dataservice.title }}
58
59
  </ObjectCardHeader>
@@ -98,6 +99,11 @@
98
99
  </p>
99
100
  </div>
100
101
  </div>
102
+ <DataserviceQualityInline
103
+ v-if="dataservice.quality"
104
+ :quality="dataservice.quality"
105
+ />
106
+
101
107
  <ObjectCardShortDescription
102
108
  v-if="showDescription"
103
109
  :text="dataservice.description"
@@ -115,6 +121,7 @@ import { useComponentsConfig } from '../config'
115
121
  import { useFormatDate } from '../functions/dates'
116
122
  import { summarize } from '../functions/helpers'
117
123
  import type { Dataservice } from '../types/dataservices'
124
+ import type { TitleTag } from '../types/ui'
118
125
  import { useTranslation } from '../composables/useTranslation'
119
126
  import OrganizationLogo from './OrganizationLogo.vue'
120
127
  import Avatar from './Avatar.vue'
@@ -124,12 +131,14 @@ import ObjectCardBadge from './ObjectCardBadge.vue'
124
131
  import ObjectCardHeader from './ObjectCardHeader.vue'
125
132
  import ObjectCardOwner from './ObjectCardOwner.vue'
126
133
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
134
+ import DataserviceQualityInline from './DataserviceQualityInline.vue'
127
135
 
128
136
  type Props = {
129
137
  dataservice: Dataservice
130
138
  dataserviceUrl?: RouteLocationRaw
131
139
  organizationUrl?: RouteLocationRaw
132
140
  showDescription?: boolean
141
+ titleTag?: TitleTag
133
142
  }
134
143
 
135
144
  const props = withDefaults(defineProps<Props>(), {
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <Toggletip
3
+ :styled-button="false"
4
+ button-class="flex items-center gap-1 rounded-sm p-1.5 -m-1.5 border-transparent -outline-offset-2 hover:bg-gray-some transition-colors w-full text-left"
5
+ :button-props="{ title: t('Qualité des métadonnées') }"
6
+ >
7
+ <QualityScore
8
+ :score="quality.score"
9
+ class="w-full"
10
+ />
11
+ <RiInformationLine
12
+ class="size-5 shrink-0"
13
+ aria-hidden="true"
14
+ />
15
+ <template #toggletip>
16
+ <DataserviceQualityTooltipContent :quality />
17
+ </template>
18
+ </Toggletip>
19
+ <template v-if="!hideWarnings">
20
+ <ul class="list-none pl-0">
21
+ <QualityItemWarning
22
+ :quality-item="quality.has_description"
23
+ :message="t('Description non renseignée')"
24
+ />
25
+ <QualityItemWarning
26
+ :quality-item="quality.has_machine_documentation"
27
+ :message="t('Documentation machine (OpenAPI) manquante')"
28
+ />
29
+ <QualityItemWarning
30
+ :quality-item="quality.has_technical_documentation"
31
+ :message="t('Documentation technique manquante')"
32
+ />
33
+ <QualityItemWarning
34
+ :quality-item="quality.has_business_documentation"
35
+ :message="t('Documentation métier manquante')"
36
+ />
37
+ <QualityItemWarning
38
+ :quality-item="quality.license"
39
+ :message="t('Licence non renseignée')"
40
+ />
41
+ <QualityItemWarning
42
+ :quality-item="quality.has_contact_point"
43
+ :message="t('Point de contact non renseigné')"
44
+ />
45
+ <QualityItemWarning
46
+ :quality-item="quality.has_base_api_url"
47
+ :message="t(`URL de base de l'API non renseignée`)"
48
+ />
49
+ <QualityItemWarning
50
+ :quality-item="quality.availability_documented"
51
+ :message="t('Disponibilité non documentée')"
52
+ />
53
+ <QualityItemWarning
54
+ :quality-item="quality.rate_limiting_documented"
55
+ :message="t(`Limite d'appels non documentée`)"
56
+ />
57
+ <QualityItemWarning
58
+ :quality-item="quality.access_conditions_clear"
59
+ :message="t(`Conditions d'accès non précisées`)"
60
+ />
61
+ </ul>
62
+ </template>
63
+ </template>
64
+
65
+ <script setup lang="ts">
66
+ import { RiInformationLine } from '@remixicon/vue'
67
+ import type { DataserviceQuality } from '../types/dataservices'
68
+ import QualityScore from './QualityScore.vue'
69
+ import QualityItemWarning from './QualityItemWarning.vue'
70
+ import DataserviceQualityTooltipContent from './DataserviceQualityTooltipContent.vue'
71
+ import Toggletip from './Toggletip.vue'
72
+ import { useTranslation } from '../composables/useTranslation'
73
+
74
+ withDefaults(defineProps<{
75
+ quality: DataserviceQuality
76
+ hideWarnings?: boolean
77
+ }>(), {
78
+ hideWarnings: false,
79
+ })
80
+
81
+ const { t } = useTranslation()
82
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div class="m-0 text-sm text-gray-medium">
3
+ <Toggletip
4
+ :styled-button="false"
5
+ button-class="border-transparent -outline-offset-2 inline-flex items-center justify-center hover:bg-gray-lower transition-colors"
6
+ :button-props="{ class: 'relative z-2 gap-1 rounded-sm px-1 -mx-1 group/quality', title: t('Qualité des métadonnées') }"
7
+ >
8
+ <RiInformationLine class="size-3.5 shrink-0" />
9
+ <span class="text-gray-medium text-sm group-hover/quality:underline">{{ t('Métadonnées :') }}</span>
10
+ <QualityScore :score="quality.score" />
11
+ <template #toggletip>
12
+ <DataserviceQualityTooltipContent :quality />
13
+ </template>
14
+ </Toggletip>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { RiInformationLine } from '@remixicon/vue'
20
+ import type { DataserviceQuality } from '../types/dataservices'
21
+ import QualityScore from './QualityScore.vue'
22
+ import DataserviceQualityTooltipContent from './DataserviceQualityTooltipContent.vue'
23
+ import Toggletip from './Toggletip.vue'
24
+ import { useTranslation } from '../composables/useTranslation'
25
+
26
+ defineProps<{
27
+ quality: DataserviceQuality
28
+ }>()
29
+
30
+ const { t } = useTranslation()
31
+ </script>