Переглянути джерело

Merge branch 'develop' into feature/allow-custom-templates-on-enterprise

trgiangdo 11 місяців тому
батько
коміт
85474ae3e0
33 змінених файлів з 1404 додано та 616 видалено
  1. 1 1
      .github/ISSUE_TEMPLATE/feature-request.yml
  2. 1 1
      CODE_OF_CONDUCT.md
  3. 258 275
      frontend/taipy-gui/package-lock.json
  4. 15 0
      frontend/taipy-gui/packaging/taipy-gui.d.ts
  5. 170 0
      frontend/taipy-gui/src/components/Taipy/TableSort.spec.tsx
  6. 265 0
      frontend/taipy-gui/src/components/Taipy/TableSort.tsx
  7. 3 0
      frontend/taipy-gui/src/extensions/exports.ts
  8. 127 124
      frontend/taipy/package-lock.json
  9. 267 29
      frontend/taipy/src/CoreSelector.tsx
  10. 2 0
      frontend/taipy/src/DataNodeViewer.tsx
  11. 1 2
      frontend/taipy/src/JobSelector.tsx
  12. 2 0
      frontend/taipy/src/NodeSelector.tsx
  13. 6 51
      frontend/taipy/src/ScenarioSelector.tsx
  14. 1 1
      taipy/config/CODE_OF_CONDUCT.md
  15. 1 1
      taipy/core/CODE_OF_CONDUCT.md
  16. 1 3
      taipy/core/_entity/_reload.py
  17. 4 3
      taipy/core/_orchestrator/_orchestrator.py
  18. 1 1
      taipy/core/_repository/_sql_repository.py
  19. 1 0
      taipy/core/notification/notifier.py
  20. 88 0
      taipy/core/submission/_submission_manager.py
  21. 2 65
      taipy/core/submission/submission.py
  22. 1 1
      taipy/gui/CODE_OF_CONDUCT.md
  23. 21 4
      taipy/gui/_renderers/builder.py
  24. 4 2
      taipy/gui/extension/library.py
  25. 9 3
      taipy/gui_core/_GuiCoreLib.py
  26. 56 16
      taipy/gui_core/_adapters.py
  27. 50 20
      taipy/gui_core/_context.py
  28. 18 5
      taipy/gui_core/viselements.json
  29. 1 1
      taipy/rest/CODE_OF_CONDUCT.md
  30. 1 1
      taipy/templates/CODE_OF_CONDUCT.md
  31. 1 1
      tests/config/common/test_template_handler.py
  32. 16 0
      tests/core/notification/test_notifier.py
  33. 9 5
      tests/core/submission/test_submission.py

+ 1 - 1
.github/ISSUE_TEMPLATE/feature-request.yml

@@ -1,6 +1,6 @@
 name: 💡 Feature Request
 name: 💡 Feature Request
 description: Have any new idea or new feature for Taipy? Please suggest!
 description: Have any new idea or new feature for Taipy? Please suggest!
-title: "[Feature] <write a small description here>"
+title: "<write a small description here>"
 labels: ["✨New feature"]
 labels: ["✨New feature"]
 body:
 body:
   - type: markdown
   - type: markdown

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 and orientation.
 
 

Різницю між файлами не показано, бо вона завелика
+ 258 - 275
frontend/taipy-gui/package-lock.json


+ 15 - 0
frontend/taipy-gui/packaging/taipy-gui.d.ts

@@ -132,6 +132,21 @@ export interface TableFilterProps {
 }
 }
 export declare const TableFilter: (props: TableFilterProps) => JSX.Element;
 export declare const TableFilter: (props: TableFilterProps) => JSX.Element;
 
 
+export interface SortDesc {
+    col: string;
+    order: boolean;
+}
+
+export interface TableSortProps {
+    columns: Record<string, ColumnDesc>;
+    colsOrder?: Array<string>;
+    onValidate: (data: Array<SortDesc>) => void;
+    appliedSorts?: Array<SortDesc>;
+    className?: string;
+}
+
+export declare const TableSort: (props: TableSortProps) => JSX.Element;
+
 export declare const Router: () => JSX.Element;
 export declare const Router: () => JSX.Element;
 
 
 /**
 /**

+ 170 - 0
frontend/taipy-gui/src/components/Taipy/TableSort.spec.tsx

@@ -0,0 +1,170 @@
+/*
+ * Copyright 2021-2024 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.
+ */
+
+import React from "react";
+import { getByTitle, render } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import userEvent from "@testing-library/user-event";
+
+import TableSort from "./TableSort";
+import { ColumnDesc } from "./tableUtils";
+
+const tableColumns = {
+    StringCol: { dfid: "StringCol", type: "object", index: 0, format: "", filter: true },
+    NumberCol: { dfid: "NumberCol", type: "int", index: 1, format: "", filter: true },
+    BoolCol: { dfid: "BoolCol", type: "bool", index: 2, format: "", filter: true },
+    DateCol: { dfid: "DateCol", type: "datetime", index: 3, format: "", filter: true },
+} as Record<string, ColumnDesc>;
+const colsOrder = ["StringCol", "NumberCol", "BoolCol", "DateCol"];
+
+beforeEach(() => {
+    // add window.matchMedia
+    // this is necessary for the date picker to be rendered in desktop mode.
+    // if this is not provided, the mobile mode is rendered, which might lead to unexpected behavior
+    Object.defineProperty(window, "matchMedia", {
+        writable: true,
+        value: (query: string): MediaQueryList => ({
+            media: query,
+            // this is the media query that @material-ui/pickers uses to determine if a device is a desktop device
+            matches: query === "(pointer: fine)",
+            onchange: () => {},
+            addEventListener: () => {},
+            removeEventListener: () => {},
+            addListener: () => {},
+            removeListener: () => {},
+            dispatchEvent: () => false,
+        }),
+    });
+});
+
+afterEach(() => {
+    // @ts-ignore
+    delete window.matchMedia;
+});
+
+describe("Table Filter Component", () => {
+    it("renders an icon", async () => {
+        const { getByTestId } = render(
+            <TableSort columns={tableColumns} colsOrder={colsOrder} onValidate={jest.fn()} />
+        );
+        const elt = getByTestId("SortByAlphaIcon");
+        expect(elt.parentElement?.parentElement?.tagName).toBe("BUTTON");
+    });
+    it("renders popover when clicked", async () => {
+        const { getByTestId, getAllByText, getAllByTestId } = render(
+            <TableSort columns={tableColumns} colsOrder={colsOrder} onValidate={jest.fn()} />
+        );
+        const elt = getByTestId("SortByAlphaIcon");
+        await userEvent.click(elt);
+        expect(getAllByText("Column")).toHaveLength(2);
+        const dropdownElts = getAllByTestId("ArrowDropDownIcon");
+        expect(dropdownElts).toHaveLength(1);
+        expect(getByTestId("CheckIcon").parentElement).toBeDisabled();
+        expect(getByTestId("DeleteIcon").parentElement).toBeDisabled();
+    });
+    it("behaves on column", async () => {
+        const { getByTestId, getAllByTestId, findByRole, getByText } = render(
+            <TableSort columns={tableColumns} colsOrder={colsOrder} onValidate={jest.fn()} />
+        );
+        const elt = getByTestId("SortByAlphaIcon");
+        await userEvent.click(elt);
+        const dropdownElts = getAllByTestId("ArrowDropDownIcon");
+        expect(dropdownElts).toHaveLength(1);
+        await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
+        await findByRole("listbox");
+        await userEvent.click(getByText("StringCol"));
+        await findByRole("checkbox");
+        const validate = getByTestId("CheckIcon").parentElement;
+        expect(validate).not.toBeDisabled();
+    });
+    it("adds a row on validation", async () => {
+        const onValidate = jest.fn();
+        const { getByTestId, getAllByTestId, findByRole, getByText } = render(
+            <TableSort columns={tableColumns} colsOrder={colsOrder} onValidate={onValidate} />
+        );
+        const elt = getByTestId("SortByAlphaIcon");
+        await userEvent.click(elt);
+        const dropdownElts = getAllByTestId("ArrowDropDownIcon");
+        expect(dropdownElts).toHaveLength(1);
+        await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
+        await findByRole("listbox");
+        await userEvent.click(getByText("StringCol"));
+        await findByRole("checkbox");
+        const validate = getByTestId("CheckIcon");
+        expect(validate.parentElement).not.toBeDisabled();
+        await userEvent.click(validate);
+        const ddElts = getAllByTestId("ArrowDropDownIcon");
+        expect(ddElts).toHaveLength(2);
+        getByText("1");
+        expect(onValidate).toHaveBeenCalled();
+    });
+    it("delete a row", async () => {
+        const onValidate = jest.fn();
+        const { getByTestId, getAllByTestId, findByRole, getByText } = render(
+            <TableSort columns={tableColumns} colsOrder={colsOrder} onValidate={onValidate} />
+        );
+        const elt = getByTestId("SortByAlphaIcon");
+        await userEvent.click(elt);
+        const dropdownElts = getAllByTestId("ArrowDropDownIcon");
+        expect(dropdownElts).toHaveLength(1);
+        await userEvent.click(dropdownElts[0].parentElement?.firstElementChild || dropdownElts[0]);
+        await findByRole("listbox");
+        await userEvent.click(getByText("StringCol"));
+        await findByRole("checkbox");
+        const validate = getByTestId("CheckIcon");
+        expect(validate.parentElement).not.toBeDisabled();
+        await userEvent.click(validate);
+        const ddElts = getAllByTestId("ArrowDropDownIcon");
+        expect(ddElts).toHaveLength(2);
+        const deletes = getAllByTestId("DeleteIcon");
+        expect(deletes).toHaveLength(2);
+        expect(deletes[0].parentElement).not.toBeDisabled();
+        expect(deletes[1].parentElement).toBeDisabled();
+        await userEvent.click(deletes[0]);
+        const ddElts2 = getAllByTestId("ArrowDropDownIcon");
+        expect(ddElts2).toHaveLength(1);
+    });
+    it("reset filters", async () => {
+        const onValidate = jest.fn();
+        const { getAllByTestId, getByTestId } = render(
+            <TableSort
+                columns={tableColumns}
+                colsOrder={colsOrder}
+                onValidate={onValidate}
+                appliedSorts={[{ col: "StringCol", order: true }]}
+            />
+        );
+        const elt = getByTestId("SortByAlphaIcon");
+        await userEvent.click(elt);
+        const deletes = getAllByTestId("DeleteIcon");
+        expect(deletes).toHaveLength(2);
+        expect(deletes[0].parentElement).not.toBeDisabled();
+        expect(deletes[1].parentElement).toBeDisabled();
+        await userEvent.click(deletes[0]);
+        expect(onValidate).toHaveBeenCalled();
+    });
+    it("ignores unapplicable filters", async () => {
+        const { getAllByTestId, getByTestId } = render(
+            <TableSort
+                columns={tableColumns}
+                colsOrder={colsOrder}
+                onValidate={jest.fn()}
+                appliedSorts={[{ col: "unknown col", order: true }]}
+            />
+        );
+        const elt = getByTestId("SortByAlphaIcon");
+        await userEvent.click(elt);
+        const ddElts2 = getAllByTestId("ArrowDropDownIcon");
+        expect(ddElts2).toHaveLength(1);
+    });
+});

+ 265 - 0
frontend/taipy-gui/src/components/Taipy/TableSort.tsx

