@datagouv/components-next 1.0.2-dev.163 → 1.0.2-dev.165

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 (53) hide show
  1. package/dist/{Datafair.client-DXPU1V24.js → Datafair.client-B-OG5WtK.js} +1 -1
  2. package/dist/ImagePreview.client-D0c-ifUi.js +36 -0
  3. package/dist/{JsonPreview.client-D1SnGfWG.js → JsonPreview.client-Dwh9sW9M.js} +2 -2
  4. package/dist/{MapContainer.client-DlsaEVGx.js → MapContainer.client-C2AZ7sOB.js} +2 -2
  5. package/dist/{PdfPreview.client-C9OrLR0z.js → PdfPreview.client-8NzNfW8b.js} +2 -2
  6. package/dist/{Pmtiles.client-CyylWfTy.js → Pmtiles.client-BFH4rTB7.js} +5842 -5660
  7. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-CtXZOHXI.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-BOgE441d.js} +7 -10
  8. package/dist/{XmlPreview.client-DoX7RurH.js → XmlPreview.client-DMOUO508.js} +3 -3
  9. package/dist/components-next.css +1 -1
  10. package/dist/components-next.js +179 -171
  11. package/dist/components.css +1 -1
  12. package/dist/{index-Br1sa7wQ.js → index-C9obT9DJ.js} +4392 -4293
  13. package/dist/{leaflet-src-DWG7tmpQ.js → leaflet-src-B4AZU0vr.js} +1 -1
  14. package/dist/{main-BU6kP4dE.js → main-DNoYWfit.js} +16159 -15631
  15. package/dist/{vue3-xml-viewer.common-Cpui-Yi9.js → vue3-xml-viewer.common-Cp7Cu5ME.js} +1 -1
  16. package/package.json +4 -5
  17. package/src/components/Chart/ChartViewer.vue +103 -10
  18. package/src/components/Chart/ChartViewerWrapper.vue +8 -0
  19. package/src/components/DataserviceCard.vue +6 -0
  20. package/src/components/DataserviceQuality.vue +82 -0
  21. package/src/components/DataserviceQualityInline.vue +31 -0
  22. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  23. package/src/components/DatasetQuality.vue +11 -11
  24. package/src/components/DatasetQualityInline.vue +2 -2
  25. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  26. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  27. package/src/components/ResourceAccordion/ResourceAccordion.vue +8 -3
  28. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +15 -1
  29. package/src/components/ReuseCard.vue +2 -2
  30. package/src/components/ReuseDetails.vue +2 -2
  31. package/src/components/ReuseHorizontalCard.vue +2 -2
  32. package/src/components/Search/GlobalSearch.vue +2 -2
  33. package/src/components/TabularExplorer/TabularCell.vue +5 -4
  34. package/src/components/TabularExplorer/TabularCellPopover.vue +1 -1
  35. package/src/components/TabularExplorer/TabularExplorer.vue +168 -49
  36. package/src/components/TabularExplorer/TabularFilterContent.vue +27 -10
  37. package/src/components/TabularExplorer/TabularFilterPopover.vue +1 -0
  38. package/src/components/TabularExplorer/types.ts +3 -3
  39. package/src/composables/useHasTabularData.ts +2 -1
  40. package/src/composables/useResourceCapabilities.ts +27 -14
  41. package/src/config.ts +1 -0
  42. package/src/functions/charts.ts +23 -3
  43. package/src/functions/resources.ts +56 -0
  44. package/src/functions/tabular.ts +44 -1
  45. package/src/main.ts +15 -6
  46. package/src/types/dataservices.ts +15 -0
  47. package/src/types/organizations.ts +10 -0
  48. package/src/types/reuses.ts +12 -1
  49. package/src/types/search.ts +2 -2
  50. package/src/types/visualizations.ts +1 -1
  51. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  52. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  53. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-BU6kP4dE.js";
1
+ import { g as Ke } from "./main-DNoYWfit.js";
2
2
  import We from "vue";
