Parcourir la source

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

trgiangdo il y a 11 mois
Parent
commit
85474ae3e0
33 fichiers modifiés avec 1404 ajouts et 616 suppressions
  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
 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"]
 body:
   - type: markdown

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 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
 and orientation.
 

Fichier diff supprimé car celui-ci est trop grand
+ 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 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;
 
 /**

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

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

@@ -45,11 +45,11 @@
       "version": "3.2.0"
     },
     "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": {
-        "@babel/highlight": "^7.24.2",
+        "@babel/highlight": "^7.24.6",
         "picocolors": "^1.0.0"
       },
       "engines": {
@@ -57,38 +57,38 @@
       }
     },
     "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": {
-        "@babel/types": "^7.24.0"
+        "@babel/types": "^7.24.6"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "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": {
         "node": ">=6.9.0"
       }
     },
     "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": {
         "node": ">=6.9.0"
       }
     },
     "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": {
-        "@babel/helper-validator-identifier": "^7.24.5",
+        "@babel/helper-validator-identifier": "^7.24.6",
         "chalk": "^2.4.2",
         "js-tokens": "^4.0.0",
         "picocolors": "^1.0.0"
@@ -162,9 +162,9 @@
       }
     },
     "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": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -173,12 +173,12 @@
       }
     },
     "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": {
-        "@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"
       },
       "engines": {
@@ -423,9 +423,9 @@
       }
     },
     "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": {
         "@floating-ui/dom": "^1.0.0"
       },
@@ -858,11 +858,11 @@
       }
     },
     "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": {
-        "@babel/runtime": "^7.24.0",
+        "@babel/runtime": "^7.24.5",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
@@ -923,11 +923,11 @@
       }
     },
     "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": {
-        "@babel/runtime": "^7.24.0",
+        "@babel/runtime": "^7.24.5",
         "@mui/base": "^5.0.0-beta.40",
         "@mui/system": "^5.15.14",
         "@mui/utils": "^5.15.14",
@@ -1160,9 +1160,9 @@
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
     },
     "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": {
         "@types/prop-types": "*",
         "csstype": "^3.0.2"
@@ -1192,16 +1192,16 @@
       "dev": true
     },
     "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,
       "dependencies": {
         "@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",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -1225,15 +1225,15 @@
       }
     },
     "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,
       "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"
       },
       "engines": {
@@ -1253,13 +1253,13 @@
       }
     },
     "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,
       "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": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1270,13 +1270,13 @@
       }
     },
     "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,
       "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",
         "ts-api-utils": "^1.3.0"
       },
@@ -1297,9 +1297,9 @@
       }
     },
     "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,
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1310,13 +1310,13 @@
       }
     },
     "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,
       "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",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -1338,15 +1338,15 @@
       }
     },
     "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,
       "dependencies": {
         "@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": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1360,12 +1360,12 @@
       }
     },
     "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,
       "dependencies": {
-        "@typescript-eslint/types": "7.9.0",
+        "@typescript-eslint/types": "7.11.0",
         "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
@@ -1648,9 +1648,9 @@
       }
     },
     "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,
       "dependencies": {
         "fast-deep-equal": "^3.1.3",
@@ -1901,12 +1901,12 @@
       }
     },
     "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,
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
@@ -1978,9 +1978,9 @@
       }
     },
     "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,
       "funding": [
         {
@@ -2326,9 +2326,9 @@
       }
     },
     "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
     },
     "node_modules/enhanced-resolve": {
@@ -2471,9 +2471,9 @@
       }
     },
     "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
     },
     "node_modules/es-object-atoms": {
@@ -2604,29 +2604,29 @@
       }
     },
     "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,
       "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.toreversed": "^1.1.2",
         "array.prototype.tosorted": "^1.1.3",
         "doctrine": "^2.1.0",
-        "es-iterator-helpers": "^1.0.17",
+        "es-iterator-helpers": "^1.0.19",
         "estraverse": "^5.3.0",
         "jsx-ast-utils": "^2.4.1 || ^3.0.0",
         "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",
         "resolve": "^2.0.0-next.5",
         "semver": "^6.3.1",
-        "string.prototype.matchall": "^4.0.10"
+        "string.prototype.matchall": "^4.0.11"
       },
       "engines": {
         "node": ">=4"
@@ -2746,12 +2746,12 @@
       }
     },
     "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,
       "dependencies": {
-        "@types/eslint": "^8.56.5",
+        "@types/eslint": "^8.56.10",
         "jest-worker": "^29.7.0",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
@@ -2765,7 +2765,7 @@
         "url": "https://opencollective.com/webpack"
       },
       "peerDependencies": {
-        "eslint": "^8.0.0",
+        "eslint": "^8.0.0 || ^9.0.0",
         "webpack": "^5.0.0"
       }
     },
@@ -2935,9 +2935,9 @@
       }
     },
     "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,
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -3110,6 +3110,7 @@
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
       "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
       "dev": true,
       "dependencies": {
         "fs.realpath": "^1.0.0",
@@ -3403,6 +3404,7 @@
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "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,
       "dependencies": {
         "once": "^1.3.0",
@@ -4045,12 +4047,12 @@
       }
     },
     "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,
       "dependencies": {
-        "braces": "^3.0.2",
+        "braces": "^3.0.3",
         "picomatch": "^2.3.1"
       },
       "engines": {
@@ -4714,6 +4716,7 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
       "dev": true,
       "dependencies": {
         "glob": "^7.1.3"
@@ -4831,9 +4834,9 @@
       }
     },
     "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,
       "dependencies": {
         "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.
  */
 