@@ -0,0 +1,265 @@
+/*
+ * Copyright 2021-2024 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.
+ */
+
+import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import CheckIcon from "@mui/icons-material/Check";
+import DeleteIcon from "@mui/icons-material/Delete";
+import SortByAlpha from "@mui/icons-material/SortByAlpha";
+import Badge from "@mui/material/Badge";
+import FormControl from "@mui/material/FormControl";
+import Grid from "@mui/material/Grid";
+import IconButton from "@mui/material/IconButton";
+import InputLabel from "@mui/material/InputLabel";
+import MenuItem from "@mui/material/MenuItem";
+import OutlinedInput from "@mui/material/OutlinedInput";
+import Popover, { PopoverOrigin } from "@mui/material/Popover";
+import Select, { SelectChangeEvent } from "@mui/material/Select";
+import Switch from "@mui/material/Switch";
+import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
+
+import { ColumnDesc, getsortByIndex, iconInRowSx } from "./tableUtils";
+import { getSuffixedClassNames } from "./utils";
+
+export interface SortDesc {
+    col: string;
+    order: boolean;
+}
+
+interface TableSortProps {
+    columns: Record<string, ColumnDesc>;
+    colsOrder?: Array<string>;
+    onValidate: (data: Array<SortDesc>) => void;
+    appliedSorts?: Array<SortDesc>;
+    className?: string;
+}
+
+interface SortRowProps {
+    idx: number;
+    sort?: SortDesc;
+    columns: Record<string, ColumnDesc>;
+    colsOrder: Array<string>;
+    setSort: (idx: number, fd: SortDesc, remove?: boolean) => void;
+    appliedSorts?: Array<SortDesc>;
+}
+
+const anchorOrigin = {
+    vertical: "bottom",
+    horizontal: "right",
+} as PopoverOrigin;
+
+const gridSx = { p: "0.5em", minWidth: "36rem" };
+const badgeSx = {
+    "& .MuiBadge-badge": {
+        height: "10px",
+        minWidth: "10px",
+        width: "10px",
+        borderRadius: "5px",
+    },
+};
+const orderCaptionSx = { ml: 1 };
+
+const getSortDesc = (columns: Record<string, ColumnDesc>, colId?: string, asc?: boolean) =>
+    colId && asc !== undefined
+        ? ({
+              col: columns[colId].dfid,
+              order: !!asc,
+          } as SortDesc)
+        : undefined;
+
+const SortRow = (props: SortRowProps) => {
+    const { idx, setSort, columns, colsOrder, sort, appliedSorts } = props;
+
+    const [colId, setColId] = useState("");
+    const [order, setOrder] = useState(true); // true => asc
+    const [enableCheck, setEnableCheck] = useState(false);
+    const [enableDel, setEnableDel] = useState(false);
+
+    const cols = useMemo(() => {
+        if (!Array.isArray(appliedSorts) || appliedSorts.length == 0) {
+            return colsOrder;
+        }
+        return colsOrder.filter((col) => col == sort?.col || !appliedSorts.some((fd) => col === fd.col));
+    }, [colsOrder, appliedSorts, sort?.col]);
+
+    const onColSelect = useCallback(
+        (e: SelectChangeEvent<string>) => {
+            setColId(e.target.value);
+            setEnableCheck(!!getSortDesc(columns, e.target.value, order));
+        },
+        [columns, order]
+    );
+    const onOrderSwitch = useCallback(
+        (e: ChangeEvent<HTMLInputElement>) => {
+            setOrder(e.target.checked);
+            setEnableCheck(!!getSortDesc(columns, colId, e.target.checked));
+        },
+        [columns, colId]
+    );
+
+    const onDeleteClick = useCallback(() => setSort(idx, undefined as unknown as SortDesc, true), [idx, setSort]);
+    const onCheckClick = useCallback(() => {
+        const fd = getSortDesc(columns, colId, order);
+        fd && setSort(idx, fd);
+    }, [idx, setSort, columns, colId, order]);
+
+    useEffect(() => {
+        if (sort && idx > -1) {
+            const col = Object.keys(columns).find((col) => columns[col].dfid === sort.col) || "";
+            setColId(col);
+            setOrder(sort.order);
+            setEnableCheck(false);
+            setEnableDel(!!getSortDesc(columns, col, sort.order));
+        } else {
+            setColId("");
+            setOrder(true);
+            setEnableCheck(false);
+            setEnableDel(false);
+        }
+    }, [columns, sort, idx]);
+
+    return cols.length ? (
+        <Grid container item xs={12} alignItems="center">
+            <Grid item xs={6}>
+                <FormControl margin="dense">
+                    <InputLabel>Column</InputLabel>
+                    <Select value={colId || ""} onChange={onColSelect} input={<OutlinedInput label="Column" />}>
+                        {cols.map((col) => (
+                            <MenuItem key={col} value={col}>
+                                {columns[col].title || columns[col].dfid}
+                            </MenuItem>
+                        ))}
+                    </Select>
+                </FormControl>
+            </Grid>
+            <Grid item xs={4}>
+                <Switch checked={order} onChange={onOrderSwitch} />
+                <Typography variant="caption" color="text.secondary" sx={orderCaptionSx}>
+                    {order ? "asc" : "desc"}
+                </Typography>
+            </Grid>
+            <Grid item xs={1}>
+                <Tooltip title="Validate">
+                    <span>
+                        <IconButton onClick={onCheckClick} disabled={!enableCheck} sx={iconInRowSx}>
+                            <CheckIcon />
+                        </IconButton>
+                    </span>
+                </Tooltip>
+            </Grid>
+            <Grid item xs={1}>
+                <Tooltip title="Delete">
+                    <span>
+                        <IconButton onClick={onDeleteClick} disabled={!enableDel} sx={iconInRowSx}>
+                            <DeleteIcon />
+                        </IconButton>
+                    </span>
+                </Tooltip>
+            </Grid>
+        </Grid>
+    ) : null;
+};
+
+const TableSort = (props: TableSortProps) => {
+    const { onValidate, appliedSorts, columns, className = "" } = props;
+
+    const [showSort, setShowSort] = useState(false);
+    const sortRef = useRef<HTMLButtonElement | null>(null);
+    const [sorts, setSorts] = useState<Array<SortDesc>>([]);
+
+    const colsOrder = useMemo(() => {
+        if (props.colsOrder) {
+            return props.colsOrder;
+        }
+        return Object.keys(columns).sort(getsortByIndex(columns));
+    }, [props.colsOrder, columns]);
+
+    const onShowSortClick = useCallback(() => setShowSort((f) => !f), []);
+
+    const updateSort = useCallback(
+        (idx: number, nsd: SortDesc, remove?: boolean) => {
+            setSorts((sds) => {
+                let newSds;
+                if (idx > -1) {
+                    if (remove) {
+                        sds.splice(idx, 1);
+                        newSds = [...sds];
+                    } else {
+                        newSds = sds.map((fd, index) => (index == idx ? nsd : fd));
+                    }
+                } else if (remove) {
+                    newSds = sds;
+                } else {
+                    newSds = [...sds, nsd];
+                }
+                onValidate([...newSds]);
+                return newSds;
+            });
+        },
+        [onValidate]
+    );
+
+    useEffect(() => {
+        columns &&
+            appliedSorts &&
+            setSorts(appliedSorts.filter((fd) => Object.values(columns).some((cd) => cd.dfid === fd.col)));
+    }, [columns, appliedSorts]);
+
+    return (
+        <>
+            <Tooltip title={`${sorts.length} sort${sorts.length > 1 ? "s" : ""} applied`}>
+                <IconButton
+                    onClick={onShowSortClick}
+                    size="small"
+                    ref={sortRef}
+                    sx={iconInRowSx}
+                    className={getSuffixedClassNames(className, "-sort-icon")}
+                >
+                    <Badge badgeContent={sorts.length} color="primary" sx={badgeSx}>
+                        <SortByAlpha fontSize="inherit" />
+                    </Badge>
+                </IconButton>
+            </Tooltip>
+            <Popover
+                anchorEl={sortRef.current}
+                anchorOrigin={anchorOrigin}
+                open={showSort}
+                onClose={onShowSortClick}
+                className={getSuffixedClassNames(className, "-filter")}
+            >
+                <Grid container sx={gridSx} gap={0.5}>
+                    {sorts.map((sd, idx) => (
+                        <SortRow
+                            key={"fd" + idx}
+                            idx={idx}
+                            sort={sd}
+                            columns={columns}
+                            colsOrder={colsOrder}
+                            setSort={updateSort}
+                            appliedSorts={sorts}
+                        />
+                    ))}
+                    <SortRow
+                        idx={-(sorts.length + 1)}
+                        columns={columns}
+                        colsOrder={colsOrder}
+                        setSort={updateSort}
+                        appliedSorts={sorts}
+                    />
+                </Grid>
+            </Popover>
+        </>
+    );
+};
+
+export default TableSort;

+ 3 - 0
frontend/taipy-gui/src/extensions/exports.ts

@@ -17,6 +17,7 @@ import Login from "../components/Taipy/Login";
 import Router from "../components/Router";
 import Router from "../components/Router";
 import Table from "../components/Taipy/Table";
 import Table from "../components/Taipy/Table";
 import TableFilter, { FilterDesc } from "../components/Taipy/TableFilter";
 import TableFilter, { FilterDesc } from "../components/Taipy/TableFilter";
+import TableSort, { SortDesc } from "../components/Taipy/TableSort";
 import Metric from "../components/Taipy/Metric";
 import Metric from "../components/Taipy/Metric";
 import { useLovListMemo, LoV, LoVElt } from "../components/Taipy/lovUtils";
 import { useLovListMemo, LoV, LoVElt } from "../components/Taipy/lovUtils";
 import { LovItem } from "../utils/lov";
 import { LovItem } from "../utils/lov";
@@ -46,6 +47,7 @@ export {
     Router,
     Router,
     Table,
     Table,
     TableFilter,
     TableFilter,
+    TableSort,
     Metric,
     Metric,
     TaipyContext as Context,
     TaipyContext as Context,
     createRequestDataUpdateAction,
     createRequestDataUpdateAction,
@@ -70,6 +72,7 @@ export type {
     LovItem,
     LovItem,
     RowType,
     RowType,
     RowValue,
     RowValue,
+    SortDesc,
     TaipyStore as Store,
     TaipyStore as Store,
     TaipyState as State,
     TaipyState as State,
     TaipyBaseAction as Action,
     TaipyBaseAction as Action,

+ 127 - 124
frontend/taipy/package-lock.json

@@ -45,11 +45,11 @@
       "version": "3.2.0"
       "version": "3.2.0"
     },
     },
     "node_modules/@babel/code-frame": {
     "node_modules/@babel/code-frame": {
-      "version": "7.24.2",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
-      "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
+      "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==",
       "dependencies": {
       "dependencies": {
-        "@babel/highlight": "^7.24.2",
+        "@babel/highlight": "^7.24.6",
         "picocolors": "^1.0.0"
         "picocolors": "^1.0.0"
       },
       },
       "engines": {
       "engines": {
@@ -57,38 +57,38 @@
       }
       }
     },
     },
     "node_modules/@babel/helper-module-imports": {
     "node_modules/@babel/helper-module-imports": {
-      "version": "7.24.3",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
-      "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz",
+      "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==",
       "dependencies": {
       "dependencies": {
-        "@babel/types": "^7.24.0"
+        "@babel/types": "^7.24.6"
       },
       },
       "engines": {
       "engines": {
         "node": ">=6.9.0"
         "node": ">=6.9.0"
       }
       }
     },
     },
     "node_modules/@babel/helper-string-parser": {
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.24.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
-      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz",
+      "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==",
       "engines": {
       "engines": {
         "node": ">=6.9.0"
         "node": ">=6.9.0"
       }
       }
     },
     },
     "node_modules/@babel/helper-validator-identifier": {
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
-      "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz",
+      "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==",
       "engines": {
       "engines": {
         "node": ">=6.9.0"
         "node": ">=6.9.0"
       }
       }
     },
     },
     "node_modules/@babel/highlight": {
     "node_modules/@babel/highlight": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz",
-      "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz",
+      "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==",
       "dependencies": {
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.5",
+        "@babel/helper-validator-identifier": "^7.24.6",
         "chalk": "^2.4.2",
         "chalk": "^2.4.2",
         "js-tokens": "^4.0.0",
         "js-tokens": "^4.0.0",
         "picocolors": "^1.0.0"
         "picocolors": "^1.0.0"
@@ -162,9 +162,9 @@
       }
       }
     },
     },
     "node_modules/@babel/runtime": {
     "node_modules/@babel/runtime": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
-      "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz",
+      "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
       "dependencies": {
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
         "regenerator-runtime": "^0.14.0"
       },
       },
