From fc4412738064bfc449f8ec85211950fa3071319b Mon Sep 17 00:00:00 2001 From: hdinia <33469289+hdinia@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:05:51 +0200 Subject: [PATCH 1/2] feat(ui-reserves): add read-only mode to GroupedDataTable and reserves view --- .../src/components/GroupedDataTable/index.tsx | 17 ++++++++---- .../areas/$areaId/reserves/general.tsx | 10 +++++++ .../explore/modeling/areas/$areaId/route.tsx | 26 ++++++------------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/webapp/src/components/GroupedDataTable/index.tsx b/webapp/src/components/GroupedDataTable/index.tsx index 1e04c4310b..acfa8e0e49 100644 --- a/webapp/src/components/GroupedDataTable/index.tsx +++ b/webapp/src/components/GroupedDataTable/index.tsx @@ -72,6 +72,7 @@ export interface GroupedDataTableProps< onNameClick?: (row: TData) => void; nameLinkOptions?: (row: TData) => ToOptions; onDataChange?: (data: TData[]) => void; + readOnly?: boolean; isLoading?: boolean; deleteConfirmationMessage?: string | ((rows: TData[]) => string); fillPendingRow?: ( @@ -97,6 +98,7 @@ function GroupedDataTable({}); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); - const callbacksRef = useUpdatedRef({ onNameClick, nameLinkOptions }); + const callbacksRef = useUpdatedRef({ onNameClick, nameLinkOptions, readOnly }); const pendingRows = useRef>>([]); const { createOps, deleteOps, totalOps } = useOperationInProgressCount(); const { isDarkMode } = useThemeColorScheme(); @@ -140,9 +142,9 @@ function GroupedDataTable { - const { onNameClick, nameLinkOptions } = callbacksRef.current; + const { onNameClick, nameLinkOptions, readOnly } = callbacksRef.current; - if (isPendingRow(row.original)) { + if (isPendingRow(row.original) || readOnly) { return renderedCellValue; } @@ -221,6 +223,10 @@ function GroupedDataTable { const isPending = isPendingRow(row.original); + if (readOnly) { + return {}; + } + return { onClick: () => { if (isPending) { @@ -254,6 +260,7 @@ function GroupedDataTable} variant="contained" onClick={() => setOpenDialog("add")} + disabled={readOnly} > {t("button.add")} @@ -263,7 +270,7 @@ function GroupedDataTable} variant="outlined" onClick={() => setOpenDialog("duplicate")} - disabled={table.getSelectedRowModel().rows.length !== 1} + disabled={readOnly || table.getSelectedRowModel().rows.length !== 1} > {t("global.duplicate")} @@ -274,7 +281,7 @@ function GroupedDataTable setOpenDialog("delete")} - disabled={table.getSelectedRowModel().rows.length === 0} + disabled={readOnly || table.getSelectedRowModel().rows.length === 0} > {t("global.delete")} diff --git a/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx b/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx index 1c2dbcfc1a..513638ec8e 100644 --- a/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx +++ b/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx @@ -14,9 +14,11 @@ import GroupedDataTable from "@/components/GroupedDataTable"; import type { RowData } from "@/components/GroupedDataTable/types"; +import usePromise from "@/hooks/usePromise"; import { reserveMutations } from "@/queries/reserves/mutations"; import { reserveQueries } from "@/queries/reserves/queries"; import type { Reserve } from "@/services/api/studies/areas/reserves/types"; +import { getOptimization } from "@/services/api/studies/config/optimization"; import { Chip } from "@mui/material"; import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; @@ -74,6 +76,13 @@ function ReservesGeneral() { const { studyId, areaId } = Route.useParams(); const queryClient = useQueryClient(); + // Reserves are editable only when enabled in the optimization configuration + // ("includeReserves"). Otherwise the table is shown in read-only mode. + const { data: showReserves } = usePromise( + () => getOptimization({ studyId }).then((o) => o.includeReserves), + [studyId], + ); + const { queryKey: listQueryKey } = reserveQueries.list(studyId, areaId); const { data: rows } = useSuspenseQuery({ @@ -144,6 +153,7 @@ function ReservesGeneral() { key={`${studyId}-${areaId}`} data={rows} columns={columns} + readOnly={!showReserves} onCreate={handleCreate} renderCreateDialog={({ open, onClose, onSubmit, existingNames }) => ( getStudySynthesis(state, study.id)?.enr_modelling); - // The reserves tab is only shown when reserves are enabled in the - // optimization configuration ("includeReserves"). - const { data: showReserves } = usePromise( - () => getOptimization({ studyId: study.id }).then((o) => o.includeReserves), - [study.id], - ); - return ( Date: Mon, 29 Jun 2026 14:55:02 +0200 Subject: [PATCH 2/2] review --- webapp/public/locales/en/main.json | 2 + webapp/public/locales/fr/main.json | 2 + .../src/components/GroupedDataTable/index.tsx | 4 +- .../areas/$areaId/reserves/general.tsx | 63 +++++++++++-------- 4 files changed, 43 insertions(+), 28 deletions(-) diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 301892f48d..0cb501b26b 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -639,6 +639,8 @@ "study.modeling.renewables.select": "Renewable", "study.modeling.renewables.parameters": "Parameters", "study.modeling.reserves": "Reserves", + "study.modeling.reserves.error.optimizationConfig": "Unable to retrieve the reserves configuration", + "study.modeling.reserves.readOnly.alert": "Reserves are read-only. Enable them in the optimization configuration to edit.", "study.modeling.reserves.needs": "Needs", "study.modeling.reserves.needs.select": "Reserve", "study.modeling.reserves.needs.empty": "Create a reserve to define its needs", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index ef283e9973..f8e78eb6df 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -639,6 +639,8 @@ "study.modeling.renewables.select": "Renouvelable", "study.modeling.renewables.parameters": "Paramètres", "study.modeling.reserves": "Réserves", + "study.modeling.reserves.error.optimizationConfig": "Impossible de récupérer la configuration des réserves", + "study.modeling.reserves.readOnly.alert": "Les réserves sont en lecture seule. Pour les modifier, activez-les dans la configuration d'optimisation.", "study.modeling.reserves.needs": "Besoins", "study.modeling.reserves.needs.select": "Réserve", "study.modeling.reserves.needs.empty": "Créez une réserve pour définir ses besoins", diff --git a/webapp/src/components/GroupedDataTable/index.tsx b/webapp/src/components/GroupedDataTable/index.tsx index acfa8e0e49..06d99317b2 100644 --- a/webapp/src/components/GroupedDataTable/index.tsx +++ b/webapp/src/components/GroupedDataTable/index.tsx @@ -221,12 +221,12 @@ function GroupedDataTable { - const isPending = isPendingRow(row.original); - if (readOnly) { return {}; } + const isPending = isPendingRow(row.original); + return { onClick: () => { if (isPending) { diff --git a/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx b/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx index 513638ec8e..abe60022ef 100644 --- a/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx +++ b/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/reserves/general.tsx @@ -14,12 +14,12 @@ import GroupedDataTable from "@/components/GroupedDataTable"; import type { RowData } from "@/components/GroupedDataTable/types"; -import usePromise from "@/hooks/usePromise"; +import usePromiseWithSnackbarError from "@/hooks/usePromiseWithSnackbarError"; import { reserveMutations } from "@/queries/reserves/mutations"; import { reserveQueries } from "@/queries/reserves/queries"; import type { Reserve } from "@/services/api/studies/areas/reserves/types"; import { getOptimization } from "@/services/api/studies/config/optimization"; -import { Chip } from "@mui/material"; +import { Alert, Chip } from "@mui/material"; import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { createMRTColumnHelper } from "material-react-table"; @@ -78,9 +78,12 @@ function ReservesGeneral() { // Reserves are editable only when enabled in the optimization configuration // ("includeReserves"). Otherwise the table is shown in read-only mode. - const { data: showReserves } = usePromise( + const { data: showReserves, isLoading } = usePromiseWithSnackbarError( () => getOptimization({ studyId }).then((o) => o.includeReserves), - [studyId], + { + errorMessage: t("study.modeling.reserves.error.optimizationConfig"), + deps: [studyId], + }, ); const { queryKey: listQueryKey } = reserveQueries.list(studyId, areaId); @@ -149,28 +152,36 @@ function ReservesGeneral() { //////////////////////////////////////////////////////////////// return ( - ( - + <> + {showReserves === false && ( + + {t("study.modeling.reserves.readOnly.alert")} + )} - onDuplicate={handleDuplicate} - onDelete={handleDelete} - deleteConfirmationMessage={(rowsToDelete) => - t("study.modeling.reserves.question.delete", { - count: rowsToDelete.length, - reserveNames: rowsToDelete.map((row) => row.name), - }) - } - /> + ( + + )} + onDuplicate={handleDuplicate} + onDelete={handleDelete} + deleteConfirmationMessage={(rowsToDelete) => + t("study.modeling.reserves.question.delete", { + count: rowsToDelete.length, + reserveNames: rowsToDelete.map((row) => row.name), + }) + } + /> + ); }