Skip to content

Commit

Permalink
More dry sidebar header node wrapper and comments from previous PR (#65)
Browse files Browse the repository at this point in the history
* One more refactor to remove complexity from external api on Sidebar node
wrapper

* On input tab create a new child tmp folder under it to make easier build
a tree

* Address some comments from this PR: #60
  • Loading branch information
andresgutgon authored Jul 29, 2024
1 parent 8602afd commit af5788d
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@ describe('getDocumentsAtCommit', () => {
expect(documents[0]!.documentUuid).toBe(allDocs[0]!.documentUuid)
})

it('get docs from HEAD', async (ctx) => {
const { project, documents } = await ctx.factories.createProject({
it('get docs from HEAD without soft deleted', async (ctx) => {
const { commit, project, documents } = await ctx.factories.createProject({
documents: { doc1: 'Doc 1', doc2: 'Doc 2' },
})
const documentsScope = new DocumentVersionsRepository(project.workspaceId)
const { commit: draft } = await factories.createDraft({ project })
await factories.markAsSoftDelete(
documents.find((d) => d.path === 'doc2')!.documentUuid,
)
await factories.markAsSoftDelete({
documentUuid: documents.find((d) => d.path === 'doc2')!.documentUuid,
commitId: commit.id,
})
const filteredDocs = await documentsScope
.getDocumentsAtCommit({ commit: draft })
.then((r) => r.unwrap())
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/services/commits/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import {
commits,
Database,
database,
Project,
Result,
Transaction,
} from '@latitude-data/core'

export async function createCommit({
commit: { projectId, title, mergedAt },
project,
data: { title, mergedAt },
db = database,
}: {
commit: {
projectId: number
project: Project
data: {
title?: string
mergedAt?: Date
}
Expand All @@ -22,7 +24,7 @@ export async function createCommit({
const result = await tx
.insert(commits)
.values({
projectId,
projectId: project.id,
title,
mergedAt,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { describe, expect, it } from 'vitest'
import { destroyOrSoftDeleteDocuments } from './destroyOrSoftDeleteDocuments'

describe('destroyOrSoftDeleteDocuments', () => {
it('hardDestroyDocuments', async () => {
it('remove documents that were not present in merged commits', async () => {
const { project } = await factories.createProject()
const { commit: draft } = await factories.createDraft({ project })
const { documentVersion: draftDocument } =
Expand All @@ -29,7 +29,7 @@ describe('destroyOrSoftDeleteDocuments', () => {
expect(documents.length).toBe(0)
})

it('createDocumentsAsSoftDeleted', async () => {
it('mark as deleted documents that were present in merged commits and not in the draft commit', async () => {
const { project, documents: allDocs } = await factories.createProject({
documents: { doc1: 'Doc 1' },
})
Expand All @@ -50,7 +50,7 @@ describe('destroyOrSoftDeleteDocuments', () => {
expect(drafDocument!.deletedAt).not.toBe(null)
})

it('updateDocumetsAsSoftDeleted', async () => {
it('mark as deleted documents present in the draft commit', async () => {
const { project, documents: allDocs } = await factories.createProject({
documents: { doc1: 'Doc 1' },
})
Expand All @@ -66,7 +66,7 @@ describe('destroyOrSoftDeleteDocuments', () => {
await database
.update(documentVersions)
.set({
resolvedContent: '[CHACHED] Doc 1 (version 1)',
resolvedContent: '[CACHED] Doc 1 (version 1)',
})
.where(eq(documentVersions.commitId, draft.id))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,14 @@ async function hardDestroyDocuments({
}

async function createDocumentsAsSoftDeleted({
toBeSoftDeleted,
toBeCreated,
commitId,
tx,
}: {
toBeSoftDeleted: DocumentVersion[]
toBeCreated: DocumentVersion[]
commitId: number
tx: typeof database
}) {
const toBeCreated = toBeSoftDeleted.filter((d) => d.commitId !== commitId)

if (!toBeCreated.length) return

return tx.insert(documentVersions).values(
Expand All @@ -83,18 +81,15 @@ async function createDocumentsAsSoftDeleted({
}

async function updateDocumetsAsSoftDeleted({
toBeSoftDeleted,
toBeUpdated,
commitId,
tx,
}: {
toBeSoftDeleted: DocumentVersion[]
toBeUpdated: DocumentVersion[]
commitId: number
tx: typeof database
}) {
const uuids = toBeSoftDeleted
.filter((d) => d.commitId === commitId)
.map((d) => d.documentUuid)

const uuids = toBeUpdated.map((d) => d.documentUuid)
if (!uuids.length) return

return tx
Expand Down Expand Up @@ -123,7 +118,7 @@ async function invalidateDocumentsCacheInCommit(
* A document can:
*
* 1. Not exists in previous commits. In this case, it will be hard deleted
* 1. Exists in previous commits and in the commit. It will be updated the `deletedAt` field
* 2. Exists in previous commits and in the commit. It will be updated the `deletedAt` field
* 3. Exists in previous commits but not in the commit. It will be created as soft deleted
*/
export async function destroyOrSoftDeleteDocuments({
Expand All @@ -143,11 +138,13 @@ export async function destroyOrSoftDeleteDocuments({
commitId,
})
const toBeSoftDeleted = getToBeSoftDeleted({ documents, existingUuids })
const toBeCreated = toBeSoftDeleted.filter((d) => d.commitId !== commitId)
const toBeUpdated = toBeSoftDeleted.filter((d) => d.commitId === commitId)

await Promise.all([
hardDestroyDocuments({ documents, existingUuids, tx }),
createDocumentsAsSoftDeleted({ toBeSoftDeleted, commitId, tx }),
updateDocumetsAsSoftDeleted({ toBeSoftDeleted, commitId, tx }),
createDocumentsAsSoftDeleted({ toBeCreated, commitId, tx }),
updateDocumetsAsSoftDeleted({ toBeUpdated, commitId, tx }),
invalidateDocumentsCacheInCommit(commitId, tx),
])

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/services/projects/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export async function createProject(
)[0]!

const result = await createCommit({
commit: {
projectId: project.id,
project,
data: {
title: 'Initial version',
mergedAt: new Date(),
},
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/tests/factories/commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ export type ICreateDraft = {
project?: Project | ICreateProject
}
export async function createDraft({ project }: Partial<ICreateDraft> = {}) {
let projectId = hasOwnProperty<number, object, string>(project, 'id')
? project.id
: (await createProject(project)).project.id
let projectModel = hasOwnProperty<number, object, string>(project, 'id')
? (project as unknown as Project)
: (await createProject(project)).project

const result = await createCommitFn({ commit: { projectId: projectId! } })
const result = await createCommitFn({ project: projectModel, data: {} })
const commit = result.unwrap()

return { commit }
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/tests/factories/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { database } from '$core/client'
import { documentVersions, type Commit } from '$core/schema'
import { createNewDocument } from '$core/services/documents/create'
import { updateDocument } from '$core/services/documents/update'
import { eq } from 'drizzle-orm'
import { and, eq } from 'drizzle-orm'

export type IDocumentVersionData = {
commit: Commit
Expand All @@ -18,11 +18,19 @@ function makeRandomDocumentVersionData() {
}
}

export async function markAsSoftDelete(documentUuid: string, tx = database) {
export async function markAsSoftDelete(
{ commitId, documentUuid }: { commitId: number; documentUuid: string },
tx = database,
) {
return tx
.update(documentVersions)
.set({ deletedAt: new Date() })
.where(eq(documentVersions.documentUuid, documentUuid))
.where(
and(
eq(documentVersions.documentUuid, documentUuid),
eq(documentVersions.commitId, commitId),
),
)
}

export async function createDocumentVersion(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import {
forwardRef,
KeyboardEventHandler,
ReactNode,
RefObject,
useEffect,
useState,
type ChangeEventHandler,
} from 'react'
import { forwardRef, ReactNode, useEffect, useRef, useState } from 'react'

import { DropdownMenu, MenuOption } from '$ui/ds/atoms/DropdownMenu'
import { Input } from '$ui/ds/atoms/Input'
import Text from '$ui/ds/atoms/Text'
import { cn } from '$ui/lib/utils'
import { useNodeValidator } from '$ui/sections/Document/Sidebar/Files/NodeHeaderWrapper/useNodeValidator'

import { Node } from '../useTree'

Expand All @@ -32,11 +25,11 @@ function IndentationBar({
return (
<div key={index} className='h-6 min-w-6'>
{index > 0 ? (
<div className='-ml-[3px] relative w-6 h-full flex justify-center'>
<div className='relative w-6 h-full flex justify-center'>
{hasChildren || !showBorder ? (
<div className='bg-border w-px h-8 -mt-1' />
<div className='-ml-px bg-border w-px h-8 -mt-1' />
) : (
<div className='relative -mt-1'>
<div className='-ml-px relative -mt-1'>
<div className='border-l h-2.5' />
<div className='absolute top-2.5 border-l border-b h-2 w-2 rounded-bl-sm' />
</div>
Expand All @@ -57,28 +50,41 @@ type Props = {
actions: MenuOption[]
icons: ReactNode
indentation: IndentType[]
inputRef: RefObject<HTMLInputElement>
error?: string
onChangeInput: ChangeEventHandler<HTMLInputElement>
onKeyDownInput: KeyboardEventHandler<HTMLInputElement>
onSaveValue: (args: { path: string; id: string }) => void
onSaveValueAndTab?: (args: { path: string; id: string }) => void
onLeaveWithoutSave?: (args: { id: string }) => void
}
const NodeHeaderWrapper = forwardRef<HTMLDivElement, Props>(function Foo(
{
node,
inputRef,
open,
onSaveValue,
onSaveValueAndTab,
onLeaveWithoutSave,
selected = false,
isEditing = false,
error,
onChangeInput,
onKeyDownInput,
onClick,
icons,
indentation,
actions,
},
ref,
) {
const inputRef = useRef<HTMLInputElement>(null)
const nodeRef = useRef<HTMLDivElement>(null)
const { isEditing, error, onInputChange, onInputKeyDown } = useNodeValidator({
name: node.name,
nodeRef,
inputRef,
saveValue: async ({ path }: { path: string }) => {
return onSaveValue({ path, id: node.id })
},
saveAndAddOther: ({ path }) => {
onSaveValueAndTab?.({ path, id: node.id })
},
leaveWithoutSave: () => {
onLeaveWithoutSave?.({ id: node.id })
},
})
const [actionsOpen, setActionsOpen] = useState(false)

// Litle trick to focus the input after the component is mounted
Expand Down Expand Up @@ -108,22 +114,29 @@ const NodeHeaderWrapper = forwardRef<HTMLDivElement, Props>(function Foo(
>
<div
onClick={onClick}
className='min-w-0 flex-grow flex flex-row items-center justify-between gap-x-1 py-0.5'
className='min-w-0 flex-grow flex flex-row items-center justify-between py-0.5'
>
<IndentationBar
indentation={indentation}
hasChildren={open && node.children.length > 0}
/>
<div className='flex flex-row items-center gap-x-1'>{icons}</div>
<div className='flex flex-row items-center gap-x-1 mr-1'>{icons}</div>
{isEditing ? (
<div className='pr-1 flex items-center'>
<Input
tabIndex={0}
ref={inputRef}
autoFocus
onKeyDown={onKeyDownInput}
onChange={onChangeInput}
onKeyDown={onInputKeyDown}
onChange={onInputChange}
errors={error ? [error] : undefined}
placeholder={
onSaveValueAndTab
? 'Tab to create another folder'
: node.isFile
? 'File name'
: 'Folder name'
}
name='name'
type='text'
size='small'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {
useState,
} from 'react'

import { Node as SidebarNode } from '$ui/sections/Document/Sidebar/Files/useTree'

function useOnClickOutside<E extends HTMLElement>({
enabled,
ref,
Expand Down Expand Up @@ -44,19 +42,21 @@ const PATH_REGEXP = /^([\w-]+\/)*([\w-.])+$/
const INVALID_MSG =
"Invalid path, no spaces. Only letters, numbers, '-' and '_'"
export function useNodeValidator({
node,
name,
inputRef,
nodeRef,
leaveWithoutSave,
saveValue,
saveAndAddOther,
}: {
node: SidebarNode
name: string | undefined
inputRef: RefObject<HTMLInputElement>
nodeRef: RefObject<HTMLDivElement>
saveValue: (args: { path: string }) => Promise<void>
saveAndAddOther?: (args: { path: string }) => void
leaveWithoutSave?: () => void
}) {
const [isEditing, setIsEditing] = useState(node.name === ' ')
const [isEditing, setIsEditing] = useState(name === ' ')
const [validationError, setError] = useState<string>()
const onInputChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
Expand Down Expand Up @@ -100,9 +100,16 @@ export function useNodeValidator({
const val = inputRef.current?.value ?? ''
const value = val.trim()
const isValid = PATH_REGEXP.test(value)
const key = event.key

if (event.key === 'Escape') {
if (key === 'Escape') {
leaveWithoutSave?.()
} else if (key === 'Tab') {
event.preventDefault()
if (!isValid) return

saveAndAddOther?.({ path: value })
setIsEditing(false)
} else if (event.key === 'Enter' && isValid) {
await saveValue({ path: value })
setIsEditing(false)
Expand Down
Loading

0 comments on commit af5788d

Please sign in to comment.