@@ -173,12 +173,12 @@
       }
       }
     },
     },
     "node_modules/@babel/types": {
     "node_modules/@babel/types": {
-      "version": "7.24.5",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz",
-      "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==",
+      "version": "7.24.6",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz",
+      "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==",
       "dependencies": {
       "dependencies": {
-        "@babel/helper-string-parser": "^7.24.1",
-        "@babel/helper-validator-identifier": "^7.24.5",
+        "@babel/helper-string-parser": "^7.24.6",
+        "@babel/helper-validator-identifier": "^7.24.6",
         "to-fast-properties": "^2.0.0"
         "to-fast-properties": "^2.0.0"
       },
       },
       "engines": {
       "engines": {
@@ -423,9 +423,9 @@
       }
       }
     },
     },
     "node_modules/@floating-ui/react-dom": {
     "node_modules/@floating-ui/react-dom": {
-      "version": "2.0.9",
-      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz",
-      "integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz",
+      "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==",
       "dependencies": {
       "dependencies": {
         "@floating-ui/dom": "^1.0.0"
         "@floating-ui/dom": "^1.0.0"
       },
       },
@@ -858,11 +858,11 @@
       }
       }
     },
     },
     "node_modules/@mui/x-date-pickers": {
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.4.0.tgz",
-      "integrity": "sha512-Xh0LD/PCYIWWSchvtnEHdUfIsnANA0QOppUkCJ+4b8mN7z+TMEBA/LHmzA2+edxo7eanyfJ7L52znxwPP4vX8Q==",
+      "version": "7.5.1",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.5.1.tgz",
+      "integrity": "sha512-O3K2pewxk5u9mK8PG0+xgIOAn+GSBRWHtU0ZbzaqCjS8ZbxNT2OhkI0aXqp/W2ECVwxYaGjwtjl3ypQIdqRvjw==",
       "dependencies": {
       "dependencies": {
-        "@babel/runtime": "^7.24.0",
+        "@babel/runtime": "^7.24.5",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@mui/utils": "^5.15.14",
@@ -923,11 +923,11 @@
       }
       }
     },
     },
     "node_modules/@mui/x-tree-view": {
     "node_modules/@mui/x-tree-view": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.4.0.tgz",
-      "integrity": "sha512-gUAZ21wUbc4cpk5sAsUjZNtdryxIVgVYRYiZsz8OTzDk82JUlGmULF6Tpex93NYI+tykkrz1+/4/Tg9MIIAKUg==",
+      "version": "7.5.1",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.5.1.tgz",
+      "integrity": "sha512-b4Lfclg1Lpa+kSs305snl/xFG5yOxq3/oVZEyPIseg8oOfl0r79UKqCIdO2iQqQydjUsUMbLtnR9TFypxlwCbQ==",
       "dependencies": {
       "dependencies": {
-        "@babel/runtime": "^7.24.0",
+        "@babel/runtime": "^7.24.5",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
         "@mui/utils": "^5.15.14",
@@ -1160,9 +1160,9 @@
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
     },
     },
     "node_modules/@types/react": {
     "node_modules/@types/react": {
-      "version": "18.3.2",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.2.tgz",
-      "integrity": "sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==",
+      "version": "18.3.3",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
+      "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
       "dependencies": {
       "dependencies": {
         "@types/prop-types": "*",
         "@types/prop-types": "*",
         "csstype": "^3.0.2"
         "csstype": "^3.0.2"
@@ -1192,16 +1192,16 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
-      "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
+      "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "7.9.0",
-        "@typescript-eslint/type-utils": "7.9.0",
-        "@typescript-eslint/utils": "7.9.0",
-        "@typescript-eslint/visitor-keys": "7.9.0",
+        "@typescript-eslint/scope-manager": "7.11.0",
+        "@typescript-eslint/type-utils": "7.11.0",
+        "@typescript-eslint/utils": "7.11.0",
+        "@typescript-eslint/visitor-keys": "7.11.0",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
         "natural-compare": "^1.4.0",
@@ -1225,15 +1225,15 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/parser": {
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
-      "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
+      "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.9.0",
-        "@typescript-eslint/types": "7.9.0",
-        "@typescript-eslint/typescript-estree": "7.9.0",
-        "@typescript-eslint/visitor-keys": "7.9.0",
+        "@typescript-eslint/scope-manager": "7.11.0",
+        "@typescript-eslint/types": "7.11.0",
+        "@typescript-eslint/typescript-estree": "7.11.0",
+        "@typescript-eslint/visitor-keys": "7.11.0",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
       },
       },
       "engines": {
       "engines": {
@@ -1253,13 +1253,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/scope-manager": {
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
-      "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
+      "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.9.0",
-        "@typescript-eslint/visitor-keys": "7.9.0"
+        "@typescript-eslint/types": "7.11.0",
+        "@typescript-eslint/visitor-keys": "7.11.0"
       },
       },
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1270,13 +1270,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/type-utils": {
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
-      "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
+      "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.9.0",
-        "@typescript-eslint/utils": "7.9.0",
+        "@typescript-eslint/typescript-estree": "7.11.0",
+        "@typescript-eslint/utils": "7.11.0",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
         "ts-api-utils": "^1.3.0"
       },
       },
@@ -1297,9 +1297,9 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/types": {
     "node_modules/@typescript-eslint/types": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
-      "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
+      "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
       "dev": true,
       "dev": true,
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1310,13 +1310,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/typescript-estree": {
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
-      "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
+      "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.9.0",
-        "@typescript-eslint/visitor-keys": "7.9.0",
+        "@typescript-eslint/types": "7.11.0",
+        "@typescript-eslint/visitor-keys": "7.11.0",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
         "is-glob": "^4.0.3",
@@ -1338,15 +1338,15 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/utils": {
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
-      "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
+      "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "7.9.0",
-        "@typescript-eslint/types": "7.9.0",
-        "@typescript-eslint/typescript-estree": "7.9.0"
+        "@typescript-eslint/scope-manager": "7.11.0",
+        "@typescript-eslint/types": "7.11.0",
+        "@typescript-eslint/typescript-estree": "7.11.0"
       },
       },
       "engines": {
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
         "node": "^18.18.0 || >=20.0.0"
@@ -1360,12 +1360,12 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/visitor-keys": {
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
-      "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
+      "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "7.9.0",
+        "@typescript-eslint/types": "7.11.0",
         "eslint-visitor-keys": "^3.4.3"
         "eslint-visitor-keys": "^3.4.3"
       },
       },
       "engines": {
       "engines": {
@@ -1648,9 +1648,9 @@
       }
       }
     },
     },
     "node_modules/ajv-formats/node_modules/ajv": {
     "node_modules/ajv-formats/node_modules/ajv": {
-      "version": "8.13.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz",
-      "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz",
+      "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
         "fast-deep-equal": "^3.1.3",
@@ -1901,12 +1901,12 @@
       }
       }
     },
     },
     "node_modules/braces": {
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       },
       "engines": {
       "engines": {
         "node": ">=8"
         "node": ">=8"
@@ -1978,9 +1978,9 @@
       }
       }
     },
     },
     "node_modules/caniuse-lite": {
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001620",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz",
-      "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==",
+      "version": "1.0.30001624",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001624.tgz",
+      "integrity": "sha512-0dWnQG87UevOCPYaOR49CBcLBwoZLpws+k6W37nLjWUhumP1Isusj0p2u+3KhjNloRWK9OKMgjBBzPujQHw4nA==",
       "dev": true,
       "dev": true,
       "funding": [
       "funding": [
         {
         {
@@ -2326,9 +2326,9 @@
       }
       }
     },
     },
     "node_modules/electron-to-chromium": {
     "node_modules/electron-to-chromium": {
-      "version": "1.4.772",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.772.tgz",
-      "integrity": "sha512-jFfEbxR/abTTJA3ci+2ok1NTuOBBtB4jH+UT6PUmRN+DY3WSD4FFRsgoVQ+QNIJ0T7wrXwzsWCI2WKC46b++2A==",
+      "version": "1.4.783",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz",
+      "integrity": "sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/enhanced-resolve": {
     "node_modules/enhanced-resolve": {
@@ -2471,9 +2471,9 @@
       }
       }
     },
     },
     "node_modules/es-module-lexer": {
     "node_modules/es-module-lexer": {
-      "version": "1.5.2",
-      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz",
-      "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA==",
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz",
+      "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/es-object-atoms": {
     "node_modules/es-object-atoms": {
@@ -2604,29 +2604,29 @@
       }
       }
     },
     },
     "node_modules/eslint-plugin-react": {
     "node_modules/eslint-plugin-react": {
-      "version": "7.34.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz",
-      "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==",
+      "version": "7.34.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz",
+      "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "array-includes": "^3.1.7",
-        "array.prototype.findlast": "^1.2.4",
+        "array-includes": "^3.1.8",
+        "array.prototype.findlast": "^1.2.5",
         "array.prototype.flatmap": "^1.3.2",
         "array.prototype.flatmap": "^1.3.2",
         "array.prototype.toreversed": "^1.1.2",
         "array.prototype.toreversed": "^1.1.2",
         "array.prototype.tosorted": "^1.1.3",
         "array.prototype.tosorted": "^1.1.3",
         "doctrine": "^2.1.0",
         "doctrine": "^2.1.0",
-        "es-iterator-helpers": "^1.0.17",
+        "es-iterator-helpers": "^1.0.19",
         "estraverse": "^5.3.0",
         "estraverse": "^5.3.0",
         "jsx-ast-utils": "^2.4.1 || ^3.0.0",
         "jsx-ast-utils": "^2.4.1 || ^3.0.0",
         "minimatch": "^3.1.2",
         "minimatch": "^3.1.2",
-        "object.entries": "^1.1.7",
-        "object.fromentries": "^2.0.7",
-        "object.hasown": "^1.1.3",
-        "object.values": "^1.1.7",
+        "object.entries": "^1.1.8",
+        "object.fromentries": "^2.0.8",
+        "object.hasown": "^1.1.4",
+        "object.values": "^1.2.0",
         "prop-types": "^15.8.1",
         "prop-types": "^15.8.1",
         "resolve": "^2.0.0-next.5",
         "resolve": "^2.0.0-next.5",
         "semver": "^6.3.1",
         "semver": "^6.3.1",
-        "string.prototype.matchall": "^4.0.10"
+        "string.prototype.matchall": "^4.0.11"
       },
       },
       "engines": {
       "engines": {
         "node": ">=4"
         "node": ">=4"
@@ -2746,12 +2746,12 @@
       }
       }
     },
     },
     "node_modules/eslint-webpack-plugin": {
     "node_modules/eslint-webpack-plugin": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.1.0.tgz",
-      "integrity": "sha512-C3wAG2jyockIhN0YRLuKieKj2nx/gnE/VHmoHemD5ifnAtY6ZU+jNPfzPoX4Zd6RIbUyWTiZUh/ofUlBhoAX7w==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.2.0.tgz",
+      "integrity": "sha512-rsfpFQ01AWQbqtjgPRr2usVRxhWDuG0YDYcG8DJOteD3EFnpeuYuOwk0PQiN7PRBTqS6ElNdtPZPggj8If9WnA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@types/eslint": "^8.56.5",
+        "@types/eslint": "^8.56.10",
         "jest-worker": "^29.7.0",
         "jest-worker": "^29.7.0",
         "micromatch": "^4.0.5",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
         "normalize-path": "^3.0.0",
@@ -2765,7 +2765,7 @@
         "url": "https://opencollective.com/webpack"
         "url": "https://opencollective.com/webpack"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "eslint": "^8.0.0",
+        "eslint": "^8.0.0 || ^9.0.0",
         "webpack": "^5.0.0"
         "webpack": "^5.0.0"
       }
       }
     },
     },
