Browse Source

#115 select, update and delete scenario (#133)

* #115 select, update and delete scenario

* unused var

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 2 năm trước cách đây
mục cha
commit
f1ad4df0d7
5 tập tin đã thay đổi với 619 bổ sung821 xóa
  1. 45 0
      gui/.eslintrc.js
  2. 242 405
      gui/package-lock.json
  3. 0 1
      gui/package.json
  4. 253 384
      gui/src/ScenarioSelector.tsx
  5. 79 31
      src/taipy/gui_core/GuiCoreLib.py

+ 45 - 0
gui/.eslintrc.js

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+module.exports =  {
+  parser:  '@typescript-eslint/parser',  // Specifies the ESLint parser
+  extends:  [
+    'plugin:react/recommended',  // Uses the recommended rules from @eslint-plugin-react
+    'plugin:@typescript-eslint/recommended',  // Uses the recommended rules from @typescript-eslint/eslint-plugin
+  ],
+  plugins: [
+    "@typescript-eslint",
+    "react-hooks",
+    "eslint-plugin-tsdoc"
+  ],
+  parserOptions:  {
+    ecmaVersion:  2018,  // Allows for the parsing of modern ECMAScript features
+    sourceType:  'module',  // Allows for the use of imports
+    ecmaFeatures:  {
+      jsx:  true,  // Allows for the parsing of JSX
+    },
+  },
+  rules:  {
+    // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+    "@typescript-eslint/explicit-function-return-type": "off",
+    "@typescript-eslint/explicit-module-boundary-types": "off",
+    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
+    "react-hooks/exhaustive-deps": "error", // Checks effect dependencies
+    "tsdoc/syntax": "off", // "warn" to check tsdoc syntax
+  },
+  settings:  {
+    react:  {
+      version:  'detect',  // Tells eslint-plugin-react to automatically detect the version of React to use
+    },
+  },
+};

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 242 - 405
gui/package-lock.json


+ 0 - 1
gui/package.json

@@ -27,7 +27,6 @@
     "formik": "^2.2.9",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
