Skip to content

Commit eaafed5

Browse files
authored
904 allow folder renaming (#915)
* backend patch endpoint and test * codegen * redux reducer * rename is working * temp * default name works now * black formatter * add auth wrapper to only allow delete edit when it's uer * fix default renaming
1 parent 64afb8d commit eaafed5

File tree

15 files changed

+375
-89
lines changed

15 files changed

+375
-89
lines changed

backend/app/models/folders.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ class FolderBase(BaseModel):
1212
name: str = "N/A"
1313

1414

15+
class FolderPatch(BaseModel):
16+
name: Optional[str]
17+
parent_folder: Optional[PydanticObjectId]
18+
19+
1520
class FolderIn(FolderBase):
1621
parent_folder: Optional[PydanticObjectId]
1722

backend/app/routers/datasets.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@
5555
StorageType,
5656
)
5757
from app.models.folder_and_file import FolderFileViewList
58-
from app.models.folders import FolderOut, FolderIn, FolderDB, FolderDBViewList
58+
from app.models.folders import (
59+
FolderOut,
60+
FolderIn,
61+
FolderDB,
62+
FolderDBViewList,
63+
FolderPatch,
64+
)
5965
from app.models.metadata import MetadataDB
6066
from app.models.pages import Paged, _get_page_query, _construct_page_metadata
6167
from app.models.thumbnails import ThumbnailDB
@@ -600,6 +606,48 @@ async def _delete_nested_folders(parent_folder_id):
600606
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
601607

602608

609+
@router.get("/{dataset_id}/folders/{folder_id}")
610+
async def get_folder(
611+
dataset_id: str,
612+
folder_id: str,
613+
allow: bool = Depends(Authorization("viewer")),
614+
):
615+
if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None:
616+
if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None:
617+
return folder.dict()
618+
else:
619+
raise HTTPException(status_code=404, detail=f"Folder {folder_id} not found")
620+
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
621+
622+
623+
@router.patch("/{dataset_id}/folders/{folder_id}", response_model=FolderOut)
624+
async def patch_folder(
625+
dataset_id: str,
626+
folder_id: str,
627+
folder_info: FolderPatch,
628+
user=Depends(get_current_user),
629+
allow: bool = Depends(Authorization("editor")),
630+
):
631+
if await DatasetDB.get(PydanticObjectId(dataset_id)) is not None:
632+
if (folder := await FolderDB.get(PydanticObjectId(folder_id))) is not None:
633+
# update folder
634+
if folder_info.name is not None:
635+
folder.name = folder_info.name
636+
# allow moving folder around within the hierarchy
637+
if folder_info.parent_folder is not None:
638+
if (
639+
await FolderDB.get(PydanticObjectId(folder_info.parent_folder))
640+
is not None
641+
):
642+
folder.parent_folder = folder_info.parent_folder
643+
folder.modified = datetime.datetime.utcnow()
644+
await folder.save()
645+
return folder.dict()
646+
else:
647+
raise HTTPException(status_code=404, detail=f"Folder {folder_id} not found")
648+
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")
649+
650+
603651
# new endpoint for /{dataset_id}/local_files
604652
# new endpoint for /{dataset_id}/remote_files
605653

backend/app/tests/test_folders.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,20 @@ def test_create_nested(client: TestClient, headers: dict):
3030
)
3131
assert response.status_code == 200
3232
assert len(response.json()["data"]) == 1
33+
34+
35+
def test_rename_folder(client: TestClient, headers: dict):
36+
dataset_id = create_dataset(client, headers).get("id")
37+
38+
# create folder
39+
folder_id = create_folder(client, headers, dataset_id, "original name").get("id")
40+
41+
# rename
42+
response = client.patch(
43+
f"{settings.API_V2_STR}/datasets/{dataset_id}/folders/{folder_id}",
44+
json={"name": "edited name"},
45+
headers=headers,
46+
)
47+
assert response.status_code == 200
48+
assert response.json().get("id") is not None
49+
assert response.json().get("name") == "edited name"

frontend/src/actions/dataset.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,51 @@ export function folderAdded(datasetId, folderName, parentFolder = null) {
293293
};
294294
}
295295

