Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

table cell action when value is a button #948

Merged
merged 3 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
onRowSelection,
onRowClick,
lineStyle,
nanValue,
nanValue
},
}: {
index: number;
Expand Down Expand Up @@ -450,12 +450,14 @@
);

const onRowSelection: OnRowSelection = useCallback(
(rowIndex: number, colName?: string) =>
(rowIndex: number, colName?: string, value?: string) =>

Check warning on line 453 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
dispatch(
createSendActionNameAction(updateVarName, module, {
action: onAction,
index: getRowIndex(rows[rowIndex], rowIndex),
col: colName === undefined ? null : colName,
value,
reason: value === undefined ? "click": "button",

Check warning on line 460 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 460 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
user_data: userData,
})

Check warning on line 462 in frontend/taipy-gui/src/components/Taipy/AutoLoadingTable.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
),
Expand Down Expand Up @@ -502,7 +504,7 @@
onRowSelection: active && onAction ? onRowSelection : undefined,
onRowClick: active && onAction ? onRowClick : undefined,
lineStyle: props.lineStyle,
nanValue: props.nanValue,
nanValue: props.nanValue
}),
[
rows,
Expand All @@ -521,7 +523,7 @@
onRowClick,
props.lineStyle,
props.nanValue,
size,
size
]
);

Expand Down
66 changes: 66 additions & 0 deletions frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<PaginatedTable data={undefined} defaultColumns={tableColumns} />);
Expand Down Expand Up @@ -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(
<TaipyContext.Provider value={{ state, dispatch }}>
<PaginatedTable data={undefined} defaultColumns={editableColumns} showAll={true} onAction="onSelect" />
</TaipyContext.Provider>
);

rerender(
<TaipyContext.Provider value={{ state: { ...state }, dispatch }}>
<PaginatedTable
data={buttonValue as TableValueType}
defaultColumns={buttonColumns}
showAll={true}
onAction="onSelect"
/>
</TaipyContext.Provider>
);

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",
});
Expand Down
4 changes: 3 additions & 1 deletion frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
),
Expand Down
156 changes: 96 additions & 60 deletions frontend/taipy-gui/src/components/Taipy/tableUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
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.
Expand Down Expand Up @@ -155,7 +155,7 @@
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;
}
Expand All @@ -165,7 +165,7 @@
}

export interface OnRowSelection {
(rowIndex: number, colName?: string): void;
(rowIndex: number, colName?: string, value?: string): void;
}

export interface OnRowClick {
Expand Down Expand Up @@ -220,13 +220,6 @@
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 <Switch checked={val as boolean} size="small" title={val ? "True" : "False"} sx={defaultCursorIcon} />;
}
return <span style={defaultCursor}>{formatValue(val as RowValue, col, formatConf, nanValue)}</span>;
};

const getCellProps = (col: ColumnDesc, base: Partial<TableCellProps> = {}): Partial<TableCellProps> => {
switch (col.type) {
case "bool":
Expand Down Expand Up @@ -272,6 +265,8 @@
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,
Expand All @@ -291,55 +286,75 @@
const [deletion, setDeletion] = useState(false);

const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => 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<HTMLInputElement>) => 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(")")) {
FredLL-Avaiga marked this conversation as resolved.
Show resolved Hide resolved
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<HTMLElement>) => {
evt && evt.stopPropagation();
let castVal = val;
switch (colDesc.type) {
case "bool":
castVal = isBooleanTrue(val as RowValue);
break;
case "int":
try {
castVal = parseInt(val as string, 10);
} catch (e) {
// ignore
}
break;
case "float":
try {
castVal = parseFloat(val as string);
} catch (e) {
// ignore
}
break;
case "datetime":
if (val === null) {
castVal = val;

Check warning on line 332 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} else if (isValid(val)) {
castVal = dateToString(
getTimeZonedDate(val as Date, formatConfig.timeZone, withTime),
withTime
);

Check warning on line 337 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} else {
return;
}

Check warning on line 340 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 340 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 340 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 340 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 340 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
break;

Check warning on line 341 in frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
onValidation &&
onValidation(
castVal 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<HTMLElement>) => {
evt && evt.stopPropagation();
colDesc.type?.startsWith("date")
? setVal(getDateTime(value as string, formatConfig.timeZone, withTime))
Expand All @@ -363,10 +378,14 @@
[onCheckClick, onEditClick]
);

const onDeleteCheckClick = useCallback(() => {
onDeletion && onDeletion(rowIndex);
setDeletion((d) => !d);
}, [onDeletion, rowIndex]);
const onDeleteCheckClick = useCallback(
(evt?: MouseEvent<HTMLElement>) => {
evt && evt.stopPropagation();
onDeletion && onDeletion(rowIndex);
setDeletion((d) => !d);
},
[onDeletion, rowIndex]
);

const onDeleteClick = useCallback(
(evt?: MouseEvent) => {
Expand All @@ -391,11 +410,11 @@
);

const onSelect = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
(e: MouseEvent<HTMLElement>) => {
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(
Expand Down Expand Up @@ -490,6 +509,7 @@
freeSolo={!!colDesc.freeLov}
value={val as string}
onChange={onCompleteChange}
onOpen={onCompleteClose}
renderInput={(params) => (
<TextField
{...params}
Expand All @@ -501,6 +521,7 @@
sx={tableFontSx}
/>
)}
disableClearable={!colDesc.freeLov}
/>
<Box sx={iconsWrapperSx}>
<IconButton onClick={onCheckClick} size="small" sx={iconInRowSx}>
Expand Down Expand Up @@ -558,8 +579,23 @@
) : null
) : (
<Box sx={cellBoxSx} onClick={onSelect}>
{renderCellValue(value, colDesc, formatConfig, nanValue)}
{onValidation ? (
{button ? (
<Button size="small" onClick={onSelect} sx={ButtonSx}>
{formatValue(button[0] as RowValue, colDesc, formatConfig, nanValue)}
</Button>
) : val !== null && val !== undefined && colDesc.type && colDesc.type.startsWith("bool") ? (
<Switch
checked={val as boolean}
size="small"
title={val ? "True" : "False"}
sx={defaultCursorIcon}
/>
) : (
<span style={defaultCursor}>
{formatValue(val as RowValue, colDesc, formatConfig, nanValue)}
</span>
)}
{onValidation && !button ? (
<Box sx={iconsWrapperSx}>
<IconButton onClick={onEditClick} size="small" sx={iconInRowSx}>
<EditIcon fontSize="inherit" />
Expand Down
2 changes: 1 addition & 1 deletion taipy/gui/viselements.json
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,7 @@
{
"name": "on_action",
"type": "str",
"doc": "The name of a function that is triggered when the user selects a row.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the tabular data variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>index (int): the row index.</li>\n<li>col (str): the column name.</li></ul></li></ul>.",
"doc": "The name of a function that is triggered when the user selects a row.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the tabular data variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>index (int): the row index.</li>\n<li>col (str): the column name.</li>\n<li>reason (str): the origin of the action: \"click\", or \"button\" if the cell contains a Markdown link syntax.</li>\n<li>value (str): the *link value* indicated in the cell when using a Markdown link syntax (that is, <i>reason</i> is set to \"button\").</li></ul></li></ul>.",
"signature": [["state", "State"], ["var_name", "str"], ["payload", "dict"]]
},
{
Expand Down
Loading