@@ -2935,9 +2935,9 @@
       }
       }
     },
     },
     "node_modules/fill-range": {
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "to-regex-range": "^5.0.1"
         "to-regex-range": "^5.0.1"
@@ -3110,6 +3110,7 @@
       "version": "7.2.3",
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "fs.realpath": "^1.0.0",
@@ -3403,6 +3404,7 @@
       "version": "1.0.6",
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
       "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "once": "^1.3.0",
         "once": "^1.3.0",
@@ -4045,12 +4047,12 @@
       }
       }
     },
     },
     "node_modules/micromatch": {
     "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
+      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "braces": "^3.0.2",
+        "braces": "^3.0.3",
         "picomatch": "^2.3.1"
         "picomatch": "^2.3.1"
       },
       },
       "engines": {
       "engines": {
@@ -4714,6 +4716,7 @@
       "version": "3.0.2",
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "glob": "^7.1.3"
         "glob": "^7.1.3"
@@ -4831,9 +4834,9 @@
       }
       }
     },
     },
     "node_modules/schema-utils/node_modules/ajv": {
     "node_modules/schema-utils/node_modules/ajv": {
-      "version": "8.13.0",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz",
-      "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==",
+      "version": "8.14.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz",
+      "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
         "fast-deep-equal": "^3.1.3",

+ 267 - 29
frontend/taipy/src/CoreSelector.tsx

@@ -11,16 +11,28 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { useCallback, SyntheticEvent, useState, useEffect, useMemo, ComponentType, MouseEvent } from "react";
-import { Theme, alpha } from "@mui/material";
+import React, {
+    useCallback,
+    SyntheticEvent,
+    useState,
+    useEffect,
+    useMemo,
+    ComponentType,
+    MouseEvent,
+    ChangeEvent,
+} from "react";
+import { TextField, Theme, alpha } from "@mui/material";
 import Badge, { BadgeOrigin } from "@mui/material/Badge";
 import Badge, { BadgeOrigin } from "@mui/material/Badge";
-import Box from "@mui/material/Box";
 import FormControlLabel from "@mui/material/FormControlLabel";
 import FormControlLabel from "@mui/material/FormControlLabel";
 import Grid from "@mui/material/Grid";
 import Grid from "@mui/material/Grid";
 import IconButton from "@mui/material/IconButton";
 import IconButton from "@mui/material/IconButton";
 import Switch from "@mui/material/Switch";
 import Switch from "@mui/material/Switch";
 import Tooltip from "@mui/material/Tooltip";
 import Tooltip from "@mui/material/Tooltip";
-import { ChevronRight, FlagOutlined, PushPinOutlined } from "@mui/icons-material";
+import ChevronRight from "@mui/icons-material/ChevronRight";
+import FlagOutlined from "@mui/icons-material/FlagOutlined";
+import PushPinOutlined from "@mui/icons-material/PushPinOutlined";
+import SearchOffOutlined from "@mui/icons-material/SearchOffOutlined";
+import SearchOutlined from "@mui/icons-material/SearchOutlined";
 import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView";
 import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView";
 import { TreeItem } from "@mui/x-tree-view/TreeItem";
 import { TreeItem } from "@mui/x-tree-view/TreeItem";
 
 
@@ -32,9 +44,14 @@ import {
     useDispatchRequestUpdateOnFirstRender,
     useDispatchRequestUpdateOnFirstRender,
     createRequestUpdateAction,
     createRequestUpdateAction,
     useDynamicProperty,
     useDynamicProperty,
+    ColumnDesc,
+    FilterDesc,
+    TableFilter,
+    SortDesc,
+    TableSort,
 } from "taipy-gui";
 } from "taipy-gui";
 
 
-import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence } from "./utils/types";
+import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence, Sequences } from "./utils/types";
 import {
 import {
     Cycle as CycleIcon,
     Cycle as CycleIcon,
     Datanode as DatanodeIcon,
     Datanode as DatanodeIcon,
@@ -48,6 +65,7 @@ import {
     EmptyArray,
     EmptyArray,
     FlagSx,
     FlagSx,
     ParentItemSx,
     ParentItemSx,
+    getUpdateVarNames,
     iconLabelSx,
     iconLabelSx,
     tinyIconButtonSx,
     tinyIconButtonSx,
     tinySelPinIconButtonSx,
     tinySelPinIconButtonSx,
@@ -89,6 +107,10 @@ interface CoreSelectorProps {
     editComponent?: ComponentType<EditProps>;
     editComponent?: ComponentType<EditProps>;
     showPins?: boolean;
     showPins?: boolean;
     onSelect?: (id: string | string[]) => void;
     onSelect?: (id: string | string[]) => void;
+    updateCoreVars: string;
+    filter?: string;
+    sort?: string;
+    showSearch: boolean;
 }
 }
 
 
 const tinyPinIconButtonSx = (theme: Theme) => ({
 const tinyPinIconButtonSx = (theme: Theme) => ({
@@ -102,7 +124,8 @@ const tinyPinIconButtonSx = (theme: Theme) => ({
     },
     },
 });
 });
 
 
-const switchBoxSx = { ml: 2 };
+const switchBoxSx = { ml: 2, width: (theme: Theme) => `calc(100% - ${theme.spacing(2)})` };
+const iconInRowSx = { fontSize: "body2.fontSize" };
 
 
 const CoreItem = (props: {
 const CoreItem = (props: {
     item: Entity;
     item: Entity;
@@ -243,6 +266,61 @@ const getExpandedIds = (nodeId: string, exp?: string[], entities?: Entities) =>
     return exp || [];
     return exp || [];
 };
 };
 
 
+const emptyEntity = [] as unknown as Entity;
+const filterTree = (entities: Entities, search: string, leafType: NodeType, count?: { nb: number }) => {
+    let top = false;
+    if (!count) {
+        count = { nb: 0 };
+        top = true;
+    }
+    const filtered = entities
+        .map((item) => {
+            const [, label, items, nodeType] = item;
+            if (nodeType !== leafType || label.toLowerCase().includes(search)) {
+                const newItem = [...item];
+                if (Array.isArray(items) && items.length) {
+                    newItem[2] = filterTree(items, search, leafType, count) as Scenarios | DataNodes | Sequences;
+                }
+                return newItem as Entity;
+            }
+            count.nb++;
+            return emptyEntity;
+        })
+        .filter((i) => (i as unknown[]).length !== 0);
+    if (top && count.nb == 0) {
+        return entities;
+    }
+    return filtered;
+};
+
+const localStoreSet = (val: string, ...ids: string[]) => {
+    const id = ids.filter(i => !!i).join(" ");
+    if (!id) {
+        return;
+    }
+    try {
+        id && localStorage && localStorage.setItem(id, val);
+    } catch (e) {
+        // Too bad
+    }
+};
+
+const localStoreGet = (...ids: string[]) => {
+    const id = ids.filter(i => !!i).join(" ");
+    if (!id) {
+        return undefined;
+    }
+    const val = localStorage && localStorage.getItem(id);
+    if (!val) {
+        return undefined;
+    }
+    try {
+        return JSON.parse(val);
+    } catch (e) {
+        return undefined;
+    }
+};
+
 const CoreSelector = (props: CoreSelectorProps) => {
 const CoreSelector = (props: CoreSelectorProps) => {
     const {
     const {
         id = "",
         id = "",
@@ -261,6 +339,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
         onChange,
         onChange,
         onSelect,
         onSelect,
         coreChanged,
         coreChanged,
+        updateCoreVars,
+        showSearch,
     } = props;
     } = props;
 
 
     const [selectedItems, setSelectedItems] = useState<string[]>([]);
     const [selectedItems, setSelectedItems] = useState<string[]>([]);
@@ -294,7 +374,11 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 const res = isSelected ? [...old, nodeId] : old.filter((id) => id !== nodeId);
                 const res = isSelected ? [...old, nodeId] : old.filter((id) => id !== nodeId);
                 const scenariosVar = getUpdateVar(updateVars, lovPropertyName);
                 const scenariosVar = getUpdateVar(updateVars, lovPropertyName);
                 const val = multiple ? res : isSelectable ? nodeId : "";
                 const val = multiple ? res : isSelectable ? nodeId : "";
-                setTimeout(() => dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, scenariosVar)), 1);
+                setTimeout(
+                    () =>
+                        dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, scenariosVar)),
+                    1
+                );
                 onSelect && isSelectable && onSelect(val);
                 onSelect && isSelectable && onSelect(val);
                 return res;
                 return res;
             });
             });
