김재형

Implement file move

import React, { useState } from "react";
import { Popconfirm, Popover, Button, message } from "antd";
import { FileItem } from "./useFileList";
import styles from "./FileItemActions.module.scss";
import { FileListPopover } from "./FileListPopover";
export type FileItemActionsProps = {
item: FileItem;
onMove: (id: number, to: number) => void;
onDelete: (id: number) => void;
};
export function FileItemActions({
item,
onMove,
onDelete,
}: FileItemActionsProps) {
const [move, setMove] = useState<boolean>(false);
return (
<div className={styles.actions}>
<Button type="link" size="small">
이름 변경
</Button>
<Button type="link" size="small">
공유
</Button>
<Popover
title="이동할 폴더를 선택하세요"
content={
<FileListPopover
root={item.parent}
onSelect={(to: number) => {
if (to === item.parent) {
return message.error("같은 폴더로는 이동할 수 없습니다");
}
onMove(item.id, to);
setMove(false);
}}
onCancel={() => setMove(false)}
/>
}
trigger="click"
visible={move}
onVisibleChange={setMove}
>
<Button type="link" size="small">
이동
</Button>
</Popover>
<Button type="link" size="small">
복사
</Button>
<Popconfirm
title="정말로 삭제하시겠습니까?"
onConfirm={() => onDelete(item.id)}
okText="삭제"
cancelText="취소"
>
<Button type="link" size="small">
삭제
</Button>
</Popconfirm>
</div>
);
}
......@@ -3,9 +3,3 @@
justify-content: space-between;
margin-bottom: 20px;
}
.actions {
a {
margin: 0 4px;
}
}
......
import React, { useCallback } from "react";
import { Table, Popconfirm, message } from "antd";
import { Table, message, Button } from "antd";
import { ColumnsType } from "antd/lib/table";
import filesize from "filesize";
......@@ -7,6 +7,7 @@ import { useParams } from "react-router-dom";
import { useFileList, FileItem } from "./useFileList";
import { useApi } from "util/useApi";
import { FileListItem } from "./FileListItem";
import { FileItemActions } from "./FileItemActions";
import styles from "./FileList.module.scss";
......@@ -16,11 +17,32 @@ export function FileList() {
const api = useApi();
const handleMove = useCallback(
async (id: number, to: number) => {
try {
const body = new URLSearchParams();
body.set("parent", to.toString(10));
await api.post(`/items/${id}/move/`, { body });
await reload();
message.info("이동되었습니다");
} catch {
message.error("파일 이동에 실패했습니다");
}
},
[api, reload]
);
const handleDelete = useCallback(
async (id: number) => {
await api.delete(`/items/${id}/`);
await reload();
message.info("삭제되었습니다");
try {
await api.delete(`/items/${id}/`);
await reload();
message.info("삭제되었습니다");
} catch {
message.error("파일 삭제에 실패했습니다");
}
},
[api, reload]
);
......@@ -46,14 +68,19 @@ export function FileList() {
<div>
<div className={styles.header}>
<div>{data.parent !== null && <h3>{data.name}</h3>}</div>
<div className={styles.actions}>
<a>파일 업로드</a>
<a>새 폴더</a>
<div>
<Button type="link" size="small">
파일 업로드
</Button>
<Button type="link" size="small">
새 폴더
</Button>
</div>
</div>
<Table
rowKey="id"
columns={getColumns({
handleMove,
handleDelete,
})}
dataSource={list}
......@@ -64,10 +91,14 @@ export function FileList() {
}
type GetColumnsParams = {
handleMove: (id: number, to: number) => void;
handleDelete: (id: number) => void;
};
function getColumns({ handleDelete }: GetColumnsParams): ColumnsType<FileItem> {
function getColumns({
handleMove,
handleDelete,
}: GetColumnsParams): ColumnsType<FileItem> {
return [
{
title: "파일명",
......@@ -87,22 +118,14 @@ function getColumns({ handleDelete }: GetColumnsParams): ColumnsType<FileItem> {
title: "",
key: "action",
dataIndex: "",
width: 200,
width: 300,
render: (__: any, item) =>
item.is_folder ? null : (
<div className={styles.actions}>
<a>공유</a>
<a>이동</a>
<a>복사</a>
<Popconfirm
title="정말로 삭제하시겠습니까?"
onConfirm={() => handleDelete(item.id)}
okText="삭제"
cancelText="취소"
>
<a>삭제</a>
</Popconfirm>
</div>
<FileItemActions
item={item}
onMove={handleMove}
onDelete={handleDelete}
/>
),
},
];
......
.list {
list-style: none;
padding-left: 0;
}
import React, { useState } from "react";
import { useFileList } from "./useFileList";
import { Button } from "antd";
import styles from "./FileListPopover.module.scss";
export type FileListPopoverProps = {
root: number;
onSelect: (id: number) => void;
onCancel?: () => void;
};
export function FileListPopover({
root,
onSelect,
onCancel,
}: FileListPopoverProps) {
const [id, setId] = useState<number>(root);
const { data } = useFileList(id);
if (!data) {
return null;
}
const list = data.list
.filter((item) => item.is_folder)
.map((item) => ({ id: item.id, name: item.name }));
if (data.parent !== null) {
list.unshift({ id: data.parent, name: ".." });
}
return (
<div>
<div>{data.name}</div>
<ul className={styles.list}>
{list.map((item) => (
<li key={item.id}>
<Button type="link" size="small" onClick={() => setId(item.id)}>
{item.name}
</Button>
</li>
))}
</ul>
<div className="ant-popover-buttons">
<Button size="small" onClick={onCancel}>
취소
</Button>
<Button type="primary" size="small" onClick={() => onSelect(id)}>
선택
</Button>
</div>
</div>
);
}
......@@ -24,7 +24,7 @@ export interface FileItem {
id: number;
}
export function useFileList(id: string) {
export function useFileList(id: string | number) {
const [data, setData] = useState<FileListData | null>(null);
const reload = useCallback(async () => {
......