Skip to content

Commit

Permalink
selection message (#2299)
Browse files Browse the repository at this point in the history
show select all when multiple
resolves #1834

Co-authored-by: Fred Lefévère-Laoide <[email protected]>
  • Loading branch information
FredLL-Avaiga and Fred Lefévère-Laoide authored Dec 3, 2024
1 parent 23b6901 commit 7b0f109
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 39 deletions.
38 changes: 38 additions & 0 deletions frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,44 @@ describe("Selector Component", () => {
await userEvent.click(elt);
expect(queryAllByRole("listbox")).toHaveLength(0);
});
it("renders selectionMessage if defined", async () => {
const { getByText, getByRole } = render(<Selector lov={lov} dropdown={true} selectionMessage="a selection message" />);
const butElt = getByRole("combobox");
expect(butElt).toBeInTheDocument();
await userEvent.click(butElt);
getByRole("listbox");
const elt = getByText("Item 2");
await userEvent.click(elt);
const msg = getByText("a selection message");
expect(msg).toBeInTheDocument();
});
it("renders showSelectAll in dropdown if True", async () => {
const { getByText, getByRole } = render(<Selector lov={lov} dropdown={true} multiple={true} showSelectAll={true} />);
const checkElt = getByRole("checkbox");
expect(checkElt).toBeInTheDocument();
expect(checkElt).not.toBeChecked();
const butElt = getByRole("combobox");
await userEvent.click(butElt);
getByRole("listbox");
const elt = getByText("Item 2");
await userEvent.click(elt);
expect(checkElt.parentElement).toHaveClass("MuiCheckbox-indeterminate");
await userEvent.click(checkElt);
expect(checkElt).toBeChecked();
});
it("renders showSelectAll in list if True", async () => {
const { getByText, getByRole } = render(<Selector lov={lov} multiple={true} showSelectAll={true} />);
const msgElt = getByText(/select all/i);
expect(msgElt).toBeInTheDocument();
const checkElement = msgElt.parentElement?.querySelector("input");
expect(checkElement).not.toBeNull();
expect(checkElement).not.toBeChecked();
const elt = getByText("Item 2");
await userEvent.click(elt);
expect(checkElement?.parentElement).toHaveClass("MuiCheckbox-indeterminate");
checkElement && await userEvent.click(checkElement);
expect(checkElement).toBeChecked();
});
});

