mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 15:24:52 +09:00
Use global drag and drop hint overlay
This commit is contained in:
1 parent
920b5c1655
commit
c34a13b3e4
6 files changed
+149
-167
No files matched your search
@@ -0,0 +1,42 @@
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import { DraggingContext } from "../model/is_dragging";
|
||||
import { useContext } from "react";
|
||||
|
||||
// This component is used to display a dropzone when a file is being dragged over the parent component
|
||||
export function Dropzone() {
|
||||
const isDroppingFile = useContext(DraggingContext);
|
||||
|
||||
return <div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: "90",
|
||||
backgroundColor: "#88888844",
|
||||
borderRadius: "20px",
|
||||
border: "dashed 2px rgb(22, 119, 255)",
|
||||
display: isDroppingFile ? "block" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="dropzone-icon"
|
||||
style={{
|
||||
fontSize: "72px",
|
||||
color: "rgb(22, 119, 255)",
|
||||
display: "flex",
|
||||
zIndex: "100",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<InboxOutlined />
|
||||
<p style={{ fontSize: "24px", display: "block", textAlign: "center", marginTop: "8px" }}>
|
||||
Drop to upload
|
||||
</p>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Typography, Upload } from "antd";
|
||||
import ImportOutlined from "@ant-design/icons/ImportOutlined";
|
||||
import PlusOutlined from "@ant-design/icons/PlusOutlined";
|
||||
import UploadOutlined from "@ant-design/icons/UploadOutlined";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
@@ -17,7 +16,8 @@ import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import SectionHeader from "./SectionHeader";
|
||||
import ModDetails from "./ModDetails";
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import { Dropzone } from "./Dropzone";
|
||||
import UploadButton from "./UploadButton";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
@@ -134,7 +134,6 @@ export default function ModsPage() {
|
||||
let navigate = useNavigate();
|
||||
let [mods] = useMods();
|
||||
let [modPacks] = useModPacks();
|
||||
const [isDroppingFile, setIsDroppingFile] = useState(false);
|
||||
|
||||
function actions(mod: lib.ModInfo) {
|
||||
return <Space>
|
||||
@@ -176,27 +175,13 @@ export default function ModsPage() {
|
||||
name="file"
|
||||
accept=".zip"
|
||||
multiple
|
||||
showUploadList={false}
|
||||
headers={{
|
||||
"X-Access-Token": control.connector.token || "",
|
||||
}}
|
||||
action={`${webRoot}api/upload-mod`}
|
||||
>
|
||||
<Button
|
||||
onDragEnter={e => setIsDroppingFile(true)}
|
||||
onDragLeave={e => {
|
||||
// Only process if we are leaving the dropzone for an element that is not a child of the button
|
||||
if ((e.currentTarget as Node).contains(e.relatedTarget as Node)) {
|
||||
return;
|
||||
}
|
||||
setIsDroppingFile(false);
|
||||
}}
|
||||
onDrop={e => setIsDroppingFile(false)}
|
||||
style={{
|
||||
// Distinct border when dropping file
|
||||
border: isDroppingFile ? "dashed 2px rgb(22, 119, 255)" : undefined,
|
||||
}}
|
||||
icon={isDroppingFile ? <InboxOutlined /> : <UploadOutlined />}
|
||||
>Upload</Button>
|
||||
<UploadButton />
|
||||
</Upload>;
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: "90",
|
||||
backgroundColor: "#88888844",
|
||||
borderRadius: "20px",
|
||||
border: "dashed 2px rgb(22, 119, 255)",
|
||||
display: isDroppingFile ? "block" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="dropzone-icon"
|
||||
style={{
|
||||
fontSize: "72px",
|
||||
color: "rgb(22, 119, 255)",
|
||||
display: "flex",
|
||||
zIndex: "100",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<InboxOutlined />
|
||||
<p style={{ fontSize: "24px", display: "block", textAlign: "center", marginTop: "8px" }}>
|
||||
Drop to upload
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Dropzone />
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
|
||||
@@ -15,7 +15,8 @@ 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";
|
||||
import { Dropzone } from "./Dropzone";
|
||||
import UploadButton from "./UploadButton";
|
||||
|
||||
|
||||
type ModalProps = {
|
||||
@@ -205,7 +206,6 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) {
|
||||
let [saves] = useSavesOfInstance(props.instance.id);
|
||||
let [starting, setStarting] = useState(false);
|
||||
let [uploadingFiles, setUploadingFiles] = useState<File[]>([]);
|
||||
const [isDroppingFile, setIsDroppingFile] = useState<boolean>(false);
|
||||
|
||||
let hostOffline = ["unassigned", "unknown"].includes(props.instance.status!);
|
||||
const saveTable = <Table
|
||||
@@ -348,7 +348,9 @@ export default function SavesList(props: { instance: lib.InstanceDetails }) {
|
||||
return <div>
|
||||
<SectionHeader title="Saves" extra={<Space>
|
||||
{account.hasPermission("core.instance.save.upload") && <Upload {...uploadProps} >
|
||||
<Button disabled={hostOffline}>Upload save</Button>
|
||||
<UploadButton disabled={hostOffline}>
|
||||
Upload save
|
||||
</UploadButton>
|
||||
</Upload>}
|
||||
{account.hasPermission("core.instance.save.create") && <CreateSaveModal instance={props.instance} />}
|
||||
</Space>} />
|
||||
@@ -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)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: "90",
|
||||
backgroundColor: "#88888844",
|
||||
borderRadius: "20px",
|
||||
border: "dashed 2px rgb(22, 119, 255)",
|
||||
display: isDroppingFile ? "block" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="dropzone-icon"
|
||||
style={{
|
||||
fontSize: "72px",
|
||||
color: "rgb(22, 119, 255)",
|
||||
display: "flex",
|
||||
zIndex: "100",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<InboxOutlined />
|
||||
<p style={{
|
||||
fontSize: "24px",
|
||||
display: "block",
|
||||
textAlign: "center",
|
||||
marginTop: "8px",
|
||||
}}>
|
||||
Drop to upload
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Dropzone />
|
||||
{saveTable}
|
||||
</div>
|
||||
</Upload.Dragger>
|
||||
|
||||
@@ -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<string|null>(null);
|
||||
let [currentSidebarPath, setCurrentSidebarPath] = useState<string | null>(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 <Layout style={{ minHeight: "100vh" }}>
|
||||
<Header className="header">
|
||||
<div className="site-logo" />
|
||||
<span className="site-name">Clusterio</span>
|
||||
<span className="site-version">{ webUiPackage.version }</span>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
defaultSelectedKeys={["1"]}
|
||||
items={[{ label: "Dashboard", key: "1" }]}
|
||||
/>
|
||||
<Dropdown
|
||||
className="account-dropdown-header"
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
menu={accountMenuProps}
|
||||
>
|
||||
<UserOutlined/>
|
||||
</Dropdown>
|
||||
</Header>
|
||||
<Layout className="site-layout">
|
||||
<Sider
|
||||
collapsible
|
||||
collapsedWidth={0}
|
||||
breakpoint="md"
|
||||
zeroWidthTriggerStyle={{ top: 6, zIndex: -1 }}
|
||||
width={250}
|
||||
className="site-layout-sider"
|
||||
>
|
||||
// Prevent issues with event ordering
|
||||
let dragChange = 0;
|
||||
function setDraggingProxy(value: number) {
|
||||
dragChange += value;
|
||||
setDragging(dragging + dragChange);
|
||||
}
|
||||
|
||||
return <Layout
|
||||
style={{ minHeight: "100vh" }}
|
||||
onDragEnter={() => 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()}
|
||||
>
|
||||
<DraggingContext.Provider value={dragging > 0}>
|
||||
<Header className="header">
|
||||
<div className="site-logo" />
|
||||
<span className="site-name">Clusterio</span>
|
||||
<span className="site-version">{webUiPackage.version}</span>
|
||||
<Menu
|
||||
mode="inline"
|
||||
defaultOpenKeys={[...menuGroups.keys()]}
|
||||
selectedKeys={currentSidebarPath ? [currentSidebarPath] : []}
|
||||
style={{ height: "100%", borderRight: 0 }}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
items={menuItems}
|
||||
theme="dark"
|
||||
mode="horizontal"
|
||||
defaultSelectedKeys={["1"]}
|
||||
items={[{ label: "Dashboard", key: "1" }]}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout className="site-layout-content-container">
|
||||
<Routes>
|
||||
{combinedPages.map(({path, sidebarPath, content}) => <Route
|
||||
path={path}
|
||||
key={path}
|
||||
element={<Fragment key={path}>
|
||||
<SetSidebar path={sidebarPath ? sidebarPath : path} />
|
||||
<ErrorBoundary Component={ErrorPage}>
|
||||
{content}
|
||||
</ErrorBoundary>
|
||||
</Fragment>}
|
||||
/>)}
|
||||
<Route element={<SetSidebar path={null} />} />
|
||||
</Routes>
|
||||
<Dropdown
|
||||
className="account-dropdown-header"
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
menu={accountMenuProps}
|
||||
>
|
||||
<UserOutlined />
|
||||
</Dropdown>
|
||||
</Header>
|
||||
<Layout className="site-layout">
|
||||
<Sider
|
||||
collapsible
|
||||
collapsedWidth={0}
|
||||
breakpoint="md"
|
||||
zeroWidthTriggerStyle={{ top: 6, zIndex: -1 }}
|
||||
width={250}
|
||||
className="site-layout-sider"
|
||||
>
|
||||
<Menu
|
||||
mode="inline"
|
||||
defaultOpenKeys={[...menuGroups.keys()]}
|
||||
selectedKeys={currentSidebarPath ? [currentSidebarPath] : []}
|
||||
style={{ height: "100%", borderRight: 0 }}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
items={menuItems}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout className="site-layout-content-container">
|
||||
<Routes>
|
||||
{combinedPages.map(({ path, sidebarPath, content }) => <Route
|
||||
path={path}
|
||||
key={path}
|
||||
element={<Fragment key={path}>
|
||||
<SetSidebar path={sidebarPath ? sidebarPath : path} />
|
||||
<ErrorBoundary Component={ErrorPage}>
|
||||
{content}
|
||||
</ErrorBoundary>
|
||||
</Fragment>}
|
||||
/>)}
|
||||
<Route element={<SetSidebar path={null} />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</DraggingContext.Provider>
|
||||
</Layout>;
|
||||
}
|
||||
@@ -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<typeof Button>) {
|
||||
const isDroppingFile = useContext(DraggingContext);
|
||||
return <Button
|
||||
{...props}
|
||||
style={{
|
||||
...props.style,
|
||||
// Distinct border when dropping file
|
||||
border: isDroppingFile ? "dashed 2px rgb(22, 119, 255)" : undefined,
|
||||
}}
|
||||
icon={isDroppingFile ? <InboxOutlined /> : props.icon || <UploadOutlined />}
|
||||
>
|
||||
{props.children || "Upload"}
|
||||
</Button>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const DraggingContext = createContext(false);
|
||||
Reference in new issue
Block a user