From ba3505aaa206bc198006a948580975d33eb28798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= <90181748+FredLL-Avaiga@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:23:44 +0200 Subject: [PATCH] Table cell bool rendering (#1825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../src/components/Taipy/AutoLoadingTable.tsx | 8 ++- .../src/components/Taipy/PaginatedTable.tsx | 2 + .../src/components/Taipy/tableUtils.tsx | 61 +++++++++++++++--- taipy/gui/_renderers/builder.py | 64 +++++++++---------- taipy/gui/_renderers/factory.py | 1 + taipy/gui/utils/table_col_builder.py | 37 ++++++----- taipy/gui/viselements.json | 7 ++ 7 files changed, 121 insertions(+), 59 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx index c1ce917570..17f016877c 100644 --- a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx +++ b/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} /> ))} @@ -193,6 +196,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { downloadable = false, compare = false, onCompare = "", + useCheckbox = false, } = props; const [rows, setRows] = useState([]); const [compRows, setCompRows] = useState([]); @@ -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, diff --git a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx index 9a90a89e60..f3cae4885d 100644 --- a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx +++ b/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>({}); @@ -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} /> ))} diff --git a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx index b561604f50..e20279cea1 100644 --- a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx +++ b/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; comp?: RowValue; + useCheckbox?: boolean; } export const defaultColumns = {} as Record; @@ -293,6 +295,7 @@ export const EditableCell = (props: EditableCellProps) => { tooltip, tableCellProps = emptyObject, comp, + useCheckbox = false, } = props; const [val, setVal] = useState(value); const [edit, setEdit] = useState(false); @@ -306,14 +309,16 @@ export const EditableCell = (props: EditableCellProps) => { const onBoolChange = useCallback((e: ChangeEvent) => 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") ? ( + lightBool ? ( + + ) : ( + ) @@ -498,6 +524,7 @@ export const EditableCell = (props: EditableCellProps) => { slotProps={textFieldProps} inputRef={setInputFocus} sx={tableFontSx} + className={getSuffixedClassNames(tableClassName, "-date")} /> ) : ( { slotProps={textFieldProps} inputRef={setInputFocus} sx={tableFontSx} + className={getSuffixedClassNames(tableClassName, "-date")} /> )} @@ -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={ @@ -582,6 +612,7 @@ export const EditableCell = (props: EditableCellProps) => { onKeyDown={onDeleteKeyDown} inputRef={setInputFocus} sx={tableFontSx} + className={getSuffixedClassNames(tableClassName, "-delete")} endAdornment={ @@ -624,13 +655,23 @@ export const EditableCell = (props: EditableCellProps) => { ) ) : value !== null && value !== undefined && colDesc.type && colDesc.type.startsWith("bool") ? ( - + useCheckbox ? ( + + ) : ( + + ) ) : ( {formatValue(value as RowValue, colDesc, formatConfig, nanValue)} diff --git a/taipy/gui/_renderers/builder.py b/taipy/gui/_renderers/builder.py index db56b9f78d..3656bca828 100644 --- a/taipy/gui/_renderers/builder.py +++ b/taipy/gui/_renderers/builder.py @@ -154,22 +154,22 @@ def _get_variable_hash_names( # 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__ == "": - 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 @@ def get_name_indexed_property(self, name: str) -> t.Dict[str, t.Any]: 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 @@ def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, 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 @@ def __set_dynamic_string_attribute( 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 @@ def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]): 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 @@ def _set_partial(self): 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 @@ def _set_kind(self): 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 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 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 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901 ) 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]): diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py index bcaaaed3cb..2ad05c4428 100644 --- a/taipy/gui/_renderers/factory.py +++ b/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() diff --git a/taipy/gui/utils/table_col_builder.py b/taipy/gui/utils/table_col_builder.py index a3b8ae184a..4caf3ce7a0 100644 --- a/taipy/gui/utils/table_col_builder.py +++ b/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 diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index 9bd492fbd2..ca7bca260b 100644 --- a/taipy/gui/viselements.json +++ b/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." } ] }