-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 Box from "@mui/material/Box";
 import FormControlLabel from "@mui/material/FormControlLabel";
 import Grid from "@mui/material/Grid";
 import IconButton from "@mui/material/IconButton";
 import Switch from "@mui/material/Switch";
 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 { TreeItem } from "@mui/x-tree-view/TreeItem";
 
@@ -32,9 +44,14 @@ import {
     useDispatchRequestUpdateOnFirstRender,
     createRequestUpdateAction,
     useDynamicProperty,
+    ColumnDesc,
+    FilterDesc,
+    TableFilter,
+    SortDesc,
+    TableSort,
 } 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 {
     Cycle as CycleIcon,
     Datanode as DatanodeIcon,
@@ -48,6 +65,7 @@ import {
     EmptyArray,
     FlagSx,
     ParentItemSx,
+    getUpdateVarNames,
     iconLabelSx,
     tinyIconButtonSx,
     tinySelPinIconButtonSx,
@@ -89,6 +107,10 @@ interface CoreSelectorProps {
     editComponent?: ComponentType<EditProps>;
     showPins?: boolean;
     onSelect?: (id: string | string[]) => void;
+    updateCoreVars: string;
+    filter?: string;
+    sort?: string;
+    showSearch: boolean;
 }
 
 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: {
     item: Entity;
@@ -243,6 +266,61 @@ const getExpandedIds = (nodeId: string, exp?: string[], entities?: Entities) =>
     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 {
         id = "",
@@ -261,6 +339,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
         onChange,
         onSelect,
         coreChanged,
+        updateCoreVars,
+        showSearch,
     } = props;
 
     const [selectedItems, setSelectedItems] = useState<string[]>([]);
@@ -294,7 +374,11 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 const res = isSelected ? [...old, nodeId] : old.filter((id) => id !== nodeId);
                 const scenariosVar = getUpdateVar(updateVars, lovPropertyName);
                 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);
                 return res;
             });
