folder control

This commit is contained in:
Takeshi Kimata
2024-02-23 20:40:14 +09:00
parent 25bf3390dc
commit af83abc232
10 changed files with 428 additions and 18 deletions

View File

@@ -36,8 +36,12 @@ app.use('/projects', projectsDeleteRoute);
// "/folders" // "/folders"
const foldersIndexRoute = require('./routes/folders/index')(sequelize); const foldersIndexRoute = require('./routes/folders/index')(sequelize);
const foldersNewRoute = require('./routes/folders/new')(sequelize); const foldersNewRoute = require('./routes/folders/new')(sequelize);
const foldersEditRoute = require('./routes/folders/edit')(sequelize);
const foldersDeleteRoute = require('./routes/folders/delete')(sequelize);
app.use('/folders', foldersIndexRoute); app.use('/folders', foldersIndexRoute);
app.use('/folders', foldersNewRoute); app.use('/folders', foldersNewRoute);
app.use('/folders', foldersEditRoute);
app.use('/folders', foldersDeleteRoute);
// "/runs" // "/runs"
const runsIndexRoute = require('./routes/runs/index')(sequelize); const runsIndexRoute = require('./routes/runs/index')(sequelize);

View File

@@ -0,0 +1,25 @@
const express = require("express");
const router = express.Router();
const defineFolder = require("../../models/folders");
const { DataTypes } = require("sequelize");
module.exports = function (sequelize) {
const Folder = defineFolder(sequelize, DataTypes);
router.delete("/:folderId", async (req, res) => {
const folderId = req.params.folderId;
try {
const folder = await Folder.findByPk(folderId);
if (!folder) {
return res.status(404).send("Folder not found");
}
await folder.destroy();
res.status(204).send();
} catch (error) {
console.error(error);
res.status(500).send("Internal Server Error");
}
});
return router;
};

View File

@@ -0,0 +1,31 @@
const express = require("express");
const router = express.Router();
const defineFolder = require("../../models/folders");
const { DataTypes } = require("sequelize");
module.exports = function (sequelize) {
const Folder = defineFolder(sequelize, DataTypes);
router.put("/:folderId", async (req, res) => {
const folderId = req.params.folderId;
const { name, detail, projectId, parentFolderId } = req.body;
try {
const folder = await Folder.findByPk(folderId);
if (!folder) {
return res.status(404).send("Folder not found");
}
await folder.update({
name,
detail,
projectId,
parentFolderId,
});
res.json(folder);
} catch (error) {
console.error(error);
res.status(500).send("Internal Server Error");
}
});
return router;
};

View File

