diff --git a/components/AdminSidebar/AdminSidebarMenu/AdminSidebarMenu.vue b/components/AdminSidebar/AdminSidebarMenu/AdminSidebarMenu.vue index e9bb4b13c..891cce7dd 100644 --- a/components/AdminSidebar/AdminSidebarMenu/AdminSidebarMenu.vue +++ b/components/AdminSidebar/AdminSidebarMenu/AdminSidebarMenu.vue @@ -150,6 +150,11 @@ :label="$t('Réutilisations')" to="/admin/site/reuses" /> + + + + {{ t('Graphiques') }} + + + + {{ t('Graphiques') }} + + + + + + {{ t('{n} graphiques | {n} graphique | {n} graphiques', pageData.total) }} + + + + + + + + + + page = changedPage" + /> + + + + + + + {{ t(`Il n'y a pas encore de graphique sur le site`) }} + + + + + + diff --git a/components/Charts/ChartConfigurator.vue b/components/Charts/ChartConfigurator.vue index 3ff95eeef..cc91edf07 100644 --- a/components/Charts/ChartConfigurator.vue +++ b/components/Charts/ChartConfigurator.vue @@ -80,46 +80,6 @@ - - - {{ $t('Graphiques existants') }} - - - - - {{ $t('Sélectionnez un graphique') }} - - - {{ column.title }} - - - - {{ $t('Charger') }} - - - import type { Resource, PaginatedArray, ChartForm, Chart, Filter, AndFilters, GenericFilter, ColumnType, ColumnDefinition, ColumnsDefinition, DataSeriesType, DataSeriesForm, FilterCondition, CombinedSort, Owned, XAxisType } from '@datagouv/components-next' -import { buildTypeConfig, buildColumnsFromProfile, useGetProfile, useHasTabularData, toast, BrandedButton, toChartApi, toChartForm, SearchableSelect, Listbox, useTranslation } from '@datagouv/components-next' +import { buildTypeConfig, buildColumnsFromProfile, useGetProfile, useHasTabularData, toast, BrandedButton, toChartApi, SearchableSelect, Listbox, useTranslation } from '@datagouv/components-next' import type { Component } from 'vue' import { computed, defineAsyncComponent, reactive, ref, watch } from 'vue' import { RiAddLine, RiArrowDownLine, RiArrowDownSLine, RiArrowUpLine, RiBarChartLine, RiCalculatorLine, RiLineChartLine, RiText } from '@remixicon/vue' -import { useAPI } from '~/utils/api' import { isMeAdmin } from '~/utils/auth' import { keepValidSortCombined } from '~/utils/charts' import ChartFilterRow from './ChartFilterRow.vue' @@ -508,7 +467,6 @@ const { t } = useTranslation() const hasTabularData = useHasTabularData() const getProfile = useGetProfile() const isAdmin = isMeAdmin() -const { data: charts, refresh } = await useAPI>('/api/1/visualizations/', { lazy: true }) const $chartsApi = $fetch.create({ baseURL: runtimeConfig.public.chartsApiBase as string, @@ -525,6 +483,14 @@ const form = defineModel({ required: true, }) +const props = defineProps<{ + /** + * Full chart being edited, used only to initialize producer/resources/dataset + * and to keep the chart id for saving — the form data itself comes from the v-model + */ + initialChart?: Chart +}>() + const columns = ref({}) const producer = ref(null) const dataset = ref(null) @@ -541,7 +507,6 @@ watch(producer, (newProducer) => { } }, { immediate: true }) const savedChart = ref(null) -const selectedChartId = ref('') const chartForViewer = ref(toChartApi(form.value)) @@ -792,101 +757,75 @@ async function suggestDataset(q: string): Promise> { }) } -async function loadChart(id: string) { - try { - const data = await $api(`/api/1/visualizations/${id}/`) - if (data) { - savedChart.value = data - - const chartResources = new Set() - for (const serie of data.series) { - if (serie.resource_id) { - chartResources.add(serie.resource_id) - } - } - - form.value = toChartForm(data) +async function initializeFromChart(data: Chart) { + savedChart.value = data - await loadMissingResourcesForChart(Array.from(chartResources)) - await loadColumnsForResources(Array.from(chartResources)) + const chartResources = new Set() + for (const serie of data.series) { + if (serie.resource_id) { + chartResources.add(serie.resource_id) + } + } - if (data.organization) { - producer.value = { organization: data.organization, owner: null } - } - if (data.owner) { - producer.value = { organization: null, owner: data.owner } - } + await loadMissingResourcesForChart(Array.from(chartResources)) + await loadColumnsForResources(Array.from(chartResources)) - if (!dataset.value && data.series.length > 0 && data.series[0]?.resource_id) { - try { - const resourceData = await $chartsApi<{ resource: Resource, dataset_id: string }>(`/api/2/datasets/resources/${data.series[0].resource_id}/`) - if (resourceData.dataset_id) { - const fetchedDataset = await $chartsApi(`/api/2/datasets/${resourceData.dataset_id}/`) - dataset.value = fetchedDataset - } - } - catch (error) { - console.error('Failed to load dataset for chart:', error) - } - } - - if (data.series.length > 0 && data.series[0]?.resource_id) { - await nextTick() + if (data.organization) { + producer.value = { organization: data.organization, owner: null } + } + if (data.owner) { + producer.value = { organization: null, owner: data.owner } + } - selectedResource.value = data.series[0].resource_id + if (!dataset.value && data.series.length > 0 && data.series[0]?.resource_id) { + try { + const resourceData = await $chartsApi<{ resource: Resource, dataset_id: string }>(`/api/2/datasets/resources/${data.series[0].resource_id}/`) + if (resourceData.dataset_id) { + const fetchedDataset = await $chartsApi(`/api/2/datasets/${resourceData.dataset_id}/`) + dataset.value = fetchedDataset } - - toast.success(t('Graphique chargé !')) + } + catch (error) { + console.error('Failed to load dataset for chart:', error) } } - catch (error) { - console.error('Failed to load chart:', error) - toast.error(t('Erreur lors du chargement du graphique')) - } -} -function loadSelectedChart() { - if (selectedChartId.value) { - loadChart(selectedChartId.value) + if (data.series.length > 0 && data.series[0]?.resource_id) { + await nextTick() + + selectedResource.value = data.series[0].resource_id } } async function saveChart() { - try { - const chartForApi = toChartApi(form.value) - const update = savedChart.value?.id - if (update) { - savedChart.value = await $api(`/api/1/visualizations/${savedChart.value!.id}/`, { - method: 'PATCH', - body: JSON.stringify(chartForApi), - }) - } - else { - savedChart.value = await $api('/api/1/visualizations/', { - method: 'POST', - body: JSON.stringify(chartForApi), - }) - } - - const imageUrl = chartViewerWrapperRef.value?.capture() - if (imageUrl) { - const i = await fetch(imageUrl) - const imageBlob = await i.blob() - const formData = new FormData() - formData.set('file', imageBlob, 'image.png') - await $fileApi(`/api/1/visualizations/${savedChart.value.id}/image/`, { - method: 'POST', - body: formData, - }) - } - - toast.success(update ? t('Graphique mis à jour !') : t('Graphique sauvegardé !')) - await refresh() - selectedChartId.value = savedChart.value.id + const chartForApi = toChartApi(form.value) + const update = savedChart.value?.id + if (update) { + savedChart.value = await $api(`/api/1/visualizations/${savedChart.value!.id}/`, { + method: 'PATCH', + body: JSON.stringify(chartForApi), + }) } - catch (error) { - console.error('Failed to save chart:', error) + else { + savedChart.value = await $api('/api/1/visualizations/', { + method: 'POST', + body: JSON.stringify(chartForApi), + }) + } + + const imageUrl = chartViewerWrapperRef.value?.capture() + if (imageUrl) { + const i = await fetch(imageUrl) + const imageBlob = await i.blob() + const formData = new FormData() + formData.set('file', imageBlob, 'image.png') + await $fileApi(`/api/1/visualizations/${savedChart.value.id}/image/`, { + method: 'POST', + body: formData, + }) } + + toast.success(update ? t('Graphique mis à jour !') : t('Graphique sauvegardé !')) } function removeFilter(index: number) { @@ -1025,4 +964,8 @@ watch( }, { immediate: true }, ) + +if (props.initialChart) { + await initializeFromChart(props.initialChart) +} diff --git a/datagouv-components/src/components/ChartCard.vue b/datagouv-components/src/components/ChartCard.vue new file mode 100644 index 000000000..af2d2c554 --- /dev/null +++ b/datagouv-components/src/components/ChartCard.vue @@ -0,0 +1,102 @@ + + + + + {{ t('Supprimé') }} + + + {{ t('Brouillon') }} + + + + + + + + + + {{ chart.title }} + + + + + + + {{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(chart.last_modified, { dateStyle: 'medium' }) }) }} + + + + + + {{ summarize(chart.metrics.views) }} + + + + + + + + + + diff --git a/datagouv-components/src/components/ObjectCard.vue b/datagouv-components/src/components/ObjectCard.vue index c5b64b655..3bf26a07f 100644 --- a/datagouv-components/src/components/ObjectCard.vue +++ b/datagouv-components/src/components/ObjectCard.vue @@ -28,12 +28,15 @@ import { computed } from 'vue' const props = withDefaults(defineProps<{ articleClass?: string | string[] | Record - mediaSize?: 'sm' | 'lg' + mediaSize?: 'sm' | 'lg' | 'xl' }>(), { mediaSize: 'sm', }) const mediaContainerClass = computed(() => { + if (props.mediaSize === 'xl') { + return 'w-[250px] h-[190px]' + } if (props.mediaSize === 'lg') { return 'w-[225px] h-[120px]' } diff --git a/datagouv-components/src/components/Placeholder.vue b/datagouv-components/src/components/Placeholder.vue index 88ac0bee0..5eb4da84d 100644 --- a/datagouv-components/src/components/Placeholder.vue +++ b/datagouv-components/src/components/Placeholder.vue @@ -10,10 +10,10 @@ diff --git a/datagouv-components/src/main.ts b/datagouv-components/src/main.ts index 60ff6846e..3ca905a81 100644 --- a/datagouv-components/src/main.ts +++ b/datagouv-components/src/main.ts @@ -38,6 +38,7 @@ import Avatar from './components/Avatar.vue' import AvatarWithName from './components/AvatarWithName.vue' import BannerAction from './components/BannerAction.vue' import BrandedButton from './components/BrandedButton.vue' +import ChartCard from './components/ChartCard.vue' import CopyButton from './components/CopyButton.vue' import DataserviceCard from './components/DataserviceCard.vue' import DataserviceQuality from './components/DataserviceQuality.vue' @@ -344,6 +345,7 @@ export { AvatarWithName, BannerAction, BrandedButton, + ChartCard, CopyButton, DataserviceCard, DataserviceQuality, diff --git a/datagouv-components/src/types/visualizations.ts b/datagouv-components/src/types/visualizations.ts index b9d5d15a4..e94069af8 100644 --- a/datagouv-components/src/types/visualizations.ts +++ b/datagouv-components/src/types/visualizations.ts @@ -67,6 +67,7 @@ export type Chart = Owned & { created_at: string last_modified: string deleted_at: string | null + image: string | null uri: string page: string x_axis: XAxis diff --git a/pages/admin/beta/charts/[cid].vue b/pages/admin/beta/charts/[cid].vue new file mode 100644 index 000000000..52ec1e2ae --- /dev/null +++ b/pages/admin/beta/charts/[cid].vue @@ -0,0 +1,46 @@ + + + + + {{ $t('Accueil') }} + + + {{ $t('Visualisations') }} + + + {{ $t('Édition') }} + + + + + + + + + diff --git a/pages/admin/site/charts.vue b/pages/admin/site/charts.vue new file mode 100644 index 000000000..0e4a317e7 --- /dev/null +++ b/pages/admin/site/charts.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/pages/design/cards.vue b/pages/design/cards.vue index 7de322b61..671d18a0c 100644 --- a/pages/design/cards.vue +++ b/pages/design/cards.vue @@ -65,6 +65,26 @@ + + Chart + + + + + + + Organization @@ -144,12 +164,15 @@
+ {{ t(`Il n'y a pas encore de graphique sur le site`) }} +
+ {{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(chart.last_modified, { dateStyle: 'medium' }) }) }} +
+ {{ summarize(chart.metrics.views) }} +