Просмотр исходного кода

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 1 год назад
Родитель
Сommit
074301b299

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

@@ -304,23 +304,31 @@ export interface ColumnDesc {
     title?: string;
     /** The order of the column. */
     index: number;
-    /** The width. */
+    /** The column width. */
     width?: number | string;
-    /** If set to true, the column should not be editable. */
+    /** If true, the column cannot be edited. */
     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;
+    /** The name of the column that holds the tooltip to
+     *  show on the cells. */
+    tooltip?: string;
     /** The value that would replace a NaN value. */
     nanValue?: string;
-    /** The TimeZone identifier used if the type is Date. */
+    /** The TimeZone identifier used if the type is `date`. */
     tz?: string;
     /** The flag that allows filtering. */
     filter?: boolean;
-    /** The identifier for the aggregation function. */
+    /** The name of the aggregation function. */
     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;
     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.

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

@@ -11,7 +11,8 @@
  * 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 DeleteIcon from "@mui/icons-material/Delete";
 import FilterListIcon from "@mui/icons-material/FilterList";
@@ -82,30 +83,39 @@ const actionsByType = {
     },
 } 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) =>
-    (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) => {
     if (colId && act && val !== undefined) {
         const colType = getTypeFromDf(columns[colId].type);
-        if (!val && (colType == "date" || colType == "number" || colType == "boolean")) {
+        if (!val && (colType === "date" || colType === "number" || colType === "boolean")) {
             return;
         }
         try {
-            const typedVal =
-                colType == "number"
-                    ? parseFloat(val)
-                    : colType == "boolean"
-                    ? val == "1"
-                    : colType == "date"
-                    ? getDateTime(val)
-                    : val;
             return {
                 col: columns[colId].dfid,
                 action: act,
-                value: typedVal,
+                value:
+                    colType === "number"
+                        ? parseFloat(val)
+                        : colType === "boolean"
+                        ? val === "1"
+                        : colType === "date"
+                        ? getDateTime(val)
+                        : val,
             } as FilterDesc;
         } catch (e) {
             console.info("could not parse value ", val, e);
@@ -143,6 +153,13 @@ const FilterRow = (props: FilterRowProps) => {
         },
         [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(
         (e: SelectChangeEvent<string>) => {
             setVal(e.target.value);
@@ -190,6 +207,7 @@ const FilterRow = (props: FilterRowProps) => {
 
     const colType = getTypeFromDf(colId in columns ? columns[colId].type : "");
     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 (
         <Grid container item xs={12} alignItems="center">
@@ -223,7 +241,7 @@ const FilterRow = (props: FilterRowProps) => {
                 {colType == "number" ? (
                     <TextField
                         type="number"
-                        value={typeof val == "number" ? val : val || ""}
+                        value={typeof val === "number" ? val : val || ""}
                         onChange={onValueChange}
                         label="Number"
                         margin="dense"
@@ -247,6 +265,23 @@ const FilterRow = (props: FilterRowProps) => {
                         format={colFormat}
                         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
                         value={val || ""}
@@ -285,7 +320,7 @@ const TableFilter = (props: TableFilterProps) => {
     const filterRef = useRef<HTMLButtonElement | null>(null);
     const [filters, setFilters] = useState<Array<FilterDesc>>([]);
 
-    const colsOrder = useMemo(()=> {
+    const colsOrder = useMemo(() => {
         if (props.colsOrder) {
             return props.colsOrder;
         }
@@ -338,18 +373,7 @@ const TableFilter = (props: TableFilterProps) => {
                     sx={iconInRowSx}
                     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" />
                     </Badge>
                 </IconButton>

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

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

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

@@ -99,7 +99,7 @@ interface ScenarioSelectorProps {
     showPins?: boolean;
     showDialog?: boolean;
     multiple?: boolean;
-    filterBy?: string;
+    filter?: string;
     updateScVars?: string;
 }
 
@@ -449,17 +449,17 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
 
     const colFilters = useMemo(() => {
         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)
-                ? 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;
                   }, {} as Record<string, ColumnDesc>)
                 : undefined;
         } catch (e) {
             return undefined;
         }
-    }, [props.filterBy]);
+    }, [props.filter]);
     const [filters, setFilters] = useState<FilterDesc[]>([]);
 
     const applyFilters = useCallback(

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

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

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

@@ -24,12 +24,12 @@ export type ScenarioFull = [
     string,     // label
     string[],   // tags
     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)
     string[],   // authorized_tags
     boolean,    // deletable
     boolean,    // promotable
-    boolean,    // submittable
+    string,     // notSubmittableReason
     boolean,    // readable
     boolean     // editable
 ];
@@ -216,7 +216,6 @@ export const selectSx = { m: 1, width: 300 };
 
 export const DeleteIconSx = { height: 50, width: 50, p: 0 };
 
-
 export const EmptyArray = [];
 
 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_drill,
     _is_boolean,
-    _is_boolean_true,
+    _is_true,
     _MapDict,
     _to_camel_case,
 )
@@ -206,7 +206,7 @@ class _Builder:
 
     def __get_boolean_attribute(self, name: str, default_value=False):
         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):
         """
@@ -481,7 +481,7 @@ class _Builder:
     def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]):
         rebuild = self.__attributes.get("rebuild", False)
         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))
             rebuild_name = f"bool({self.__gui._get_real_var_name(rebuild_hash)[0]})" if rebuild_hash else "None"
             try:
@@ -825,7 +825,7 @@ class _Builder:
 
     def _set_labels(self, var_name: str = "labels"):
         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)
             elif isinstance(value, (dict, _MapDict)):
                 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 ._runtime_manager import _RuntimeManager
 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 .datatype import _get_data_type
 from .date import _date_to_string, _string_to_date

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

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

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

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

+ 16 - 17
taipy/gui_core/_GuiCoreLib.py

@@ -12,18 +12,27 @@
 import typing as t
 from datetime import datetime
 
+from taipy.core import Cycle, DataNode, Job, Scenario, Sequence, Task
 from taipy.gui import Gui
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
 
 from ..version import _get_version
 from ._adapters import (
     _GuiCoreDatanodeAdapter,
+    _GuiCoreDoNotUpdate,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioDagAdapter,
     _GuiCoreScenarioProperties,
 )
 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):
     __LIB_NAME = "taipy_gui_core"
@@ -64,7 +73,7 @@ class _GuiCore(ElementLibrary):
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
-                "filter_by": ElementProperty(_GuiCoreScenarioProperties),
+                "filter": ElementProperty(_GuiCoreScenarioProperties, _GuiCoreScenarioProperties.DEFAULT),
             },
             inner_properties={
                 "inner_scenarios": ElementProperty(
@@ -75,9 +84,7 @@ class _GuiCore(ElementLibrary):
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(
-                    PropertyType.react, f"{{{__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),
                 "scenario_edit": ElementProperty(
                     _GuiCoreScenarioAdapter,
@@ -116,9 +123,7 @@ class _GuiCore(ElementLibrary):
                 "on_submit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.submit_entity}}"),
                 "on_delete": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "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(
                     PropertyType.string,
                     f"error_id={__SCENARIO_SELECTOR_ERROR_VAR}<tp:uniq:sv>",
@@ -188,9 +193,7 @@ class _GuiCore(ElementLibrary):
             inner_properties={
                 "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_data_node}}"),
                 "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(
                     PropertyType.lov,
                     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(
                     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(
                     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(
                     PropertyType.dynamic_string,
@@ -260,9 +261,7 @@ class _GuiCore(ElementLibrary):
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "type": ElementProperty(PropertyType.inner, __JOB_ADAPTER),
                 "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(
                     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
 # specific language governing permissions and limitations under the License.
 
-import inspect
 import json
 import math
 import typing as t
+from datetime import date, datetime
 from enum import Enum
 from numbers import Number
 from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
@@ -22,10 +22,7 @@ import pandas as pd
 from taipy.core import (
     Cycle,
     DataNode,
-    Job,
     Scenario,
-    Sequence,
-    Task,
     is_deletable,
     is_editable,
     is_promotable,
@@ -33,26 +30,19 @@ from taipy.core import (
     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
 from taipy.gui._warnings import _warn
 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
-class _GCDoNotUpdate(_DoNotUpdate):
+class _GuiCoreDoNotUpdate(_DoNotUpdate):
     def __repr__(self):
         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):
     CYCLE = 0
     SCENARIO = 1
@@ -89,7 +79,7 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                             (
                                 s.get_simple_label(),
                                 [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),
                             )
                             for s in scenario.sequences.values()
@@ -102,7 +92,7 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                         list(scenario.properties.get("authorized_tags", [])) if scenario.properties else [],
                         is_deletable(scenario),
                         is_promotable(scenario),
-                        is_submittable(scenario),
+                        "" if (reason := is_submittable(scenario)) else f"Scenario not submittable: {reason.reasons}",
                         is_readable(scenario),
                         is_editable(scenario),
                     ]
@@ -241,14 +231,6 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
         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] = {
     "==": eq,
     "!=": 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:
+        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):
-            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:
         _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
     return True
 
 
+def _get_datanode_property(attr: str):
+    if (parts := attr.split(".")) and len(parts) > 1:
+        return parts[1]
+    return None
+
+
 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
     def get_hash():
         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):
         data = super().get()
+        if _is_boolean(data):
+            if _is_true(data):
+                data = _GuiCoreScenarioProperties.DEFAULT
+            else:
+                return None
         if isinstance(data, str):
             data = data.split(";")
         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(
                 [
-                    (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

+ 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.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):
@@ -243,8 +249,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
             )
         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
 
     def adapt_scenarios(self, cycle: t.List):
@@ -276,18 +282,24 @@ class _GuiCoreContext(CoreEventConsumerBase):
             filtered_list = list(adapted_list)
             for fd in filters:
                 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")
                 action = fd.get("action", "")
                 if isinstance(val, str) and col_type == "date":
                     val = datetime.fromisoformat(val[:-1])
                 # level 1 filtering
                 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
                 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
                 ]
             # remove empty cycles
@@ -515,11 +527,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
             if sequence := data.get("sequence"):
                 entity = entity.sequences.get(sequence)
 
-            if not is_submittable(entity):
+            if not (reason := is_submittable(entity)):
                 _GuiCoreContext.__assign_var(
                     state,
                     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
             if entity:

+ 1 - 1
taipy/gui_core/viselements.json

@@ -99,7 +99,7 @@
                         "doc": "TODO: If True, the user can select multiple scenarios."
                     },
                     {
-                        "name": "filter_by",
+                        "name": "filter",
                         "type": "str|list[str]",
                         "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.core import Job, JobId, Scenario, Task
+from taipy.core.common.reason import Reason
 from taipy.core.data.pickle import PickleDataNode
 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_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):
@@ -49,7 +51,7 @@ class MockState:
 class TestGuiCoreContext_is_submittable:
     def test_submit_entity(self):
         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())
             assign = Mock()
@@ -67,7 +69,7 @@ class TestGuiCoreContext_is_submittable:
             assert assign.call_args.args[0] == "error_var"
             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()
                 gui_core_context.submit_entity(
                     MockState(assign=assign),
@@ -81,4 +83,4 @@ class TestGuiCoreContext_is_submittable:
                 )
                 assign.assert_called_once()
                 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])