diff --git a/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx
index 93f8fd475..bcc9cde44 100644
--- a/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx
@@ -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();
+ 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();
+ 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();
+ 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", () => {
diff --git a/frontend/taipy-gui/src/components/Taipy/Selector.tsx b/frontend/taipy-gui/src/components/Taipy/Selector.tsx
index 11cfcb1d0..741d1af25 100644
--- a/frontend/taipy-gui/src/components/Taipy/Selector.tsx
+++ b/frontend/taipy-gui/src/components/Taipy/Selector.tsx
@@ -128,6 +128,9 @@ const renderBoxSx = {
interface SelectorProps extends SelTreeProps {
dropdown?: boolean;
mode?: string;
+ defaultSelectionMessage?: string;
+ selectionMessage?: string;
+ showSelectAll?: boolean;
}
const Selector = (props: SelectorProps) => {
@@ -145,6 +148,7 @@ const Selector = (props: SelectorProps) => {
height,
valueById,
mode = "",
+ showSelectAll = false,
} = props;
const [searchValue, setSearchValue] = useState("");
const [selectedValue, setSelectedValue] = useState([]);
@@ -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);
@@ -281,6 +286,24 @@ const Selector = (props: SelectorProps) => {
[dispatch, updateVarName, propagate, updateVars, valueById, props.onChange, module]
);
+ const handleCheckAllChange = useCallback(
+ (event: SelectChangeEvent, 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(() => (multiple ? [] : null));
const handleAutoChange = useCallback(
(e: SyntheticEvent, sel: LovItem | LovItem[] | null) => {
@@ -411,43 +434,72 @@ const Selector = (props: SelectorProps) => {
multiple={multiple}
value={dropdownValue}
onChange={handleChange}
- input={}
+ input={
+
+ 0 &&
+ selectedValue.length < lovList.length
+ }
+ checked={selectedValue.length == lovList.length}
+ onChange={handleCheckAllChange}
+ >
+
+ ) : null
+ }
+ />
+ }
disabled={!active}
renderValue={(selected) => (
- {lovList
- .filter((it) =>
- Array.isArray(selected) ? selected.includes(it.id) : selected === it.id
- )
- .map((item, idx) => {
- if (multiple) {
- const chipProps = {} as Record;
- if (typeof item.item === "string") {
- chipProps.label = item.item;
- } else {
- chipProps.label = item.item.text || "";
- chipProps.avatar = ;
- }
- return (
-
- );
- } else if (idx === 0) {
- return typeof item.item === "string" ? (
- 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;
+ if (typeof item.item === "string") {
+ chipProps.label = item.item;
+ } else {
+ chipProps.label = item.item.text || "";
+ chipProps.avatar = ;
+ }
+ return (
+
+ );
+ } else if (idx === 0) {
+ return typeof item.item === "string" ? (
+ item.item
+ ) : (
+
+ );
+ } else {
+ return null;
+ }
+ })}
)}
MenuProps={getMenuProps(height)}
@@ -479,7 +531,7 @@ const Selector = (props: SelectorProps) => {
) : null}
- {filter && (
+ {filter ? (
{
value={searchValue}
onChange={handleInput}
disabled={!active}
+ startAdornment={
+ multiple && showSelectAll ? (
+
+ 0 &&
+ selectedValue.length < lovList.length
+ }
+ checked={selectedValue.length == lovList.length}
+ onChange={handleCheckAllChange}
+ >
+
+ ) : null
+ }
+ />
+
+ ) : multiple && showSelectAll ? (
+
+ 0 && selectedValue.length < lovList.length
+ }
+ checked={selectedValue.length == lovList.length}
+ onChange={handleCheckAllChange}
+ >
+ }
+ label={selectedValue.length == lovList.length ? "Deselect All" : "Select All"}
/>
- )}
+ ) : null}
{lovList
.filter((elt) => showItem(elt, searchValue))
diff --git a/taipy/gui/_renderers/factory.py b/taipy/gui/_renderers/factory.py
index 4945d5fb0..e8c84e356 100644
--- a/taipy/gui/_renderers/factory.py
+++ b/taipy/gui/_renderers/factory.py
@@ -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",
@@ -507,6 +506,8 @@ class _Factory:
("label",),
("mode",),
("lov", PropertyType.lov),
+ ("selection_message", PropertyType.dynamic_string),
+ ("show_select_all", PropertyType.boolean),
]
)
._set_propagate(),
@@ -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,
diff --git a/taipy/gui/viselements.json b/taipy/gui/viselements.json
index f87c8bb11..25adeafe9 100644
--- a/taipy/gui/viselements.json
+++ b/taipy/gui/viselements.json
@@ -1105,12 +1105,23 @@
"default_value": "False",
"doc": "If True, the list of items is shown in a dropdown menu.
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",