Browse Source

Search box in scenario management elements selector control (#1310)

* search in selector
resolves #1305

* codespell and search reset

* reduce icon size

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 11 tháng trước cách đây
mục cha
commit
4d3e5d6e5b

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 

+ 180 - 29
frontend/taipy/src/CoreSelector.tsx

@@ -11,16 +11,28 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useCallback, SyntheticEvent, useState, useEffect, useMemo, ComponentType, MouseEvent } from "react";
-import { Theme, alpha } from "@mui/material";
+import React, {
+    useCallback,
+    SyntheticEvent,
+    useState,
+    useEffect,
+    useMemo,
+    ComponentType,
+    MouseEvent,
+    ChangeEvent,
+} from "react";
+import { TextField, Theme, alpha } from "@mui/material";
 import Badge, { BadgeOrigin } from "@mui/material/Badge";
-import Box from "@mui/material/Box";
 import FormControlLabel from "@mui/material/FormControlLabel";
 import Grid from "@mui/material/Grid";
 import IconButton from "@mui/material/IconButton";
 import Switch from "@mui/material/Switch";
 import Tooltip from "@mui/material/Tooltip";
-import { ChevronRight, FlagOutlined, PushPinOutlined } from "@mui/icons-material";
+import ChevronRight from "@mui/icons-material/ChevronRight";
+import FlagOutlined from "@mui/icons-material/FlagOutlined";
+import PushPinOutlined from "@mui/icons-material/PushPinOutlined";
+import SearchOffOutlined from "@mui/icons-material/SearchOffOutlined";
+import SearchOutlined from "@mui/icons-material/SearchOutlined";
 import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView";
 import { TreeItem } from "@mui/x-tree-view/TreeItem";
 
@@ -32,9 +44,12 @@ import {
     useDispatchRequestUpdateOnFirstRender,
     createRequestUpdateAction,
     useDynamicProperty,
+    ColumnDesc,
+    FilterDesc,
+    TableFilter,
 } from "taipy-gui";
 
-import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence } from "./utils/types";
+import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Sequence, Sequences } from "./utils/types";
 import {
     Cycle as CycleIcon,
     Datanode as DatanodeIcon,
@@ -48,6 +63,7 @@ import {
     EmptyArray,
     FlagSx,
     ParentItemSx,
+    getUpdateVarNames,
     iconLabelSx,
     tinyIconButtonSx,
     tinySelPinIconButtonSx,
@@ -89,6 +105,9 @@ interface CoreSelectorProps {
     editComponent?: ComponentType<EditProps>;
     showPins?: boolean;
     onSelect?: (id: string | string[]) => void;
+    updateCoreVars: string;
+    filter?: string;
+    showSearch: boolean;
 }
 
 const tinyPinIconButtonSx = (theme: Theme) => ({
@@ -102,7 +121,8 @@ const tinyPinIconButtonSx = (theme: Theme) => ({
     },
 });
 
-const switchBoxSx = { ml: 2 };
+const switchBoxSx = { ml: 2, width: (theme: Theme) => `calc(100% - ${theme.spacing(2)})` };
+const iconInRowSx = { fontSize: "body2.fontSize" };
 
 const CoreItem = (props: {
     item: Entity;
@@ -243,6 +263,33 @@ const getExpandedIds = (nodeId: string, exp?: string[], entities?: Entities) =>
     return exp || [];
 };
 
+const emptyEntity = [] as unknown as Entity;
+const filterTree = (entities: Entities, search: string, leafType: NodeType, count?: { nb: number }) => {
+    let top = false;
+    if (!count) {
+        count = { nb: 0 };
+        top = true;
+    }
+    const filtered = entities
+        .map((item) => {
+            const [, label, items, nodeType] = item;
+            if (nodeType !== leafType || label.toLowerCase().includes(search)) {
+                const newItem = [...item];
+                if (Array.isArray(items) && items.length) {
+                    newItem[2] = filterTree(items, search, leafType, count) as Scenarios | DataNodes | Sequences;
+                }
+                return newItem as Entity;
+            }
+            count.nb++;
+            return emptyEntity;
+        })
+        .filter((i) => (i as unknown[]).length !== 0);
+    if (top && count.nb == 0) {
+        return entities;
+    }
+    return filtered;
+};
+
 const CoreSelector = (props: CoreSelectorProps) => {
     const {
         id = "",
@@ -261,6 +308,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
         onChange,
         onSelect,
         coreChanged,
+        updateCoreVars,
+        showSearch,
     } = props;
 
     const [selectedItems, setSelectedItems] = useState<string[]>([]);
@@ -294,7 +343,11 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 const res = isSelected ? [...old, nodeId] : old.filter((id) => id !== nodeId);
                 const scenariosVar = getUpdateVar(updateVars, lovPropertyName);
                 const val = multiple ? res : isSelectable ? nodeId : "";
-                setTimeout(() => dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, scenariosVar)), 1);
+                setTimeout(
+                    () =>
+                        dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, scenariosVar)),
+                    1
+                );
                 onSelect && isSelectable && onSelect(val);
                 return res;
             });
