Finish dnd treeview, add mobile support for scrolling in dnd treeview

This commit is contained in:
Sina Blattmann 2023-03-09 15:25:57 +01:00
parent 53346740ab
commit 8899f55bd0
12 changed files with 165 additions and 49 deletions

View File

@ -30,6 +30,7 @@
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dnd-scrolling": "^1.3.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-intl": "^6.2.10", "react-intl": "^6.2.10",
"react-router-dom": "^6.8.0", "react-router-dom": "^6.8.0",
@ -6551,6 +6552,14 @@
"wrap-ansi": "^7.0.0" "wrap-ansi": "^7.0.0"
} }
}, },
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
@ -7435,6 +7444,17 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"dependencies": {
"clone": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-lazy-prop": { "node_modules/define-lazy-prop": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@ -12913,6 +12933,11 @@
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="
}, },
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
},
"node_modules/lodash.uniq": { "node_modules/lodash.uniq": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@ -15490,6 +15515,23 @@
"dnd-core": "^16.0.1" "dnd-core": "^16.0.1"
} }
}, },
"node_modules/react-dnd-scrolling": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/react-dnd-scrolling/-/react-dnd-scrolling-1.3.3.tgz",
"integrity": "sha512-IEUbvK7fPWzOpPXaMvYQJR+ZGDPUtOwNpAJeEqUdTqQb0quwaytr/LfiAFGbWI87D55InsGaTUDyWkfXURyLNA==",
"dependencies": {
"defaults": "^1.0.4",
"hoist-non-react-statics": "3.x",
"lodash.throttle": "^4.1.1",
"prop-types": "15.x",
"raf": "^3.4.1"
},
"peerDependencies": {
"react": "16.x || 17.x || 18.x",
"react-dnd": "10.x || 11.x || 14.x || 15.x || 16.x",
"react-dom": "16.x || 17.x || 18.x"
}
},
"node_modules/react-dnd-touch-backend": { "node_modules/react-dnd-touch-backend": {
"version": "16.0.1", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz",
@ -23257,6 +23299,11 @@
"wrap-ansi": "^7.0.0" "wrap-ansi": "^7.0.0"
} }
}, },
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="
},
"clsx": { "clsx": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
@ -23894,6 +23941,14 @@
"execa": "^5.0.0" "execa": "^5.0.0"
} }
}, },
"defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"requires": {
"clone": "^1.0.2"
}
},
"define-lazy-prop": { "define-lazy-prop": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@ -27886,6 +27941,11 @@
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="
}, },
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
},
"lodash.uniq": { "lodash.uniq": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@ -29561,6 +29621,18 @@
"dnd-core": "^16.0.1" "dnd-core": "^16.0.1"
} }
}, },
"react-dnd-scrolling": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/react-dnd-scrolling/-/react-dnd-scrolling-1.3.3.tgz",
"integrity": "sha512-IEUbvK7fPWzOpPXaMvYQJR+ZGDPUtOwNpAJeEqUdTqQb0quwaytr/LfiAFGbWI87D55InsGaTUDyWkfXURyLNA==",
"requires": {
"defaults": "^1.0.4",
"hoist-non-react-statics": "3.x",
"lodash.throttle": "^4.1.1",
"prop-types": "15.x",
"raf": "^3.4.1"
}
},
"react-dnd-touch-backend": { "react-dnd-touch-backend": {
"version": "16.0.1", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz",

View File

@ -25,6 +25,7 @@
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dnd-scrolling": "^1.3.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-intl": "^6.2.10", "react-intl": "^6.2.10",
"react-router-dom": "^6.8.0", "react-router-dom": "^6.8.0",

View File

@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Alert, Button, CircularProgress, Grid } from "@mui/material"; import { Alert, Button, CircularProgress, Grid } from "@mui/material";
import Container from "@mui/material/Container"; import Container from "@mui/material/Container";
import { axiosConfigWithoutToken } from "./config/axiosConfig"; import axiosConfig, { axiosConfigWithoutToken } from "./config/axiosConfig";
import InnovenergyTextfield from "./components/Layout/InnovenergyTextfield"; import InnovenergyTextfield from "./components/Layout/InnovenergyTextfield";
const loginUser = async (username: string, password: string) => { const loginUser = async (username: string, password: string) => {
@ -11,18 +11,30 @@ const loginUser = async (username: string, password: string) => {
}); });
}; };
const Login = ({ setToken }: { setToken: any }) => { const Login = ({ setToken }: { setToken: (value: string) => void }) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(); const [error, setError] = useState();
const handleSubmit = (e: any) => { const verifyToken = async () => {
axiosConfig.get("/GetAllInstallations");
};
const handleSubmit = () => {
setLoading(true); setLoading(true);
loginUser(username, password).then(({ data }) => { loginUser(username, password).then(({ data }) => {
// TODO change this if they return err codes from backend
if (typeof data === "string") { if (typeof data === "string") {
setToken(data); verifyToken()
setLoading(false); .then(() => {
setToken(data);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
} }
setError(data); setError(data);
setLoading(false); setLoading(false);

View File

@ -8,10 +8,10 @@ import InnovenergyTextfield from "../Layout/InnovenergyTextfield";
interface I_CustomerFormProps { interface I_CustomerFormProps {
values: I_Folder; values: I_Folder;
id: string | undefined; id: string;
} }
const updateFolder = (data: any) => { const updateFolder = (data: I_Folder) => {
return axiosConfig.put("/UpdateFolder", data); return axiosConfig.put("/UpdateFolder", data);
}; };
const FolderForm = (props: I_CustomerFormProps) => { const FolderForm = (props: I_CustomerFormProps) => {
@ -26,9 +26,11 @@ const FolderForm = (props: I_CustomerFormProps) => {
information: values.information, information: values.information,
}, },
onSubmit: (formikValues) => { onSubmit: (formikValues) => {
const idAsNumber = parseInt(id, 10);
updateFolder({ updateFolder({
...values,
...formikValues, ...formikValues,
id, id: idAsNumber,
}).then((res) => { }).then((res) => {
setSnackbarOpen(true); setSnackbarOpen(true);
}); });

View File

@ -6,7 +6,7 @@ import InstallationDetail from "../Installations/Installation";
import NavigationButtons from "../Layout/NavigationButtons"; import NavigationButtons from "../Layout/NavigationButtons";
import Folder from "./Folder"; import Folder from "./Folder";
import GroupTabs from "./GroupTabs"; import GroupTabs from "./GroupTabs";
import GroupTree from "./GroupTree"; import GroupTree from "./Tree/GroupTree";
const Groups = () => { const Groups = () => {
return ( return (

View File

@ -1,7 +1,7 @@
import { DragLayerMonitorProps } from "@minoru/react-dnd-treeview"; import { DragLayerMonitorProps } from "@minoru/react-dnd-treeview";
import { I_Folder, I_Installation } from "../../util/types"; import { I_Installation, I_Folder } from "../../../util/types";
import TypeIcon from "./TypeIcon";
import styles from "./DragPreview.module.scss"; import styles from "./DragPreview.module.scss";
import TypeIcon from "./TypeIcon";
interface DragPreviewProps { interface DragPreviewProps {
monitorProps: DragLayerMonitorProps<I_Installation | I_Folder>; monitorProps: DragLayerMonitorProps<I_Installation | I_Folder>;

View File

@ -18,3 +18,8 @@
.dropTarget { .dropTarget {
background-color: #e8f0fe; background-color: #e8f0fe;
} }
.treeContainer {
height: 500px;
overflow: auto;
}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import axiosConfig from "../../config/axiosConfig"; import axiosConfig from "../../../config/axiosConfig";
import { I_Folder, I_Installation } from "../../util/types"; import { I_Folder, I_Installation } from "../../../util/types";
import { CircularProgress, Grid } from "@mui/material"; import { CircularProgress, Grid } from "@mui/material";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { import {
@ -12,6 +12,7 @@ import {
} from "@minoru/react-dnd-treeview"; } from "@minoru/react-dnd-treeview";
import TreeNode from "./TreeNode"; import TreeNode from "./TreeNode";
import styles from "./GroupTree.module.scss"; import styles from "./GroupTree.module.scss";
import withScrolling from "react-dnd-scrolling";
import DragPreview from "./DragPreview"; import DragPreview from "./DragPreview";
const getTreeData = ( const getTreeData = (
@ -32,6 +33,7 @@ const getTreeData = (
const GroupTree = () => { const GroupTree = () => {
const [data, setData] = useState<(I_Folder | I_Installation)[]>(); const [data, setData] = useState<(I_Folder | I_Installation)[]>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const ScrollingComponent = withScrolling("div");
useEffect(() => { useEffect(() => {
getData(); getData();
@ -50,10 +52,15 @@ const GroupTree = () => {
{ dropTargetId, dragSource }: DropOptions<I_Folder | I_Installation> { dropTargetId, dragSource }: DropOptions<I_Folder | I_Installation>
) => { ) => {
axiosConfig axiosConfig
.put("/UpdateFolder", { .put(
...dragSource?.data, dragSource?.data?.type === "Folder"
parentId: dropTargetId, ? "/UpdateFolder"
}) : "/UpdateInstallation",
{
...dragSource?.data,
parentId: dropTargetId,
}
)
.then(() => { .then(() => {
getData(); getData();
}); });
@ -68,35 +75,38 @@ const GroupTree = () => {
} else if (data && data?.length > 1) { } else if (data && data?.length > 1) {
return ( return (
<DndProvider backend={MultiBackend} options={getBackendOptions()}> <DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree<I_Installation | I_Folder> <ScrollingComponent className={styles.treeContainer}>
tree={getTreeData(data)} <Tree<I_Installation | I_Folder>
rootId={0} tree={getTreeData(data)}
dragPreviewRender={(monitorProps) => ( rootId={0}
<DragPreview monitorProps={monitorProps} /> dragPreviewRender={(monitorProps) => (
)} <DragPreview monitorProps={monitorProps} />
classes={{ )}
container: styles.tree, classes={{
root: styles.treeRoot, container: styles.tree,
draggingSource: styles.draggingSource, root: styles.treeRoot,
dropTarget: styles.dropTarget, draggingSource: styles.draggingSource,
}} dropTarget: styles.dropTarget,
render={( }}
node: NodeModel<I_Installation | I_Folder>, render={(
{ depth, isOpen, onToggle, hasChild } node: NodeModel<I_Installation | I_Folder>,
) => ( { depth, isOpen, onToggle, hasChild, handleRef }
<TreeNode ) => (
node={node} <TreeNode
depth={depth} node={node}
isOpen={isOpen} depth={depth}
onToggle={onToggle} isOpen={isOpen}
hasChild={hasChild} onToggle={onToggle}
/> hasChild={hasChild}
)} handleRef={handleRef}
onDrop={( />
tree: NodeModel<I_Folder | I_Installation>[], )}
options: DropOptions<I_Folder | I_Installation> onDrop={(
) => handleDrop(tree, options)} tree: NodeModel<I_Folder | I_Installation>[],
/> options: DropOptions<I_Folder | I_Installation>
) => handleDrop(tree, options)}
/>
</ScrollingComponent>
</DndProvider> </DndProvider>
); );
} }

View File

@ -25,3 +25,12 @@
.labelGridItem { .labelGridItem {
padding-inline-start: 8px; padding-inline-start: 8px;
} }
.handle {
cursor: grab;
display: flex;
}
.handle > svg {
pointer-events: none;
}

View File

@ -4,9 +4,10 @@ import ArrowRightIcon from "@mui/icons-material/ArrowRight";
import { NodeModel } from "@minoru/react-dnd-treeview"; import { NodeModel } from "@minoru/react-dnd-treeview";
import styles from "./TreeNode.module.scss"; import styles from "./TreeNode.module.scss";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import routes from "../../routes.json"; import routes from "../../../routes.json";
import { I_Folder, I_Installation } from "../../util/types"; import { I_Folder, I_Installation } from "../../../util/types";
import TypeIcon from "./TypeIcon"; import TypeIcon from "./TypeIcon";
import DragHandleIcon from "@mui/icons-material/DragHandle";
type Props = { type Props = {
node: NodeModel<I_Installation | I_Folder>; node: NodeModel<I_Installation | I_Folder>;
@ -14,10 +15,11 @@ type Props = {
isOpen: boolean; isOpen: boolean;
onToggle: (id: NodeModel["id"]) => void; onToggle: (id: NodeModel["id"]) => void;
hasChild: boolean; hasChild: boolean;
handleRef: React.RefObject<any>;
}; };
const TreeNode: React.FC<Props> = (props) => { const TreeNode: React.FC<Props> = (props) => {
const { node, isOpen, hasChild, onToggle, depth } = props; const { node, isOpen, hasChild, onToggle, depth, handleRef } = props;
const indent = depth * 24; const indent = depth * 24;
const handleToggle = (e: React.MouseEvent) => { const handleToggle = (e: React.MouseEvent) => {
@ -55,6 +57,9 @@ const TreeNode: React.FC<Props> = (props) => {
<Typography variant="body2">{node.text}</Typography> <Typography variant="body2">{node.text}</Typography>
</div> </div>
</Link> </Link>
<div className={`${styles.handle} drag-handle`} ref={handleRef}>
<DragHandleIcon />
</div>
</div> </div>
); );
}; };