From 7a75ecbafb1b155896969a4e68e3eb8ea9f6a8b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carlos=20Sans=C3=B3n?=
<57395395+csansoon@users.noreply.github.com>
Date: Wed, 24 Jul 2024 11:18:18 +0200
Subject: [PATCH] Document Editor page (#56)
---
.../src/actions/documents/getContentByPath.ts | 23 +++++
.../src/actions/documents/updateContent.ts | 25 ++++++
.../src/app/(private)/_data-access/index.ts | 15 ++++
.../[...documentPath]/route.test.ts | 12 ++-
.../_components/DocumentEditor/index.tsx | 80 ++++++++++++++++++
.../documents/[documentUuid]/layout.tsx | 2 +-
.../documents/[documentUuid]/page.tsx | 16 +++-
packages/web-ui/package.json | 1 +
.../molecules/DocumentTextEditor/editor.tsx | 83 ++++++++++++-------
.../ds/molecules/DocumentTextEditor/index.tsx | 13 ++-
packages/web-ui/src/index.ts | 7 +-
.../src/sections/Document/Editor/index.tsx | 46 ++++++++--
packages/web-ui/src/sections/index.ts | 7 ++
pnpm-lock.yaml | 12 +++
14 files changed, 292 insertions(+), 50 deletions(-)
create mode 100644 apps/web/src/actions/documents/getContentByPath.ts
create mode 100644 apps/web/src/actions/documents/updateContent.ts
create mode 100644 apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/index.tsx
create mode 100644 packages/web-ui/src/sections/index.ts
diff --git a/apps/web/src/actions/documents/getContentByPath.ts b/apps/web/src/actions/documents/getContentByPath.ts
new file mode 100644
index 000000000..49d1be1b3
--- /dev/null
+++ b/apps/web/src/actions/documents/getContentByPath.ts
@@ -0,0 +1,23 @@
+'use server'
+
+import { getDocumentByPath } from '$/app/(private)/_data-access'
+import { z } from 'zod'
+
+import { withProject } from '../procedures'
+
+export const getDocumentContentByPathAction = withProject
+ .createServerAction()
+ .input(
+ z.object({
+ commitId: z.number(),
+ path: z.string(),
+ }),
+ { type: 'json' },
+ )
+ .handler(async ({ input }) => {
+ const document = await getDocumentByPath({
+ commitId: input.commitId,
+ path: input.path,
+ })
+ return document.content
+ })
diff --git a/apps/web/src/actions/documents/updateContent.ts b/apps/web/src/actions/documents/updateContent.ts
new file mode 100644
index 000000000..eaf97f8fc
--- /dev/null
+++ b/apps/web/src/actions/documents/updateContent.ts
@@ -0,0 +1,25 @@
+'use server'
+
+import { updateDocument } from '@latitude-data/core'
+import { z } from 'zod'
+
+import { withProject } from '../procedures'
+
+export const updateDocumentContentAction = withProject
+ .createServerAction()
+ .input(
+ z.object({
+ documentUuid: z.string(),
+ commitId: z.number(),
+ content: z.string(),
+ }),
+ { type: 'json' },
+ )
+ .handler(async ({ input }) => {
+ const result = await updateDocument({
+ commitId: input.commitId,
+ documentUuid: input.documentUuid,
+ content: input.content,
+ })
+ return result.unwrap()
+ })
diff --git a/apps/web/src/app/(private)/_data-access/index.ts b/apps/web/src/app/(private)/_data-access/index.ts
index b50902378..8523bb865 100644
--- a/apps/web/src/app/(private)/_data-access/index.ts
+++ b/apps/web/src/app/(private)/_data-access/index.ts
@@ -2,8 +2,10 @@ import { cache } from 'react'
import {
getDocumentAtCommit,
+ NotFoundError,
findCommitByUuid as originalfindCommit,
findProject as originalFindProject,
+ getDocumentsAtCommit as originalGetDocumentsAtCommit,
getFirstProject as originalGetFirstProject,
type FindCommitByUuidProps,
type FindProjectProps,
@@ -45,3 +47,16 @@ export const getDocumentByUuid = cache(
return document
},
)
+
+export const getDocumentByPath = cache(
+ async ({ commitId, path }: { commitId: number; path: string }) => {
+ const documents = (
+ await originalGetDocumentsAtCommit({ commitId })
+ ).unwrap()
+ const document = documents.find((d) => d.path === path)
+ if (!document) {
+ throw new NotFoundError('Document not found')
+ }
+ return document
+ },
+)
diff --git a/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts b/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts
index 229e7da5d..f09e866cf 100644
--- a/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts
+++ b/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts
@@ -31,7 +31,9 @@ describe('GET documentVersion', () => {
)
expect(response.status).toBe(200)
- expect((await response.json()).id).toEqual(doc.id)
+ const responseDoc = await response.json()
+ expect(responseDoc.documentUuid).toEqual(doc.documentUuid)
+ expect(responseDoc.commitId).toEqual(doc.commitId)
})
test('returns the document in main branch if commitUuid is HEAD', async (ctx) => {
@@ -57,7 +59,9 @@ describe('GET documentVersion', () => {
)
expect(response.status).toBe(200)
- expect((await response.json()).id).toEqual(doc.id)
+ const responseDoc = await response.json()
+ expect(responseDoc.documentUuid).toEqual(doc.documentUuid)
+ expect(responseDoc.commitId).toEqual(doc.commitId)
})
test('returns 404 if document is not found', async (ctx) => {
@@ -103,6 +107,8 @@ describe('GET documentVersion', () => {
)
expect(response.status).toBe(200)
- expect((await response.json()).id).toEqual(doc.id)
+ const responseDoc = await response.json()
+ expect(responseDoc.documentUuid).toEqual(doc.documentUuid)
+ expect(responseDoc.commitId).toEqual(doc.commitId)
})
})
diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/index.tsx
new file mode 100644
index 000000000..bfb82eb4c
--- /dev/null
+++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/index.tsx
@@ -0,0 +1,80 @@
+'use client'
+
+import { Suspense, useCallback, useRef } from 'react'
+
+import { Commit, DocumentVersion } from '@latitude-data/core'
+import { DocumentEditor, useToast } from '@latitude-data/web-ui'
+import { getDocumentContentByPathAction } from '$/actions/documents/getContentByPath'
+import { updateDocumentContentAction } from '$/actions/documents/updateContent'
+import { useServerAction } from 'zsa-react'
+
+export default function ClientDocumentEditor({
+ commit,
+ document,
+}: {
+ commit: Commit
+ document: DocumentVersion
+}) {
+ const updateDocumentAction = useServerAction(updateDocumentContentAction)
+ const readDocumentContentAction = useServerAction(
+ getDocumentContentByPathAction,
+ )
+ const { toast } = useToast()
+
+ const documentsByPathRef = useRef<{ [path: string]: string | undefined }>({})
+
+ const readDocument = useCallback(
+ async (path: string) => {
+ const documentsByPath = documentsByPathRef.current
+ if (!(path in documentsByPath)) {
+ const [content, error] = await readDocumentContentAction.execute({
+ projectId: commit.projectId,
+ commitId: commit.id,
+ path,
+ })
+ documentsByPathRef.current = {
+ ...documentsByPath,
+ [path]: error ? undefined : content,
+ }
+ }
+
+ const documentContent = documentsByPath[path]
+ if (documentContent === undefined) {
+ throw new Error('Document not found')
+ }
+
+ return documentContent
+ },
+ [readDocumentContentAction.status, commit.id],
+ )
+
+ const saveDocumentContent = useCallback(
+ async (content: string) => {
+ const [_, error] = await updateDocumentAction.execute({
+ projectId: commit.projectId,
+ documentUuid: document.documentUuid,
+ commitId: commit.id,
+ content,
+ })
+
+ if (error) {
+ toast({
+ title: 'Could not save document',
+ description: error.message,
+ variant: 'destructive',
+ })
+ }
+ },
+ [commit, document, updateDocumentAction, toast],
+ )
+
+ return (
+