Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: attach files to documents #46

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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