296+
export const FOLDER_UPDATED = "FOLDER_UPDATED";
297+
298+
export function updateFolder(datasetId, folderId, formData) {
299+
return (dispatch) => {
300+
return V2.DatasetsService.patchFolderApiV2DatasetsDatasetIdFoldersFolderIdPatch(
301+
datasetId,
302+
folderId,
303+
formData
304+
)
305+
.then((json) => {
306+
dispatch({
307+
type: FOLDER_UPDATED,
308+
folder: json,
309+
receivedAt: Date.now(),
310+
});
311+
})
312+
.catch((reason) => {
313+
dispatch(
314+
handleErrors(reason, updateFolder(datasetId, folderId, formData))
315+
);
316+
});
317+
};
318+
}
319+
320+
export const GET_FOLDER = "GET_FOLDER";
321+
322+
export function getFolder(datasetId, folderId) {
323+
return (dispatch) => {
324+
return V2.DatasetsService.getFolderApiV2DatasetsDatasetIdFoldersFolderIdGet(
325+
datasetId,
326+
folderId
327+
)
328+
.then((json) => {
329+
dispatch({
330+
type: GET_FOLDER,
331+
folder: json,
332+
receivedAt: Date.now(),
333+
});
334+
})
335+
.catch((reason) => {
336+
dispatch(handleErrors(reason, getFolder(datasetId, folderId)));
337+
});
338+
};
339+
}
340+
296341
export const GET_FOLDER_PATH = "GET_FOLDER_PATH";
297342

298343
export function fetchFolderPath(folderId) {

frontend/src/actions/folder.js

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,6 @@
11
import { V2 } from "../openapi";
22
import { handleErrors } from "./common";
33

4-
export const FOLDER_ADDED = "FOLDER_ADDED";
5-
6-
export function folderAdded(datasetId, folderName, parentFolder = null) {
7-
return (dispatch) => {
8-
const folder = { name: folderName, parent_folder: parentFolder };
9-
return V2.DatasetsService.addFolderApiV2DatasetsDatasetIdFoldersPost(
10-
datasetId,
11-
folder
12-
)
13-
.then((json) => {
14-
dispatch({
15-
type: FOLDER_ADDED,
16-
folder: json,
17-
receivedAt: Date.now(),
18-
});
19-
})
20-
.catch((reason) => {
21-
dispatch(
22-
handleErrors(reason, folderAdded(datasetId, folderName, parentFolder))
23-
);
24-
});
25-
};
26-
}
27-
284
export const GET_FOLDER_PATH = "GET_FOLDER_PATH";
295

306
export function fetchFolderPath(folderId) {

frontend/src/components/datasets/EditNameModal.tsx

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,69 @@
1-
import React, {useState} from "react";
1+
import React, { useState } from "react";
22

33
import {
44
Button,
55
Container,
66
Dialog,
7-
DialogActions, DialogContent,
8-
DialogTitle, TextField
7+
DialogActions,
8+
DialogContent,
9+
DialogTitle,
10+
TextField,
911
} from "@mui/material";
1012

1113
import LoadingOverlay from "react-loading-overlay-ts";
1214

13-
import {useDispatch, useSelector,} from "react-redux";
14-
import {DatasetIn} from "../../openapi/v2";
15-
import {updateDataset} from "../../actions/dataset";
16-
import {RootState} from "../../types/data";
17-
15+
import { useDispatch, useSelector } from "react-redux";
16+
import { DatasetIn } from "../../openapi/v2";
17+
import { updateDataset } from "../../actions/dataset";
18+
import { RootState } from "../../types/data";
1819

1920
type EditNameModalProps = {
20-
datasetId: string
21-
handleClose:(open:boolean) => void,
21+
datasetId: string;
22+
handleClose: (open: boolean) => void;
2223
open: boolean;
23-
}
24+
};
2425

2526
export default function EditNameModal(props: EditNameModalProps) {
26-
const {datasetId, open, handleClose} = props;
27+
const { datasetId, open, handleClose } = props;
2728
const dispatch = useDispatch();
28-
const editDataset = (datasetId: string | undefined, formData: DatasetIn) => dispatch(updateDataset(datasetId, formData));
29-
29+
const editDataset = (datasetId: string | undefined, formData: DatasetIn) =>
30+
dispatch(updateDataset(datasetId, formData));
3031

3132
const about = useSelector((state: RootState) => state.dataset.about);
3233

3334
const [loading, setLoading] = useState(false);
3435
const [name, setName] = useState(about["name"]);
3536

36-
37-
3837
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
3938
setName(event.target.value);
4039
};
4140

4241
const onSave = async () => {
4342
setLoading(true);
44-
editDataset(datasetId, {"name": name});
43+
editDataset(datasetId, { name: name });
4544
setName("");
4645
setLoading(false);
4746
handleClose(true);
4847
};
4948

5049
return (
5150
<Container>
52-
<LoadingOverlay
53-
active={loading}
54-
spinner
55-
text="Saving..."
56-
>
51+
<LoadingOverlay active={loading} spinner text="Saving...">
5752
<Dialog open={open} onClose={handleClose} fullWidth={true}>
5853
<DialogTitle>Rename Dataset</DialogTitle>
5954
<DialogContent>
60-
<TextField
61-
id="outlined-name"
62-
variant="standard"
63-
fullWidth
64-
defaultValue={about["name"]}
65-
onChange={handleChange}
66-
/>
55+
<TextField
56+
id="outlined-name"
57+
variant="standard"
58+
fullWidth
59+
defaultValue={about["name"]}
60+
onChange={handleChange}
61+
/>
6762
</DialogContent>
6863
<DialogActions>
69-
<Button variant="contained" onClick={onSave} disabled={name == ""}>Save</Button>
64+
<Button variant="contained" onClick={onSave} disabled={name == ""}>
65+
Save
66+
</Button>
7067
<Button onClick={handleClose}>Cancel</Button>
7168
</DialogActions>
7269
</Dialog>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useState } from "react";
2+
3+
import {
4+
Button,
5+
Container,
6+
Dialog,
7+
DialogActions,
8+
DialogContent,
9+
DialogTitle,
10+
TextField,
11+
} from "@mui/material";
12+
13+
import LoadingOverlay from "react-loading-overlay-ts";
14+
15+
import { useDispatch } from "react-redux";
16+
import { updateFolder as updateFolderAction } from "../../actions/dataset";
17+
import { FolderPatch } from "../../openapi/v2";
18+
19+
type EditNameModalProps = {
20+
datasetId: string;
21+
folderId: string;
22+
initialFolderName: string;
23+
handleClose: () => void;
24+
open: boolean;
25+
};
26+
27+
export default function EditFolderNameModal(props: EditNameModalProps) {
28+
const { datasetId, folderId, initialFolderName, open, handleClose } = props;
29+
const dispatch = useDispatch();
30+
const updateFolder = (
31+
datasetId: string | undefined,
32+
folderId: string | undefined,
33+
formData: FolderPatch
34+
) => dispatch(updateFolderAction(datasetId, folderId, formData));
35+
36+
const [loading, setLoading] = useState(false);
37+
const [name, setName] = useState();
38+
const [defaultName, setDefaultName] = useState(initialFolderName);
39+
40+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
41+
setName(event.target.value);
42+
};
43+
44+
const onSave = async () => {
45+
setLoading(true);
46+
updateFolder(datasetId, folderId, { name: name });
47+
setName("");
48+
setDefaultName(name);
49+
setLoading(false);
50+
handleClose();
51+
};
52+
53+
return (
54+
<Container>
55+
<LoadingOverlay active={loading} spinner text="Saving...">
56+
<Dialog open={open} onClose={handleClose} fullWidth={true}>
57+
<DialogTitle>Rename Folder</DialogTitle>
58+
<DialogContent>
59+
<TextField
60+
id="outlined-name"
61+
variant="standard"
62+
fullWidth
63+
defaultValue={defaultName}
64+
onChange={handleChange}
65+
/>
66+
</DialogContent>
67+
<DialogActions>
68+
<Button variant="contained" onClick={onSave} disabled={name == ""}>
69+
Save
70+
</Button>
71+
<Button onClick={handleClose}>Cancel</Button>
72+
</DialogActions>
73+
</Dialog>
74+
</LoadingOverlay>
75+
</Container>
76+
);
77+
}

