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

selector: selection message and select all #2299

Merged
merged 1 commit into from
Dec 3, 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
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),
FredLL-Avaiga marked this conversation as resolved.
Show resolved Hide resolved
("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
Loading