Skip to content

Commit

Permalink
Document Editor page
Browse files Browse the repository at this point in the history
  • Loading branch information
csansoon committed Jul 24, 2024
1 parent e28a430 commit 61ba4dc
Show file tree
Hide file tree
Showing 14 changed files with 292 additions and 50 deletions.
23 changes: 23 additions & 0 deletions apps/web/src/actions/documents/getContentByPath.ts
Original file line number Diff line number Diff line change
@@ -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
})
25 changes: 25 additions & 0 deletions apps/web/src/actions/documents/updateContent.ts
Original file line number Diff line number Diff line change
@@ -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()
})
15 changes: 15 additions & 0 deletions apps/web/src/app/(private)/_data-access/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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)
})
})
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense fallback={<div>Loading...</div>}>
<DocumentEditor
document={document.content}
saveDocumentContent={saveDocumentContent}
readDocument={readDocument}
/>
</Suspense>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default async function DocumentLayout({
documentUuid={params.documentUuid}
documentPath={document.path}
/>
<div className='p-32'>{children}</div>
{children}
</DocumentDetailWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
export default function DocumentPage({
import { findCommit, getDocumentByUuid } from '$/app/(private)/_data-access'

import ClientDocumentEditor from './_components/DocumentEditor'

export default async function DocumentPage({
params,
}: {
params: { projectId: string; commitUuid: string; documentUuid: string }
}) {
return <div>Documents {params.documentUuid}</div>
const commit = await findCommit({
projectId: Number(params.projectId),
uuid: params.commitUuid,
})
const document = await getDocumentByUuid({
documentUuid: params.documentUuid,
commitId: commit.id,
})
return <ClientDocumentEditor commit={commit} document={document} />
}
1 change: 1 addition & 0 deletions packages/web-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"react-dom": "18.3.0",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"use-debounce": "^10.0.1",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
Expand Down
83 changes: 53 additions & 30 deletions packages/web-ui/src/ds/molecules/DocumentTextEditor/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ export function DocumentTextEditor({
value,
metadata,
onChange,
readOnlyMessage,
}: DocumentTextEditorProps) {
const [defaultValue, _] = useState(value)
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const monacoRef = useRef<Monaco | null>(null)
const [isEditorMounted, setIsEditorMounted] = useState(false) // to avoid race conditions

function handleEditorWillMount(monaco: Monaco) {
const handleEditorWillMount = useCallback((monaco: Monaco) => {
const style = getComputedStyle(document.body)

monaco.languages.register({ id: 'document' })
Expand All @@ -39,16 +41,16 @@ export function DocumentTextEditor({
'editor.background': style.getPropertyValue('--secondary'),
},
})
}
}, [])

function handleEditorDidMount(
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
) {
editorRef.current = editor
monacoRef.current = monaco
setIsEditorMounted(true)
}
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
editorRef.current = editor
monacoRef.current = monaco
setIsEditorMounted(true)
},
[],
)

const updateMarkers = useCallback(() => {
if (!metadata) return
Expand Down Expand Up @@ -77,28 +79,49 @@ export function DocumentTextEditor({
updateMarkers()
}, [metadata, isEditorMounted])

function handleValueChange(value: string | undefined) {
if (value) onChange?.(value)
}
const handleValueChange = useCallback(
(value: string | undefined) => {
onChange?.(value ?? '')
},
[onChange],
)

return (
<div className='flex flex-col relative h-full w-full rounded-lg border border-border overflow-hidden'>
<Editor
height='100%'
width='100%'
theme='latitude'
language='document'
defaultValue={value}
beforeMount={handleEditorWillMount}
onMount={handleEditorDidMount}
onChange={handleValueChange}
options={{
lineNumbers: 'off',
minimap: {
enabled: false,
},
}}
/>
<div className='relative h-full rounded-lg border border-border overflow-hidden'>
<div className='absolute top-0 left-0 right-0 bottom-0'>
<Editor
height='100%'
width='100%'
theme='latitude'
language='document'
defaultValue={defaultValue}
beforeMount={handleEditorWillMount}
onMount={handleEditorDidMount}
onChange={handleValueChange}
options={{
fixedOverflowWidgets: true,
lineDecorationsWidth: 0,
padding: {
top: 16,
bottom: 16,
},
lineNumbers: 'off',
minimap: {
enabled: false,
},
copyWithSyntaxHighlighting: false,
cursorSmoothCaretAnimation: 'on',
occurrencesHighlight: 'off',
renderLineHighlight: 'none',
tabSize: 2,
wordWrap: 'on',
readOnly: !!readOnlyMessage,
readOnlyMessage: readOnlyMessage
? { value: readOnlyMessage, supportHtml: true }
: undefined,
}}
/>
</div>
</div>
)
}
13 changes: 11 additions & 2 deletions packages/web-ui/src/ds/molecules/DocumentTextEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import React, { lazy } from 'react'
import React, { lazy, useEffect, useState } from 'react'

import type { ConversationMetadata } from '@latitude-data/compiler'

Expand All @@ -10,6 +10,7 @@ export type DocumentTextEditorProps = {
value: string
metadata?: ConversationMetadata
onChange?: (value: string) => void
readOnlyMessage?: string
}

const DocumentTextEditor = lazy(() =>
Expand All @@ -25,7 +26,15 @@ function EditorWrapper(props: DocumentTextEditorProps) {
// When imported, Monaco automatically tries to use the window object.
// Since this is not available when rendering on the server, we only
// render the fallback component for SSR.
if (typeof window === 'undefined') return <DocumentTextEditorFallback />
const [isBrowser, setIsBrowser] = useState(false)

useEffect(() => {
setIsBrowser(typeof window !== 'undefined')
}, [])

if (!isBrowser) {
return <DocumentTextEditorFallback />
}
return <DocumentTextEditor {...props} />
}

Expand Down
7 changes: 1 addition & 6 deletions packages/web-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,4 @@ export * from './ds/tokens'
export * from './ds/atoms'
export * from './ds/molecules'
export * from './providers'

export { default as DocumentSidebar } from './sections/Document/Sidebar'
export type { SidebarDocument } from './sections/Document/Sidebar/Files/useTree'

export { default as DocumentDetailWrapper } from './sections/Document/DetailWrapper'
export * from './sections/Document/Sidebar/Files'
export * from './sections'
Loading

0 comments on commit 61ba4dc

Please sign in to comment.