-    "taipy-gui": "file:C:/Users/MJ/.virtualenvs/taipy-lNlhytKY/Lib/site-packages/taipy/gui/webapp"
   },
   "scripts": {
     "install": "node scripts/install.js",

+ 253 - 384
gui/src/ScenarioSelector.tsx

@@ -29,15 +29,7 @@ import Dialog from "@mui/material/Dialog";
 import Select from "@mui/material/Select";
 import TextField from "@mui/material/TextField";
 import Typography from "@mui/material/Typography";
-import {
-  ChevronRight,
-  ExpandMore,
-  FlagOutlined,
-  Close,
-  DeleteOutline,
-  Add,
-  EditOutlined,
-} from "@mui/icons-material";
+import { ChevronRight, ExpandMore, FlagOutlined, Close, DeleteOutline, Add, EditOutlined } from "@mui/icons-material";
 import TreeItem from "@mui/lab/TreeItem";
 import TreeView from "@mui/lab/TreeView";
 import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
@@ -60,10 +52,25 @@ enum NodeType {
   SCENARIO = 1,
 }
 
+type Property = {
+  id: string;
+  key: string;
+  value: string;
+};
+
 type Scenario = [string, string, undefined, number, boolean];
 type Scenarios = Array<Scenario>;
 type Cycles = Array<[string, string, Scenarios, number, boolean]>;
 
+// id, is_primary, config_id, creation_date, label, tags, properties(key, value), pipelines(id, label), authorized_tags
+type ScenarioFull = [string, boolean, string, string, string, string[], Array<[string, string]>, Array<[string, string]>, string[]];
+interface ScenarioDict {
+  config: string;
+  name: string;
+  date: string;
+  properties: Array<[string, string]>;
+}
+
 interface ScenarioSelectorProps {
   id?: string;
   defaultShowAddButton: boolean;
@@ -75,27 +82,45 @@ interface ScenarioSelectorProps {
   value?: Record<string, any>;
   updateVarName?: string;
   scenarios?: Cycles | Scenarios;
-  onScenarioCreate: string;
+  onScenarioCrud: string;
   onChange?: string;
   coreChanged?: Record<string, unknown>;
   updateVars: string;
   configs?: Array<[string, string]>;
   error?: string;
   propagate?: boolean;
-  scenario?: Record<string, string>;
+  scenarioEdit?: ScenarioFull;
+  onScenarioSelect: string;
 }
 
 interface ScenarioNodesProps {
   scenarios?: Scenarios | Scenario;
   showPrimary?: boolean;
-  openEditDialog: () => void;
+  openEditDialog: (e: React.MouseEvent<HTMLElement>) => void;
 }
 
 interface ScenarioNodesContentProps {
+  scenarioId?: string;
   label?: string;
-  openEditDialog: () => void;
+  openEditDialog: (e: React.MouseEvent<HTMLElement>) => void;
+}
+
+interface ScenarioEditDialogProps {
+  scenario?: ScenarioFull;
+  submit: (...values: any[]) => void;
+  open: boolean;
+  actionEdit: boolean;
+  configs?: Array<[string, string]>;
+  close: () => void;
 }
 
+const emptyScenario: ScenarioDict = {
+  config: "",
+  name: "",
+  date: new Date().toISOString(),
+  properties: [],
+};
+
 const BadgePos = {
   vertical: "top",
   horizontal: "left",
@@ -117,80 +142,6 @@ const FlagSx = {
 
 const ActionContentSx = { mr: 2, ml: 2 };
 
-const ScenarioNodesContent = ({
-  label,
-  openEditDialog,
-}: ScenarioNodesContentProps) => {
-  return (
-    <Grid
-      container
-      alignItems="center"
-      direction="row"
-      flexWrap="nowrap"
-      justifyContent="space-between"
-      spacing={1}
-    >
-      <Grid item>{label}</Grid>
-      <Grid item>
-        <EditOutlined
-          fontSize="small"
-          color="primary"
-          onClick={openEditDialog}
-        />
-      </Grid>
-    </Grid>
-  );
-};
-
-const ScenarioNodes = ({
-  scenarios = [],
-  showPrimary = true,
-  openEditDialog,
-}: ScenarioNodesProps) => {
-  const sc =
-    Array.isArray(scenarios) && scenarios.length && Array.isArray(scenarios[0])
-      ? (scenarios as Scenarios)
-      : scenarios
-      ? [scenarios as Scenario]
-      : [];
-  return (
-    <>
-      {sc.map(([id, label, _, _nodeType, primary]) => (
-        <TreeItem
-          key={id}
-          nodeId={id}
-          label={
-            showPrimary && primary ? (
-              <Badge
-                badgeContent={<FlagOutlined sx={FlagSx} />}
-                color="primary"
-                anchorOrigin={BadgePos}
-                sx={BadgeSx}
-              >
-                <ScenarioNodesContent
-                  label={label}
-                  openEditDialog={openEditDialog}
-                />
-              </Badge>
-            ) : (
-              <ScenarioNodesContent
-                label={label}
-                openEditDialog={openEditDialog}
-              />
-            )
-          }
-        />
-      ))}
-    </>
-  );
-};
-
-type Property = {
-  id: string;
-  key: string;
-  value: string;
-};
-
 const MainBoxSx = {
   maxWidth: 300,
   overflowY: "auto",
@@ -215,91 +166,64 @@ const CycleSx = {
 
 const DialogContentSx = {
   width: "500px",
+  maxHeight: "calc(100vh - 256px)",
 };
 
 const CancelBtnSx = {
   mr: 2,
 };
 
-const IconButtonSx = { 
-  p: 0, 
-}
+const IconButtonSx = {
+  p: 0,
+};
 
-const ScenarioSelector = (props: ScenarioSelectorProps) => {
-  const { id = "", scenarios = [], propagate = true } = props;
-  const [open, setOpen] = useState(false);
+const ScenarioNodesContent = ({ scenarioId, label, openEditDialog }: ScenarioNodesContentProps) => {
+  return (
+    <Grid container alignItems="center" direction="row" flexWrap="nowrap" justifyContent="space-between" spacing={1}>
+      <Grid item>{label}</Grid>
+      <Grid item>
+        <IconButton data-id={scenarioId} onClick={openEditDialog}>
+          <EditOutlined fontSize="small" color="primary" />
+        </IconButton>
+      </Grid>
+    </Grid>
+  );
+};
+
+const ScenarioNodes = ({ scenarios = [], showPrimary = true, openEditDialog }: ScenarioNodesProps) => {
+  const sc = Array.isArray(scenarios) && scenarios.length && Array.isArray(scenarios[0]) ? (scenarios as Scenarios) : scenarios ? [scenarios as Scenario] : [];
+  return (
+    <>
+      {sc.map(([id, label, _, _nodeType, primary]) => (
+        <TreeItem
+          key={id}
+          nodeId={id}
+          label={
+            showPrimary && primary ? (
+              <Badge badgeContent={<FlagOutlined sx={FlagSx} />} color="primary" anchorOrigin={BadgePos} sx={BadgeSx}>
+                <ScenarioNodesContent scenarioId={id} label={label} openEditDialog={openEditDialog} />
+              </Badge>
+            ) : (
+              <ScenarioNodesContent scenarioId={id} label={label} openEditDialog={openEditDialog} />
+            )
+          }
+        />
+      ))}
+    </>
+  );
+};
+
+const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close }: ScenarioEditDialogProps) => {
+  const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
   const [properties, setProperties] = useState<Property[]>([]);
   const [newProp, setNewProp] = useState<Property>({
     id: "",
     key: "",
     value: "",
   });
-  const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
-  const [actionEdit, setActionEdit] = useState<boolean>(false);
-
-  const dispatch = useDispatch();
-  const module = useModule();
-
-  useDispatchRequestUpdateOnFirstRender(dispatch, "", module, props.updateVars);
-
-  const showAddButton = useDynamicProperty(
-    props.showAddButton,
-    props.defaultShowAddButton,
-    true
-  );
-  const displayCycles = useDynamicProperty(
-    props.displayCycles,
-    props.defaultDisplayCycles,
-    true
-  );
-  const showPrimaryFlag = useDynamicProperty(
-    props.showPrimaryFlag,
-    props.defaultShowPrimaryFlag,
-    true
-  );
-
-  const onDeleteScenario = useCallback(() => {
-    onConfirmDialogClose();
-    onDialogClose();
-  }, []);
-
-  const onConfirmDialogOpen = useCallback(() => {
-    setConfirmDialogOpen(true);
-  }, []);
-
-  const onConfirmDialogClose = useCallback(() => {
-    setConfirmDialogOpen(false);
-  }, []);
-
-  const onDialogClose = useCallback(() => {
-    setOpen(false);
-  }, []);
-
-  const onDialogOpen = useCallback(() => {
-    setOpen(true);
-    setActionEdit(false);
-  }, []);
-
-  const openEditDialog = useCallback(() => {
-    setOpen(true);
-    setActionEdit(true);
-  }, []);
-
-  const onSubmit = (values: any) => {
-    values.properties = [...properties];
-    dispatch(
-      createSendActionNameAction(id, module, props.onScenarioCreate, values)
-    );
-    form.resetForm();
-    setOpen(false);
-    setProperties([]);
-  };
 
   const propertyAdd = () => {
-    setProperties((props) => [
-      ...props,
-      { ...newProp, id: props.length + 1 + "" },
-    ]);
+    setProperties((props) => [...props, { ...newProp, id: props.length + 1 + "" }]);
     setNewProp({ id: "", key: "", value: "" });
   };
 
@@ -308,144 +232,58 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     setProperties((props) => props.filter((item) => item.id !== id));
   }, []);
 
-  const updatePropertyField = useCallback(
-    (e: React.ChangeEvent<HTMLInputElement>) => {
-      const { idx = "", name = "" } =
-        e.currentTarget.parentElement?.parentElement?.dataset || {};
-      if (name) {
-        if (idx) {
-          setProperties((props) =>
-            props.map((p, i) => {
-              if (idx == i + "") {
-                p[name as keyof Property] = e.target.value;
-              }
-              return p;
-            })
-          );
-        } else {
-          setNewProp((np) => ({ ...np, [name]: e.target.value }));
-        }
+  const updatePropertyField = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+    const { idx = "", name = "" } = e.currentTarget.parentElement?.parentElement?.dataset || {};
+    if (name) {
+      if (idx) {
+        setProperties((props) =>
+          props.map((p, i) => {
+            if (idx == i + "") {
+              p[name as keyof Property] = e.target.value;
+            }
+            return p;
+          })
+        );
+      } else {
+        setNewProp((np) => ({ ...np, [name]: e.target.value }));
       }
-    },
-    []
-  );
-
-  const form = useFormik({
-    initialValues: {
-      config: "",
-      name: "",
-      date: new Date().toISOString(),
-      properties: [],
-    },
-    onSubmit,
-  });
+    }
+  }, []);
 
-  // Refresh on broadcast
   useEffect(() => {
-    if (props.coreChanged?.scenario) {
-      const updateVar = getUpdateVar(props.updateVars, "scenarios");
-      updateVar &&
-        dispatch(createRequestUpdateAction(id, module, [updateVar], true));
-    }
-  }, [props.coreChanged, props.updateVars, module, dispatch]);
+    form.setValues(scenario ? { config: scenario[2], name: scenario[4], date: scenario[3], properties: scenario[6] } : emptyScenario);
+    setProperties(scenario ? scenario[6].map(([k, v], i) => ({ id: i + "", key: k, value: v })) : []);
+  }, [scenario]);
 
-  const onSelect = useCallback(
-    (e: React.SyntheticEvent, nodeIds: Array<string> | string) => {
-      const scenariosVar = getUpdateVar(props.updateVars, "scenarios");
-      dispatch(
-        createSendUpdateAction(
-          props.updateVarName,
-          nodeIds,
-          module,
-          props.onChange,
-          propagate,
-          scenariosVar
-        )
-      );
+  const form = useFormik({
+    initialValues: emptyScenario,
+    onSubmit: (values: any) => {
+      values.properties = [...properties];
+      actionEdit && scenario && (values.id = scenario[0]);
+      setProperties([]);
+      submit(actionEdit, false, values);
+      form.resetForm();
+      close();
     },
-    [props.updateVarName, props.updateVars, props.onChange, propagate, module]
-  );
+  });
 
-  return (
-    <div>
-      <Box sx={MainBoxSx}>
-        <TreeView
-          defaultCollapseIcon={<ExpandMore />}
-          defaultExpandIcon={<ChevronRight />}
-          sx={TreeViewSx}
-          onNodeSelect={onSelect}
-        >
-          {scenarios
-            ? scenarios.map((item) => {
-                const [id, label, scenarios, nodeType, _] = item;
-                return (
-                  <>
-                    {displayCycles ? (
-                      nodeType === NodeType.CYCLE ? (
-                        <TreeItem
-                          key={id}
-                          nodeId={id}
-                          label={label}
-                          sx={CycleSx}
-                        >
-                          <ScenarioNodes
-                            scenarios={scenarios}
-                            showPrimary={showPrimaryFlag}
-                            openEditDialog={openEditDialog}
-                          />
-                        </TreeItem>
-                      ) : (
-                        <ScenarioNodes
-                          scenarios={item as Scenario}
-                          showPrimary={showPrimaryFlag}
-                          openEditDialog={openEditDialog}
-                        />
-                      )
-                    ) : nodeType === NodeType.SCENARIO ? (
-                      <ScenarioNodes
-                        scenarios={item as Scenario}
-                        showPrimary={showPrimaryFlag}
-                        openEditDialog={openEditDialog}
-                      />
-                    ) : (
-                      <ScenarioNodes
-                        scenarios={scenarios}
-                        showPrimary={showPrimaryFlag}
-                        openEditDialog={openEditDialog}
-                      />
-                    )}
-                  </>
-                );
-              })
-            : null}
-        </TreeView>
+  const onDeleteScenario = useCallback(() => {
+    submit(actionEdit, true, { id: scenario && scenario[0] });
+    setConfirmDialogOpen(false);
+    close();
+  }, [close, actionEdit, scenario]);
 
-        {showAddButton ? (
-          <Button variant="outlined" onClick={onDialogOpen} fullWidth>
-            ADD SCENARIO &nbsp;&nbsp;
-            <Add />
-          </Button>
-        ) : null}
+  const onConfirmDialogOpen = useCallback(() => setConfirmDialogOpen(true), []);
 
-        <Box>{props.error}</Box>
-      </Box>
+  const onConfirmDialogClose = useCallback(() => setConfirmDialogOpen(false), []);
 
-      <Dialog onClose={onDialogClose} open={open}>
+  return (
+    <>
+      <Dialog onClose={close} open={open}>
         <DialogTitle>
-          <Grid
-            container
-            direction="row"
-            justifyContent="space-between"
-            alignItems="center"
-          >
-            <Typography variant="h5">{`${
-              actionEdit ? `Edit` : `Create`
-            } scenario`}</Typography>
-            <IconButton
-              aria-label="close"
-              onClick={onDialogClose}
-              sx={IconButtonSx}
-            >
+          <Grid container direction="row" justifyContent="space-between" alignItems="center">
+            <Typography variant="h5">{`${actionEdit ? `Edit` : `Create`} scenario`}</Typography>
+            <IconButton aria-label="close" onClick={close} sx={IconButtonSx}>
               <Close />
             </IconButton>
           </Grid>
@@ -464,18 +302,15 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                       error={!!form.errors.config && form.touched.config}
                       disabled={actionEdit}
                     >
-                      {props.configs
-                        ? props.configs.map(([id, label]) => (
+                      {configs
+                        ? configs.map(([id, label]) => (
                             <MenuItem key={id} value={id}>
                               {label}
                             </MenuItem>
                           ))
                         : null}
                     </Select>
-                    <FormHelperText
-                      error={!!form.errors.config && form.touched.config}
-                      sx={{ pl: 12 }}
-                    >
+                    <FormHelperText error={!!form.errors.config && form.touched.config} sx={{ pl: 12 }}>
                       {form.errors.config}
                     </FormHelperText>
                   </FormControl>
@@ -498,9 +333,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                     <DatePicker
                       label="Date"
                       value={new Date(form.values.date)}
-                      onChange={(date) =>
-                        form.setFieldValue("date", date?.toISOString())
-                      }
+                      onChange={(date) => form.setFieldValue("date", date?.toISOString())}
                       disabled={actionEdit}
                     />
                   </LocalizationProvider>
@@ -511,40 +344,15 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
               </Grid>
               {properties
                 ? properties.map((item, index) => (
-                    <Grid
-                      item
-                      xs={12}
-                      key={item.id}
-                      container
-                      justifyContent="space-between"
-                    >
+                    <Grid item xs={12} key={item.id} container justifyContent="space-between">
                       <Grid item xs={4}>
-                        <TextField
-                          value={item.key}
-                          label="Key"
-                          variant="outlined"
-                          data-name="key"
-                          data-idx={index}
-                          onChange={updatePropertyField}
-                        />
+                        <TextField value={item.key} label="Key" variant="outlined" data-name="key" data-idx={index} onChange={updatePropertyField} />
                       </Grid>
                       <Grid item xs={5}>
-                        <TextField
-                          value={item.value}
-                          label="Value"
-                          variant="outlined"
-                          data-name="value"
-                          data-idx={index}
-                          onChange={updatePropertyField}
-                        />
+                        <TextField value={item.value} label="Value" variant="outlined" data-name="value" data-idx={index} onChange={updatePropertyField} />
                       </Grid>
                       <Grid item xs={2}>
-                        <Button
-                          variant="outlined"
-                          component="label"
-                          data-id={item.id}
-                          onClick={propertyDelete}
-                        >
+                        <Button variant="outlined" component="label" data-id={item.id} onClick={propertyDelete}>
                           <DeleteOutline />
                         </Button>
                       </Grid>
@@ -553,30 +361,13 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                 : null}
               <Grid item xs={12} container justifyContent="space-between">
                 <Grid item xs={4}>
-                  <TextField
-                    value={newProp.key}
-                    data-name="key"
-                    onChange={updatePropertyField}
-                    label="Key"
-                    variant="outlined"
-                  />
+                  <TextField value={newProp.key} data-name="key" onChange={updatePropertyField} label="Key" variant="outlined" />
                 </Grid>
                 <Grid item xs={5}>
-                  <TextField
-                    value={newProp.value}
-                    data-name="value"
-                    onChange={updatePropertyField}
-                    label="Value"
-                    variant="outlined"
-                  />
+                  <TextField value={newProp.value} data-name="value" onChange={updatePropertyField} label="Value" variant="outlined" />
                 </Grid>
                 <Grid item xs={2}>
-                  <Button
-                    variant="outlined"
-                    component="label"
-                    onClick={propertyAdd}
-                    disabled={!newProp.key || !newProp.value}
-                  >
+                  <Button variant="outlined" component="label" onClick={propertyAdd} disabled={!newProp.key || !newProp.value}>
                     <Add />
                   </Button>
                 </Grid>
@@ -588,32 +379,19 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
             <Grid container justifyContent="space-between" sx={ActionContentSx}>
               {actionEdit && (
                 <Grid item xs={6}>
-                  <Button
-                    variant="outlined"
-                    color="primary"
-                    onClick={onConfirmDialogOpen}
-                  >
+                  <Button variant="outlined" color="primary" onClick={onConfirmDialogOpen}>
                     DELETE
                   </Button>
                 </Grid>
               )}
-              <Grid
-                item
-                container
-                xs={actionEdit ? 6 : 12}
-                justifyContent="flex-end"
-              >
+              <Grid item container xs={actionEdit ? 6 : 12} justifyContent="flex-end">
                 <Grid item sx={CancelBtnSx}>
-                  <Button variant="outlined" onClick={onDialogClose}>
+                  <Button variant="outlined" onClick={close}>
                     CANCEL
                   </Button>
                 </Grid>
                 <Grid item>
-                  <Button
-                    variant="contained"
-                    type="submit"
-                    disabled={!form.values.config || !form.values.name}
-                  >
+                  <Button variant="contained" type="submit" disabled={!form.values.config || !form.values.name}>
                     {actionEdit ? "APPLY" : "CREATE"}
                   </Button>
                 </Grid>
@@ -625,45 +403,136 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
 
       <Dialog onClose={onConfirmDialogClose} open={confirmDialogOpen}>
         <DialogTitle>
-          <Grid
-            container
-            direction="row"
-            justifyContent="space-between"
-            alignItems="center"
-          >
+          <Grid container direction="row" justifyContent="space-between" alignItems="center">
             <Typography variant="h5">Delete Scenario</Typography>
-            <IconButton
-              aria-label="close"
-              onClick={onDialogClose}
-              sx={IconButtonSx}
-            >
+            <IconButton aria-label="close" onClick={onConfirmDialogClose} sx={IconButtonSx}>
               <Close />
             </IconButton>
           </Grid>
         </DialogTitle>
         <DialogContent dividers>
-          <Typography>
-            Are you sure you want to delete this scenario?
-          </Typography>
+          <Typography>Are you sure you want to delete this scenario?</Typography>
         </DialogContent>
 
         <DialogActions>
-          <Button
-            variant="outlined"
-            color="inherit"
-            onClick={onConfirmDialogClose}
-          >
+          <Button variant="outlined" color="inherit" onClick={onConfirmDialogClose}>
             CANCEL
           </Button>
-          <Button
-            variant="contained"
-            color="primary"
-            onClick={onDeleteScenario}
-          >
+          <Button variant="contained" color="primary" onClick={onDeleteScenario}>
             DELETE
           </Button>
         </DialogActions>
       </Dialog>
+    </>
+  );
+};
+
+const ScenarioSelector = (props: ScenarioSelectorProps) => {
+  const { id = "", scenarios = [], propagate = true } = props;
+  const [open, setOpen] = useState(false);
+  const [actionEdit, setActionEdit] = useState<boolean>(false);
+
+  const dispatch = useDispatch();
+  const module = useModule();
+
+  useDispatchRequestUpdateOnFirstRender(dispatch, "", module, props.updateVars);
+
+  const showAddButton = useDynamicProperty(props.showAddButton, props.defaultShowAddButton, true);
+  const displayCycles = useDynamicProperty(props.displayCycles, props.defaultDisplayCycles, true);
+  const showPrimaryFlag = useDynamicProperty(props.showPrimaryFlag, props.defaultShowPrimaryFlag, true);
+
+  const onDialogOpen = useCallback(() => {
+    setOpen(true);
+    setActionEdit(false);
+  }, []);
+
+  const onDialogClose = useCallback(() => {
+    setOpen(false);
+    setActionEdit(false);
+  }, []);
+
+  const openEditDialog = useCallback(
+    (e: React.MouseEvent<HTMLElement>) => {
+      const { id: scenId } = e.currentTarget?.dataset || {};
+      scenId && props.onScenarioSelect && dispatch(createSendActionNameAction(id, module, props.onScenarioSelect, scenId));
+      setOpen(true);
+      setActionEdit(true);
+      return false;
+    },
+    [props.onScenarioSelect]
+  );
+
+  const onSubmit = useCallback(
+    (...values: any[]) => dispatch(createSendActionNameAction(id, module, props.onScenarioCrud, ...values)),
+    [id, module, props.onScenarioCrud]
+  );
+
+  // Refresh on broadcast
+  useEffect(() => {
+    if (props.coreChanged?.scenario) {
+      const updateVar = getUpdateVar(props.updateVars, "scenarios");
+      updateVar && dispatch(createRequestUpdateAction(id, module, [updateVar], true));
+    }
+  }, [props.coreChanged, props.updateVars, module, dispatch]);
+
+  const onSelect = useCallback(
+    (e: React.SyntheticEvent, nodeIds: Array<string> | string) => {
+      const { cycle = false } = (e.currentTarget as HTMLElement)?.parentElement?.dataset || {};
+      if (cycle) {
+        return;
+      }
+      const scenariosVar = getUpdateVar(props.updateVars, "scenarios");
+      dispatch(createSendUpdateAction(props.updateVarName, nodeIds, module, props.onChange, propagate, scenariosVar));
+    },
+    [props.updateVarName, props.updateVars, props.onChange, propagate, module]
+  );
+
+  return (
+    <div>
+      <Box sx={MainBoxSx}>
+        <TreeView defaultCollapseIcon={<ExpandMore />} defaultExpandIcon={<ChevronRight />} sx={TreeViewSx} onNodeSelect={onSelect}>
+          {scenarios
+            ? scenarios.map((item) => {
+                const [id, label, scenarios, nodeType, _] = item;
+                return (
+                  <>
+                    {displayCycles ? (
+                      nodeType === NodeType.CYCLE ? (
+                        <TreeItem key={id} nodeId={id} label={label} sx={CycleSx} data-cycle>
+                          <ScenarioNodes scenarios={scenarios} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
+                        </TreeItem>
+                      ) : (
+                        <ScenarioNodes scenarios={item as Scenario} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
+                      )
+                    ) : nodeType === NodeType.SCENARIO ? (
+                      <ScenarioNodes scenarios={item as Scenario} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
+                    ) : (
+                      <ScenarioNodes scenarios={scenarios} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
+                    )}
+                  </>
+                );
+              })
+            : null}
+        </TreeView>
+
+        {showAddButton ? (
+          <Button variant="outlined" onClick={onDialogOpen} fullWidth>
+            ADD SCENARIO &nbsp;&nbsp;
+            <Add />
+          </Button>
+        ) : null}
+
+        <Box>{props.error}</Box>
+      </Box>
+
+      <ScenarioEditDialog
+        close={onDialogClose}
+        actionEdit={actionEdit}
+        open={open}
+        configs={props.configs}
+        scenario={props.scenarioEdit}
+        submit={onSubmit}
+      ></ScenarioEditDialog>
     </div>
   );
 };

+ 79 - 31
src/taipy/gui_core/GuiCoreLib.py

@@ -24,6 +24,8 @@ from taipy.gui.utils import _TaipyBase
 
 
 class GuiCoreScenarioAdapter(_TaipyBase):
+    __INNER_PROPS = ["name"]
+
     def get(self):
         data = super().get()
         if isinstance(data, Scenario):
@@ -32,9 +34,9 @@ class GuiCoreScenarioAdapter(_TaipyBase):
                 data.is_primary,
                 data.config_id,
                 data.creation_date,
-                data.get_label(),
+                data.get_simple_label(),
                 list(data.tags),
-                list(data.properties.items()),
+                [(k, v) for k, v in data.properties.items() if k not in GuiCoreScenarioAdapter.__INNER_PROPS],
                 [(p.id, p.get_simple_label()) for p in data.pipelines.values()],
                 list(data.properties.get("authorized_tags", set())),
             ]
@@ -46,12 +48,14 @@ class GuiCoreScenarioAdapter(_TaipyBase):
 
 
 class GuiCoreContext(CoreEventConsumerBase):
+    __PROP_SCENARIO_ID = "id"
     __PROP_SCENARIO_CONFIG_ID = "config"
     __PROP_SCENARIO_DATE = "date"
     __PROP_SCENARIO_NAME = "name"
     __SCENARIO_PROPS = (__PROP_SCENARIO_CONFIG_ID, __PROP_SCENARIO_DATE, __PROP_SCENARIO_NAME)
     _CORE_CHANGED_NAME = "core_changed"
     _ERROR_VAR = "gui_core_error"
+    _SCENARIO_SELECTOR_ID_VAR = "gui_core_sc_id"
 
     def __init__(self, gui: Gui) -> None:
         self.gui = gui
@@ -85,6 +89,21 @@ class GuiCoreContext(CoreEventConsumerBase):
                     self.cycles_scenarios.append(cycle)
         return self.cycles_scenarios
 
+    def select_scenario(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
+        args = payload.get("args")
+        if args is None or not isinstance(args, list) or len(args) == 0:
+            return
+        scenario_id = args[0]
+        state.assign(GuiCoreContext._SCENARIO_SELECTOR_ID_VAR, scenario_id)
+
+    def get_scenario_by_id(self, id: str) -> t.Optional[Scenario]:
+        if not id:
+            return None
+        try:
+            return tp.get(id)
+        except Exception:
+            return None
+
     def get_scenario_configs(self):
         if self.scenario_configs is None:
             configs = tp.Config.scenarios
@@ -92,33 +111,58 @@ class GuiCoreContext(CoreEventConsumerBase):
                 self.scenario_configs = [(id, f"{c.id}") for id, c in configs.items()]
         return self.scenario_configs
 
-    def create_new_scenario(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
+    def crud_scenario(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
         args = payload.get("args")
-        if args is None or not isinstance(args, list) or len(args) == 0 or not isinstance(args[0], dict):
+        if (
+            args is None
+            or not isinstance(args, list)
+            or len(args) < 3
+            or not isinstance(args[0], bool)
+            or not isinstance(args[1], bool)
+            or not isinstance(args[2], dict)
+        ):
             return
-        data = args[0]
-        config_id = data.get(GuiCoreContext.__PROP_SCENARIO_CONFIG_ID)
-        scenario_config = tp.Config.scenarios.get(config_id)
-        if scenario_config is None:
-            state.assign(GuiCoreContext._ERROR_VAR, f"Invalid configuration id ({config_id})")
-            return
-        date_str = data.get(GuiCoreContext.__PROP_SCENARIO_DATE)
-        try:
-            date = parser.parse(date_str) if isinstance(date_str, str) else None
-        except Exception as e:
-            state.assign(GuiCoreContext._ERROR_VAR, f"Invalid date ({date_str}).{e}")
-            return
-        try:
-            scenario = tp.create_scenario(scenario_config, date, data.get(GuiCoreContext.__PROP_SCENARIO_NAME))
-            if props := data.get("properties"):
-                with scenario as sc:
-                    for prop in props:
-                        key = prop.get("key")
-                        if key and key not in GuiCoreContext.__SCENARIO_PROPS:
-                            sc._properties[key] = prop.get("value")
-            state.assign(GuiCoreContext._ERROR_VAR, "")
-        except Exception as e:
-            state.assign(GuiCoreContext._ERROR_VAR, f"Error creating Scenario. {e}")
+        update = args[0]
+        delete = args[1]
+        data = args[2]
+        name = data.get(GuiCoreContext.__PROP_SCENARIO_NAME)
+        if update:
+            scenario_id = data.get(GuiCoreContext.__PROP_SCENARIO_ID)
+            if delete:
+                try:
+                    tp.delete(scenario_id)
+                except Exception as e:
+                    state.assign(GuiCoreContext._ERROR_VAR, f"Error deleting Scenario. {e}")
+            else:
+                scenario = tp.get(scenario_id)
+        else:
+            config_id = data.get(GuiCoreContext.__PROP_SCENARIO_CONFIG_ID)
+            scenario_config = tp.Config.scenarios.get(config_id)
+            if scenario_config is None:
+                state.assign(GuiCoreContext._ERROR_VAR, f"Invalid configuration id ({config_id})")
+                return
+            date_str = data.get(GuiCoreContext.__PROP_SCENARIO_DATE)
+            try:
+                date = parser.parse(date_str) if isinstance(date_str, str) else None
+            except Exception as e:
+                state.assign(GuiCoreContext._ERROR_VAR, f"Invalid date ({date_str}).{e}")
+                return
+            try:
+                scenario = tp.create_scenario(scenario_config, date, name)
+            except Exception as e:
+                state.assign(GuiCoreContext._ERROR_VAR, f"Error creating Scenario. {e}")
+        if scenario:
+            with scenario as sc:
+                sc._properties[GuiCoreContext.__PROP_SCENARIO_NAME] = name
+                if props := data.get("properties"):
+                    try:
+                        for prop in props:
+                            key = prop.get("key")
+                            if key and key not in GuiCoreContext.__SCENARIO_PROPS:
+                                sc._properties[key] = prop.get("value")
+                        state.assign(GuiCoreContext._ERROR_VAR, "")
+                    except Exception as e:
+                        state.assign(GuiCoreContext._ERROR_VAR, f"Error creating Scenario. {e}")
 
     def broadcast_core_changed(self):
         self.gui.broadcast(GuiCoreContext._CORE_CHANGED_NAME, "")
@@ -137,18 +181,20 @@ class GuiCore(ElementLibrary):
                 "show_primary_flag": ElementProperty(PropertyType.dynamic_boolean, True),
                 "value": ElementProperty(PropertyType.lov_value),
                 "on_change": ElementProperty(PropertyType.function),
-                "scenario": ElementProperty(GuiCoreScenarioAdapter),
             },
             inner_properties={
                 "scenarios": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_scenarios()}}"),
-                "on_scenario_create": ElementProperty(
-                    PropertyType.function, f"{{{__CTX_VAR_NAME}.create_new_scenario}}"
-                ),
+                "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
                 "error": ElementProperty(PropertyType.react, f"{{{GuiCoreContext._ERROR_VAR}}}"),
                 "type": ElementProperty(PropertyType.inner, Scenario),
                 "adapter": ElementProperty(PropertyType.inner, GuiCoreContext.scenario_adapter),
+                "scenario_edit": ElementProperty(
+                    GuiCoreScenarioAdapter,
+                    f"{{{__CTX_VAR_NAME}.get_scenario_by_id({GuiCoreContext._SCENARIO_SELECTOR_ID_VAR})}}",
+                ),
+                "on_scenario_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.select_scenario}}"),
             },
         )
     }
@@ -168,3 +214,5 @@ class GuiCore(ElementLibrary):
     def on_user_init(self, state: State):
         state._add_attribute(GuiCoreContext._ERROR_VAR)
         state._gui._bind_var_val(GuiCoreContext._ERROR_VAR, "")
+        state._add_attribute(GuiCoreContext._SCENARIO_SELECTOR_ID_VAR)
+        state._gui._bind_var_val(GuiCoreContext._SCENARIO_SELECTOR_ID_VAR, "")

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác