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 1e04c4310b..06d99317b2 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; } @@ -219,6 +221,10 @@ function GroupedDataTable { + if (readOnly) { + return {}; + } + const isPending = isPendingRow(row.original); return { @@ -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..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,10 +14,12 @@ import GroupedDataTable from "@/components/GroupedDataTable"; import type { RowData } from "@/components/GroupedDataTable/types"; +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 { Chip } from "@mui/material"; +import { getOptimization } from "@/services/api/studies/config/optimization"; +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"; @@ -74,6 +76,16 @@ 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, isLoading } = usePromiseWithSnackbarError( + () => getOptimization({ studyId }).then((o) => o.includeReserves), + { + errorMessage: t("study.modeling.reserves.error.optimizationConfig"), + deps: [studyId], + }, + ); + const { queryKey: listQueryKey } = reserveQueries.list(studyId, areaId); const { data: rows } = useSuspenseQuery({ @@ -140,27 +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), + }) + } + /> + ); } diff --git a/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/route.tsx b/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/route.tsx index 9cc860372a..5bcb456e24 100644 --- a/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/route.tsx +++ b/webapp/src/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/route.tsx @@ -14,10 +14,8 @@ import TabsView from "@/components/page/TabsView"; import ViewWrapper from "@/components/page/ViewWrapper"; -import usePromise from "@/hooks/usePromise"; import useAppSelector from "@/redux/hooks/useAppSelector"; import { getStudySynthesis } from "@/redux/selectors"; -import { getOptimization } from "@/services/api/studies/config/optimization"; import useStudy from "@/routes/_authenticated/studies/$studyId/-hooks/useStudy"; import useArea from "@/routes/_authenticated/studies/$studyId/explore/modeling/areas/$areaId/-hooks/useArea"; import { createFileRoute, linkOptions } from "@tanstack/react-router"; @@ -42,13 +40,6 @@ function AreaLayout() { // editable in "/studies/$studyId/explore/configuration/advanced-params" const enrModelling = useAppSelector((state) => 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 (