@@ -304,8 +357,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
 
     useEffect(() => {
         if (value !== undefined && value !== null) {
-            setSelectedItems(Array.isArray(value) ? value : value ? [value]: []);
-            setExpandedItems((exp) => typeof value === "string" ? getExpandedIds(value, exp, props.entities) : exp);
+            setSelectedItems(Array.isArray(value) ? value : value ? [value] : []);
+            setExpandedItems((exp) => (typeof value === "string" ? getExpandedIds(value, exp, props.entities) : exp));
         } else if (defaultValue) {
             try {
                 const parsedValue = JSON.parse(defaultValue);
@@ -332,14 +385,25 @@ const CoreSelector = (props: CoreSelectorProps) => {
             setSelectedItems((old) => {
                 if (old.length) {
                     const lovVar = getUpdateVar(updateVars, lovPropertyName);
-                    setTimeout(() => dispatch(
-                        createSendUpdateAction(updateVarName, multiple ? [] : "", module, onChange, propagate, lovVar)
-                    ), 1);
+                    setTimeout(
+                        () =>
+                            dispatch(
+                                createSendUpdateAction(
+                                    updateVarName,
+                                    multiple ? [] : "",
+                                    module,
+                                    onChange,
+                                    propagate,
+                                    lovVar
+                                )
+                            ),
+                        1
+                    );
                     return [];
                 }
                 return old;
             });
-            }
+        }
     }, [entities, updateVars, lovPropertyName, updateVarName, multiple, module, onChange, propagate, dispatch]);
 
     // Refresh on broadcast
@@ -408,22 +472,109 @@ const CoreSelector = (props: CoreSelectorProps) => {
         [showPins, props.entities]
     );
 
+    // filters
+    const colFilters = useMemo(() => {
+        try {
+            const res = props.filter ? (JSON.parse(props.filter) as Array<[string, string, string[]]>) : undefined;
+            return Array.isArray(res)
+                ? res.reduce((pv, [name, coltype, lov], idx) => {
+                      pv[name] = { dfid: name, type: coltype, index: idx, filter: true, lov: lov, freeLov: !!lov };
+                      return pv;
+                  }, {} as Record<string, ColumnDesc>)
+                : undefined;
+        } catch (e) {
+            return undefined;
+        }
+    }, [props.filter]);
+    const [filters, setFilters] = useState<FilterDesc[]>([]);
+
+    const applyFilters = useCallback(
+        (filters: FilterDesc[]) => {
+            setFilters((old) => {
+                if (old.length != filters.length || JSON.stringify(old) != JSON.stringify(filters)) {
+                    const filterVar = getUpdateVar(updateCoreVars, "filter");
+                    dispatch(
+                        createRequestUpdateAction(
+                            props.id,
+                            module,
+                            getUpdateVarNames(updateVars, lovPropertyName),
+                            true,
+                            filterVar ? { [filterVar]: filters } : undefined
+                        )
+                    );
+                    return filters;
+                }
+                return old;
+            });
+        },
+        [updateVars, dispatch, props.id, updateCoreVars, lovPropertyName, module]
+    );
+
+    // Search
+    const [searchValue, setSearchValue] = useState("");
+    const onSearch = useCallback((e: ChangeEvent<HTMLInputElement>) => setSearchValue(e.currentTarget.value), []);
+    const foundEntities = useMemo(() => {
+        if (!entities || searchValue === "") {
+            return entities;
+        }
+        return filterTree(entities, searchValue.toLowerCase(), props.leafType);
+    }, [entities, searchValue, props.leafType]);
+    const [revealSearch, setRevealSearch] = useState(false);
+    const onRevealSearch = useCallback(() => {
+        setRevealSearch((r) => !r);
+        setSearchValue("");
+    }, []);
+
     return (
         <>
-            {showPins ? (
-                <Box sx={switchBoxSx}>
-                    <FormControlLabel
-                        control={
-                            <Switch
-                                onChange={onShowPinsChange}
-                                checked={hideNonPinned}
-                                disabled={!hideNonPinned && !Object.keys(pins[0]).length}
-                            />
-                        }
-                        label="Pinned only"
-                    />
-                </Box>
-            ) : null}
+            <Grid container sx={switchBoxSx} gap={1}>
+                {active && colFilters ? (
+                    <Grid item>
+                        <TableFilter
+                            columns={colFilters}
+                            appliedFilters={filters}
+                            filteredCount={0}
+                            onValidate={applyFilters}
+                        ></TableFilter>
+                    </Grid>
+                ) : null}
+                {showPins ? (
+                    <Grid item>
+                        <FormControlLabel
+                            control={
+                                <Switch
+                                    onChange={onShowPinsChange}
+                                    checked={hideNonPinned}
+                                    disabled={!hideNonPinned && !Object.keys(pins[0]).length}
+                                />
+                            }
+                            label="Pinned only"
+                        />
+                    </Grid>
+                ) : null}
+                {showSearch ? (
+                    <Grid item>
+                        <IconButton onClick={onRevealSearch} size="small" sx={iconInRowSx}>
+                            {revealSearch ? (
+                                <SearchOffOutlined fontSize="inherit" />
+                            ) : (
+                                <SearchOutlined fontSize="inherit" />
+                            )}
+                        </IconButton>
+                    </Grid>
+                ) : null}
+                {showSearch && revealSearch ? (
+                    <Grid item xs={12}>
+                        <TextField
+                            margin="dense"
+                            value={searchValue}
+                            onChange={onSearch}
+                            fullWidth
+                            label="Search"
+                        ></TextField>
+                    </Grid>
+                ) : null}
+            </Grid>
             <SimpleTreeView
                 slots={treeSlots}
                 sx={treeViewSx}
@@ -433,8 +584,8 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 expandedItems={expandedItems}
                 onItemExpansionToggle={onItemExpand}
             >
-                {entities
-                    ? entities.map((item) => (
+                {foundEntities
+                    ? foundEntities.map((item) => (
                           <CoreItem
                               key={item ? item[0] : ""}
                               item={item}

+ 2 - 0
frontend/taipy/src/DataNodeViewer.tsx

@@ -833,6 +833,8 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                                             updateVarName={scenarioUpdateVars[0]}
                                                             updateVars={`scenarios=${scenarioUpdateVars[1]}`}
                                                             onSelect={handleClose}
+                                                            updateCoreVars=""
+                                                            showSearch={false}
                                                         />
                                                     </Popover>
                                                 </>

+ 1 - 2
frontend/taipy/src/JobSelector.tsx

@@ -409,8 +409,7 @@ const JobSelectedTableRow = ({
     showCancel,
     showDelete
 }: JobSelectedTableRowProps) => {
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    const [id, jobName, _, entityId, entityName, submitId, creationDate, status] = row;
+    const [id, jobName, , entityId, entityName, submitId, creationDate, status] = row;
 
     return (
         <TableRow

+ 2 - 0
frontend/taipy/src/NodeSelector.tsx

@@ -51,6 +51,8 @@ const NodeSelector = (props: NodeSelectorProps) => {
                 lovPropertyName="datanodes"
                 showPins={showPins}
                 multiple={multiple}
+                showSearch={false}
+                updateCoreVars=""
             />
             <Box>{props.error}</Box>
         </Box>

+ 6 - 51
frontend/taipy/src/ScenarioSelector.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useEffect, useState, useCallback, useMemo } from "react";
+import React, { useEffect, useState, useCallback } from "react";
 import { Theme, Tooltip, alpha } from "@mui/material";
 
 import Box from "@mui/material/Box";
@@ -41,15 +41,11 @@ import {
     createSendActionNameAction,
     getUpdateVar,
     createSendUpdateAction,
-    TableFilter,
-    ColumnDesc,
-    FilterDesc,
     useDynamicProperty,
-    createRequestUpdateAction,
 } from "taipy-gui";
 
 import ConfirmDialog from "./utils/ConfirmDialog";
-import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx, getUpdateVarNames } from "./utils";
+import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx } from "./utils";
 import CoreSelector, { EditProps } from "./CoreSelector";
 import { Cycles, NodeType, Scenarios } from "./utils/types";
 
@@ -101,6 +97,7 @@ interface ScenarioSelectorProps {
     multiple?: boolean;
     filter?: string;
     updateScVars?: string;
+    showSearch?: boolean;
 }
 
 interface ScenarioEditDialogProps {
@@ -437,6 +434,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
         multiple = false,
         updateVars = "",
         updateScVars = "",
+        showSearch = true
     } = props;
     const [open, setOpen] = useState(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
@@ -447,43 +445,6 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     const dispatch = useDispatch();
     const module = useModule();
 
-    const colFilters = useMemo(() => {
-        try {
-            const res = props.filter ? (JSON.parse(props.filter) as Array<[string, string, string[]]>) : undefined;
-            return Array.isArray(res)
-                ? res.reduce((pv, [name, coltype, lov], idx) => {
-                      pv[name] = { dfid: name, type: coltype, index: idx, filter: true, lov: lov, freeLov: !!lov };
-                      return pv;
-                  }, {} as Record<string, ColumnDesc>)
-                : undefined;
-        } catch (e) {
-            return undefined;
-        }
-    }, [props.filter]);
-    const [filters, setFilters] = useState<FilterDesc[]>([]);
-
-    const applyFilters = useCallback(
-        (filters: FilterDesc[]) => {
-            setFilters((old) => {
-                if (old.length != filters.length || JSON.stringify(old) != JSON.stringify(filters)) {
-                    const filterVar = getUpdateVar(updateScVars, "filter");
-                    dispatch(
-                        createRequestUpdateAction(
-                            props.id,
-                            module,
-                            getUpdateVarNames(updateVars, "innerScenarios"),
-                            true,
-                            filterVar ? { [filterVar]: filters } : undefined
-                        )
-                    );
-                    return filters;
-                }
-                return old;
-            });
-        },
-        [updateVars, dispatch, props.id, updateScVars, module]
-    );
-
     const onSubmit = useCallback(
         (...values: unknown[]) => {
             dispatch(
@@ -567,14 +528,6 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     return (
         <>
             <Box sx={MainTreeBoxSx} id={props.id} className={className}>
-                {active && colFilters ? (
-                    <TableFilter
-                        columns={colFilters}
-                        appliedFilters={filters}
-                        filteredCount={0}
-                        onValidate={applyFilters}
-                    ></TableFilter>
-                ) : null}
                 <CoreSelector
                     {...props}
                     entities={props.innerScenarios}
@@ -583,6 +536,8 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                     editComponent={EditScenario}
                     showPins={showPins}
                     multiple={multiple}
+                    updateCoreVars={updateScVars}
+                    showSearch={showSearch}
                 />
                 {showAddButton ? (
                     <Button variant="outlined" onClick={onDialogOpen} fullWidth endIcon={<Add />} disabled={!active}>

+ 1 - 1
taipy/config/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 

+ 1 - 1
taipy/core/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 

+ 1 - 1
taipy/core/_repository/_sql_repository.py

@@ -32,7 +32,7 @@ class _SQLRepository(_AbstractRepository[ModelType, Entity]):
     def __init__(self, model_type: Type[ModelType], converter: Type[Converter]):
         """
         Holds common methods to be used and extended when the need for saving
-        dataclasses in a SqlLite database.
+        dataclasses in a sqlite database.
 
         Some lines have type: ignore because MyPy won't recognize some generic attributes. This
         should be revised in the future.

+ 1 - 1
taipy/gui/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 

+ 1 - 0
taipy/gui_core/_GuiCoreLib.py

@@ -74,6 +74,7 @@ class _GuiCore(ElementLibrary):
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
                 "filter": ElementProperty(_GuiCoreScenarioProperties, _GuiCoreScenarioProperties.DEFAULT),
+                "show_search": ElementProperty(PropertyType.boolean, True),
             },
             inner_properties={
                 "inner_scenarios": ElementProperty(

+ 1 - 1
taipy/rest/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 

+ 1 - 1
taipy/templates/CODE_OF_CONDUCT.md

@@ -5,7 +5,7 @@
 We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
+identity and expression, level of experience, education, socioeconomic status,
 nationality, personal appearance, race, religion, or sexual identity
 and orientation.
 

+ 1 - 1
tests/config/common/test_template_handler.py

@@ -127,7 +127,7 @@ def test_to_bool():
     with pytest.raises(InconsistentEnvVariableError):
         _TemplateHandler._to_bool("no")
     with pytest.raises(InconsistentEnvVariableError):
-        _TemplateHandler._to_bool("tru")
+        _TemplateHandler._to_bool("tru") # codespell:ignore tru
     with pytest.raises(InconsistentEnvVariableError):
         _TemplateHandler._to_bool("tru_e")