From 35b8a8afdf67e7fa77d682582d9e146f0d4e0915 Mon Sep 17 00:00:00 2001 From: Adam Laycock Date: Sun, 13 Oct 2024 21:24:56 +0100 Subject: [PATCH] feat: attach files to documents --- app/routes/app.documents.$document._index.tsx | 48 +++++- app/routes/app.documents.$document.attach.tsx | 155 ++++++++++++++++++ app/routes/app.documents.tsx | 8 + .../migration.sql | 18 ++ prisma/schema.prisma | 7 +- 5 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 app/routes/app.documents.$document.attach.tsx create mode 100644 prisma/migrations/20241013175935_add_attachments_to_document/migration.sql diff --git a/app/routes/app.documents.$document._index.tsx b/app/routes/app.documents.$document._index.tsx index c448de8..d43cc40 100644 --- a/app/routes/app.documents.$document._index.tsx +++ b/app/routes/app.documents.$document._index.tsx @@ -8,6 +8,10 @@ import {MDXComponent} from '~/lib/mdx' import {pageTitle} from '~/lib/utils/page-title' import {formatAsDateTime} from '~/lib/utils/format' import {trackRecentItem} from '~/lib/utils/recent-item' +import {LinkButton} from '~/lib/components/button' +import {can} from '~/lib/rbac.server' + +import {type Attachment} from './app.documents.$document.attach' export const loader = async ({request, params}: LoaderFunctionArgs) => { const user = await ensureUser(request, 'document:view', { @@ -27,7 +31,20 @@ export const loader = async ({request, params}: LoaderFunctionArgs) => { const code = await buildMDXBundle(document.body) - return json({user, document, code}) + const canWrite = await can(user.role, 'document:write', { + user: {role: user.role, id: user.id}, + documentId: params.document + }) + + const attachments = JSON.parse(document.attachments) as Array + + return json({ + user, + document, + code, + canWrite, + attachments: attachments.filter(v => v !== null) + }) } export const meta: MetaFunction = ({data}) => { @@ -35,7 +52,7 @@ export const meta: MetaFunction = ({data}) => { } const DocumentView = () => { - const {document, code} = useLoaderData() + const {document, code, canWrite, attachments} = useLoaderData() return (
@@ -44,6 +61,33 @@ const DocumentView = () => {
+

+ Attachments +

+
+ {attachments.map(({uri, originalFileName}) => { + return ( + + 💾 {originalFileName} + + ) + })} +
+ {canWrite ? ( + + Manage Attachments + + ) : ( + '' + )}

Revision History

diff --git a/app/routes/app.documents.$document.attach.tsx b/app/routes/app.documents.$document.attach.tsx new file mode 100644 index 0000000..edf4acd --- /dev/null +++ b/app/routes/app.documents.$document.attach.tsx @@ -0,0 +1,155 @@ +import { + type LoaderFunctionArgs, + type MetaFunction, + json, + type ActionFunctionArgs, + redirect, + unstable_parseMultipartFormData +} from '@remix-run/node' +import {useLoaderData} from '@remix-run/react' +import {basename} from 'path' +import {invariant} from '@arcath/utils' + +import {ensureUser} from '~/lib/utils/ensure-user' +import {getPrisma} from '~/lib/prisma.server' +import {pageTitle} from '~/lib/utils/page-title' +import {Input} from '~/lib/components/input' +import {Button} from '~/lib/components/button' +import {getUploadMetaData} from '~/lib/utils/upload-handler.server' +import {getUploadHandler} from '~/lib/utils/upload-handler.server' +import {AButton} from '~/lib/components/button' + +export type Attachment = { + uri: string + originalFileName: string + type: string +} + +export const loader = async ({request, params}: LoaderFunctionArgs) => { + const user = await ensureUser(request, 'document:write', { + documentId: params.document + }) + + const prisma = getPrisma() + + const document = await prisma.document.findFirstOrThrow({ + where: {id: params.document} + }) + + const attachments = ( + JSON.parse(document.attachments) as Array + ).filter(v => v !== null) + + const {searchParams} = new URL(request.url) + const del = searchParams.get('delete') + + if (del) { + delete attachments[parseInt(del)] + + await prisma.document.update({ + where: {id: params.document}, + data: {attachments: JSON.stringify(attachments)} + }) + } + + return json({ + user, + document, + attachments: attachments.filter(v => v !== null) + }) +} + +export const meta: MetaFunction = ({data}) => { + return [{title: pageTitle('Document', data!.document.title, 'Attachments')}] +} + +export const action = async ({request, params}: ActionFunctionArgs) => { + await ensureUser(request, 'document:write', { + documentId: params.document + }) + + const prisma = getPrisma() + + const document = await prisma.document.findFirstOrThrow({ + where: {id: params.document} + }) + + const uploadHandler = getUploadHandler() + + const formData = await unstable_parseMultipartFormData(request, uploadHandler) + + const file = formData.get('file') as unknown as + | {filepath: string; type: string} + | undefined + + invariant(file) + + const fileName = basename(file.filepath) + + const metaData = getUploadMetaData(fileName) + + const newAttachment: Attachment = { + uri: `/uploads/${fileName}`, + originalFileName: metaData ? metaData.originalFileName : fileName, + type: file.type + } + + const attachments = JSON.parse(document.attachments) as Array + + attachments.push(newAttachment) + + await prisma.document.update({ + where: {id: params.document}, + data: {attachments: JSON.stringify(attachments)} + }) + + return redirect(`/app/documents/${document.id}`) +} + +const AttachToDocumentPage = () => { + const {attachments} = useLoaderData() + + return ( +
+
+

Attachments

+
+ + + + + + + + + + + {attachments.map(({uri, originalFileName, type}, i) => { + return ( + + + + + + + ) + })} + + + + + +
URIFile NameFile Type
{uri}{originalFileName}{type} + 🗑️ +
+ + + +
+
+
+
+ ) +} + +export default AttachToDocumentPage diff --git a/app/routes/app.documents.tsx b/app/routes/app.documents.tsx index 3705e46..fa595e6 100644 --- a/app/routes/app.documents.tsx +++ b/app/routes/app.documents.tsx @@ -29,6 +29,14 @@ const Documents = () => { invariant(params) switch (id) { + case 'routes/app.documents.$document.attach': + return [ + { + link: `/app/documents/${params.document}`, + label: 'Back to Document', + className: 'bg-warning' + } + ] case 'routes/app.documents._index': return [ { diff --git a/prisma/migrations/20241013175935_add_attachments_to_document/migration.sql b/prisma/migrations/20241013175935_add_attachments_to_document/migration.sql new file mode 100644 index 0000000..7380b19 --- /dev/null +++ b/prisma/migrations/20241013175935_add_attachments_to_document/migration.sql @@ -0,0 +1,18 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Document" ( + "id" TEXT NOT NULL PRIMARY KEY, + "body" TEXT NOT NULL, + "title" TEXT NOT NULL, + "attachments" TEXT NOT NULL DEFAULT '[]', + "aclId" TEXT NOT NULL DEFAULT '', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Document_aclId_fkey" FOREIGN KEY ("aclId") REFERENCES "ACL" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Document" ("aclId", "body", "createdAt", "id", "title", "updatedAt") SELECT "aclId", "body", "createdAt", "id", "title", "updatedAt" FROM "Document"; +DROP TABLE "Document"; +ALTER TABLE "new_Document" RENAME TO "Document"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 267af33..0192b3b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -130,9 +130,10 @@ model Session { } model Document { - id String @id @default(uuid()) - body String - title String + id String @id @default(uuid()) + body String + title String + attachments String @default("[]") history DocumentHistory[]