Procházet zdrojové kódy

Filter scenarios (#1307)

* Filter scenarios
support Reason from is_submittable
resolves #1075

* fix is_submittable test

* Fab's comments

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide před 1 rokem
rodič
revize
074301b299

+ 14 - 6
frontend/taipy-gui/packaging/taipy-gui.d.ts

@@ -304,23 +304,31 @@ export interface ColumnDesc {
     title?: string;
     title?: string;
     /** The order of the column. */
     /** The order of the column. */
     index: number;
     index: number;
-    /** The width. */
+    /** The column width. */
     width?: number | string;
     width?: number | string;
-    /** If set to true, the column should not be editable. */
+    /** If true, the column cannot be edited. */
     notEditable?: boolean;
     notEditable?: boolean;
-    /** The column name that would hold the css classname to apply to the cell. */
+    /** The name of the column that holds the CSS classname to
+     *  apply to the cells. */
     style?: string;
     style?: string;
+    /** The name of the column that holds the tooltip to
+     *  show on the cells. */
+    tooltip?: string;
     /** The value that would replace a NaN value. */
     /** The value that would replace a NaN value. */
     nanValue?: string;
     nanValue?: string;
-    /** The TimeZone identifier used if the type is Date. */
+    /** The TimeZone identifier used if the type is `date`. */
     tz?: string;
     tz?: string;
     /** The flag that allows filtering. */
     /** The flag that allows filtering. */
     filter?: boolean;
     filter?: boolean;
-    /** The identifier for the aggregation function. */
+    /** The name of the aggregation function. */
     apply?: string;
     apply?: string;
-    /** The flag that would allow the user to aggregate the column. */
+    /** The flag that allows the user to aggregate the column. */
     groupBy?: boolean;
     groupBy?: boolean;
     widthHint?: number;
     widthHint?: number;
+    /** The list of values that can be used on edit. */
+    lov?: string[];
+    /** If true the user can enter any value besides the lov values. */
+    freeLov?: boolean;
 }
 }
 /**
 /**
  * A cell value type.
  * A cell value type.

+ 51 - 27
frontend/taipy-gui/src/components/Taipy/TableFilter.tsx

@@ -11,7 +11,8 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import React, { ChangeEvent, SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import Autocomplete from "@mui/material/Autocomplete";
 import CheckIcon from "@mui/icons-material/Check";
 import CheckIcon from "@mui/icons-material/Check";
 import DeleteIcon from "@mui/icons-material/Delete";
 import DeleteIcon from "@mui/icons-material/Delete";
 import FilterListIcon from "@mui/icons-material/FilterList";
 import FilterListIcon from "@mui/icons-material/FilterList";
@@ -82,30 +83,39 @@ const actionsByType = {
     },
     },
 } as Record<string, Record<string, string>>;
 } as Record<string, Record<string, string>>;
 
 
-const gridSx = { p: "0.5em" };
+const gridSx = { p: "0.5em", minWidth: "36rem" };
+const autocompleteSx = { "& .MuiInputBase-root": { padding: "0" } };
+const badgeSx = {
+    "& .MuiBadge-badge": {
+        height: "10px",
+        minWidth: "10px",
+        width: "10px",
+        borderRadius: "5px",
+    },
+};
 
 
 const getActionsByType = (colType?: string) =>
 const getActionsByType = (colType?: string) =>
-    (colType && colType in actionsByType && actionsByType[colType]) || actionsByType["string"];
+    (colType && colType in actionsByType && actionsByType[colType]) ||
+    (colType === "any" ? { ...actionsByType.string, ...actionsByType.number } : actionsByType.string);
 
 
 const getFilterDesc = (columns: Record<string, ColumnDesc>, colId?: string, act?: string, val?: string) => {
 const getFilterDesc = (columns: Record<string, ColumnDesc>, colId?: string, act?: string, val?: string) => {
     if (colId && act && val !== undefined) {
     if (colId && act && val !== undefined) {
         const colType = getTypeFromDf(columns[colId].type);
         const colType = getTypeFromDf(columns[colId].type);
-        if (!val && (colType == "date" || colType == "number" || colType == "boolean")) {
+        if (!val && (colType === "date" || colType === "number" || colType === "boolean")) {
             return;
             return;
         }
         }
         try {
         try {
-            const typedVal =
-                colType == "number"
-                    ? parseFloat(val)
-                    : colType == "boolean"
-                    ? val == "1"
-                    : colType == "date"
-                    ? getDateTime(val)
-                    : val;
             return {
             return {
                 col: columns[colId].dfid,
                 col: columns[colId].dfid,
                 action: act,
                 action: act,
-                value: typedVal,
+                value:
+                    colType === "number"
+                        ? parseFloat(val)
+                        : colType === "boolean"
+                        ? val === "1"
+                        : colType === "date"
+                        ? getDateTime(val)
+                        : val,
             } as FilterDesc;
             } as FilterDesc;
         } catch (e) {
         } catch (e) {
             console.info("could not parse value ", val, e);
             console.info("could not parse value ", val, e);
@@ -143,6 +153,13 @@ const FilterRow = (props: FilterRowProps) => {
         },
         },
         [columns, colId, action]
         [columns, colId, action]
     );
     );
+    const onValueAutoComp = useCallback(
+        (e: SyntheticEvent, value: string | null) => {
+            setVal(value || "");
+            setEnableCheck(!!getFilterDesc(columns, colId, action, value || ""));
+        },
+        [columns, colId, action]
+    );
     const onValueSelect = useCallback(
     const onValueSelect = useCallback(
         (e: SelectChangeEvent<string>) => {
         (e: SelectChangeEvent<string>) => {
             setVal(e.target.value);
             setVal(e.target.value);
@@ -190,6 +207,7 @@ const FilterRow = (props: FilterRowProps) => {
 
 
     const colType = getTypeFromDf(colId in columns ? columns[colId].type : "");
     const colType = getTypeFromDf(colId in columns ? columns[colId].type : "");
     const colFormat = colId in columns && columns[colId].format ? columns[colId].format : defaultDateFormat;
     const colFormat = colId in columns && columns[colId].format ? columns[colId].format : defaultDateFormat;
+    const colLov = colId in columns && columns[colId].lov ? columns[colId].lov : undefined;
 
 
     return (
     return (
         <Grid container item xs={12} alignItems="center">
         <Grid container item xs={12} alignItems="center">
@@ -223,7 +241,7 @@ const FilterRow = (props: FilterRowProps) => {
                 {colType == "number" ? (
                 {colType == "number" ? (
                     <TextField
                     <TextField
                         type="number"
                         type="number"
-                        value={typeof val == "number" ? val : val || ""}
+                        value={typeof val === "number" ? val : val || ""}
                         onChange={onValueChange}
                         onChange={onValueChange}
                         label="Number"
                         label="Number"
                         margin="dense"
                         margin="dense"
@@ -247,6 +265,23 @@ const FilterRow = (props: FilterRowProps) => {
                         format={colFormat}
                         format={colFormat}
                         margin="dense"
                         margin="dense"
                     />
                     />
+                ) : colLov ? (
+                    <Autocomplete
+                        freeSolo
+                        disableClearable
+                        options={colLov}
+                        value={val || ""}
+                        onChange={onValueAutoComp}
+                        renderInput={(params) => (
+                            <TextField
+                                {...params}
+                                className="MuiAutocomplete-inputRootDense"
+                                label={`${val ? "" : "Empty "}String`}
+                                margin="dense"
+                            />
+                        )}
+                        sx={autocompleteSx}
+                    />
                 ) : (
                 ) : (
                     <TextField
                     <TextField
                         value={val || ""}
                         value={val || ""}
@@ -285,7 +320,7 @@ const TableFilter = (props: TableFilterProps) => {
     const filterRef = useRef<HTMLButtonElement | null>(null);
     const filterRef = useRef<HTMLButtonElement | null>(null);
     const [filters, setFilters] = useState<Array<FilterDesc>>([]);
     const [filters, setFilters] = useState<Array<FilterDesc>>([]);
 
 
-    const colsOrder = useMemo(()=> {
+    const colsOrder = useMemo(() => {
         if (props.colsOrder) {
         if (props.colsOrder) {
             return props.colsOrder;
             return props.colsOrder;
         }
         }
@@ -338,18 +373,7 @@ const TableFilter = (props: TableFilterProps) => {
                     sx={iconInRowSx}
                     sx={iconInRowSx}
                     className={getSuffixedClassNames(className, "-filter-icon")}
                     className={getSuffixedClassNames(className, "-filter-icon")}
                 >
                 >
-                    <Badge
-                        badgeContent={filters.length}
-                        color="primary"
-                        sx={{
-                            "& .MuiBadge-badge": {
-                                height: "10px",
-                                minWidth: "10px",
-                                width: "10px",
-                                borderRadius: "5px",
-                            },
-                        }}
-                    >
+                    <Badge badgeContent={filters.length} color="primary" sx={badgeSx}>
                         <FilterListIcon fontSize="inherit" />
                         <FilterListIcon fontSize="inherit" />
                     </Badge>
                     </Badge>
                 </IconButton>
                 </IconButton>

+ 1 - 0
frontend/taipy/package-lock.json

@@ -41,6 +41,7 @@
       }
       }
     },
     },
     "../../taipy/gui/webapp": {
     "../../taipy/gui/webapp": {
+      "name": "taipy-gui",
       "version": "3.2.0"
       "version": "3.2.0"
     },
     },
     "node_modules/@babel/code-frame": {
     "node_modules/@babel/code-frame": {

+ 5 - 5
frontend/taipy/src/ScenarioSelector.tsx

@@ -99,7 +99,7 @@ interface ScenarioSelectorProps {
     showPins?: boolean;
     showPins?: boolean;
     showDialog?: boolean;
     showDialog?: boolean;
     multiple?: boolean;
     multiple?: boolean;
-    filterBy?: string;
+    filter?: string;
     updateScVars?: string;
     updateScVars?: string;
 }
 }
 
 
@@ -449,17 +449,17 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
 
 
     const colFilters = useMemo(() => {
     const colFilters = useMemo(() => {
         try {
         try {
-            const res = props.filterBy ? (JSON.parse(props.filterBy) as Array<[string, string]>) : undefined;
+            const res = props.filter ? (JSON.parse(props.filter) as Array<[string, string, string[]]>) : undefined;
             return Array.isArray(res)
             return Array.isArray(res)
-                ? res.reduce((pv, [name, coltype], idx) => {
-                      pv[name] = { dfid: name, type: coltype, index: idx, filter: true };
+                ? res.reduce((pv, [name, coltype, lov], idx) => {
+                      pv[name] = { dfid: name, type: coltype, index: idx, filter: true, lov: lov, freeLov: !!lov };
                       return pv;
                       return pv;
                   }, {} as Record<string, ColumnDesc>)
                   }, {} as Record<string, ColumnDesc>)
                 : undefined;
                 : undefined;
         } catch (e) {
         } catch (e) {
             return undefined;
             return undefined;
         }
         }
-    }, [props.filterBy]);
+    }, [props.filter]);
     const [filters, setFilters] = useState<FilterDesc[]>([]);
     const [filters, setFilters] = useState<FilterDesc[]>([]);
 
 
     const applyFilters = useCallback(
     const applyFilters = useCallback(

+ 22 - 14
frontend/taipy/src/ScenarioViewer.tsx

@@ -104,7 +104,7 @@ interface SequencesRowProps {
     onFocus: (e: MouseEvent<HTMLElement>) => void;
     onFocus: (e: MouseEvent<HTMLElement>) => void;
     focusName: string;
     focusName: string;
     setFocusName: (name: string) => void;
     setFocusName: (name: string) => void;
-    submittable: boolean;
+    notSubmittableReason: string;
     editable: boolean;
     editable: boolean;
     isValid: (sLabel: string, label: string) => boolean;
     isValid: (sLabel: string, label: string) => boolean;
 }
 }
@@ -118,7 +118,7 @@ const tagsAutocompleteSx = {
     maxWidth: "none",
     maxWidth: "none",
 };
 };
 
 
-type SequenceFull = [string, string[], boolean, boolean];
+type SequenceFull = [string, string[], string, boolean];
 // enum SeFProps {
 // enum SeFProps {
 //     label,
 //     label,
 //     tasks,
 //     tasks,
@@ -139,7 +139,7 @@ const SequenceRow = ({
     onFocus,
     onFocus,
     focusName,
     focusName,
     setFocusName,
     setFocusName,
-    submittable,
+    notSubmittableReason,
     editable,
     editable,
     isValid,
     isValid,
 }: SequencesRowProps) => {
 }: SequencesRowProps) => {
@@ -197,7 +197,7 @@ const SequenceRow = ({
 
 
     const name = `sequence${number}`;
     const name = `sequence${number}`;
     const disabled = !enableScenarioFields || !active;
     const disabled = !enableScenarioFields || !active;
-    const disabledSubmit = disabled || !submittable;
+    const disabledSubmit = disabled || !!notSubmittableReason;
 
 
     return (
     return (
         <Grid item xs={12} container justifyContent="space-between" data-focus={name} onClick={onFocus} sx={hoverSx}>
         <Grid item xs={12} container justifyContent="space-between" data-focus={name} onClick={onFocus} sx={hoverSx}>
@@ -285,7 +285,9 @@ const SequenceRow = ({
                         {pLabel && submit ? (
                         {pLabel && submit ? (
                             <Tooltip
                             <Tooltip
                                 title={
                                 title={
-                                    disabledSubmit ? `Cannot submit Sequence '${label}'` : `Submit Sequence '${label}'`
+                                    disabledSubmit
+                                        ? notSubmittableReason || `Cannot submit Sequence '${label}'`
+                                        : `Submit Sequence '${label}'`
                                 }
                                 }
                             >
                             >
                                 <span>
                                 <span>
@@ -323,7 +325,7 @@ const invalidScenario: ScenarioFull = [
     [],
     [],
     false,
     false,
     false,
     false,
-    false,
+    "invalid",
     false,
     false,
     false,
     false,
 ];
 ];
@@ -380,7 +382,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         scAuthorizedTags,
         scAuthorizedTags,
         scDeletable,
         scDeletable,
         scPromotable,
         scPromotable,
-        scSubmittable,
+        scNotSubmittableReason,
         scReadable,
         scReadable,
         scEditable,
         scEditable,
     ] = scenario || invalidScenario;
     ] = scenario || invalidScenario;
@@ -430,7 +432,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                         id: scId,
                         id: scId,
                         sequence: label,
                         sequence: label,
                         on_submission_change: props.onSubmissionChange,
                         on_submission_change: props.onSubmissionChange,
-                        error_id: getUpdateVar(updateScVar, "error_id")
+                        error_id: getUpdateVar(updateScVar, "error_id"),
                     })
                     })
                 );
                 );
         },
         },
@@ -445,7 +447,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                     createSendActionNameAction(id, module, props.onSubmit, {
                     createSendActionNameAction(id, module, props.onSubmit, {
                         id: scId,
                         id: scId,
                         on_submission_change: props.onSubmissionChange,
                         on_submission_change: props.onSubmissionChange,
-                        error_id: getUpdateVar(updateScVar, "error_id")
+                        error_id: getUpdateVar(updateScVar, "error_id"),
                     })
                     })
                 );
                 );
             }
             }
@@ -571,7 +573,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         [sequences]
         [sequences]
     );
     );
 
 
-    const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], true, true]]), []);
+    const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], "", true]]), []);
 
 
     // on scenario change
     // on scenario change
     useEffect(() => {
     useEffect(() => {
@@ -590,7 +592,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         }
         }
     }, [props.coreChanged, props.updateVarName, id, module, dispatch, scId]);
     }, [props.coreChanged, props.updateVarName, id, module, dispatch, scId]);
 
 
-    const disabled = !valid || !active || !scSubmittable;
+    const disabled = !valid || !active || !!scNotSubmittableReason;
 
 
     return (
     return (
         <>
         <>
@@ -621,7 +623,13 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                             </Grid>
                             </Grid>
                             <Grid item>
                             <Grid item>
                                 {showSubmit ? (
                                 {showSubmit ? (
-                                    <Tooltip title={disabled ? "Cannot submit Scenario" : "Submit Scenario"}>
+                                    <Tooltip
+                                        title={
+                                            disabled
+                                                ? scNotSubmittableReason || "Cannot submit Scenario"
+                                                : "Submit Scenario"
+                                        }
+                                    >
                                         <span>
                                         <span>
                                             <Button
                                             <Button
                                                 onClick={submitScenario}
                                                 onClick={submitScenario}
@@ -840,7 +848,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                     </Grid>
                                     </Grid>
 
 
                                     {sequences.map((item, index) => {
                                     {sequences.map((item, index) => {
-                                        const [label, taskIds, submittable, editable] = item;
+                                        const [label, taskIds, notSubmittableReason, editable] = item;
                                         return (
                                         return (
                                             <SequenceRow
                                             <SequenceRow
                                                 active={active}
                                                 active={active}
@@ -856,7 +864,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                                 onFocus={onFocus}
                                                 onFocus={onFocus}
                                                 focusName={focusName}
                                                 focusName={focusName}
                                                 setFocusName={setFocusName}
                                                 setFocusName={setFocusName}
-                                                submittable={submittable}
+                                                notSubmittableReason={notSubmittableReason}
                                                 editable={editable}
                                                 editable={editable}
                                                 isValid={isValidSequence}
                                                 isValid={isValidSequence}
                                             />
                                             />

+ 2 - 3
frontend/taipy/src/utils.ts

@@ -24,12 +24,12 @@ export type ScenarioFull = [
     string,     // label
     string,     // label
     string[],   // tags
     string[],   // tags
     Array<[string, string]>,    // properties
     Array<[string, string]>,    // properties
-    Array<[string, string[], boolean, boolean]>,   // sequences (label, task ids, submittable, editable)
+    Array<[string, string[], string, boolean]>,   // sequences (label, task ids, notSubmittableReason, editable)
     Record<string, string>, // tasks (id: label)
     Record<string, string>, // tasks (id: label)
     string[],   // authorized_tags
     string[],   // authorized_tags
     boolean,    // deletable
     boolean,    // deletable
     boolean,    // promotable
     boolean,    // promotable
-    boolean,    // submittable
+    string,     // notSubmittableReason
     boolean,    // readable
     boolean,    // readable
     boolean     // editable
     boolean     // editable
 ];
 ];
@@ -216,7 +216,6 @@ export const selectSx = { m: 1, width: 300 };
 
 
 export const DeleteIconSx = { height: 50, width: 50, p: 0 };
 export const DeleteIconSx = { height: 50, width: 50, p: 0 };
 
 
-
 export const EmptyArray = [];
 export const EmptyArray = [];
 
 
 export const getUpdateVarNames = (updateVars: string, ...vars: string[]) => vars.map((v) => getUpdateVar(updateVars, v) || "").filter(v => v);
 export const getUpdateVarNames = (updateVars: string, ...vars: string[]) => vars.map((v) => getUpdateVar(updateVars, v) || "").filter(v => v);

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

@@ -31,7 +31,7 @@ from ..utils import (
     _getscopeattr,
     _getscopeattr,
     _getscopeattr_drill,
     _getscopeattr_drill,
     _is_boolean,
     _is_boolean,
-    _is_boolean_true,
+    _is_true,
     _MapDict,
     _MapDict,
     _to_camel_case,
     _to_camel_case,
 )
 )
@@ -206,7 +206,7 @@ class _Builder:
 
 
     def __get_boolean_attribute(self, name: str, default_value=False):
     def __get_boolean_attribute(self, name: str, default_value=False):
         boolattr = self.__attributes.get(name, default_value)
         boolattr = self.__attributes.get(name, default_value)
-        return _is_boolean_true(boolattr) if isinstance(boolattr, str) else bool(boolattr)
+        return _is_true(boolattr) if isinstance(boolattr, str) else bool(boolattr)
 
 
     def set_boolean_attribute(self, name: str, value: bool):
     def set_boolean_attribute(self, name: str, value: bool):
         """
         """
@@ -481,7 +481,7 @@ class _Builder:
     def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]):
     def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]):
         rebuild = self.__attributes.get("rebuild", False)
         rebuild = self.__attributes.get("rebuild", False)
         rebuild_hash = self.__hashes.get("rebuild")
         rebuild_hash = self.__hashes.get("rebuild")
-        if rebuild_hash or _is_boolean_true(rebuild):
+        if rebuild_hash or _is_true(rebuild):
             attributes, hashes = self.__filter_attributes_hashes(self.__filter_attribute_names(attribute_names))
             attributes, hashes = self.__filter_attributes_hashes(self.__filter_attribute_names(attribute_names))
             rebuild_name = f"bool({self.__gui._get_real_var_name(rebuild_hash)[0]})" if rebuild_hash else "None"
             rebuild_name = f"bool({self.__gui._get_real_var_name(rebuild_hash)[0]})" if rebuild_hash else "None"
             try:
             try:
@@ -825,7 +825,7 @@ class _Builder:
 
 
     def _set_labels(self, var_name: str = "labels"):
     def _set_labels(self, var_name: str = "labels"):
         if value := self.__attributes.get(var_name):
         if value := self.__attributes.get(var_name):
-            if _is_boolean_true(value):
+            if _is_true(value):
                 return self.__set_react_attribute(_to_camel_case(var_name), True)
                 return self.__set_react_attribute(_to_camel_case(var_name), True)
             elif isinstance(value, (dict, _MapDict)):
             elif isinstance(value, (dict, _MapDict)):
                 return self.set_dict_attribute(var_name)
                 return self.set_dict_attribute(var_name)

+ 1 - 1
taipy/gui/utils/__init__.py

@@ -21,7 +21,7 @@ from ._locals_context import _LocalsContext
 from ._map_dict import _MapDict
 from ._map_dict import _MapDict
 from ._runtime_manager import _RuntimeManager
 from ._runtime_manager import _RuntimeManager
 from ._variable_directory import _variable_decode, _variable_encode, _VariableDirectory
 from ._variable_directory import _variable_decode, _variable_encode, _VariableDirectory
-from .boolean import _is_boolean, _is_boolean_true
+from .boolean import _is_boolean, _is_true
 from .clientvarname import _get_broadcast_var_name, _get_client_var_name, _to_camel_case
 from .clientvarname import _get_broadcast_var_name, _get_client_var_name, _to_camel_case
 from .datatype import _get_data_type
 from .datatype import _get_data_type
 from .date import _date_to_string, _string_to_date
 from .date import _date_to_string, _string_to_date

+ 1 - 1
taipy/gui/utils/boolean.py

@@ -12,7 +12,7 @@
 import typing as t
 import typing as t
 
 
 
 
-def _is_boolean_true(s: t.Union[bool, str]) -> bool:
+def _is_true(s: t.Union[bool, str]) -> bool:
     return (
     return (
         s
         s
         if isinstance(s, bool)
         if isinstance(s, bool)

+ 4 - 4
taipy/gui/utils/table_col_builder.py

@@ -13,7 +13,7 @@ import re
 import typing as t
 import typing as t
 
 
 from .._warnings import _warn
 from .._warnings import _warn
-from .boolean import _is_boolean, _is_boolean_true
+from .boolean import _is_boolean, _is_true
 from .clientvarname import _to_camel_case
 from .clientvarname import _to_camel_case
 
 
 
 
@@ -49,7 +49,7 @@ def _enhance_columns(  # noqa: C901
     _update_col_desc_from_indexed(attributes, columns, "width", elt_name)
     _update_col_desc_from_indexed(attributes, columns, "width", elt_name)
     filters = _get_name_indexed_property(attributes, "filter")
     filters = _get_name_indexed_property(attributes, "filter")
     for k, v in filters.items():
     for k, v in filters.items():
-        if _is_boolean_true(v):
+        if _is_true(v):
             if col_desc := _get_column_desc(columns, k):
             if col_desc := _get_column_desc(columns, k):
                 col_desc["filter"] = True
                 col_desc["filter"] = True
             else:
             else:
@@ -58,12 +58,12 @@ def _enhance_columns(  # noqa: C901
     for k, v in editables.items():
     for k, v in editables.items():
         if _is_boolean(v):
         if _is_boolean(v):
             if col_desc := _get_column_desc(columns, k):
             if col_desc := _get_column_desc(columns, k):
-                col_desc["notEditable"] = not _is_boolean_true(v)
+                col_desc["notEditable"] = not _is_true(v)
             else:
             else:
                 _warn(f"{elt_name}: editable[{k}] is not in the list of displayed columns.")
                 _warn(f"{elt_name}: editable[{k}] is not in the list of displayed columns.")
     group_by = _get_name_indexed_property(attributes, "group_by")
     group_by = _get_name_indexed_property(attributes, "group_by")
     for k, v in group_by.items():
     for k, v in group_by.items():
-        if _is_boolean_true(v):
+        if _is_true(v):
             if col_desc := _get_column_desc(columns, k):
             if col_desc := _get_column_desc(columns, k):
                 col_desc["groupBy"] = True
                 col_desc["groupBy"] = True
             else:
             else:

+ 16 - 17
taipy/gui_core/_GuiCoreLib.py

@@ -12,18 +12,27 @@
 import typing as t
 import typing as t
 from datetime import datetime
 from datetime import datetime
 
 
+from taipy.core import Cycle, DataNode, Job, Scenario, Sequence, Task
 from taipy.gui import Gui
 from taipy.gui import Gui
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
 
 
 from ..version import _get_version
 from ..version import _get_version
 from ._adapters import (
 from ._adapters import (
     _GuiCoreDatanodeAdapter,
     _GuiCoreDatanodeAdapter,
+    _GuiCoreDoNotUpdate,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioDagAdapter,
     _GuiCoreScenarioDagAdapter,
     _GuiCoreScenarioProperties,
     _GuiCoreScenarioProperties,
 )
 )
 from ._context import _GuiCoreContext
 from ._context import _GuiCoreContext
 
 
+Scenario.__bases__ += (_GuiCoreDoNotUpdate,)
+Sequence.__bases__ += (_GuiCoreDoNotUpdate,)
+DataNode.__bases__ += (_GuiCoreDoNotUpdate,)
+Cycle.__bases__ += (_GuiCoreDoNotUpdate,)
+Job.__bases__ += (_GuiCoreDoNotUpdate,)
+Task.__bases__ += (_GuiCoreDoNotUpdate,)
+
 
 
 class _GuiCore(ElementLibrary):
 class _GuiCore(ElementLibrary):
     __LIB_NAME = "taipy_gui_core"
     __LIB_NAME = "taipy_gui_core"
@@ -64,7 +73,7 @@ class _GuiCore(ElementLibrary):
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
                 "multiple": ElementProperty(PropertyType.boolean, False),
-                "filter_by": ElementProperty(_GuiCoreScenarioProperties),
+                "filter": ElementProperty(_GuiCoreScenarioProperties, _GuiCoreScenarioProperties.DEFAULT),
             },
             },
             inner_properties={
             inner_properties={
                 "inner_scenarios": ElementProperty(
                 "inner_scenarios": ElementProperty(
@@ -75,9 +84,7 @@ class _GuiCore(ElementLibrary):
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(
-                    PropertyType.react, f"{{{__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sc>}}"
-                ),
+                "error": ElementProperty(PropertyType.react, f"{{{__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sc>}}"),
                 "type": ElementProperty(PropertyType.inner, __SCENARIO_ADAPTER),
                 "type": ElementProperty(PropertyType.inner, __SCENARIO_ADAPTER),
                 "scenario_edit": ElementProperty(
                 "scenario_edit": ElementProperty(
                     _GuiCoreScenarioAdapter,
                     _GuiCoreScenarioAdapter,
@@ -116,9 +123,7 @@ class _GuiCore(ElementLibrary):
                 "on_submit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.submit_entity}}"),
                 "on_submit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.submit_entity}}"),
                 "on_delete": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "on_delete": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(
-                    PropertyType.react, f"{{{__SCENARIO_VIZ_ERROR_VAR}<tp:uniq:sv>}}"
-                ),
+                "error": ElementProperty(PropertyType.react, f"{{{__SCENARIO_VIZ_ERROR_VAR}<tp:uniq:sv>}}"),
                 "update_sc_vars": ElementProperty(
                 "update_sc_vars": ElementProperty(
                     PropertyType.string,
                     PropertyType.string,
                     f"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sv>",
                     f"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sv>",
@@ -188,9 +193,7 @@ class _GuiCore(ElementLibrary):
             inner_properties={
             inner_properties={
                 "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_data_node}}"),
                 "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_data_node}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(
-                    PropertyType.react, f"{{{__DATANODE_VIZ_ERROR_VAR}<tp:uniq:dn>}}"
-                ),
+                "error": ElementProperty(PropertyType.react, f"{{{__DATANODE_VIZ_ERROR_VAR}<tp:uniq:dn>}}"),
                 "scenarios": ElementProperty(
                 "scenarios": ElementProperty(
                     PropertyType.lov,
                     PropertyType.lov,
                     f"{{{__CTX_VAR_NAME}.get_scenarios_for_owner({__DATANODE_VIZ_OWNER_ID_VAR}<tp:uniq:dn>)}}",
                     f"{{{__CTX_VAR_NAME}.get_scenarios_for_owner({__DATANODE_VIZ_OWNER_ID_VAR}<tp:uniq:dn>)}}",
@@ -203,13 +206,11 @@ class _GuiCore(ElementLibrary):
                 ),
                 ),
                 "history": ElementProperty(
                 "history": ElementProperty(
                     PropertyType.react,
                     PropertyType.react,
-                    f"{{{__CTX_VAR_NAME}.get_data_node_history("
-                    + f"{__DATANODE_VIZ_HISTORY_ID_VAR}<tp:uniq:dn>)}}",
+                    f"{{{__CTX_VAR_NAME}.get_data_node_history(" + f"{__DATANODE_VIZ_HISTORY_ID_VAR}<tp:uniq:dn>)}}",
                 ),
                 ),
                 "tabular_data": ElementProperty(
                 "tabular_data": ElementProperty(
                     PropertyType.data,
                     PropertyType.data,
-                    f"{{{__CTX_VAR_NAME}.get_data_node_tabular_data("
-                    + f"{__DATANODE_VIZ_DATA_ID_VAR}<tp:uniq:dn>)}}",
+                    f"{{{__CTX_VAR_NAME}.get_data_node_tabular_data(" + f"{__DATANODE_VIZ_DATA_ID_VAR}<tp:uniq:dn>)}}",
                 ),
                 ),
                 "tabular_columns": ElementProperty(
                 "tabular_columns": ElementProperty(
                     PropertyType.dynamic_string,
                     PropertyType.dynamic_string,
@@ -260,9 +261,7 @@ class _GuiCore(ElementLibrary):
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "type": ElementProperty(PropertyType.inner, __JOB_ADAPTER),
                 "type": ElementProperty(PropertyType.inner, __JOB_ADAPTER),
                 "on_job_action": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.act_on_jobs}}"),
                 "on_job_action": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.act_on_jobs}}"),
-                "error": ElementProperty(
-                    PropertyType.dynamic_string, f"{{{__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>}}"
-                ),
+                "error": ElementProperty(PropertyType.dynamic_string, f"{{{__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>}}"),
                 "update_jb_vars": ElementProperty(
                 "update_jb_vars": ElementProperty(
                     PropertyType.string, f"error_id={__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>"
                     PropertyType.string, f"error_id={__JOB_SELECTOR_ERROR_VAR}<tp:uniq:jb>"
                 ),
                 ),

+ 75 - 35
taipy/gui_core/_adapters.py

@@ -9,10 +9,10 @@
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 # specific language governing permissions and limitations under the License.
 # specific language governing permissions and limitations under the License.
 
 
-import inspect
 import json
 import json
 import math
 import math
 import typing as t
 import typing as t
+from datetime import date, datetime
 from enum import Enum
 from enum import Enum
 from numbers import Number
 from numbers import Number
 from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
 from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
@@ -22,10 +22,7 @@ import pandas as pd
 from taipy.core import (
 from taipy.core import (
     Cycle,
     Cycle,
     DataNode,
     DataNode,
-    Job,
     Scenario,
     Scenario,
-    Sequence,
-    Task,
     is_deletable,
     is_deletable,
     is_editable,
     is_editable,
     is_promotable,
     is_promotable,
@@ -33,26 +30,19 @@ from taipy.core import (
     is_submittable,
     is_submittable,
 )
 )
 from taipy.core import get as core_get
 from taipy.core import get as core_get
+from taipy.core.config import Config
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
 from taipy.gui._warnings import _warn
 from taipy.gui._warnings import _warn
 from taipy.gui.gui import _DoNotUpdate
 from taipy.gui.gui import _DoNotUpdate
-from taipy.gui.utils import _TaipyBase
+from taipy.gui.utils import _is_boolean, _is_true, _TaipyBase
 
 
 
 
 # prevent gui from trying to push scenario instances to the front-end
 # prevent gui from trying to push scenario instances to the front-end
-class _GCDoNotUpdate(_DoNotUpdate):
+class _GuiCoreDoNotUpdate(_DoNotUpdate):
     def __repr__(self):
     def __repr__(self):
         return self.get_label() if hasattr(self, "get_label") else super().__repr__()
         return self.get_label() if hasattr(self, "get_label") else super().__repr__()
 
 
 
 
-Scenario.__bases__ += (_GCDoNotUpdate,)
-Sequence.__bases__ += (_GCDoNotUpdate,)
-DataNode.__bases__ += (_GCDoNotUpdate,)
-Cycle.__bases__ += (_GCDoNotUpdate,)
-Job.__bases__ += (_GCDoNotUpdate,)
-Task.__bases__ += (_GCDoNotUpdate,)
-
-
 class _EntityType(Enum):
 class _EntityType(Enum):
     CYCLE = 0
     CYCLE = 0
     SCENARIO = 1
     SCENARIO = 1
@@ -89,7 +79,7 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                             (
                             (
                                 s.get_simple_label(),
                                 s.get_simple_label(),
                                 [t.id for t in s.tasks.values()] if hasattr(s, "tasks") else [],
                                 [t.id for t in s.tasks.values()] if hasattr(s, "tasks") else [],
-                                is_submittable(s),
+                                "" if (reason := is_submittable(s)) else f"Sequence not submittable: {reason.reasons}",
                                 is_editable(s),
                                 is_editable(s),
                             )
                             )
                             for s in scenario.sequences.values()
                             for s in scenario.sequences.values()
@@ -102,7 +92,7 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                         list(scenario.properties.get("authorized_tags", [])) if scenario.properties else [],
                         list(scenario.properties.get("authorized_tags", [])) if scenario.properties else [],
                         is_deletable(scenario),
                         is_deletable(scenario),
                         is_promotable(scenario),
                         is_promotable(scenario),
-                        is_submittable(scenario),
+                        "" if (reason := is_submittable(scenario)) else f"Scenario not submittable: {reason.reasons}",
                         is_readable(scenario),
                         is_readable(scenario),
                         is_editable(scenario),
                         is_editable(scenario),
                     ]
                     ]
@@ -241,14 +231,6 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
         return _TaipyBase._HOLDER_PREFIX + "Dn"
         return _TaipyBase._HOLDER_PREFIX + "Dn"
 
 
 
 
-def _attr_filter(attrVal: t.Any):
-    return not inspect.isroutine(attrVal)
-
-
-def _attr_type(attr: str):
-    return "date" if "date" in attr else "boolean" if attr.startswith("is_") else "string"
-
-
 _operators: t.Dict[str, t.Callable] = {
 _operators: t.Dict[str, t.Callable] = {
     "==": eq,
     "==": eq,
     "!=": ne,
     "!=": ne,
@@ -260,37 +242,95 @@ _operators: t.Dict[str, t.Callable] = {
 }
 }
 
 
 
 
-def _invoke_action(ent: t.Any, col: str, action: str, val: t.Any) -> bool:
+def _invoke_action(ent: t.Any, col: str, col_type: str, is_dn: bool, action: str, val: t.Any) -> bool:
     try:
     try:
+        if col_type == "any":
+            # when a property is not found, return True only if action is not equals
+            entity = getattr(ent, col.split(".")[0]) if is_dn else ent
+            if not hasattr(entity, "properties") or not entity.properties.get(col):
+                return action == "!="
         if op := _operators.get(action):
         if op := _operators.get(action):
-            return op(attrgetter(col)(ent), val)
+            cur_val = attrgetter(col)(ent)
+            return op(cur_val.isoformat() if isinstance(cur_val, (datetime, date)) else cur_val, val)
     except Exception as e:
     except Exception as e:
         _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
         _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
     return True
     return True
 
 
 
 
+def _get_datanode_property(attr: str):
+    if (parts := attr.split(".")) and len(parts) > 1:
+        return parts[1]
+    return None
+
+
 class _GuiCoreScenarioProperties(_TaipyBase):
 class _GuiCoreScenarioProperties(_TaipyBase):
-    __SCENARIO_ATTRIBUTES = [a[0] for a in inspect.getmembers(Scenario, _attr_filter) if not a[0].startswith("_")]
-    __DATANODE_ATTRIBUTES = [a[0] for a in inspect.getmembers(DataNode, _attr_filter) if not a[0].startswith("_")]
+    __SC_TYPES = {
+        "Config id": "string",
+        "Label": "string",
+        "Creation date": "date",
+        "Cycle label": "string",
+        "Cycle start": "date",
+        "Cycle end": "date",
+        "Primary": "boolean",
+        "Tags": "string",
+    }
+    __SC_LABELS = {
+        "Config id": "config_id",
+        "Creation date": "creation_date",
+        "Label": "name",
+        "Cycle label": "cycle.name",
+        "Cycle start": "cycle.start",
+        "Cycle end": "cycle.end",
+        "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
 
 
     @staticmethod
     @staticmethod
     def get_hash():
     def get_hash():
         return _TaipyBase._HOLDER_PREFIX + "ScP"
         return _TaipyBase._HOLDER_PREFIX + "ScP"
 
 
+    @staticmethod
+    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")
+
+    @staticmethod
+    def get_col_name(attr: str):
+        if prop := _get_datanode_property(attr):
+            return f'{attr.split(".")[0]}.{_GuiCoreScenarioProperties.__DN_LABELS.get(prop, prop)}'
+        return _GuiCoreScenarioProperties.__SC_LABELS.get(attr, attr)
+
     def get(self):
     def get(self):
         data = super().get()
         data = super().get()
+        if _is_boolean(data):
+            if _is_true(data):
+                data = _GuiCoreScenarioProperties.DEFAULT
+            else:
+                return None
         if isinstance(data, str):
         if isinstance(data, str):
             data = data.split(";")
             data = data.split(";")
         if isinstance(data, (list, tuple)):
         if isinstance(data, (list, tuple)):
+            flist = []
+            for f in data:
+                if f == "*":
+                    flist.extend(_GuiCoreScenarioProperties.DEFAULT)
+                else:
+                    flist.append(f)
+            if _GuiCoreScenarioProperties.__ENUMS is None:
+                _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", [])],
+                }
             return json.dumps(
             return json.dumps(
                 [
                 [
-                    (attr, _attr_type(attr))
-                    for attr in data
-                    if attr
-                    and isinstance(attr, str)
-                    and (parts := attr.split("."))
-                    and (len(parts) > 1 and parts[1] in _GuiCoreScenarioProperties.__DATANODE_ATTRIBUTES)
-                    or attr in _GuiCoreScenarioProperties.__SCENARIO_ATTRIBUTES
+                    (attr, _GuiCoreScenarioProperties.get_type(attr), _GuiCoreScenarioProperties.__ENUMS.get(attr))
+                    for attr in flist
+                    if attr and isinstance(attr, str)
                 ]
                 ]
             )
             )
         return None
         return None

+ 21 - 8
taipy/gui_core/_context.py

@@ -60,7 +60,13 @@ from taipy.gui import Gui, State
 from taipy.gui._warnings import _warn
 from taipy.gui._warnings import _warn
 from taipy.gui.gui import _DoNotUpdate
 from taipy.gui.gui import _DoNotUpdate
 
 
-from ._adapters import _attr_type, _EntityType, _GuiCoreDatanodeAdapter, _invoke_action
+from ._adapters import (
+    _EntityType,
+    _get_datanode_property,
+    _GuiCoreDatanodeAdapter,
+    _GuiCoreScenarioProperties,
+    _invoke_action,
+)
 
 
 
 
 class _GuiCoreContext(CoreEventConsumerBase):
 class _GuiCoreContext(CoreEventConsumerBase):
@@ -243,8 +249,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
             )
             )
         return None
         return None
 
 
-    def filter_scenarios(self, cycle: t.List, col: str, action: str, val: t.Any):
-        cycle[2] = [e for e in cycle[2] if _invoke_action(e, col, action, val)]
+    def filter_scenarios(self, cycle: t.List, col: str, col_type: str, is_dn: bool, action: str, val: t.Any):
+        cycle[2] = [e for e in cycle[2] if _invoke_action(e, col, col_type, is_dn, action, val)]
         return cycle
         return cycle
 
 
     def adapt_scenarios(self, cycle: t.List):
     def adapt_scenarios(self, cycle: t.List):
@@ -276,18 +282,24 @@ class _GuiCoreContext(CoreEventConsumerBase):
             filtered_list = list(adapted_list)
             filtered_list = list(adapted_list)
             for fd in filters:
             for fd in filters:
                 col = fd.get("col", "")
                 col = fd.get("col", "")
-                col_type = _attr_type(col)
+                is_datanode_prop = _get_datanode_property(col) is not None
+                col = _GuiCoreScenarioProperties.get_col_name(col)
+                col_type = _GuiCoreScenarioProperties.get_type(col)
                 val = fd.get("value")
                 val = fd.get("value")
                 action = fd.get("action", "")
                 action = fd.get("action", "")
                 if isinstance(val, str) and col_type == "date":
                 if isinstance(val, str) and col_type == "date":
                     val = datetime.fromisoformat(val[:-1])
                     val = datetime.fromisoformat(val[:-1])
                 # level 1 filtering
                 # level 1 filtering
                 filtered_list = [
                 filtered_list = [
-                    e for e in filtered_list if not isinstance(e, Scenario) or _invoke_action(e, col, action, val)
+                    e
+                    for e in filtered_list
+                    if not isinstance(e, Scenario) or _invoke_action(e, col, col_type, is_datanode_prop, action, val)
                 ]
                 ]
                 # level 2 filtering
                 # level 2 filtering
                 filtered_list = [
                 filtered_list = [
-                    self.filter_scenarios(e, col, action, val) if not isinstance(e, Scenario) else e
+                    self.filter_scenarios(e, col, col_type, is_datanode_prop, action, val)
+                    if not isinstance(e, Scenario)
+                    else e
                     for e in filtered_list
                     for e in filtered_list
                 ]
                 ]
             # remove empty cycles
             # remove empty cycles
@@ -515,11 +527,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
             if sequence := data.get("sequence"):
             if sequence := data.get("sequence"):
                 entity = entity.sequences.get(sequence)
                 entity = entity.sequences.get(sequence)
 
 
-            if not is_submittable(entity):
+            if not (reason := is_submittable(entity)):
                 _GuiCoreContext.__assign_var(
                 _GuiCoreContext.__assign_var(
                     state,
                     state,
                     error_var,
                     error_var,
-                    f"{'Sequence' if sequence else 'Scenario'} {sequence or scenario_id} is not submittable.",
+                    f"{'Sequence' if sequence else 'Scenario'} {sequence or scenario_id} is not submittable: "
+                    + reason.reasons,
                 )
                 )
                 return
                 return
             if entity:
             if entity:

+ 1 - 1
taipy/gui_core/viselements.json

@@ -99,7 +99,7 @@
                         "doc": "TODO: If True, the user can select multiple scenarios."
                         "doc": "TODO: If True, the user can select multiple scenarios."
                     },
                     },
                     {
                     {
-                        "name": "filter_by",
+                        "name": "filter",
                         "type": "str|list[str]",
                         "type": "str|list[str]",
                         "doc": "TODO: a list of scenario attributes to filter on."
                         "doc": "TODO: a list of scenario attributes to filter on."
                     }
                     }

+ 10 - 8
tests/gui_core/test_context_is_submitable.py

@@ -13,6 +13,7 @@ from unittest.mock import Mock, patch
 
 
 from taipy.config.common.scope import Scope
 from taipy.config.common.scope import Scope
 from taipy.core import Job, JobId, Scenario, Task
 from taipy.core import Job, JobId, Scenario, Task
+from taipy.core.common.reason import Reason
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.data.pickle import PickleDataNode
 from taipy.gui_core._context import _GuiCoreContext
 from taipy.gui_core._context import _GuiCoreContext
 
 
@@ -22,13 +23,14 @@ a_job = Job(JobId("JOB_job_id"), a_task, "submit_id", a_scenario.id)
 a_job.isfinished = lambda s: True  # type: ignore[attr-defined]
 a_job.isfinished = lambda s: True  # type: ignore[attr-defined]
 a_datanode = PickleDataNode("data_node_config_id", Scope.SCENARIO)
 a_datanode = PickleDataNode("data_node_config_id", Scope.SCENARIO)
 
 
+def mock_is_submittable_reason(entity_id):
+    reason = Reason(entity_id)
+    reason._add_reason(entity_id, "a reason")
+    return reason
 
 
-def mock_is_submittable_false(entity_id):
-    return False
 
 
-
-def mock_is_true(entity_id):
-    return True
+def mock_has_no_reason(entity_id):
+    return Reason(entity_id)
 
 
 
 
 def mock_core_get(entity_id):
 def mock_core_get(entity_id):
@@ -49,7 +51,7 @@ class MockState:
 class TestGuiCoreContext_is_submittable:
 class TestGuiCoreContext_is_submittable:
     def test_submit_entity(self):
     def test_submit_entity(self):
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get), patch(
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get), patch(
-            "taipy.gui_core._context.is_submittable", side_effect=mock_is_true
+            "taipy.gui_core._context.is_submittable", side_effect=mock_has_no_reason
         ):
         ):
             gui_core_context = _GuiCoreContext(Mock())
             gui_core_context = _GuiCoreContext(Mock())
             assign = Mock()
             assign = Mock()
@@ -67,7 +69,7 @@ class TestGuiCoreContext_is_submittable:
             assert assign.call_args.args[0] == "error_var"
             assert assign.call_args.args[0] == "error_var"
             assert str(assign.call_args.args[1]).startswith("Error submitting entity.")
             assert str(assign.call_args.args[1]).startswith("Error submitting entity.")
 
 
-            with patch("taipy.gui_core._context.is_submittable", side_effect=mock_is_submittable_false):
+            with patch("taipy.gui_core._context.is_submittable", side_effect=mock_is_submittable_reason):
                 assign.reset_mock()
                 assign.reset_mock()
                 gui_core_context.submit_entity(
                 gui_core_context.submit_entity(
                     MockState(assign=assign),
                     MockState(assign=assign),
@@ -81,4 +83,4 @@ class TestGuiCoreContext_is_submittable:
                 )
                 )
                 assign.assert_called_once()
                 assign.assert_called_once()
                 assert assign.call_args.args[0] == "error_var"
                 assert assign.call_args.args[0] == "error_var"
-                assert str(assign.call_args.args[1]).endswith("is not submittable.")
+                assert "is not submittable" in str(assign.call_args.args[1])