From 00af4a372c699cff49df19e142771fae2c8798e2 Mon Sep 17 00:00:00 2001 From: MartinBelthle <102529366+MartinBelthle@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:25:46 +0100 Subject: [PATCH 01/62] refactor(bc): remove duplicate class BindingConstraintType (#1860) --- antarest/study/business/table_mode_management.py | 11 +++-------- tests/integration/test_integration.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/antarest/study/business/table_mode_management.py b/antarest/study/business/table_mode_management.py index c124adf27d..e8c32a0c13 100644 --- a/antarest/study/business/table_mode_management.py +++ b/antarest/study/business/table_mode_management.py @@ -10,6 +10,7 @@ from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands from antarest.study.common.default_values import FilteringOptions, LinkProperties, NodalOptimization from antarest.study.model import RawStudy +from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import BindingConstraintFrequency from antarest.study.storage.rawstudy.model.filesystem.config.thermal import LawOption, TimeSeriesGenerationOption from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService @@ -40,12 +41,6 @@ class TransmissionCapacity(EnumIgnoreCase): ENABLED = "enabled" -class BindingConstraintType(EnumIgnoreCase): - HOURLY = "hourly" - DAILY = "daily" - WEEKLY = "weekly" - - class BindingConstraintOperator(EnumIgnoreCase): LESS = "less" GREATER = "greater" @@ -114,7 +109,7 @@ class RenewableColumns(FormFieldsBaseModel): class BindingConstraintColumns(FormFieldsBaseModel): - type: Optional[BindingConstraintType] + type: Optional[BindingConstraintFrequency] operator: Optional[BindingConstraintOperator] enabled: Optional[StrictBool] @@ -337,7 +332,7 @@ class PathVars(TypedDict, total=False): TableTemplateType.BINDING_CONSTRAINT: { "type": { "path": f"{BINDING_CONSTRAINT_PATH}/type", - "default_value": BindingConstraintType.HOURLY.value, + "default_value": BindingConstraintFrequency.HOURLY.value, }, "operator": { "path": f"{BINDING_CONSTRAINT_PATH}/operator", diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 1e9fd99caa..5bd1323398 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -20,11 +20,11 @@ FIELDS_INFO_BY_TYPE, AssetType, BindingConstraintOperator, - BindingConstraintType, TableTemplateType, TransmissionCapacity, ) from antarest.study.model import MatrixIndex, StudyDownloadLevelDTO +from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import BindingConstraintFrequency from antarest.study.storage.rawstudy.model.filesystem.config.renewable import RenewableClusterGroup from antarest.study.storage.rawstudy.model.filesystem.config.thermal import LawOption, TimeSeriesGenerationOption from antarest.study.storage.variantstudy.model.command.common import CommandName @@ -543,7 +543,7 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: "args": { "name": "binding constraint 1", "enabled": True, - "time_step": BindingConstraintType.HOURLY.value, + "time_step": BindingConstraintFrequency.HOURLY.value, "operator": BindingConstraintOperator.LESS.value, "coeffs": {"area 1.cluster 1": [2.0, 4]}, }, @@ -561,7 +561,7 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: "args": { "name": "binding constraint 2", "enabled": True, - "time_step": BindingConstraintType.HOURLY.value, + "time_step": BindingConstraintFrequency.HOURLY.value, "operator": BindingConstraintOperator.LESS.value, "coeffs": {}, }, @@ -1684,12 +1684,12 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: assert res_table_data_json == { "binding constraint 1": { "enabled": True, - "type": BindingConstraintType.HOURLY.value, + "type": BindingConstraintFrequency.HOURLY.value, "operator": BindingConstraintOperator.LESS.value, }, "binding constraint 2": { "enabled": True, - "type": BindingConstraintType.HOURLY.value, + "type": BindingConstraintFrequency.HOURLY.value, "operator": BindingConstraintOperator.LESS.value, }, } @@ -1706,7 +1706,7 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: "operator": BindingConstraintOperator.BOTH.value, }, "binding constraint 2": { - "type": BindingConstraintType.WEEKLY.value, + "type": BindingConstraintFrequency.WEEKLY.value, "operator": BindingConstraintOperator.EQUAL.value, }, }, @@ -1723,12 +1723,12 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: assert res_table_data_json == { "binding constraint 1": { "enabled": False, - "type": BindingConstraintType.HOURLY.value, + "type": BindingConstraintFrequency.HOURLY.value, "operator": BindingConstraintOperator.BOTH.value, }, "binding constraint 2": { "enabled": True, - "type": BindingConstraintType.WEEKLY.value, + "type": BindingConstraintFrequency.WEEKLY.value, "operator": BindingConstraintOperator.EQUAL.value, }, } From 4a129953e0d449aeb5979c4e0397f58ab51eb77e Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:20:21 +0100 Subject: [PATCH 02/62] feat(ui-common): update FormDialog * add loading submit button * add `isCreationForm` prop --- .../components/common/dialogs/FormDialog.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/common/dialogs/FormDialog.tsx b/webapp/src/components/common/dialogs/FormDialog.tsx index 8c1097f4e9..8f0ab7673f 100644 --- a/webapp/src/components/common/dialogs/FormDialog.tsx +++ b/webapp/src/components/common/dialogs/FormDialog.tsx @@ -3,6 +3,9 @@ import { Button } from "@mui/material"; import { useId, useState } from "react"; import { FieldValues, FormState } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { LoadingButton } from "@mui/lab"; +import * as RA from "ramda-adjunct"; +import SaveIcon from "@mui/icons-material/Save"; import BasicDialog, { BasicDialogProps } from "./BasicDialog"; import Form, { FormProps } from "../Form"; @@ -23,6 +26,7 @@ export interface FormDialogProps< > extends SuperType { cancelButtonText?: string; onCancel: VoidFunction; + isCreationForm?: boolean; } // TODO: `formState.isSubmitting` doesn't update when auto submit enabled @@ -44,6 +48,8 @@ function FormDialog< onClose, cancelButtonText, submitButtonText, + submitButtonIcon, + isCreationForm = false, ...dialogProps } = props; @@ -59,7 +65,7 @@ function FormDialog< const { t } = useTranslation(); const formId = useId(); const [isSubmitting, setIsSubmitting] = useState(false); - const [isSubmitAllowed, setIsSubmitAllowed] = useState(false); + const [isSubmitAllowed, setIsSubmitAllowed] = useState(isCreationForm); //////////////////////////////////////////////////////////////// // Event Handlers @@ -69,7 +75,7 @@ function FormDialog< const { isSubmitting, isDirty } = formState; onStateChange?.(formState); setIsSubmitting(isSubmitting); - setIsSubmitAllowed(isDirty && !isSubmitting); + setIsSubmitAllowed((isDirty || isCreationForm) && !isSubmitting); }; //////////////////////////////////////////////////////////////// @@ -97,14 +103,23 @@ function FormDialog< {cancelButtonText || t("button.close")} {!autoSubmit && ( - + )} } From bdda0cd8b3c4336d95ff91d090a973b4dfde29a8 Mon Sep 17 00:00:00 2001 From: Samir Kamal <1954121+skamril@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:20:44 +0100 Subject: [PATCH 03/62] feat(ui-common): update GroupedDataTable * add duplicate dialog * add creation loading --- .../{CreateRowDialog.tsx => CreateDialog.tsx} | 16 ++-- .../GroupedDataTable/DuplicateDialog.tsx | 76 ++++++++++++++++ .../common/GroupedDataTable/index.tsx | 88 ++++++++++++------- .../common/GroupedDataTable/utils.ts | 15 +++- 4 files changed, 151 insertions(+), 44 deletions(-) rename webapp/src/components/common/GroupedDataTable/{CreateRowDialog.tsx => CreateDialog.tsx} (91%) create mode 100644 webapp/src/components/common/GroupedDataTable/DuplicateDialog.tsx diff --git a/webapp/src/components/common/GroupedDataTable/CreateRowDialog.tsx b/webapp/src/components/common/GroupedDataTable/CreateDialog.tsx similarity index 91% rename from webapp/src/components/common/GroupedDataTable/CreateRowDialog.tsx rename to webapp/src/components/common/GroupedDataTable/CreateDialog.tsx index 4fc9e483f2..d85cd669fd 100644 --- a/webapp/src/components/common/GroupedDataTable/CreateRowDialog.tsx +++ b/webapp/src/components/common/GroupedDataTable/CreateDialog.tsx @@ -5,13 +5,13 @@ import StringFE from "../fieldEditors/StringFE"; import Fieldset from "../Fieldset"; import { SubmitHandlerPlus } from "../Form/types"; import SelectFE from "../fieldEditors/SelectFE"; -import { TRow } from "."; import { nameToId } from "../../../services/utils"; +import { TRow } from "./utils"; interface Props { open: boolean; onClose: VoidFunction; - onSubmit: (values: TData) => void; + onSubmit: (values: TData) => Promise; groups: string[] | readonly string[]; existingNames: Array; } @@ -21,7 +21,7 @@ const defaultValues = { group: "", }; -function CreateRowDialog({ +function CreateDialog({ open, onClose, onSubmit, @@ -32,10 +32,10 @@ function CreateRowDialog({ // Event Handlers //////////////////////////////////////////////////////////////// - const handleSubmit = ({ + const handleSubmit = async ({ values, }: SubmitHandlerPlus) => { - onSubmit({ + await onSubmit({ ...values, id: nameToId(values.name), name: values.name.trim(), @@ -55,9 +55,7 @@ function CreateRowDialog({ open={open} onCancel={onClose} onSubmit={handleSubmit} - config={{ - defaultValues, - }} + config={{ defaultValues }} > {({ control }) => (
@@ -99,4 +97,4 @@ function CreateRowDialog({ ); } -export default CreateRowDialog; +export default CreateDialog; diff --git a/webapp/src/components/common/GroupedDataTable/DuplicateDialog.tsx b/webapp/src/components/common/GroupedDataTable/DuplicateDialog.tsx new file mode 100644 index 0000000000..93daa1a3bc --- /dev/null +++ b/webapp/src/components/common/GroupedDataTable/DuplicateDialog.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import ControlPointDuplicateIcon from "@mui/icons-material/ControlPointDuplicate"; +import Fieldset from "../Fieldset"; +import FormDialog from "../dialogs/FormDialog"; +import { SubmitHandlerPlus } from "../Form/types"; +import StringFE from "../fieldEditors/StringFE"; + +interface Props { + open: boolean; + onClose: VoidFunction; + onSubmit: (name: string) => Promise; + existingNames: string[]; + defaultName: string; +} + +function DuplicateDialog(props: Props) { + const { open, onClose, onSubmit, existingNames, defaultName } = props; + const { t } = useTranslation(); + const defaultValues = { name: defaultName }; + + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// + + const handleSubmit = async ({ + values: { name }, + }: SubmitHandlerPlus) => { + await onSubmit(name); + onClose(); + }; + + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + + return ( + + {({ control }) => ( +
+ { + const regex = /^[a-zA-Z0-9_\-() &]+$/; + if (!regex.test(v.trim())) { + return t("form.field.specialChars", { 0: "&()_-" }); + } + if (v.trim().length <= 0) { + return t("form.field.required"); + } + if (existingNames.includes(v.trim().toLowerCase())) { + return t("form.field.duplicate", { 0: v }); + } + }, + }} + sx={{ m: 0 }} + /> +
+ )} +
+ ); +} + +export default DuplicateDialog; diff --git a/webapp/src/components/common/GroupedDataTable/index.tsx b/webapp/src/components/common/GroupedDataTable/index.tsx index 17f1df92a1..68d7cc2816 100644 --- a/webapp/src/components/common/GroupedDataTable/index.tsx +++ b/webapp/src/components/common/GroupedDataTable/index.tsx @@ -1,10 +1,11 @@ /* eslint-disable react/jsx-pascal-case */ /* eslint-disable camelcase */ import Box from "@mui/material/Box"; -import AddIcon from "@mui/icons-material/Add"; -import { Button } from "@mui/material"; +import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; +import ControlPointDuplicateIcon from "@mui/icons-material/ControlPointDuplicate"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import DeleteIcon from "@mui/icons-material/Delete"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { Button } from "@mui/material"; import { MaterialReactTable, MRT_RowSelectionState, @@ -14,11 +15,10 @@ import { } from "material-react-table"; import { useTranslation } from "react-i18next"; import { useMemo, useState } from "react"; -import CreateRowDialog from "./CreateRowDialog"; +import CreateDialog from "./CreateDialog"; import ConfirmationDialog from "../dialogs/ConfirmationDialog"; -import { generateUniqueValue } from "./utils"; - -export type TRow = { id: string; name: string; group: string }; +import { TRow, generateUniqueValue } from "./utils"; +import DuplicateDialog from "./DuplicateDialog"; export interface GroupedDataTableProps { data: TData[]; @@ -36,8 +36,9 @@ function GroupedDataTable({ onDelete, }: GroupedDataTableProps) { const { t } = useTranslation(); - const [createDialogOpen, setCreateDialogOpen] = useState(false); - const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); + const [openDialog, setOpenDialog] = useState< + "add" | "duplicate" | "delete" | "" + >(""); const [tableData, setTableData] = useState(data); const [rowSelection, setRowSelection] = useState({}); @@ -51,23 +52,38 @@ function GroupedDataTable({ [rowSelection], ); + const selectedRow = useMemo(() => { + if (isOneRowSelected) { + const selectedIndex = Object.keys(rowSelection).find( + (key) => rowSelection[key], + ); + return selectedIndex && tableData[+selectedIndex]; + } + }, [isOneRowSelected, rowSelection, tableData]); + const existingNames = useMemo( () => tableData.map((row) => row.name.toLowerCase()), [tableData], ); + //////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////// + + const closeDialog = () => setOpenDialog(""); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// - const handleCreateRow = async (values: TData) => { + const handleCreate = async (values: TData) => { if (onCreate) { const newRow = await onCreate(values); setTableData((prevTableData) => [...prevTableData, newRow]); } }; - const handleDeleteSelection = () => { + const handleDelete = () => { if (!onDelete) { return; } @@ -84,20 +100,14 @@ function GroupedDataTable({ prevTableData.filter((row) => !rowIdsToDelete.includes(row.id)), ); setRowSelection({}); - setConfirmDialogOpen(false); + closeDialog(); }; - const handleDuplicateRow = async () => { - const selectedIndex = Object.keys(rowSelection).find( - (key) => rowSelection[key], - ); - const selectedRow = selectedIndex && tableData[+selectedIndex]; - + const handleDuplicate = async (name: string) => { if (!selectedRow) { return; } - const name = generateUniqueValue("name", selectedRow.name, tableData); const id = generateUniqueValue("id", name, tableData); const duplicatedRow = { @@ -162,29 +172,29 @@ function GroupedDataTable({ {onCreate && ( )} {onDelete && (