mirror of
https://github.com/stonith404/pingvin-share.git
synced 2024-07-04 00:10:17 +02:00
feat: add description field to share
This commit is contained in:
parent
78dd4a7e2a
commit
8728fa5207
|
@ -39,6 +39,7 @@ model Share {
|
||||||
isZipReady Boolean @default(false)
|
isZipReady Boolean @default(false)
|
||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
expiration DateTime
|
expiration DateTime
|
||||||
|
description String?
|
||||||
|
|
||||||
creatorId String?
|
creatorId String?
|
||||||
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
creator User? @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import {
|
import {
|
||||||
IsEmail,
|
IsEmail,
|
||||||
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
Length,
|
Length,
|
||||||
Matches,
|
Matches,
|
||||||
|
MaxLength,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { ShareSecurityDTO } from "./shareSecurity.dto";
|
import { ShareSecurityDTO } from "./shareSecurity.dto";
|
||||||
|
@ -19,6 +21,10 @@ export class CreateShareDTO {
|
||||||
@IsString()
|
@IsString()
|
||||||
expiration: string;
|
expiration: string;
|
||||||
|
|
||||||
|
@MaxLength(512)
|
||||||
|
@IsOptional()
|
||||||
|
description: string;
|
||||||
|
|
||||||
@IsEmail({}, { each: true })
|
@IsEmail({}, { each: true })
|
||||||
recipients: string[];
|
recipients: string[];
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@ export class ShareDTO {
|
||||||
@Type(() => PublicUserDTO)
|
@Type(() => PublicUserDTO)
|
||||||
creator: PublicUserDTO;
|
creator: PublicUserDTO;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
description: string;
|
||||||
|
|
||||||
from(partial: Partial<ShareDTO>) {
|
from(partial: Partial<ShareDTO>) {
|
||||||
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
|
return plainToClass(ShareDTO, partial, { excludeExtraneousValues: true });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PickType } from "@nestjs/mapped-types";
|
import { PickType } from "@nestjs/mapped-types";
|
||||||
import { UserDTO } from "./user.dto";
|
import { UserDTO } from "./user.dto";
|
||||||
|
|
||||||
export class PublicUserDTO extends PickType(UserDTO, ["email"] as const) {}
|
export class PublicUserDTO extends PickType(UserDTO, ["username"] as const) {}
|
||||||
|
|
|
@ -9,35 +9,10 @@ const FileList = ({
|
||||||
shareId,
|
shareId,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
files: any[];
|
files?: any[];
|
||||||
shareId: string;
|
shareId: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const rows = files.map((file) => (
|
|
||||||
<tr key={file.name}>
|
|
||||||
<td>{file.name}</td>
|
|
||||||
<td>{byteStringToHumanSizeString(file.size)}</td>
|
|
||||||
<td>
|
|
||||||
{file.uploadingState ? (
|
|
||||||
file.uploadingState != "finished" ? (
|
|
||||||
<Loader size={22} />
|
|
||||||
) : (
|
|
||||||
<TbCircleCheck color="green" size={22} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<ActionIcon
|
|
||||||
size={25}
|
|
||||||
onClick={async () => {
|
|
||||||
await shareService.downloadFile(shareId, file.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TbDownload />
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -47,7 +22,34 @@ const FileList = ({
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{isLoading ? skeletonRows : rows}</tbody>
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? skeletonRows
|
||||||
|
: files!.map((file) => (
|
||||||
|
<tr key={file.name}>
|
||||||
|
<td>{file.name}</td>
|
||||||
|
<td>{byteStringToHumanSizeString(file.size)}</td>
|
||||||
|
<td>
|
||||||
|
{file.uploadingState ? (
|
||||||
|
file.uploadingState != "finished" ? (
|
||||||
|
<Loader size={22} />
|
||||||
|
) : (
|
||||||
|
<TbCircleCheck color="green" size={22} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<ActionIcon
|
||||||
|
size={25}
|
||||||
|
onClick={async () => {
|
||||||
|
await shareService.downloadFile(shareId, file.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TbDownload />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
Select,
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
Textarea,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
@ -22,7 +23,7 @@ import { useState } from "react";
|
||||||
import { TbAlertCircle } from "react-icons/tb";
|
import { TbAlertCircle } from "react-icons/tb";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import shareService from "../../../services/share.service";
|
import shareService from "../../../services/share.service";
|
||||||
import { ShareSecurity } from "../../../types/share.type";
|
import { CreateShare } from "../../../types/share.type";
|
||||||
import ExpirationPreview from "../ExpirationPreview";
|
import ExpirationPreview from "../ExpirationPreview";
|
||||||
|
|
||||||
const showCreateUploadModal = (
|
const showCreateUploadModal = (
|
||||||
|
@ -32,12 +33,7 @@ const showCreateUploadModal = (
|
||||||
allowUnauthenticatedShares: boolean;
|
allowUnauthenticatedShares: boolean;
|
||||||
enableEmailRecepients: boolean;
|
enableEmailRecepients: boolean;
|
||||||
},
|
},
|
||||||
uploadCallback: (
|
uploadCallback: (createShare: CreateShare) => void
|
||||||
id: string,
|
|
||||||
expiration: string,
|
|
||||||
recipients: string[],
|
|
||||||
security: ShareSecurity
|
|
||||||
) => void
|
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={4}>Share</Title>,
|
title: <Title order={4}>Share</Title>,
|
||||||
|
@ -54,12 +50,7 @@ const CreateUploadModalBody = ({
|
||||||
uploadCallback,
|
uploadCallback,
|
||||||
options,
|
options,
|
||||||
}: {
|
}: {
|
||||||
uploadCallback: (
|
uploadCallback: (createShare: CreateShare) => void;
|
||||||
id: string,
|
|
||||||
expiration: string,
|
|
||||||
recipients: string[],
|
|
||||||
security: ShareSecurity
|
|
||||||
) => void;
|
|
||||||
options: {
|
options: {
|
||||||
isUserSignedIn: boolean;
|
isUserSignedIn: boolean;
|
||||||
allowUnauthenticatedShares: boolean;
|
allowUnauthenticatedShares: boolean;
|
||||||
|
@ -88,6 +79,7 @@ const CreateUploadModalBody = ({
|
||||||
recipients: [] as string[],
|
recipients: [] as string[],
|
||||||
password: undefined,
|
password: undefined,
|
||||||
maxViews: undefined,
|
maxViews: undefined,
|
||||||
|
description: undefined,
|
||||||
expiration_num: 1,
|
expiration_num: 1,
|
||||||
expiration_unit: "-days",
|
expiration_unit: "-days",
|
||||||
never_expires: false,
|
never_expires: false,
|
||||||
|
@ -116,9 +108,15 @@ const CreateUploadModalBody = ({
|
||||||
const expiration = form.values.never_expires
|
const expiration = form.values.never_expires
|
||||||
? "never"
|
? "never"
|
||||||
: form.values.expiration_num + form.values.expiration_unit;
|
: form.values.expiration_num + form.values.expiration_unit;
|
||||||
uploadCallback(values.link, expiration, values.recipients, {
|
uploadCallback({
|
||||||
password: values.password,
|
id: values.link,
|
||||||
maxViews: values.maxViews,
|
expiration: expiration,
|
||||||
|
recipients: values.recipients,
|
||||||
|
description: values.description,
|
||||||
|
security: {
|
||||||
|
password: values.password,
|
||||||
|
maxViews: values.maxViews,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
modals.closeAll();
|
modals.closeAll();
|
||||||
}
|
}
|
||||||
|
@ -258,6 +256,18 @@ const CreateUploadModalBody = ({
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
)}
|
)}
|
||||||
|
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
|
||||||
|
<Accordion.Control>Description</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Stack align="stretch">
|
||||||
|
<Textarea
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Note for the recepients"
|
||||||
|
{...form.getInputProps("description")}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
|
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
|
||||||
<Accordion.Control>Security options</Accordion.Control>
|
<Accordion.Control>Security options</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Group } from "@mantine/core";
|
import { Box, Group, Text, Title } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
@ -8,6 +8,7 @@ import FileList from "../../components/share/FileList";
|
||||||
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
|
import showEnterPasswordModal from "../../components/share/showEnterPasswordModal";
|
||||||
import showErrorModal from "../../components/share/showErrorModal";
|
import showErrorModal from "../../components/share/showErrorModal";
|
||||||
import shareService from "../../services/share.service";
|
import shareService from "../../services/share.service";
|
||||||
|
import { Share as ShareType } from "../../types/share.type";
|
||||||
|
|
||||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
return {
|
return {
|
||||||
|
@ -17,7 +18,7 @@ export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
const Share = ({ shareId }: { shareId: string }) => {
|
const Share = ({ shareId }: { shareId: string }) => {
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const [files, setFiles] = useState<any[]>([]);
|
const [share, setShare] = useState<ShareType>();
|
||||||
|
|
||||||
const getShareToken = async (password?: string) => {
|
const getShareToken = async (password?: string) => {
|
||||||
await shareService
|
await shareService
|
||||||
|
@ -41,7 +42,7 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||||
shareService
|
shareService
|
||||||
.get(shareId)
|
.get(shareId)
|
||||||
.then((share) => {
|
.then((share) => {
|
||||||
setFiles(share.files);
|
setShare(share);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
const { error } = e.response.data;
|
const { error } = e.response.data;
|
||||||
|
@ -77,12 +78,16 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||||
title={`Share ${shareId}`}
|
title={`Share ${shareId}`}
|
||||||
description="Look what I've shared with you."
|
description="Look what I've shared with you."
|
||||||
/>
|
/>
|
||||||
{files.length > 1 && (
|
|
||||||
<Group position="right" mb="lg">
|
<Group position="apart" mb="lg">
|
||||||
<DownloadAllButton shareId={shareId} />
|
<Box style={{ maxWidth: "70%" }}>
|
||||||
</Group>
|
<Title order={3}>{share?.id}</Title>
|
||||||
)}
|
<Text size="sm">{share?.description}</Text>
|
||||||
<FileList files={files} shareId={shareId} isLoading={files.length == 0} />
|
</Box>
|
||||||
|
{share?.files.length > 1 && <DownloadAllButton shareId={shareId} />}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<FileList files={share?.files} shareId={shareId} isLoading={!share} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,10 +13,10 @@ import useConfig from "../hooks/config.hook";
|
||||||
import useUser from "../hooks/user.hook";
|
import useUser from "../hooks/user.hook";
|
||||||
import shareService from "../services/share.service";
|
import shareService from "../services/share.service";
|
||||||
import { FileUpload } from "../types/File.type";
|
import { FileUpload } from "../types/File.type";
|
||||||
import { Share, ShareSecurity } from "../types/share.type";
|
import { CreateShare, Share } from "../types/share.type";
|
||||||
import toast from "../utils/toast.util";
|
import toast from "../utils/toast.util";
|
||||||
|
|
||||||
let share: Share;
|
let createdShare: Share;
|
||||||
const promiseLimit = pLimit(3);
|
const promiseLimit = pLimit(3);
|
||||||
|
|
||||||
const Upload = () => {
|
const Upload = () => {
|
||||||
|
@ -28,12 +28,7 @@ const Upload = () => {
|
||||||
const [files, setFiles] = useState<FileUpload[]>([]);
|
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||||
const [isUploading, setisUploading] = useState(false);
|
const [isUploading, setisUploading] = useState(false);
|
||||||
|
|
||||||
const uploadFiles = async (
|
const uploadFiles = async (share: CreateShare) => {
|
||||||
id: string,
|
|
||||||
expiration: string,
|
|
||||||
recipients: string[],
|
|
||||||
security: ShareSecurity
|
|
||||||
) => {
|
|
||||||
setisUploading(true);
|
setisUploading(true);
|
||||||
try {
|
try {
|
||||||
setFiles((files) =>
|
setFiles((files) =>
|
||||||
|
@ -42,7 +37,8 @@ const Upload = () => {
|
||||||
return file;
|
return file;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
share = await shareService.create(id, expiration, recipients, security);
|
createdShare = await shareService.create(share);
|
||||||
|
|
||||||
const uploadPromises = files.map((file, i) => {
|
const uploadPromises = files.map((file, i) => {
|
||||||
// Callback to indicate current upload progress
|
// Callback to indicate current upload progress
|
||||||
const progressCallBack = (progress: number) => {
|
const progressCallBack = (progress: number) => {
|
||||||
|
@ -91,9 +87,9 @@ const Upload = () => {
|
||||||
toast.error(`${fileErrorCount} file(s) failed to upload. Try again.`);
|
toast.error(`${fileErrorCount} file(s) failed to upload. Try again.`);
|
||||||
} else {
|
} else {
|
||||||
shareService
|
shareService
|
||||||
.completeShare(share.id)
|
.completeShare(createdShare.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
showCompletedUploadModal(modals, share);
|
showCompletedUploadModal(modals, createdShare);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import {
|
import {
|
||||||
|
CreateShare,
|
||||||
MyShare,
|
MyShare,
|
||||||
Share,
|
Share,
|
||||||
ShareMetaData,
|
ShareMetaData,
|
||||||
ShareSecurity,
|
|
||||||
} from "../types/share.type";
|
} from "../types/share.type";
|
||||||
import api from "./api.service";
|
import api from "./api.service";
|
||||||
|
|
||||||
const create = async (
|
const create = async (share: CreateShare) => {
|
||||||
id: string,
|
const { id, expiration, recipients, security, description } = share;
|
||||||
expiration: string,
|
return (
|
||||||
recipients: string[],
|
await api.post("shares", {
|
||||||
security?: ShareSecurity
|
id,
|
||||||
) => {
|
expiration,
|
||||||
return (await api.post("shares", { id, expiration, recipients, security }))
|
recipients,
|
||||||
.data;
|
security,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeShare = async (id: string) => {
|
const completeShare = async (id: string) => {
|
||||||
|
|
|
@ -4,9 +4,18 @@ export type Share = {
|
||||||
id: string;
|
id: string;
|
||||||
files: any;
|
files: any;
|
||||||
creator: User;
|
creator: User;
|
||||||
|
description?: string;
|
||||||
expiration: Date;
|
expiration: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateShare = {
|
||||||
|
id: string;
|
||||||
|
description?: string;
|
||||||
|
recipients: string[];
|
||||||
|
expiration: string;
|
||||||
|
security: ShareSecurity;
|
||||||
|
};
|
||||||
|
|
||||||
export type ShareMetaData = {
|
export type ShareMetaData = {
|
||||||
id: string;
|
id: string;
|
||||||
isZipReady: boolean;
|
isZipReady: boolean;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user