Skip to content

Commit

Permalink
Rename files
Browse files Browse the repository at this point in the history
  • Loading branch information
csansoon committed Sep 20, 2024
1 parent c087a2b commit 1ecd9ca
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 7 deletions.
32 changes: 32 additions & 0 deletions apps/web/src/actions/documents/renamePathsAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use server'

import { CommitsRepository } from '@latitude-data/core/repositories'
import { z } from 'zod'

import { withProject } from '../procedures'
import { renameDocumentPaths } from '@latitude-data/core/services/documents/renameDocumentPaths'

export const renameDocumentPathsAction = withProject
.createServerAction()
.input(
z.object({
commitUuid: z.string(),
oldPath: z.string(),
newPath: z.string(),
}),
{ type: 'json' },
)
.handler(async ({ input, ctx }) => {
const commitsScope = new CommitsRepository(ctx.project.workspaceId)
const commit = await commitsScope
.getCommitByUuid({ uuid: input.commitUuid, projectId: ctx.project.id })
.then((r) => r.unwrap())

const result = await renameDocumentPaths({
commit,
oldPath: input.oldPath,
newPath: input.newPath,
})

return result.unwrap()
})
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function ClientFilesTree({
[selectedSegment, project.id, commit.uuid, isHead],
)

const { createFile, destroyFile, destroyFolder, isDestroying, data } =
const { createFile, destroyFile, destroyFolder, renamePaths, isDestroying, data } =
useDocumentVersions(
{ commitUuid: commit.uuid, projectId: project.id },
{
Expand All @@ -68,6 +68,7 @@ export default function ClientFilesTree({
navigateToDocument={navigateToDocument}
onMergeCommitClick={onMergeCommitClick}
createFile={createFile}
renamePaths={renamePaths}
destroyFile={destroyFile}
destroyFolder={destroyFolder}
isDestroying={isDestroying}
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/stores/documentVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ROUTES } from '$/services/routes'
import { useRouter } from 'next/navigation'
import useSWR, { SWRConfiguration } from 'swr'
import { useServerAction } from 'zsa-react'
import { renameDocumentPathsAction } from '$/actions/documents/renamePathsAction'

export default function useDocumentVersions(
{
Expand All @@ -35,6 +36,9 @@ export default function useDocumentVersions(
},
},
)
const { execute: executeRenamePaths } = useServerAction(
renameDocumentPathsAction,
)
const { execute: executeDestroyDocument, isPending: isDestroyingFile } =
useServerAction(destroyDocumentAction)
const { execute: executeDestroyFolder, isPending: isDestroyingFolder } =
Expand Down Expand Up @@ -105,6 +109,28 @@ export default function useDocumentVersions(
[executeCreateDocument, mutate, data, commitUuid],
)

const renamePaths = useCallback(
async ({ oldPath, newPath }: { oldPath: string; newPath: string }) => {
if (!projectId) return

const [_, error] = await executeRenamePaths({
oldPath,
newPath,
projectId,
commitUuid,
})

if (error) {
toast({
title: 'Error renaming paths',
description: error.formErrors?.[0] || error.message,
variant: 'destructive',
})
}
},
[executeRenamePaths, mutate, data, commitUuid],
)

const destroyFile = useCallback(
async (documentUuid: string) => {
if (!projectId) return
Expand Down Expand Up @@ -190,6 +216,7 @@ export default function useDocumentVersions(
isLoading: isLoading,
error: swrError,
createFile,
renamePaths,
destroyFile,
destroyFolder,
updateContent,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/services/documents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './destroyDocument'
export * from './destroyFolder'
export * from './recomputeChanges'
export * from './getResolvedContent'
export * from './renameDocumentPaths'
80 changes: 80 additions & 0 deletions packages/core/src/services/documents/renameDocumentPaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it } from 'vitest'

import { Providers } from '../../constants'
import { BadRequestError } from '../../lib'
import { DocumentVersionsRepository } from '../../repositories'
import * as factories from '../../tests/factories'
import { renameDocumentPaths } from './renameDocumentPaths'

describe('renameDocumentPaths', () => {
it('renames a single document path', async () => {
const { project, user } = await factories.createProject({
providers: [{ type: Providers.OpenAI, name: 'openai' }],
documents: {
'a/b': factories.helpers.createPrompt({ provider: 'openai' }),
'a/b/c': factories.helpers.createPrompt({ provider: 'openai' }),
},
})
const { commit: draft } = await factories.createDraft({ project, user })

const result = await renameDocumentPaths({
commit: draft,
oldPath: 'a/b', // a/b/c should not be affected, since I'm only renaming a file
newPath: 'new/path',
})

expect(result.ok).toBeTruthy()

const docsScope = new DocumentVersionsRepository(project.workspaceId)
const docs = await docsScope
.getDocumentsAtCommit(draft)
.then((r) => r.unwrap())
const paths = docs.map((d) => d.path).sort()
expect(paths).toEqual(['a/b/c', 'new/path'])
})

it('renames a folder path', async () => {
const { project, user } = await factories.createProject({
providers: [{ type: Providers.OpenAI, name: 'openai' }],
documents: {
'a/b/c': factories.helpers.createPrompt({ provider: 'openai' }),
'a/b/c/d': factories.helpers.createPrompt({ provider: 'openai' }),
'not/affected': factories.helpers.createPrompt({ provider: 'openai' }),
},
})
const { commit: draft } = await factories.createDraft({ project, user })

const result = await renameDocumentPaths({
commit: draft,
oldPath: 'a/b/',
newPath: 'newpath/',
})

expect(result.ok).toBeTruthy()

const docsScope = new DocumentVersionsRepository(project.workspaceId)
const docs = await docsScope
.getDocumentsAtCommit(draft)
.then((r) => r.unwrap())
const paths = docs.map((d) => d.path).sort()
expect(paths).toEqual(['newpath/c', 'newpath/c/d', 'not/affected'])
})

it('fails when trying to rename a folder as a document', async () => {
const { project, user } = await factories.createProject({
providers: [{ type: Providers.OpenAI, name: 'openai' }],
documents: {
'a/b': factories.helpers.createPrompt({ provider: 'openai' }),
},
})
const { commit: draft } = await factories.createDraft({ project, user })

const result = await renameDocumentPaths({
commit: draft,
oldPath: 'a/',
newPath: 'new/path',
})

expect(result.error).toBeInstanceOf(BadRequestError)
})
})
60 changes: 60 additions & 0 deletions packages/core/src/services/documents/renameDocumentPaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Commit, DocumentVersion } from '../../browser'
import { database } from '../../client'
import { findWorkspaceFromCommit } from '../../data-access'
import { Result, Transaction, TypedResult } from '../../lib'
import { BadRequestError } from '../../lib/errors'
import { DocumentVersionsRepository } from '../../repositories'
import { updateDocument } from './update'

export async function renameDocumentPaths(
{
commit,
oldPath,
newPath,
}: {
commit: Commit
oldPath: string
newPath: string
},
db = database,
): Promise<TypedResult<DocumentVersion[], Error>> {
return await Transaction.call(async (tx) => {
if (commit.mergedAt !== null) {
return Result.error(new BadRequestError('Cannot modify a merged commit'))
}

if (oldPath.endsWith('/') !== newPath.endsWith('/')) {
return Result.error(new BadRequestError('Trying to rename a folder as a document or vice versa'))
}

const workspace = await findWorkspaceFromCommit(commit, tx)
const docsScope = new DocumentVersionsRepository(workspace!.id, tx)

const currentDocs = await docsScope
.getDocumentsAtCommit(commit)
.then((r) => r.unwrap())

const docsToUpdate = currentDocs.filter((d) =>
oldPath.endsWith('/') ? d.path.startsWith(oldPath) : d.path === oldPath,
)

const updatedDocs = await Promise.all(
docsToUpdate.map(async (document) => {
const updatedPath = newPath + document.path.slice(oldPath.length)
// A simple replace would also replace other instances of oldPath in the path
// For example, relpacing "a" to "b" in "a/name" would result in "b/nbme"
const updatedDoc = await updateDocument(
{
commit,
document,
path: updatedPath,
},
tx,
)
return updatedDoc.unwrap()
}),
)

return Result.ok(updatedDocs)
}, db)
}
2 changes: 2 additions & 0 deletions packages/env/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const env = createEnv({
SENTRY_DSN: z.string().optional(),
SENTRY_ORG: z.string().optional(),
SENTRY_PROJECT: z.string().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string(),
NEXT_PUBLIC_POSTHOG_HOST: z.string(),
},
runtimeEnv: {
...process.env,
Expand Down
2 changes: 2 additions & 0 deletions packages/web-ui/src/ds/atoms/Icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
LoaderCircle,
Lock,
Moon,
Pencil,
RefreshCcw,
SquareDot,
SquareMinus,
Expand Down Expand Up @@ -63,6 +64,7 @@ const Icons = {
sun: Sun,
eye: Eye,
externalLink: ExternalLink,
pencil: Pencil,
refresh: RefreshCcw,
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react'
import { useCallback, useMemo, useState } from 'react'

import { Icon } from '../../../../../ds/atoms'
import { MenuOption } from '../../../../../ds/atoms/DropdownMenu'
Expand Down Expand Up @@ -57,8 +57,22 @@ export default function DocumentHeader({

onNavigateToDocument(node.doc!.documentUuid)
}, [node.doc!.documentUuid, selected, node.isPersisted, onNavigateToDocument])
const [isEditing, setIsEditing] = useState(node.name === " ")
const actions = useMemo<MenuOption[]>(
() => [
{
label: 'Rename',
disabled: isMerged,
iconProps: { name: 'pencil' },
onClick: () => {
if (isMerged) {
onMergeCommitClick()
return
}

setIsEditing(true)
},
},
{
label: 'Delete file',
type: 'destructive',
Expand All @@ -80,6 +94,8 @@ export default function DocumentHeader({
isFile
open={open}
name={node.name}
isEditing={isEditing}
setIsEditing={setIsEditing}
hasChildren={false}
actions={actions}
selected={selected}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react'
import { useCallback, useMemo, useState } from 'react'

import { Icon } from '../../../../../ds/atoms'
import { MenuOption } from '../../../../../ds/atoms/DropdownMenu'
Expand Down Expand Up @@ -72,8 +72,22 @@ export default function FolderHeader({
},
[node.path, togglePath, open, isMerged, onMergeCommitClick, addFolder],
)
const [isEditing, setIsEditing] = useState(node.name === ' ')
const actions = useMemo<MenuOption[]>(
() => [
{
label: 'Rename',
disabled: isMerged,
iconProps: { name: 'pencil' },
onClick: () => {
if (isMerged) {
onMergeCommitClick()
return
}

setIsEditing(true)
},
},
{
label: 'New folder',
disabled: isMerged,
Expand Down Expand Up @@ -120,6 +134,8 @@ export default function FolderHeader({
return (
<NodeHeaderWrapper
name={node.name}
isEditing={isEditing}
setIsEditing={setIsEditing}
hasChildren={node.children.length > 0}
onClick={onToggleOpen}
onSaveValue={({ path }) => updateFolder({ id: node.id, path })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type Props = {
hasChildren?: boolean
isFile?: boolean
selected?: boolean
isEditing: boolean
setIsEditing: (isEditing: boolean) => void
onClick?: () => void
actions?: MenuOption[]
icons: ReactNode
Expand All @@ -60,6 +62,8 @@ function NodeHeaderWrapper({
open,
hasChildren = false,
isFile = false,
isEditing,
setIsEditing,
onSaveValue,
onSaveValueAndTab,
onLeaveWithoutSave,
Expand All @@ -72,10 +76,12 @@ function NodeHeaderWrapper({
const [tmpName, setTmpName] = useState(name)
const inputRef = useRef<HTMLInputElement>(null)
const nodeRef = useRef<HTMLDivElement>(null)
const { isEditing, error, onInputChange, onInputKeyDown } = useNodeValidator({
const { error, inputValue, onInputChange, onInputKeyDown } = useNodeValidator({
name,
nodeRef,
inputRef,
isEditing,
setIsEditing,
saveValue: ({ path }) => {
setTmpName(path)
onSaveValue({ path })
Expand Down Expand Up @@ -123,6 +129,7 @@ function NodeHeaderWrapper({
tabIndex={0}
ref={inputRef}
autoFocus
value={inputValue}
onKeyDown={onInputKeyDown}
onChange={onInputChange}
errors={error ? [error] : undefined}
Expand Down
Loading

0 comments on commit 1ecd9ca

Please sign in to comment.