Skip to content

Commit

Permalink
Merge pull request #46 from Longridge-High-School/feat-attachments-to…
Browse files Browse the repository at this point in the history
…-docs

feat: attach files to documents
  • Loading branch information
Arcath authored Oct 14, 2024
2 parents 57a3562 + 35b8a8a commit 9193071
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 5 deletions.
48 changes: 46 additions & 2 deletions app/routes/app.documents.$document._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand All @@ -27,15 +31,28 @@ 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<Attachment>

return json({
user,
document,
code,
canWrite,
attachments: attachments.filter(v => v !== null)
})
}

export const meta: MetaFunction<typeof loader> = ({data}) => {
return [{title: pageTitle('Document', data!.document.title)}]
}

const DocumentView = () => {
const {document, code} = useLoaderData<typeof loader>()
const {document, code, canWrite, attachments} = useLoaderData<typeof loader>()

return (
<div className="grid grid-cols-4 gap-4">
Expand All @@ -44,6 +61,33 @@ const DocumentView = () => {
<MDXComponent code={code} />
</div>
<div>
<h3 className="border-b border-b-gray-200 text-xl font-light mb-4">
Attachments
</h3>
<div className="flex flex-wrap gap-2">
{attachments.map(({uri, originalFileName}) => {
return (
<a
key={uri}
href={uri}
download={originalFileName}
className="bg-gray-300 p-2 rounded inline-block"
>
💾 {originalFileName}
</a>
)
})}
</div>
{canWrite ? (
<LinkButton
to={`/app/documents/${document.id}/attach`}
className="bg-info text-sm mt-4"
>
Manage Attachments
</LinkButton>
) : (
''
)}
<h3 className="border-b border-b-gray-200 text-xl font-light mb-4">
Revision History
</h3>
Expand Down
155 changes: 155 additions & 0 deletions app/routes/app.documents.$document.attach.tsx
Original file line number Diff line number Diff line change
@@ -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'

Check warning on line 17 in app/routes/app.documents.$document.attach.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'/home/runner/work/net-doc/net-doc/app/lib/components/button.tsx' imported multiple times
import {getUploadMetaData} from '~/lib/utils/upload-handler.server'

Check warning on line 18 in app/routes/app.documents.$document.attach.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'/home/runner/work/net-doc/net-doc/app/lib/utils/upload-handler.server.ts' imported multiple times
import {getUploadHandler} from '~/lib/utils/upload-handler.server'

Check warning on line 19 in app/routes/app.documents.$document.attach.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'/home/runner/work/net-doc/net-doc/app/lib/utils/upload-handler.server.ts' imported multiple times
import {AButton} from '~/lib/components/button'

Check warning on line 20 in app/routes/app.documents.$document.attach.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'/home/runner/work/net-doc/net-doc/app/lib/components/button.tsx' imported multiple times

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<Attachment>
).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<typeof loader> = ({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<Attachment>

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<typeof loader>()

return (
<div className="grid grid-cols-4 gap-4">
<div className="entry col-span-3">
<h2>Attachments</h2>
<form method="POST" encType="multipart/form-data">
<table>
<thead>
<tr>
<th>URI</th>
<th>File Name</th>
<th>File Type</th>
<th></th>
</tr>
</thead>
<tbody>
{attachments.map(({uri, originalFileName, type}, i) => {
return (
<tr key={uri}>
<td>{uri}</td>
<td>{originalFileName}</td>
<td>{type}</td>
<td>
<AButton href={`?delete=${i}`}>🗑️</AButton>
</td>
</tr>
)
})}
</tbody>
<tfoot>
<td colSpan={2}>
<Input type="file" name="file" />
</td>
<td colSpan={2}>
<Button className="bg-success">Upload</Button>
</td>
</tfoot>
</table>
</form>
</div>
</div>
)
}

export default AttachToDocumentPage
8 changes: 8 additions & 0 deletions app/routes/app.documents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 4 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down

0 comments on commit 9193071

Please sign in to comment.