frontend/src/components/files/FilesTable.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import FolderIcon from "@mui/icons-material/Folder";
1212
import { theme } from "../../theme";
1313
import { parseDate } from "../../utils/common";
1414
import { FilesTableFileEntry } from "./FilesTableFileEntry";
15-
import FolderMenu from "./FolderMenu";
1615
import { FileOut, FolderOut } from "../../openapi/v2";
16+
import { useSelector } from "react-redux";
17+
import { RootState } from "../../types/data";
18+
import FolderMenu from "./FolderMenu";
19+
import { AuthWrapper } from "../auth/AuthWrapper";
1720

1821
type FilesTableProps = {
1922
datasetId: string | undefined;
@@ -70,6 +73,10 @@ export default function FilesTable(props: FilesTableProps) {
7073
history(`/public/datasets/${datasetId}?folder=${selectedFolderId}`);
7174
};
7275

76+
const datasetRole = useSelector(
77+
(state: RootState) => state.dataset.datasetRole
78+
);
79+
7380
return (
7481
<TableContainer component={Paper}>
7582
<Table sx={{ minWidth: 650 }} aria-label="simple table">
@@ -107,7 +114,13 @@ export default function FilesTable(props: FilesTableProps) {
107114
<TableCell align="right">&nbsp;</TableCell>
108115
<TableCell align="right">&nbsp;</TableCell>
109116
<TableCell align="right">
110-
<FolderMenu folder={item} />
117+
{/*owner, editor can delete and edit folder*/}
118+
<AuthWrapper
119+
currRole={datasetRole.role}
120+
allowedRoles={["owner", "editor"]}
121+
>
122+
<FolderMenu folder={item} />
123+
</AuthWrapper>
111124
</TableCell>
112125
</TableRow>
113126
) : (

0 commit comments

Comments
 (0)