@@ -0,0 +1,140 @@
"use client";
import React from "react";
import { useState, useEffect } from "react";
import {
Button,
Input,
Textarea,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@nextui-org/react";
import { FolderType } from "./layout";
type Props = {
isOpen: boolean;
editingFolder: FolderType;
onCancel: () => void;
onSubmit: (name: string, detail: string) => void;
};
export function FolderDialog({
isOpen,
editingFolder,
onCancel,
onSubmit,
}: Props) {
const [folderName, setFolderName] = useState({
text: editingFolder ? editingFolder.name : "",
isValid: false,
errorMessage: "",
});
const [folderDetail, setFolderDetail] = useState({
text: editingFolder ? editingFolder.detail : "",
isValid: false,
errorMessage: "",
});
useEffect(() => {
if (editingFolder) {
setFolderName({
...folderName,
text: editingFolder.name,
});
setFolderDetail({
...folderDetail,
text: editingFolder.detail ? editingFolder.detail : "",
});
} else {
setFolderName({
...folderName,
text: "",
});
setFolderDetail({
...folderDetail,
text: "",
});
}
}, [editingFolder]);
const clear = () => {
setFolderName({
isValid: false,
text: "",
errorMessage: "",
});
setFolderDetail({
isValid: false,
text: "",
errorMessage: "",
});
};
const validate = () => {
if (!folderName.text) {
setFolderName({
text: "",
isValid: false,
errorMessage: "Please enter folder name",
});
return;
}
onSubmit(folderName.text, folderDetail.text);
clear();
};
return (
<Modal
isOpen={isOpen}
onOpenChange={() => {
onCancel();
}}
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">Folder</ModalHeader>
<ModalBody>
<Input
type="text"
label="Folder Name"
value={folderName.text}
isInvalid={folderName.isValid}
errorMessage={folderName.errorMessage}
onChange={(e) => {
setFolderName({
...folderName,
text: e.target.value,
});
}}
/>
<Textarea
label="Folder Detail"
value={folderDetail.text}
isInvalid={folderDetail.isValid}
errorMessage={folderDetail.errorMessage}
onChange={(e) => {
setFolderDetail({
...folderDetail,
text: e.target.value,
});
}}
/>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onCancel}>
Close
</Button>
<Button color="primary" onPress={validate}>
{editingFolder ? "Update" : "Create"}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@@ -0,0 +1,38 @@
import {
Button,
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from "@nextui-org/react";
import { MoreVertical } from "lucide-react";
import { FolderType } from "./layout";
export default function FolderEditMenu(
folder: FolderType,
onEditClick: (folder: FolderType) => void,
onDeleteClick: (folderId: number) => void
) {
return (
<Dropdown>
<DropdownTrigger>
<Button isIconOnly size="sm" className="bg-transparent rounded-full">
<MoreVertical size={16} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Static Actions">
<DropdownItem key="edit" onClick={() => onEditClick(folder)}>
Edit folder
</DropdownItem>
<DropdownItem
key="delete"
className="text-danger"
color="danger"
onClick={() => onDeleteClick(folder.id)}
>
Delete folder
</DropdownItem>
</DropdownMenu>
</Dropdown>
);
}

View File

@@ -4,9 +4,21 @@ import Config from "@/config/config";
const apiServer = Config.apiServer; const apiServer = Config.apiServer;
import { Button, Listbox, ListboxItem } from "@nextui-org/react"; import { Button, Listbox, ListboxItem } from "@nextui-org/react";
import { Folder, Plus } from "lucide-react"; import { Folder, Plus } from "lucide-react";
import { FolderDialog } from "./folder-dialog";
import FolderEditMenu from "./folder-edit-menu";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import useGetCurrentIds from "@/utils/useGetCurrentIds"; import useGetCurrentIds from "@/utils/useGetCurrentIds";
export type FolderType = {
id: number;
name: string;
detail: string;
projectId: number;
parentFolderId: number | null;
createdAt: string;
updatedAt: string;
};
/** /**
* fetch folder records * fetch folder records
* *
@@ -34,21 +46,165 @@ async function fetchFolders(url) {
} }
} }
export default function ProjectsLayout({ /**
* Create project
*/
async function createFolder(
name: string,
detail: string,
projectId: number,
parentFolderId: number
) {
const newFolderData = {
name: name,
detail: detail,
projectId: projectId,
parentFolderId: parentFolderId,
};
const fetchOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newFolderData),
};
const url = `${apiServer}/folders`;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error creating new project:", error);
throw error;
}
}
/**
* Update folder
*/
async function updateFolder(
folderId: number,
name: string,
detail: string,
projectId: number,
parentFolderId: number
) {
const updateFolderData = {
name: name,
detail: detail,
projectId: projectId,
parentFolderId: parentFolderId,
};
const fetchOptions = {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updateFolderData),
};
const url = `${apiServer}/folders/${folderId}`;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error updating project:", error);
throw error;
}
}
/**
* Delete folder
*/
async function deleteFolder(folderId: number) {
const fetchOptions = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
};
const url = `${apiServer}/folders/${folderId}`;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
} catch (error) {
console.error("Error deleting project:", error);
throw error;
}
}
export default function FoldersLayout({
children, children,
params, params,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
params: { projectId: string }; params: { projectId: number };
}) { }) {
const [folders, setFolders] = useState([]); const [folders, setFolders] = useState([]);
const [selectedFolder, setSelectedFolder] = useState({ const [selectedFolder, setSelectedFolder] = useState<FolderType>({});
id: null,
name: "",
});
const url = `${apiServer}/folders?projectId=${params.projectId}`; const url = `${apiServer}/folders?projectId=${params.projectId}`;
const { folderId } = useGetCurrentIds(); const { folderId } = useGetCurrentIds();
const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false);
const [editingFolder, setEditingProject] = useState<FolderType | null>(null);
const openDialogForCreate = () => {
setIsFolderDialogOpen(true);
setEditingProject(null);
};
const closeDialog = () => {
setIsFolderDialogOpen(false);
setEditingProject(null);
};
const onSubmit = async (name: string, detail: string) => {
if (editingFolder) {
const updatedProject = await updateFolder(
editingFolder.id,
name,
detail,
params.projectId,
null
);
const updatedProjects = folders.map((project) =>
project.id === updatedProject.id ? updatedProject : project
);
setFolders(updatedProjects);
} else {
const newProject = await createFolder(
name,
detail,
params.projectId,
null
);
setFolders([...folders, newProject]);
}
closeDialog();
};
const onEditClick = (folder: FolderType) => {
setEditingProject(folder);
setIsFolderDialogOpen(true);
};
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
try { try {
@@ -76,6 +232,7 @@ export default function ProjectsLayout({
size="sm" size="sm"
variant="bordered" variant="bordered"
className="m-2" className="m-2"
onClick={openDialogForCreate}
> >
New Folder New Folder
</Button> </Button>
@@ -94,6 +251,14 @@ export default function ProjectsLayout({
? "bg-gray-300" ? "bg-gray-300"
: "" : ""
} }
endContent={
<FolderEditMenu
key={index}
folder={folder}
onEditClick={onEditClick}
onDeleteClick={() => console.log("TODO")}
/>
}
> >
{folder.name} {folder.name}
</ListboxItem> </ListboxItem>
@@ -101,9 +266,18 @@ export default function ProjectsLayout({
</Listbox> </Listbox>
</div> </div>
<div className="flex-grow w-full"> <div className="flex-grow w-full">
<h3 className="border-b-1 w-full font-bold p-3">{selectedFolder ? selectedFolder.name : "Select Folder"}</h3> <h3 className="border-b-1 w-full font-bold p-3">
{selectedFolder ? selectedFolder.name : "Select Folder"}
</h3>
{children} {children}
</div> </div>
<FolderDialog
isOpen={isFolderDialogOpen}
editingFolder={editingFolder}
onCancel={closeDialog}
onSubmit={onSubmit}
/>
</div> </div>
); );
} }

View File

@@ -5,5 +5,3 @@ export default function Page() {
</> </>
); );
} }
// TODO Automatically navigate to head folder page

