From 67dc88be1320bd32dee08ad87ede24cb577e0860 Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Tue, 16 Apr 2024 11:49:57 +0200 Subject: [PATCH 01/15] feat: add react beautiful dnd lib --- package-lock.json | 117 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + 2 files changed, 119 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0b69c2a0..0d703e6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "next": "^14.1.4", "postcss": "8.4.38", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "server-only": "^0.0.1", "sharp": "^0.33.3", @@ -31,6 +32,7 @@ "@testing-library/react": "^14.2.2", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@types/react-beautiful-dnd": "^13.1.8", "cypress": "^13.8.0", "eslint": "^8.56.0", "eslint-config-next": "^14.2.2", @@ -2864,6 +2866,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2969,6 +2980,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-beautiful-dnd": { + "version": "13.1.8", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz", + "integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.25", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz", @@ -2977,6 +2997,17 @@ "@types/react": "*" } }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -4378,6 +4409,14 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -9001,6 +9040,11 @@ "tmpl": "1.0.5" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9932,6 +9976,11 @@ } ] }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -9943,6 +9992,24 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -9960,6 +10027,35 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -9988,6 +10084,14 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -10912,6 +11016,11 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -11294,6 +11403,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 0c69c21e..15b122cd 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "next": "^14.1.4", "postcss": "8.4.38", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "server-only": "^0.0.1", "sharp": "^0.33.3", @@ -35,6 +36,7 @@ "@testing-library/react": "^14.2.2", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@types/react-beautiful-dnd": "^13.1.8", "cypress": "^13.8.0", "eslint": "^8.56.0", "eslint-config-next": "^14.2.2", From 6c03fee150c68af3c207f6d1702d9ebd6d4f17ad Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Tue, 16 Apr 2024 11:55:49 +0200 Subject: [PATCH 02/15] feat: draggable dashboard menu --- src/app/(dashboard)/jobmonitor/layout.tsx | 9 +++ src/components/ui/DashboardDrawer.tsx | 77 +++++++++++++++-------- src/components/ui/DrawerItemGroup.tsx | 73 +++++++++++++++++++++ src/components/ui/StrictModeDroppable.tsx | 16 +++++ 4 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 src/app/(dashboard)/jobmonitor/layout.tsx create mode 100644 src/components/ui/DrawerItemGroup.tsx create mode 100644 src/components/ui/StrictModeDroppable.tsx diff --git a/src/app/(dashboard)/jobmonitor/layout.tsx b/src/app/(dashboard)/jobmonitor/layout.tsx new file mode 100644 index 00000000..c0bcec50 --- /dev/null +++ b/src/app/(dashboard)/jobmonitor/layout.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export default function JobMonitorLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index ccc641e1..8ffd50c7 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -1,5 +1,4 @@ import { usePathname } from "next/navigation"; -import NextLink from "next/link"; import { Drawer, Icon, @@ -13,19 +12,31 @@ import { import { Dashboard, FolderCopy } from "@mui/icons-material"; import MonitorIcon from "@mui/icons-material/Monitor"; import MenuBookIcon from "@mui/icons-material/MenuBook"; -import { ReactEventHandler } from "react"; +import React, { ComponentType, ReactEventHandler } from "react"; +import { DragDropContext, Droppable } from "react-beautiful-dnd"; +import DrawerItemGroup from "./DrawerItemGroup"; import { DiracLogo } from "./DiracLogo"; // Define the sections that are accessible to users. // Each section has an associated icon and path. -const userSections: Record< - string, - { icon: React.ComponentType; path: string } -> = { - Dashboard: { icon: Dashboard, path: "/" }, - "Job Monitor": { icon: MonitorIcon, path: "/jobmonitor" }, - "File Catalog": { icon: FolderCopy, path: "/filecatalog" }, -}; +let userSections: { + title: string; + items: { title: string; id: number; icon: ComponentType; path: string }[]; +}[] = [ + { + title: "Dashboard", + items: [ + { title: "Dashboard", id: 0, icon: Dashboard, path: "/" }, + { title: "Job Monitor", id: 1, icon: MonitorIcon, path: "/jobmonitor" }, + ], + }, + { + title: "Other", + items: [ + { title: "File Catalog", id: 2, icon: FolderCopy, path: "/filecatalog" }, + ], + }, +]; interface DashboardDrawerProps { variant: "permanent" | "temporary"; @@ -34,6 +45,27 @@ interface DashboardDrawerProps { handleDrawerToggle: ReactEventHandler; } +function onDragEnd(result: any) { + // Reorder the list of items in the group. + if (!result.destination) { + return; + } + const source = result.source; + const destination = result.destination; + + const sourceGroup = userSections.find( + (group) => group.title == source.droppableId, + ); + const destinationGroup = userSections.find( + (group) => group.title == destination.droppableId, + ); + + if (sourceGroup && destinationGroup) { + const [removed] = sourceGroup.items.splice(source.index, 1); + destinationGroup.items.splice(destination.index, 0, removed); + } +} + export default function DashboardDrawer(props: DashboardDrawerProps) { // Get the current URL const pathname = usePathname(); @@ -67,22 +99,15 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { {/* Map over user sections and render them as list items in the drawer. */} - - {Object.keys(userSections).map((title: string) => ( - - - - - - - - - ))} - + + + {userSections.map(({ title, items }, index: number) => ( + + + + ))} + + {/* Render a link to documentation, positioned at the bottom of the drawer. */} diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx new file mode 100644 index 00000000..14cc2bba --- /dev/null +++ b/src/components/ui/DrawerItemGroup.tsx @@ -0,0 +1,73 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Icon, + ListItemButton, + ListItemIcon, + ListItemText, +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { Draggable, Droppable } from "react-beautiful-dnd"; +import React from "react"; +import Link from "next/link"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { StrictModeDroppable } from "./StrictModeDroppable"; + +export default function DrawerItemGroup({ + title, + items, +}: { + title: string; + items: { + title: string; + id: number; + icon: React.ComponentType; + path: string; + }[]; +}) { + return ( + + {/* Accordion summary */} + }> + {title} + + {/* Accordion details */} + + {(provided, snapshot) => ( + + {items.map(({ title, id, icon, path }, index) => ( + + {(provided) => ( +
+ + + + + +
+ +
+
+
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+ ); +} diff --git a/src/components/ui/StrictModeDroppable.tsx b/src/components/ui/StrictModeDroppable.tsx new file mode 100644 index 00000000..db689f05 --- /dev/null +++ b/src/components/ui/StrictModeDroppable.tsx @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; +import { Droppable, DroppableProps } from "react-beautiful-dnd"; +export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => { + const [enabled, setEnabled] = useState(false); + useEffect(() => { + const animation = requestAnimationFrame(() => setEnabled(true)); + return () => { + cancelAnimationFrame(animation); + setEnabled(false); + }; + }, []); + if (!enabled) { + return null; + } + return {children}; +}; From e80c73f8022506acaf6520ba999b3d16726c4dcb Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Tue, 16 Apr 2024 11:56:51 +0200 Subject: [PATCH 03/15] feat: change pages structure to avoid dashboard menu refresh --- .../{ => (dashboard)}/jobmonitor/error.tsx | 0 src/app/(dashboard)/jobmonitor/layout.tsx | 2 -- src/app/{ => (dashboard)}/jobmonitor/page.tsx | 0 src/app/(dashboard)/layout.tsx | 33 +++++++++++++++++++ src/app/{ => (dashboard)}/page.tsx | 0 src/app/jobmonitor/layout.tsx | 7 ---- src/components/applications/JobMonitor.tsx | 18 +++------- src/components/applications/UserDashboard.tsx | 19 +++-------- 8 files changed, 41 insertions(+), 38 deletions(-) rename src/app/{ => (dashboard)}/jobmonitor/error.tsx (100%) rename src/app/{ => (dashboard)}/jobmonitor/page.tsx (100%) create mode 100644 src/app/(dashboard)/layout.tsx rename src/app/{ => (dashboard)}/page.tsx (100%) delete mode 100644 src/app/jobmonitor/layout.tsx diff --git a/src/app/jobmonitor/error.tsx b/src/app/(dashboard)/jobmonitor/error.tsx similarity index 100% rename from src/app/jobmonitor/error.tsx rename to src/app/(dashboard)/jobmonitor/error.tsx diff --git a/src/app/(dashboard)/jobmonitor/layout.tsx b/src/app/(dashboard)/jobmonitor/layout.tsx index c0bcec50..1f26a53c 100644 --- a/src/app/(dashboard)/jobmonitor/layout.tsx +++ b/src/app/(dashboard)/jobmonitor/layout.tsx @@ -1,5 +1,3 @@ -import React from "react"; - export default function JobMonitorLayout({ children, }: { diff --git a/src/app/jobmonitor/page.tsx b/src/app/(dashboard)/jobmonitor/page.tsx similarity index 100% rename from src/app/jobmonitor/page.tsx rename to src/app/(dashboard)/jobmonitor/page.tsx diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx new file mode 100644 index 00000000..d28307df --- /dev/null +++ b/src/app/(dashboard)/layout.tsx @@ -0,0 +1,33 @@ +"use client"; +import * as React from "react"; +import CssBaseline from "@mui/material/CssBaseline"; +import { Box } from "@mui/material"; +import { useMUITheme } from "@/hooks/theme"; +import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; +import Dashboard from "@/components/layout/Dashboard"; + +export default function JobMonitorLayout({ + children, +}: { + children: React.ReactNode; +}) { + const theme = useMUITheme(); + + return ( +
+ + + + + {children} + + + +
+ ); +} diff --git a/src/app/page.tsx b/src/app/(dashboard)/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/(dashboard)/page.tsx diff --git a/src/app/jobmonitor/layout.tsx b/src/app/jobmonitor/layout.tsx deleted file mode 100644 index 1f26a53c..00000000 --- a/src/app/jobmonitor/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function JobMonitorLayout({ - children, -}: { - children: React.ReactNode; -}) { - return
{children}
; -} diff --git a/src/components/applications/JobMonitor.tsx b/src/components/applications/JobMonitor.tsx index e4224299..09c2fa6f 100644 --- a/src/components/applications/JobMonitor.tsx +++ b/src/components/applications/JobMonitor.tsx @@ -15,19 +15,9 @@ export default function JobMonitor() { const theme = useMUITheme(); return ( - - - - -

Job Monitor

- -
-
-
+
+

Job Monitor

+ +
); } diff --git a/src/components/applications/UserDashboard.tsx b/src/components/applications/UserDashboard.tsx index 15a701a9..57b93774 100644 --- a/src/components/applications/UserDashboard.tsx +++ b/src/components/applications/UserDashboard.tsx @@ -13,7 +13,6 @@ import { useMUITheme } from "@/hooks/theme"; * @returns User Dashboard content */ export default function UserDashboard() { - const theme = useMUITheme(); const { configuration } = useOIDCContext(); const { accessTokenPayload } = useOidcAccessToken(configuration?.scope); @@ -22,20 +21,10 @@ export default function UserDashboard() { } return ( - - - - -

Hello {accessTokenPayload["preferred_username"]}

+
+

Hello {accessTokenPayload["preferred_username"]}

-

To start with, select an application in the side bar

- - - +

To start with, select an application in the side bar

+
); } From cdb036da31e17b74c5f21151ddf93504dd51608f Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Tue, 16 Apr 2024 15:20:44 +0200 Subject: [PATCH 04/15] feat: useState for Groups --- src/components/ui/DashboardDrawer.tsx | 123 ++++++++++++++++---------- src/components/ui/DrawerItemGroup.tsx | 55 ++++++++++-- 2 files changed, 123 insertions(+), 55 deletions(-) diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index 8ffd50c7..375c6343 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -12,32 +12,11 @@ import { import { Dashboard, FolderCopy } from "@mui/icons-material"; import MonitorIcon from "@mui/icons-material/Monitor"; import MenuBookIcon from "@mui/icons-material/MenuBook"; -import React, { ComponentType, ReactEventHandler } from "react"; +import React, { ComponentType, ReactEventHandler, useState } from "react"; import { DragDropContext, Droppable } from "react-beautiful-dnd"; import DrawerItemGroup from "./DrawerItemGroup"; import { DiracLogo } from "./DiracLogo"; -// Define the sections that are accessible to users. -// Each section has an associated icon and path. -let userSections: { - title: string; - items: { title: string; id: number; icon: ComponentType; path: string }[]; -}[] = [ - { - title: "Dashboard", - items: [ - { title: "Dashboard", id: 0, icon: Dashboard, path: "/" }, - { title: "Job Monitor", id: 1, icon: MonitorIcon, path: "/jobmonitor" }, - ], - }, - { - title: "Other", - items: [ - { title: "File Catalog", id: 2, icon: FolderCopy, path: "/filecatalog" }, - ], - }, -]; - interface DashboardDrawerProps { variant: "permanent" | "temporary"; mobileOpen: boolean; @@ -45,27 +24,6 @@ interface DashboardDrawerProps { handleDrawerToggle: ReactEventHandler; } -function onDragEnd(result: any) { - // Reorder the list of items in the group. - if (!result.destination) { - return; - } - const source = result.source; - const destination = result.destination; - - const sourceGroup = userSections.find( - (group) => group.title == source.droppableId, - ); - const destinationGroup = userSections.find( - (group) => group.title == destination.droppableId, - ); - - if (sourceGroup && destinationGroup) { - const [removed] = sourceGroup.items.splice(source.index, 1); - destinationGroup.items.splice(destination.index, 0, removed); - } -} - export default function DashboardDrawer(props: DashboardDrawerProps) { // Get the current URL const pathname = usePathname(); @@ -75,6 +33,79 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { // Check if the drawer is in "temporary" mode. const isTemporary = props.variant === "temporary"; + // Define the sections that are accessible to users. + // Each section has an associated icon and path. + const [userSections, setSections] = useState([ + { + title: "Dashboard", + extended: true, + items: [ + { title: "Dashboard", id: 0, icon: Dashboard, path: "/" }, + { title: "Job Monitor", id: 1, icon: MonitorIcon, path: "/jobmonitor" }, + ], + }, + { + title: "Other", + extended: false, + items: [ + { + title: "File Catalog", + id: 2, + icon: FolderCopy, + path: "/filecatalog", + }, + ], + }, + { + title: "Other2", + extended: false, + items: [], + }, + ] as { + title: string; + extended: boolean; + items: { title: string; id: number; icon: ComponentType; path: string }[]; + }[]); + + /** + * Handles the drag end event for reordering items in the group. + * + * @param result - The result object containing information about the drag event. + */ + function onDragEnd(result: any) { + // Reorder the list of items in the group. + if (!result.destination) { + return; + } + const source = result.source; + const destination = result.destination; + + const sourceGroup = userSections.find( + (group) => group.title == source.droppableId, + ); + const destinationGroup = userSections.find( + (group) => group.title == destination.droppableId, + ); + + if (sourceGroup && destinationGroup) { + const sourceItems = [...sourceGroup.items]; + const destinationItems = [...destinationGroup.items]; + + const [removed] = sourceItems.splice(source.index, 1); + destinationItems.splice(destination.index, 0, removed); + + setSections((sections) => + sections.map((section) => + section.title === sourceGroup.title + ? { ...section, items: sourceItems } + : section.title === destinationGroup.title + ? { ...section, items: destinationItems } + : section, + ), + ); + } + } + return ( - {userSections.map(({ title, items }, index: number) => ( - - + {userSections.map((group) => ( + + ))} diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx index 14cc2bba..74eb4368 100644 --- a/src/components/ui/DrawerItemGroup.tsx +++ b/src/components/ui/DrawerItemGroup.tsx @@ -15,19 +15,56 @@ import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; import { StrictModeDroppable } from "./StrictModeDroppable"; export default function DrawerItemGroup({ - title, - items, + group: { title, extended: expanded, items }, + setSections, }: { - title: string; - items: { + group: { title: string; - id: number; - icon: React.ComponentType; - path: string; - }[]; + extended: boolean; + items: { + title: string; + id: number; + icon: React.ComponentType; + path: string; + }[]; + }; + setSections: React.Dispatch< + React.SetStateAction< + { + title: string; + extended: boolean; + items: { + title: string; + id: number; + icon: React.ComponentType; + path: string; + }[]; + }[] + > + >; }) { + const handleChange = (title: string) => (event: any, isExpanded: any) => { + // Set the extended state of the accordion group. + setSections((sections) => + sections.map((section) => + section.title === title + ? { ...section, extended: isExpanded } + : section, + ), + ); + }; + return ( - + {/* Accordion summary */} }> {title} From c0b8886dedd88839cd805e040ce1f9065117b6cd Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Wed, 17 Apr 2024 13:39:54 +0200 Subject: [PATCH 05/15] feat: Migrating to pragmatic dnd - removing react beautiful dnd --- package-lock.json | 112 ---------------------- package.json | 2 - src/components/ui/DashboardDrawer.tsx | 17 ++-- src/components/ui/DrawerItemGroup.tsx | 54 ++++------- src/components/ui/StrictModeDroppable.tsx | 16 ---- 5 files changed, 25 insertions(+), 176 deletions(-) delete mode 100644 src/components/ui/StrictModeDroppable.tsx diff --git a/package-lock.json b/package-lock.json index 0d703e6e..42104690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "next": "^14.1.4", "postcss": "8.4.38", "react": "^18.2.0", - "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "server-only": "^0.0.1", "sharp": "^0.33.3", @@ -32,7 +31,6 @@ "@testing-library/react": "^14.2.2", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", - "@types/react-beautiful-dnd": "^13.1.8", "cypress": "^13.8.0", "eslint": "^8.56.0", "eslint-config-next": "^14.2.2", @@ -2866,15 +2864,6 @@ "@types/node": "*" } }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2980,15 +2969,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-beautiful-dnd": { - "version": "13.1.8", - "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz", - "integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-dom": { "version": "18.2.25", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz", @@ -2997,17 +2977,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-redux": { - "version": "7.1.33", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", - "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -4409,14 +4378,6 @@ "node": ">= 8" } }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -9040,11 +9001,6 @@ "tmpl": "1.0.5" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9992,24 +9948,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -10027,35 +9965,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, - "node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/react-redux/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -10084,14 +9993,6 @@ "node": ">=8" } }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -11016,11 +10917,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -11403,14 +11299,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-memo-one": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", - "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index 15b122cd..0c69c21e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "next": "^14.1.4", "postcss": "8.4.38", "react": "^18.2.0", - "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "server-only": "^0.0.1", "sharp": "^0.33.3", @@ -36,7 +35,6 @@ "@testing-library/react": "^14.2.2", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", - "@types/react-beautiful-dnd": "^13.1.8", "cypress": "^13.8.0", "eslint": "^8.56.0", "eslint-config-next": "^14.2.2", diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index 375c6343..f7b59967 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -13,7 +13,6 @@ import { Dashboard, FolderCopy } from "@mui/icons-material"; import MonitorIcon from "@mui/icons-material/Monitor"; import MenuBookIcon from "@mui/icons-material/MenuBook"; import React, { ComponentType, ReactEventHandler, useState } from "react"; -import { DragDropContext, Droppable } from "react-beautiful-dnd"; import DrawerItemGroup from "./DrawerItemGroup"; import { DiracLogo } from "./DiracLogo"; @@ -130,15 +129,13 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { {/* Map over user sections and render them as list items in the drawer. */} - - - {userSections.map((group) => ( - - - - ))} - - + + {userSections.map((group) => ( + + + + ))} + {/* Render a link to documentation, positioned at the bottom of the drawer. */} diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx index 74eb4368..83c0bfad 100644 --- a/src/components/ui/DrawerItemGroup.tsx +++ b/src/components/ui/DrawerItemGroup.tsx @@ -8,11 +8,9 @@ import { ListItemText, } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { Draggable, Droppable } from "react-beautiful-dnd"; import React from "react"; import Link from "next/link"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; -import { StrictModeDroppable } from "./StrictModeDroppable"; export default function DrawerItemGroup({ group: { title, extended: expanded, items }, @@ -70,41 +68,25 @@ export default function DrawerItemGroup({ {title} {/* Accordion details */} - - {(provided, snapshot) => ( - + {items.map(({ title, id, icon, path }, index) => ( + - {items.map(({ title, id, icon, path }, index) => ( - - {(provided) => ( -
- - - - - -
- -
-
-
- )} -
- ))} - {provided.placeholder} -
- )} -
+ + + + +
+ +
+ + ))} +
); } diff --git a/src/components/ui/StrictModeDroppable.tsx b/src/components/ui/StrictModeDroppable.tsx deleted file mode 100644 index db689f05..00000000 --- a/src/components/ui/StrictModeDroppable.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useEffect, useState } from "react"; -import { Droppable, DroppableProps } from "react-beautiful-dnd"; -export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => { - const [enabled, setEnabled] = useState(false); - useEffect(() => { - const animation = requestAnimationFrame(() => setEnabled(true)); - return () => { - cancelAnimationFrame(animation); - setEnabled(false); - }; - }, []); - if (!enabled) { - return null; - } - return {children}; -}; From 519c7c58a871c8314d179711dfceffd710abe131 Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Thu, 18 Apr 2024 14:28:00 +0200 Subject: [PATCH 06/15] feat: Migrating to pragmatic dnd - implementation --- package-lock.json | 99 +++++++++++++---- package.json | 3 + src/app/{ => (dashboard)}/error.tsx | 0 src/components/ui/DashboardDrawer.tsx | 148 ++++++++++++++++++++------ src/components/ui/DrawerItem.tsx | 117 ++++++++++++++++++++ src/components/ui/DrawerItemGroup.tsx | 58 +++++----- 6 files changed, 345 insertions(+), 80 deletions(-) rename src/app/{ => (dashboard)}/error.tsx (100%) create mode 100644 src/components/ui/DrawerItem.tsx diff --git a/package-lock.json b/package-lock.json index 42104690..d08f70fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.1.3", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", + "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^1.1.0", "@axa-fr/react-oidc": "^7.21.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", @@ -74,6 +77,76 @@ "node": ">=6.0.0" } }, + "node_modules/@atlaskit/platform-feature-flags": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@atlaskit/platform-feature-flags/-/platform-feature-flags-0.2.5.tgz", + "integrity": "sha512-0fD2aDxn2mE59D4acUhVib+YF2HDYuuPH50aYwpQdcV/CsVkAaJsMKy8WhWSulcRFeMYp72kfIfdy0qGdRB7Uw==", + "dependencies": { + "@babel/runtime": "^7.0.0" + } + }, + "node_modules/@atlaskit/pragmatic-drag-and-drop": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.3.tgz", + "integrity": "sha512-lx6ZMPSU8zPhUfAkdKajNAFWDDIqdtM8eQzCsqCRalXWumpclcvqeN8VCLkmclcQDEUhV8c2utKbcuhm7hvRIw==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "bind-event-listener": "^2.1.1", + "raf-schd": "^4.0.3" + } + }, + "node_modules/@atlaskit/pragmatic-drag-and-drop-hitbox": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop-hitbox/-/pragmatic-drag-and-drop-hitbox-1.0.3.tgz", + "integrity": "sha512-/Sbu/HqN2VGLYBhnsG7SbRNg98XKkbF6L7XDdBi+izRybfaK1FeMfodPpm/xnBHPJzwYMdkE0qtLyv6afhgMUA==", + "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.1.0", + "@babel/runtime": "^7.0.0" + } + }, + "node_modules/@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/-/pragmatic-drag-and-drop-react-drop-indicator-1.1.0.tgz", + "integrity": "sha512-h6TClbK1axZflyQL17mDZO44psPyk3i7P7xn/lSzPfMEXdUuGHPsGBr5jH0QyxJ+flHA9GlfNDqggJK3L/HkFg==", + "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.1.0", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.0", + "@atlaskit/tokens": "^1.43.0", + "@babel/runtime": "^7.0.0", + "@emotion/react": "^11.7.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/node_modules/@atlaskit/tokens": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@atlaskit/tokens/-/tokens-1.43.0.tgz", + "integrity": "sha512-3rRxGRnJGQBVKGqNqy+Zuad3xuDZ7uD+aFGRcU2OpLuIpiFLX95agDZ9w0HGzNiDw9eWi2f1j8Uzq06AyaRqTw==", + "dependencies": { + "@atlaskit/ds-lib": "^2.2.0", + "@atlaskit/platform-feature-flags": "^0.2.0", + "@babel/runtime": "^7.0.0", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.20.0", + "bind-event-listener": "^2.1.1" + }, + "peerDependencies": { + "react": "^16.8.0" + } + }, + "node_modules/@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/node_modules/@atlaskit/tokens/node_modules/@atlaskit/ds-lib": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@atlaskit/ds-lib/-/ds-lib-2.2.5.tgz", + "integrity": "sha512-fE7uEuB4uAJvNGncY5k+ofUYmJmVcS43MDChvhz1qG2dweYx4hktFVOowiDjt7+RtLQOXuvs7WTn3wIy25er3w==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "bind-event-listener": "^2.1.1" + }, + "peerDependencies": { + "react": "^16.8.0" + } + }, "node_modules/@axa-fr/oidc-client": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@axa-fr/oidc-client/-/oidc-client-7.21.0.tgz", @@ -235,7 +308,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", - "dev": true, "dependencies": { "@babel/types": "^7.23.4", "@jridgewell/gen-mapping": "^0.3.2", @@ -275,7 +347,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -284,7 +355,6 @@ "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" @@ -297,7 +367,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -360,7 +429,6 @@ "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -488,7 +556,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -688,7 +755,6 @@ "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", "@babel/parser": "^7.22.15", @@ -702,7 +768,6 @@ "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.23.4", "@babel/generator": "^7.23.4", @@ -723,7 +788,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "engines": { "node": ">=4" } @@ -2107,7 +2171,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -2121,7 +2184,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -2130,7 +2192,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -2138,14 +2199,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3797,6 +3856,11 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bind-event-listener": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-2.1.1.tgz", + "integrity": "sha512-O+a5c0D2se/u2VlBJmPRn45IB6R4mYMh1ok3dWxrIZ2pmLqzggBhb875mbq73508ylzofc0+hT9W41x4Y2s8lg==" + }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -4788,7 +4852,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -8372,7 +8435,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -9101,8 +9163,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", diff --git a/package.json b/package.json index 0c69c21e..5392d6d9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "prepare": "husky" }, "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.1.3", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", + "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^1.1.0", "@axa-fr/react-oidc": "^7.21.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", diff --git a/src/app/error.tsx b/src/app/(dashboard)/error.tsx similarity index 100% rename from src/app/error.tsx rename to src/app/(dashboard)/error.tsx diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index f7b59967..3910b9c1 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -12,7 +12,18 @@ import { import { Dashboard, FolderCopy } from "@mui/icons-material"; import MonitorIcon from "@mui/icons-material/Monitor"; import MenuBookIcon from "@mui/icons-material/MenuBook"; -import React, { ComponentType, ReactEventHandler, useState } from "react"; +import React, { + ComponentType, + ReactEventHandler, + useEffect, + useState, +} from "react"; +import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { + Edge, + extractClosestEdge, +} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; +import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index"; import DrawerItemGroup from "./DrawerItemGroup"; import { DiracLogo } from "./DiracLogo"; @@ -66,42 +77,113 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { items: { title: string; id: number; icon: ComponentType; path: string }[]; }[]); - /** - * Handles the drag end event for reordering items in the group. - * - * @param result - The result object containing information about the drag event. - */ - function onDragEnd(result: any) { - // Reorder the list of items in the group. - if (!result.destination) { - return; - } - const source = result.source; - const destination = result.destination; + useEffect(() => { + return monitorForElements({ + onDrop({ source, location }) { + const target = location.current.dropTargets[0]; + if (!target) { + return; + } + const sourceData = source.data; + const targetData = target.data; + + if (location.current.dropTargets.length == 2) { + const groupTitle = targetData.title; + const closestEdgeOfTarget = extractClosestEdge(targetData); + const targetIndex = targetData.index as number; + const sourceGroup = userSections.find( + (group) => group.title == sourceData.title, + ); + const targetGroup = userSections.find( + (group) => group.title == groupTitle, + ); + const sourceIndex = sourceData.index as number; + const destinationIndex = ( + closestEdgeOfTarget === "top" ? targetIndex : targetIndex + 1 + ) as number; - const sourceGroup = userSections.find( - (group) => group.title == source.droppableId, - ); - const destinationGroup = userSections.find( - (group) => group.title == destination.droppableId, - ); + reorderSections( + sourceGroup, + targetGroup, + sourceIndex, + destinationIndex, + ); + } else { + const groupTitle = targetData.title; + const sourceGroup = userSections.find( + (group) => group.title == sourceData.title, + ); + const targetGroup = userSections.find( + (group) => group.title == groupTitle, + ); + const sourceIndex = sourceData.index as number; + reorderSections(sourceGroup, targetGroup, sourceIndex); + } + }, + }); + }, [userSections]); + + function reorderSections( + sourceGroup: any, + destinationGroup: any, + sourceIndex: number, + destinationIndex: number | null = null, + ) { if (sourceGroup && destinationGroup) { - const sourceItems = [...sourceGroup.items]; - const destinationItems = [...destinationGroup.items]; - - const [removed] = sourceItems.splice(source.index, 1); - destinationItems.splice(destination.index, 0, removed); - - setSections((sections) => - sections.map((section) => - section.title === sourceGroup.title - ? { ...section, items: sourceItems } - : section.title === destinationGroup.title - ? { ...section, items: destinationItems } + if ( + sourceGroup.title === destinationGroup.title && + destinationIndex && + sourceIndex < destinationIndex + ) { + destinationIndex -= 1; + } + if ( + sourceGroup.title === destinationGroup.title && + (destinationIndex == null || sourceIndex === destinationIndex) + ) { + return; + } + + if (sourceGroup.title === destinationGroup.title) { + const sourceItems = [...sourceGroup.items]; + + const [removed] = sourceItems.splice(sourceIndex, 1); + + if (destinationIndex === null) { + destinationIndex = sourceItems.length; + } + sourceItems.splice(destinationIndex, 0, removed); + + setSections((sections) => + sections.map((section) => + section.title === sourceGroup.title + ? { ...section, items: sourceItems } : section, - ), - ); + ), + ); + } else { + const sourceItems = [...sourceGroup.items]; + + const [removed] = sourceItems.splice(sourceIndex, 1); + + const destinationItems = [...destinationGroup.items]; + + if (destinationIndex === null) { + destinationIndex = destinationItems.length; + } + destinationItems.splice(destinationIndex, 0, removed); + + setSections((sections) => + sections.map((section) => + section.title === sourceGroup.title + ? { ...section, items: sourceItems } + : section.title === destinationGroup.title + ? { ...section, items: destinationItems } + : section, + ), + ); + } } } diff --git a/src/components/ui/DrawerItem.tsx b/src/components/ui/DrawerItem.tsx new file mode 100644 index 00000000..34513168 --- /dev/null +++ b/src/components/ui/DrawerItem.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from "react"; +import Link from "next/link"; +import { + ListItemButton, + ListItemIcon, + Icon, + ListItemText, +} from "@mui/material"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { + draggable, + dropTargetForElements, +} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { DropIndicator } from "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator/box"; +import { + Edge, + attachClosestEdge, + extractClosestEdge, +} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; + +export default function DrawerItem({ + item: { title, icon, path }, + index, + groupTitle, +}: { + item: { title: string; icon: React.ComponentType; path: string }; + index: number; + groupTitle: string; +}) { + const dragRef = React.useRef(null); + const handleRef = React.useRef(null); + + const [closestEdge, setClosestEdge]: any = useState(null); + + useEffect(() => { + if (!dragRef.current || !handleRef.current) return; + const element = dragRef.current; + const handleItem = handleRef.current; + const title = groupTitle; + + return combine( + draggable({ + element: element, + dragHandle: handleItem, + getInitialData: () => ({ index, title }), + }), + dropTargetForElements({ + element: element, + getData: ({ input, element }) => { + return attachClosestEdge( + { index, title }, + { input, element, allowedEdges: ["top", "bottom"] }, + ); + }, + onDrag({ self, source }) { + const isSource = source.element === element; + if (isSource) { + setClosestEdge(null); + return; + } + const closestEdge = extractClosestEdge(self.data); + + const sourceIndex = source.data.index; + if (typeof sourceIndex === "number") { + const isItemBeforeSource = + index === sourceIndex - 1 && source.data.title === title; + const isItemAfterSource = + index === sourceIndex + 1 && source.data.title === title; + + const isDropIndicatorHidden = + (isItemBeforeSource && closestEdge === "bottom") || + (isItemAfterSource && closestEdge === "top"); + + if (isDropIndicatorHidden) { + setClosestEdge(null); + return; + } + } + setClosestEdge(closestEdge); + }, + onDragLeave() { + setClosestEdge(null); + }, + onDrop: () => { + setClosestEdge(null); + }, + }), + ); + }, [index, groupTitle]); + + return ( + <> + + + + + +
+ +
+ {closestEdge && } +
+ + ); +} diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx index 83c0bfad..e3c45144 100644 --- a/src/components/ui/DrawerItemGroup.tsx +++ b/src/components/ui/DrawerItemGroup.tsx @@ -1,16 +1,8 @@ -import { - Accordion, - AccordionDetails, - AccordionSummary, - Icon, - ListItemButton, - ListItemIcon, - ListItemText, -} from "@mui/material"; +import { Accordion, AccordionDetails, AccordionSummary } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import React from "react"; -import Link from "next/link"; -import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import React, { useEffect } from "react"; +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import DrawerItem from "./DrawerItem"; export default function DrawerItemGroup({ group: { title, extended: expanded, items }, @@ -41,6 +33,23 @@ export default function DrawerItemGroup({ > >; }) { + const dropRef = React.useRef(null); + const [hovered, setHovered] = React.useState(false); + + useEffect(() => { + if (!dropRef.current) return; + const dropItem = dropRef.current; + + return dropTargetForElements({ + element: dropItem, + getData: () => ({ title }), + onDragStart: () => setHovered(true), + onDrop: () => setHovered(false), + onDragEnter: () => setHovered(true), + onDragLeave: () => setHovered(false), + }); + }); + const handleChange = (title: string) => (event: any, isExpanded: any) => { // Set the extended state of the accordion group. setSections((sections) => @@ -51,17 +60,19 @@ export default function DrawerItemGroup({ ), ); }; - + const groupTitle = title; return ( {/* Accordion summary */} }> @@ -70,21 +81,12 @@ export default function DrawerItemGroup({ {/* Accordion details */} {items.map(({ title, id, icon, path }, index) => ( - - - - - -
- -
-
+ ))}
From b123fc0ed4d58e5ac5776da4176a8ce40c33bcd1 Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Mon, 22 Apr 2024 09:04:00 +0200 Subject: [PATCH 07/15] fix: dragging preview bug --- src/components/ui/DrawerItem.tsx | 33 ++++++++++++++++++++++++--- src/components/ui/DrawerItemGroup.tsx | 5 +++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/components/ui/DrawerItem.tsx b/src/components/ui/DrawerItem.tsx index 34513168..c8778b0f 100644 --- a/src/components/ui/DrawerItem.tsx +++ b/src/components/ui/DrawerItem.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { createRoot } from "react-dom/client"; import Link from "next/link"; import { ListItemButton, @@ -18,6 +19,10 @@ import { attachClosestEdge, extractClosestEdge, } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; +import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; +import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; +import { ThemeProvider } from "@/contexts/ThemeProvider"; +import { useMUITheme } from "@/hooks/theme"; export default function DrawerItem({ item: { title, icon, path }, @@ -30,6 +35,7 @@ export default function DrawerItem({ }) { const dragRef = React.useRef(null); const handleRef = React.useRef(null); + const theme = useMUITheme(); const [closestEdge, setClosestEdge]: any = useState(null); @@ -44,6 +50,27 @@ export default function DrawerItem({ element: element, dragHandle: handleItem, getInitialData: () => ({ index, title }), + onGenerateDragPreview: ({ nativeSetDragImage }) => { + setCustomNativeDragPreview({ + nativeSetDragImage, + getOffset: () => ({ x: 165, y: 20 }), + render: ({ container }) => { + const root = createRoot(container); + root.render( + + + + + , + ); + return () => root.unmount(); + }, + }); + }, }), dropTargetForElements({ element: element, @@ -87,7 +114,7 @@ export default function DrawerItem({ }, }), ); - }, [index, groupTitle]); + }, [index, groupTitle, icon, path, theme]); return ( <> @@ -103,13 +130,13 @@ export default function DrawerItem({ -
+ -
+ {closestEdge && } diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx index e3c45144..ea7b1764 100644 --- a/src/components/ui/DrawerItemGroup.tsx +++ b/src/components/ui/DrawerItemGroup.tsx @@ -44,7 +44,10 @@ export default function DrawerItemGroup({ element: dropItem, getData: () => ({ title }), onDragStart: () => setHovered(true), - onDrop: () => setHovered(false), + onDrop: () => { + setHovered(false); + handleChange(title)(null, true); + }, onDragEnter: () => setHovered(true), onDragLeave: () => setHovered(false), }); From 1d6deff1cde4270a630ec713fe0b742e40136675 Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Tue, 23 Apr 2024 11:11:08 +0200 Subject: [PATCH 08/15] feat: Working Dashboard menu --- .../applications/ApplicationList.ts | 19 + src/components/ui/ApplicationDialog.tsx | 113 ++++++ src/components/ui/DashboardDrawer.tsx | 373 +++++++++++++++--- src/components/ui/DrawerItem.tsx | 33 +- src/components/ui/DrawerItemGroup.tsx | 31 +- 5 files changed, 491 insertions(+), 78 deletions(-) create mode 100644 src/components/applications/ApplicationList.ts create mode 100644 src/components/ui/ApplicationDialog.tsx diff --git a/src/components/applications/ApplicationList.ts b/src/components/applications/ApplicationList.ts new file mode 100644 index 00000000..e2b06441 --- /dev/null +++ b/src/components/applications/ApplicationList.ts @@ -0,0 +1,19 @@ +import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; +import JobMonitor from "./JobMonitor"; +import UserDashboard from "./UserDashboard"; + +export const applicationList = [ + { name: "Dashboard", path: "/", component: UserDashboard, icon: Dashboard }, + { + name: "Job Monitor", + path: "/jobmonitor", + component: JobMonitor, + icon: Monitor, + }, + { + name: "File Catalog", + path: "/filecatalog", + component: JobMonitor, + icon: FolderCopy, + }, +]; diff --git a/src/components/ui/ApplicationDialog.tsx b/src/components/ui/ApplicationDialog.tsx new file mode 100644 index 00000000..e4c87826 --- /dev/null +++ b/src/components/ui/ApplicationDialog.tsx @@ -0,0 +1,113 @@ +import React, { ComponentType } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + Button, + Grid, + Icon, +} from "@mui/material"; +import { applicationList } from "../applications/ApplicationList"; + +/** + * Renders a dialog component for creating a new application. + * + * @param {Object} props - The component props. + * @param {boolean} props.appDialogOpen - Determines whether the dialog is open or not. + * @param {React.Dispatch>} props.setAppDialogOpen - Function to set the open state of the dialog. + * @param {(name: string, path: string, icon: ComponentType) => void} props.handleCreateApp - Function to handle the creation of a new application. + * @returns {JSX.Element} The rendered dialog component. + */ +export default function AppDialog({ + appDialogOpen, + setAppDialogOpen, + handleCreateApp, +}: { + appDialogOpen: boolean; + setAppDialogOpen: React.Dispatch>; + handleCreateApp: (name: string, path: string, icon: ComponentType) => void; +}) { + const [appType, setAppType] = React.useState(""); + return ( + setAppDialogOpen(false)} + aria-labelledby="application-dialog-label" + PaperProps={{ + component: "form", + onSubmit: (event: React.FormEvent) => { + event.preventDefault(); + + const path = applicationList.find((app) => app.name === appType) + ?.path; + if (!path) { + console.error("Path not found for application type", appType); + return; + } + const icon = applicationList.find((app) => app.name === appType) + ?.icon; + if (!icon) { + console.error("Icon not found for application type", appType); + return; + } + + handleCreateApp(appType, path, icon as React.ComponentType); + + setAppDialogOpen(false); + }, + }} + fullWidth + maxWidth="sm" + > + New Application + + + Choose the type of application you would like to create. + + + {applicationList.map((app) => ( + + + + ))} + + {/* + + */} + + + + + + ); +} diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index 3910b9c1..ec7680d0 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -1,31 +1,34 @@ import { usePathname } from "next/navigation"; import { + Box, + Button, Drawer, - Icon, List, ListItem, ListItemButton, ListItemIcon, ListItemText, + Menu, + MenuItem, + Popover, + TextField, Toolbar, } from "@mui/material"; -import { Dashboard, FolderCopy } from "@mui/icons-material"; -import MonitorIcon from "@mui/icons-material/Monitor"; +import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; import MenuBookIcon from "@mui/icons-material/MenuBook"; +import AddIcon from "@mui/icons-material/Add"; import React, { + Component, ComponentType, ReactEventHandler, useEffect, useState, } from "react"; import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; -import { - Edge, - extractClosestEdge, -} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; -import { getReorderDestinationIndex } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index"; +import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; import DrawerItemGroup from "./DrawerItemGroup"; import { DiracLogo } from "./DiracLogo"; +import AppDialog from "./ApplicationDialog"; interface DashboardDrawerProps { variant: "permanent" | "temporary"; @@ -34,6 +37,13 @@ interface DashboardDrawerProps { handleDrawerToggle: ReactEventHandler; } +/** + * Represents a drawer component used in the dashboard. + * + * @component + * @param {DashboardDrawerProps} props - The props for the DashboardDrawer component. + * @returns {JSX.Element} The rendered DashboardDrawer component. + */ export default function DashboardDrawer(props: DashboardDrawerProps) { // Get the current URL const pathname = usePathname(); @@ -43,6 +53,22 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { // Check if the drawer is in "temporary" mode. const isTemporary = props.variant === "temporary"; + // Wether the modal for Application Creation is open + const [appDialogOpen, setAppDialogOpen] = useState(false); + + const [contextMenu, setContextMenu] = React.useState<{ + mouseX: number; + mouseY: number; + } | null>(null); + + const [contextState, setContextState] = useState<{ + type: string | null; + id: string | null; + }>({ type: null, id: null }); + + const [popAnchorEl, setPopAnchorEl] = React.useState(null); + const [renameValue, setRenameValue] = React.useState(""); + // Define the sections that are accessible to users. // Each section has an associated icon and path. const [userSections, setSections] = useState([ @@ -50,8 +76,13 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { title: "Dashboard", extended: true, items: [ - { title: "Dashboard", id: 0, icon: Dashboard, path: "/" }, - { title: "Job Monitor", id: 1, icon: MonitorIcon, path: "/jobmonitor" }, + { title: "Dashboard", id: "Dashboard0", icon: Dashboard, path: "/" }, + { + title: "Job Monitor", + id: "JobMonitor0", + icon: Monitor, + path: "/jobmonitor", + }, ], }, { @@ -60,7 +91,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { items: [ { title: "File Catalog", - id: 2, + id: "FileCatatlog0", icon: FolderCopy, path: "/filecatalog", }, @@ -74,7 +105,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { ] as { title: string; extended: boolean; - items: { title: string; id: number; icon: ComponentType; path: string }[]; + items: { title: string; id: string; icon: ComponentType; path: string }[]; }[]); useEffect(() => { @@ -124,6 +155,15 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { }); }, [userSections]); + /** + * Reorders sections within a group or between different groups. + * + * @param sourceGroup - The source group from which the section is being moved. + * @param destinationGroup - The destination group where the section is being moved to. + * @param sourceIndex - The index of the section within the source group. + * @param destinationIndex - The index where the section should be placed in the destination group. + * If null, the section will be placed at the end of the destination group. + */ function reorderSections( sourceGroup: any, destinationGroup: any, @@ -187,52 +227,271 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { } } + /** + * Handles the creation of a new app in the dashboard drawer. + * + * @param appName - The name of the app. + * @param path - The path of the app. + * @param icon - The icon component for the app. + */ + const handleAppCreation = ( + appType: string, + path: string, + icon: ComponentType, + ) => { + let group = userSections[userSections.length - 1]; + if (!group) { + //create a new group if there is no group + group = { + title: `Group ${userSections.length + 1}`, + extended: false, + items: [], + }; + setSections([group]); + } + + let title = `${appType} ${userSections.reduce( + (sum, group) => + sum + group.items.filter((item) => item.icon === icon).length, + 1, + )}`; + while (group.items.some((item) => item.title === title)) { + title = `${appType} ${parseInt(title.split(" ")[1]) + 1}`; + } + + const newApp = { + title, + id: `${title}${userSections.reduce( + (sum, group) => sum + group.items.length, + 0, + )}`, + icon: icon, + path: path, + }; + group.items.push(newApp); + }; + + let isContextStateStable = true; + + const handleContextMenu = + (type: "group" | "item" | null = null, id: string | null = null) => + (event: React.MouseEvent) => { + event.preventDefault(); + if (contextMenu !== null) { + handleCloseContextMenu(); + return; + } + setContextMenu({ + mouseX: event.clientX + 2, + mouseY: event.clientY - 6, + }); + if (isContextStateStable) { + setContextState({ type, id }); + isContextStateStable = false; + } + }; + + const handleCloseContextMenu = () => { + setContextMenu(null); + setContextState({ type: null, id: null }); + isContextStateStable = true; + }; + + const handleNewGroup = () => { + const newGroup = { + title: `Group ${userSections.length + 1}`, + extended: false, + items: [], + }; + while (userSections.some((group) => group.title === newGroup.title)) { + newGroup.title = `Group ${parseInt(newGroup.title.split(" ")[1]) + 1}`; + } + + setSections([...userSections, newGroup]); + handleCloseContextMenu(); + }; + + const handleDelete = () => { + if (contextState.type === "group") { + const newSections = userSections.filter( + (group) => group.title !== contextState.id, + ); + setSections(newSections); + } else if (contextState.type === "item") { + const newSections = userSections.map((group) => { + const newItems = group.items.filter( + (item) => item.id !== contextState.id, + ); + return { ...group, items: newItems }; + }); + setSections(newSections); + } + handleCloseContextMenu(); + }; + + const handleRenameClick = (event: any) => { + setPopAnchorEl(event.currentTarget); + }; + + const popClose = () => { + setRenameValue(""); + setPopAnchorEl(null); + }; + + const handleRename = () => { + if (contextState.type === "group") { + //check if the name is already taken + if (userSections.some((group) => group.title === renameValue)) { + return; + } + //rename the group + const newSections = userSections.map((group) => { + if (group.title === contextState.id) { + return { ...group, title: renameValue }; + } + return group; + }); + setSections(newSections); + } else if (contextState.type === "item") { + const newSections = userSections.map((group) => { + const newItems = group.items.map((item) => { + if (item.id === contextState.id) { + return { ...item, title: renameValue }; + } + return item; + }); + return { ...group, items: newItems }; + }); + setSections(newSections); + } + + popClose(); + handleCloseContextMenu(); + }; + return ( - -
- {/* Display the logo in the toolbar section of the drawer. */} - - - - {/* Map over user sections and render them as list items in the drawer. */} - - {userSections.map((group) => ( - - + <> + +
+ {/* Display the logo in the toolbar section of the drawer. */} + + + + {/* Map over user sections and render them as list items in the drawer. */} + + {userSections.map((group) => ( + + + + ))} + + {/* Render a link to documentation, positioned at the bottom of the drawer. */} + + + setAppDialogOpen(true)}> + + + + + - ))} - - {/* Render a link to documentation, positioned at the bottom of the drawer. */} - - - - - - - - - - -
-
+ + + + + + + + +
+
+ + {contextState.type && ( + Rename + )} + {contextState.type && ( + Delete + )} + + New Group + + +
{ + e.preventDefault(); + handleRename(); + }} + > + + setRenameValue(e.target.value)} + /> + + +
+
+
+ + ); } diff --git a/src/components/ui/DrawerItem.tsx b/src/components/ui/DrawerItem.tsx index c8778b0f..ac70b15a 100644 --- a/src/components/ui/DrawerItem.tsx +++ b/src/components/ui/DrawerItem.tsx @@ -21,6 +21,7 @@ import { } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview"; import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; +import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source"; import { ThemeProvider } from "@/contexts/ThemeProvider"; import { useMUITheme } from "@/hooks/theme"; @@ -43,32 +44,42 @@ export default function DrawerItem({ if (!dragRef.current || !handleRef.current) return; const element = dragRef.current; const handleItem = handleRef.current; - const title = groupTitle; return combine( draggable({ element: element, dragHandle: handleItem, - getInitialData: () => ({ index, title }), - onGenerateDragPreview: ({ nativeSetDragImage }) => { + getInitialData: () => ({ index, title: groupTitle }), + onGenerateDragPreview: ({ nativeSetDragImage, source, location }) => { setCustomNativeDragPreview({ nativeSetDragImage, - getOffset: () => ({ x: 165, y: 20 }), render: ({ container }) => { const root = createRoot(container); root.render( - +
+ +
, ); return () => root.unmount(); }, + getOffset: ({ container }) => { + const elementPos = source.element.getBoundingClientRect(); + const x = location.current.input.pageX - elementPos.x; + const y = location.current.input.pageY - elementPos.y; + return { x, y }; + }, }); }, }), @@ -76,7 +87,7 @@ export default function DrawerItem({ element: element, getData: ({ input, element }) => { return attachClosestEdge( - { index, title }, + { index, title: groupTitle }, { input, element, allowedEdges: ["top", "bottom"] }, ); }, @@ -114,7 +125,7 @@ export default function DrawerItem({ }, }), ); - }, [index, groupTitle, icon, path, theme]); + }, [index, groupTitle, icon, path, theme, title]); return ( <> diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx index ea7b1764..39b0f3f4 100644 --- a/src/components/ui/DrawerItemGroup.tsx +++ b/src/components/ui/DrawerItemGroup.tsx @@ -4,16 +4,24 @@ import React, { useEffect } from "react"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import DrawerItem from "./DrawerItem"; +/** + * Represents a group of items in a drawer. + * + * @param group - The group object containing the title, expanded state, and items. + * @param setSections - The function to update the sections state. + * @returns The JSX element representing the drawer item group. + */ export default function DrawerItemGroup({ group: { title, extended: expanded, items }, setSections, + handleContextMenu, }: { group: { title: string; extended: boolean; items: { title: string; - id: number; + id: string; icon: React.ComponentType; path: string; }[]; @@ -25,13 +33,17 @@ export default function DrawerItemGroup({ extended: boolean; items: { title: string; - id: number; + id: string; icon: React.ComponentType; path: string; }[]; }[] > >; + handleContextMenu: ( + type: "group" | "item" | null, + id: string | null, + ) => (event: React.MouseEvent) => void; }) { const dropRef = React.useRef(null); const [hovered, setHovered] = React.useState(false); @@ -68,8 +80,6 @@ export default function DrawerItemGroup({ {items.map(({ title, id, icon, path }, index) => ( - +
+ +
))}
From 203f2eb5e40778724a0f1d1c1fb67b97a1e922a0 Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Tue, 23 Apr 2024 17:15:23 +0200 Subject: [PATCH 09/15] feat: Application Provider & Section in URL --- src/app/(dashboard)/layout.tsx | 31 ++--- src/components/ui/DashboardDrawer.tsx | 168 ++++++++++---------------- src/components/ui/DataTable.tsx | 7 +- src/components/ui/DrawerItemGroup.tsx | 16 +-- src/contexts/ApplicationsProvider.tsx | 97 +++++++++++++++ src/hooks/searchParamsUtils.tsx | 61 ++++++++++ src/types/UserSection.ts | 12 ++ 7 files changed, 261 insertions(+), 131 deletions(-) create mode 100644 src/contexts/ApplicationsProvider.tsx create mode 100644 src/hooks/searchParamsUtils.tsx create mode 100644 src/types/UserSection.ts diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index d28307df..c3252673 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -2,9 +2,10 @@ import * as React from "react"; import CssBaseline from "@mui/material/CssBaseline"; import { Box } from "@mui/material"; -import { useMUITheme } from "@/hooks/theme"; import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; +import { useMUITheme } from "@/hooks/theme"; import Dashboard from "@/components/layout/Dashboard"; +import ApplicationsProvider from "@/contexts/ApplicationsProvider"; export default function JobMonitorLayout({ children, @@ -15,19 +16,21 @@ export default function JobMonitorLayout({ return (
- - - - - {children} - - - + + + + + + {children} + + + +
); } diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index ec7680d0..be68101b 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -14,13 +14,12 @@ import { TextField, Toolbar, } from "@mui/material"; -import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; import MenuBookIcon from "@mui/icons-material/MenuBook"; import AddIcon from "@mui/icons-material/Add"; import React, { - Component, ComponentType, ReactEventHandler, + useContext, useEffect, useState, } from "react"; @@ -29,6 +28,7 @@ import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/clo import DrawerItemGroup from "./DrawerItemGroup"; import { DiracLogo } from "./DiracLogo"; import AppDialog from "./ApplicationDialog"; +import { ApplicationsContext } from "@/contexts/ApplicationsProvider"; interface DashboardDrawerProps { variant: "permanent" | "temporary"; @@ -71,42 +71,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { // Define the sections that are accessible to users. // Each section has an associated icon and path. - const [userSections, setSections] = useState([ - { - title: "Dashboard", - extended: true, - items: [ - { title: "Dashboard", id: "Dashboard0", icon: Dashboard, path: "/" }, - { - title: "Job Monitor", - id: "JobMonitor0", - icon: Monitor, - path: "/jobmonitor", - }, - ], - }, - { - title: "Other", - extended: false, - items: [ - { - title: "File Catalog", - id: "FileCatatlog0", - icon: FolderCopy, - path: "/filecatalog", - }, - ], - }, - { - title: "Other2", - extended: false, - items: [], - }, - ] as { - title: string; - extended: boolean; - items: { title: string; id: string; icon: ComponentType; path: string }[]; - }[]); + const [userSections, setSections] = useContext(ApplicationsContext); useEffect(() => { return monitorForElements({ @@ -153,79 +118,79 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { } }, }); - }, [userSections]); - /** - * Reorders sections within a group or between different groups. - * - * @param sourceGroup - The source group from which the section is being moved. - * @param destinationGroup - The destination group where the section is being moved to. - * @param sourceIndex - The index of the section within the source group. - * @param destinationIndex - The index where the section should be placed in the destination group. - * If null, the section will be placed at the end of the destination group. - */ - function reorderSections( - sourceGroup: any, - destinationGroup: any, - sourceIndex: number, - destinationIndex: number | null = null, - ) { - if (sourceGroup && destinationGroup) { - if ( - sourceGroup.title === destinationGroup.title && - destinationIndex && - sourceIndex < destinationIndex - ) { - destinationIndex -= 1; - } - if ( - sourceGroup.title === destinationGroup.title && - (destinationIndex == null || sourceIndex === destinationIndex) - ) { - return; - } + /** + * Reorders sections within a group or between different groups. + * + * @param sourceGroup - The source group from which the section is being moved. + * @param destinationGroup - The destination group where the section is being moved to. + * @param sourceIndex - The index of the section within the source group. + * @param destinationIndex - The index where the section should be placed in the destination group. + * If null, the section will be placed at the end of the destination group. + */ + function reorderSections( + sourceGroup: any, + destinationGroup: any, + sourceIndex: number, + destinationIndex: number | null = null, + ) { + if (sourceGroup && destinationGroup) { + if ( + sourceGroup.title === destinationGroup.title && + destinationIndex && + sourceIndex < destinationIndex + ) { + destinationIndex -= 1; + } + if ( + sourceGroup.title === destinationGroup.title && + (destinationIndex == null || sourceIndex === destinationIndex) + ) { + return; + } - if (sourceGroup.title === destinationGroup.title) { - const sourceItems = [...sourceGroup.items]; + if (sourceGroup.title === destinationGroup.title) { + const sourceItems = [...sourceGroup.items]; - const [removed] = sourceItems.splice(sourceIndex, 1); + const [removed] = sourceItems.splice(sourceIndex, 1); - if (destinationIndex === null) { - destinationIndex = sourceItems.length; - } - sourceItems.splice(destinationIndex, 0, removed); - - setSections((sections) => - sections.map((section) => - section.title === sourceGroup.title - ? { ...section, items: sourceItems } - : section, - ), - ); - } else { - const sourceItems = [...sourceGroup.items]; + if (destinationIndex === null) { + destinationIndex = sourceItems.length; + } + sourceItems.splice(destinationIndex, 0, removed); - const [removed] = sourceItems.splice(sourceIndex, 1); + setSections((sections) => + sections.map((section) => + section.title === sourceGroup.title + ? { ...section, items: sourceItems } + : section, + ), + ); + } else { + const sourceItems = [...sourceGroup.items]; - const destinationItems = [...destinationGroup.items]; + const [removed] = sourceItems.splice(sourceIndex, 1); - if (destinationIndex === null) { - destinationIndex = destinationItems.length; + const destinationItems = [...destinationGroup.items]; + + if (destinationIndex === null) { + destinationIndex = destinationItems.length; + } + destinationItems.splice(destinationIndex, 0, removed); + + setSections((sections) => + sections.map((section) => + section.title === sourceGroup.title + ? { ...section, items: sourceItems } + : section.title === destinationGroup.title + ? { ...section, items: destinationItems } + : section, + ), + ); } - destinationItems.splice(destinationIndex, 0, removed); - - setSections((sections) => - sections.map((section) => - section.title === sourceGroup.title - ? { ...section, items: sourceItems } - : section.title === destinationGroup.title - ? { ...section, items: destinationItems } - : section, - ), - ); } } - } + }, [setSections, userSections]); /** * Handles the creation of a new app in the dashboard drawer. @@ -265,6 +230,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { (sum, group) => sum + group.items.length, 0, )}`, + type: appType, icon: icon, path: path, }; diff --git a/src/components/ui/DataTable.tsx b/src/components/ui/DataTable.tsx index 802084f0..78ead256 100644 --- a/src/components/ui/DataTable.tsx +++ b/src/components/ui/DataTable.tsx @@ -30,6 +30,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { FilterToolbar } from "./FilterToolbar"; import { Filter } from "@/types/Filter"; import { Column } from "@/types/Column"; +import { useSearchParamsUtils } from "@/hooks/searchParamsUtils"; /** * Descending comparator function @@ -346,6 +347,7 @@ export function DataTable(props: DataTableProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const { getParam, setParam, removeParam } = useSearchParamsUtils(); // Manage URL search params const createQueryString = React.useCallback( @@ -373,9 +375,10 @@ export function DataTable(props: DataTableProps) { const queryString = createQueryString(newFilters); // Push new URL to history without reloading the page - router.push(pathname + "?" + queryString); + // router.push(pathname + "?" + queryString); + setParam("filter", queryString); }, - [createQueryString, pathname, router], + [createQueryString, setParam], ); // Handle the application of filters diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx index 39b0f3f4..126145a8 100644 --- a/src/components/ui/DrawerItemGroup.tsx +++ b/src/components/ui/DrawerItemGroup.tsx @@ -3,6 +3,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import React, { useEffect } from "react"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import DrawerItem from "./DrawerItem"; +import { UserSection } from "@/types/UserSection"; /** * Represents a group of items in a drawer. @@ -26,20 +27,7 @@ export default function DrawerItemGroup({ path: string; }[]; }; - setSections: React.Dispatch< - React.SetStateAction< - { - title: string; - extended: boolean; - items: { - title: string; - id: string; - icon: React.ComponentType; - path: string; - }[]; - }[] - > - >; + setSections: React.Dispatch>; handleContextMenu: ( type: "group" | "item" | null, id: string | null, diff --git a/src/contexts/ApplicationsProvider.tsx b/src/contexts/ApplicationsProvider.tsx new file mode 100644 index 00000000..4bdb1b9e --- /dev/null +++ b/src/contexts/ApplicationsProvider.tsx @@ -0,0 +1,97 @@ +import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; +import React, { createContext, useEffect, useState } from "react"; +import { useSearchParamsUtils } from "@/hooks/searchParamsUtils"; +import { applicationList } from "@/components/applications/ApplicationList"; +import { UserSection } from "@/types/UserSection"; + +// Create a context for the userSections state +export const ApplicationsContext = createContext< + [UserSection[], React.Dispatch>] +>([[], () => {}]); + +// Create the ApplicationsProvider component +const ApplicationsProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [userSections, setSections] = useState([ + { + title: "Dashboard", + extended: true, + items: [ + { + title: "Dashboard", + type: "Dashboard", + id: "Dashboard0", + icon: Dashboard, + path: "/", + }, + { + title: "Job Monitor", + type: "Job Monitor", + id: "JobMonitor0", + icon: Monitor, + path: "/jobmonitor", + }, + ], + }, + { + title: "Other", + extended: false, + items: [ + { + title: "File Catalog", + type: "File Catalog", + id: "FileCatatlog0", + icon: FolderCopy, + path: "/filecatalog", + }, + ], + }, + ]); + const { getParam, setParam } = useSearchParamsUtils(); + + useEffect(() => { + // get user sections from searchParams + const sectionsParams = getParam("sections"); + if (sectionsParams) { + const newSections = JSON.parse(sectionsParams).map( + (section: { items: any[] }) => { + section.items = section.items.map((item: any) => { + return { + ...item, + //get icon from ApplicationList + icon: applicationList.find((app) => app.name === item.type)?.icon, + }; + }); + return section; + }, + ); + setSections(newSections); + } + }, [getParam]); + + useEffect(() => { + // save user sections to searchParams (but not icons) + const newSections = userSections.map((section) => { + return { + ...section, + items: section.items.map((item) => { + return { + ...item, + icon: () => null, + }; + }), + }; + }); + setParam("sections", JSON.stringify(newSections)); + console.log(JSON.stringify(newSections)); + }, [setParam, userSections]); + + return ( + + {children} + + ); +}; + +export default ApplicationsProvider; diff --git a/src/hooks/searchParamsUtils.tsx b/src/hooks/searchParamsUtils.tsx new file mode 100644 index 00000000..6aa61cc6 --- /dev/null +++ b/src/hooks/searchParamsUtils.tsx @@ -0,0 +1,61 @@ +import { useRouter, useSearchParams, usePathname } from "next/navigation"; +import { useCallback } from "react"; + +/** + * Custom hook for managing search parameters in the URL. + * Provides functions to get, set, and remove search parameters. + * + * @returns An object containing the `getParam`, `setParam`, and `removeParam` functions. + */ +export function useSearchParamsUtils() { + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + + const getParams = useCallback(() => { + return searchParams.entries(); + }, [searchParams]); + + const buildQueryString = useCallback((params: Record) => { + const query = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + query.set(key, value); + }); + return query.toString(); + }, []); + + const getParam = useCallback( + (key: string) => { + return searchParams.get(key); + }, + [searchParams], + ); + + const setParam = useCallback( + (key: string, value: string) => { + const params = Object.fromEntries(getParams()); + router.push( + `${pathname}?${buildQueryString({ + ...params, + [key]: value, + })}`, + ); + }, + [buildQueryString, getParams, pathname, router], + ); + + const removeParam = useCallback( + (key: string) => { + const { [key]: _, ...rest } = Object.fromEntries(getParams()); + + router.push( + `${pathname}?${buildQueryString({ + ...rest, + })}`, + ); + }, + [buildQueryString, getParams, pathname, router], + ); + + return { getParam, setParam, removeParam }; +} diff --git a/src/types/UserSection.ts b/src/types/UserSection.ts new file mode 100644 index 00000000..45334321 --- /dev/null +++ b/src/types/UserSection.ts @@ -0,0 +1,12 @@ +// Define the type for the userSections state +export type UserSection = { + title: string; + extended: boolean; + items: { + title: string; + type: string; + id: string; + icon: React.ComponentType; + path: string; + }[]; +}; From dc7982b21f98da2eac676db10e7ce817fda9d991 Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Wed, 24 Apr 2024 10:10:09 +0200 Subject: [PATCH 10/15] fix: app creation not updating sections correctly --- src/components/ui/DashboardDrawer.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index be68101b..1b0bbeb5 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -205,14 +205,14 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { icon: ComponentType, ) => { let group = userSections[userSections.length - 1]; - if (!group) { + const empty = !group; + if (empty) { //create a new group if there is no group group = { title: `Group ${userSections.length + 1}`, extended: false, items: [], }; - setSections([group]); } let title = `${appType} ${userSections.reduce( @@ -235,6 +235,13 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { path: path, }; group.items.push(newApp); + if (empty) { + setSections([...userSections, group]); + } else { + setSections( + userSections.map((g) => (g.title === group.title ? group : g)), + ); + } }; let isContextStateStable = true; From 43376a4ff1f64737c6e11aff2a144d4131b60b50 Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Wed, 24 Apr 2024 10:12:17 +0200 Subject: [PATCH 11/15] feat: shorten URL with jsonCrush lib --- package-lock.json | 6 ++++++ package.json | 1 + src/contexts/ApplicationsProvider.tsx | 6 +++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d08f70fe..472f19d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/react": "18.2.48", "@types/react-dom": "18.2.25", "autoprefixer": "10.4.19", + "jsoncrush": "^1.1.8", "next": "^14.1.4", "postcss": "8.4.38", "react": "^18.2.0", @@ -8489,6 +8490,11 @@ "node": ">=6" } }, + "node_modules/jsoncrush": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/jsoncrush/-/jsoncrush-1.1.8.tgz", + "integrity": "sha512-lvIMGzMUA0fjuqwNcxlTNRq2bibPZ9auqT/LyGdlR5hvydJtA/BasSgkx4qclqTKVeTidrJvsS/oVjlTCPQ4Nw==" + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", diff --git a/package.json b/package.json index 5392d6d9..712d2e94 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/react": "18.2.48", "@types/react-dom": "18.2.25", "autoprefixer": "10.4.19", + "jsoncrush": "^1.1.8", "next": "^14.1.4", "postcss": "8.4.38", "react": "^18.2.0", diff --git a/src/contexts/ApplicationsProvider.tsx b/src/contexts/ApplicationsProvider.tsx index 4bdb1b9e..08c28f8d 100644 --- a/src/contexts/ApplicationsProvider.tsx +++ b/src/contexts/ApplicationsProvider.tsx @@ -1,5 +1,6 @@ import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; import React, { createContext, useEffect, useState } from "react"; +import JSONCrush from "jsoncrush"; import { useSearchParamsUtils } from "@/hooks/searchParamsUtils"; import { applicationList } from "@/components/applications/ApplicationList"; import { UserSection } from "@/types/UserSection"; @@ -54,7 +55,7 @@ const ApplicationsProvider: React.FC<{ children: React.ReactNode }> = ({ // get user sections from searchParams const sectionsParams = getParam("sections"); if (sectionsParams) { - const newSections = JSON.parse(sectionsParams).map( + const newSections = JSON.parse(JSONCrush.uncrush(sectionsParams)).map( (section: { items: any[] }) => { section.items = section.items.map((item: any) => { return { @@ -83,8 +84,7 @@ const ApplicationsProvider: React.FC<{ children: React.ReactNode }> = ({ }), }; }); - setParam("sections", JSON.stringify(newSections)); - console.log(JSON.stringify(newSections)); + setParam("sections", JSONCrush.crush(JSON.stringify(newSections))); }, [setParam, userSections]); return ( From 9f5edbdfcaa59791225ff25a69155ac637709d6a Mon Sep 17 00:00:00 2001 From: Loris Van Katwijk Date: Wed, 24 Apr 2024 17:07:42 +0200 Subject: [PATCH 12/15] fix: jobmonitor filter urls --- src/components/ui/ApplicationDialog.tsx | 16 ---- src/components/ui/DataTable.tsx | 49 +++++------ src/contexts/ApplicationsProvider.tsx | 103 +++++++++++++----------- src/hooks/searchParamsUtils.tsx | 48 +++++------ 4 files changed, 100 insertions(+), 116 deletions(-) diff --git a/src/components/ui/ApplicationDialog.tsx b/src/components/ui/ApplicationDialog.tsx index e4c87826..2314866f 100644 --- a/src/components/ui/ApplicationDialog.tsx +++ b/src/components/ui/ApplicationDialog.tsx @@ -83,22 +83,6 @@ export default function AppDialog({ ))} - {/* - - */} - - ); -} diff --git a/src/app/(dashboard)/jobmonitor/layout.tsx b/src/app/(dashboard)/jobmonitor/layout.tsx deleted file mode 100644 index 1f26a53c..00000000 --- a/src/app/(dashboard)/jobmonitor/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function JobMonitorLayout({ - children, -}: { - children: React.ReactNode; -}) { - return
{children}
; -} diff --git a/src/app/(dashboard)/jobmonitor/page.tsx b/src/app/(dashboard)/jobmonitor/page.tsx deleted file mode 100644 index 54fc0ed6..00000000 --- a/src/app/(dashboard)/jobmonitor/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import JobMonitor from "@/components/applications/JobMonitor"; - -export default async function Page() { - return ; -} diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 16520780..eb79c070 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -8,7 +8,7 @@ import Dashboard from "@/components/layout/Dashboard"; import ApplicationsProvider from "@/contexts/ApplicationsProvider"; import { OIDCSecure } from "@/components/layout/OIDCSecure"; -export default function JobMonitorLayout({ +export default function DashboardLayout({ children, }: { children: React.ReactNode; diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx index 3967341a..ed193572 100644 --- a/src/app/(dashboard)/page.tsx +++ b/src/app/(dashboard)/page.tsx @@ -10,12 +10,16 @@ export default function Page() { const appId = searchParams.get("appId"); const [sections] = React.useContext(ApplicationsContext); - const appType = sections - .find((section) => section.items.some((item) => item.id === appId)) - ?.items.find((item) => item.id === appId)?.type; + const appType = React.useMemo(() => { + const section = sections.find((section) => + section.items.some((item) => item.id === appId), + ); + return section?.items.find((item) => item.id === appId)?.type; + }, [sections, appId]); - const Component = applicationList.find((app) => app.name === appType) - ?.component; + const Component = React.useMemo(() => { + return applicationList.find((app) => app.name === appType)?.component; + }, [appType]); return Component ? : ; } diff --git a/src/components/applications/ApplicationList.ts b/src/components/applications/ApplicationList.ts index 32d26fe5..7bda2436 100644 --- a/src/components/applications/ApplicationList.ts +++ b/src/components/applications/ApplicationList.ts @@ -1,8 +1,9 @@ import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; import JobMonitor from "./JobMonitor"; import UserDashboard from "./UserDashboard"; +import ApplicationConfig from "@/types/ApplicationConfig"; -export const applicationList = [ +export const applicationList: ApplicationConfig[] = [ { name: "Dashboard", component: UserDashboard, icon: Dashboard }, { name: "Job Monitor", diff --git a/src/components/layout/OIDCSecure.tsx b/src/components/layout/OIDCSecure.tsx index 515ebd5c..bc6f9adb 100644 --- a/src/components/layout/OIDCSecure.tsx +++ b/src/components/layout/OIDCSecure.tsx @@ -23,6 +23,7 @@ export function OIDCSecure({ children }: OIDCProps) { if (!isAuthenticated) { router.push( "/auth?" + + // URLSearchParams to ensure that auth redirects users to the URL they came from new URLSearchParams({ redirect: window.location.href }).toString(), ); } diff --git a/src/components/ui/ApplicationDialog.tsx b/src/components/ui/ApplicationDialog.tsx index 0d15dddc..58864845 100644 --- a/src/components/ui/ApplicationDialog.tsx +++ b/src/components/ui/ApplicationDialog.tsx @@ -8,7 +8,9 @@ import { Button, Grid, Icon, + IconButton, } from "@mui/material"; +import { Close } from "@mui/icons-material"; import { applicationList } from "../applications/ApplicationList"; /** @@ -55,10 +57,26 @@ export default function AppDialog({ fullWidth maxWidth="sm" > - New Application + + Available applications + + setAppDialogOpen(false)} + sx={{ + position: "absolute", + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500], + }} + > + + - Choose the type of application you would like to create. + Click on any application to open it in a new instance in the drawer. + Multiple instances of the same application can be opened + simultaneously. {applicationList.map((app) => ( @@ -78,14 +96,6 @@ export default function AppDialog({ ))} - - - ); } diff --git a/src/components/ui/DashboardDrawer.tsx b/src/components/ui/DashboardDrawer.tsx index 9ea65d65..637e36c8 100644 --- a/src/components/ui/DashboardDrawer.tsx +++ b/src/components/ui/DashboardDrawer.tsx @@ -1,4 +1,3 @@ -import { usePathname } from "next/navigation"; import { Box, Button, @@ -25,10 +24,11 @@ import React, { } from "react"; import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; +import Image from "next/image"; import DrawerItemGroup from "./DrawerItemGroup"; -import { DiracLogo } from "./DiracLogo"; import AppDialog from "./ApplicationDialog"; import { ApplicationsContext } from "@/contexts/ApplicationsProvider"; +import { useMUITheme } from "@/hooks/theme"; interface DashboardDrawerProps { variant: "permanent" | "temporary"; @@ -45,15 +45,12 @@ interface DashboardDrawerProps { * @returns {JSX.Element} The rendered DashboardDrawer component. */ export default function DashboardDrawer(props: DashboardDrawerProps) { - // Get the current URL - const pathname = usePathname(); // Determine the container for the Drawer based on whether the window object exists. const container = window !== undefined ? () => window.document.body : undefined; // Check if the drawer is in "temporary" mode. const isTemporary = props.variant === "temporary"; - - // Wether the modal for Application Creation is open + // Whether the modal for Application Creation is open const [appDialogOpen, setAppDialogOpen] = useState(false); const [contextMenu, setContextMenu] = React.useState<{ @@ -73,7 +70,10 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { // Each section has an associated icon and path. const [userSections, setSections] = useContext(ApplicationsContext); + const theme = useMUITheme(); + useEffect(() => { + // Handle changes to sections when drag and drop occurs. return monitorForElements({ onDrop({ source, location }) { const target = location.current.dropTargets[0]; @@ -84,6 +84,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { const targetData = target.data; if (location.current.dropTargets.length == 2) { + // If the target is an item const groupTitle = targetData.title; const closestEdgeOfTarget = extractClosestEdge(targetData); const targetIndex = targetData.index as number; @@ -105,6 +106,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { destinationIndex, ); } else { + // If the target is a group const groupTitle = targetData.title; const sourceGroup = userSections.find( (group) => group.title == sourceData.title, @@ -140,13 +142,13 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { destinationIndex && sourceIndex < destinationIndex ) { - destinationIndex -= 1; + destinationIndex -= 1; // Corrects the index within the same group if needed } if ( sourceGroup.title === destinationGroup.title && (destinationIndex == null || sourceIndex === destinationIndex) ) { - return; + return; // Nothing to do } if (sourceGroup.title === destinationGroup.title) { @@ -195,8 +197,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { /** * Handles the creation of a new app in the dashboard drawer. * - * @param appName - The name of the app. - * @param path - The path of the app. + * @param appType - The type of the app to be created. * @param icon - The icon component for the app. */ const handleAppCreation = (appType: string, icon: ComponentType) => { @@ -361,8 +362,20 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { style={{ display: "flex", flexDirection: "column", height: "100%" }} > {/* Display the logo in the toolbar section of the drawer. */} - - + + DIRAC logo {/* Map over user sections and render them as list items in the drawer. */} @@ -380,8 +393,17 @@ export default function DashboardDrawer(props: DashboardDrawerProps) {
))}
- {/* Render a link to documentation, positioned at the bottom of the drawer. */} - + + {/* Render a link to documentation and a button to add applications, positioned at the bottom of the drawer. */} + setAppDialogOpen(true)}> diff --git a/src/components/ui/DiracLogo.tsx b/src/components/ui/DiracLogo.tsx deleted file mode 100644 index cff07b6f..00000000 --- a/src/components/ui/DiracLogo.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import NextLink from "next/link"; -import Image from "next/image"; - -/** - * Logo of the DIRAC interware redirecting to the root page - * @returns a NextLink embedding an Image - */ -export function DiracLogo() { - return ( - <> - - DIRAC logo - - - ); -} diff --git a/src/components/ui/DrawerItem.tsx b/src/components/ui/DrawerItem.tsx index b2fff815..71f11bb8 100644 --- a/src/components/ui/DrawerItem.tsx +++ b/src/components/ui/DrawerItem.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; -import Link from "next/link"; import { ListItemButton, ListItemIcon, @@ -25,6 +24,14 @@ import { ThemeProvider } from "@/contexts/ThemeProvider"; import { useMUITheme } from "@/hooks/theme"; import { useSearchParamsUtils } from "@/hooks/searchParamsUtils"; +/** + * Represents a drawer item component. + * + * @param item - The item object containing the title, id, and icon. + * @param index - The index of the item. + * @param groupTitle - The title of the group. + * @returns The rendered JSX for the drawer item. + */ export default function DrawerItem({ item: { title, id, icon }, index, @@ -34,11 +41,13 @@ export default function DrawerItem({ index: number; groupTitle: string; }) { + // Ref to use for the draggable element const dragRef = React.useRef(null); + // Ref to use for the handle of the draggable element, must be a child of the draggable element const handleRef = React.useRef(null); const theme = useMUITheme(); const { setParam } = useSearchParamsUtils(); - + // Represents the closest edge to the mouse cursor const [closestEdge, setClosestEdge]: any = useState(null); useEffect(() => { @@ -47,16 +56,21 @@ export default function DrawerItem({ const handleItem = handleRef.current; return combine( + // makes the item draggable draggable({ element: element, dragHandle: handleItem, + // Sets the initial data for the drag and drop interaction getInitialData: () => ({ index, title: groupTitle }), + // Sets a lightweight version of the real item as a preview onGenerateDragPreview: ({ nativeSetDragImage, source, location }) => { setCustomNativeDragPreview({ nativeSetDragImage, render: ({ container }) => { const root = createRoot(container); root.render( + // Wraps the preview in the theme provider to ensure the correct theme is applied + // This is necessary because the preview is rendered outside the main app
root.unmount(); }, + // Seamless transition between the preview and the real element getOffset: ({ container }) => { const elementPos = source.element.getBoundingClientRect(); const x = location.current.input.pageX - elementPos.x; @@ -80,6 +95,7 @@ export default function DrawerItem({ }); }, }), + // Makes the item a target for dragged elements. Attach the closest edge data and highlight the destination when hovering over the item dropTargetForElements({ element: element, getData: ({ input, element }) => { @@ -150,6 +166,15 @@ export default function DrawerItem({ ); } +/** + * Lightweight preview of an item in the drawer. + * Used when dragging an item to give a visual representation of it with minimal resources. + * + * @param {Object} props - The component props. + * @param {string} props.title - The title of the item. + * @param {React.ComponentType} props.icon - The icon component for the item. + * @returns {JSX.Element} The rendered item preview. + */ function ItemPreview({ title, icon, diff --git a/src/components/ui/DrawerItemGroup.tsx b/src/components/ui/DrawerItemGroup.tsx index 717caf83..f70f7fd8 100644 --- a/src/components/ui/DrawerItemGroup.tsx +++ b/src/components/ui/DrawerItemGroup.tsx @@ -8,37 +8,38 @@ import { UserSection } from "@/types/UserSection"; /** * Represents a group of items in a drawer. * - * @param group - The group object containing the title, expanded state, and items. - * @param setSections - The function to update the sections state. - * @returns The JSX element representing the drawer item group. + * @component + * @param {Object} props - The component props. + * @param {Object} props.group - The group object containing the title, expanded state, and items. + * @param {string} props.group.title - The title of the group. + * @param {boolean} props.group.extended - The expanded state of the group. + * @param {Array} props.group.items - The array of items in the group. + * @param {Function} props.setSections - The function to set the sections state. + * @param {Function} props.handleContextMenu - The function to handle the context menu. + * @returns {JSX.Element} The rendered DrawerItemGroup component. */ export default function DrawerItemGroup({ group: { title, extended: expanded, items }, setSections, handleContextMenu, }: { - group: { - title: string; - extended: boolean; - items: { - title: string; - id: string; - icon: React.ComponentType; - }[]; - }; + group: UserSection; setSections: React.Dispatch>; handleContextMenu: ( type: "group" | "item" | null, id: string | null, ) => (event: React.MouseEvent) => void; }) { + // Ref to use for the drag and drop target const dropRef = React.useRef(null); + // State to track whether the user is hovering over the item during a drag operation const [hovered, setHovered] = React.useState(false); useEffect(() => { if (!dropRef.current) return; const dropItem = dropRef.current; + // Makes the element a valid drop target, sets up the data transfer and manage the hovered state return dropTargetForElements({ element: dropItem, getData: () => ({ title }), @@ -52,6 +53,7 @@ export default function DrawerItemGroup({ }); }); + // Handles expansion of the accordion group const handleChange = (title: string) => (event: any, isExpanded: any) => { // Set the extended state of the accordion group. setSections((sections) => @@ -62,7 +64,6 @@ export default function DrawerItemGroup({ ), ); }; - const groupTitle = title; return ( {/* Accordion details */} - {items.map(({ title, id, icon }, index) => ( + {items.map(({ title: itemTitle, id, icon }, index) => (
))} diff --git a/src/contexts/ApplicationsProvider.tsx b/src/contexts/ApplicationsProvider.tsx index 9f9cc45b..2b346688 100644 --- a/src/contexts/ApplicationsProvider.tsx +++ b/src/contexts/ApplicationsProvider.tsx @@ -13,9 +13,7 @@ export const ApplicationsContext = createContext< >([[], () => {}]); // Create the ApplicationsProvider component -const ApplicationsProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { +const ApplicationsProvider = ({ children }: { children: React.ReactNode }) => { const [userSections, setSections] = useState([]); const { getParam, setParam } = useSearchParamsUtils(); diff --git a/src/hooks/theme.tsx b/src/hooks/theme.tsx index 049d9bd1..03ce9a0d 100644 --- a/src/hooks/theme.tsx +++ b/src/hooks/theme.tsx @@ -36,6 +36,11 @@ export const useMUITheme = () => { }, }); + const scrollbarBackground = theme === "dark" ? "#333" : "#f1f1f1"; + const scrollbarThumbBackground = theme === "dark" ? "#888" : "#ccc"; + const scrollbarThumbHoverBackground = theme === "dark" ? "#555" : "#999"; + const scrollbarColor = `${scrollbarThumbBackground} ${scrollbarBackground}`; + muiTheme.components = { MuiCssBaseline: { styleOverrides: ` @@ -44,18 +49,18 @@ export const useMUITheme = () => { border-radius: 5px; } ::-webkit-scrollbar-track { - background: ${theme === "dark" ? "#333" : "#f1f1f1"}; + background: ${scrollbarBackground}; } ::-webkit-scrollbar-thumb { - background: ${theme === "dark" ? "#888" : "#ccc"}; + background: ${scrollbarThumbBackground}; border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { - background: ${theme === "dark" ? "#555" : "#999"}; + background: ${scrollbarThumbHoverBackground}; } @supports not selector(::-webkit-scrollbar) { html { - scrollbar-color: ${theme === "dark" ? "#888 #333" : "#ccc #f1f1f1"}; + scrollbar-color: ${scrollbarColor}; } } `, diff --git a/src/types/ApplicationConfig.ts b/src/types/ApplicationConfig.ts new file mode 100644 index 00000000..324a53d8 --- /dev/null +++ b/src/types/ApplicationConfig.ts @@ -0,0 +1,8 @@ +import { SvgIconComponent } from "@mui/icons-material"; +import { ElementType } from "react"; + +export default interface ApplicationConfig { + name: string; + component: ElementType; + icon: SvgIconComponent; +} diff --git a/test/unit-tests/Dashboard.test.tsx b/test/unit-tests/Dashboard.test.tsx index 4df42844..f6551ffd 100644 --- a/test/unit-tests/Dashboard.test.tsx +++ b/test/unit-tests/Dashboard.test.tsx @@ -47,21 +47,19 @@ jest.mock("next/navigation", () => { }; }); -const MockThemeProvider: React.FC<{ children: React.ReactNode }> = ({ +const MockApplicationProvider: React.FC<{ children: React.ReactNode }> = ({ children, }): JSX.Element => ( - - { - mockSections = test(); - }), - ]} - > - {children} - - + { + mockSections = test(); + }), + ]} + > + {children} + ); describe("", () => { @@ -126,9 +124,11 @@ describe("", () => { describe("", () => { it("renders correctly", () => { const { getByText } = render( - - - , + + + + + , ); expect(getByText("App 1 Icon")).toBeInTheDocument(); @@ -136,9 +136,11 @@ describe("", () => { it("handles context menu", () => { const { getByText, getByTestId } = render( - - - , + + + + + , ); fireEvent.contextMenu(getByText("App 1")); diff --git a/test/unit-tests/DiracLogo.test.tsx b/test/unit-tests/DiracLogo.test.tsx deleted file mode 100644 index 9f2a9985..00000000 --- a/test/unit-tests/DiracLogo.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import { render } from "@testing-library/react"; -import { DiracLogo } from "@/components/ui/DiracLogo"; - -describe("", () => { - it("renders the logo image with correct attributes", () => { - const { getByAltText } = render(); - - // Check if the image is rendered with the correct alt text - const logoImage = getByAltText("DIRAC logo"); - expect(logoImage).toBeInTheDocument(); - }); - - it("renders the link that redirects to the root page", () => { - const { getByRole } = render(); - - // Check if the link is rendered and points to the root page - const link = getByRole("link"); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute("href", "/"); - }); -});