mirror of
https://github.com/stonith404/pingvin-share.git
synced 2025-02-19 01:55:48 -05:00
feat: ability to add and delete files of existing share (#306)
* feat(share): delete file api, revert complete share api. * feat(share): share edit page. * feat(share): Modify the DropZone title of the edit sharing UI. * feat(share): i18n for edit share. (en, zh) * feat(share): allow creator get share by id. * feat(share): add edit button in account/shares. * style(share): lint. * chore: some minor adjustments. * refactor: run formatter * refactor: remove unused return --------- Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
parent
e377ed10e1
commit
98380e2d48
15 changed files with 493 additions and 36 deletions
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
|
@ -81,4 +82,14 @@ export class FileController {
|
|||
|
||||
return new StreamableFile(file.file);
|
||||
}
|
||||
|
||||
@Delete(":fileId")
|
||||
@SkipThrottle()
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async remove(
|
||||
@Param("fileId") fileId: string,
|
||||
@Param("shareId") shareId: string,
|
||||
) {
|
||||
await this.fileService.remove(shareId, fileId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,6 +124,18 @@ export class FileService {
|
|||
};
|
||||
}
|
||||
|
||||
async remove(shareId: string, fileId: string) {
|
||||
const fileMetaData = await this.prisma.file.findUnique({
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (!fileMetaData) throw new NotFoundException("File not found");
|
||||
|
||||
fs.unlinkSync(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
||||
|
||||
await this.prisma.file.delete({ where: { id: fileId } });
|
||||
}
|
||||
|
||||
async deleteAllFiles(shareId: string) {
|
||||
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
|
||||
recursive: true,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
|
@ -7,12 +6,21 @@ import {
|
|||
import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { JwtGuard } from "../../auth/guard/jwt.guard";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class ShareOwnerGuard implements CanActivate {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
export class ShareOwnerGuard extends JwtGuard {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
private prisma: PrismaService,
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
if (!(await super.canActivate(context))) return false;
|
||||
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
const shareId = Object.prototype.hasOwnProperty.call(
|
||||
request.params,
|
||||
|
|
|
@ -43,6 +43,12 @@ export class ShareController {
|
|||
return new ShareDTO().from(await this.shareService.get(id));
|
||||
}
|
||||
|
||||
@Get(":id/from-owner")
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async getFromOwner(@Param("id") id: string) {
|
||||
return new ShareDTO().from(await this.shareService.get(id));
|
||||
}
|
||||
|
||||
@Get(":id/metaData")
|
||||
@UseGuards(ShareSecurityGuard)
|
||||
async getMetaData(@Param("id") id: string) {
|
||||
|
@ -62,12 +68,6 @@ export class ShareController {
|
|||
);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@UseGuards(JwtGuard, ShareOwnerGuard)
|
||||
async remove(@Param("id") id: string) {
|
||||
await this.shareService.remove(id);
|
||||
}
|
||||
|
||||
@Post(":id/complete")
|
||||
@HttpCode(202)
|
||||
@UseGuards(CreateShareGuard, ShareOwnerGuard)
|
||||
|
@ -78,6 +78,18 @@ export class ShareController {
|
|||
);
|
||||
}
|
||||
|
||||
@Delete(":id/complete")
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async revertComplete(@Param("id") id: string) {
|
||||
return new ShareDTO().from(await this.shareService.revertComplete(id));
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async remove(@Param("id") id: string) {
|
||||
await this.shareService.remove(id);
|
||||
}
|
||||
|
||||
@Throttle(10, 60)
|
||||
@Get("isShareIdAvailable/:id")
|
||||
async isShareIdAvailable(@Param("id") id: string) {
|
||||
|
|
|
@ -182,6 +182,13 @@ export class ShareService {
|
|||
});
|
||||
}
|
||||
|
||||
async revertComplete(id: string) {
|
||||
return this.prisma.share.update({
|
||||
where: { id },
|
||||
data: { uploadLocked: false, isZipReady: false },
|
||||
});
|
||||
}
|
||||
|
||||
async getSharesByUser(userId: string) {
|
||||
const shares = await this.prisma.share.findMany({
|
||||
where: {
|
||||
|
|
|
@ -33,10 +33,12 @@ const useStyles = createStyles((theme) => ({
|
|||
}));
|
||||
|
||||
const Dropzone = ({
|
||||
title,
|
||||
isUploading,
|
||||
maxShareSize,
|
||||
showCreateUploadModalCallback,
|
||||
}: {
|
||||
title?: string;
|
||||
isUploading: boolean;
|
||||
maxShareSize: number;
|
||||
showCreateUploadModalCallback: (files: FileUpload[]) => void;
|
||||
|
@ -78,7 +80,7 @@ const Dropzone = ({
|
|||
<TbCloudUpload size={50} />
|
||||
</Group>
|
||||
<Text align="center" weight={700} size="lg" mt="xl">
|
||||
<FormattedMessage id="upload.dropzone.title" />
|
||||
{title || <FormattedMessage id="upload.dropzone.title" />}
|
||||
</Text>
|
||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||
<FormattedMessage
|
||||
|
|
238
frontend/src/components/upload/EditableUpload.tsx
Normal file
238
frontend/src/components/upload/EditableUpload.tsx
Normal file
|
@ -0,0 +1,238 @@
|
|||
import { Button, Group } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { cleanNotifications } from "@mantine/notifications";
|
||||
import { AxiosError } from "axios";
|
||||
import pLimit from "p-limit";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Dropzone from "../../components/upload/Dropzone";
|
||||
import FileList from "../../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const promiseLimit = pLimit(3);
|
||||
const chunkSize = 10 * 1024 * 1024; // 10MB
|
||||
let errorToastShown = false;
|
||||
|
||||
const EditableUpload = ({
|
||||
maxShareSize,
|
||||
shareId,
|
||||
files: savedFiles = [],
|
||||
}: {
|
||||
maxShareSize?: number;
|
||||
isReverseShare?: boolean;
|
||||
shareId: string;
|
||||
files?: FileMetaData[];
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
|
||||
const [existingFiles, setExistingFiles] =
|
||||
useState<Array<FileMetaData & { deleted?: boolean }>>(savedFiles);
|
||||
const [uploadingFiles, setUploadingFiles] = useState<FileUpload[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const existingAndUploadedFiles: FileListItem[] = useMemo(
|
||||
() => [...uploadingFiles, ...existingFiles],
|
||||
[existingFiles, uploadingFiles],
|
||||
);
|
||||
const dirty = useMemo(() => {
|
||||
return (
|
||||
existingFiles.some((file) => !!file.deleted) || !!uploadingFiles.length
|
||||
);
|
||||
}, [existingFiles, uploadingFiles]);
|
||||
|
||||
const setFiles = (files: FileListItem[]) => {
|
||||
const _uploadFiles = files.filter(
|
||||
(file) => "uploadingProgress" in file,
|
||||
) as FileUpload[];
|
||||
const _existingFiles = files.filter(
|
||||
(file) => !("uploadingProgress" in file),
|
||||
) as FileMetaData[];
|
||||
|
||||
setUploadingFiles(_uploadFiles);
|
||||
setExistingFiles(_existingFiles);
|
||||
};
|
||||
|
||||
maxShareSize ??= parseInt(config.get("share.maxSize"));
|
||||
|
||||
const uploadFiles = async (files: FileUpload[]) => {
|
||||
const fileUploadPromises = files.map(async (file, fileIndex) =>
|
||||
// Limit the number of concurrent uploads to 3
|
||||
promiseLimit(async () => {
|
||||
let fileId: string;
|
||||
|
||||
const setFileProgress = (progress: number) => {
|
||||
setUploadingFiles((files) =>
|
||||
files.map((file, callbackIndex) => {
|
||||
if (fileIndex == callbackIndex) {
|
||||
file.uploadingProgress = progress;
|
||||
}
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
setFileProgress(1);
|
||||
|
||||
let chunks = Math.ceil(file.size / chunkSize);
|
||||
|
||||
// If the file is 0 bytes, we still need to upload 1 chunk
|
||||
if (chunks == 0) chunks++;
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
|
||||
const from = chunkIndex * chunkSize;
|
||||
const to = from + chunkSize;
|
||||
const blob = file.slice(from, to);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) =>
|
||||
await shareService
|
||||
.uploadFile(
|
||||
shareId,
|
||||
event,
|
||||
{
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
},
|
||||
chunkIndex,
|
||||
chunks,
|
||||
)
|
||||
.then((response) => {
|
||||
fileId = response.id;
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject);
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
setFileProgress(((chunkIndex + 1) / chunks) * 100);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof AxiosError &&
|
||||
e.response?.data.error == "unexpected_chunk_index"
|
||||
) {
|
||||
// Retry with the expected chunk index
|
||||
chunkIndex = e.response!.data!.expectedChunkIndex - 1;
|
||||
continue;
|
||||
} else {
|
||||
setFileProgress(-1);
|
||||
// Retry after 5 seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
chunkIndex = -1;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(fileUploadPromises);
|
||||
};
|
||||
|
||||
const removeFiles = async () => {
|
||||
const removedFiles = existingFiles.filter((file) => !!file.deleted);
|
||||
|
||||
if (removedFiles.length > 0) {
|
||||
await Promise.all(
|
||||
removedFiles.map(async (file) => {
|
||||
await shareService.removeFile(shareId, file.id);
|
||||
}),
|
||||
);
|
||||
|
||||
setExistingFiles(existingFiles.filter((file) => !file.deleted));
|
||||
}
|
||||
};
|
||||
|
||||
const revertComplete = async () => {
|
||||
await shareService.revertComplete(shareId).then();
|
||||
};
|
||||
|
||||
const completeShare = async () => {
|
||||
return await shareService.completeShare(shareId);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
await revertComplete();
|
||||
await uploadFiles(uploadingFiles);
|
||||
|
||||
const hasFailed = uploadingFiles.some(
|
||||
(file) => file.uploadingProgress == -1,
|
||||
);
|
||||
|
||||
if (!hasFailed) {
|
||||
await removeFiles();
|
||||
}
|
||||
|
||||
await completeShare();
|
||||
|
||||
if (!hasFailed) {
|
||||
toast.success(t("share.edit.notify.save-success"));
|
||||
router.back();
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("share.edit.notify.generic-error"));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const appendFiles = (appendingFiles: FileUpload[]) => {
|
||||
setUploadingFiles([...appendingFiles, ...uploadingFiles]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check if there are any files that failed to upload
|
||||
const fileErrorCount = uploadingFiles.filter(
|
||||
(file) => file.uploadingProgress == -1,
|
||||
).length;
|
||||
|
||||
if (fileErrorCount > 0) {
|
||||
if (!errorToastShown) {
|
||||
toast.error(
|
||||
t("upload.notify.count-failed", { count: fileErrorCount }),
|
||||
{
|
||||
withCloseButton: false,
|
||||
autoClose: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
errorToastShown = true;
|
||||
} else {
|
||||
cleanNotifications();
|
||||
errorToastShown = false;
|
||||
}
|
||||
}, [uploadingFiles]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group position="right" mb={20}>
|
||||
<Button loading={isUploading} disabled={!dirty} onClick={() => save()}>
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
<Dropzone
|
||||
title={t("share.edit.append-upload")}
|
||||
maxShareSize={maxShareSize}
|
||||
showCreateUploadModalCallback={appendFiles}
|
||||
isUploading={isUploading}
|
||||
/>
|
||||
{existingAndUploadedFiles.length > 0 && (
|
||||
<FileList files={existingAndUploadedFiles} setFiles={setFiles} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default EditableUpload;
|
|
@ -1,41 +1,106 @@
|
|||
import { ActionIcon, Table } from "@mantine/core";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbTrash } from "react-icons/tb";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
import { GrUndo } from "react-icons/gr";
|
||||
import { FileListItem } from "../../types/File.type";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import UploadProgressIndicator from "./UploadProgressIndicator";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const FileList = ({
|
||||
const FileListRow = ({
|
||||
file,
|
||||
onRemove,
|
||||
onRestore,
|
||||
}: {
|
||||
file: FileListItem;
|
||||
onRemove?: () => void;
|
||||
onRestore?: () => void;
|
||||
}) => {
|
||||
{
|
||||
const uploadable = "uploadingProgress" in file;
|
||||
const uploading = uploadable && file.uploadingProgress !== 0;
|
||||
const removable = uploadable
|
||||
? file.uploadingProgress === 0
|
||||
: onRemove && !file.deleted;
|
||||
const restorable = onRestore && !uploadable && !!file.deleted; // maybe undefined, force boolean
|
||||
const deleted = !uploadable && !!file.deleted;
|
||||
|
||||
return (
|
||||
<tr
|
||||
style={{
|
||||
color: deleted ? "rgba(120, 120, 120, 0.5)" : "inherit",
|
||||
textDecoration: deleted ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteToHumanSizeString(+file.size)}</td>
|
||||
<td>
|
||||
{removable && (
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{uploading && (
|
||||
<UploadProgressIndicator progress={file.uploadingProgress} />
|
||||
)}
|
||||
{restorable && (
|
||||
<ActionIcon
|
||||
color="primary"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={onRestore}
|
||||
>
|
||||
<GrUndo />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const FileList = <T extends FileListItem = FileListItem>({
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
files: FileUpload[];
|
||||
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||
files: T[];
|
||||
setFiles: (files: T[]) => void;
|
||||
}) => {
|
||||
const remove = (index: number) => {
|
||||
files.splice(index, 1);
|
||||
const file = files[index];
|
||||
|
||||
if ("uploadingProgress" in file) {
|
||||
files.splice(index, 1);
|
||||
} else {
|
||||
files[index] = { ...file, deleted: true };
|
||||
}
|
||||
|
||||
setFiles([...files]);
|
||||
};
|
||||
|
||||
const restore = (index: number) => {
|
||||
const file = files[index];
|
||||
|
||||
if ("uploadingProgress" in file) {
|
||||
return;
|
||||
} else {
|
||||
files[index] = { ...file, deleted: false };
|
||||
}
|
||||
|
||||
setFiles([...files]);
|
||||
};
|
||||
|
||||
const rows = files.map((file, i) => (
|
||||
<tr key={i}>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteToHumanSizeString(file.size)}</td>
|
||||
<td>
|
||||
{file.uploadingProgress == 0 ? (
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => remove(i)}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<UploadProgressIndicator progress={file.uploadingProgress} />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<FileListRow
|
||||
key={i}
|
||||
file={file}
|
||||
onRemove={() => remove(i)}
|
||||
onRestore={() => restore(i)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
|
|
|
@ -358,6 +358,13 @@ export default {
|
|||
|
||||
// END /share/[id]
|
||||
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
|
||||
// /admin/config
|
||||
"admin.config.title": "Configuration",
|
||||
"admin.config.category.general": "General",
|
||||
|
|
|
@ -264,6 +264,12 @@ export default {
|
|||
"share.modal.file-preview.error.not-supported.title": "该文件类型不支持预览",
|
||||
"share.modal.file-preview.error.not-supported.description": "该文件类型不支持预览,请下载后打开查看",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "编辑 {shareId}",
|
||||
"share.edit.append-upload": "追加文件",
|
||||
"share.edit.notify.generic-error": "保存共享的过程中发生了错误",
|
||||
"share.edit.notify.save-success": "共享已更新成功",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "配置管理",
|
||||
"admin.config.category.general": "通用",
|
||||
|
|
|
@ -16,7 +16,7 @@ import { useModals } from "@mantine/modals";
|
|||
import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||
import { TbEdit, TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||
|
@ -110,6 +110,11 @@ const MyShares = () => {
|
|||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<Link href={`/share/${share.id}/edit`}>
|
||||
<ActionIcon color="orange" variant="light" size={25}>
|
||||
<TbEdit />
|
||||
</ActionIcon>
|
||||
</Link>
|
||||
<ActionIcon
|
||||
color="blue"
|
||||
variant="light"
|
||||
|
|
65
frontend/src/pages/share/[shareId]/edit.tsx
Normal file
65
frontend/src/pages/share/[shareId]/edit.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { LoadingOverlay } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useEffect, useState } from "react";
|
||||
import showErrorModal from "../../../components/share/showErrorModal";
|
||||
import shareService from "../../../services/share.service";
|
||||
import { Share as ShareType } from "../../../types/share.type";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import EditableUpload from "../../../components/upload/EditableUpload";
|
||||
import Meta from "../../../components/Meta";
|
||||
|
||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: { shareId: context.params!.shareId },
|
||||
};
|
||||
}
|
||||
|
||||
const Share = ({ shareId }: { shareId: string }) => {
|
||||
const t = useTranslate();
|
||||
const modals = useModals();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [share, setShare] = useState<ShareType>();
|
||||
|
||||
useEffect(() => {
|
||||
shareService
|
||||
.getFromOwner(shareId)
|
||||
.then((share) => {
|
||||
setShare(share);
|
||||
})
|
||||
.catch((e) => {
|
||||
const { error } = e.response.data;
|
||||
if (e.response.status == 404) {
|
||||
if (error == "share_removed") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.removed.title"),
|
||||
e.response.data.message,
|
||||
);
|
||||
} else {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.not-found.title"),
|
||||
t("share.error.not-found.description"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <LoadingOverlay visible />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("share.edit.title", { shareId })} />
|
||||
<EditableUpload shareId={shareId} files={share?.files || []} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Share;
|
|
@ -202,7 +202,9 @@ const Upload = ({
|
|||
showCreateUploadModalCallback={showCreateUploadModalCallback}
|
||||
isUploading={isUploading}
|
||||
/>
|
||||
{files.length > 0 && <FileList files={files} setFiles={setFiles} />}
|
||||
{files.length > 0 && (
|
||||
<FileList<FileUpload> files={files} setFiles={setFiles} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,10 +19,18 @@ const completeShare = async (id: string) => {
|
|||
return (await api.post(`shares/${id}/complete`)).data;
|
||||
};
|
||||
|
||||
const revertComplete = async (id: string) => {
|
||||
return (await api.delete(`shares/${id}/complete`)).data;
|
||||
};
|
||||
|
||||
const get = async (id: string): Promise<Share> => {
|
||||
return (await api.get(`shares/${id}`)).data;
|
||||
};
|
||||
|
||||
const getFromOwner = async (id: string): Promise<Share> => {
|
||||
return (await api.get(`shares/${id}/from-owner`)).data;
|
||||
};
|
||||
|
||||
const getMetaData = async (id: string): Promise<ShareMetaData> => {
|
||||
return (await api.get(`shares/${id}/metaData`)).data;
|
||||
};
|
||||
|
@ -63,6 +71,10 @@ const downloadFile = async (shareId: string, fileId: string) => {
|
|||
window.location.href = `${window.location.origin}/api/shares/${shareId}/files/${fileId}`;
|
||||
};
|
||||
|
||||
const removeFile = async (shareId: string, fileId: string) => {
|
||||
await api.delete(`shares/${shareId}/files/${fileId}`);
|
||||
};
|
||||
|
||||
const uploadFile = async (
|
||||
shareId: string,
|
||||
readerEvent: ProgressEvent<FileReader>,
|
||||
|
@ -121,14 +133,17 @@ const removeReverseShare = async (id: string) => {
|
|||
export default {
|
||||
create,
|
||||
completeShare,
|
||||
revertComplete,
|
||||
getShareToken,
|
||||
get,
|
||||
getFromOwner,
|
||||
remove,
|
||||
getMetaData,
|
||||
doesFileSupportPreview,
|
||||
getMyShares,
|
||||
isShareIdAvailable,
|
||||
downloadFile,
|
||||
removeFile,
|
||||
uploadFile,
|
||||
setReverseShare,
|
||||
createReverseShare,
|
||||
|
|
|
@ -7,3 +7,5 @@ export type FileMetaData = {
|
|||
name: string;
|
||||
size: string;
|
||||
};
|
||||
|
||||
export type FileListItem = FileUpload | (FileMetaData & { deleted?: boolean });
|
||||
|
|
Loading…
Add table
Reference in a new issue