View File

@@ -1,6 +1,6 @@
import Sidebar from "./sidebar"; import Sidebar from "./sidebar";
export default function ProjectsLayout({ export default function SidebarLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;

View File

@@ -182,12 +182,12 @@ export default function ProjectsPage() {
closeDialog(); closeDialog();
}; };
const onEditClicked = (project: ProjectType) => { const onEditClick = (project: ProjectType) => {
setEditingProject(project); setEditingProject(project);
setIsProjectDialogOpen(true); setIsProjectDialogOpen(true);
}; };
const onDeleteClicked = async (projectId: number) => { const onDeleteClick = async (projectId: number) => {
try { try {
await deleteProject(projectId); await deleteProject(projectId);
setProjects(projects.filter((project) => project.id !== projectId)); setProjects(projects.filter((project) => project.id !== projectId));
@@ -214,8 +214,8 @@ export default function ProjectsPage() {
<ProjectCard <ProjectCard
key={index} key={index}
project={project} project={project}
onEditClicked={onEditClicked} onEditClick={onEditClick}
onDeleteClicked={onDeleteClicked} onDeleteClick={onDeleteClick}
/> />
))} ))}
</div> </div>

View File

@@ -13,7 +13,7 @@ import {
} from "@nextui-org/react"; } from "@nextui-org/react";
import { MoreVertical } from "lucide-react"; import { MoreVertical } from "lucide-react";
export function ProjectCard({ project, onEditClicked, onDeleteClicked }) { export function ProjectCard({ project, onEditClick, onDeleteClick }) {
return ( return (
<Card className="w-[250px]"> <Card className="w-[250px]">
<CardHeader className="flex gap-3 h-[50px] justify-between text-ellipsis overflow-hidden"> <CardHeader className="flex gap-3 h-[50px] justify-between text-ellipsis overflow-hidden">
@@ -29,14 +29,14 @@ export function ProjectCard({ project, onEditClicked, onDeleteClicked }) {
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="Static Actions"> <DropdownMenu aria-label="Static Actions">
<DropdownItem key="edit" onClick={() => onEditClicked(project)}> <DropdownItem key="edit" onClick={() => onEditClick(project)}>
Edit project Edit project
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
key="delete" key="delete"
className="text-danger" className="text-danger"
color="danger" color="danger"
onClick={() => onDeleteClicked(project.id)} onClick={() => onDeleteClick(project.id)}
> >
Delete project Delete project
</DropdownItem> </DropdownItem>