describe("Selector Component with dropdown + filter", () => {
Expand Down
161 changes: 125 additions & 36 deletions frontend/taipy-gui/src/components/Taipy/Selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ const renderBoxSx = {
interface SelectorProps extends SelTreeProps {
dropdown?: boolean;
mode?: string;
defaultSelectionMessage?: string;
selectionMessage?: string;
showSelectAll?: boolean;
}

const Selector = (props: SelectorProps) => {
Expand All @@ -145,6 +148,7 @@ const Selector = (props: SelectorProps) => {
height,
valueById,
mode = "",
showSelectAll = false,
} = props;
const [searchValue, setSearchValue] = useState("");
const [selectedValue, setSelectedValue] = useState<string[]>([]);
Expand All @@ -155,6 +159,7 @@ const Selector = (props: SelectorProps) => {
const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
const active = useDynamicProperty(props.active, props.defaultActive, true);
const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
const selectionMessage = useDynamicProperty(props.selectionMessage, props.defaultSelectionMessage, undefined);

useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars, updateVarName);

Expand Down Expand Up @@ -281,6 +286,24 @@ const Selector = (props: SelectorProps) => {
[dispatch, updateVarName, propagate, updateVars, valueById, props.onChange, module]
);

const handleCheckAllChange = useCallback(
(event: SelectChangeEvent<HTMLInputElement>, checked: boolean) => {
const sel = checked ? lovList.map((elt) => elt.id) : [];
setSelectedValue(sel);
dispatch(
createSendUpdateAction(
updateVarName,
sel,
module,
props.onChange,
propagate,
valueById ? undefined : getUpdateVar(updateVars, "lov")
)
);
},
[lovList, dispatch, updateVarName, propagate, updateVars, valueById, props.onChange, module]
);

const [autoValue, setAutoValue] = useState<LovItem | LovItem[] | null>(() => (multiple ? [] : null));
const handleAutoChange = useCallback(
(e: SyntheticEvent, sel: LovItem | LovItem[] | null) => {
Expand Down Expand Up @@ -411,43 +434,72 @@ const Selector = (props: SelectorProps) => {
multiple={multiple}
value={dropdownValue}
onChange={handleChange}
input={<OutlinedInput label={props.label} />}
input={
<OutlinedInput
label={props.label}
startAdornment={
multiple && showSelectAll ? (
<Tooltip
title={
selectedValue.length == lovList.length
? "Deselect All"
: "Select All"
}
>
<Checkbox
disabled={!active}
indeterminate={
selectedValue.length > 0 &&
selectedValue.length < lovList.length
}
checked={selectedValue.length == lovList.length}
onChange={handleCheckAllChange}
></Checkbox>
</Tooltip>
) : null
}
/>
}
disabled={!active}
renderValue={(selected) => (
<Box sx={renderBoxSx}>
{lovList
.filter((it) =>
Array.isArray(selected) ? selected.includes(it.id) : selected === it.id
)
.map((item, idx) => {
if (multiple) {
const chipProps = {} as Record<string, unknown>;
if (typeof item.item === "string") {
chipProps.label = item.item;
} else {
chipProps.label = item.item.text || "";
chipProps.avatar = <Avatar src={item.item.path} />;
}
return (
<Chip
key={item.id}
{...chipProps}
onDelete={handleDelete}
data-id={item.id}
onMouseDown={doNotPropagateEvent}
disabled={!active}
/>
);
} else if (idx === 0) {
return typeof item.item === "string" ? (
item.item
) : (
<LovImage item={item.item} />
);
} else {
return null;
}
})}
{typeof selectionMessage === "string"
? selectionMessage
: lovList
.filter((it) =>
Array.isArray(selected)
? selected.includes(it.id)
: selected === it.id
)
.map((item, idx) => {
if (multiple) {
const chipProps = {} as Record<string, unknown>;
if (typeof item.item === "string") {
chipProps.label = item.item;
} else {
chipProps.label = item.item.text || "";
chipProps.avatar = <Avatar src={item.item.path} />;
}
return (
<Chip
key={item.id}
{...chipProps}
onDelete={handleDelete}
data-id={item.id}
onMouseDown={doNotPropagateEvent}
disabled={!active}
/>
);
} else if (idx === 0) {
return typeof item.item === "string" ? (
item.item
) : (
<LovImage item={item.item} />
);
} else {
return null;
}
})}
</Box>
)}
MenuProps={getMenuProps(height)}
Expand Down Expand Up @@ -479,17 +531,54 @@ const Selector = (props: SelectorProps) => {
) : null}
<Tooltip title={hover || ""}>
<Paper sx={paperSx}>
{filter && (
{filter ? (
<Box>
<OutlinedInput
margin="dense"
placeholder="Search field"
value={searchValue}
onChange={handleInput}
disabled={!active}
startAdornment={
multiple && showSelectAll ? (
<Tooltip
title={
selectedValue.length == lovList.length
? "Deselect All"
: "Select All"
}
>
<Checkbox
disabled={!active}
indeterminate={
selectedValue.length > 0 &&
selectedValue.length < lovList.length
}
checked={selectedValue.length == lovList.length}
onChange={handleCheckAllChange}
></Checkbox>
</Tooltip>
) : null
}
/>
</Box>
) : multiple && showSelectAll ? (
<Box paddingLeft={1}>
<FormControlLabel
control={
<Checkbox
disabled={!active}
indeterminate={
selectedValue.length > 0 && selectedValue.length < lovList.length
}
checked={selectedValue.length == lovList.length}
onChange={handleCheckAllChange}
></Checkbox>
}
label={selectedValue.length == lovList.length ? "Deselect All" : "Select All"}
/>
</Box>
)}
) : null}
<List sx={listSx} id={id}>
{lovList
.filter((elt) => showItem(elt, searchValue))
Expand Down
8 changes: 5 additions & 3 deletions taipy/gui/_renderers/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ class _Factory:
__LIBRARIES: t.Dict[str, t.List["ElementLibrary"]] = {}

__CONTROL_BUILDERS = {
"alert":
lambda gui, control_type, attrs: _Builder(
"alert": lambda gui, control_type, attrs: _Builder(
gui=gui,
control_type=control_type,
element_name="Alert",
Expand Down Expand Up @@ -507,6 +506,8 @@ class _Factory:
("label",),
("mode",),
("lov", PropertyType.lov),
("selection_message", PropertyType.dynamic_string),
("show_select_all", PropertyType.boolean),
]
)
._set_propagate(),
Expand Down Expand Up @@ -550,7 +551,8 @@ class _Factory:
("without_close", PropertyType.boolean, False),
("hover_text", PropertyType.dynamic_string),
]
)._set_indexed_icons(),
)
._set_indexed_icons(),
"table": lambda gui, control_type, attrs: _Builder(
gui=gui,
control_type=control_type,
Expand Down
11 changes: 11 additions & 0 deletions taipy/gui/viselements.json
Original file line number Diff line number Diff line change
Expand Up @@ -1105,12 +1105,23 @@
"default_value": "False",
"doc": "If True, the list of items is shown in a dropdown menu.<br/><br/>You cannot use the filter in that situation."
},
{
"name": "selection_message",
"type": "dynamic(str)",
"doc": "TODO the message shown in the selection area of a dropdown selector when at least one element is selected, list the selected elements if None."
},
{
"name": "multiple",
"type": "bool",
"default_value": "False",
"doc": "If True, the user can select multiple items."
},
{
"name": "show_select_all",
"type": "bool",
"default_value": "False",
"doc": "TODO If True and multiple, show a select all option"
},
{
"name": "filter",
"type": "bool",
Expand Down

0 comments on commit 7b0f109

Please sign in to comment.