From 6d7fe6be2965ab44b2decee851441e619d726a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= Date: Fri, 8 Mar 2024 13:56:15 +0100 Subject: [PATCH 1/3] table cell action when value is [button label](button value) fix lov clear selection resolves #945 resolves #947 --- .../src/components/Taipy/AutoLoadingTable.tsx | 10 +- .../src/components/Taipy/PaginatedTable.tsx | 4 +- .../src/components/Taipy/tableUtils.tsx | 156 +++++++++++------- taipy/gui/viselements.json | 2 +- 4 files changed, 106 insertions(+), 66 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx index 1c281e652c..a0d4a42da2 100644 --- a/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx +++ b/frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx @@ -110,7 +110,7 @@ const Row = ({ onRowSelection, onRowClick, lineStyle, - nanValue, + nanValue }, }: { index: number; @@ -450,12 +450,14 @@ const AutoLoadingTable = (props: TaipyTableProps) => { ); const onRowSelection: OnRowSelection = useCallback( - (rowIndex: number, colName?: string) => + (rowIndex: number, colName?: string, value?: string) => dispatch( createSendActionNameAction(updateVarName, module, { action: onAction, index: getRowIndex(rows[rowIndex], rowIndex), col: colName === undefined ? null : colName, + value, + reason: value === undefined ? "click": "button", user_data: userData, }) ), @@ -502,7 +504,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { onRowSelection: active && onAction ? onRowSelection : undefined, onRowClick: active && onAction ? onRowClick : undefined, lineStyle: props.lineStyle, - nanValue: props.nanValue, + nanValue: props.nanValue }), [ rows, @@ -521,7 +523,7 @@ const AutoLoadingTable = (props: TaipyTableProps) => { onRowClick, props.lineStyle, props.nanValue, - size, + size ] ); diff --git a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx index d7d87d6f38..f398e6cc32 100644 --- a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx +++ b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx @@ -393,12 +393,14 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => { ); const onRowSelection: OnRowSelection = useCallback( - (rowIndex: number, colName?: string) => + (rowIndex: number, colName?: string, value?: string) => dispatch( createSendActionNameAction(updateVarName, module, { action: onAction, index: getRowIndex(rows[rowIndex], rowIndex, startIndex), col: colName === undefined ? null : colName, + value, + reason: value === undefined ? "click": "button", user_data: userData, }) ), diff --git a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx index 9891f1b427..4b9d4ac9ae 100644 --- a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx +++ b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx @@ -39,7 +39,7 @@ import { isValid } from "date-fns"; import { FormatConfig } from "../../context/taipyReducers"; import { dateToString, getDateTime, getDateTimeString, getNumberString, getTimeZonedDate } from "../../utils/index"; import { TaipyActiveProps, TaipyMultiSelectProps, getSuffixedClassNames } from "./utils"; -import { FilterOptionsState, TextField } from "@mui/material"; +import { Button, FilterOptionsState, TextField } from "@mui/material"; /** * A column description as received by the backend. @@ -155,7 +155,7 @@ export const iconInRowSx = { fontSize: "body2.fontSize" }; export const iconsWrapperSx = { gridColumnStart: 2, display: "flex", alignItems: "center" } as CSSProperties; const cellBoxSx = { display: "grid", gridTemplateColumns: "1fr auto", alignItems: "center" } as CSSProperties; const tableFontSx = { fontSize: "body2.fontSize" }; - +const ButtonSx = { minHeight: "unset", mb: "unset", padding: "unset", lineHeight: "unset" }; export interface OnCellValidation { (value: RowValue, rowIndex: number, colName: string, userValue: string, tz?: string): void; } @@ -165,7 +165,7 @@ export interface OnRowDeletion { } export interface OnRowSelection { - (rowIndex: number, colName?: string): void; + (rowIndex: number, colName?: string, value?: string): void; } export interface OnRowClick { @@ -220,13 +220,6 @@ const isBooleanTrue = (val: RowValue) => const defaultCursor = { cursor: "default" }; const defaultCursorIcon = { ...iconInRowSx, "& .MuiSwitch-input": defaultCursor }; -const renderCellValue = (val: RowValue | boolean, col: ColumnDesc, formatConf: FormatConfig, nanValue?: string) => { - if (val !== null && val !== undefined && col.type && col.type.startsWith("bool")) { - return ; - } - return {formatValue(val as RowValue, col, formatConf, nanValue)}; -}; - const getCellProps = (col: ColumnDesc, base: Partial = {}): Partial => { switch (col.type) { case "bool": @@ -272,6 +265,8 @@ const filter = createFilterOptions(); const getOptionKey = (option: string) => (Array.isArray(option) ? option[0] : option); const getOptionLabel = (option: string) => (Array.isArray(option) ? option[1] : option); +const onCompleteClose = (evt: SyntheticEvent) => evt.stopPropagation(); + export const EditableCell = (props: EditableCellProps) => { const { onValidation, @@ -291,55 +286,75 @@ export const EditableCell = (props: EditableCellProps) => { const [deletion, setDeletion] = useState(false); const onChange = useCallback((e: ChangeEvent) => setVal(e.target.value), []); - const onCompleteChange = useCallback((e: SyntheticEvent, value: string | null) => setVal(value), []); + const onCompleteChange = useCallback((e: SyntheticEvent, value: string | null) => { + e.stopPropagation(); + setVal(value); + }, []); const onBoolChange = useCallback((e: ChangeEvent) => setVal(e.target.checked), []); const onDateChange = useCallback((date: Date | null) => setVal(date), []); const withTime = useMemo(() => !!colDesc.format && colDesc.format.toLowerCase().includes("h"), [colDesc.format]); - const onCheckClick = useCallback(() => { - let castedVal = val; - switch (colDesc.type) { - case "bool": - castedVal = isBooleanTrue(val as RowValue); - break; - case "int": - try { - castedVal = parseInt(val as string, 10); - } catch (e) { - // ignore - } - break; - case "float": - try { - castedVal = parseFloat(val as string); - } catch (e) { - // ignore - } - break; - case "datetime": - if (val === null) { - castedVal = val; - } else if (isValid(val)) { - castedVal = dateToString(getTimeZonedDate(val as Date, formatConfig.timeZone, withTime), withTime); - } else { - return; - } - break; + const button = useMemo(() => { + if (onSelection && typeof value == "string" && value.startsWith("[") && value.endsWith(")")) { + const parts = value.slice(1, -1).split("]("); + if (parts.length == 2) { + return parts as [string, string]; + } } - onValidation && - onValidation( - castedVal as RowValue, - rowIndex, - colDesc.dfid, - val as string, - colDesc.type == "datetime" ? formatConfig.timeZone : undefined - ); - setEdit((e) => !e); - }, [onValidation, val, rowIndex, colDesc.dfid, colDesc.type, formatConfig.timeZone, withTime]); + return undefined; + }, [value, onSelection]); + + const onCheckClick = useCallback( + (evt?: MouseEvent) => { + evt && evt.stopPropagation(); + let castedVal = val; + switch (colDesc.type) { + case "bool": + castedVal = isBooleanTrue(val as RowValue); + break; + case "int": + try { + castedVal = parseInt(val as string, 10); + } catch (e) { + // ignore + } + break; + case "float": + try { + castedVal = parseFloat(val as string); + } catch (e) { + // ignore + } + break; + case "datetime": + if (val === null) { + castedVal = val; + } else if (isValid(val)) { + castedVal = dateToString( + getTimeZonedDate(val as Date, formatConfig.timeZone, withTime), + withTime + ); + } else { + return; + } + break; + } + onValidation && + onValidation( + castedVal as RowValue, + rowIndex, + colDesc.dfid, + val as string, + colDesc.type == "datetime" ? formatConfig.timeZone : undefined + ); + setEdit((e) => !e); + }, + [onValidation, val, rowIndex, colDesc.dfid, colDesc.type, formatConfig.timeZone, withTime] + ); const onEditClick = useCallback( - (evt?: MouseEvent) => { + (evt?: MouseEvent) => { evt && evt.stopPropagation(); colDesc.type?.startsWith("date") ? setVal(getDateTime(value as string, formatConfig.timeZone, withTime)) @@ -363,10 +378,14 @@ export const EditableCell = (props: EditableCellProps) => { [onCheckClick, onEditClick] ); - const onDeleteCheckClick = useCallback(() => { - onDeletion && onDeletion(rowIndex); - setDeletion((d) => !d); - }, [onDeletion, rowIndex]); + const onDeleteCheckClick = useCallback( + (evt?: MouseEvent) => { + evt && evt.stopPropagation(); + onDeletion && onDeletion(rowIndex); + setDeletion((d) => !d); + }, + [onDeletion, rowIndex] + ); const onDeleteClick = useCallback( (evt?: MouseEvent) => { @@ -391,11 +410,11 @@ export const EditableCell = (props: EditableCellProps) => { ); const onSelect = useCallback( - (e: MouseEvent) => { + (e: MouseEvent) => { e.stopPropagation(); - onSelection && onSelection(rowIndex, colDesc.dfid); + onSelection && onSelection(rowIndex, colDesc.dfid, button && button[1]); }, - [onSelection, rowIndex, colDesc.dfid] + [onSelection, rowIndex, colDesc.dfid, button] ); const filterOptions = useCallback( @@ -490,6 +509,7 @@ export const EditableCell = (props: EditableCellProps) => { freeSolo={!!colDesc.freeLov} value={val as string} onChange={onCompleteChange} + onOpen={onCompleteClose} renderInput={(params) => ( { sx={tableFontSx} /> )} + disableClearable={!colDesc.freeLov} /> @@ -558,8 +579,23 @@ export const EditableCell = (props: EditableCellProps) => { ) : null ) : ( - {renderCellValue(value, colDesc, formatConfig, nanValue)} - {onValidation ? ( + {button ? ( + + ) : val !== null && val !== undefined && colDesc.type && colDesc.type.startsWith("bool") ? ( + + ) : ( + + {formatValue(val as RowValue, colDesc, formatConfig, nanValue)} + + )} + {onValidation && !button ? ( diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index 80d404c302..cacadf35a3 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1111,7 +1111,7 @@ { "name": "on_action", "type": "str", - "doc": "The name of a function that is triggered when the user selects a row.
All parameters of that function are optional:\n
    \n
  • state (State^): the state instance.
  • \n
  • var_name (str): the name of the tabular data variable.
  • \n
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • \n
    • index (int): the row index.
    • \n
    • col (str): the column name.
