浏览代码

Merge branch 'develop' into gmarabout/improve_dispatcher_shutdown

Grégoire Marabout 1 年之前
父节点
当前提交
ee5487d408

文件差异内容过多而无法显示
+ 264 - 251
frontend/taipy-gui/package-lock.json


+ 2 - 2
frontend/taipy-gui/package.json

@@ -88,8 +88,8 @@
     "@types/react-window-infinite-loader": "^1.0.5",
     "@types/sprintf-js": "^1.1.2",
     "@types/uuid": "^9.0.0",
-    "@typescript-eslint/eslint-plugin": "^6.7.0",
-    "@typescript-eslint/parser": "^6.7.0",
+    "@typescript-eslint/eslint-plugin": "^7.0.1",
+    "@typescript-eslint/parser": "^7.0.1",
     "add-asset-html-webpack-plugin": "^6.0.0",
     "autoprefixer": "^10.4.0",
     "copy-webpack-plugin": "^12.0.1",

+ 19 - 0
frontend/taipy-gui/src/components/Taipy/Input.spec.tsx

@@ -90,6 +90,25 @@ describe("Input Component", () => {
             type: "SEND_ACTION_ACTION",
         });
     });
+    it("dispatch a well formed update message with change_delay=-1", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const { getByDisplayValue } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Input value="Val" type="text" updateVarName="varname" changeDelay={-1} />
+            </TaipyContext.Provider>
+        );
+        const elt = getByDisplayValue("Val");
+        await userEvent.click(elt);
+        await userEvent.keyboard("data{Enter}");
+        await waitFor(() => expect(dispatch).toHaveBeenCalled());
+        expect(dispatch).toHaveBeenLastCalledWith({
+            name: "varname",
+            payload: { value: "Valdata" },
+            propagate: true,
+            type: "SEND_UPDATE_ACTION",
+        });
+    });
     it("dispatch a no action message on unsupported key", async () => {
         const dispatch = jest.fn();
         const state: TaipyState = INITIAL_STATE;

+ 13 - 9
frontend/taipy-gui/src/components/Taipy/Input.tsx

@@ -48,10 +48,10 @@ const Input = (props: TaipyInputProps) => {
     const [value, setValue] = useState(defaultValue);
     const dispatch = useDispatch();
     const delayCall = useRef(-1);
-    const [actionKeys] = useState(() => (onAction ? getActionKeys(props.actionKeys) : []));
+    const [actionKeys] = useState(() => getActionKeys(props.actionKeys));
     const module = useModule();
 
-    const changeDelay = typeof props.changeDelay === "number" && props.changeDelay >= 0 ? props.changeDelay : 300;
+    const changeDelay = typeof props.changeDelay === "number" ? (props.changeDelay >= 0 ? props.changeDelay : -1) : 300;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
@@ -60,7 +60,11 @@ const Input = (props: TaipyInputProps) => {
         (e: React.ChangeEvent<HTMLInputElement>) => {
             const val = e.target.value;
             setValue(val);
-            if (changeDelay) {
+            if (changeDelay === 0) {
+                dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
+                return;
+            }
+            if (changeDelay > 0) {
                 if (delayCall.current > 0) {
                     clearTimeout(delayCall.current);
                 }
@@ -68,8 +72,6 @@ const Input = (props: TaipyInputProps) => {
                     delayCall.current = -1;
                     dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
                 }, changeDelay);
-            } else {
-                dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
             }
         },
         [updateVarName, dispatch, propagate, onChange, changeDelay, module]
@@ -77,14 +79,16 @@ const Input = (props: TaipyInputProps) => {
 
     const handleAction = useCallback(
         (evt: KeyboardEvent<HTMLDivElement>) => {
-            if (onAction && !evt.shiftKey && !evt.ctrlKey && !evt.altKey && actionKeys.includes(evt.key)) {
+            if (!evt.shiftKey && !evt.ctrlKey && !evt.altKey && actionKeys.includes(evt.key)) {
                 const val = evt.currentTarget.querySelector("input")?.value;
-                if (changeDelay && delayCall.current > 0) {
+                if (changeDelay > 0 && delayCall.current > 0) {
                     clearTimeout(delayCall.current);
                     delayCall.current = -1;
                     dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
+                } else if (changeDelay === -1) {
+                    dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate));
                 }
-                dispatch(createSendActionNameAction(id, module, onAction, evt.key, updateVarName, val));
+                onAction && dispatch(createSendActionNameAction(id, module, onAction, evt.key, updateVarName, val));
                 evt.preventDefault();
             }
         },
@@ -109,7 +113,7 @@ const Input = (props: TaipyInputProps) => {
                 label={props.label}
                 onChange={handleInput}
                 disabled={!active}
-                onKeyDown={onAction ? handleAction : undefined}
+                onKeyDown={handleAction}
                 multiline={multiline}
                 minRows={linesShown}
             />

文件差异内容过多而无法显示
+ 292 - 245
frontend/taipy/package-lock.json


+ 2 - 2
frontend/taipy/package.json

@@ -4,8 +4,8 @@
   "private": true,
   "devDependencies": {
     "@types/react": "^18.0.15",
-    "@typescript-eslint/eslint-plugin": "^6.0.0",
-    "@typescript-eslint/parser": "^6.0.0",
+    "@typescript-eslint/eslint-plugin": "^7.0.1",
+    "@typescript-eslint/parser": "^7.0.1",
     "child_process": "^1.0.2",
     "dotenv": "^16.0.3",
     "eslint": "^8.20.0",

+ 1 - 3
frontend/taipy/src/PropertiesEditor.tsx

@@ -22,7 +22,7 @@ import { DeleteOutline, CheckCircle, Cancel } from "@mui/icons-material";
 
 import { createSendActionNameAction, useDispatch, useModule } from "taipy-gui";
 
-import { FieldNoMaxWidth, IconPaddingSx, disableColor, hoverSx } from "./utils";
+import { DeleteIconSx, FieldNoMaxWidth, IconPaddingSx, disableColor, hoverSx } from "./utils";
 
 type Property = {
     id: string;
@@ -36,8 +36,6 @@ type PropertiesEditPayload = {
     deleted_properties?: Array<Partial<Property>>;
 };
 
-const DeleteIconSx = { height: 50, width: 50, p: 0 };
-
 interface PropertiesEditorProps {
     id?: string;
     entityId: string;

+ 250 - 117
frontend/taipy/src/ScenarioViewer.tsx

@@ -26,7 +26,13 @@ import InputAdornment from "@mui/material/InputAdornment";
 import TextField from "@mui/material/TextField";
 import Tooltip from "@mui/material/Tooltip";
 import Typography from "@mui/material/Typography";
-import { FlagOutlined, Send, CheckCircle, Cancel, ArrowForwardIosSharp } from "@mui/icons-material";
+import Add from "@mui/icons-material/Add";
+import ArrowForwardIosSharp from "@mui/icons-material/ArrowForwardIosSharp";
+import Cancel from "@mui/icons-material/Cancel";
+import CheckCircle from "@mui/icons-material/CheckCircle";
+import DeleteOutline from "@mui/icons-material/DeleteOutline";
+import FlagOutlined from "@mui/icons-material/FlagOutlined";
+import Send from "@mui/icons-material/Send";
 import deepEqual from "fast-deep-equal/es6";
 
 import {
@@ -86,17 +92,19 @@ interface ScenarioViewerProps {
 interface SequencesRowProps {
     active: boolean;
     number: number;
-    id: string;
     label: string;
+    taskIds: string[];
+    tasks: Record<string, string>;
     enableScenarioFields: boolean;
-    submitEntity: (id: string) => void;
+    submitEntity: (label: string) => void;
     submit: boolean;
-    editLabel: (id: string, label: string) => void;
+    editSequence: (sLabel: string, label: string, taskIds: string[], del?: boolean) => void;
     onFocus: (e: MouseEvent<HTMLElement>) => void;
     focusName: string;
     setFocusName: (name: string) => void;
     submittable: boolean;
     editable: boolean;
+    isValid: (sLabel: string, label: string) => boolean;
 }
 
 const ChipSx = { ml: 1 };
@@ -108,113 +116,186 @@ const tagsAutocompleteSx = {
     maxWidth: "none",
 };
 
+type SequenceFull = [string, string[], boolean, boolean];
+// enum SeFProps {
+//     label,
+//     tasks,
+//     submittable,
+//     editable,
+// }
+
 const SequenceRow = ({
     active,
     number,
-    id,
-    label,
+    label: pLabel,
+    taskIds: pTaskIds,
+    tasks,
     submitEntity,
     enableScenarioFields,
     submit,
-    editLabel,
+    editSequence,
     onFocus,
     focusName,
     setFocusName,
     submittable,
     editable,
+    isValid,
 }: SequencesRowProps) => {
-    const [sequence, setSequence] = useState<string>(label);
+    const [label, setLabel] = useState("");
+    const [taskIds, setTaskIds] = useState<string[]>([]);
+    const [valid, setValid] = useState(false);
+
+    const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setLabel(e.currentTarget.value), []);
 
-    const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setSequence(e.currentTarget.value), []);
-    const onSaveField = useCallback(
+    const onSaveSequence = useCallback(
         (e?: MouseEvent<Element>) => {
             e && e.stopPropagation();
-            editLabel(id, sequence);
+            if (isValid(pLabel, label)) {
+                editSequence(pLabel, label, taskIds);
+            } else {
+                setValid(false);
+            }
         },
-        [id, sequence, editLabel]
+        [pLabel, label, taskIds, editSequence, isValid]
     );
-    const onCancelField = useCallback(
+    const onCancelSequence = useCallback(
         (e?: MouseEvent<Element>) => {
             e && e.stopPropagation();
-            setSequence(label);
+            setLabel(pLabel);
+            setTaskIds(pTaskIds);
             setFocusName("");
         },
-        [label, setFocusName]
+        [pLabel, pTaskIds, setFocusName]
     );
     const onSubmitSequence = useCallback(
         (e: MouseEvent<HTMLElement>) => {
             e.stopPropagation();
-            submitEntity(id);
+            submitEntity(pLabel);
         },
-        [submitEntity, id]
+        [submitEntity, pLabel]
     );
-    const onKeyDown = useCallback(
-        (e: KeyboardEvent<HTMLInputElement>) => {
-            if (!e.shiftKey && !e.ctrlKey && !e.altKey) {
-                if (e.key == "Enter") {
-                    onSaveField();
-                    e.preventDefault();
-                    e.stopPropagation();
-                } else if (e.key == "Escape") {
-                    onCancelField();
-                    e.preventDefault();
-                    e.stopPropagation();
-                }
-            }
+    const onDeleteSequence = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            editSequence(pLabel, "", [], true);
         },
-        [onSaveField, onCancelField]
+        [editSequence, pLabel]
     );
 
-    useEffect(() => setSequence(label), [label]);
+    useEffect(() => setValid(isValid(pLabel, label)), [pLabel, label, isValid]);
+
+    // Tasks
+    const onChangeTasks = useCallback((_: SyntheticEvent, taskIds: string[]) => setTaskIds(taskIds), []);
+    const getTaskLabel = useCallback((id: string) => tasks[id], [tasks]);
+
+    useEffect(() => {
+        setLabel(pLabel);
+        setTaskIds(pTaskIds);
+    }, [pLabel, pTaskIds]);
 
     const name = `sequence${number}`;
-    const disabled = !enableScenarioFields || !active || !submittable;
+    const disabled = !enableScenarioFields || !active;
+    const disabledSubmit = disabled || !submittable;
 
     return (
         <Grid item xs={12} container justifyContent="space-between" data-focus={name} onClick={onFocus} sx={hoverSx}>
-            <Grid item container xs={10}>
-                {active && editable && focusName === name ? (
-                    <TextField
-                        label={`Sequence ${number + 1}`}
-                        variant="outlined"
-                        value={sequence}
-                        onChange={onChange}
-                        sx={FieldNoMaxWidth}
-                        disabled={!enableScenarioFields || !active}
-                        fullWidth
-                        InputProps={{
-                            onKeyDown: onKeyDown,
-                            endAdornment: (
-                                <InputAdornment position="end">
-                                    <Tooltip title="Apply">
-                                        <IconButton sx={IconPaddingSx} onClick={onSaveField} size="small">
-                                            <CheckCircle color="primary" />
-                                        </IconButton>
-                                    </Tooltip>
-                                    <Tooltip title="Cancel">
-                                        <IconButton sx={IconPaddingSx} onClick={onCancelField} size="small">
-                                            <Cancel color="inherit" />
-                                        </IconButton>
-                                    </Tooltip>
-                                </InputAdornment>
-                            ),
-                        }}
-                    />
-                ) : (
-                    <Typography variant="subtitle2">{sequence}</Typography>
-                )}
-            </Grid>
-            <Grid item xs={2} container alignContent="center" alignItems="center" justifyContent="center">
-                {submit ? (
-                    <Tooltip title={disabled ? "Cannot submit Sequence" : "Submit Sequence"}>
-                        <span>
-                            <IconButton size="small" onClick={onSubmitSequence} disabled={disabled}>
-                                <Send color={disableColor("info", disabled)} />
+            {active && editable && focusName === name ? (
+                <>
+                    <Grid item xs={4}>
+                        <TextField
+                            label={`Sequence ${number + 1}`}
+                            variant="outlined"
+                            value={label}
+                            onChange={onChange}
+                            sx={FieldNoMaxWidth}
+                            disabled={disabled}
+                            fullWidth
+                            error={!valid}
+                            helperText={valid ? "" : label ? "This name is already used." : "Cannot be empty."}
+                        />
+                    </Grid>
+                    <Grid item xs={4}>
+                        <Autocomplete
+                            multiple
+                            options={Object.keys(tasks)}
+                            getOptionLabel={getTaskLabel}
+                            renderTags={(values: readonly string[], getTagProps) =>
+                                values.map((id: string, index: number) => {
+                                    return (
+                                        // eslint-disable-next-line react/jsx-key
+                                        <Chip
+                                            variant="outlined"
+                                            label={tasks[id]}
+                                            sx={IconPaddingSx}
+                                            {...getTagProps({ index })}
+                                        />
+                                    );
+                                })
+                            }
+                            value={taskIds}
+                            onChange={onChangeTasks}
+                            fullWidth
+                            renderInput={(params) => (
+                                <TextField
+                                    {...params}
+                                    variant="outlined"
+                                    label="Tasks"
+                                    sx={tagsAutocompleteSx}
+                                    fullWidth
+                                />
+                            )}
+                            disabled={disabled}
+                        />
+                    </Grid>
+                    <Grid item xs={2} container alignContent="center" alignItems="center" justifyContent="center">
+                        <Tooltip title="Apply">
+                            <IconButton sx={IconPaddingSx} onClick={onSaveSequence} size="small" disabled={!valid}>
+                                <CheckCircle color={disableColor("primary", !valid)} />
                             </IconButton>
-                        </span>
-                    </Tooltip>
-                ) : null}
-            </Grid>
+                        </Tooltip>
+                        <Tooltip title="Cancel">
+                            <IconButton sx={IconPaddingSx} onClick={onCancelSequence} size="small">
+                                <Cancel color="inherit" />
+                            </IconButton>
+                        </Tooltip>
+                    </Grid>
+                </>
+            ) : (
+                <>
+                    <Grid item xs={5}>
+                        <Typography variant="subtitle2">{label || "New Sequence"}</Typography>
+                    </Grid>
+                    <Grid item xs={5}>
+                        {taskIds.map((id) =>
+                            tasks[id] ? <Chip key={id} label={tasks[id]} variant="outlined" /> : null
+                        )}
+                    </Grid>
+                    <Grid item xs={1} alignContent="center" alignItems="center" justifyContent="center">
+                        <Tooltip title={`Delete Sequence '${label}'`}>
+                            <span>
+                                <IconButton size="small" onClick={onDeleteSequence} disabled={disabled}>
+                                    <DeleteOutline color={disableColor("primary", disabled)} />
+                                </IconButton>
+                            </span>
+                        </Tooltip>
+                    </Grid>
+                    <Grid item xs={1} alignContent="center" alignItems="center" justifyContent="center">
+                        {pLabel && submit ? (
+                            <Tooltip
+                                title={
+                                    disabledSubmit ? `Cannot submit Sequence '${label}'` : `Submit Sequence '${label}'`
+                                }
+                            >
+                                <span>
+                                    <IconButton size="small" onClick={onSubmitSequence} disabled={disabledSubmit}>
+                                        <Send color={disableColor("info", disabledSubmit)} />
+                                    </IconButton>
+                                </span>
+                            </Tooltip>
+                        ) : null}
+                    </Grid>
+                </>
+            )}
         </Grid>
     );
 };
@@ -226,7 +307,24 @@ const getValidScenario = (scenar: ScenarioFull | ScenarioFull[]) =>
         ? (scenar[0] as ScenarioFull)
         : undefined;
 
-const invalidScenario: ScenarioFull = ["", false, "", "", "", "", [], [], [], [], false, false, false, false, false];
+const invalidScenario: ScenarioFull = [
+    "",
+    false,
+    "",
+    "",
+    "",
+    "",
+    [],
+    [],
+    [],
+    {},
+    [],
+    false,
+    false,
+    false,
+    false,
+    false,
+];
 
 const ScenarioViewer = (props: ScenarioViewerProps) => {
     const {
@@ -275,6 +373,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         scTags,
         scProperties,
         scSequences,
+        scTasks,
         scAuthorizedTags,
         scDeletable,
         scPromotable,
@@ -315,16 +414,17 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
 
     // submits
     const submitSequence = useCallback(
-        (sequenceId: string) => {
-            dispatch(
-                createSendActionNameAction(id, module, props.onSubmit, {
-                    id: sequenceId,
-                    on_submission_change: props.onSubmissionChange,
-                    type: "Sequence",
-                })
-            );
+        (label: string) => {
+            label &&
+                dispatch(
+                    createSendActionNameAction(id, module, props.onSubmit, {
+                        id: scId,
+                        sequence: label,
+                        on_submission_change: props.onSubmissionChange,
+                    })
+                );
         },
-        [props.onSubmit, props.onSubmissionChange, id, dispatch, module]
+        [scId, props.onSubmit, props.onSubmissionChange, id, dispatch, module]
     );
 
     const submitScenario = useCallback(
@@ -420,24 +520,43 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     );
 
     // sequences
+    const [sequences, setSequences] = useState<SequenceFull[]>([]);
     const editSequence = useCallback(
-        (id: string, label: string) => {
+        (seqLabel: string, label: string, taskIds: string[], del?: boolean) => {
             if (valid) {
-                dispatch(
-                    createSendActionNameAction(id, module, props.onEdit, { id: id, name: label, type: "Sequence" })
-                );
+                if (del || label) {
+                    dispatch(
+                        createSendActionNameAction(id, module, props.onEdit, {
+                            id: scId,
+                            sequence: seqLabel,
+                            name: label,
+                            task_ids: taskIds,
+                            del: !!del,
+                        })
+                    );
+                } else {
+                    setSequences((seqs) => seqs.filter((seq) => seq[0]));
+                }
                 setFocusName("");
             }
         },
-        [valid, props.onEdit, dispatch, module]
+        [valid, id, scId, props.onEdit, dispatch, module]
     );
+    const isValidSequence = useCallback(
+        (sLabel: string, label: string) => !!label && (sLabel == label || !sequences.find((seq) => seq[0] === label)),
+        [sequences]
+    );
+
+    const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], true, true]]), []);
 
     // on scenario change
     useEffect(() => {
         showTags && setTags(scTags);
         setLabel(scLabel);
+        showSequences && setSequences(scSequences);
         setUserExpanded(expanded && valid);
-    }, [scTags, scLabel, valid, showTags, expanded]);
+        setFocusName("");
+    }, [scTags, scLabel, scSequences, valid, showTags, showSequences, expanded]);
 
     // Refresh on broadcast
     useEffect(() => {
@@ -480,9 +599,15 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                 {showSubmit ? (
                                     <Tooltip title={disabled ? "Cannot submit Scenario" : "Submit Scenario"}>
                                         <span>
-                                            <IconButton sx={IconPaddingSx} onClick={submitScenario} disabled={disabled}>
-                                                <Send fontSize="medium" color={disableColor("info", disabled)} />
-                                            </IconButton>
+                                            <Button
+                                                onClick={submitScenario}
+                                                disabled={disabled}
+                                                endIcon={
+                                                    <Send fontSize="medium" color={disableColor("info", disabled)} />
+                                                }
+                                            >
+                                                Submit
+                                            </Button>
                                         </span>
                                     </Tooltip>
                                 ) : null}
@@ -679,31 +804,39 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                             {showSequences ? (
                                 <>
                                     <Grid item xs={12} container justifyContent="space-between">
-                                        <Typography variant="h6">Sequences</Typography>
+                                        <Grid item xs={9}>
+                                            <Typography variant="h6">Sequences</Typography>
+                                        </Grid>
+                                        <Grid item xs={3} sx={{ ml: "auto" }}>
+                                            <Button onClick={addSequenceHandler} endIcon={<Add />}>
+                                                Add
+                                            </Button>
+                                        </Grid>
                                     </Grid>
 
-                                    {scSequences &&
-                                        scSequences.map((item, index) => {
-                                            const [key, value, submittable, editable] = item;
-                                            return (
-                                                <SequenceRow
-                                                    active={active}
-                                                    number={index}
-                                                    id={key}
-                                                    label={value}
-                                                    key={key}
-                                                    submitEntity={submitSequence}
-                                                    enableScenarioFields={valid}
-                                                    submit={showSubmitSequences}
-                                                    editLabel={editSequence}
-                                                    onFocus={onFocus}
-                                                    focusName={focusName}
-                                                    setFocusName={setFocusName}
-                                                    submittable={submittable}
-                                                    editable={editable}
-                                                />
-                                            );
-                                        })}
+                                    {sequences.map((item, index) => {
+                                        const [label, taskIds, submittable, editable] = item;
+                                        return (
+                                            <SequenceRow
+                                                active={active}
+                                                number={index}
+                                                label={label}
+                                                taskIds={taskIds}
+                                                tasks={scTasks}
+                                                key={label}
+                                                submitEntity={submitSequence}
+                                                enableScenarioFields={valid}
+                                                submit={showSubmitSequences}
+                                                editSequence={editSequence}
+                                                onFocus={onFocus}
+                                                focusName={focusName}
+                                                setFocusName={setFocusName}
+                                                submittable={submittable}
+                                                editable={editable}
+                                                isValid={isValidSequence}
+                                            />
+                                        );
+                                    })}
 
                                     <Grid item xs={12}>
                                         <Divider />

+ 6 - 1
frontend/taipy/src/utils.ts

@@ -24,7 +24,8 @@ export type ScenarioFull = [
     string,     // label
     string[],   // tags
     Array<[string, string]>,    // properties
-    Array<[string, string, boolean, boolean]>,   // sequences
+    Array<[string, string[], boolean, boolean]>,   // sequences (label, task ids, submittable, editable)
+    Record<string, string>, // tasks (id: label)
     string[],   // authorized_tags
     boolean,    // deletable
     boolean,    // promotable
@@ -43,6 +44,7 @@ export enum ScFProps {
     tags,
     properties,
     sequences,
+    tasks,
     authorized_tags,
     deletable,
     promotable,
@@ -211,3 +213,6 @@ export const MenuProps = {
     },
 };
 export const selectSx = { m: 1, width: 300 };
+
+export const DeleteIconSx = { height: 50, width: 50, p: 0 };
+

+ 1 - 0
taipy/core/sequence/_sequence_manager.py

@@ -183,6 +183,7 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
             sequence_name, scenario_id = sequence_id.split(Scenario._ID_PREFIX)
             scenario_id = f"{Scenario._ID_PREFIX}{scenario_id}"
             sequence_name = sequence_name.split(Sequence._ID_PREFIX)[1].strip("_")
+            sequence_name = sequence_name.replace("TPSPACE", " ")
             return sequence_name, scenario_id
         except (ValueError, IndexError):
             cls._logger.error(f"SequenceId {sequence_id} is invalid.")

+ 1 - 1
taipy/core/sequence/sequence.py

@@ -74,7 +74,7 @@ class Sequence(_Entity, Submittable, _Labeled):
 
     @staticmethod
     def _new_id(sequence_name: str, scenario_id) -> SequenceId:
-        seq_id = sequence_name.replace(" ", "_")
+        seq_id = sequence_name.replace(" ", "TPSPACE")
         return SequenceId(Sequence._SEPARATOR.join([Sequence._ID_PREFIX, _validate_id(seq_id), scenario_id]))
 
     def __hash__(self):

+ 1 - 1
taipy/gui/viselements.json

@@ -1448,7 +1448,7 @@
             "name": "change_delay",
             "type": "int",
             "default_value": "<i>App config</i>",
-            "doc": "Minimum time between triggering two calls to the <i>on_change</i> callback.<br/>The default value is defined at the application configuration level by the <strong>change_delay</strong> configuration option. if None, the delay is set to 300 ms."
+            "doc": "Minimum time between triggering two calls to the <i>on_change</i> callback.<br/>The default value is defined at the application configuration level by the <strong>change_delay</strong> configuration option. if None, the delay is set to 300 ms.<br/>If set to -1, the input change is triggered only when the user presses the Enter key."
           },
           {
             "name": "on_action",

+ 10 - 2
taipy/gui_core/_adapters.py

@@ -77,11 +77,19 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                         if scenario.properties
                         else [],
                         [
-                            (p.id, p.get_simple_label(), is_submittable(p), is_editable(p))
-                            for p in scenario.sequences.values()
+                            (
+                                s.get_simple_label(),
+                                [t.id for t in s.tasks.values()] if hasattr(s, "tasks") else [],
+                                is_submittable(s),
+                                is_editable(s),
+                            )
+                            for s in scenario.sequences.values()
                         ]
                         if hasattr(scenario, "sequences") and scenario.sequences
                         else [],
+                        {t.id: t.get_simple_label() for t in scenario.tasks.values()}
+                        if hasattr(scenario, "tasks")
+                        else {},
                         list(scenario.properties.get("authorized_tags", [])) if scenario.properties else [],
                         is_deletable(scenario),
                         is_promotable(scenario),

+ 56 - 31
taipy/gui_core/_context.py

@@ -411,42 +411,64 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return
         data = args[0]
         entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
-        if not self.__check_readable_editable(
-            state, entity_id, data.get("type", "Scenario"), _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR
-        ):
+        sequence = data.get("sequence")
+        if not self.__check_readable_editable(state, entity_id, "Scenario", _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR):
             return
-        entity: t.Union[Scenario, Sequence] = core_get(entity_id)
-        if entity:
+        scenario: Scenario = core_get(entity_id)
+        if scenario:
             try:
-                if isinstance(entity, Scenario):
-                    primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
-                    if primary is True:
-                        if not is_promotable(entity):
+                if not sequence:
+                    if isinstance(sequence, str) and (name := data.get(_GuiCoreContext.__PROP_ENTITY_NAME)):
+                        scenario.add_sequence(name, data.get("task_ids"))
+                    else:
+                        primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
+                        if primary is True:
+                            if not is_promotable(scenario):
+                                state.assign(
+                                    _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Scenario {entity_id} is not promotable."
+                                )
+                                return
+                            set_primary(scenario)
+                        self.__edit_properties(scenario, data)
+                else:
+                    if data.get("del", False):
+                        scenario.remove_sequence(sequence)
+                    else:
+                        name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
+                        if sequence != name:
+                            scenario.rename_sequence(sequence, name)
+                        if seqEntity := scenario.sequences.get(name):
+                            seqEntity.tasks = data.get("task_ids")
+                            self.__edit_properties(seqEntity, data)
+                        else:
                             state.assign(
-                                _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Scenario {entity_id} is not promotable."
+                                _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
+                                f"Sequence {name} is not available in Scenario {entity_id}.",
                             )
                             return
-                        set_primary(entity)
-                self.__edit_properties(entity, data)
+
                 state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
             except Exception as e:
-                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error updating Scenario. {e}")
+                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error updating {type(scenario).__name__}. {e}")
 
     def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
         data = args[0]
-        entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
-        if not is_submittable(entity_id):
-            state.assign(
-                _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
-                f"{data.get('type', 'Scenario')} {entity_id} is not submittable.",
-            )
-            return
-        entity = core_get(entity_id)
-        if entity:
-            try:
+        try:
+            scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
+            entity = core_get(scenario_id)
+            if sequence := data.get("sequence"):
+                entity = entity.sequences.get(sequence)
+
+            if not is_submittable(entity):
+                state.assign(
+                    _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
+                    f"{'Sequence' if sequence else 'Scenario'} {sequence or scenario_id} is not submittable.",
+                )
+                return
+            if entity:
                 on_submission = data.get("on_submission_change")
                 submission_entity = core_submit(
                     entity,
@@ -462,8 +484,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             self.client_submission[submission_entity.id] = SubmissionStatus.SUBMITTED
                         self.submission_status_callback(submission_entity.id)
                 state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
-            except Exception as e:
-                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
+        except Exception as e:
+            state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
 
     def __do_datanodes_tree(self):
         if self.data_nodes_by_owner is None:
@@ -627,7 +649,10 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     ent.tags = {t for t in tags}
             name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
             if isinstance(name, str):
-                ent.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
+                if hasattr(ent, _GuiCoreContext.__PROP_ENTITY_NAME):
+                    setattr(ent, _GuiCoreContext.__PROP_ENTITY_NAME, name)
+                else:
+                    ent.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
             props = data.get("properties")
             if isinstance(props, (list, tuple)):
                 for prop in props:
@@ -720,12 +745,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return (None, None, None, f"Data unavailable for {dn.get_simple_label()}")
         return _DoNotUpdate()
 
-    def __check_readable_editable(self, state: State, id: str, type: str, var: str):
-        if not is_readable(t.cast(DataNodeId, id)):
-            state.assign(var, f"{type} {id} is not readable.")
+    def __check_readable_editable(self, state: State, id: str, ent_type: str, var: str):
+        if not is_readable(t.cast(ScenarioId, id)):
+            state.assign(var, f"{ent_type} {id} is not readable.")
             return False
-        if not is_editable(t.cast(DataNodeId, id)):
-            state.assign(var, f"{type} {id} is not editable.")
+        if not is_editable(t.cast(ScenarioId, id)):
+            state.assign(var, f"{ent_type} {id} is not editable.")
             return False
         return True
 

+ 5 - 5
tests/core/scenario/test_scenario.py

@@ -387,7 +387,7 @@ def test_add_rename_and_remove_sequences():
     sequence_1 = Sequence({"name": "seq_1"}, [task_1], SequenceId(f"SEQUENCE_seq_1_{scenario.id}"))
     sequence_2 = Sequence({"name": "seq_2"}, [task_1, task_2], SequenceId(f"SEQUENCE_seq_2_{scenario.id}"))
     new_seq_2 = Sequence({"name": "seq_2"}, [task_1, task_2], SequenceId(f"SEQUENCE_new_seq_2_{scenario.id}"))
-    sequence_3 = Sequence({"name": "seq_3"}, [task_1, task_5, task_3], SequenceId(f"SEQUENCE_seq_3_{scenario.id}"))
+    seq_3 = Sequence({"name": "seq 3"}, [task_1, task_5, task_3], SequenceId(f"SEQUENCE_seqTPSPACE3_{scenario.id}"))
 
     task_manager = _TaskManagerFactory._build_manager()
     data_manager = _DataManagerFactory._build_manager()
@@ -412,7 +412,7 @@ def test_add_rename_and_remove_sequences():
     assert scenario.sequences == {"seq_2": sequence_2}
 
     scenario.add_sequences({"seq_1": [task_1], "seq 3": [task_1, task_5, task_3]})
-    assert scenario.sequences == {"seq_2": sequence_2, "seq_1": sequence_1, "seq 3": sequence_3}
+    assert scenario.sequences == {"seq_2": sequence_2, "seq_1": sequence_1, "seq 3": seq_3}
 
     scenario.remove_sequences(["seq_2", "seq 3"])
     assert scenario.sequences == {"seq_1": sequence_1}
@@ -421,13 +421,13 @@ def test_add_rename_and_remove_sequences():
     assert scenario.sequences == {"seq_1": sequence_1, "seq_2": sequence_2}
 
     scenario.add_sequence("seq 3", [task_1.id, task_5.id, task_3.id])
-    assert scenario.sequences == {"seq_1": sequence_1, "seq_2": sequence_2, "seq 3": sequence_3}
+    assert scenario.sequences == {"seq_1": sequence_1, "seq_2": sequence_2, "seq 3": seq_3}
 
     scenario.remove_sequence("seq_1")
-    assert scenario.sequences == {"seq_2": sequence_2, "seq 3": sequence_3}
+    assert scenario.sequences == {"seq_2": sequence_2, "seq 3": seq_3}
 
     scenario.rename_sequence("seq_2", "new_seq_2")
-    assert scenario.sequences == {"new_seq_2": new_seq_2, "seq 3": sequence_3}
+    assert scenario.sequences == {"new_seq_2": new_seq_2, "seq 3": seq_3}
 
 
 def test_update_sequence(data_node):

部分文件因为文件数量过多而无法显示