From 55974839d9eb44ea4d2573dba720bb66b9dc3e13 Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Thu, 16 Mar 2023 08:39:39 +0100 Subject: [PATCH 1/2] [WIP] fix drag ;and drop tree bug --- .../Context/GroupContextProvider.tsx | 34 ++++++++++++++++++- .../src/components/Groups/FolderForm.tsx | 23 ++++++++++--- .../src/components/Groups/Tree/GroupTree.tsx | 33 ++---------------- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/typescript/Frontend/src/components/Context/GroupContextProvider.tsx b/typescript/Frontend/src/components/Context/GroupContextProvider.tsx index 8201a12ff..b34a331eb 100644 --- a/typescript/Frontend/src/components/Context/GroupContextProvider.tsx +++ b/typescript/Frontend/src/components/Context/GroupContextProvider.tsx @@ -1,3 +1,4 @@ +import { NodeModel } from "@minoru/react-dnd-treeview"; import { createContext, ReactNode, useCallback, useState } from "react"; import axiosConfig from "../../config/axiosConfig"; import { I_Folder, I_Installation } from "../../util/types"; @@ -9,6 +10,8 @@ interface GroupContextProviderProps { loading: boolean; setLoading: (value: boolean) => void; getError: boolean; + tree: NodeModel[]; + setTree: (value: NodeModel[]) => void; } export const GroupContext = createContext({ @@ -18,19 +21,39 @@ export const GroupContext = createContext({ loading: false, setLoading: () => {}, getError: false, + tree: [], + setTree: () => {}, }); +const getTreeData = ( + data: (I_Folder | I_Installation)[] +): NodeModel[] => { + return data.map((element) => { + const isFolder = element.type === "Folder"; + return { + id: isFolder ? element.id : "installation-" + element.id, + parent: element.parentId, + text: element.name, + droppable: isFolder, + data: element, + }; + }); +}; + const GroupContextProvider = ({ children }: { children: ReactNode }) => { const [data, setData] = useState<(I_Folder | I_Installation)[]>([]); const [loading, setLoading] = useState(false); const [getError, setGetError] = useState(false); + const [tree, setTree] = useState[]>([]); const fetchData = useCallback(async () => { + console.log("fetchData"); setLoading(true); return axiosConfig .get("/GetAllFoldersAndInstallations") .then((res) => { setData(res.data); + setTree(getTreeData(res.data)); setLoading(false); }) .catch((err) => { @@ -41,7 +64,16 @@ const GroupContextProvider = ({ children }: { children: ReactNode }) => { return ( {children} diff --git a/typescript/Frontend/src/components/Groups/FolderForm.tsx b/typescript/Frontend/src/components/Groups/FolderForm.tsx index 40c2bf57a..39854c716 100644 --- a/typescript/Frontend/src/components/Groups/FolderForm.tsx +++ b/typescript/Frontend/src/components/Groups/FolderForm.tsx @@ -1,12 +1,13 @@ +import { NodeModel } from "@minoru/react-dnd-treeview"; import { Button, CircularProgress, Grid } from "@mui/material"; import { useFormik } from "formik"; import { useContext, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import axiosConfig from "../../config/axiosConfig"; -import { I_Folder } from "../../util/types"; +import { I_Folder, I_Installation } from "../../util/types"; +import { GroupContext } from "../Context/GroupContextProvider"; import InnovenergySnackbar from "../InnovenergySnackbar"; import InnovenergyTextfield from "../Layout/InnovenergyTextfield"; -import { GroupContext } from "../Context/GroupContextProvider"; interface I_CustomerFormProps { values: I_Folder; @@ -16,9 +17,8 @@ interface I_CustomerFormProps { const updateFolder = (data: I_Folder) => { return axiosConfig.put("/UpdateFolder", data); }; -const FolderForm = (props: I_CustomerFormProps) => { - const { fetchData } = useContext(GroupContext); +const FolderForm = (props: I_CustomerFormProps) => { const { values, id } = props; const intl = useIntl(); @@ -26,6 +26,19 @@ const FolderForm = (props: I_CustomerFormProps) => { const [error, setError] = useState(); const [loading, setLoading] = useState(false); + const { setTree, tree } = useContext(GroupContext); + + const updateTree = (newValues: I_Folder) => { + const elementToUpdate = tree.find( + (element) => element.data?.id.toString() === id + ); + if (elementToUpdate) { + elementToUpdate.data = newValues; + elementToUpdate.text = newValues.name; + setTree([...tree]); + } + }; + const formik = useFormik({ initialValues: { name: values.name, @@ -40,9 +53,9 @@ const FolderForm = (props: I_CustomerFormProps) => { id: idAsNumber, }) .then((res) => { + updateTree({ ...values, ...formikValues, id: idAsNumber }); setSnackbarOpen(true); setLoading(false); - fetchData(); }) .catch((err) => { setSnackbarOpen(true); diff --git a/typescript/Frontend/src/components/Groups/Tree/GroupTree.tsx b/typescript/Frontend/src/components/Groups/Tree/GroupTree.tsx index bdc4c35ee..81ee11077 100644 --- a/typescript/Frontend/src/components/Groups/Tree/GroupTree.tsx +++ b/typescript/Frontend/src/components/Groups/Tree/GroupTree.tsx @@ -17,23 +17,8 @@ import DragPreview from "./DragPreview"; import InnovenergySnackbar from "../../InnovenergySnackbar"; import { GroupContext } from "../../Context/GroupContextProvider"; -const getTreeData = ( - data: (I_Folder | I_Installation)[] -): NodeModel[] => { - return data.map((element) => { - const isFolder = element.type === "Folder"; - return { - id: isFolder ? element.id : "installation-" + element.id, - parent: element.parentId, - text: element.name, - droppable: isFolder, - data: element, - }; - }); -}; - const GroupTree = () => { - const { data, fetchData, loading, setLoading, getError } = + const { data, fetchData, loading, setLoading, getError, setTree, tree } = useContext(GroupContext); const [putError, setPutError] = useState(false); const [snackbarOpen, setSnackbarOpen] = useState(false); @@ -44,18 +29,6 @@ const GroupTree = () => { fetchData(); }, [fetchData]); - /* const findParent = (element: I_Folder | I_Installation): any => { - if (data) { - const parent = data.find( - (el) => el.type === "Folder" && el.id === element.parentId - ); - if (parent) { - return findParent(parent); - } - return element.parentId; - } - }; */ - const handleDrop = ( newTree: NodeModel[], { dropTargetId, dragSource }: DropOptions @@ -72,7 +45,7 @@ const GroupTree = () => { ) .then(() => { setSnackbarOpen(true); - fetchData(); + setTree(newTree); }) .catch((err) => { setPutError(err); @@ -92,7 +65,7 @@ const GroupTree = () => { - tree={getTreeData(data)} + tree={tree} rootId={0} dragPreviewRender={(monitorProps) => ( From e136767b12e11d61191dff653b52673d2f241c2b Mon Sep 17 00:00:00 2001 From: ig Date: Thu, 16 Mar 2023 14:13:39 +0100 Subject: [PATCH 2/2] Add tree traversal functions. Skip self in Folder/User descendants --- .../Backend/DataTypes/Methods/Credentials.cs | 8 +- .../App/Backend/DataTypes/Methods/Folder.cs | 4 +- csharp/App/Backend/DataTypes/Methods/User.cs | 4 +- csharp/App/Backend/Program.cs | 2 - csharp/Lib/Utils/TreeTraversal.cs | 147 ++++++++++++++++++ csharp/Lib/Utils/Utils.cs | 129 +++------------ 6 files changed, 176 insertions(+), 118 deletions(-) create mode 100644 csharp/Lib/Utils/TreeTraversal.cs diff --git a/csharp/App/Backend/DataTypes/Methods/Credentials.cs b/csharp/App/Backend/DataTypes/Methods/Credentials.cs index 1a8e24703..2e29149b1 100644 --- a/csharp/App/Backend/DataTypes/Methods/Credentials.cs +++ b/csharp/App/Backend/DataTypes/Methods/Credentials.cs @@ -8,12 +8,14 @@ public static class CredentialsMethods { public static Session? Login(this Credentials credentials) { - if (credentials.Username.IsNullOrEmpty() || credentials.Password.IsNullOrEmpty()) + var (username, password) = credentials; + + if (username.IsNullOrEmpty() || password.IsNullOrEmpty()) return null; - var user = Db.GetUserByEmail(credentials.Username); + var user = Db.GetUserByEmail(username); - if (user is null || !user.VerifyPassword(credentials.Password)) + if (user is null || !user.VerifyPassword(password)) return null; var session = new Session(user); diff --git a/csharp/App/Backend/DataTypes/Methods/Folder.cs b/csharp/App/Backend/DataTypes/Methods/Folder.cs index 321aca9eb..f643856ff 100644 --- a/csharp/App/Backend/DataTypes/Methods/Folder.cs +++ b/csharp/App/Backend/DataTypes/Methods/Folder.cs @@ -21,7 +21,9 @@ public static class FolderMethods public static IEnumerable DescendantFolders(this Folder parent) { - return parent.Traverse(ChildFolders); + return parent + .TraverseDepthFirstPreOrder(ChildFolders) + .Skip(1); // skip self } public static Boolean IsDescendantOf(this Folder folder, Folder ancestor) diff --git a/csharp/App/Backend/DataTypes/Methods/User.cs b/csharp/App/Backend/DataTypes/Methods/User.cs index b7498ccfe..e541dcdd7 100644 --- a/csharp/App/Backend/DataTypes/Methods/User.cs +++ b/csharp/App/Backend/DataTypes/Methods/User.cs @@ -73,7 +73,9 @@ public static class UserMethods public static IEnumerable DescendantUsers(this User parent) { - return parent.Traverse(ChildUsers); + return parent + .TraverseDepthFirstPreOrder(ChildUsers) + .Skip(1); // skip self } public static Boolean IsDescendantOf(this User user, User ancestor) diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index d32326b7a..e7500ff1a 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -1,4 +1,3 @@ -using System.Reactive.Linq; using InnovEnergy.App.Backend.Database; using Microsoft.OpenApi.Models; @@ -8,7 +7,6 @@ public static class Program { public static void Main(String[] args) { - Db.CreateFakeRelations(); var builder = WebApplication.CreateBuilder(args); diff --git a/csharp/Lib/Utils/TreeTraversal.cs b/csharp/Lib/Utils/TreeTraversal.cs new file mode 100644 index 000000000..4fef44baf --- /dev/null +++ b/csharp/Lib/Utils/TreeTraversal.cs @@ -0,0 +1,147 @@ +namespace InnovEnergy.Lib.Utils; + +public static class TreeTraversal +{ + // public static IEnumerable TraverseDepthFirstPreOrder(this T root, Func> getChildren) + // { + // var stack = new Stack>(); + // + // var iterator = root.AsSingleEnumerator(); + // + // while (true) + // { + // while (iterator.MoveNext()) + // { + // yield return iterator.Current; + // + // iterator = getChildren(iterator.Current).GetEnumerator(); + // stack.Push(iterator); + // } + // + // iterator.Dispose(); + // if (stack.Count == 0) + // yield break; + // + // iterator = stack.Pop(); + // } + // + // } + + public static IEnumerable TraverseDepthFirstPreOrder(this T root, Func> getChildren) + { + return Traverse(root, + getChildren, + branchOpen: true, + branchClose: false, + leaf: true); + } + + public static IEnumerable TraverseDepthFirstPostOrder(this T root, Func> getChildren) + { + return Traverse(root, + getChildren, + branchOpen: false, + branchClose: true, + leaf: true); + } + + public static IEnumerable TraverseLeaves(this T root, Func> getChildren) + { + return Traverse(root, + getChildren, + branchOpen: false, + branchClose: true, + leaf: false); + } + + + private static IEnumerable Traverse(T root, + Func> getChildren, + Boolean branchOpen, + Boolean branchClose, + Boolean leaf) + { + // the if-checks on the constant booleans are + // almost free because of modern branch predictors + + var stack = new Stack>(); + var it = root.AsSingleEnumerator(); + it.MoveNext(); + + while (true) + { + //////// going down //////// + + while (true) + { + var cit = getChildren(it.Current).GetEnumerator(); + + if (cit.MoveNext()) // node has children, must be a branch + { + if (branchOpen) + yield return it.Current; + + stack.Push(it); + it = cit; + } + else // no children, hence a leaf + { + var node = it.Current; + + if (leaf) + yield return node; + + if (!it.MoveNext()) + break; // no more siblings: goto parent + } + } + + //////// going up //////// + + while (true) + { + it.Dispose(); + if (stack.Count == 0) yield break; // we got to the bottom of the stack, were done + + it = stack.Pop(); + + var node = it.Current; + + if (branchClose) + yield return node; // we've seen all its children: close the branch + + if (it.MoveNext()) + break; + } + } + } + + + public static IEnumerable TraverseBreadthFirst(this T node, Func> getChildren) + { + var queue = new Queue>(); + + var iterator = node.AsSingleEnumerator(); + + while(true) + { + while (iterator.MoveNext()) + { + yield return iterator.Current; + + iterator.Current + .Apply(getChildren) + .GetEnumerator() + .Apply(queue.Enqueue); + } + + iterator.Dispose(); + + if (queue.Count == 0) + yield break; + + iterator = queue.Dequeue(); + } + + } +} \ No newline at end of file diff --git a/csharp/Lib/Utils/Utils.cs b/csharp/Lib/Utils/Utils.cs index 690d6abaa..c76476a35 100644 --- a/csharp/Lib/Utils/Utils.cs +++ b/csharp/Lib/Utils/Utils.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using static System.Runtime.CompilerServices.MethodImplOptions; @@ -6,6 +7,7 @@ namespace InnovEnergy.Lib.Utils; public static class Utils { + public static Boolean IsNull([NotNullWhen(returnValue: false)] this T t) => t is null; public static IEnumerable GetEnumStrings(this T e) where T : Enum { @@ -20,7 +22,7 @@ public static class Utils [DebuggerStepThrough][MethodImpl(AggressiveInlining)] public static T ConvertTo(this IConvertible c) where T : IConvertible { - var t = typeof (T); + var t = typeof(T); var type = t.IsEnum ? Enum.GetUnderlyingType(t) @@ -50,13 +52,13 @@ public static class Utils [DebuggerStepThrough][MethodImpl(AggressiveInlining | AggressiveOptimization)] public static R Apply(this T t, Func f) => f(t); - [DebuggerStepThrough] - [MethodImpl(AggressiveInlining | AggressiveOptimization)] + + [DebuggerStepThrough][MethodImpl(AggressiveInlining | AggressiveOptimization)] public static R Apply(this (T1 p1, T2 p2) t, Func f) => f(t.p1, t.p2); - [DebuggerStepThrough] - [MethodImpl(AggressiveInlining | AggressiveOptimization)] - public static R Apply(this (T1 p1, T2 p2, T3 p3) t, Func f) => f(t.p1, t.p2, t.p3); + + [DebuggerStepThrough][MethodImpl(AggressiveInlining | AggressiveOptimization)] + public static R Apply(this (T1 p1, T2 p2, T3 p3) t, Func f) => f(t.p1, t.p2, t.p3); [DebuggerStepThrough][MethodImpl(AggressiveInlining | AggressiveOptimization)] public static R ApplyOrDefault(this T t, Func f, R @default) @@ -79,7 +81,7 @@ public static class Utils ? res : res + length; } - + public static Decimal Modulo(this Decimal dividend, Decimal divisor) { var res = dividend % divisor; @@ -89,111 +91,39 @@ public static class Utils : res + divisor; } - public static IEnumerable Traverse(this T root, Func> getChildren) - { - var stack = new Stack>(); - var it = root.AsSingleEnumerator(); - it.MoveNext(); - - while (true) - { - //////// going down //////// - - while (true) - { - var cit = getChildren(it.Current).GetEnumerator(); - - if (cit.MoveNext()) // node has children, must be a branch - { - yield return it.Current; - - stack.Push(it); - it = cit; - } - else // no children, hence a leaf - { - var node = it.Current; - - yield return node; - - if (!it.MoveNext()) - break; // no more siblings: goto parent - } - } - - //////// going up //////// - - while (true) - { - it.Dispose(); - if (stack.Count == 0) - yield break; // we got to the bottom of the stack, were done - - it = stack.Pop(); - - if (it.MoveNext()) - break; - } - } - - } public static Int32 Clamp(this Int32 value, Int32 minValue, Int32 maxValue) { var clamped = Math.Min(maxValue, value); clamped = Math.Max(minValue, clamped); - + return clamped; } - + public static Double Clamp(this Double value, Double minValue, Double maxValue) { var clamped = Math.Min(maxValue, value); clamped = Math.Max(minValue, clamped); - + return clamped; } - - + + public static Decimal Clamp(this Decimal value, Decimal minValue, Decimal maxValue) { var clamped = Math.Min(maxValue, value); clamped = Math.Max(minValue, clamped); - + return clamped; } - -#pragma warning disable 8714 - - public static async Task ApplyOrDefault(this T t, Func> f, R @default) - { - try - { - return await f(t); - } - catch - { - return @default; - } - } - - public static R ValueOrDefault(this Dictionary dict, T key, R defaultValue) + public static R ValueOrDefault(this Dictionary dict, T key, R defaultValue) where T : notnull { return dict.TryGetValue(key, out var value) - ? value - : defaultValue; + ? value + : defaultValue; } - - public static Dictionary CombineDicts(params IEnumerable>[] dicts) - { - return dicts.Flatten().ToDictionary(kv => kv.Key, kv => kv.Value); - } - -#pragma warning restore 8714 - - public static void CopyFilesRecursively(String source, String target) { CopyFilesRecursively(new DirectoryInfo(source), new DirectoryInfo(target)); @@ -210,27 +140,4 @@ public static class Utils public static String ExecutingProcessName => Process.GetCurrentProcess().ProcessName; - - public static IEnumerable> TraverseWithPath(this T root, Func> getChildren) - { - var stack = new Stack>(); - stack.Push(root.AsSingleEnumerator()); - - do - { - for (var top = stack.Peek(); top.MoveNext(); top = Push(top)) - yield return stack.Select(p => p.Current); - - var popped = stack.Pop(); - popped.Dispose(); - } - while (stack.Count > 0); - - IEnumerator Push(IEnumerator node) - { - var top = getChildren(node.Current).GetEnumerator(); - stack.Push(top); - return top; - } - } } \ No newline at end of file