From ebf65363740788fcb1449441489ded1f74b0e3bd Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Mon, 26 Feb 2024 22:03:47 -0600 Subject: [PATCH 1/5] Change style of mod upload button on drag and drop --- packages/web_ui/src/components/ModsPage.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/web_ui/src/components/ModsPage.tsx b/packages/web_ui/src/components/ModsPage.tsx index 0e5541ae..e380bcb5 100644 --- a/packages/web_ui/src/components/ModsPage.tsx +++ b/packages/web_ui/src/components/ModsPage.tsx @@ -17,6 +17,7 @@ import PageLayout from "./PageLayout"; import PluginExtra from "./PluginExtra"; import SectionHeader from "./SectionHeader"; import ModDetails from "./ModDetails"; +import { InboxOutlined } from "@ant-design/icons"; const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare; @@ -131,6 +132,7 @@ export default function ModsPage() { let navigate = useNavigate(); let [mods] = useMods(); let [modPacks] = useModPacks(); + const [isDroppingFile, setIsDroppingFile] = useState(false); function actions(mod: lib.ModInfo) { return @@ -173,11 +175,26 @@ export default function ModsPage() { accept=".zip" multiple headers={{ - "X-Access-Token": control.connector.token||"", + "X-Access-Token": control.connector.token || "", }} action={`${webRoot}api/upload-mod`} > - + ; } From b70feb37f26efb6955cd066deb6fcf0c232a1050 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Mon, 26 Feb 2024 22:04:37 -0600 Subject: [PATCH 2/5] Show table overlay when dragging and dropping saves/mods --- packages/web_ui/src/components/ModsPage.tsx | 170 +++++++++++++------ packages/web_ui/src/components/SavesList.tsx | 69 +++++++- packages/web_ui/src/style.css | 2 + 3 files changed, 183 insertions(+), 58 deletions(-) diff --git a/packages/web_ui/src/components/ModsPage.tsx b/packages/web_ui/src/components/ModsPage.tsx index e380bcb5..7b2e85ed 100644 --- a/packages/web_ui/src/components/ModsPage.tsx +++ b/packages/web_ui/src/components/ModsPage.tsx @@ -56,11 +56,13 @@ function ImportModPackButton() { name="string" rules={[ { required: true }, - { async validator(rule, value) { - if (value) { - lib.ModPack.fromModPackString(value); - } - }}, + { + async validator(rule, value) { + if (value) { + lib.ModPack.fromModPackString(value); + } + }, + }, ]} > @@ -236,54 +238,118 @@ export default function ModsPage() { })} /> - ( - strcmp(a.name, b.name) || a.integerVersion - b.integerVersion - ), - }, - { - title: "Version", - key: "version", - align: "right", - render: (_, mod) => <> - {`${mod.version} `} - {`/ ${mod.factorioVersion}`} - , - }, - { - title: "Filename", - dataIndex: "filename", - responsive: ["xl"], - sorter: (a, b) => strcmp(a.filename, b.filename), - }, - { - title: "Size", - key: "size", - responsive: ["lg"], - render: (_, mod) => lib.formatBytes(mod.size), - align: "right", - sorter: (a, b) => a.size - b.size, - }, - { - title: "Action", - key: "action", - responsive: ["lg"], - render: (_, mod) => actions(mod), - }, - ]} - expandable={{ - expandedRowRender: (mod: lib.ModInfo) => , - expandedRowClassName: () => "no-expanded-padding", + + mod.id} - /> + action={`${webRoot}api/upload-mod`} + showUploadList={false} + > +
{ + setIsDroppingFile(true); + }} + onDragLeave={e => { + if ((e.currentTarget as Node).contains(e.relatedTarget as Node)) { + return; + } + setIsDroppingFile(false); + }} + onDrop={e => setIsDroppingFile(false)} + > +
+
+ +

+ Drop to upload +

