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