1
0
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:
Elias Schneider 2022-12-12 11:54:13 +01:00
parent 78dd4a7e2a
commit 8728fa5207
10 changed files with 108 additions and 73 deletions

View File

@ -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)

View File

@ -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[];

View File

@ -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 });
} }

View File

@ -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) {}

View File

@ -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>
); );
}; };

View File

@ -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>

View File

@ -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} />
</> </>
); );
}; };

View File

@ -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(() =>

View File

@ -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) => {

View File

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