3
3
  function Fe($, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.163",
3
+ "version": "1.0.2-dev.165",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,7 +23,7 @@
23
23
  "src"
24
24
  ],
25
25
  "dependencies": {
26
- "@floating-ui/vue": "^1.1.11",
26
+ "@floating-ui/vue": "^2.0.1",
27
27
  "@headlessui/vue": "^1.7.23",
28
28
  "@remixicon/vue": "^4.9.0",
29
29
  "@types/hast": "^3.0.4",
@@ -31,7 +31,7 @@
31
31
  "@vueuse/core": "^14.3.0",
32
32
  "@vueuse/router": "^14.3.0",
33
33
  "chart.js": "^4.5.1",
34
- "dompurify": "^3.4.7",
34
+ "dompurify": "^3.4.11",
35
35
  "echarts": "^6.1.0",
36
36
  "geopf-extensions-openlayers": "1.0.0-beta.11",
37
37
  "leaflet": "^1.9.4",
@@ -63,7 +63,6 @@
63
63
  "yaml": "^2.9.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@intlify/cli": "^0.13.1",
67
66
  "@tailwindcss/cli": "^4.3.0",
68
67
  "@tailwindcss/typography": "^0.5.19",
69
68
  "@tsconfig/node22": "^22.0.5",
@@ -83,7 +82,7 @@
83
82
  "typescript": "^5.9.3",
84
83
  "vite": "^7.3.3",
85
84
  "vite-plugin-vue-devtools": "^8.1.2",
86
- "vue": "^3.5.35",
85
+ "vue": "^3.5.40",
87
86
  "vue-router": "^5.0.7",
88
87
  "vue-tsc": "^3.3.2"
89
88
  },
@@ -13,7 +13,8 @@ import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, Datas
13
13
  import { init, type ECharts as EChartsType } from 'echarts'
14
14
  import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
15
15
  import { summarize } from '../../functions/helpers'
16
- import type { Chart, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
16
+ import { useFormatDate } from '../../functions/dates'
17
+ import type { Chart, ColumnDefinition, ColumnsDefinition, XAxis, YAxis, XAxisForm, ChartForApi } from '../../types/visualizations'
17
18
  import { useTranslation } from '../../composables/useTranslation'
18
19
 
19
20
  use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DatasetComponent])
@@ -22,17 +23,26 @@ const props = defineProps<{
22
23
  chart: Chart | ChartForApi
23
24
  series: {
24
25
  data: Record<string, Array<Record<string, unknown>>>
25
- columns: Record<string, Array<{ name: string, type: string }>>
26
+ columns: ColumnsDefinition
26
27
  }
27
28
  }>()
28
29
 
29
30
  const { locale } = useTranslation()
31
+ const { formatDate } = useFormatDate()
30
32
  const chartContainer = ref<HTMLElement | null>(null)
31
33
  let echartsInstance: EChartsType | null = null
32
34
 
33
- function mapXAxisType(xAxis: XAxis | XAxisForm): 'category' | 'value' {
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' {
34
43
  if (!xAxis) return 'category'
35
- return xAxis.type === 'continuous' ? 'value' : 'category'
44
+ if (xAxis.type !== 'continuous') return 'category'
45
+ return isDateAxis.value ? 'time' : 'value'
36
46
  }
37
47
 
38
48
  function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefined {
@@ -44,6 +54,77 @@ function buildYAxisFormatter(yAxis: YAxis): ((value: number) => string) | undefi
44
54
  }
45
55
  }
