From e7f7735436d7d064b0540014dc085fb683d2b1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Sans=C3=B3n?= Date: Tue, 16 Jul 2024 11:35:53 +0200 Subject: [PATCH] Current commit management --- .../(private)/[commitUuid]/editor/page.tsx | 11 +++ .../src/app/(private)/[commitUuid]/layout.tsx | 32 +++++++ apps/web/src/app/(private)/layout.tsx | 10 +- .../commits/[commitUuid]/documents/route.ts | 5 +- .../components/Sidebar/DocumentTree/index.tsx | 24 +++-- apps/web/src/components/Sidebar/index.tsx | 1 - apps/web/src/stores/documentVersions.ts | 13 +-- packages/core/src/data-access/commits.ts | 92 ++++++++++++++++--- .../core/src/data-access/documentVersions.ts | 17 ---- .../documentVersions/materializeAtCommit.ts | 15 +-- .../molecules/DocumentTextEditor/editor.tsx | 5 + .../ds/molecules/DocumentTextEditor/index.tsx | 1 + packages/web-ui/src/index.ts | 1 + .../web-ui/src/providers/commitProvider.tsx | 34 +++++++ packages/web-ui/src/providers/index.ts | 1 + .../src/sections/DocumentEditor/index.tsx | 3 + 16 files changed, 194 insertions(+), 71 deletions(-) create mode 100644 apps/web/src/app/(private)/[commitUuid]/editor/page.tsx create mode 100644 apps/web/src/app/(private)/[commitUuid]/layout.tsx create mode 100644 packages/web-ui/src/providers/commitProvider.tsx create mode 100644 packages/web-ui/src/providers/index.ts diff --git a/apps/web/src/app/(private)/[commitUuid]/editor/page.tsx b/apps/web/src/app/(private)/[commitUuid]/editor/page.tsx new file mode 100644 index 000000000..7c191cc63 --- /dev/null +++ b/apps/web/src/app/(private)/[commitUuid]/editor/page.tsx @@ -0,0 +1,11 @@ +import { DocumentEditor } from '@latitude-data/web-ui' + +export const dynamic = 'force-dynamic' + +export default function Editor() { + return ( +
+ +
+ ) +} diff --git a/apps/web/src/app/(private)/[commitUuid]/layout.tsx b/apps/web/src/app/(private)/[commitUuid]/layout.tsx new file mode 100644 index 000000000..f6b0d9d6b --- /dev/null +++ b/apps/web/src/app/(private)/[commitUuid]/layout.tsx @@ -0,0 +1,32 @@ +import { ReactNode } from 'react' + +// import Sidebar from '$/components/Sidebar' +import { CommitProvider } from '@latitude-data/web-ui' +import { getNextCommitId, getPreviousCommitId } from '$core/data-access' + +// import { getIsCommitEditableAction } from '$/actions/commits' + +export default async function PrivateLayout({ + children, + params, +}: { + children: ReactNode + params: { commitUuid: string } +}) { + const commitUuid = params.commitUuid + + const nextCommitId = await getNextCommitId({ uuid: commitUuid }) + const previousCommitId = await getPreviousCommitId({ uuid: commitUuid }) + + const isDraft = + nextCommitId.unwrap() === null && previousCommitId.unwrap() === null + + return ( + +
+
{/* */}
+
{children}
+
+
+ ) +} diff --git a/apps/web/src/app/(private)/layout.tsx b/apps/web/src/app/(private)/layout.tsx index 709e988ba..63e0f847d 100644 --- a/apps/web/src/app/(private)/layout.tsx +++ b/apps/web/src/app/(private)/layout.tsx @@ -1,6 +1,5 @@ import { ReactNode } from 'react' -import Sidebar from '$/components/Sidebar' import { getSession } from '$/services/auth/getSession' import { ROUTES } from '$/services/routes' import { redirect } from 'next/navigation' @@ -14,12 +13,5 @@ export default async function PrivateLayout({ if (!data.session) { return redirect(ROUTES.auth.login) } - return ( -
-
- -
-
{children}
-
- ) + return children } diff --git a/apps/web/src/app/api/commits/[commitUuid]/documents/route.ts b/apps/web/src/app/api/commits/[commitUuid]/documents/route.ts index 41a125cf1..17dded423 100644 --- a/apps/web/src/app/api/commits/[commitUuid]/documents/route.ts +++ b/apps/web/src/app/api/commits/[commitUuid]/documents/route.ts @@ -2,12 +2,11 @@ import { materializeDocumentsAtCommit } from '@latitude-data/core' import { NextRequest, NextResponse } from 'next/server' export async function GET( - req: NextRequest, + _: NextRequest, { commitUuid }: { commitUuid: string }, ) { try { - const staged = Boolean(req.nextUrl.searchParams.get('staged') || false) - const documents = await materializeDocumentsAtCommit({ commitUuid, staged }) + const documents = await materializeDocumentsAtCommit({ commitUuid }) return NextResponse.json(documents) } catch (err: unknown) { diff --git a/apps/web/src/components/Sidebar/DocumentTree/index.tsx b/apps/web/src/components/Sidebar/DocumentTree/index.tsx index 3f2f1d2cf..203d093a9 100644 --- a/apps/web/src/components/Sidebar/DocumentTree/index.tsx +++ b/apps/web/src/components/Sidebar/DocumentTree/index.tsx @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker' import type { DocumentType, DocumentVersion } from '@latitude-data/core' +import { useCurrentCommit } from '@latitude-data/web-ui' import useDocumentVersions from '$/stores/documentVersions' import { Node, useTree } from '../toTree' @@ -20,16 +21,19 @@ export function CreateNode({ parentId }: { parentId?: number }) { } function CreateFolder({ parentId }: { parentId?: number }) { - const { create } = useDocumentVersions({ staged: true }) + const { commitUuid, isDraft } = useCurrentCommit() + const { create } = useDocumentVersions({ commitUuid: commitUuid }) return ( @@ -37,9 +41,16 @@ function CreateFolder({ parentId }: { parentId?: number }) { } function CreateDocument({ parentId }: { parentId?: number }) { - const { create } = useDocumentVersions({ staged: true }) + const { commitUuid, isDraft } = useCurrentCommit() + const { create } = useDocumentVersions({ commitUuid: commitUuid }) return ( - ) @@ -74,8 +85,9 @@ export default function DocumentTree({ }: { documents: DocumentVersion[] }) { + const { commitUuid } = useCurrentCommit() const { documents } = useDocumentVersions( - { staged: true }, + { commitUuid }, { fallbackData: serverDocuments }, ) const rootNode = useTree({ documents }) diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index c03362159..265170e63 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -5,7 +5,6 @@ import DocumentTree, { CreateNode } from './DocumentTree' export default async function Sidebar() { const documents = await materializeDocumentsAtCommit({ commitUuid: HEAD_COMMIT, - staged: true, }) return ( diff --git a/apps/web/src/stores/documentVersions.ts b/apps/web/src/stores/documentVersions.ts index 6afd8e2a7..f739519e4 100644 --- a/apps/web/src/stores/documentVersions.ts +++ b/apps/web/src/stores/documentVersions.ts @@ -11,19 +11,13 @@ import { useServerAction } from 'zsa-react' const FIXME_HARDCODED_PROJECT_ID = 1 export default function useDocumentVersions( { - commitUuid = HEAD_COMMIT, - staged = false, + commitUuid, }: { commitUuid?: string - staged?: boolean }, opts?: SWRConfiguration, ) { - const key = - `/api/commits/${commitUuid}/documents?` + - new URLSearchParams({ - staged: String(staged), - }).toString() + const key = `/api/commits/${commitUuid ?? HEAD_COMMIT}/documents` const { mutate, data, ...rest } = useSWR( key, @@ -34,7 +28,6 @@ export default function useDocumentVersions( const { execute } = useServerAction(createDocumentVersionAction) const create = useCallback( async (payload: { - commitUuid?: string name: string documentType?: DocumentType parentId?: number @@ -43,7 +36,7 @@ export default function useDocumentVersions( ...payload, projectId: FIXME_HARDCODED_PROJECT_ID, name: payload.name!, - commitUuid: payload.commitUuid || HEAD_COMMIT, + commitUuid: commitUuid || HEAD_COMMIT, }) const prev = documents ?? [] diff --git a/packages/core/src/data-access/commits.ts b/packages/core/src/data-access/commits.ts index e605596ad..cd5a27884 100644 --- a/packages/core/src/data-access/commits.ts +++ b/packages/core/src/data-access/commits.ts @@ -1,26 +1,94 @@ import { database } from '$core/client' -import { HEAD_COMMIT } from '$core/constants' +import { HEAD_COMMIT, Result, TypedResult } from '$core/constants' +import { LatitudeError, NotFoundError } from '$core/lib/errors' import { commits } from '$core/schema' -import { desc, eq, isNull } from 'drizzle-orm' +import { and, desc, eq, isNotNull, isNull } from 'drizzle-orm' -const selectCondition = (uuid?: Exclude) => { - if (!uuid) return isNull(commits.nextCommitId) +export async function findHeadCommit( + tx = database, +): Promise> { + // Commits that are being pointed at by other commits. + const childCommits = tx + .$with('child_commits') + .as( + tx + .select({ id: commits.nextCommitId }) + .from(commits) + .where(isNotNull(commits.nextCommitId)) + .orderBy(desc(commits.id)) + .limit(1), + ) - return eq(commits.uuid, uuid) -} + // Commits that have previous commits but no next commits. + const headCommits = await tx + .with(childCommits) + .select({ id: commits.id }) + .from(commits) + .leftJoin(childCommits, eq(commits.id, childCommits.id)) + .where(and(isNull(commits.nextCommitId), isNotNull(childCommits.id))) + + if (headCommits.length < 1) { + return Result.error(new NotFoundError('No head commit found')) + } -export async function findCommit({ uuid }: { uuid?: string }, tx = database) { - if (uuid === HEAD_COMMIT) { - return tx.query.commits.findFirst({ orderBy: desc(commits.id) }) + if (headCommits.length > 1) { + return Result.error(new LatitudeError('Multiple head commits found')) } - return tx.query.commits.findFirst({ where: selectCondition(uuid) }) + const headCommit = headCommits[0]! + return Result.ok(headCommit.id) +} + +export async function findCommit( + { uuid }: { uuid: string }, + tx = database, +): Promise> { + if (uuid === HEAD_COMMIT) return findHeadCommit(tx) + + const res = await tx + .select() + .from(commits) + .where(eq(commits.uuid, uuid)) + .limit(1) + + if (!res.length) return Result.error(new NotFoundError('Commit not found')) + const commit = res[0]! + return Result.ok(commit.id) } export async function listCommits() { return database.select().from(commits) } -export async function listStagedCommits() { - return database.select().from(commits).where(isNull(commits.nextCommitId)) +export async function getNextCommitId( + { uuid }: { uuid: string }, + tx = database, +): Promise> { + if (uuid === HEAD_COMMIT) return Result.ok(null) + const res = await tx + .select({ nextCommitId: commits.nextCommitId }) + .from(commits) + .where(eq(commits.uuid, uuid)) + .limit(1) + + if (!res.length) return Result.error(new NotFoundError('Commit not found')) + + return Result.ok(res[0]!.nextCommitId) +} + +export async function getPreviousCommitId( + { uuid }: { uuid: string }, + tx = database, +): Promise> { + const commitIdResult = await findCommit({ uuid }, tx) + if (!Result.isOk(commitIdResult)) return commitIdResult + const commitId = commitIdResult.unwrap() + + const res = await tx + .select({ id: commits.id }) + .from(commits) + .where(eq(commits.nextCommitId, commitId)) + + if (!res.length) return Result.ok(null) + return Result.ok(res[0]!.id) } diff --git a/packages/core/src/data-access/documentVersions.ts b/packages/core/src/data-access/documentVersions.ts index e1ad4f3e3..2655b79fe 100644 --- a/packages/core/src/data-access/documentVersions.ts +++ b/packages/core/src/data-access/documentVersions.ts @@ -1,23 +1,6 @@ import { commits, database, documentVersions } from '@latitude-data/core' import { desc, eq, inArray, lte } from 'drizzle-orm' -import { listStagedCommits } from './commits' - -export async function listStagedDocuments() { - const commits = await listStagedCommits() - if (!commits.length) return [] - - return database - .select() - .from(documentVersions) - .where( - inArray( - documentVersions.commitId, - commits.map((c) => c.id), - ), - ) -} - export async function getDocumentsAtCommit(commitUuid: string) { const referenceCommitId = await database .select({ id: commits.id }) diff --git a/packages/core/src/services/documentVersions/materializeAtCommit.ts b/packages/core/src/services/documentVersions/materializeAtCommit.ts index a7490097e..fb9de1e40 100644 --- a/packages/core/src/services/documentVersions/materializeAtCommit.ts +++ b/packages/core/src/services/documentVersions/materializeAtCommit.ts @@ -1,27 +1,16 @@ -import { uniqBy } from 'lodash-es' - import { HEAD_COMMIT } from '$core/constants' -import { - getDocumentsAtCommit, - listdocumentSnapshots, - listStagedDocuments, -} from '$core/data-access' +import { getDocumentsAtCommit, listdocumentSnapshots } from '$core/data-access' export async function materializeDocumentsAtCommit({ commitUuid = HEAD_COMMIT, - staged = true, }: { commitUuid: string - staged: boolean }) { if (commitUuid === HEAD_COMMIT) { const snapshots = (await listdocumentSnapshots()).map( (snap) => snap.document_versions, ) - if (!staged) return snapshots - - const versions = await listStagedDocuments() - return uniqBy([...versions, ...snapshots], (doc) => doc.documentUuid) + return snapshots } else { return await getDocumentsAtCommit(commitUuid) } diff --git a/packages/web-ui/src/ds/molecules/DocumentTextEditor/editor.tsx b/packages/web-ui/src/ds/molecules/DocumentTextEditor/editor.tsx index 592c06b90..b6fa8c8bc 100644 --- a/packages/web-ui/src/ds/molecules/DocumentTextEditor/editor.tsx +++ b/packages/web-ui/src/ds/molecules/DocumentTextEditor/editor.tsx @@ -21,6 +21,7 @@ export function DocumentTextEditor({ value, metadata, onChange, + disabled, }: DocumentTextEditorProps) { const editorRef = useRef(null) const monacoRef = useRef(null) @@ -94,6 +95,10 @@ export function DocumentTextEditor({ onChange={handleValueChange} options={{ lineNumbers: 'off', + readOnly: disabled, + readOnlyMessage: { + value: 'Create a new draft to edit this document', + }, minimap: { enabled: false, }, diff --git a/packages/web-ui/src/ds/molecules/DocumentTextEditor/index.tsx b/packages/web-ui/src/ds/molecules/DocumentTextEditor/index.tsx index 144165c18..f28604f9a 100644 --- a/packages/web-ui/src/ds/molecules/DocumentTextEditor/index.tsx +++ b/packages/web-ui/src/ds/molecules/DocumentTextEditor/index.tsx @@ -10,6 +10,7 @@ export type DocumentTextEditorProps = { value: string metadata?: ConversationMetadata onChange?: (value: string) => void + disabled?: boolean } const DocumentTextEditor = lazy(() => diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index 3b623a7cc..ff7a6152a 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -3,3 +3,4 @@ export * from './ds/atoms' export * from './ds/molecules' export * from './layouts' export * from './sections' +export * from './providers' diff --git a/packages/web-ui/src/providers/commitProvider.tsx b/packages/web-ui/src/providers/commitProvider.tsx new file mode 100644 index 000000000..4d94f6c77 --- /dev/null +++ b/packages/web-ui/src/providers/commitProvider.tsx @@ -0,0 +1,34 @@ +'use client' + +import { createContext, ReactNode, useContext } from 'react' + +import { HEAD_COMMIT } from '@latitude-data/core' + +// const HEAD_COMMIT = 'HEAD' + +interface CommitContextType { + commitUuid: string | typeof HEAD_COMMIT + isDraft: boolean +} + +const CommitContext = createContext({ + commitUuid: HEAD_COMMIT, + isDraft: false, +}) + +const CommitProvider = ({ + children, + ...context +}: { + children: ReactNode +} & CommitContextType) => { + return ( + {children} + ) +} + +const useCurrentCommit = () => { + return useContext(CommitContext) +} + +export { CommitProvider, useCurrentCommit } diff --git a/packages/web-ui/src/providers/index.ts b/packages/web-ui/src/providers/index.ts new file mode 100644 index 000000000..9f1634d77 --- /dev/null +++ b/packages/web-ui/src/providers/index.ts @@ -0,0 +1 @@ +export * from './commitProvider' diff --git a/packages/web-ui/src/sections/DocumentEditor/index.tsx b/packages/web-ui/src/sections/DocumentEditor/index.tsx index 87d8a8642..4bb478ffc 100644 --- a/packages/web-ui/src/sections/DocumentEditor/index.tsx +++ b/packages/web-ui/src/sections/DocumentEditor/index.tsx @@ -8,6 +8,7 @@ import { DocumentTextEditor, DocumentTextEditorFallback, } from '$ui/ds/molecules' +import { useCurrentCommit } from '$ui/providers/commitProvider' function Header({ title, children }: { title: string; children?: ReactNode }) { return ( @@ -19,6 +20,7 @@ function Header({ title, children }: { title: string; children?: ReactNode }) { } export function DocumentEditor({ document }: { document: string }) { + const { isDraft } = useCurrentCommit() const [value, setValue] = useState(document) const [metadata, setMetadata] = useState() useEffect(() => { @@ -38,6 +40,7 @@ export function DocumentEditor({ document }: { document: string }) { value={value} metadata={metadata} onChange={setValue} + disabled={!isDraft} />