.", + "doc": "The name of a function that is triggered when the user selects a row.
All parameters of that function are optional:\n
    \n
  • state (State^): the state instance.
  • \n
  • var_name (str): the name of the tabular data variable.
  • \n
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • \n
    • index (int): the row index.
    • \n
    • col (str): the column name.
    • \n
    • reason (str): the origin of the action: cmick or button.
    • \n
    • value (str): the value associated with the button when reason == button and original value is [label](button value).
.", "signature": [["state", "State"], ["var_name", "str"], ["payload", "dict"]] }, { From d77a2b2385c5dc1df43032fa6e49f01f59f4d985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= Date: Fri, 8 Mar 2024 15:09:28 +0100 Subject: [PATCH 2/3] improve test --- .../components/Taipy/PaginatedTable.spec.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx index b741a6adb0..303b4fa3fa 100644 --- a/frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx +++ b/frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx @@ -122,6 +122,33 @@ const editableColumns = JSON.stringify({ Code: { dfid: "Code", type: "str", index: 3 }, }); +const buttonValue = { + "0--1-bool,int,float,Code--asc": { + data: [ + { + bool: true, + int: 856, + float: 1.5, + Code: "[Button Label](button action)", + }, + { + bool: false, + int: 823, + float: 2.5, + Code: "ZZZ", + }, + ], + rowcount: 2, + start: 0, + }, +}; +const buttonColumns = JSON.stringify({ + bool: { dfid: "bool", type: "bool", index: 0 }, + int: { dfid: "int", type: "int", index: 1 }, + float: { dfid: "float", type: "float", index: 2 }, + Code: { dfid: "Code", type: "str", index: 3 }, +}); + describe("PaginatedTable Component", () => { it("renders", async () => { const { getByText } = render(); @@ -539,6 +566,45 @@ describe("PaginatedTable Component", () => { args: [], col: "int", index: 1, + reason: "click", + value: undefined + }, + type: "SEND_ACTION_ACTION", + }); + }); + it("can click on button", async () => { + const dispatch = jest.fn(); + const state: TaipyState = INITIAL_STATE; + const { getByText, rerender } = render( + + + + ); + + rerender( + + + + ); + + dispatch.mockClear(); + const elt = getByText("Button Label"); + expect(elt.tagName).toBe("BUTTON"); + await userEvent.click(elt); + expect(dispatch).toHaveBeenCalledWith({ + name: "", + payload: { + action: "onSelect", + args: [], + col: "Code", + index: 0, + reason: "button", + value: "button action" }, type: "SEND_ACTION_ACTION", }); From 012ee854365970c19ba3ebbcecbbf35f46b7feff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= Date: Fri, 8 Mar 2024 16:35:14 +0100 Subject: [PATCH 3/3] fab's comments --- .../taipy-gui/src/components/Taipy/tableUtils.tsx | 14 +++++++------- taipy/gui/viselements.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx index 4b9d4ac9ae..af60f731fc 100644 --- a/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx +++ b/frontend/taipy-gui/src/components/Taipy/tableUtils.tsx @@ -308,30 +308,30 @@ export const EditableCell = (props: EditableCellProps) => { const onCheckClick = useCallback( (evt?: MouseEvent) => { evt && evt.stopPropagation(); - let castedVal = val; + let castVal = val; switch (colDesc.type) { case "bool": - castedVal = isBooleanTrue(val as RowValue); + castVal = isBooleanTrue(val as RowValue); break; case "int": try { - castedVal = parseInt(val as string, 10); + castVal = parseInt(val as string, 10); } catch (e) { // ignore } break; case "float": try { - castedVal = parseFloat(val as string); + castVal = parseFloat(val as string); } catch (e) { // ignore } break; case "datetime": if (val === null) { - castedVal = val; + castVal = val; } else if (isValid(val)) { - castedVal = dateToString( + castVal = dateToString( getTimeZonedDate(val as Date, formatConfig.timeZone, withTime), withTime ); @@ -342,7 +342,7 @@ export const EditableCell = (props: EditableCellProps) => { } onValidation && onValidation( - castedVal as RowValue, + castVal as RowValue, rowIndex, colDesc.dfid, val as string, diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json index cacadf35a3..4e98653ae1 100644 --- a/taipy/gui/viselements.json +++ b/taipy/gui/viselements.json @@ -1111,7 +1111,7 @@ { "name": "on_action", "type": "str", - "doc": "The name of a function that is triggered when the user selects a row.
All parameters of that function are optional:\n
    \n
  • state (State^): the state instance.
  • \n
  • var_name (str): the name of the tabular data variable.
  • \n
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • \n
    • index (int): the row index.
    • \n
    • col (str): the column name.
    • \n
    • reason (str): the origin of the action: cmick or button.
    • \n
    • value (str): the value associated with the button when reason == button and original value is [label](button value).
.", + "doc": "The name of a function that is triggered when the user selects a row.
All parameters of that function are optional:\n
    \n
  • state (State^): the state instance.
  • \n
  • var_name (str): the name of the tabular data variable.
  • \n
  • payload (dict): the details on this callback's invocation.
    This dictionary has the following keys:\n
      \n
    • action: the name of the action that triggered this callback.
    • \n
    • index (int): the row index.
    • \n
    • col (str): the column name.
    • \n
    • reason (str): the origin of the action: \"click\", or \"button\" if the cell contains a Markdown link syntax.
    • \n
    • value (str): the *link value* indicated in the cell when using a Markdown link syntax (that is, reason is set to \"button\").
.", "signature": [["state", "State"], ["var_name", "str"], ["payload", "dict"]] }, {