diff --git a/package.json b/package.json index 34b6171..81632a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zipline", - "version": "3.7.0-rc2", + "version": "3.7.0-rc3", "license": "MIT", "scripts": { "dev": "npm-run-all build:server dev:run", diff --git a/prisma/migrations/20230128183334_folders/migration.sql b/prisma/migrations/20230128183334_folders/migration.sql new file mode 100644 index 0000000..48476af --- /dev/null +++ b/prisma/migrations/20230128183334_folders/migration.sql @@ -0,0 +1,19 @@ +-- AlterTable +ALTER TABLE "File" ADD COLUMN "folderId" INTEGER; + +-- CreateTable +CREATE TABLE "Folder" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "Folder_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 62823eb..06f66df 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,9 +21,22 @@ model User { totpSecret String? domains String[] oauth OAuth[] - files File[] + files File[] urls Url[] Invite Invite[] + Folder Folder[] +} + +model Folder { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + + files File[] } enum FileNameFormat { @@ -34,30 +47,33 @@ enum FileNameFormat { } model File { - id Int @id @default(autoincrement()) - name String + id Int @id @default(autoincrement()) + name String originalName String? - mimetype String @default("image/png") - createdAt DateTime @default(now()) - expiresAt DateTime? - maxViews Int? - views Int @default(0) - favorite Boolean @default(false) - embed Boolean @default(false) - password String? - invisible InvisibleFile? - format FileNameFormat @default(RANDOM) - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - userId Int? + mimetype String @default("image/png") + createdAt DateTime @default(now()) + expiresAt DateTime? + maxViews Int? + views Int @default(0) + favorite Boolean @default(false) + embed Boolean @default(false) + password String? + invisible InvisibleFile? + format FileNameFormat @default(RANDOM) + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + userId Int? + + folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + folderId Int? } model InvisibleFile { - id Int @id @default(autoincrement()) - invis String @unique - // imageId Int @unique - fileId Int @unique - // image File @relation(fields: [imageId], references: [id], onDelete: Cascade) - file File @relation(fields: [fileId], references: [id], onDelete: Cascade) + id Int @id @default(autoincrement()) + invis String @unique + + fileId Int @unique + file File @relation(fields: [fileId], references: [id], onDelete: Cascade) } model Url { @@ -68,30 +84,33 @@ model Url { maxViews Int? views Int @default(0) invisible InvisibleUrl? - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - userId Int? + + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + userId Int? } model InvisibleUrl { id Int @id @default(autoincrement()) invis String @unique + urlId String @unique url Url @relation(fields: [urlId], references: [id], onDelete: Cascade) } model Stats { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) createdAt DateTime @default(now()) - data Json + data Json } model Invite { - id Int @id @default(autoincrement()) - code String @unique - createdAt DateTime @default(now()) - expiresAt DateTime? - used Boolean @default(false) - createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) + id Int @id @default(autoincrement()) + code String @unique + createdAt DateTime @default(now()) + expiresAt DateTime? + used Boolean @default(false) + + createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade) createdById Int } diff --git a/src/components/File.tsx b/src/components/File.tsx index 399f71d..015d9d6 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -5,6 +5,7 @@ import { LoadingOverlay, Modal, Paper, + Select, SimpleGrid, Stack, Text, @@ -13,8 +14,11 @@ import { } from '@mantine/core'; import { useClipboard } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; -import { useFileDelete, useFileFavorite } from 'lib/queries/files'; +import useFetch from 'hooks/useFetch'; +import { invalidateFiles, useFileDelete, useFileFavorite } from 'lib/queries/files'; +import { invalidateFolders, useFolders } from 'lib/queries/folders'; import { relativeTime } from 'lib/utils/client'; +import { useRouter } from 'next/router'; import { useState } from 'react'; import { CalendarIcon, @@ -30,10 +34,17 @@ import { ImageIcon, StarIcon, InfoIcon, + FolderMinusIcon, + FolderPlusIcon, } from './icons'; import MutedText from './MutedText'; import Type from './Type'; +const CREATE_FOLDER_OPTION = { + label: 'Create new folder', + value: 'create', +}; + export function FileMeta({ Icon, title, subtitle, ...other }) { return other.tooltip ? ( @@ -56,12 +67,15 @@ export function FileMeta({ Icon, title, subtitle, ...other }) { ); } -export default function File({ image, disableMediaPreview, exifEnabled }) { +export default function File({ image, disableMediaPreview, exifEnabled, refreshImages }) { const [open, setOpen] = useState(false); const [overrideRender, setOverrideRender] = useState(false); const deleteFile = useFileDelete(); const favoriteFile = useFileFavorite(); const clipboard = useClipboard(); + const router = useRouter(); + + const folders = useFolders(); const loading = deleteFile.isLoading || favoriteFile.isLoading; @@ -125,6 +139,65 @@ export default function File({ image, disableMediaPreview, exifEnabled }) { ); }; + const inFolder = image.folderId; + + const refresh = () => { + refreshImages(); + folders.refetch(); + }; + + const removeFromFolder = async () => { + const res = await useFetch('/api/user/folders/' + image.folderId, 'DELETE', { + file: Number(image.id), + }); + + refresh(); + + if (!res.error) { + showNotification({ + title: 'Removed from folder', + message: res.name, + color: 'green', + icon: , + }); + } else { + showNotification({ + title: 'Failed to remove from folder', + message: res.error, + color: 'red', + icon: , + }); + } + }; + + const addToFolder = async (t) => { + if (t === CREATE_FOLDER_OPTION.value) { + router.push('/dashboard/folders?create=' + image.id); + } else { + const res = await useFetch('/api/user/folders/' + t, 'POST', { + file: Number(image.id), + }); + + refresh(); + + if (!res.error) { + showNotification({ + title: 'Added to folder', + message: res.name, + color: 'green', + icon: , + }); + } else { + showNotification({ + title: 'Failed to add to folder', + message: res.error, + color: 'red', + icon: , + }); + } + } + }; + return ( <> setOpen(false)} title={{image.name}} size='xl'> @@ -182,9 +255,6 @@ export default function File({ image, disableMediaPreview, exifEnabled }) { {exifEnabled && ( - // - // - // )} + {inFolder && !folders.isLoading ? ( + f.id === image.folderId)?.name ?? '' + }"`} + > + + + + + ) : ( + +