김재형

Implement file move

1 +import React, { useState } from "react";
2 +import { Popconfirm, Popover, Button, message } from "antd";
3 +import { FileItem } from "./useFileList";
4 +import styles from "./FileItemActions.module.scss";
5 +import { FileListPopover } from "./FileListPopover";
6 +
7 +export type FileItemActionsProps = {
8 + item: FileItem;
9 + onMove: (id: number, to: number) => void;
10 + onDelete: (id: number) => void;
11 +};
12 +
13 +export function FileItemActions({
14 + item,
15 + onMove,
16 + onDelete,
17 +}: FileItemActionsProps) {
18 + const [move, setMove] = useState<boolean>(false);
19 +
20 + return (
21 + <div className={styles.actions}>
22 + <Button type="link" size="small">
23 + 이름 변경
24 + </Button>
25 + <Button type="link" size="small">
26 + 공유
27 + </Button>
28 + <Popover
29 + title="이동할 폴더를 선택하세요"
30 + content={
31 + <FileListPopover
32 + root={item.parent}
33 + onSelect={(to: number) => {
34 + if (to === item.parent) {
35 + return message.error("같은 폴더로는 이동할 수 없습니다");
36 + }
37 + onMove(item.id, to);
38 + setMove(false);
39 + }}
40 + onCancel={() => setMove(false)}
41 + />
42 + }
43 + trigger="click"
44 + visible={move}
45 + onVisibleChange={setMove}
46 + >
47 + <Button type="link" size="small">
48 + 이동
49 + </Button>
50 + </Popover>
51 + <Button type="link" size="small">
52 + 복사
53 + </Button>
54 + <Popconfirm
55 + title="정말로 삭제하시겠습니까?"
56 + onConfirm={() => onDelete(item.id)}
57 + okText="삭제"
58 + cancelText="취소"
59 + >
60 + <Button type="link" size="small">
61 + 삭제
62 + </Button>
63 + </Popconfirm>
64 + </div>
65 + );
66 +}
...@@ -3,9 +3,3 @@ ...@@ -3,9 +3,3 @@
3 justify-content: space-between; 3 justify-content: space-between;
4 margin-bottom: 20px; 4 margin-bottom: 20px;
5 } 5 }
6 -
7 -.actions {
8 - a {
9 - margin: 0 4px;
10 - }
11 -}
......
1 import React, { useCallback } from "react"; 1 import React, { useCallback } from "react";
2 -import { Table, Popconfirm, message } from "antd"; 2 +import { Table, message, Button } from "antd";
3 import { ColumnsType } from "antd/lib/table"; 3 import { ColumnsType } from "antd/lib/table";
4 import filesize from "filesize"; 4 import filesize from "filesize";
5 5
...@@ -7,6 +7,7 @@ import { useParams } from "react-router-dom"; ...@@ -7,6 +7,7 @@ import { useParams } from "react-router-dom";
7 import { useFileList, FileItem } from "./useFileList"; 7 import { useFileList, FileItem } from "./useFileList";
8 import { useApi } from "util/useApi"; 8 import { useApi } from "util/useApi";
9 import { FileListItem } from "./FileListItem"; 9 import { FileListItem } from "./FileListItem";
10 +import { FileItemActions } from "./FileItemActions";
10 11
11 import styles from "./FileList.module.scss"; 12 import styles from "./FileList.module.scss";
12 13
...@@ -16,11 +17,32 @@ export function FileList() { ...@@ -16,11 +17,32 @@ export function FileList() {
16 17
17 const api = useApi(); 18 const api = useApi();
18 19
20 + const handleMove = useCallback(
21 + async (id: number, to: number) => {
22 + try {
23 + const body = new URLSearchParams();
24 + body.set("parent", to.toString(10));
25 +
26 + await api.post(`/items/${id}/move/`, { body });
27 + await reload();
28 +
29 + message.info("이동되었습니다");
30 + } catch {
31 + message.error("파일 이동에 실패했습니다");
32 + }
33 + },
34 + [api, reload]
35 + );
36 +
19 const handleDelete = useCallback( 37 const handleDelete = useCallback(
20 async (id: number) => { 38 async (id: number) => {
21 - await api.delete(`/items/${id}/`); 39 + try {
22 - await reload(); 40 + await api.delete(`/items/${id}/`);
23 - message.info("삭제되었습니다"); 41 + await reload();
42 + message.info("삭제되었습니다");
43 + } catch {
44 + message.error("파일 삭제에 실패했습니다");
45 + }
24 }, 46 },
25 [api, reload] 47 [api, reload]
26 ); 48 );
...@@ -46,14 +68,19 @@ export function FileList() { ...@@ -46,14 +68,19 @@ export function FileList() {
46 <div> 68 <div>
47 <div className={styles.header}> 69 <div className={styles.header}>
48 <div>{data.parent !== null && <h3>{data.name}</h3>}</div> 70 <div>{data.parent !== null && <h3>{data.name}</h3>}</div>
49 - <div className={styles.actions}> 71 + <div>
50 - <a>파일 업로드</a> 72 + <Button type="link" size="small">
51 - <a>새 폴더</a> 73 + 파일 업로드
74 + </Button>
75 + <Button type="link" size="small">
76 + 새 폴더
77 + </Button>
52 </div> 78 </div>
53 </div> 79 </div>
54 <Table 80 <Table
55 rowKey="id" 81 rowKey="id"
56 columns={getColumns({ 82 columns={getColumns({
83 + handleMove,
57 handleDelete, 84 handleDelete,
58 })} 85 })}
59 dataSource={list} 86 dataSource={list}
...@@ -64,10 +91,14 @@ export function FileList() { ...@@ -64,10 +91,14 @@ export function FileList() {
64 } 91 }
65 92
66 type GetColumnsParams = { 93 type GetColumnsParams = {
94 + handleMove: (id: number, to: number) => void;
67 handleDelete: (id: number) => void; 95 handleDelete: (id: number) => void;
68 }; 96 };
69 97
70 -function getColumns({ handleDelete }: GetColumnsParams): ColumnsType<FileItem> { 98 +function getColumns({
99 + handleMove,
100 + handleDelete,
101 +}: GetColumnsParams): ColumnsType<FileItem> {
71 return [ 102 return [
72 { 103 {
73 title: "파일명", 104 title: "파일명",
...@@ -87,22 +118,14 @@ function getColumns({ handleDelete }: GetColumnsParams): ColumnsType<FileItem> { ...@@ -87,22 +118,14 @@ function getColumns({ handleDelete }: GetColumnsParams): ColumnsType<FileItem> {
87 title: "", 118 title: "",
88 key: "action", 119 key: "action",
89 dataIndex: "", 120 dataIndex: "",
90 - width: 200, 121 + width: 300,
91 render: (__: any, item) => 122 render: (__: any, item) =>
92 item.is_folder ? null : ( 123 item.is_folder ? null : (
93 - <div className={styles.actions}> 124 + <FileItemActions
94 - <a>공유</a> 125 + item={item}
95 - <a>이동</a> 126 + onMove={handleMove}
96 - <a>복사</a> 127 + onDelete={handleDelete}
97 - <Popconfirm 128 + />
98 - title="정말로 삭제하시겠습니까?"
99 - onConfirm={() => handleDelete(item.id)}
100 - okText="삭제"
101 - cancelText="취소"
102 - >
103 - <a>삭제</a>
104 - </Popconfirm>
105 - </div>
106 ), 129 ),
107 }, 130 },
108 ]; 131 ];
......
1 +.list {
2 + list-style: none;
3 + padding-left: 0;
4 +}
1 +import React, { useState } from "react";
2 +import { useFileList } from "./useFileList";
3 +import { Button } from "antd";
4 +
5 +import styles from "./FileListPopover.module.scss";
6 +
7 +export type FileListPopoverProps = {
8 + root: number;
9 + onSelect: (id: number) => void;
10 + onCancel?: () => void;
11 +};
12 +
13 +export function FileListPopover({
14 + root,
15 + onSelect,
16 + onCancel,
17 +}: FileListPopoverProps) {
18 + const [id, setId] = useState<number>(root);
19 + const { data } = useFileList(id);
20 +
21 + if (!data) {
22 + return null;
23 + }
24 +
25 + const list = data.list
26 + .filter((item) => item.is_folder)
27 + .map((item) => ({ id: item.id, name: item.name }));
28 +
29 + if (data.parent !== null) {
30 + list.unshift({ id: data.parent, name: ".." });
31 + }
32 +
33 + return (
34 + <div>
35 + <div>{data.name}</div>
36 + <ul className={styles.list}>
37 + {list.map((item) => (
38 + <li key={item.id}>
39 + <Button type="link" size="small" onClick={() => setId(item.id)}>
40 + {item.name}
41 + </Button>
42 + </li>
43 + ))}
44 + </ul>
45 + <div className="ant-popover-buttons">
46 + <Button size="small" onClick={onCancel}>
47 + 취소
48 + </Button>
49 + <Button type="primary" size="small" onClick={() => onSelect(id)}>
50 + 선택
51 + </Button>
52 + </div>
53 + </div>
54 + );
55 +}
...@@ -24,7 +24,7 @@ export interface FileItem { ...@@ -24,7 +24,7 @@ export interface FileItem {
24 id: number; 24 id: number;
25 } 25 }
26 26
27 -export function useFileList(id: string) { 27 +export function useFileList(id: string | number) {
28 const [data, setData] = useState<FileListData | null>(null); 28 const [data, setData] = useState<FileListData | null>(null);
29 29
30 const reload = useCallback(async () => { 30 const reload = useCallback(async () => {
......