Showing
7 changed files
with
171 additions
and
29 deletions
File mode changed
frontend/src/file/FileItemActions.tsx
0 → 100644
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 | +} |
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 | ]; | ... | ... |
frontend/src/file/FileListPopover.tsx
0 → 100644
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 () => { | ... | ... |
-
Please register or login to post a comment