@@ -304,8 +388,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
 
 
     useEffect(() => {
     useEffect(() => {
         if (value !== undefined && value !== null) {
         if (value !== undefined && value !== null) {
-            setSelectedItems(Array.isArray(value) ? value : value ? [value]: []);
-            setExpandedItems((exp) => typeof value === "string" ? getExpandedIds(value, exp, props.entities) : exp);
+            setSelectedItems(Array.isArray(value) ? value : value ? [value] : []);
+            setExpandedItems((exp) => (typeof value === "string" ? getExpandedIds(value, exp, props.entities) : exp));
         } else if (defaultValue) {
         } else if (defaultValue) {
             try {
             try {
                 const parsedValue = JSON.parse(defaultValue);
                 const parsedValue = JSON.parse(defaultValue);
@@ -332,14 +416,25 @@ const CoreSelector = (props: CoreSelectorProps) => {
             setSelectedItems((old) => {
             setSelectedItems((old) => {
                 if (old.length) {
                 if (old.length) {
                     const lovVar = getUpdateVar(updateVars, lovPropertyName);
                     const lovVar = getUpdateVar(updateVars, lovPropertyName);
-                    setTimeout(() => dispatch(
-                        createSendUpdateAction(updateVarName, multiple ? [] : "", module, onChange, propagate, lovVar)
-                    ), 1);
+                    setTimeout(
+                        () =>
+                            dispatch(
+                                createSendUpdateAction(
+                                    updateVarName,
+                                    multiple ? [] : "",
+                                    module,
+                                    onChange,
+                                    propagate,
+                                    lovVar
+                                )
+                            ),
+                        1
+                    );
                     return [];
                     return [];
                 }
                 }
                 return old;
                 return old;
             });
             });
-            }
+        }
     }, [entities, updateVars, lovPropertyName, updateVarName, multiple, module, onChange, propagate, dispatch]);
     }, [entities, updateVars, lovPropertyName, updateVarName, multiple, module, onChange, propagate, dispatch]);
 
 
     // Refresh on broadcast
     // Refresh on broadcast
@@ -408,22 +503,165 @@ const CoreSelector = (props: CoreSelectorProps) => {
         [showPins, props.entities]
         [showPins, props.entities]
     );
     );
 
 
+    // filters
+    const colFilters = useMemo(() => {
+        try {
+            const res = props.filter ? (JSON.parse(props.filter) as Array<[string, string, string[]]>) : undefined;
+            return Array.isArray(res)
+                ? res.reduce((pv, [name, coltype, lov], idx) => {
+                      pv[name] = { dfid: name, type: coltype, index: idx, filter: true, lov: lov, freeLov: !!lov };
+                      return pv;
+                  }, {} as Record<string, ColumnDesc>)
+                : undefined;
+        } catch (e) {
+            return undefined;
+        }
+    }, [props.filter]);
+    const [filters, setFilters] = useState<FilterDesc[]>([]);
+
+    const applyFilters = useCallback(
+        (filters: FilterDesc[]) => {
+            setFilters((old) => {
+                const jsonFilters = JSON.stringify(filters);
+                if (old.length != filters.length || JSON.stringify(old) != jsonFilters) {
+                    localStoreSet(jsonFilters, id, lovPropertyName, "filter");
+                    const filterVar = getUpdateVar(updateCoreVars, "filter");
+                    dispatch(
+                        createRequestUpdateAction(
+                            id,
+                            module,
+                            getUpdateVarNames(updateVars, lovPropertyName),
+                            true,
+                            filterVar ? { [filterVar]: filters } : undefined
+                        )
+                    );
+                    return filters;
+                }
+                return old;
+            });
+        },
+        [updateVars, dispatch, id, updateCoreVars, lovPropertyName, module]
+    );
+
+    // sort
+    const colSorts = useMemo(() => {
+        try {
+            const res = props.sort ? (JSON.parse(props.sort) as Array<[string]>) : undefined;
+            return Array.isArray(res)
+                ? res.reduce((pv, [name], idx) => {
+                      pv[name] = { dfid: name, type: "str", index: idx };
+                      return pv;
+                  }, {} as Record<string, ColumnDesc>)
+                : undefined;
+        } catch (e) {
+            return undefined;
+        }
+    }, [props.sort]);
+    const [sorts, setSorts] = useState<SortDesc[]>([]);
+
+    const applySorts = useCallback(
+        (sorts: SortDesc[]) => {
+            setSorts((old) => {
+                const jsonSorts = JSON.stringify(sorts);
+                if (old.length != sorts.length || JSON.stringify(old) != jsonSorts) {
+                    localStoreSet(jsonSorts, id, lovPropertyName, "sort");
+                    const sortVar = getUpdateVar(updateCoreVars, "sort");
+                    dispatch(
+                        createRequestUpdateAction(
+                            id,
+                            module,
+                            getUpdateVarNames(updateVars, lovPropertyName),
+                            true,
+                            sortVar ? { [sortVar]: sorts } : undefined
+                        )
+                    );
+                    return sorts;
+                }
+                return old;
+            });
+        },
+        [updateVars, dispatch, id, updateCoreVars, lovPropertyName, module]
+    );
+
+    useEffect(() => {
+        if (lovPropertyName) {
+            const filters = localStoreGet(id, lovPropertyName, "filter");
+            filters && applyFilters(filters);
+            const sorts = localStoreGet(id, lovPropertyName, "sort");
+            sorts && applySorts(sorts);
+        }
+    }, [id, lovPropertyName, applyFilters, applySorts]);
+
+    // Search
+    const [searchValue, setSearchValue] = useState("");
+    const onSearch = useCallback((e: ChangeEvent<HTMLInputElement>) => setSearchValue(e.currentTarget.value), []);
+    const foundEntities = useMemo(() => {
+        if (!entities || searchValue === "") {
+            return entities;
+        }
+        return filterTree(entities, searchValue.toLowerCase(), props.leafType);
+    }, [entities, searchValue, props.leafType]);
+    const [revealSearch, setRevealSearch] = useState(false);
+    const onRevealSearch = useCallback(() => {
+        setRevealSearch((r) => !r);
+        setSearchValue("");
+    }, []);
+
     return (
     return (
         <>
         <>
-            {showPins ? (
-                <Box sx={switchBoxSx}>
-                    <FormControlLabel
-                        control={
-                            <Switch
-                                onChange={onShowPinsChange}
-                                checked={hideNonPinned}
-                                disabled={!hideNonPinned && !Object.keys(pins[0]).length}
-                            />
-                        }
-                        label="Pinned only"
-                    />
-                </Box>
-            ) : null}
+            <Grid container sx={switchBoxSx} gap={1}>
+                {active && colFilters ? (
+                    <Grid item>
+                        <TableFilter
+                            columns={colFilters}
+                            appliedFilters={filters}
+                            filteredCount={0}
+                            onValidate={applyFilters}
+                        ></TableFilter>
+                    </Grid>
+                ) : null}
+                {active && colSorts ? (
+                    <Grid item>
+                        <TableSort columns={colSorts} appliedSorts={sorts} onValidate={applySorts}></TableSort>
+                    </Grid>
+                ) : null}
+                {showPins ? (
+                    <Grid item>
+                        <FormControlLabel
+                            control={
+                                <Switch
+                                    onChange={onShowPinsChange}
+                                    checked={hideNonPinned}
+                                    disabled={!hideNonPinned && !Object.keys(pins[0]).length}
+                                />
+                            }
+                            label="Pinned only"
+                        />
+                    </Grid>
+                ) : null}
+                {showSearch ? (
+                    <Grid item>
+                        <IconButton onClick={onRevealSearch} size="small" sx={iconInRowSx}>
+                            {revealSearch ? (
+                                <SearchOffOutlined fontSize="inherit" />
+                            ) : (
+                                <SearchOutlined fontSize="inherit" />
+                            )}
+                        </IconButton>
+                    </Grid>
+                ) : null}
+                {showSearch && revealSearch ? (
+                    <Grid item xs={12}>
+                        <TextField
+                            margin="dense"
+                            value={searchValue}
+                            onChange={onSearch}
+                            fullWidth
+                            label="Search"
+                        ></TextField>
+                    </Grid>
+                ) : null}
+            </Grid>
             <SimpleTreeView
             <SimpleTreeView
                 slots={treeSlots}
                 slots={treeSlots}
                 sx={treeViewSx}
                 sx={treeViewSx}
@@ -433,8 +671,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 expandedItems={expandedItems}
                 expandedItems={expandedItems}
                 onItemExpansionToggle={onItemExpand}
                 onItemExpansionToggle={onItemExpand}
             >
             >
-                {entities
-                    ? entities.map((item) => (
+                {foundEntities
+                    ? foundEntities.map((item) => (
                           <CoreItem
                           <CoreItem
                               key={item ? item[0] : ""}
                               key={item ? item[0] : ""}
                               item={item}
                               item={item}

+ 2 - 0
frontend/taipy/src/DataNodeViewer.tsx

@@ -833,6 +833,8 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                                             updateVarName={scenarioUpdateVars[0]}
                                                             updateVarName={scenarioUpdateVars[0]}
                                                             updateVars={`scenarios=${scenarioUpdateVars[1]}`}
                                                             updateVars={`scenarios=${scenarioUpdateVars[1]}`}
                                                             onSelect={handleClose}
                                                             onSelect={handleClose}
+                                                            updateCoreVars=""
+                                                            showSearch={false}
                                                         />
                                                         />
                                                     </Popover>
                                                     </Popover>
                                                 </>
                                                 </>

+ 1 - 2
frontend/taipy/src/JobSelector.tsx

@@ -409,8 +409,7 @@ const JobSelectedTableRow = ({
     showCancel,
     showCancel,
     showDelete
     showDelete
 }: JobSelectedTableRowProps) => {
 }: JobSelectedTableRowProps) => {
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    const [id, jobName, _, entityId, entityName, submitId, creationDate, status] = row;
+    const [id, jobName, , entityId, entityName, submitId, creationDate, status] = row;
 
 
     return (
     return (
         <TableRow
         <TableRow

+ 2 - 0
frontend/taipy/src/NodeSelector.tsx

@@ -51,6 +51,8 @@ const NodeSelector = (props: NodeSelectorProps) => {
                 lovPropertyName="datanodes"
                 lovPropertyName="datanodes"
                 showPins={showPins}
                 showPins={showPins}
                 multiple={multiple}
                 multiple={multiple}
+                showSearch={false}
+                updateCoreVars=""
             />
             />
             <Box>{props.error}</Box>
             <Box>{props.error}</Box>
         </Box>
         </Box>

+ 6 - 51
frontend/taipy/src/ScenarioSelector.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { useEffect, useState, useCallback, useMemo } from "react";
+import React, { useEffect, useState, useCallback } from "react";
 import { Theme, Tooltip, alpha } from "@mui/material";
 import { Theme, Tooltip, alpha } from "@mui/material";
 
 
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
@@ -41,15 +41,11 @@ import {
     createSendActionNameAction,
     createSendActionNameAction,
     getUpdateVar,
     getUpdateVar,
     createSendUpdateAction,
     createSendUpdateAction,
-    TableFilter,
-    ColumnDesc,
-    FilterDesc,
     useDynamicProperty,
     useDynamicProperty,
-    createRequestUpdateAction,
 } from "taipy-gui";
 } from "taipy-gui";
 
 
 import ConfirmDialog from "./utils/ConfirmDialog";
 import ConfirmDialog from "./utils/ConfirmDialog";
-import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx, getUpdateVarNames } from "./utils";
+import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx } from "./utils";
 import CoreSelector, { EditProps } from "./CoreSelector";
 import CoreSelector, { EditProps } from "./CoreSelector";
 import { Cycles, NodeType, Scenarios } from "./utils/types";
 import { Cycles, NodeType, Scenarios } from "./utils/types";
 
 
@@ -101,6 +97,7 @@ interface ScenarioSelectorProps {
     multiple?: boolean;
     multiple?: boolean;
     filter?: string;
     filter?: string;
     updateScVars?: string;
     updateScVars?: string;
+    showSearch?: boolean;
 }
 }
 
 
 interface ScenarioEditDialogProps {
 interface ScenarioEditDialogProps {
@@ -437,6 +434,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
         multiple = false,
         multiple = false,
         updateVars = "",
         updateVars = "",
         updateScVars = "",
         updateScVars = "",
+        showSearch = true
     } = props;
     } = props;
     const [open, setOpen] = useState(false);
     const [open, setOpen] = useState(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
@@ -447,43 +445,6 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     const dispatch = useDispatch();
     const dispatch = useDispatch();
     const module = useModule();
     const module = useModule();
 
 
-    const colFilters = useMemo(() => {
-        try {
-            const res = props.filter ? (JSON.parse(props.filter) as Array<[string, string, string[]]>) : undefined;
-            return Array.isArray(res)
-                ? res.reduce((pv, [name, coltype, lov], idx) => {
-                      pv[name] = { dfid: name, type: coltype, index: idx, filter: true, lov: lov, freeLov: !!lov };
-                      return pv;
-                  }, {} as Record<string, ColumnDesc>)
-                : undefined;
-        } catch (e) {
-            return undefined;
-        }
-    }, [props.filter]);
-    const [filters, setFilters] = useState<FilterDesc[]>([]);
-
-    const applyFilters = useCallback(
-        (filters: FilterDesc[]) => {
-            setFilters((old) => {
-                if (old.length != filters.length || JSON.stringify(old) != JSON.stringify(filters)) {
-                    const filterVar = getUpdateVar(updateScVars, "filter");
-                    dispatch(
-                        createRequestUpdateAction(
-                            props.id,
-                            module,
-                            getUpdateVarNames(updateVars, "innerScenarios"),
-                            true,
-                            filterVar ? { [filterVar]: filters } : undefined
-                        )
-                    );
-                    return filters;
-                }
-                return old;
-            });
-        },
-        [updateVars, dispatch, props.id, updateScVars, module]
-    );
-
     const onSubmit = useCallback(
     const onSubmit = useCallback(
         (...values: unknown[]) => {
         (...values: unknown[]) => {
             dispatch(
             dispatch(
@@ -567,14 +528,6 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     return (
     return (
         <>
         <>
             <Box sx={MainTreeBoxSx} id={props.id} className={className}>
             <Box sx={MainTreeBoxSx} id={props.id} className={className}>
-                {active && colFilters ? (
-                    <TableFilter
-                        columns={colFilters}
-                        appliedFilters={filters}
-                        filteredCount={0}
-                        onValidate={applyFilters}
-                    ></TableFilter>
-                ) : null}
                 <CoreSelector
                 <CoreSelector
                     {...props}
                     {...props}
                     entities={props.innerScenarios}
                     entities={props.innerScenarios}
@@ -583,6 +536,8 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                     editComponent={EditScenario}
                     editComponent={EditScenario}
                     showPins={showPins}
                     showPins={showPins}
                     multiple={multiple}
                     multiple={multiple}
+                    updateCoreVars={updateScVars}
+                    showSearch={showSearch}
                 />
                 />
                 {showAddButton ? (
                 {showAddButton ? (
                     <Button variant="outlined" onClick={onDialogOpen} fullWidth endIcon={<Add />} disabled={!active}>
                     <Button variant="outlined" onClick={onDialogOpen} fullWidth endIcon={<Add />} disabled={!active}>

+ 1 - 1
taipy/config/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 and orientation.
 
 

+ 1 - 1
taipy/core/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 and orientation.
 
 

+ 1 - 3
taipy/core/_entity/_reload.py

@@ -104,6 +104,4 @@ def _get_manager(manager: str) -> _Manager:
         "job": _JobManagerFactory._build_manager(),
         "job": _JobManagerFactory._build_manager(),
         "task": _TaskManagerFactory._build_manager(),
         "task": _TaskManagerFactory._build_manager(),
         "submission": _SubmissionManagerFactory._build_manager(),
         "submission": _SubmissionManagerFactory._build_manager(),
-    }[
-        manager
-    ]  # type: ignore
+    }[manager]  # type: ignore

+ 4 - 3
taipy/core/_orchestrator/_orchestrator.py

@@ -165,10 +165,11 @@ class _Orchestrator(_AbstractOrchestrator):
 
 
     @classmethod
     @classmethod
     def _update_submission_status(cls, job: Job):
     def _update_submission_status(cls, job: Job):
-        if submission := _SubmissionManagerFactory._build_manager()._get(job.submit_id):
-            submission._update_submission_status(job)
+        submission_manager = _SubmissionManagerFactory._build_manager()
+        if submission := submission_manager._get(job.submit_id):
+            submission_manager._update_submission_status(submission, job)
         else:
         else:
-            submissions = _SubmissionManagerFactory._build_manager()._get_all()
+            submissions = submission_manager._get_all()
             cls.__logger.error(f"Submission {job.submit_id} not found.")
             cls.__logger.error(f"Submission {job.submit_id} not found.")
             msg = "\n--------------------------------------------------------------------------------\n"
             msg = "\n--------------------------------------------------------------------------------\n"
             msg += f"Submission {job.submit_id} not found.\n"
             msg += f"Submission {job.submit_id} not found.\n"

+ 1 - 1
taipy/core/_repository/_sql_repository.py

@@ -32,7 +32,7 @@ class _SQLRepository(_AbstractRepository[ModelType, Entity]):
     def __init__(self, model_type: Type[ModelType], converter: Type[Converter]):
     def __init__(self, model_type: Type[ModelType], converter: Type[Converter]):
         """
         """
         Holds common methods to be used and extended when the need for saving
         Holds common methods to be used and extended when the need for saving
-        dataclasses in a SqlLite database.
+        dataclasses in a sqlite database.
 
 
         Some lines have type: ignore because MyPy won't recognize some generic attributes. This
         Some lines have type: ignore because MyPy won't recognize some generic attributes. This
         should be revised in the future.
         should be revised in the future.

+ 1 - 0
taipy/core/notification/notifier.py

@@ -82,6 +82,7 @@ class Notifier:
                     <li>TASK</li>
                     <li>TASK</li>
                     <li>DATA_NODE</li>
                     <li>DATA_NODE</li>
                     <li>JOB</li>
                     <li>JOB</li>
+                    <li>SUBMISSION</li>
                 </ul>
                 </ul>
             entity_id (Optional[str]): If provided, the listener will be notified
             entity_id (Optional[str]): If provided, the listener will be notified
                 for all events related to this entity. Otherwise, the listener
                 for all events related to this entity. Otherwise, the listener

+ 88 - 0
taipy/core/submission/_submission_manager.py

@@ -9,13 +9,17 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # 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.
 # specific language governing permissions and limitations under the License.
 
 
+from threading import Lock
 from typing import List, Optional, Union
 from typing import List, Optional, Union
 
 
+from taipy.logger._taipy_logger import _TaipyLogger
+
 from .._entity._entity_ids import _EntityIds
 from .._entity._entity_ids import _EntityIds
 from .._manager._manager import _Manager
 from .._manager._manager import _Manager
 from .._repository._abstract_repository import _AbstractRepository
 from .._repository._abstract_repository import _AbstractRepository
 from .._version._version_mixin import _VersionMixin
 from .._version._version_mixin import _VersionMixin
 from ..exceptions.exceptions import SubmissionNotDeletedException
 from ..exceptions.exceptions import SubmissionNotDeletedException
+from ..job.job import Job, Status
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..scenario.scenario import Scenario
 from ..scenario.scenario import Scenario
 from ..sequence.sequence import Sequence
 from ..sequence.sequence import Sequence
@@ -27,6 +31,8 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
     _ENTITY_NAME = Submission.__name__
     _ENTITY_NAME = Submission.__name__
     _repository: _AbstractRepository
     _repository: _AbstractRepository
     _EVENT_ENTITY_TYPE = EventEntityType.SUBMISSION
     _EVENT_ENTITY_TYPE = EventEntityType.SUBMISSION
+    __lock = Lock()
+    __logger = _TaipyLogger._get_logger()
 
 
     @classmethod
     @classmethod
     def _get_all(cls, version_number: Optional[str] = None) -> List[Submission]:
     def _get_all(cls, version_number: Optional[str] = None) -> List[Submission]:
@@ -47,6 +53,88 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
 
 
         return submission
         return submission
 
 
+    @classmethod
+    def _update_submission_status(cls, submission: Submission, job: Job):
+        with cls.__lock:
+            submission = cls._get(submission)
+
+            if submission._submission_status == SubmissionStatus.FAILED:
+                return
+
+            job_status = job.status
+            if job_status == Status.FAILED:
+                submission._submission_status = SubmissionStatus.FAILED
+                cls._set(submission)
+                cls.__logger.debug(
+                    f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`."
+                )
+                return
+            if job_status == Status.CANCELED:
+                submission._is_canceled = True
+            elif job_status == Status.BLOCKED:
+                submission._blocked_jobs.add(job.id)
+                submission._pending_jobs.discard(job.id)
+            elif job_status == Status.PENDING or job_status == Status.SUBMITTED:
+                submission._pending_jobs.add(job.id)
+                submission._blocked_jobs.discard(job.id)
+            elif job_status == Status.RUNNING:
+                submission._running_jobs.add(job.id)
+                submission._pending_jobs.discard(job.id)
+            elif job_status == Status.COMPLETED or job_status == Status.SKIPPED:
+                submission._is_completed = True  # type: ignore
+                submission._blocked_jobs.discard(job.id)
+                submission._pending_jobs.discard(job.id)
+                submission._running_jobs.discard(job.id)
+            elif job_status == Status.ABANDONED:
+                submission._is_abandoned = True  # type: ignore
+                submission._running_jobs.discard(job.id)
+                submission._blocked_jobs.discard(job.id)
+                submission._pending_jobs.discard(job.id)
+            cls._set(submission)
+
+            # The submission_status is set later to make sure notification for updating
+            # the submission_status attribute is triggered
+            if submission._is_canceled:
+                cls._set_submission_status(submission, SubmissionStatus.CANCELED, job)
+            elif submission._is_abandoned:
+                cls._set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
+            elif submission._running_jobs:
+                cls._set_submission_status(submission, SubmissionStatus.RUNNING, job)
+            elif submission._pending_jobs:
+                cls._set_submission_status(submission, SubmissionStatus.PENDING, job)
+            elif submission._blocked_jobs:
+                cls._set_submission_status(submission, SubmissionStatus.BLOCKED, job)
+            elif submission._is_completed:
+                cls._set_submission_status(submission, SubmissionStatus.COMPLETED, job)
+            else:
+                cls._set_submission_status(submission, SubmissionStatus.UNDEFINED, job)
+            cls.__logger.debug(
+                f"{job.id} status is {job_status}. Submission status set to `{submission._submission_status}`"
+            )
+
+    @classmethod
+    def _set_submission_status(cls, submission: Submission, new_submission_status: SubmissionStatus, job: Job):
+        if not submission._is_in_context:
+            submission = cls._get(submission)
+        _current_submission_status = submission._submission_status
+        submission._submission_status = new_submission_status
+
+        cls._set(submission)
+
+        if _current_submission_status != submission._submission_status:
+            event = _make_event(
+                submission,
+                EventOperation.UPDATE,
+                "submission_status",
+                submission._submission_status,
+                job_triggered_submission_status_changed=job.id,
+            )
+
+            if not submission._is_in_context:
+                Notifier.publish(event)
+            else:
+                submission._in_context_attributes_changed_collector.append(event)
+
     @classmethod
     @classmethod
     def _get_latest(cls, entity: Union[Scenario, Sequence, Task]) -> Optional[Submission]:
     def _get_latest(cls, entity: Union[Scenario, Sequence, Task]) -> Optional[Submission]:
         entity_id = entity.id if not isinstance(entity, str) else entity
         entity_id = entity.id if not isinstance(entity, str) else entity

+ 2 - 65
taipy/core/submission/submission.py

@@ -14,15 +14,13 @@ import uuid
 from datetime import datetime
 from datetime import datetime
 from typing import Any, Dict, List, Optional, Set, Union
 from typing import Any, Dict, List, Optional, Set, Union
 
 
-from taipy.logger._taipy_logger import _TaipyLogger
-
 from .._entity._entity import _Entity
 from .._entity._entity import _Entity
 from .._entity._labeled import _Labeled
 from .._entity._labeled import _Labeled
 from .._entity._properties import _Properties
 from .._entity._properties import _Properties
 from .._entity._reload import _Reloader, _self_reload, _self_setter
 from .._entity._reload import _Reloader, _self_reload, _self_setter
 from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_manager_factory import _VersionManagerFactory
-from ..job.job import Job, JobId, Status
-from ..notification.event import Event, EventEntityType, EventOperation, _make_event
+from ..job.job import Job, JobId
+from ..notification import Event, EventEntityType, EventOperation, _make_event
 from .submission_id import SubmissionId
 from .submission_id import SubmissionId
 from .submission_status import SubmissionStatus
 from .submission_status import SubmissionStatus
 
 
@@ -45,7 +43,6 @@ class Submission(_Entity, _Labeled):
     _MANAGER_NAME = "submission"
     _MANAGER_NAME = "submission"
     __SEPARATOR = "_"
     __SEPARATOR = "_"
     lock = threading.Lock()
     lock = threading.Lock()
-    __logger = _TaipyLogger._get_logger()
 
 
     def __init__(
     def __init__(
         self,
         self,
@@ -192,66 +189,6 @@ class Submission(_Entity, _Labeled):
     def __ge__(self, other):
     def __ge__(self, other):
         return self.creation_date.timestamp() >= other.creation_date.timestamp()
         return self.creation_date.timestamp() >= other.creation_date.timestamp()
 
 
-    def _update_submission_status(self, job: Job):
-        from ._submission_manager_factory import _SubmissionManagerFactory
-
-        with self.lock:
-            submission_manager = _SubmissionManagerFactory._build_manager()
-            submission = submission_manager._get(self)
-            if submission._submission_status == SubmissionStatus.FAILED:
-                return
-
-            job_status = job.status
-            if job_status == Status.FAILED:
-                submission._submission_status = SubmissionStatus.FAILED
-                submission_manager._set(submission)
-                self.__logger.debug(
-                    f"{job.id} status is {job_status}. Submission status set to " f"{submission._submission_status}"
-                )
-                return
-            if job_status == Status.CANCELED:
-                submission._is_canceled = True
-            elif job_status == Status.BLOCKED:
-                submission._blocked_jobs.add(job.id)
-                submission._pending_jobs.discard(job.id)
-            elif job_status == Status.PENDING or job_status == Status.SUBMITTED:
-                submission._pending_jobs.add(job.id)
-                submission._blocked_jobs.discard(job.id)
-            elif job_status == Status.RUNNING:
-                submission._running_jobs.add(job.id)
-                submission._pending_jobs.discard(job.id)
-            elif job_status == Status.COMPLETED or job_status == Status.SKIPPED:
-                submission._is_completed = True  # type: ignore
-                submission._blocked_jobs.discard(job.id)
-                submission._pending_jobs.discard(job.id)
-                submission._running_jobs.discard(job.id)
-            elif job_status == Status.ABANDONED:
-                submission._is_abandoned = True  # type: ignore
-                submission._running_jobs.discard(job.id)
-                submission._blocked_jobs.discard(job.id)
-                submission._pending_jobs.discard(job.id)
-            submission_manager._set(submission)
-
-            # The submission_status is set later to make sure notification for updating
-            # the submission_status attribute is triggered
-            if submission._is_canceled:
-                submission.submission_status = SubmissionStatus.CANCELED  # type: ignore
-            elif submission._is_abandoned:
-                submission.submission_status = SubmissionStatus.UNDEFINED  # type: ignore
-            elif submission._running_jobs:
-                submission.submission_status = SubmissionStatus.RUNNING  # type: ignore
-            elif submission._pending_jobs:
-                submission.submission_status = SubmissionStatus.PENDING  # type: ignore
-            elif submission._blocked_jobs:
-                submission.submission_status = SubmissionStatus.BLOCKED  # type: ignore
-            elif submission._is_completed:
-                submission.submission_status = SubmissionStatus.COMPLETED  # type: ignore
-            else:
-                submission.submission_status = SubmissionStatus.UNDEFINED  # type: ignore
-            self.__logger.debug(
-                f"{job.id} status is {job_status}. Submission status set to " f"{submission._submission_status}"
-            )
-
     def is_finished(self) -> bool:
     def is_finished(self) -> bool:
         """Indicate if the submission is finished.
         """Indicate if the submission is finished.
 
 

+ 1 - 1
taipy/gui/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 and orientation.
 
 

+ 21 - 4
taipy/gui/_renderers/builder.py

@@ -597,8 +597,15 @@ class _Builder:
         return self
         return self
 
 
     def __set_list_attribute(
     def __set_list_attribute(
-        self, name: str, hash_name: t.Optional[str], val: t.Any, elt_type: t.Type, dynamic=True
+        self,
+        name: str,
+        hash_name: t.Optional[str],
+        val: t.Any,
+        elt_type: t.Type,
+        dynamic=True,
+        default_val: t.Optional[t.Any] = None,
     ) -> t.List[str]:
     ) -> t.List[str]:
+        val = default_val if val is None else val
         if not hash_name and isinstance(val, str):
         if not hash_name and isinstance(val, str):
             val = [elt_type(t.strip()) for t in val.split(";")]
             val = [elt_type(t.strip()) for t in val.split(";")]
         if isinstance(val, list):
         if isinstance(val, list):
@@ -966,8 +973,15 @@ class _Builder:
                     attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, False)
                     attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, False)
                 )
                 )
             elif var_type == PropertyType.string_list:
             elif var_type == PropertyType.string_list:
-                self.__set_list_attribute(
-                    attr[0], self.__hashes.get(attr[0]), self.__attributes.get(attr[0]), str, False
+                self.__update_vars.extend(
+                    self.__set_list_attribute(
+                        attr[0],
+                        self.__hashes.get(attr[0]),
+                        self.__attributes.get(attr[0]),
+                        str,
+                        False,
+                        _get_tuple_val(attr, 2, None),
+                    )
                 )
                 )
             elif var_type == PropertyType.function:
             elif var_type == PropertyType.function:
                 self.__set_function_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True))
                 self.__set_function_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True))
@@ -1013,7 +1027,10 @@ class _Builder:
                     self.__update_vars.append(f"{prop_name}={hash_name}")
                     self.__update_vars.append(f"{prop_name}={hash_name}")
                     self.__set_react_attribute(prop_name, hash_name)
                     self.__set_react_attribute(prop_name, hash_name)
                 else:
                 else:
-                    self.set_attribute(prop_name, var_type(self.__attributes.get(attr[0]), "").get())
+                    val = self.__attributes.get(attr[0])
+                    self.set_attribute(
+                        prop_name, var_type(_get_tuple_val(attr, 2, None) if val is None else val, "").get()
+                    )
 
 
         self.__set_refresh_on_update()
         self.__set_refresh_on_update()
         return self
         return self

+ 4 - 2
taipy/gui/extension/library.py

@@ -184,14 +184,16 @@ class Element:
                             hash_value = "None"
                             hash_value = "None"
                         val = val[: m.start()] + hash_value + val[m.end() :]
                         val = val[: m.start()] + hash_value + val[m.end() :]
                     # handling unique id replacement in inner properties <tp:uniq:...>
                     # handling unique id replacement in inner properties <tp:uniq:...>
+                    has_uniq = False
                     while m := Element.__RE_UNIQUE_VAR.search(val):
                     while m := Element.__RE_UNIQUE_VAR.search(val):
+                        has_uniq = True
                         id = uniques.get(m.group(1))
                         id = uniques.get(m.group(1))
                         if id is None:
                         if id is None:
                             id = len(uniques) + 1
                             id = len(uniques) + 1
                             uniques[m.group(1)] = id
                             uniques[m.group(1)] = id
                         val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}"
                         val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}"
-                        if gui._is_expression(val):
-                            gui._evaluate_expr(val, True)
+                    if has_uniq and gui._is_expression(val):
+                        gui._evaluate_expr(val, True)
 
 
                 attributes[prop] = val
                 attributes[prop] = val
         # this modifies attributes
         # this modifies attributes

+ 9 - 3
taipy/gui_core/_GuiCoreLib.py

@@ -22,7 +22,8 @@ from ._adapters import (
     _GuiCoreDoNotUpdate,
     _GuiCoreDoNotUpdate,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioDagAdapter,
     _GuiCoreScenarioDagAdapter,
-    _GuiCoreScenarioProperties,
+    _GuiCoreScenarioFilter,
+    _GuiCoreScenarioSort,
 )
 )
 from ._context import _GuiCoreContext
 from ._context import _GuiCoreContext
 
 
@@ -44,6 +45,7 @@ class _GuiCore(ElementLibrary):
     __SCENARIO_SELECTOR_ERROR_VAR = "__tpgc_sc_error"
     __SCENARIO_SELECTOR_ERROR_VAR = "__tpgc_sc_error"
     __SCENARIO_SELECTOR_ID_VAR = "__tpgc_sc_id"
     __SCENARIO_SELECTOR_ID_VAR = "__tpgc_sc_id"
     __SCENARIO_SELECTOR_FILTER_VAR = "__tpgc_sc_filter"
     __SCENARIO_SELECTOR_FILTER_VAR = "__tpgc_sc_filter"
+    __SCENARIO_SELECTOR_SORT_VAR = "__tpgc_sc_sort"
     __SCENARIO_VIZ_ERROR_VAR = "__tpgc_sv_error"
     __SCENARIO_VIZ_ERROR_VAR = "__tpgc_sv_error"
     __JOB_SELECTOR_ERROR_VAR = "__tpgc_js_error"
     __JOB_SELECTOR_ERROR_VAR = "__tpgc_js_error"
     __DATANODE_VIZ_ERROR_VAR = "__tpgc_dv_error"
     __DATANODE_VIZ_ERROR_VAR = "__tpgc_dv_error"
@@ -73,13 +75,16 @@ class _GuiCore(ElementLibrary):
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
                 "multiple": ElementProperty(PropertyType.boolean, False),
-                "filter": ElementProperty(_GuiCoreScenarioProperties, _GuiCoreScenarioProperties.DEFAULT),
+                "filter": ElementProperty(_GuiCoreScenarioFilter, _GuiCoreScenarioFilter.DEFAULT),
+                "sort": ElementProperty(_GuiCoreScenarioSort, _GuiCoreScenarioSort.DEFAULT),
+                "show_search": ElementProperty(PropertyType.boolean, True),
             },
             },
             inner_properties={
             inner_properties={
                 "inner_scenarios": ElementProperty(
                 "inner_scenarios": ElementProperty(
                     PropertyType.lov,
                     PropertyType.lov,
                     f"{{{__CTX_VAR_NAME}.get_scenarios(<tp:prop:{__SEL_SCENARIOS_PROP}>, "
                     f"{{{__CTX_VAR_NAME}.get_scenarios(<tp:prop:{__SEL_SCENARIOS_PROP}>, "
-                    + f"{__SCENARIO_SELECTOR_FILTER_VAR}<tp:uniq:sc>)}}",
+                    + f"{__SCENARIO_SELECTOR_FILTER_VAR}<tp:uniq:sc>, "
+                    + f"{__SCENARIO_SELECTOR_SORT_VAR}<tp:uniq:sc>)}}",
                 ),
                 ),
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
@@ -94,6 +99,7 @@ class _GuiCore(ElementLibrary):
                 "update_sc_vars": ElementProperty(
                 "update_sc_vars": ElementProperty(
                     PropertyType.string,
                     PropertyType.string,
                     f"filter={__SCENARIO_SELECTOR_FILTER_VAR}<tp:uniq:sc>;"
                     f"filter={__SCENARIO_SELECTOR_FILTER_VAR}<tp:uniq:sc>;"
+                    + f"sort={__SCENARIO_SELECTOR_SORT_VAR}<tp:uniq:sc>;"
                     + f"sc_id={__SCENARIO_SELECTOR_ID_VAR}<tp:uniq:sc>;"
                     + f"sc_id={__SCENARIO_SELECTOR_ID_VAR}<tp:uniq:sc>;"
                     + f"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sc>",
                     + f"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sc>",
                 ),
                 ),

+ 56 - 16
taipy/gui_core/_adapters.py

@@ -11,7 +11,9 @@
 
 
 import json
 import json
 import math
 import math
+import sys
 import typing as t
 import typing as t
+from abc import abstractmethod
 from datetime import date, datetime
 from datetime import date, datetime
 from enum import Enum
 from enum import Enum
 from numbers import Number
 from numbers import Number
@@ -19,16 +21,7 @@ from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
 
 
 import pandas as pd
 import pandas as pd
 
 
-from taipy.core import (
-    Cycle,
-    DataNode,
-    Scenario,
-    is_deletable,
-    is_editable,
-    is_promotable,
-    is_readable,
-    is_submittable,
-)
+from taipy.core import Cycle, DataNode, Scenario, is_deletable, is_editable, is_promotable, is_readable, is_submittable
 from taipy.core import get as core_get
 from taipy.core import get as core_get
 from taipy.core.config import Config
 from taipy.core.config import Config
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
@@ -264,7 +257,7 @@ def _get_datanode_property(attr: str):
 
 
 
 
 class _GuiCoreScenarioProperties(_TaipyBase):
 class _GuiCoreScenarioProperties(_TaipyBase):
-    __SC_TYPES = {
+    _SC_TYPES = {
         "Config id": "string",
         "Config id": "string",
         "Label": "string",
         "Label": "string",
         "Creation date": "date",
         "Creation date": "date",
@@ -284,7 +277,6 @@ class _GuiCoreScenarioProperties(_TaipyBase):
         "Primary": "is_primary",
         "Primary": "is_primary",
         "Tags": "tags",
         "Tags": "tags",
     }
     }
-    DEFAULT = list(__SC_TYPES.keys())
     __DN_TYPES = {"Up to date": "boolean", "Valid": "boolean", "Last edit date": "date"}
     __DN_TYPES = {"Up to date": "boolean", "Valid": "boolean", "Last edit date": "date"}
     __DN_LABELS = {"Up to date": "is_up_to_date", "Valid": "is_valid", "Last edit date": "last_edit_date"}
     __DN_LABELS = {"Up to date": "is_up_to_date", "Valid": "is_valid", "Last edit date": "last_edit_date"}
     __ENUMS = None
     __ENUMS = None
@@ -297,7 +289,7 @@ class _GuiCoreScenarioProperties(_TaipyBase):
     def get_type(attr: str):
     def get_type(attr: str):
         if prop := _get_datanode_property(attr):
         if prop := _get_datanode_property(attr):
             return _GuiCoreScenarioProperties.__DN_TYPES.get(prop, "any")
             return _GuiCoreScenarioProperties.__DN_TYPES.get(prop, "any")
-        return _GuiCoreScenarioProperties.__SC_TYPES.get(attr, "any")
+        return _GuiCoreScenarioProperties._SC_TYPES.get(attr, "any")
 
 
     @staticmethod
     @staticmethod
     def get_col_name(attr: str):
     def get_col_name(attr: str):
@@ -305,11 +297,21 @@ class _GuiCoreScenarioProperties(_TaipyBase):
             return f'{attr.split(".")[0]}.{_GuiCoreScenarioProperties.__DN_LABELS.get(prop, prop)}'
             return f'{attr.split(".")[0]}.{_GuiCoreScenarioProperties.__DN_LABELS.get(prop, prop)}'
         return _GuiCoreScenarioProperties.__SC_LABELS.get(attr, attr)
         return _GuiCoreScenarioProperties.__SC_LABELS.get(attr, attr)
 
 
+    @staticmethod
+    @abstractmethod
+    def get_default_list():
+        raise NotImplementedError
+
+    @staticmethod
+    @abstractmethod
+    def full_desc():
+        raise NotImplementedError
+
     def get(self):
     def get(self):
         data = super().get()
         data = super().get()
         if _is_boolean(data):
         if _is_boolean(data):
             if _is_true(data):
             if _is_true(data):
-                data = _GuiCoreScenarioProperties.DEFAULT
+                data = self.get_default_list()
             else:
             else:
                 return None
                 return None
         if isinstance(data, str):
         if isinstance(data, str):
@@ -318,10 +320,10 @@ class _GuiCoreScenarioProperties(_TaipyBase):
             flist = []
             flist = []
             for f in data:
             for f in data:
                 if f == "*":
                 if f == "*":
-                    flist.extend(_GuiCoreScenarioProperties.DEFAULT)
+                    flist.extend(self.get_default_list())
                 else:
                 else:
                     flist.append(f)
                     flist.append(f)
-            if _GuiCoreScenarioProperties.__ENUMS is None:
+            if _GuiCoreScenarioProperties.__ENUMS is None and self.full_desc():
                 _GuiCoreScenarioProperties.__ENUMS = {
                 _GuiCoreScenarioProperties.__ENUMS = {
                     "Config id": [c for c in Config.scenarios.keys() if c != "default"],
                     "Config id": [c for c in Config.scenarios.keys() if c != "default"],
                     "Tags": [t for s in Config.scenarios.values() for t in s.properties.get("authorized_tags", [])],
                     "Tags": [t for s in Config.scenarios.values() for t in s.properties.get("authorized_tags", [])],
@@ -329,8 +331,46 @@ class _GuiCoreScenarioProperties(_TaipyBase):
             return json.dumps(
             return json.dumps(
                 [
                 [
                     (attr, _GuiCoreScenarioProperties.get_type(attr), _GuiCoreScenarioProperties.__ENUMS.get(attr))
                     (attr, _GuiCoreScenarioProperties.get_type(attr), _GuiCoreScenarioProperties.__ENUMS.get(attr))
+                    if self.full_desc()
+                    else (attr,)
                     for attr in flist
                     for attr in flist
                     if attr and isinstance(attr, str)
                     if attr and isinstance(attr, str)
                 ]
                 ]
             )
             )
         return None
         return None
+
+
+class _GuiCoreScenarioFilter(_GuiCoreScenarioProperties):
+    DEFAULT = list(_GuiCoreScenarioProperties._SC_TYPES.keys())
+
+    @staticmethod
+    def full_desc():
+        return True
+
+    @staticmethod
+    def get_hash():
+        return _TaipyBase._HOLDER_PREFIX + "ScF"
+
+    @staticmethod
+    def get_default_list():
+        return _GuiCoreScenarioFilter.DEFAULT
+
+
+class _GuiCoreScenarioSort(_GuiCoreScenarioProperties):
+    DEFAULT = ["Config id", "Label", "Creation date"]
+
+    @staticmethod
+    def full_desc():
+        return False
+
+    @staticmethod
+    def get_hash():
+        return _TaipyBase._HOLDER_PREFIX + "ScS"
+
+    @staticmethod
+    def get_default_list():
+        return _GuiCoreScenarioSort.DEFAULT
+
+
+def _is_debugging() -> bool:
+    return hasattr(sys, "gettrace") and sys.gettrace() is not None

+ 50 - 20
taipy/gui_core/_context.py

@@ -12,8 +12,9 @@
 import json
 import json
 import typing as t
 import typing as t
 from collections import defaultdict
 from collections import defaultdict
-from datetime import datetime
+from datetime import date, datetime
 from numbers import Number
 from numbers import Number
+from operator import attrgetter
 from threading import Lock
 from threading import Lock
 
 
 try:
 try:
@@ -66,6 +67,7 @@ from ._adapters import (
     _GuiCoreDatanodeAdapter,
     _GuiCoreDatanodeAdapter,
     _GuiCoreScenarioProperties,
     _GuiCoreScenarioProperties,
     _invoke_action,
     _invoke_action,
+    _is_debugging,
 )
 )
 
 
 
 
@@ -126,7 +128,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             with self.lock:
             with self.lock:
                 self.jobs_list = None
                 self.jobs_list = None
         elif event.entity_type == EventEntityType.SUBMISSION:
         elif event.entity_type == EventEntityType.SUBMISSION:
-            self.submission_status_callback(event.entity_id)
+            self.submission_status_callback(event.entity_id, event)
         elif event.entity_type == EventEntityType.DATA_NODE:
         elif event.entity_type == EventEntityType.DATA_NODE:
             with self.lock:
             with self.lock:
                 self.data_nodes_by_owner = None
                 self.data_nodes_by_owner = None
@@ -144,7 +146,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             {"scenario": scenario_id or True},
             {"scenario": scenario_id or True},
         )
         )
 
 
-    def submission_status_callback(self, submission_id: t.Optional[str]):
+    def submission_status_callback(self, submission_id: t.Optional[str] = None, event: t.Optional[Event] = None):
         if not submission_id or not is_readable(t.cast(SubmissionId, submission_id)):
         if not submission_id or not is_readable(t.cast(SubmissionId, submission_id)):
             return
             return
         try:
         try:
@@ -180,7 +182,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             self.gui._call_user_callback(
                             self.gui._call_user_callback(
                                 client_id,
                                 client_id,
                                 submission_name,
                                 submission_name,
-                                [core_get(submission.entity_id), {"submission_status": new_status.name}],
+                                [
+                                    core_get(submission.id),
+                                    {
+                                        "submission_status": new_status.name,
+                                        "submittable_entity": core_get(submission.entity_id),
+                                        **(event.metadata if event else {}),
+                                    },
+                                ],
                                 submission.properties.get("module_context"),
                                 submission.properties.get("module_context"),
                             )
                             )
 
 
@@ -203,7 +212,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
     def no_change_adapter(self, entity: t.List):
     def no_change_adapter(self, entity: t.List):
         return entity
         return entity
 
 
-    def cycle_adapter(self, cycle: Cycle):
+    def cycle_adapter(self, cycle: Cycle, sorts: t.Optional[t.List[t.Dict[str, t.Any]]] = None):
         try:
         try:
             if (
             if (
                 isinstance(cycle, Cycle)
                 isinstance(cycle, Cycle)
@@ -214,10 +223,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 return [
                 return [
                     cycle.id,
                     cycle.id,
                     cycle.get_simple_label(),
                     cycle.get_simple_label(),
-                    sorted(
-                        self.scenario_by_cycle.get(cycle, []),
-                        key=_GuiCoreContext.get_entity_creation_date_iso,
-                    ),
+                    self.get_sorted_entity_list(self.scenario_by_cycle.get(cycle, []), sorts),
                     _EntityType.CYCLE.value,
                     _EntityType.CYCLE.value,
                     False,
                     False,
                 ]
                 ]
@@ -257,8 +263,27 @@ class _GuiCoreContext(CoreEventConsumerBase):
         cycle[2] = [self.scenario_adapter(e) for e in cycle[2]]
         cycle[2] = [self.scenario_adapter(e) for e in cycle[2]]
         return cycle
         return cycle
 
 
+    def get_sorted_entity_list(
+        self,
+        entities: t.Union[t.List[t.Union[Cycle, Scenario]], t.List[Scenario]],
+        sorts: t.Optional[t.List[t.Dict[str, t.Any]]],
+    ):
+        if sorts:
+            sorted_list = entities
+            for sd in reversed(sorts):
+                col = sd.get("col", "")
+                col = _GuiCoreScenarioProperties.get_col_name(col)
+                order = sd.get("order", True)
+                sorted_list = sorted(sorted_list, key=_GuiCoreContext.get_entity_property(col), reverse=not order)
+        else:
+            sorted_list = sorted(entities, key=_GuiCoreContext.get_entity_property("creation_date"))
+        return [self.cycle_adapter(e, sorts) if isinstance(e, Cycle) else e for e in sorted_list]
+
     def get_scenarios(
     def get_scenarios(
-        self, scenarios: t.Optional[t.List[t.Union[Cycle, Scenario]]], filters: t.Optional[t.List[t.Dict[str, t.Any]]]
+        self,
+        scenarios: t.Optional[t.List[t.Union[Cycle, Scenario]]],
+        filters: t.Optional[t.List[t.Dict[str, t.Any]]],
+        sorts: t.Optional[t.List[t.Dict[str, t.Any]]],
     ):
     ):
         cycles_scenarios: t.List[t.Union[Cycle, Scenario]] = []
         cycles_scenarios: t.List[t.Union[Cycle, Scenario]] = []
         with self.lock:
         with self.lock:
@@ -272,11 +297,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         cycles_scenarios.append(cycle)
                         cycles_scenarios.append(cycle)
         if scenarios is not None:
         if scenarios is not None:
             cycles_scenarios = scenarios
             cycles_scenarios = scenarios
-        # sorting
-        adapted_list = [
-            self.cycle_adapter(e) if isinstance(e, Cycle) else e
-            for e in sorted(cycles_scenarios, key=_GuiCoreContext.get_entity_creation_date_iso)
-        ]
+        adapted_list = self.get_sorted_entity_list(cycles_scenarios, sorts)
         if filters:
         if filters:
             # filtering
             # filtering
             filtered_list = list(adapted_list)
             filtered_list = list(adapted_list)
@@ -565,7 +586,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             self.__do_datanodes_tree()
             self.__do_datanodes_tree()
         if scenarios is None:
         if scenarios is None:
             return (self.data_nodes_by_owner.get(None, []) if self.data_nodes_by_owner else []) + (
             return (self.data_nodes_by_owner.get(None, []) if self.data_nodes_by_owner else []) + (
-                self.get_scenarios(None, None) or []
+                self.get_scenarios(None, None, None) or []
             )
             )
         if not self.data_nodes_by_owner:
         if not self.data_nodes_by_owner:
             return []
             return []
@@ -745,9 +766,18 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         ent.properties.pop(key, None)
                         ent.properties.pop(key, None)
 
 
     @staticmethod
     @staticmethod
-    def get_entity_creation_date_iso(entity: t.Union[Scenario, Cycle]):
-        # we might be comparing naive and aware datetime ISO
-        return entity.creation_date.isoformat()
+    def get_entity_property(col: str):
+        def sort_key(entity: t.Union[Scenario, Cycle]):
+            # we compare only strings
+            try:
+                val = attrgetter(col)(entity)
+            except AttributeError as e:
+                if _is_debugging():
+                    _warn("Attribute", e)
+                val = ""
+            return val.isoformat() if isinstance(val, (datetime, date)) else str(val)
+
+        return sort_key
 
 
     def get_scenarios_for_owner(self, owner_id: str):
     def get_scenarios_for_owner(self, owner_id: str):
         cycles_scenarios: t.List[t.Union[Scenario, Cycle]] = []
         cycles_scenarios: t.List[t.Union[Scenario, Cycle]] = []
@@ -767,7 +797,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         cycles_scenarios.extend(scenarios_cycle)
                         cycles_scenarios.extend(scenarios_cycle)
                     elif isinstance(entity, Scenario):
                     elif isinstance(entity, Scenario):
                         cycles_scenarios.append(entity)
                         cycles_scenarios.append(entity)
-        return sorted(cycles_scenarios, key=_GuiCoreContext.get_entity_creation_date_iso)
+        return sorted(cycles_scenarios, key=_GuiCoreContext.get_entity_property("creation_date"))
 
 
     def get_data_node_history(self, id: str):
     def get_data_node_history(self, id: str):
         if id and (dn := core_get(id)) and isinstance(dn, DataNode):
         if id and (dn := core_get(id)) and isinstance(dn, DataNode):

+ 18 - 5
taipy/gui_core/viselements.json

@@ -100,8 +100,21 @@
                     },
                     },
                     {
                     {
                         "name": "filter",
                         "name": "filter",
-                        "type": "str|list[str]",
-                        "doc": "TODO: a list of scenario attributes to filter on."
+                        "type": "bool|str|list[str]",
+                        "default_value": "\"Config id;Label;Creation date;Cycle label;Cycle start;Cycle end;Primary;Tags\"",
+                        "doc": "TODO: a list of scenario attributes to filter on. If False, do not allow filter."
+                    },
+                    {
+                        "name": "show_search",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "TODO: If True, allows the user to search locally on label."
+                    },
+                    {
+                        "name": "sort",
+                        "type": "bool|str|list[str]",
+                        "default_value": "\"Config id;Label;Creation date\"",
+                        "doc": "TODO: a list of scenario attributes to sort on. If False, do not allow sort."
                     }
                     }
                 ]
                 ]
             }
             }
@@ -194,15 +207,15 @@
                     {
                     {
                         "name": "on_submission_change",
                         "name": "on_submission_change",
                         "type": "Callback",
                         "type": "Callback",
-                        "doc": "The name of the function that is triggered when a submission status is changed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>submittable (Submittable): the entity (usually a Scenario) that was submitted.</li>\n<li>details (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>submission_status (str): the new status of the submission (possible values: SUBMITTED, COMPLETED, CANCELED, FAILED, BLOCKED, WAITING, RUNNING).</li>\n<li>job: the Job (if any) that is at the origin of the submission status change.</li>\n</ul>",
+                        "doc": "The name of the function that is triggered when a submission status is changed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>submission (Submission): the submission entity containing submission information.</li>\n<li>details (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>submission_status (str): the new status of the submission (possible values: SUBMITTED, COMPLETED, CANCELED, FAILED, BLOCKED, WAITING, RUNNING).</li>\n<li>job: the Job (if any) that is at the origin of the submission status change.</li>\n<li>submittable_entity: submittable (Submittable): the entity (usually a Scenario) that was submitted.</li>\n</ul>",
                         "signature": [
                         "signature": [
                             [
                             [
                                 "state",
                                 "state",
                                 "State"
                                 "State"
                             ],
                             ],
                             [
                             [
-                                "submittable",
-                                "Submittable"
+                                "submission",
+                                "Submission"
                             ],
                             ],
                             [
                             [
                                 "details",
                                 "details",

+ 1 - 1
taipy/rest/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 and orientation.
 
 

+ 1 - 1
taipy/templates/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 and orientation.
 
 

+ 1 - 1
tests/config/common/test_template_handler.py

@@ -127,7 +127,7 @@ def test_to_bool():
     with pytest.raises(InconsistentEnvVariableError):
     with pytest.raises(InconsistentEnvVariableError):
         _TemplateHandler._to_bool("no")
         _TemplateHandler._to_bool("no")
     with pytest.raises(InconsistentEnvVariableError):
     with pytest.raises(InconsistentEnvVariableError):
-        _TemplateHandler._to_bool("tru")
+        _TemplateHandler._to_bool("tru") # codespell:ignore tru
     with pytest.raises(InconsistentEnvVariableError):
     with pytest.raises(InconsistentEnvVariableError):
         _TemplateHandler._to_bool("tru_e")
         _TemplateHandler._to_bool("tru_e")
 
 

+ 16 - 0
tests/core/notification/test_notifier.py

@@ -19,6 +19,7 @@ from taipy.core.notification._topic import _Topic
 from taipy.core.notification.event import Event
 from taipy.core.notification.event import Event
 from taipy.core.notification.notifier import Notifier
 from taipy.core.notification.notifier import Notifier
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
+from taipy.core.submission.submission_status import SubmissionStatus
 
 
 
 
 def test_register():
 def test_register():
@@ -742,6 +743,21 @@ def test_publish_submission_event():
         and event.attribute_name == expected_attribute_names[i]
         and event.attribute_name == expected_attribute_names[i]
         for i, event in enumerate(published_events)
         for i, event in enumerate(published_events)
     )
     )
+    assert "job_triggered_submission_status_changed" in published_events[4].metadata
+    assert published_events[4].metadata["job_triggered_submission_status_changed"] == job.id
+
+    # Test updating submission_status manually will not add the job_triggered_submission_status_changed
+    # to the metadata as no job was used to update the submission_status
+    submission.submission_status = SubmissionStatus.CANCELED
+
+    assert registration_queue.qsize() == 1
+    published_event = registration_queue.get()
+
+    assert published_event.entity_type == EventEntityType.SUBMISSION
+    assert published_event.entity_id == submission.id
+    assert published_event.operation == EventOperation.UPDATE
+    assert published_event.attribute_name == "submission_status"
+    assert "job_triggered_submission_status_changed" not in published_event.metadata
 
 
 
 
 def test_publish_deletion_event():
 def test_publish_deletion_event():

+ 9 - 5
tests/core/submission/test_submission.py

@@ -124,7 +124,7 @@ def __test_update_submission_status(job_ids, expected_submission_status):
     submission.jobs = [jobs[job_id] for job_id in job_ids]
     submission.jobs = [jobs[job_id] for job_id in job_ids]
     for job_id in job_ids:
     for job_id in job_ids:
         job = jobs[job_id]
         job = jobs[job_id]
-        submission._update_submission_status(job)
+        _SubmissionManagerFactory._build_manager()._update_submission_status(submission, job)
     assert submission.submission_status == expected_submission_status
     assert submission.submission_status == expected_submission_status
 
 
 
 
@@ -470,29 +470,33 @@ def test_auto_set_and_reload_properties():
     ],
     ],
 )
 )
 def test_update_submission_status_with_single_job_completed(job_statuses, expected_submission_statuses):
 def test_update_submission_status_with_single_job_completed(job_statuses, expected_submission_statuses):
+    submission_manager = _SubmissionManagerFactory._build_manager()
+
     job = MockJob("job_id", Status.SUBMITTED)
     job = MockJob("job_id", Status.SUBMITTED)
     submission = Submission("submission_id", "ENTITY_TYPE", "entity_config_id")
     submission = Submission("submission_id", "ENTITY_TYPE", "entity_config_id")
-    _SubmissionManagerFactory._build_manager()._set(submission)
+    submission_manager._set(submission)
 
 
     assert submission.submission_status == SubmissionStatus.SUBMITTED
     assert submission.submission_status == SubmissionStatus.SUBMITTED
 
 
     for job_status, submission_status in zip(job_statuses, expected_submission_statuses):
     for job_status, submission_status in zip(job_statuses, expected_submission_statuses):
         job.status = job_status
         job.status = job_status
-        submission._update_submission_status(job)
+        submission_manager._update_submission_status(submission, job)
         assert submission.submission_status == submission_status
         assert submission.submission_status == submission_status
 
 
 
 
 def __test_update_submission_status_with_two_jobs(job_ids, job_statuses, expected_submission_statuses):
 def __test_update_submission_status_with_two_jobs(job_ids, job_statuses, expected_submission_statuses):
+    submission_manager = _SubmissionManagerFactory._build_manager()
+
     jobs = {job_id: MockJob(job_id, Status.SUBMITTED) for job_id in job_ids}
     jobs = {job_id: MockJob(job_id, Status.SUBMITTED) for job_id in job_ids}
     submission = Submission("submission_id", "ENTITY_TYPE", "entity_config_id")
     submission = Submission("submission_id", "ENTITY_TYPE", "entity_config_id")
-    _SubmissionManagerFactory._build_manager()._set(submission)
+    submission_manager._set(submission)
 
 
     assert submission.submission_status == SubmissionStatus.SUBMITTED
     assert submission.submission_status == SubmissionStatus.SUBMITTED
 
 
     for (job_id, job_status), submission_status in zip(job_statuses, expected_submission_statuses):
     for (job_id, job_status), submission_status in zip(job_statuses, expected_submission_statuses):
         job = jobs[job_id]
         job = jobs[job_id]
         job.status = job_status
         job.status = job_status
-        submission._update_submission_status(job)
+        submission_manager._update_submission_status(submission, job)
         assert submission.submission_status == submission_status
         assert submission.submission_status == submission_status
 
 
 
 

Деякі файли не було показано, через те що забагато файлів було змінено