Преглед изворни кода

selection message (#2299)

show select all when multiple
resolves #1834

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide пре 5 месеци
родитељ
комит
7b0f109cc1

+ 38 - 0
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(<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", () => {

+ 125 - 36
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<string[]>([]);
@@ -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<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) => {
@@ -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)}
@@ -479,7 +531,7 @@ const Selector = (props: SelectorProps) => {
                     ) : null}
                     <Tooltip title={hover || ""}>
                         <Paper sx={paperSx}>
-                            {filter && (
+                            {filter ? (
                                 <Box>
                                     <OutlinedInput
                                         margin="dense"
@@ -487,9 +539,46 @@ const Selector = (props: SelectorProps) => {
                                         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))

+ 5 - 3
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,

+ 11 - 0
taipy/gui/viselements.json

@@ -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",