+
+
+
( + strcmp(a.name, b.name) || a.integerVersion - b.integerVersion + ), + }, + { + title: "Version", + key: "version", + align: "right", + render: (_, mod) => <> + {`${mod.version} `} + {`/ ${mod.factorioVersion}`} + , + }, + { + title: "Filename", + dataIndex: "filename", + responsive: ["xl"], + sorter: (a, b) => strcmp(a.filename, b.filename), + }, + { + title: "Size", + key: "size", + responsive: ["lg"], + render: (_, mod) => lib.formatBytes(mod.size), + align: "right", + sorter: (a, b) => a.size - b.size, + }, + { + title: "Action", + key: "action", + responsive: ["lg"], + render: (_, mod) => actions(mod), + }, + ]} + expandable={{ + expandedRowRender: (mod: lib.ModInfo) => , + expandedRowClassName: () => "no-expanded-padding", + }} + dataSource={[...mods.values()]} + pagination={false} + rowKey={mod => mod.id} + /> + + ; } diff --git a/packages/web_ui/src/components/SavesList.tsx b/packages/web_ui/src/components/SavesList.tsx index 92ce287c..66f4f78d 100644 --- a/packages/web_ui/src/components/SavesList.tsx +++ b/packages/web_ui/src/components/SavesList.tsx @@ -15,6 +15,7 @@ import SectionHeader from "./SectionHeader"; import { useInstances } from "../model/instance"; import { useSavesOfInstance } from "../model/saves"; import { notifyErrorHandler } from "../util/notify"; +import { InboxOutlined } from "@ant-design/icons"; type ModalProps = { @@ -150,7 +151,7 @@ function TransferModal(props: ModalProps) { autoFocus showSearch filterOption={(input, option) => ( - (option?.title.toLowerCase().indexOf(input.toLowerCase())??-1) >= 0 + (option?.title.toLowerCase().indexOf(input.toLowerCase()) ?? -1) >= 0 )} > {[...instances.values()].filter( @@ -204,6 +205,7 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) { let [saves] = useSavesOfInstance(props.instance.id); let [starting, setStarting] = useState(false); let [uploadingFiles, setUploadingFiles] = useState([]); + const [isDroppingFile, setIsDroppingFile] = useState(false); let hostOffline = ["unassigned", "unknown"].includes(props.instance.status!); const saveTable =
<> {save.name} - {save.loaded && } - {save.loadByDefault && } + {save.loaded && } + {save.loadByDefault && } , sorter: (a, b) => a.name.localeCompare(b.name), }, @@ -240,7 +242,7 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) { expandable={{ columnWidth: 33, expandRowByClick: true, - expandedRowRender: save => + expandedRowRender: save => {account.hasPermission("core.instance.start") && + ; } @@ -256,50 +241,8 @@ export default function ModsPage() { position: "relative", zIndex: "1010", }} - onDragEnter={e => { - setIsDroppingFile(true); - }} - onDragLeave={e => { - if ((e.currentTarget as Node).contains(e.relatedTarget as Node)) { - return; - } - setIsDroppingFile(false); - }} - onDrop={e => setIsDroppingFile(false)} > -
-
- -

- Drop to upload -

-
-
+
([]); - const [isDroppingFile, setIsDroppingFile] = useState(false); let hostOffline = ["unassigned", "unknown"].includes(props.instance.status!); const saveTable =
{account.hasPermission("core.instance.save.upload") && - + + Upload save + } {account.hasPermission("core.instance.save.create") && } } /> @@ -360,55 +362,8 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) { position: "relative", zIndex: "1010", }} - onDragEnter={e => { - setIsDroppingFile(true); - }} - onDragLeave={e => { - if ((e.currentTarget as Node).contains(e.relatedTarget as Node)) { - return; - } - setIsDroppingFile(false); - }} - onDrop={e => setIsDroppingFile(false)} > -
-
- -

- Drop to upload -

-
-
+ {saveTable} diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index 07a995d1..31b0374f 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -9,16 +9,18 @@ import ErrorBoundary from "./ErrorBoundary"; import ErrorPage from "./ErrorPage"; import ControlContext from "./ControlContext"; import { pages } from "../pages"; +import { DraggingContext } from "../model/is_dragging"; const { Header, Sider } = Layout; export default function SiteLayout() { let navigate = useNavigate(); - let [currentSidebarPath, setCurrentSidebarPath] = useState(null); + let [currentSidebarPath, setCurrentSidebarPath] = useState(null); let account = useAccount(); let plugins = useContext(ControlContext).plugins; + const [dragging, setDragging] = useState(0); - function SetSidebar(props: { path: string|null }) { + function SetSidebar(props: { path: string | null }) { useEffect(() => { setCurrentSidebarPath(props.path); }); @@ -26,7 +28,7 @@ export default function SiteLayout() { } let accountMenuProps: MenuProps = { - onClick: ({ key }: {key:string}) => { + onClick: ({ key }: { key: string }) => { if (key === "user") { navigate(`/users/${account.name}/view`); } else if (key === "logOut") { @@ -72,59 +74,76 @@ export default function SiteLayout() { menuItems.push({ type: "group", label: name, children: group, key: name }); } - return -
-
- Clusterio - { webUiPackage.version } - - - - -
- - + // Prevent issues with event ordering + let dragChange = 0; + function setDraggingProxy(value: number) { + dragChange += value; + setDragging(dragging + dragChange); + } + + return setDraggingProxy(1)} + onDragLeave={() => setDraggingProxy(-1)} + // Reset dragging state when dropped + onDrop={() => setDragging(0)} + // Allow entire document to be a drag target + onDragOver={(e) => e.preventDefault()} + > + 0}> +
+
+ Clusterio + {webUiPackage.version} navigate(key)} - items={menuItems} + theme="dark" + mode="horizontal" + defaultSelectedKeys={["1"]} + items={[{ label: "Dashboard", key: "1" }]} /> - - - - {combinedPages.map(({path, sidebarPath, content}) => - - - {content} - - } - />)} - } /> - + + + +
+ + + navigate(key)} + items={menuItems} + /> + + + + {combinedPages.map(({ path, sidebarPath, content }) => + + + {content} + + } + />)} + } /> + + - + ; } diff --git a/packages/web_ui/src/components/UploadButton.tsx b/packages/web_ui/src/components/UploadButton.tsx new file mode 100644 index 00000000..c6c324a9 --- /dev/null +++ b/packages/web_ui/src/components/UploadButton.tsx @@ -0,0 +1,20 @@ +import { useContext } from "react"; +import { Button } from "antd"; +import { InboxOutlined, UploadOutlined } from "@ant-design/icons"; + +import { DraggingContext } from "../model/is_dragging"; + +export default function UploadButton(props: React.ComponentProps) { + const isDroppingFile = useContext(DraggingContext); + return ; +} diff --git a/packages/web_ui/src/model/is_dragging.ts b/packages/web_ui/src/model/is_dragging.ts new file mode 100644 index 00000000..652878e2 --- /dev/null +++ b/packages/web_ui/src/model/is_dragging.ts @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const DraggingContext = createContext(false); From 05b697fd246e7858654c38650207f68c3d57a5b7 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Wed, 15 May 2024 19:43:40 +0100 Subject: [PATCH 4/5] Prevent dropping files outside of dropzones --- packages/web_ui/src/components/Dropzone.tsx | 3 +- packages/web_ui/src/components/SiteLayout.tsx | 35 +++++++++++++++++-- .../web_ui/src/components/UploadButton.tsx | 1 + 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/web_ui/src/components/Dropzone.tsx b/packages/web_ui/src/components/Dropzone.tsx index 0af48ff4..cef95cd6 100644 --- a/packages/web_ui/src/components/Dropzone.tsx +++ b/packages/web_ui/src/components/Dropzone.tsx @@ -7,6 +7,7 @@ export function Dropzone() { const isDroppingFile = useContext(DraggingContext); return
(null); @@ -86,9 +102,24 @@ export default function SiteLayout() { onDragEnter={() => setDraggingProxy(1)} onDragLeave={() => setDraggingProxy(-1)} // Reset dragging state when dropped - onDrop={() => setDragging(0)} + onDrop={(e) => { + setDragging(0); + // Prevent dropping outside of dropzone + if (!isDropzone(e.target as HTMLElement)) { + e.preventDefault(); + e.stopPropagation(); + } + }} // Allow entire document to be a drag target - onDragOver={(e) => e.preventDefault()} + onDragOver={(e) => { + e.preventDefault(); + // Change cursor + if (isDropzone(e.target as HTMLElement)) { + e.dataTransfer.dropEffect = "copy"; + } else { + e.dataTransfer.dropEffect = "none"; + } + }} > 0}>
diff --git a/packages/web_ui/src/components/UploadButton.tsx b/packages/web_ui/src/components/UploadButton.tsx index c6c324a9..652ce9a4 100644 --- a/packages/web_ui/src/components/UploadButton.tsx +++ b/packages/web_ui/src/components/UploadButton.tsx @@ -7,6 +7,7 @@ import { DraggingContext } from "../model/is_dragging"; export default function UploadButton(props: React.ComponentProps) { const isDroppingFile = useContext(DraggingContext); return
; diff --git a/packages/web_ui/src/components/SavesList.tsx b/packages/web_ui/src/components/SavesList.tsx index d5eb6ef8..9a962a87 100644 --- a/packages/web_ui/src/components/SavesList.tsx +++ b/packages/web_ui/src/components/SavesList.tsx @@ -363,7 +363,7 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) { zIndex: "1010", }} > - + {saveTable}
diff --git a/packages/web_ui/src/components/SiteLayout.tsx b/packages/web_ui/src/components/SiteLayout.tsx index 856ac492..5baece76 100644 --- a/packages/web_ui/src/components/SiteLayout.tsx +++ b/packages/web_ui/src/components/SiteLayout.tsx @@ -13,7 +13,7 @@ import { DraggingContext } from "../model/is_dragging"; const { Header, Sider } = Layout; -function isDropzone(element: HTMLElement | null): boolean { +function isActiveDropzone(element: HTMLElement | null): boolean { if (!element) { return false; } @@ -21,7 +21,7 @@ function isDropzone(element: HTMLElement | null): boolean { let depth = 0; while (element && depth < checkDepth) { if (element.classList.contains("dropzone")) { - return true; + return element.classList.contains("enabled"); } element = element.parentElement; depth += 1; @@ -105,7 +105,7 @@ export default function SiteLayout() { onDrop={(e) => { setDragging(0); // Prevent dropping outside of dropzone - if (!isDropzone(e.target as HTMLElement)) { + if (!isActiveDropzone(e.target as HTMLElement)) { e.preventDefault(); e.stopPropagation(); } @@ -114,7 +114,7 @@ export default function SiteLayout() { onDragOver={(e) => { e.preventDefault(); // Change cursor - if (isDropzone(e.target as HTMLElement)) { + if (isActiveDropzone(e.target as HTMLElement)) { e.dataTransfer.dropEffect = "copy"; } else { e.dataTransfer.dropEffect = "none"; diff --git a/packages/web_ui/src/components/UploadButton.tsx b/packages/web_ui/src/components/UploadButton.tsx index 652ce9a4..cc3dd6b7 100644 --- a/packages/web_ui/src/components/UploadButton.tsx +++ b/packages/web_ui/src/components/UploadButton.tsx @@ -6,13 +6,14 @@ import { DraggingContext } from "../model/is_dragging"; export default function UploadButton(props: React.ComponentProps) { const isDroppingFile = useContext(DraggingContext); + const borderColor = props.disabled ? "gray" : "rgb(22, 119, 255)"; return