46
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
+
47
128
  const echartsOption = computed(() => {
48
129
  const seriesCount = props.chart.series.length
49
130
  if (!props.chart.series || seriesCount === 0) return
@@ -58,12 +139,15 @@ const echartsOption = computed(() => {
58
139
  return null
59
140
  }
60
141
 
61
- const sortedData = [...props.series.data[resourceId]]
62
- const sortBy = props.chart.x_axis.sort_x_by
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)
63
147
  const sortDirection = props.chart.x_axis.sort_x_direction ?? 'asc'
64
148
 
65
149
  if (sortBy && sortDirection && props.chart.x_axis.column_x) {
66
- const sortKey = sortBy === 'axis_x' ? xColumn : yColumn
150
+ const sortKey = sortBy === 'axis_y' ? yColumn : xColumn
67
151
  sortedData.sort((a, b) => {
68
152
  const valA = a[sortKey]
69
153
  const valB = b[sortKey]
@@ -136,6 +220,8 @@ const echartsOption = computed(() => {
136
220
  seriesData.data.push(curr.data)
137
221
  }
138
222
 
223
+ const xAxisType = mapXAxisType(props.chart.x_axis)
224
+
139
225
  return {
140
226
  dataset: [...seriesData.data],
141
227
  textStyle: {
@@ -146,10 +232,12 @@ const echartsOption = computed(() => {
146
232
  trigger: 'axis' as const,
147
233
  formatter: (params: Array<{ value: Record<string, unknown>, axisValueLabel: string, seriesName: string }>) => {
148
234
  let tooltip = ''
149
- const formatter = new Intl.NumberFormat('fr-FR')
235
+ const valueFormatter = new Intl.NumberFormat(locale)
236
+ const xAxisFormatter = buildXAxisFormatter(xAxisColumn.value, { dateStyle: 'medium' })
150
237
  for (const param of params) {
151
238
  const seriesName = param.seriesName
152
- tooltip += `${format.encodeHTML(param.axisValueLabel)}: <strong>${formatter.format(Number(param.value[seriesName]))}</strong><br>`
239
+ const axisLabel = xAxisFormatter ? xAxisFormatter(param.axisValueLabel) : param.axisValueLabel
240
+ tooltip += `${format.encodeHTML(String(axisLabel))}: <strong>${valueFormatter.format(Number(param.value[seriesName]))}</strong><br>`
153
241
  }
154
242
  return tooltip
155
243
  },
@@ -166,8 +254,13 @@ const echartsOption = computed(() => {
166
254
  containLabel: true,
167
255
  },
168
256
  xAxis: {
169
- type: mapXAxisType(props.chart.x_axis),
257
+ type: xAxisType,
170
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,
171
264
  },
172
265
  yAxis: {
173
266
  type: 'value' as const,
@@ -5,6 +5,7 @@
5
5
  >
6
6
  <template #default="{ data }">
7
7
  <ChartViewer
8
+ ref="chartViewerRef"
8
9
  :chart="chart"
9
10
  :series="data"
10
11
  />
@@ -33,6 +34,13 @@ const props = defineProps<{
33
34
  }>()
34
35
 
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 })
36
44
  const config = useComponentsConfig()
37
45
  const getProfile = useGetProfile()
38
46
 
@@ -99,6 +99,11 @@
99
99
  </p>
100
100
  </div>
101
101
  </div>
102
+ <DataserviceQualityInline
103
+ v-if="dataservice.quality"
104
+ :quality="dataservice.quality"
105
+ />
106
+
102
107
  <ObjectCardShortDescription
103
108
  v-if="showDescription"
104
109
  :text="dataservice.description"
@@ -126,6 +131,7 @@ import ObjectCardBadge from './ObjectCardBadge.vue'
126
131
  import ObjectCardHeader from './ObjectCardHeader.vue'
127
132
  import ObjectCardOwner from './ObjectCardOwner.vue'
128
133
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
134
+ import DataserviceQualityInline from './DataserviceQualityInline.vue'
129
135
 
130
136
  type Props = {
131
137
  dataservice: Dataservice
@@ -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>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <p class="text-sm text-gray-plain font-bold mb-0">
3
+ {{ t("Qualité des métadonnées :") }}
4
+ </p>
5
+ <ul class="list-none pl-0 space-y-2">
6
+ <QualityItem
7
+ :passed="quality.has_description"
8
+ :message-passed="t('Description renseignée')"
9
+ :message-failed="t('Description non renseignée')"
10
+ class="pb-0"
11
+ />
12
+ <QualityItem
13
+ :passed="quality.has_machine_documentation"
14
+ :message-passed="t('Documentation machine (OpenAPI) disponible')"
15
+ :message-failed="t('Documentation machine (OpenAPI) manquante')"
16
+ class="pb-0"
17
+ />
18
+ <QualityItem
19
+ :passed="quality.has_technical_documentation"
20
+ :message-passed="t('Documentation technique renseignée')"
21
+ :message-failed="t('Documentation technique manquante')"
22
+ class="pb-0"
23
+ />
24
+ <QualityItem
25
+ :passed="quality.has_business_documentation"
26
+ :message-passed="t('Documentation métier renseignée')"
27
+ :message-failed="t('Documentation métier manquante')"
28
+ class="pb-0"
29
+ />
30
+ <QualityItem
31
+ :passed="quality.license"
32
+ :message-passed="t('Licence renseignée')"
33
+ :message-failed="t('Licence non renseignée')"
34
+ class="pb-0"
35
+ />
36
+ <QualityItem
37
+ :passed="quality.has_contact_point"
38
+ :message-passed="t('Point de contact renseigné')"
39
+ :message-failed="t('Point de contact non renseigné')"
40
+ class="pb-0"
41
+ />
42
+ <QualityItem
43
+ :passed="quality.has_base_api_url"
44
+ :message-passed="t(`URL de base de l'API renseignée`)"
45
+ :message-failed="t(`URL de base de l'API non renseignée`)"
46
+ class="pb-0"
47
+ />
48
+ <QualityItem
49
+ :passed="quality.availability_documented"
50
+ :message-passed="t('Disponibilité documentée')"
51
+ :message-failed="t('Disponibilité non documentée')"
52
+ class="pb-0"
53
+ />
54
+ <QualityItem
55
+ :passed="quality.rate_limiting_documented"
56
+ :message-passed="t(`Limite d'appels documentée`)"
57
+ :message-failed="t(`Limite d'appels non documentée`)"
58
+ class="pb-0"
59
+ />
60
+ <QualityItem
61
+ :passed="quality.access_conditions_clear"
62
+ :message-passed="t(`Conditions d'accès claires`)"
63
+ :message-failed="t(`Conditions d'accès non précisées`)"
64
+ class="pb-0"
65
+ />
66
+ </ul>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import type { DataserviceQuality } from '../types/dataservices'
71
+ import { useTranslation } from '../composables/useTranslation'
72
+ import QualityItem from './QualityItem.vue'
73
+
74
+ defineProps<{
75
+ quality: DataserviceQuality
76
+ }>()
77
+
78
+ const { t } = useTranslation()
79
+ </script>
@@ -13,7 +13,7 @@
13
13
  {{ t('Qualité des métadonnées:') }}
14
14
  </span>
15
15
  </span>
16
- <DatasetQualityScore
16
+ <QualityScore
17
17
  :score="quality.score"
18
18
  class="w-full"
19
19
  />
@@ -23,35 +23,35 @@
23
23
  </Toggletip>
24
24
  <template v-if="!hideWarnings">
25
25
  <ul class="list-none pl-0">
26
- <DatasetQualityItemWarning
26
+ <QualityItemWarning
27
27
  :quality-item="quality.dataset_description_quality"
28
28
  :message="t('Description des données non renseignée')"
29
29
  />
30
- <DatasetQualityItemWarning
30
+ <QualityItemWarning
31
31
  :quality-item="quality.resources_documentation"
32
32
  :message="t('Documentation des fichiers manquante')"
33
33
  />
34
- <DatasetQualityItemWarning
34
+ <QualityItemWarning
35
35
  :quality-item="quality.license"
36
36
  :message="t('Licence non renseignée')"
37
37
  />
38
- <DatasetQualityItemWarning
38
+ <QualityItemWarning
39
39
  :quality-item="quality.update_frequency && quality.update_fulfilled_in_time"
40
40
  :message="quality.update_frequency ? t('Fréquence de mise à jour non respectée') : t('Fréquence de mise à jour non renseignée')"
41
41
  />
42
- <DatasetQualityItemWarning
42
+ <QualityItemWarning
43
43
  :quality-item="quality.has_open_format"
44
44
  :message="t('Formats de fichiers non standards')"
45
45
  />
46
- <DatasetQualityItemWarning
46
+ <QualityItemWarning
47
47
  :quality-item="quality.temporal_coverage"
48
48
  :message="t('Couverture temporelle non renseignée')"
49
49
  />
50
- <DatasetQualityItemWarning
50
+ <QualityItemWarning
51
51
  :quality-item="quality.spatial"
52
52
  :message="t('Couverture spatiale non renseignée')"
53
53
  />
54
- <DatasetQualityItemWarning
54
+ <QualityItemWarning
55
55
  :quality-item="quality.all_resources_available"
56
56
  :message="t('Certains fichiers ne sont pas disponibles')"
57
57
  />
@@ -62,8 +62,8 @@
62
62
  <script setup lang="ts">
63
63
  import { RiInformationLine } from '@remixicon/vue'
64
64
  import type { Quality } from '../types/datasets'
65
- import DatasetQualityItemWarning from './DatasetQualityItemWarning.vue'
66
- import DatasetQualityScore from './DatasetQualityScore.vue'
65
+ import QualityItemWarning from './QualityItemWarning.vue'
66
+ import QualityScore from './QualityScore.vue'
67
67
  import Toggletip from './Toggletip.vue'
68
68
  import DatasetQualityTooltipContent from './DatasetQualityTooltipContent.vue'
69
69
  import { useTranslation } from '../composables/useTranslation'
@@ -7,7 +7,7 @@
7
7
  >
8
8
  <RiInformationLine class="size-3.5 shrink-0" />
9
9
  <span class="text-gray-medium text-sm group-hover/quality:underline">{{ t('Métadonnées :') }}</span>
10
- <DatasetQualityScore :score="quality.score" />
10
+ <QualityScore :score="quality.score" />
11
11
  <template #toggletip>
12
12
  <DatasetQualityTooltipContent :quality />
13
13
  </template>
@@ -18,7 +18,7 @@
18
18
  <script setup lang="ts">
19
19
  import { RiInformationLine } from '@remixicon/vue'
20
20
  import type { Quality } from '../types/datasets'
21
- import DatasetQualityScore from './DatasetQualityScore.vue'
21
+ import QualityScore from './QualityScore.vue'
22
22
  import DatasetQualityTooltipContent from './DatasetQualityTooltipContent.vue'
23
23
  import Toggletip from './Toggletip.vue'
24
24
  import { useTranslation } from '../composables/useTranslation'
@@ -68,7 +68,7 @@
68
68
  import type { Quality } from '../types/datasets'
69
69
  import { useComponentsConfig } from '../config'
70
70
  import { useTranslation } from '../composables/useTranslation'
71
- import DatasetQualityItem from './DatasetQualityItem.vue'
71
+ import DatasetQualityItem from './QualityItem.vue'
72
72
 
73
73
  defineProps<{
74
74
  quality: Quality
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div class="text-xs">
3
+ <PreviewUnavailable v-if="!isSizeAllowed">
4
+ {{ fileSizeBytes
5
+ ? t("Le fichier {fileType} est trop volumineux pour être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.", { fileType })
6
+ : t("La taille du fichier est inconnue, l'aperçu n'est pas disponible. Téléchargez-le depuis l'onglet Téléchargements.")
7
+ }}
8
+ </PreviewUnavailable>
9
+ <PreviewUnavailable v-else-if="hasError">
10
+ {{ t("L'aperçu de ce fichier n'a pas pu être chargé. Téléchargez-le depuis l'onglet Téléchargements.") }}
11
+ </PreviewUnavailable>
12
+ <img
13
+ v-else
14
+ :src="resource.url"
15
+ alt=""
16
+ class="block w-full h-auto max-w-[800px]"
17
+ @error="hasError = true"
18
+ >
19
+ </div>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { computed, ref } from 'vue'
24
+ import PreviewUnavailable from './PreviewUnavailable.vue'
25
+ import type { Resource } from '../../types/resources'
26
+ import { getResourceFilesize } from '../../functions/resources'
27
+ import { useTranslation } from '../../composables/useTranslation'
28
+ import { useComponentsConfig } from '../../config'
29
+
30
+ const props = defineProps<{
31
+ resource: Resource
32
+ }>()
33
+
34
+ const { t } = useTranslation()
35
+ const config = useComponentsConfig()
36
+
37
+ const hasError = ref(false)
38
+
39
+ const fileType = computed(() => props.resource.format?.toUpperCase() ?? t('Fichier'))
40
+ const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
41
+
42
+ const isSizeAllowed = computed(() => {
43
+ const size = fileSizeBytes.value
44
+ const max = config.maxImagePreviewByteSize
45
+ if (!size || !max) return false
46
+ return size <= max
47
+ })
48
+ </script>
@@ -224,6 +224,10 @@
224
224
  v-else-if="resource.format && resource.format.toLowerCase() === 'xml'"
225
225
  :resource="resource"
226
226
  />
227
+ <ImagePreview
228
+ v-else-if="isImagePreviewFormat(resource.format)"
229
+ :resource="resource"
230
+ />
227
231
  <!-- Show Datafair embedded preview (koumoul) -->
228
232
  <DatafairPreview
229
233
  v-else-if="hasDatafairPreview"
@@ -309,7 +313,7 @@ import { trackEvent } from '../../functions/matomo'
309
313
  import CopyButton from '../CopyButton.vue'
310
314
  import { useComponentsConfig } from '../../config'
311
315
  import { getOwnerName } from '../../functions/owned'
312
- import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
316
+ import { getResourceFormatIcon, getResourceTitleId, detectOgcService, getResourceExternalUrl, getResourceFilesize, isImagePreviewFormat } from '../../functions/resources'
313
317
  import BrandedButton from '../BrandedButton.vue'
314
318
  import { useTranslation } from '../../composables/useTranslation'
315
319
  import { useHasTabularData } from '../../composables/useHasTabularData'
@@ -344,6 +348,7 @@ const Pmtiles = defineAsyncComponent(() => import('./Pmtiles.client.vue'))
344
348
  const JsonPreview = defineAsyncComponent(() => import('./JsonPreview.client.vue'))
345
349
  const PdfPreview = defineAsyncComponent(() => import('./PdfPreview.client.vue'))
346
350
  const XmlPreview = defineAsyncComponent(() => import('./XmlPreview.client.vue'))
351
+ const ImagePreview = defineAsyncComponent(() => import('./ImagePreview.client.vue'))
347
352
  const DatafairPreview = defineAsyncComponent(() => import('./Datafair.client.vue'))
348
353
 
349
354
  const { t } = useTranslation()
@@ -351,11 +356,11 @@ const { formatRelativeIfRecentDate } = useFormatDate()
351
356
  const checkTabularData = useHasTabularData()
352
357
 
353
358
  const hasPreview = computed(() => {
354
- // For JSON, PDF, and XML files, show preview.
359
+ // For JSON, PDF, XML, and image files, show preview.
355
360
  // We cannot check for CORS issues here because we cannot use an async component here.
356
361
  // If there is a CORS issue when fetching the file for preview, it will be managed and displayed as an error banner by the preview component.
357
362
  const format = props.resource.format?.toLowerCase()
358
- return format === 'json' || format === 'pdf' || format === 'xml'
363
+ return format === 'json' || format === 'pdf' || format === 'xml' || isImagePreviewFormat(format)
359
364
  })
360
365
 
361
366
  const hasTabularData = computed(() => checkTabularData(props.resource))