@@ -304,8 +388,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
 
     useEffect(() => {
         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) {
             try {
                 const parsedValue = JSON.parse(defaultValue);
@@ -332,14 +416,25 @@ const CoreSelector = (props: CoreSelectorProps) => {
             setSelectedItems((old) => {
                 if (old.length) {
                     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 old;
             });
-            }
+        }
     }, [entities, updateVars, lovPropertyName, updateVarName, multiple, module, onChange, propagate, dispatch]);
 
     // Refresh on broadcast
@@ -408,22 +503,165 @@ const CoreSelector = (props: CoreSelectorProps) => {
         [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 (
         <>
-            {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
                 slots={treeSlots}
                 sx={treeViewSx}
@@ -433,8 +671,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 expandedItems={expandedItems}
                 onItemExpansionToggle={onItemExpand}
             >
-                {entities
-                    ? entities.map((item) => (
+                {foundEntities
+                    ? foundEntities.map((item) => (
                           <CoreItem
                               key={item ? item[0] : ""}
                               item={item}

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

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

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

@@ -409,8 +409,7 @@ const JobSelectedTableRow = ({
     showCancel,
     showDelete
 }: 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 (
         <TableRow

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

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

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

@@ -11,7 +11,7 @@
  * 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 Box from "@mui/material/Box";
@@ -41,15 +41,11 @@ import {
     createSendActionNameAction,
     getUpdateVar,
     createSendUpdateAction,
-    TableFilter,
-    ColumnDesc,
-    FilterDesc,
     useDynamicProperty,
-    createRequestUpdateAction,
 } from "taipy-gui";
 
 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 { Cycles, NodeType, Scenarios } from "./utils/types";
 
@@ -101,6 +97,7 @@ interface ScenarioSelectorProps {
     multiple?: boolean;
     filter?: string;
     updateScVars?: string;
+    showSearch?: boolean;
 }
 
 interface ScenarioEditDialogProps {
@@ -437,6 +434,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
         multiple = false,
         updateVars = "",
         updateScVars = "",
+        showSearch = true
     } = props;
     const [open, setOpen] = useState(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
@@ -447,43 +445,6 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     const dispatch = useDispatch();
     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(
         (...values: unknown[]) => {
             dispatch(
@@ -567,14 +528,6 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     return (
         <>
             <Box sx={MainTreeBoxSx} id={props.id} className={className}>
-                {active && colFilters ? (
-                    <TableFilter
-                        columns={colFilters}
-                        appliedFilters={filters}
-                        filteredCount={0}
-                        onValidate={applyFilters}
-                    ></TableFilter>
-                ) : null}
                 <CoreSelector
                     {...props}
                     entities={props.innerScenarios}
@@ -583,6 +536,8 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                     editComponent={EditScenario}
                     showPins={showPins}
                     multiple={multiple}
+                    updateCoreVars={updateScVars}
+                    showSearch={showSearch}
                 />
                 {showAddButton ? (
                     <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
 community a harassment-free experience for everyone, regardless of age, body
 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
 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
 community a harassment-free experience for everyone, regardless of age, body
 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
 and orientation.
 

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

@@ -104,6 +104,4 @@ def _get_manager(manager: str) -> _Manager:
         "job": _JobManagerFactory._build_manager(),
         "task": _TaskManagerFactory._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
     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:
-            submissions = _SubmissionManagerFactory._build_manager()._get_all()
+            submissions = submission_manager._get_all()
             cls.__logger.error(f"Submission {job.submit_id} not found.")
             msg = "\n--------------------------------------------------------------------------------\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]):
         """
         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
         should be revised in the future.

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

@@ -82,6 +82,7 @@ class Notifier:
                     <li>TASK</li>
                     <li>DATA_NODE</li>
                     <li>JOB</li>
+                    <li>SUBMISSION</li>
                 </ul>
             entity_id (Optional[str]): If provided, the listener will be notified
                 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
 # specific language governing permissions and limitations under the License.
 
+from threading import Lock
 from typing import List, Optional, Union
 
+from taipy.logger._taipy_logger import _TaipyLogger
+
 from .._entity._entity_ids import _EntityIds
 from .._manager._manager import _Manager
 from .._repository._abstract_repository import _AbstractRepository
 from .._version._version_mixin import _VersionMixin
 from ..exceptions.exceptions import SubmissionNotDeletedException
+from ..job.job import Job, Status
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
 from ..scenario.scenario import Scenario
 from ..sequence.sequence import Sequence
@@ -27,6 +31,8 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
     _ENTITY_NAME = Submission.__name__
     _repository: _AbstractRepository
     _EVENT_ENTITY_TYPE = EventEntityType.SUBMISSION
+    __lock = Lock()
+    __logger = _TaipyLogger._get_logger()
 
     @classmethod
     def _get_all(cls, version_number: Optional[str] = None) -> List[Submission]:
@@ -47,6 +53,88 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
 
         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
     def _get_latest(cls, entity: Union[Scenario, Sequence, Task]) -> Optional[Submission]:
         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 typing import Any, Dict, List, Optional, Set, Union
 
-from taipy.logger._taipy_logger import _TaipyLogger
-
 from .._entity._entity import _Entity
 from .._entity._labeled import _Labeled
 from .._entity._properties import _Properties
 from .._entity._reload import _Reloader, _self_reload, _self_setter
 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_status import SubmissionStatus
 
@@ -45,7 +43,6 @@ class Submission(_Entity, _Labeled):
     _MANAGER_NAME = "submission"
     __SEPARATOR = "_"
     lock = threading.Lock()
-    __logger = _TaipyLogger._get_logger()
 
     def __init__(
         self,
@@ -192,66 +189,6 @@ class Submission(_Entity, _Labeled):
     def __ge__(self, other):
         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:
         """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
 community a harassment-free experience for everyone, regardless of age, body
 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
 and orientation.
 

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

@@ -597,8 +597,15 @@ class _Builder:
         return self
 
     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]:
+        val = default_val if val is None else val
         if not hash_name and isinstance(val, str):
             val = [elt_type(t.strip()) for t in val.split(";")]
         if isinstance(val, list):
@@ -966,8 +973,15 @@ class _Builder:
                     attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, False)
                 )
             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:
                 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.__set_react_attribute(prop_name, hash_name)
                 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()
         return self

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

@@ -184,14 +184,16 @@ class Element:
                             hash_value = "None"
                         val = val[: m.start()] + hash_value + val[m.end() :]
                     # handling unique id replacement in inner properties <tp:uniq:...>
+                    has_uniq = False
                     while m := Element.__RE_UNIQUE_VAR.search(val):
+                        has_uniq = True
                         id = uniques.get(m.group(1))
                         if id is None:
                             id = len(uniques) + 1
                             uniques[m.group(1)] = id
                         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
         # this modifies attributes

+ 9 - 3
taipy/gui_core/_GuiCoreLib.py

@@ -22,7 +22,8 @@ from ._adapters import (
     _GuiCoreDoNotUpdate,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioDagAdapter,
-    _GuiCoreScenarioProperties,
+    _GuiCoreScenarioFilter,
+    _GuiCoreScenarioSort,
 )
 from ._context import _GuiCoreContext
 
@@ -44,6 +45,7 @@ class _GuiCore(ElementLibrary):
     __SCENARIO_SELECTOR_ERROR_VAR = "__tpgc_sc_error"
     __SCENARIO_SELECTOR_ID_VAR = "__tpgc_sc_id"
     __SCENARIO_SELECTOR_FILTER_VAR = "__tpgc_sc_filter"
+    __SCENARIO_SELECTOR_SORT_VAR = "__tpgc_sc_sort"
     __SCENARIO_VIZ_ERROR_VAR = "__tpgc_sv_error"
     __JOB_SELECTOR_ERROR_VAR = "__tpgc_js_error"
     __DATANODE_VIZ_ERROR_VAR = "__tpgc_dv_error"
@@ -73,13 +75,16 @@ class _GuiCore(ElementLibrary):
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 "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_scenarios": ElementProperty(
                     PropertyType.lov,
                     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}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
@@ -94,6 +99,7 @@ class _GuiCore(ElementLibrary):
                 "update_sc_vars": ElementProperty(
                     PropertyType.string,
                     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"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sc>",
                 ),

+ 56 - 16
taipy/gui_core/_adapters.py

@@ -11,7 +11,9 @@
 
 import json
 import math
+import sys
 import typing as t
+from abc import abstractmethod
 from datetime import date, datetime
 from enum import Enum
 from numbers import Number
@@ -19,16 +21,7 @@ from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
 
 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.config import Config
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
@@ -264,7 +257,7 @@ def _get_datanode_property(attr: str):
 
 
 class _GuiCoreScenarioProperties(_TaipyBase):
-    __SC_TYPES = {
+    _SC_TYPES = {
         "Config id": "string",
         "Label": "string",
         "Creation date": "date",
@@ -284,7 +277,6 @@ class _GuiCoreScenarioProperties(_TaipyBase):
         "Primary": "is_primary",
         "Tags": "tags",
     }
-    DEFAULT = list(__SC_TYPES.keys())
     __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"}
     __ENUMS = None
@@ -297,7 +289,7 @@ class _GuiCoreScenarioProperties(_TaipyBase):
     def get_type(attr: str):
         if prop := _get_datanode_property(attr):
             return _GuiCoreScenarioProperties.__DN_TYPES.get(prop, "any")
-        return _GuiCoreScenarioProperties.__SC_TYPES.get(attr, "any")
+        return _GuiCoreScenarioProperties._SC_TYPES.get(attr, "any")
 
     @staticmethod
     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 _GuiCoreScenarioProperties.__SC_LABELS.get(attr, attr)
 
+    @staticmethod
+    @abstractmethod
+    def get_default_list():
+        raise NotImplementedError
+
+    @staticmethod
+    @abstractmethod
+    def full_desc():
+        raise NotImplementedError
+
     def get(self):
         data = super().get()
         if _is_boolean(data):
             if _is_true(data):
-                data = _GuiCoreScenarioProperties.DEFAULT
+                data = self.get_default_list()
             else:
                 return None
         if isinstance(data, str):
@@ -318,10 +320,10 @@ class _GuiCoreScenarioProperties(_TaipyBase):
             flist = []
             for f in data:
                 if f == "*":
-                    flist.extend(_GuiCoreScenarioProperties.DEFAULT)
+                    flist.extend(self.get_default_list())
                 else:
                     flist.append(f)
-            if _GuiCoreScenarioProperties.__ENUMS is None:
+            if _GuiCoreScenarioProperties.__ENUMS is None and self.full_desc():
                 _GuiCoreScenarioProperties.__ENUMS = {
                     "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", [])],
@@ -329,8 +331,46 @@ class _GuiCoreScenarioProperties(_TaipyBase):
             return json.dumps(
                 [
                     (attr, _GuiCoreScenarioProperties.get_type(attr), _GuiCoreScenarioProperties.__ENUMS.get(attr))
+                    if self.full_desc()
+                    else (attr,)
                     for attr in flist
                     if attr and isinstance(attr, str)
                 ]
             )
         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 typing as t
 from collections import defaultdict
-from datetime import datetime
+from datetime import date, datetime
 from numbers import Number
+from operator import attrgetter
 from threading import Lock
 
 try:
@@ -66,6 +67,7 @@ from ._adapters import (
     _GuiCoreDatanodeAdapter,
     _GuiCoreScenarioProperties,
     _invoke_action,
+    _is_debugging,
 )
 
 
@@ -126,7 +128,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             with self.lock:
                 self.jobs_list = None
         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:
             with self.lock:
                 self.data_nodes_by_owner = None
@@ -144,7 +146,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             {"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)):
             return
         try:
@@ -180,7 +182,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             self.gui._call_user_callback(
                                 client_id,
                                 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"),
                             )
 
@@ -203,7 +212,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
     def no_change_adapter(self, entity: t.List):
         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:
             if (
                 isinstance(cycle, Cycle)
@@ -214,10 +223,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 return [
                     cycle.id,
                     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,
                     False,
                 ]
@@ -257,8 +263,27 @@ class _GuiCoreContext(CoreEventConsumerBase):
         cycle[2] = [self.scenario_adapter(e) for e in cycle[2]]
         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(
-        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]] = []
         with self.lock:
@@ -272,11 +297,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         cycles_scenarios.append(cycle)
         if scenarios is not None:
             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:
             # filtering
             filtered_list = list(adapted_list)
@@ -565,7 +586,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             self.__do_datanodes_tree()
         if scenarios is None:
             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:
             return []
@@ -745,9 +766,18 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         ent.properties.pop(key, None)
 
     @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):
         cycles_scenarios: t.List[t.Union[Scenario, Cycle]] = []
@@ -767,7 +797,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         cycles_scenarios.extend(scenarios_cycle)
                     elif isinstance(entity, Scenario):
                         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):
         if id and (dn := core_get(id)) and isinstance(dn, DataNode):

+ 18 - 5
taipy/gui_core/viselements.json

@@ -100,8 +100,21 @@
                     },
                     {
                         "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",
                         "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": [
                             [
                                 "state",
                                 "State"
                             ],
                             [
-                                "submittable",
-                                "Submittable"
+                                "submission",
+                                "Submission"
                             ],
                             [
                                 "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
 community a harassment-free experience for everyone, regardless of age, body
 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
 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
 community a harassment-free experience for everyone, regardless of age, body
 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
 and orientation.
 

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

@@ -127,7 +127,7 @@ def test_to_bool():
     with pytest.raises(InconsistentEnvVariableError):
         _TemplateHandler._to_bool("no")
     with pytest.raises(InconsistentEnvVariableError):
-        _TemplateHandler._to_bool("tru")
+        _TemplateHandler._to_bool("tru") # codespell:ignore tru
     with pytest.raises(InconsistentEnvVariableError):
         _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.notifier import Notifier
 from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
+from taipy.core.submission.submission_status import SubmissionStatus
 
 
 def test_register():
@@ -742,6 +743,21 @@ def test_publish_submission_event():
         and event.attribute_name == expected_attribute_names[i]
         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():

+ 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]
     for job_id in job_ids:
         job = jobs[job_id]
-        submission._update_submission_status(job)
+        _SubmissionManagerFactory._build_manager()._update_submission_status(submission, job)
     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):
+    submission_manager = _SubmissionManagerFactory._build_manager()
+
     job = MockJob("job_id", Status.SUBMITTED)
     submission = Submission("submission_id", "ENTITY_TYPE", "entity_config_id")
-    _SubmissionManagerFactory._build_manager()._set(submission)
+    submission_manager._set(submission)
 
     assert submission.submission_status == SubmissionStatus.SUBMITTED
 
     for job_status, submission_status in zip(job_statuses, expected_submission_statuses):
         job.status = job_status
-        submission._update_submission_status(job)
+        submission_manager._update_submission_status(submission, job)
         assert submission.submission_status == submission_status
 
 
 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}
     submission = Submission("submission_id", "ENTITY_TYPE", "entity_config_id")
-    _SubmissionManagerFactory._build_manager()._set(submission)
+    submission_manager._set(submission)
 
     assert submission.submission_status == SubmissionStatus.SUBMITTED
 
     for (job_id, job_status), submission_status in zip(job_statuses, expected_submission_statuses):
         job = jobs[job_id]
         job.status = job_status
-        submission._update_submission_status(job)
+        submission_manager._update_submission_status(submission, job)
         assert submission.submission_status == submission_status
 
 

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff