瀏覽代碼

Table cell bool rendering (#1825)

* Table cell bool rendering
commanded by light_bool_render
resolves #662

* change in edit mode too

* use_checkbox

* support lov for boolean

* send lov when necessary only

* useCheckbox

* protect colDesc.type

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 8 月之前
父節點
當前提交
ba3505aaa2

+ 7 - 1
frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

@@ -94,6 +94,7 @@ interface RowData {
     lineStyle?: string;
     nanValue?: string;
     compRows?: RowType[];
+    useCheckbox?: boolean;
 }
 
 const Row = ({
@@ -116,6 +117,7 @@ const Row = ({
         lineStyle,
         nanValue,
         compRows,
+        useCheckbox,
     },
 }: {
     index: number;
@@ -150,6 +152,7 @@ const Row = ({
                     tableCellProps={cellProps[cIdx]}
                     tooltip={getTooltip(rows[index], columns[col].tooltip, col)}
                     comp={compRows && compRows[index] && compRows[index][col]}
+                    useCheckbox={useCheckbox}
                 />
             ))}
         </TableRow>
@@ -193,6 +196,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
         downloadable = false,
         compare = false,
         onCompare = "",
+        useCheckbox = false,
     } = props;
     const [rows, setRows] = useState<RowType[]>([]);
     const [compRows, setCompRows] = useState<RowType[]>([]);
@@ -555,10 +559,12 @@ const AutoLoadingTable = (props: TaipyTableProps) => {
             lineStyle: props.lineStyle,
             nanValue: props.nanValue,
             compRows: compRows,
-        }),
+            useCheckbox: useCheckbox,
+        } as RowData),
         [
             rows,
             compRows,
+            useCheckbox,
             isItemLoaded,
             active,
             colsOrder,

+ 2 - 0
frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

@@ -108,6 +108,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
         downloadable = false,
         compare = false,
         onCompare = "",
+        useCheckbox = false,
     } = props;
     const pageSize = props.pageSize === undefined || props.pageSize < 1 ? 100 : Math.round(props.pageSize);
     const [value, setValue] = useState<Record<string, unknown>>({});
@@ -607,6 +608,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                                                     nanValue={columns[col].nanValue || props.nanValue}
                                                     tooltip={getTooltip(row, columns[col].tooltip, col)}
                                                     comp={compRows && compRows[index] && compRows[index][col]}
+                                                    useCheckbox={useCheckbox}
                                                 />
                                             ))}
                                         </TableRow>

+ 51 - 10
frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

@@ -136,6 +136,7 @@ export interface TaipyTableProps extends TaipyActiveProps, TaipyMultiSelectProps
     downloadable?: boolean;
     onCompare?: string;
     compare?: boolean;
+    useCheckbox?: boolean;
 }
 
 export const DownloadAction = "__Taipy__download_csv";
@@ -193,6 +194,7 @@ interface EditableCellProps {
     tooltip?: string;
     tableCellProps?: Partial<TableCellProps>;
     comp?: RowValue;
+    useCheckbox?: boolean;
 }
 
 export const defaultColumns = {} as Record<string, ColumnDesc>;
@@ -293,6 +295,7 @@ export const EditableCell = (props: EditableCellProps) => {
         tooltip,
         tableCellProps = emptyObject,
         comp,
+        useCheckbox = false,
     } = props;
     const [val, setVal] = useState<RowValue | Date>(value);
     const [edit, setEdit] = useState(false);
@@ -306,14 +309,16 @@ export const EditableCell = (props: EditableCellProps) => {
     const onBoolChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setVal(e.target.checked), []);
     const onDateChange = useCallback((date: Date | null) => setVal(date), []);
 
+    const boolVal = colDesc.type?.startsWith("bool") && val as boolean;
+
     const withTime = useMemo(() => !!colDesc.format && colDesc.format.toLowerCase().includes("h"), [colDesc.format]);
 
     const buttonImg = useMemo(() => {
         let m;
         if (typeof value == "string" && (m = imgButtonRe.exec(value)) !== null) {
             return {
-                text: !!m[1] ? m[3]: m[2],
-                value: !!m[1] ? m[2]: m[3],
+                text: !!m[1] ? m[3] : m[2],
+                value: !!m[1] ? m[2] : m[3],
                 img: !!m[1],
                 action: !!onSelection,
             };
@@ -450,6 +455,14 @@ export const EditableCell = (props: EditableCellProps) => {
         [colDesc.freeLov]
     );
 
+    const boolTitle = useMemo(() => {
+        if (!colDesc.type?.startsWith("bool") || !colDesc.lov  || colDesc.lov.length == 0) {
+            return boolVal ? "True": "False";
+        }
+        return colDesc.lov[boolVal ? 1: 0];
+    }, [colDesc.type, boolVal, colDesc.lov]);
+
+
     useEffect(() => {
         !onValidation && setEdit(false);
     }, [onValidation]);
@@ -472,14 +485,27 @@ export const EditableCell = (props: EditableCellProps) => {
                 {edit ? (
                     colDesc.type?.startsWith("bool") ? (
                         <Box sx={cellBoxSx}>
+                            lightBool ? (
+                            <input
+                                type="checkbox"
+                                checked={val as boolean}
+                                title={boolTitle}
+                                style={iconInRowSx}
+                                className={getSuffixedClassNames(tableClassName, "-bool")}
+                                ref={setInputFocus}
+                                onChange={onBoolChange}
+                            />
+                            ) : (
                             <Switch
                                 checked={val as boolean}
                                 size="small"
-                                title={val ? "True" : "False"}
+                                title={boolTitle}
                                 sx={iconInRowSx}
                                 onChange={onBoolChange}
                                 inputRef={setInputFocus}
+                                className={getSuffixedClassNames(tableClassName, "-bool")}
                             />
+                            )
                             <Box sx={iconsWrapperSx}>
                                 <IconButton onClick={onCheckClick} size="small" sx={iconInRowSx}>
                                     <CheckIcon fontSize="inherit" />
@@ -498,6 +524,7 @@ export const EditableCell = (props: EditableCellProps) => {
                                     slotProps={textFieldProps}
                                     inputRef={setInputFocus}
                                     sx={tableFontSx}
+                                    className={getSuffixedClassNames(tableClassName, "-date")}
                                 />
                             ) : (
                                 <DatePicker
@@ -506,6 +533,7 @@ export const EditableCell = (props: EditableCellProps) => {
                                     slotProps={textFieldProps}
                                     inputRef={setInputFocus}
                                     sx={tableFontSx}
+                                    className={getSuffixedClassNames(tableClassName, "-date")}
                                 />
                             )}
                             <Box sx={iconsWrapperSx}>
@@ -542,6 +570,7 @@ export const EditableCell = (props: EditableCellProps) => {
                                         margin="dense"
                                         variant="standard"
                                         sx={tableFontSx}
+                                        className={getSuffixedClassNames(tableClassName, "-input")}
                                     />
                                 )}
                                 disableClearable={!colDesc.freeLov}
@@ -563,6 +592,7 @@ export const EditableCell = (props: EditableCellProps) => {
                             inputRef={setInputFocus}
                             margin="dense"
                             sx={tableFontSx}
+                            className={getSuffixedClassNames(tableClassName, "-input")}
                             endAdornment={
                                 <Box sx={iconsWrapperSx}>
                                     <IconButton onClick={onCheckClick} size="small" sx={iconInRowSx}>
@@ -582,6 +612,7 @@ export const EditableCell = (props: EditableCellProps) => {
                             onKeyDown={onDeleteKeyDown}
                             inputRef={setInputFocus}
                             sx={tableFontSx}
+                            className={getSuffixedClassNames(tableClassName, "-delete")}
                             endAdornment={
                                 <Box sx={iconsWrapperSx}>
                                     <IconButton onClick={onDeleteCheckClick} size="small" sx={iconInRowSx}>
@@ -624,13 +655,23 @@ export const EditableCell = (props: EditableCellProps) => {
                                 </Button>
                             )
                         ) : value !== null && value !== undefined && colDesc.type && colDesc.type.startsWith("bool") ? (
-                            <Switch
-                                checked={value as boolean}
-                                size="small"
-                                title={value ? "True" : "False"}
-                                sx={defaultCursorIcon}
-                                className={getSuffixedClassNames(tableClassName, "-bool")}
-                            />
+                            useCheckbox ? (
+                                <input
+                                    type="checkbox"
+                                    checked={value as boolean}
+                                    title={boolTitle}
+                                    style={defaultCursor}
+                                    className={getSuffixedClassNames(tableClassName, "-bool")}
+                                />
+                            ) : (
+                                <Switch
+                                    checked={value as boolean}
+                                    size="small"
+                                    title={boolTitle}
+                                    sx={defaultCursorIcon}
+                                    className={getSuffixedClassNames(tableClassName, "-bool")}
+                                />
+                            )
                         ) : (
                             <span style={defaultCursor}>
                                 {formatValue(value as RowValue, colDesc, formatConfig, nanValue)}

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

@@ -154,22 +154,22 @@ class _Builder:
         # Bind potential function and expressions in self.attributes
         for k, v in attributes.items():
             val = v
-            hashname = hash_names.get(k)
-            if hashname is None:
+            hash_name = hash_names.get(k)
+            if hash_name is None:
                 if callable(v):
                     if v.__name__ == "<lambda>":
-                        hashname = f"__lambda_{id(v)}"
-                        gui._bind_var_val(hashname, v)
+                        hash_name = f"__lambda_{id(v)}"
+                        gui._bind_var_val(hash_name, v)
                     else:
-                        hashname = _get_expr_var_name(v.__name__)
+                        hash_name = _get_expr_var_name(v.__name__)
                 elif isinstance(v, str):
                     # need to unescape the double quotes that were escaped during preprocessing
-                    (val, hashname) = _Builder.__parse_attribute_value(gui, v.replace('\\"', '"'))
+                    (val, hash_name) = _Builder.__parse_attribute_value(gui, v.replace('\\"', '"'))
 
-                if val is not None or hashname:
+                if val is not None or hash_name:
                     attributes[k] = val
-                if hashname:
-                    hashes[k] = hashname
+                if hash_name:
+                    hashes[k] = hash_name
         return hashes
 
     @staticmethod
@@ -209,8 +209,8 @@ class _Builder:
         return _get_name_indexed_property(self.__attributes, name)
 
     def __get_boolean_attribute(self, name: str, default_value=False):
-        boolattr = self.__attributes.get(name, default_value)
-        return _is_true(boolattr) if isinstance(boolattr, str) else bool(boolattr)
+        bool_attr = self.__attributes.get(name, default_value)
+        return _is_true(bool_attr) if isinstance(bool_attr, str) else bool(bool_attr)
 
     def set_boolean_attribute(self, name: str, value: bool):
         """
@@ -309,12 +309,12 @@ class _Builder:
     def __set_string_attribute(
         self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
     ):
-        strattr = self.__attributes.get(name, default_value)
-        if strattr is None:
+        str_attr = self.__attributes.get(name, default_value)
+        if str_attr is None:
             if not optional:
                 _warn(f"Property {name} is required for control {self.__control_type}.")
             return self
-        return self.set_attribute(_to_camel_case(name), str(strattr))
+        return self.set_attribute(_to_camel_case(name), str(str_attr))
 
     def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None):
         date_attr = self.__attributes.get(var_name, default_value)
@@ -347,23 +347,23 @@ class _Builder:
     def __set_function_attribute(
         self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
     ):
-        strattr = self.__attributes.get(name, default_value)
-        if strattr is None:
+        str_attr = self.__attributes.get(name, default_value)
+        if str_attr is None:
             if not optional:
                 _warn(f"Property {name} is required for control {self.__control_type}.")
             return self
-        elif callable(strattr):
-            strattr = self.__hashes.get(name)
-            if strattr is None:
+        elif callable(str_attr):
+            str_attr = self.__hashes.get(name)
+            if str_attr is None:
                 return self
-        elif _is_boolean(strattr) and not _is_true(strattr):
+        elif _is_boolean(str_attr) and not _is_true(str_attr):
             return self.__set_react_attribute(_to_camel_case(name), False)
-        elif strattr:
-            strattr = str(strattr)
-            func = self.__gui._get_user_function(strattr)
-            if func == strattr:
-                _warn(f"{self.__control_type}.{name}: {strattr} is not a function.")
-        return self.set_attribute(_to_camel_case(name), strattr) if strattr else self
+        elif str_attr:
+            str_attr = str(str_attr)
+            func = self.__gui._get_user_function(str_attr)
+            if func == str_attr:
+                _warn(f"{self.__control_type}.{name}: {str_attr} is not a function.")
+        return self.set_attribute(_to_camel_case(name), str_attr) if str_attr else self
 
     def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None):
         attr = self.__attributes.get(name, default_value)
@@ -506,7 +506,7 @@ class _Builder:
                 self.__gui._set_building(True)
                 return self.__gui._evaluate_expr(
                     "{"
-                    + f'{fn_name}({rebuild}, {rebuild_name}, "{quote(json.dumps(attributes))}", "{quote(json.dumps(hashes))}", {", ".join([f"{k}={v2}" for k, v2 in {v: self.__gui._get_real_var_name(v)[0] for v in hashes.values()}.items()])})'  # noqa: E501
+                    + f'{fn_name}({rebuild}, {rebuild_name}, "{quote(json.dumps(attributes))}", "{quote(json.dumps(hashes))}", {", ".join([f"{k}={v2}" for k, v2 in {v: self.__gui._get_real_var_name(t.cast(str, v))[0] for v in hashes.values()}.items()])})'  # noqa: E501
                     + "}"
                 )
             finally:
@@ -882,7 +882,7 @@ class _Builder:
         return self
 
     def _set_propagate(self):
-        val = self.__get_boolean_attribute("propagate", self.__gui._config.config.get("propagate"))
+        val = self.__get_boolean_attribute("propagate", t.cast(bool, self.__gui._config.config.get("propagate")))
         return self if val else self.set_boolean_attribute("propagate", False)
 
     def __set_refresh_on_update(self):
@@ -918,7 +918,7 @@ class _Builder:
     def __get_typed_hash_name(self, hash_name: str, var_type: t.Optional[PropertyType]) -> str:
         if taipy_type := _get_taipy_type(var_type):
             expr = self.__gui._get_expr_from_hash(hash_name)
-            hash_name = self.__gui._evaluate_bind_holder(taipy_type, expr)
+            hash_name = self.__gui._evaluate_bind_holder(t.cast(t.Type[_TaipyBase], taipy_type), expr)
         return hash_name
 
     def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True):
@@ -1045,7 +1045,7 @@ class _Builder:
             elif var_type == PropertyType.dynamic_date:
                 self.__set_dynamic_date_attribute(attr[0], _get_tuple_val(attr, 2, None))
             elif var_type == PropertyType.data:
-                self.__set_dynamic_property_without_default(attr[0], var_type)
+                self.__set_dynamic_property_without_default(attr[0], t.cast(PropertyType, var_type))
             elif (
                 var_type == PropertyType.lov
                 or var_type == PropertyType.single_lov
@@ -1058,10 +1058,10 @@ class _Builder:
                 )
             elif var_type == PropertyType.lov_value:
                 self.__set_dynamic_property_without_default(
-                    attr[0], var_type, _get_tuple_val(attr, 2, None) == "optional"
+                    attr[0], t.cast(PropertyType, var_type), _get_tuple_val(attr, 2, None) == "optional"
                 )
             elif var_type == PropertyType.toHtmlContent:
-                self.__set_html_content(attr[0], "page", var_type)
+                self.__set_html_content(attr[0], "page", t.cast(PropertyType, var_type))
             elif isclass(var_type) and issubclass(var_type, _TaipyBase):
                 prop_name = _to_camel_case(attr[0])
                 if hash_name := self.__hashes.get(attr[0]):

+ 1 - 0
taipy/gui/_renderers/factory.py

@@ -552,6 +552,7 @@ class _Factory:
                 ("hover_text", PropertyType.dynamic_string),
                 ("size",),
                 ("downloadable", PropertyType.boolean),
+                ("use_checkbox", PropertyType.boolean),
             ]
         )
         ._set_propagate()

+ 21 - 16
taipy/gui/utils/table_col_builder.py

@@ -113,20 +113,25 @@ def _enhance_columns(  # noqa: C901
         else:
             _warn(f"{elt_name}: tooltip[{k}] is not in the list of displayed columns.")
     editable = attributes.get("editable", False)
-    if _is_boolean(editable) and _is_true(editable):
-        lovs = _get_name_indexed_property(attributes, "lov")
-        for k, v in lovs.items():  # pragma: no cover
-            if col_desc := _get_column_desc(columns, k):
-                value = v.strip().split(";") if isinstance(v, str) else v  # type: ignore[assignment]
-                if value is not None and not isinstance(value, (list, tuple)):
-                    _warn(f"{elt_name}: lov[{k}] should be a list.")
-                    value = None
-                if value is not None:
-                    new_value = list(filter(lambda i: i is not None, value))
-                    if len(new_value) < len(value):
-                        col_desc["freeLov"] = True
-                        value = new_value
-                    col_desc["lov"] = value
-            else:
-                _warn(f"{elt_name}: lov[{k}] is not in the list of displayed columns.")
+    loveable = _is_boolean(editable) and _is_true(editable)
+    loves = _get_name_indexed_property(attributes, "lov")
+    for k, v in loves.items():  # pragma: no cover
+        col_desc = _get_column_desc(columns, k)
+        if col_desc and (
+            loveable
+            or not col_desc.get("notEditable", True)
+            or t.cast(str, col_desc.get("type", "")).startswith("bool")
+        ):
+            value = v.strip().split(";") if isinstance(v, str) else v  # type: ignore[assignment]
+            if value is not None and not isinstance(value, (list, tuple)):
+                _warn(f"{elt_name}: lov[{k}] should be a list.")
+                value = None
+            if value is not None:
+                new_value = list(filter(lambda i: i is not None, value))
+                if len(new_value) < len(value):
+                    col_desc["freeLov"] = True
+                    value = new_value
+                col_desc["lov"] = value
+        elif not col_desc:
+            _warn(f"{elt_name}: lov[{k}] is not in the list of displayed columns.")
     return columns

+ 7 - 0
taipy/gui/viselements.json

@@ -945,6 +945,7 @@
                     {
                         "name": "downloadable",
                         "type": "bool",
+                        "default_value": "False",
                         "doc": "If True, a clickable icon is shown so the user can download the data as CSV."
                     },
                     {
@@ -965,6 +966,12 @@
                                 "list[str]"
                             ]
                         ]
+                    },
+                    {
+                        "name": "use_checkbox",
+                        "type": "bool",
+                        "default_value": "False",
+                        "doc": "If True, boolean values are rendered as a simple HTML checkbox."
                     }
                 ]
             }