From 7b0f109cc1f7f49e938a71a91fe1383d67dffb38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fred=20Lef=C3=A9v=C3=A8re-Laoide?= <90181748+FredLL-Avaiga@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:53:52 +0100 Subject: [PATCH] selection message (#2299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit show select all when multiple resolves #1834 Co-authored-by: Fred Lefévère-Laoide --- .../src/components/Taipy/Selector.spec.tsx | 38 +++++ .../src/components/Taipy/Selector.tsx | 161 ++++++++++++++---- taipy/gui/_renderers/factory.py | 8 +- taipy/gui/viselements.json | 11 ++ 4 files changed, 179 insertions(+), 39 deletions(-) diff --git a/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx b/frontend/taipy-gui/src/components/Taipy/Selector.spec.tsx index 93f8fd475f..bcc9cde44d 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 11cfcb1d06..741d1af259 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 4945d5fb09..e8c84e3569 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 f87c8bb11c..25adeafe92 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",