Skip to content

Commit e454a7f

Browse files
committed
feat(project-page): support file and directory rename/move
1 parent af3d025 commit e454a7f

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

features/ProjectTable/FileActions.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { type DeleteDirectoryButtonProps } from "./buttons/DeleteDirectoryButton
1313
import { type DeleteUnmanagedFileButtonProps } from "./buttons/DeleteUnmanagedFileButton";
1414
import { type DetachDatasetProps } from "./buttons/DetachDataset";
1515
import { FavouriteButton } from "./buttons/FavouriteButton";
16+
import { type RenameButtonProps } from "./buttons/RenameButton";
1617
import { type TableDir, type TableFile } from "./types";
1718
import { isTableDir } from "./utils";
1819

@@ -32,6 +33,10 @@ const DeleteDirectoryButton = dynamic<DeleteDirectoryButtonProps>(
3233
() => import("./buttons/DeleteDirectoryButton").then((mod) => mod.DeleteDirectoryButton),
3334
{ loading: () => <CircularProgress size="1rem" /> },
3435
);
36+
const RenameButton = dynamic<RenameButtonProps>(
37+
() => import("./buttons/RenameButton").then((mod) => mod.RenameButton),
38+
{ loading: () => <CircularProgress size="1rem" /> },
39+
);
3540
const CreateDatasetFromFileButton = dynamic<CreateDatasetFromFileButtonProps>(
3641
() =>
3742
import("./buttons/CreateDatasetFromFileButton").then((mod) => mod.CreateDatasetFromFileButton),
@@ -71,6 +76,7 @@ export const FileActions = ({ file }: FileActionsProps) => {
7176
projectId={project.project_id}
7277
type={isFile ? "file" : "directory"}
7378
/>
79+
7480
{/* Actions for files only */}
7581

7682
{/* Managed files are "detached" */}
@@ -101,6 +107,15 @@ export const FileActions = ({ file }: FileActionsProps) => {
101107
/>
102108
)}
103109

110+
{!isManagedFile && (
111+
<RenameButton
112+
disabled={!isProjectAdminOrEditor}
113+
path={file.fullPath}
114+
projectId={project.project_id}
115+
type={isFile ? "file" : "directory"}
116+
/>
117+
)}
118+
104119
{!!isFile && (
105120
<DownloadButton
106121
href={
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { useState } from "react";
2+
3+
import {
4+
getGetFilesQueryKey,
5+
useMoveFileInProject,
6+
useMovePath,
7+
} from "@squonk/data-manager-client/file-and-path";
8+
import { getGetProjectQueryKey } from "@squonk/data-manager-client/project";
9+
10+
import DriveFileRenameOutlineRoundedIcon from "@mui/icons-material/DriveFileRenameOutlineRounded";
11+
import { Box, IconButton, type IconButtonProps, Tooltip } from "@mui/material";
12+
import { useQueryClient } from "@tanstack/react-query";
13+
import { Field } from "formik";
14+
import { TextField } from "formik-mui";
15+
16+
import { FormikModalWrapper } from "../../../components/modals/FormikModalWrapper";
17+
import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
18+
19+
// removed type from IconButtonProps as it's also defined there
20+
export interface RenameButtonProps extends Omit<IconButtonProps, "type"> {
21+
projectId: string;
22+
type: "directory" | "file";
23+
path: string;
24+
}
25+
26+
export const RenameButton = ({ projectId, type, path, ...buttonProps }: RenameButtonProps) => {
27+
const { mutateAsync: moveFile } = useMoveFileInProject();
28+
const { mutateAsync: moveDirectory } = useMovePath();
29+
30+
const [open, setOpen] = useState(false);
31+
const initialValues = {
32+
dstPath: path,
33+
};
34+
35+
const { enqueueSnackbar, enqueueError } = useEnqueueError();
36+
37+
const queryClient = useQueryClient();
38+
39+
const handleSubmit = async ({ dstPath }: typeof initialValues) => {
40+
try {
41+
// E.g. for a type="file": /path/to/file.txt -> /new/path/to/file.txt
42+
const fileName = path.split("/").pop() as string; // file.txt
43+
const srcPath = "/" + path.split("/").slice(0, -1).join("/"); // /path/to
44+
const dstPathStem = "/" + dstPath.split("/").slice(0, -1).join("/"); // /new/path/to
45+
const dstFile = dstPath.split("/").pop() as string; // file.txt
46+
47+
await (type === "directory"
48+
? moveDirectory({
49+
params: {
50+
project_id: projectId,
51+
dst_path: "/" + dstPath,
52+
src_path: "/" + path,
53+
},
54+
})
55+
: moveFile({
56+
params: {
57+
project_id: projectId,
58+
file: fileName,
59+
src_path: srcPath,
60+
dst_path: dstPathStem,
61+
dst_file: dstFile,
62+
},
63+
}));
64+
65+
enqueueSnackbar({ message: "Renamed and/or moved", variant: "success" });
66+
67+
await Promise.allSettled([
68+
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(projectId) }),
69+
queryClient.invalidateQueries({
70+
queryKey: getGetFilesQueryKey({ project_id: projectId, path: srcPath }),
71+
}),
72+
queryClient.invalidateQueries({
73+
queryKey: getGetFilesQueryKey({ project_id: projectId, path: dstPathStem }),
74+
}),
75+
]);
76+
77+
setOpen(false);
78+
} catch (error) {
79+
enqueueError(error);
80+
}
81+
};
82+
83+
return (
84+
<>
85+
<Tooltip title="Rename / Move">
86+
<IconButton {...buttonProps} size="small" onClick={() => setOpen(true)}>
87+
<DriveFileRenameOutlineRoundedIcon />
88+
</IconButton>
89+
</Tooltip>
90+
<FormikModalWrapper
91+
id={`rename-${path}`}
92+
initialValues={initialValues}
93+
open={open}
94+
submitText="Rename / Move"
95+
title="Rename / Move"
96+
onClose={() => setOpen(false)}
97+
onSubmit={handleSubmit}
98+
>
99+
<Box p={1}>
100+
<Field
101+
autoFocus
102+
component={TextField}
103+
label="Destination Path"
104+
name="dstPath"
105+
sx={{ minWidth: "300px" }}
106+
/>
107+
</Box>
108+
</FormikModalWrapper>
109+
</>
110+
);
111+
};

utils/text.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const nullEmptyString = (str: string | null | undefined) => (str === "" ? null : str);

0 commit comments

Comments
 (0)