Compare commits

..

5 Commits

Author SHA1 Message Date
Elias Schneider
b49ec93c54 release: 0.20.1 2023-11-05 12:38:13 +01:00
Elias Schneider
e6584322fa chore(translations): update translations via Crowdin (#310)
* New translations en-us.ts (French)

* New translations en-us.ts (Spanish)

* New translations en-us.ts (Danish)

* New translations en-us.ts (German)

* New translations en-us.ts (Finnish)

* New translations en-us.ts (Japanese)

* New translations en-us.ts (Polish)

* New translations en-us.ts (Russian)

* New translations en-us.ts (Serbian (Cyrillic))

* New translations en-us.ts (Portuguese, Brazilian)

* New translations en-us.ts (Thai)

* New translations en-us.ts (Dutch, Belgium)

* New translations en-us.ts (Polish)

* New translations en-us.ts (Portuguese, Brazilian)

* New translations en-us.ts (German)
2023-11-05 12:37:48 +01:00
Elias Schneider
1138cd02b0 fix: share information text color in light mode 2023-11-05 12:36:42 +01:00
Elias Schneider
1ba8d0cbd1 release: 0.20.0 2023-11-04 20:40:20 +01:00
Ivan Li
98380e2d48 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>
2023-11-04 20:39:58 +01:00
34 changed files with 595 additions and 52 deletions

View File

@@ -1,3 +1,17 @@
## [0.20.1](https://github.com/stonith404/pingvin-share/compare/v0.20.0...v0.20.1) (2023-11-05)
### Bug Fixes
* share information text color in light mode ([1138cd0](https://github.com/stonith404/pingvin-share/commit/1138cd02b0b6ac1d71c4dbc2808110c672237190))
## [0.20.0](https://github.com/stonith404/pingvin-share/compare/v0.19.2...v0.20.0) (2023-11-04)
### Features
* ability to add and delete files of existing share ([#306](https://github.com/stonith404/pingvin-share/issues/306)) ([98380e2](https://github.com/stonith404/pingvin-share/commit/98380e2d48cc8ffa831d9b69cf5c0e8a40e28862))
## [0.19.2](https://github.com/stonith404/pingvin-share/compare/v0.19.1...v0.19.2) (2023-11-03) ## [0.19.2](https://github.com/stonith404/pingvin-share/compare/v0.19.1...v0.19.2) (2023-11-03)

View File

@@ -1,12 +1,12 @@
{ {
"name": "pingvin-share-backend", "name": "pingvin-share-backend",
"version": "0.19.2", "version": "0.20.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pingvin-share-backend", "name": "pingvin-share-backend",
"version": "0.19.2", "version": "0.20.1",
"dependencies": { "dependencies": {
"@nestjs/cache-manager": "^2.1.0", "@nestjs/cache-manager": "^2.1.0",
"@nestjs/common": "^10.1.2", "@nestjs/common": "^10.1.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "pingvin-share-backend", "name": "pingvin-share-backend",
"version": "0.19.2", "version": "0.20.1",
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
"dev": "cross-env NODE_ENV=development nest start --watch", "dev": "cross-env NODE_ENV=development nest start --watch",

View File

@@ -1,6 +1,7 @@
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
Param, Param,
Post, Post,
@@ -81,4 +82,14 @@ export class FileController {
return new StreamableFile(file.file); 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);
}
} }

View File

@@ -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) { async deleteAllFiles(shareId: string) {
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, { await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
recursive: true, recursive: true,

View File

@@ -1,5 +1,4 @@
import { import {
CanActivate,
ExecutionContext, ExecutionContext,
Injectable, Injectable,
NotFoundException, NotFoundException,
@@ -7,12 +6,21 @@ import {
import { User } from "@prisma/client"; import { User } from "@prisma/client";
import { Request } from "express"; import { Request } from "express";
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from "src/prisma/prisma.service";
import { JwtGuard } from "../../auth/guard/jwt.guard";
import { ConfigService } from "src/config/config.service";
@Injectable() @Injectable()
export class ShareOwnerGuard implements CanActivate { export class ShareOwnerGuard extends JwtGuard {
constructor(private prisma: PrismaService) {} constructor(
configService: ConfigService,
private prisma: PrismaService,
) {
super(configService);
}
async canActivate(context: ExecutionContext) { async canActivate(context: ExecutionContext) {
if (!(await super.canActivate(context))) return false;
const request: Request = context.switchToHttp().getRequest(); const request: Request = context.switchToHttp().getRequest();
const shareId = Object.prototype.hasOwnProperty.call( const shareId = Object.prototype.hasOwnProperty.call(
request.params, request.params,

View File

@@ -43,6 +43,12 @@ export class ShareController {
return new ShareDTO().from(await this.shareService.get(id)); 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") @Get(":id/metaData")
@UseGuards(ShareSecurityGuard) @UseGuards(ShareSecurityGuard)
async getMetaData(@Param("id") id: string) { 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") @Post(":id/complete")
@HttpCode(202) @HttpCode(202)
@UseGuards(CreateShareGuard, ShareOwnerGuard) @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) @Throttle(10, 60)
@Get("isShareIdAvailable/:id") @Get("isShareIdAvailable/:id")
async isShareIdAvailable(@Param("id") id: string) { async isShareIdAvailable(@Param("id") id: string) {

View File

@@ -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) { async getSharesByUser(userId: string) {
const shares = await this.prisma.share.findMany({ const shares = await this.prisma.share.findMany({
where: { where: {

View File

@@ -1,12 +1,12 @@
{ {
"name": "pingvin-share-frontend", "name": "pingvin-share-frontend",
"version": "0.19.2", "version": "0.20.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pingvin-share-frontend", "name": "pingvin-share-frontend",
"version": "0.19.2", "version": "0.20.1",
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0", "@emotion/server": "^11.11.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "pingvin-share-frontend", "name": "pingvin-share-frontend",
"version": "0.19.2", "version": "0.20.1",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",

View File

@@ -12,7 +12,7 @@ const showShareInformationsModal = (
modals: ModalsContextProps, modals: ModalsContextProps,
share: MyShare, share: MyShare,
appUrl: string, appUrl: string,
maxShareSize: number, maxShareSize: number
) => { ) => {
const t = translateOutsideContext(); const t = translateOutsideContext();
const link = `${appUrl}/s/${share.id}`; const link = `${appUrl}/s/${share.id}`;
@@ -36,28 +36,28 @@ const showShareInformationsModal = (
children: ( children: (
<Stack align="stretch" spacing="md"> <Stack align="stretch" spacing="md">
<Text size="sm" color="lightgray"> <Text size="sm">
<b> <b>
<FormattedMessage id="account.shares.table.id" />:{" "} <FormattedMessage id="account.shares.table.id" />:{" "}
</b> </b>
{share.id} {share.id}
</Text> </Text>
<Text size="sm" color="lightgray"> <Text size="sm">
<b> <b>
<FormattedMessage id="account.shares.table.description" />:{" "} <FormattedMessage id="account.shares.table.description" />:{" "}
</b> </b>
{share.description || "No description"} {share.description || "No description"}
</Text> </Text>
<Text size="sm" color="lightgray"> <Text size="sm">
<b> <b>
<FormattedMessage id="account.shares.table.createdAt" />:{" "} <FormattedMessage id="account.shares.table.createdAt" />:{" "}
</b> </b>
{formattedCreatedAt} {formattedCreatedAt}
</Text> </Text>
<Text size="sm" color="lightgray"> <Text size="sm">
<b> <b>
<FormattedMessage id="account.shares.table.expiresAt" />:{" "} <FormattedMessage id="account.shares.table.expiresAt" />:{" "}
</b> </b>
@@ -66,7 +66,7 @@ const showShareInformationsModal = (
<Divider /> <Divider />
<CopyTextField link={link} /> <CopyTextField link={link} />
<Divider /> <Divider />
<Text size="sm" color="lightgray"> <Text size="sm">
<b> <b>
<FormattedMessage id="account.shares.table.size" />:{" "} <FormattedMessage id="account.shares.table.size" />:{" "}
</b> </b>
@@ -76,7 +76,7 @@ const showShareInformationsModal = (
<Flex align="center" justify="center"> <Flex align="center" justify="center">
{shareSize / maxShareSize < 0.1 && ( {shareSize / maxShareSize < 0.1 && (
<Text size="xs" color="lightgray" style={{ marginRight: "4px" }}> <Text size="xs" style={{ marginRight: "4px" }}>
{formattedShareSize} {formattedShareSize}
</Text> </Text>
)} )}
@@ -87,7 +87,7 @@ const showShareInformationsModal = (
size="xl" size="xl"
radius="xl" radius="xl"
/> />
<Text size="xs" color="lightgray" style={{ marginLeft: "4px" }}> <Text size="xs" style={{ marginLeft: "4px" }}>
{formattedMaxShareSize} {formattedMaxShareSize}
</Text> </Text>
</Flex> </Flex>

View File

@@ -33,10 +33,12 @@ const useStyles = createStyles((theme) => ({
})); }));
const Dropzone = ({ const Dropzone = ({
title,
isUploading, isUploading,
maxShareSize, maxShareSize,
showCreateUploadModalCallback, showCreateUploadModalCallback,
}: { }: {
title?: string;
isUploading: boolean; isUploading: boolean;
maxShareSize: number; maxShareSize: number;
showCreateUploadModalCallback: (files: FileUpload[]) => void; showCreateUploadModalCallback: (files: FileUpload[]) => void;
@@ -78,7 +80,7 @@ const Dropzone = ({
<TbCloudUpload size={50} /> <TbCloudUpload size={50} />
</Group> </Group>
<Text align="center" weight={700} size="lg" mt="xl"> <Text align="center" weight={700} size="lg" mt="xl">
<FormattedMessage id="upload.dropzone.title" /> {title || <FormattedMessage id="upload.dropzone.title" />}
</Text> </Text>
<Text align="center" size="sm" mt="xs" color="dimmed"> <Text align="center" size="sm" mt="xs" color="dimmed">
<FormattedMessage <FormattedMessage

View 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;

View File

@@ -1,41 +1,106 @@
import { ActionIcon, Table } from "@mantine/core"; import { ActionIcon, Table } from "@mantine/core";
import { Dispatch, SetStateAction } from "react";
import { TbTrash } from "react-icons/tb"; 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 { byteToHumanSizeString } from "../../utils/fileSize.util";
import UploadProgressIndicator from "./UploadProgressIndicator"; import UploadProgressIndicator from "./UploadProgressIndicator";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
const FileList = ({ const FileListRow = ({
files, file,
setFiles, onRemove,
onRestore,
}: { }: {
files: FileUpload[]; file: FileListItem;
setFiles: Dispatch<SetStateAction<FileUpload[]>>; onRemove?: () => void;
onRestore?: () => void;
}) => { }) => {
const remove = (index: number) => { {
files.splice(index, 1); const uploadable = "uploadingProgress" in file;
setFiles([...files]); const uploading = uploadable && file.uploadingProgress !== 0;
}; const removable = uploadable
const rows = files.map((file, i) => ( ? file.uploadingProgress === 0
<tr key={i}> : 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>{file.name}</td>
<td>{byteToHumanSizeString(file.size)}</td> <td>{byteToHumanSizeString(+file.size)}</td>
<td> <td>
{file.uploadingProgress == 0 ? ( {removable && (
<ActionIcon <ActionIcon
color="red" color="red"
variant="light" variant="light"
size={25} size={25}
onClick={() => remove(i)} onClick={onRemove}
> >
<TbTrash /> <TbTrash />
</ActionIcon> </ActionIcon>
) : ( )}
{uploading && (
<UploadProgressIndicator progress={file.uploadingProgress} /> <UploadProgressIndicator progress={file.uploadingProgress} />
)} )}
{restorable && (
<ActionIcon
color="primary"
variant="light"
size={25}
onClick={onRestore}
>
<GrUndo />
</ActionIcon>
)}
</td> </td>
</tr> </tr>
);
}
};
const FileList = <T extends FileListItem = FileListItem>({
files,
setFiles,
}: {
files: T[];
setFiles: (files: T[]) => void;
}) => {
const remove = (index: number) => {
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) => (
<FileListRow
key={i}
file={file}
onRemove={() => remove(i)}
onRestore={() => restore(i)}
/>
)); ));
return ( return (

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Forhåndsvisning ikke understøttet", "share.modal.file-preview.error.not-supported.title": "Forhåndsvisning ikke understøttet",
"share.modal.file-preview.error.not-supported.description": "En forhåndsvisning for thise filtype er ikke understøttet. Download venligst filen for at se den.", "share.modal.file-preview.error.not-supported.description": "En forhåndsvisning for thise filtype er ikke understøttet. Download venligst filen for at se den.",
// END /share/[id] // 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
"admin.config.title": "Konfiguration", "admin.config.title": "Konfiguration",
"admin.config.category.general": "Generelt", "admin.config.category.general": "Generelt",

View File

@@ -115,7 +115,7 @@ export default {
"account.shares.title.empty": "Es ist so leer hier 👀", "account.shares.title.empty": "Es ist so leer hier 👀",
"account.shares.description.empty": "Du hast keine Freigaben erstellt.", "account.shares.description.empty": "Du hast keine Freigaben erstellt.",
"account.shares.button.create": "Erstelle eine", "account.shares.button.create": "Erstelle eine",
"account.shares.info.title": "Teile deine Information", "account.shares.info.title": "Freigabe Informationen",
"account.shares.table.id": "ID", "account.shares.table.id": "ID",
"account.shares.table.name": "Name", "account.shares.table.name": "Name",
"account.shares.table.description": "Beschreibung", "account.shares.table.description": "Beschreibung",
@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Vorschau wird nicht unterstützt", "share.modal.file-preview.error.not-supported.title": "Vorschau wird nicht unterstützt",
"share.modal.file-preview.error.not-supported.description": "Eine Vorschau für diesen Dateityp wird nicht unterstützt. Bitte lade die Datei herunter, um sie anzuzeigen.", "share.modal.file-preview.error.not-supported.description": "Eine Vorschau für diesen Dateityp wird nicht unterstützt. Bitte lade die Datei herunter, um sie anzuzeigen.",
// END /share/[id] // 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
"admin.config.title": "Einstellungen", "admin.config.title": "Einstellungen",
"admin.config.category.general": "Allgemein", "admin.config.category.general": "Allgemein",

View File

@@ -358,6 +358,13 @@ export default {
// END /share/[id] // 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
"admin.config.title": "Configuration", "admin.config.title": "Configuration",
"admin.config.category.general": "General", "admin.config.category.general": "General",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Vista previa no disponible", "share.modal.file-preview.error.not-supported.title": "Vista previa no disponible",
"share.modal.file-preview.error.not-supported.description": "La vista previa para este tipo de archivo no está disponible. Por favor descargue el archivo para verlo.", "share.modal.file-preview.error.not-supported.description": "La vista previa para este tipo de archivo no está disponible. Por favor descargue el archivo para verlo.",
// END /share/[id] // 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
"admin.config.title": "Configuración", "admin.config.title": "Configuración",
"admin.config.category.general": "General", "admin.config.category.general": "General",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Esikatselua ei tuettu", "share.modal.file-preview.error.not-supported.title": "Esikatselua ei tuettu",
"share.modal.file-preview.error.not-supported.description": "Esikatselua thise tiedostotyypille ei tueta. Ole hyvä ja lataa tiedosto nähdäksesi sen.", "share.modal.file-preview.error.not-supported.description": "Esikatselua thise tiedostotyypille ei tueta. Ole hyvä ja lataa tiedosto nähdäksesi sen.",
// END /share/[id] // 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
"admin.config.title": "Asetukset", "admin.config.title": "Asetukset",
"admin.config.category.general": "Yleiset", "admin.config.category.general": "Yleiset",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Aperçu non supporté", "share.modal.file-preview.error.not-supported.title": "Aperçu non supporté",
"share.modal.file-preview.error.not-supported.description": "Un aperçu pour ce type de fichier n'est pas pris en charge. Veuillez télécharger le fichier pour le voir.", "share.modal.file-preview.error.not-supported.description": "Un aperçu pour ce type de fichier n'est pas pris en charge. Veuillez télécharger le fichier pour le voir.",
// END /share/[id] // 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
"admin.config.title": "Paramètres", "admin.config.title": "Paramètres",
"admin.config.category.general": "Général", "admin.config.category.general": "Général",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "プレビューに対応していません", "share.modal.file-preview.error.not-supported.title": "プレビューに対応していません",
"share.modal.file-preview.error.not-supported.description": "これらのファイルのプレビューには対応していません。ファイルをダウンロードして、直接確認してください。", "share.modal.file-preview.error.not-supported.description": "これらのファイルのプレビューには対応していません。ファイルをダウンロードして、直接確認してください。",
// END /share/[id] // 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
"admin.config.title": "設定", "admin.config.title": "設定",
"admin.config.category.general": "一般", "admin.config.category.general": "一般",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Voorbeeld niet ondersteund", "share.modal.file-preview.error.not-supported.title": "Voorbeeld niet ondersteund",
"share.modal.file-preview.error.not-supported.description": "Een voorbeeld voor dit bestandstype wordt niet ondersteund. Download het bestand om het te bekijken.", "share.modal.file-preview.error.not-supported.description": "Een voorbeeld voor dit bestandstype wordt niet ondersteund. Download het bestand om het te bekijken.",
// END /share/[id] // 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
"admin.config.title": "Configuratie", "admin.config.title": "Configuratie",
"admin.config.category.general": "Algemeen", "admin.config.category.general": "Algemeen",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Podgląd nie jest obsługiwany", "share.modal.file-preview.error.not-supported.title": "Podgląd nie jest obsługiwany",
"share.modal.file-preview.error.not-supported.description": "Podgląd dla tego typu pliku nie jest obsługiwany. Pobierz plik, aby go zobaczyć.", "share.modal.file-preview.error.not-supported.description": "Podgląd dla tego typu pliku nie jest obsługiwany. Pobierz plik, aby go zobaczyć.",
// END /share/[id] // END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edytuj {shareId}",
"share.edit.append-upload": "Dołącz plik",
"share.edit.notify.generic-error": "W trakcie zakańczania tworzenia udziału wystąpił błąd.",
"share.edit.notify.save-success": "Udział zaktualizowany pomyślnie",
// END /share/[id]/edit
// /admin/config // /admin/config
"admin.config.title": "Konfiguracja", "admin.config.title": "Konfiguracja",
"admin.config.category.general": "Ogólne", "admin.config.category.general": "Ogólne",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Visualização não suportada", "share.modal.file-preview.error.not-supported.title": "Visualização não suportada",
"share.modal.file-preview.error.not-supported.description": "Uma visualização para este tipo de arquivo não é suportada. Faça o download do arquivo para visualizá-lo.", "share.modal.file-preview.error.not-supported.description": "Uma visualização para este tipo de arquivo não é suportada. Faça o download do arquivo para visualizá-lo.",
// END /share/[id] // END /share/[id]
// /share/[id]/edit
"share.edit.title": "Editar {shareId}",
"share.edit.append-upload": "Anexar arquivo",
"share.edit.notify.generic-error": "Ocorreu um erro ao terminar seu compartilhamento.",
"share.edit.notify.save-success": "Compartilhamento atualizado com sucesso",
// END /share/[id]/edit
// /admin/config // /admin/config
"admin.config.title": "Configuração", "admin.config.title": "Configuração",
"admin.config.category.general": "Geral", "admin.config.category.general": "Geral",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Предпросмотр не поддерживается", "share.modal.file-preview.error.not-supported.title": "Предпросмотр не поддерживается",
"share.modal.file-preview.error.not-supported.description": "Предварительный просмотр этого типа файла не поддерживается. Пожалуйста, скачайте файл для его просмотра.", "share.modal.file-preview.error.not-supported.description": "Предварительный просмотр этого типа файла не поддерживается. Пожалуйста, скачайте файл для его просмотра.",
// END /share/[id] // 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
"admin.config.title": "Конфигурация", "admin.config.title": "Конфигурация",
"admin.config.category.general": "Общее", "admin.config.category.general": "Общее",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Преглед није подржан", "share.modal.file-preview.error.not-supported.title": "Преглед није подржан",
"share.modal.file-preview.error.not-supported.description": "Преглед за овај тип датотеке није подржан. Преузмите датотеку да бисте је видели.", "share.modal.file-preview.error.not-supported.description": "Преглед за овај тип датотеке није подржан. Преузмите датотеку да бисте је видели.",
// END /share/[id] // 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
"admin.config.title": "Конфигурација", "admin.config.title": "Конфигурација",
"admin.config.category.general": "Опште", "admin.config.category.general": "Опште",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "ไม่รองรับการแสดงตัวอย่าง", "share.modal.file-preview.error.not-supported.title": "ไม่รองรับการแสดงตัวอย่าง",
"share.modal.file-preview.error.not-supported.description": "ไม่รองรับการแสดงตัวอย่างสำหรับไฟล์ประเภทนี้ โปรดดาวน์โหลดไฟล์เพื่อดู", "share.modal.file-preview.error.not-supported.description": "ไม่รองรับการแสดงตัวอย่างสำหรับไฟล์ประเภทนี้ โปรดดาวน์โหลดไฟล์เพื่อดู",
// END /share/[id] // 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
"admin.config.title": "การตั้งค่า", "admin.config.title": "การตั้งค่า",
"admin.config.category.general": "ทั่วไป", "admin.config.category.general": "ทั่วไป",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "该文件类型不支持预览", "share.modal.file-preview.error.not-supported.title": "该文件类型不支持预览",
"share.modal.file-preview.error.not-supported.description": "该文件类型不支持预览,请下载后打开查看", "share.modal.file-preview.error.not-supported.description": "该文件类型不支持预览,请下载后打开查看",
// END /share/[id] // 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
"admin.config.title": "配置管理", "admin.config.title": "配置管理",
"admin.config.category.general": "通用", "admin.config.category.general": "通用",

View File

@@ -16,7 +16,7 @@ import { useModals } from "@mantine/modals";
import moment from "moment"; import moment from "moment";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; 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 { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta"; import Meta from "../../components/Meta";
import showShareInformationsModal from "../../components/account/showShareInformationsModal"; import showShareInformationsModal from "../../components/account/showShareInformationsModal";
@@ -110,6 +110,11 @@ const MyShares = () => {
</td> </td>
<td> <td>
<Group position="right"> <Group position="right">
<Link href={`/share/${share.id}/edit`}>
<ActionIcon color="orange" variant="light" size={25}>
<TbEdit />
</ActionIcon>
</Link>
<ActionIcon <ActionIcon
color="blue" color="blue"
variant="light" variant="light"

View 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;

View File

@@ -202,7 +202,9 @@ const Upload = ({
showCreateUploadModalCallback={showCreateUploadModalCallback} showCreateUploadModalCallback={showCreateUploadModalCallback}
isUploading={isUploading} isUploading={isUploading}
/> />
{files.length > 0 && <FileList files={files} setFiles={setFiles} />} {files.length > 0 && (
<FileList<FileUpload> files={files} setFiles={setFiles} />
)}
</> </>
); );
}; };

View File

@@ -19,10 +19,18 @@ const completeShare = async (id: string) => {
return (await api.post(`shares/${id}/complete`)).data; 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> => { const get = async (id: string): Promise<Share> => {
return (await api.get(`shares/${id}`)).data; 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> => { const getMetaData = async (id: string): Promise<ShareMetaData> => {
return (await api.get(`shares/${id}/metaData`)).data; 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}`; 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 ( const uploadFile = async (
shareId: string, shareId: string,
readerEvent: ProgressEvent<FileReader>, readerEvent: ProgressEvent<FileReader>,
@@ -121,14 +133,17 @@ const removeReverseShare = async (id: string) => {
export default { export default {
create, create,
completeShare, completeShare,
revertComplete,
getShareToken, getShareToken,
get, get,
getFromOwner,
remove, remove,
getMetaData, getMetaData,
doesFileSupportPreview, doesFileSupportPreview,
getMyShares, getMyShares,
isShareIdAvailable, isShareIdAvailable,
downloadFile, downloadFile,
removeFile,
uploadFile, uploadFile,
setReverseShare, setReverseShare,
createReverseShare, createReverseShare,

View File

@@ -7,3 +7,5 @@ export type FileMetaData = {
name: string; name: string;
size: string; size: string;
}; };
export type FileListItem = FileUpload | (FileMetaData & { deleted?: boolean });

View File

@@ -1,6 +1,6 @@
{ {
"name": "pingvin-share", "name": "pingvin-share",
"version": "0.19.2", "version": "0.20.1",
"scripts": { "scripts": {
"format": "cd frontend && npm run format && cd ../backend && npm run format", "format": "cd frontend && npm run format && cd ../backend && npm run format",
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint", "lint": "cd frontend && npm run lint && cd ../backend && npm run lint",