Showing
7 changed files
with
168 additions
and
26 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) => { |
| 39 | + try { | ||
| 21 | await api.delete(`/items/${id}/`); | 40 | await api.delete(`/items/${id}/`); |
| 22 | await reload(); | 41 | await reload(); |
| 23 | message.info("삭제되었습니다"); | 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