From 1d462b8a3f9edd8b38c748c8081c068287abe343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Sans=C3=B3n?= <57395395+csansoon@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:35:25 +0200 Subject: [PATCH] Rename files (#231) --- .../actions/documents/renamePathsAction.ts | 32 ++++++++ .../Sidebar/ClientFilesTree/index.tsx | 29 ++++--- apps/web/src/stores/documentVersions.ts | 38 +++++++++ package.json | 3 +- packages/core/src/services/documents/index.ts | 1 + .../documents/renameDocumentPaths.test.ts | 80 +++++++++++++++++++ .../services/documents/renameDocumentPaths.ts | 64 +++++++++++++++ packages/env/src/index.ts | 4 + packages/web-ui/src/ds/atoms/Icons/index.tsx | 2 + .../Sidebar/Files/DocumentHeader/index.tsx | 31 ++++++- .../Sidebar/Files/FilesProvider/index.tsx | 11 ++- .../Sidebar/Files/FolderHeader/index.tsx | 42 +++++++++- .../Sidebar/Files/NodeHeaderWrapper/index.tsx | 29 ++++--- .../useNodeValidator/index.ts | 10 ++- .../Sidebar/Files/TreeToolbar/index.tsx | 4 +- .../sections/Document/Sidebar/Files/index.tsx | 34 ++++++-- .../Sidebar/Files/useTempNodes/index.ts | 11 ++- 17 files changed, 378 insertions(+), 47 deletions(-) create mode 100644 apps/web/src/actions/documents/renamePathsAction.ts create mode 100644 packages/core/src/services/documents/renameDocumentPaths.test.ts create mode 100644 packages/core/src/services/documents/renameDocumentPaths.ts diff --git a/apps/web/src/actions/documents/renamePathsAction.ts b/apps/web/src/actions/documents/renamePathsAction.ts new file mode 100644 index 000000000..3a11e218e --- /dev/null +++ b/apps/web/src/actions/documents/renamePathsAction.ts @@ -0,0 +1,32 @@ +'use server' + +import { CommitsRepository } from '@latitude-data/core/repositories' +import { renameDocumentPaths } from '@latitude-data/core/services/documents/renameDocumentPaths' +import { z } from 'zod' + +import { withProject } from '../procedures' + +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() + }) diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/ClientFilesTree/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/ClientFilesTree/index.tsx index 0695552d7..cf5ff3468 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/ClientFilesTree/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/ClientFilesTree/index.tsx @@ -29,7 +29,7 @@ export default function ClientFilesTree({ const { commit, isHead } = useCurrentCommit() const isMerged = !!commit.mergedAt const { project } = useCurrentProject() - const documentPath = currentDocument?.path + const documentUuid = currentDocument?.documentUuid const navigateToDocument = useCallback( (documentUuid: string) => { const documentDetails = ROUTES.projects @@ -42,16 +42,22 @@ export default function ClientFilesTree({ [project.id, commit.uuid, isHead], ) - const { createFile, destroyFile, destroyFolder, isDestroying, data } = - useDocumentVersions( - { commitUuid: commit.uuid, projectId: project.id }, - { - fallbackData: serverDocuments, - onSuccessCreate: (document) => { - navigateToDocument(document.documentUuid) - }, + const { + createFile, + destroyFile, + destroyFolder, + renamePaths, + isDestroying, + data, + } = useDocumentVersions( + { commitUuid: commit.uuid, projectId: project.id }, + { + fallbackData: serverDocuments, + onSuccessCreate: (document) => { + navigateToDocument(document.documentUuid) }, - ) + }, + ) const onMergeCommitClick = useCallback(() => { setWarningOpen(true) }, [setWarningOpen]) @@ -61,10 +67,11 @@ export default function ClientFilesTree({ { + if (!projectId) return + + const [updatedDocuments, error] = await executeRenamePaths({ + oldPath, + newPath, + projectId, + commitUuid, + }) + + if (updatedDocuments) { + mutate( + data.map((d) => { + const updatedDocument = updatedDocuments.find( + (ud) => ud.documentUuid === d.documentUuid, + ) + return updatedDocument ? updatedDocument : d + }), + ) + } + + 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 @@ -188,6 +225,7 @@ export default function useDocumentVersions( isLoading: isLoading, error: swrError, createFile, + renamePaths, destroyFile, destroyFolder, updateContent, diff --git a/package.json b/package.json index b44a03e62..4d6006fcf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "tc": "turbo tc", "prettier": "prettier --write \"**/*.{ts,tsx,md}\"", "prettier:check": "prettier --check \"**/*.{ts,tsx,md}\"", - "test": "turbo test" + "test": "turbo test", + "catchup": "pnpm i && pnpm build --filter=\"./packages/**/*\" && pnpm --filter \"@latitude-data/core\" db:migrate" }, "devDependencies": { "@babel/parser": "^7.25.4", diff --git a/packages/core/src/services/documents/index.ts b/packages/core/src/services/documents/index.ts index 510f860f5..8df94c316 100644 --- a/packages/core/src/services/documents/index.ts +++ b/packages/core/src/services/documents/index.ts @@ -4,3 +4,4 @@ export * from './destroyDocument' export * from './destroyFolder' export * from './recomputeChanges' export * from './getResolvedContent' +export * from './renameDocumentPaths' diff --git a/packages/core/src/services/documents/renameDocumentPaths.test.ts b/packages/core/src/services/documents/renameDocumentPaths.test.ts new file mode 100644 index 000000000..c3f451fe3 --- /dev/null +++ b/packages/core/src/services/documents/renameDocumentPaths.test.ts @@ -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) + }) +}) diff --git a/packages/core/src/services/documents/renameDocumentPaths.ts b/packages/core/src/services/documents/renameDocumentPaths.ts new file mode 100644 index 000000000..abc66cfe8 --- /dev/null +++ b/packages/core/src/services/documents/renameDocumentPaths.ts @@ -0,0 +1,64 @@ +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> { + 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) +} diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index a4bb8f0b6..c6a1f2bed 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -41,6 +41,8 @@ if (environment !== 'production') { FILES_STORAGE_PATH, DEFAULT_PROJECT_ID: '1', DEFAULT_PROVIDER_API_KEY: 'd32da7c2-94fd-49c3-8dca-b57a5c3bbe27', + NEXT_PUBLIC_POSTHOG_KEY: '', + NEXT_PUBLIC_POSTHOG_HOST: '', }, { path: pathToEnv }, ) @@ -80,6 +82,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, diff --git a/packages/web-ui/src/ds/atoms/Icons/index.tsx b/packages/web-ui/src/ds/atoms/Icons/index.tsx index 92e06cf7a..be3567374 100644 --- a/packages/web-ui/src/ds/atoms/Icons/index.tsx +++ b/packages/web-ui/src/ds/atoms/Icons/index.tsx @@ -21,6 +21,7 @@ import { LoaderCircle, Lock, Moon, + Pencil, RefreshCcw, SquareDot, SquareMinus, @@ -63,6 +64,7 @@ const Icons = { sun: Sun, eye: Eye, externalLink: ExternalLink, + pencil: Pencil, refresh: RefreshCcw, } diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/DocumentHeader/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/DocumentHeader/index.tsx index 53b29ca1f..1de42ceb6 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/DocumentHeader/index.tsx +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/DocumentHeader/index.tsx @@ -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' @@ -38,6 +38,7 @@ export default function DocumentHeader({ onNavigateToDocument, onDeleteFile, onCreateFile, + onRenameFile, } = useFileTreeContext() const { deleteTmpFolder, reset } = useTempNodes((state) => ({ reset: state.reset, @@ -45,11 +46,17 @@ export default function DocumentHeader({ })) const onSaveValue = useCallback( async ({ path }: { path: string }) => { - const parentPath = node.path.split('/').slice(0, -1).join('/') - await onCreateFile(`${parentPath}/${path}`) + const parentPathParts = node.path.split('/').slice(0, -1) + const newPathParts = path.split('/') + const newPath = [...parentPathParts, ...newPathParts].join('/') + if (node.isPersisted) { + onRenameFile({ node, path: newPath }) + } else { + onCreateFile(newPath) + } reset() }, - [reset, onCreateFile, node.path], + [reset, onCreateFile, onRenameFile, node.path, node.isPersisted], ) const handleClick = useCallback(() => { if (selected) return @@ -57,8 +64,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( () => [ + { + label: 'Rename', + disabled: isMerged, + iconProps: { name: 'pencil' }, + onClick: () => { + if (isMerged) { + onMergeCommitClick() + return + } + + setIsEditing(true) + }, + }, { label: 'Delete file', type: 'destructive', @@ -80,6 +101,8 @@ export default function DocumentHeader({ isFile open={open} name={node.name} + isEditing={isEditing} + setIsEditing={setIsEditing} hasChildren={false} actions={actions} selected={selected} diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/FilesProvider/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/FilesProvider/index.tsx index 7d93322d5..a5959ad52 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/FilesProvider/index.tsx +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/FilesProvider/index.tsx @@ -4,10 +4,11 @@ import { Node } from '../useTree' type IFilesContext = { isMerged: boolean - onCreateFile: (path: string) => Promise + onCreateFile: (path: string) => void + onRenameFile: (args: { node: Node; path: string }) => void onDeleteFile: (args: { node: Node; documentUuid: string }) => void onMergeCommitClick: () => void - currentPath?: string + currentUuid?: string onDeleteFolder: (args: { node: Node; path: string }) => void onNavigateToDocument: (documentUuid: string) => void } @@ -17,8 +18,9 @@ const FileTreeProvider = ({ isMerged, onMergeCommitClick, children, - currentPath, + currentUuid, onCreateFile, + onRenameFile, onDeleteFile, onDeleteFolder, onNavigateToDocument, @@ -28,8 +30,9 @@ const FileTreeProvider = ({ value={{ isMerged, onMergeCommitClick, - currentPath, + currentUuid, onCreateFile, + onRenameFile, onDeleteFile, onDeleteFolder, onNavigateToDocument, diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/FolderHeader/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/FolderHeader/index.tsx index bb114736e..38f7e81d4 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/FolderHeader/index.tsx +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/FolderHeader/index.tsx @@ -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' @@ -32,7 +32,8 @@ export default function FolderHeader({ indentation: IndentType[] onToggleOpen: () => void }) { - const { isMerged, onMergeCommitClick, onDeleteFolder } = useFileTreeContext() + const { isMerged, onMergeCommitClick, onDeleteFolder, onRenameFile } = + useFileTreeContext() const { openPaths, togglePath } = useOpenPaths((state) => ({ togglePath: state.togglePath, openPaths: state.openPaths, @@ -72,8 +73,41 @@ export default function FolderHeader({ }, [node.path, togglePath, open, isMerged, onMergeCommitClick, addFolder], ) + + const onSaveValue = useCallback( + ({ path }: { path: string }) => { + if (isMerged) { + onMergeCommitClick() + return + } + + if (node.isPersisted) { + const pathParts = node.path.split('/').slice(0, -1) + const newPath = [...pathParts, path].join('/') + onRenameFile({ node, path: newPath }) + } else { + updateFolder({ id: node.id, path }) + } + }, + [node.id, updateFolder, isMerged, onMergeCommitClick], + ) + + const [isEditing, setIsEditing] = useState(node.name === ' ') const actions = useMemo( () => [ + { + label: 'Rename', + disabled: isMerged, + iconProps: { name: 'pencil' }, + onClick: () => { + if (isMerged) { + onMergeCommitClick() + return + } + + setIsEditing(true) + }, + }, { label: 'New folder', disabled: isMerged, @@ -120,9 +154,11 @@ export default function FolderHeader({ return ( 0} onClick={onToggleOpen} - onSaveValue={({ path }) => updateFolder({ id: node.id, path })} + onSaveValue={({ path }) => onSaveValue({ path })} onSaveValueAndTab={({ path }) => onUpdateFolderAndAddOther({ id: node.id, path }) } diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/index.tsx index 386530b64..f887dd764 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/index.tsx +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/index.tsx @@ -46,6 +46,8 @@ type Props = { hasChildren?: boolean isFile?: boolean selected?: boolean + isEditing: boolean + setIsEditing: (isEditing: boolean) => void onClick?: () => void actions?: MenuOption[] icons: ReactNode @@ -60,6 +62,8 @@ function NodeHeaderWrapper({ open, hasChildren = false, isFile = false, + isEditing, + setIsEditing, onSaveValue, onSaveValueAndTab, onLeaveWithoutSave, @@ -72,17 +76,21 @@ function NodeHeaderWrapper({ const [tmpName, setTmpName] = useState(name) const inputRef = useRef(null) const nodeRef = useRef(null) - const { isEditing, error, onInputChange, onInputKeyDown } = useNodeValidator({ - name, - nodeRef, - inputRef, - saveValue: ({ path }) => { - setTmpName(path) - onSaveValue({ path }) + const { error, inputValue, onInputChange, onInputKeyDown } = useNodeValidator( + { + name, + nodeRef, + inputRef, + isEditing, + setIsEditing, + saveValue: ({ path }) => { + setTmpName(path) + onSaveValue({ path }) + }, + saveAndAddOther: onSaveValueAndTab, + leaveWithoutSave: onLeaveWithoutSave, }, - saveAndAddOther: onSaveValueAndTab, - leaveWithoutSave: onLeaveWithoutSave, - }) + ) // Litle trick to focus the input after the component is mounted // We wait some time to focus the input to avoid the focus being stolen // by the click event in the menu item that created this node. @@ -123,6 +131,7 @@ function NodeHeaderWrapper({ tabIndex={0} ref={inputRef} autoFocus + value={inputValue} onKeyDown={onInputKeyDown} onChange={onInputChange} errors={error ? [error] : undefined} diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/useNodeValidator/index.ts b/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/useNodeValidator/index.ts index fc4d1504b..d4aac2883 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/useNodeValidator/index.ts +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/NodeHeaderWrapper/useNodeValidator/index.ts @@ -45,6 +45,8 @@ export function useNodeValidator({ name, inputRef, nodeRef, + isEditing, + setIsEditing, leaveWithoutSave, saveValue, saveAndAddOther, @@ -52,12 +54,14 @@ export function useNodeValidator({ name: string | undefined inputRef: RefObject nodeRef: RefObject + isEditing: boolean + setIsEditing: (isEditing: boolean) => void saveValue: (args: { path: string }) => Promise | void saveAndAddOther?: (args: { path: string }) => void leaveWithoutSave?: () => void }) { - const [isEditing, setIsEditing] = useState(name === ' ') const [validationError, setError] = useState() + const [inputValue, setInputValue] = useState(name) const onInputChange: ChangeEventHandler = useCallback( (event) => { const value = event.target.value @@ -70,8 +74,9 @@ export function useNodeValidator({ } setError(error) + setInputValue(value) }, - [setError], + [setError, setInputValue], ) const onClickOutside = useCallback(async () => { const val = inputRef.current?.value ?? '' @@ -119,6 +124,7 @@ export function useNodeValidator({ ) return { + inputValue, isEditing, onInputChange, onInputKeyDown, diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/TreeToolbar/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/TreeToolbar/index.tsx index 1530dc0e0..e4d7384dd 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/TreeToolbar/index.tsx +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/TreeToolbar/index.tsx @@ -54,7 +54,9 @@ export function TreeToolbar() { open={false} hasChildren={false} isFile={isFile} - name=' ' + name='' + isEditing={true} + setIsEditing={() => {}} icons={isFile ? : } indentation={[{ isLast: true }]} onSaveValue={async ({ path }) => { diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx b/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx index 3621c5f7e..4d680f16f 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/index.tsx @@ -58,8 +58,10 @@ function FileNode({ }) { const allTmpFolders = useTempNodes((state) => state.tmpFolders) const tmpNodes = allTmpFolders[node.path] ?? [] - const { currentPath } = useFileTreeContext() - const [selected, setSelected] = useState(currentPath === node.path) + const { currentUuid } = useFileTreeContext() + const [selected, setSelected] = useState( + !!currentUuid && currentUuid === node.doc?.documentUuid, + ) const allNodes = useMemo( () => [...tmpNodes, ...node.children], [tmpNodes, node.children], @@ -79,8 +81,8 @@ function FileNode({ }, [togglePath, node.path, node.isPersisted]) useEffect(() => { - setSelected(currentPath === node.path) - }, [currentPath]) + setSelected(!!currentUuid && currentUuid === node.doc?.documentUuid) + }, [currentUuid]) return (
@@ -132,22 +134,24 @@ type DeletableElement = T extends DeletableType.File export function FilesTree({ isMerged, - currentPath, + currentUuid, documents, onMergeCommitClick, navigateToDocument, createFile, + renamePaths, destroyFile, destroyFolder, isDestroying, }: { isMerged: boolean createFile: (args: { path: string }) => Promise + renamePaths: (args: { oldPath: string; newPath: string }) => Promise destroyFile: (documentUuid: string) => Promise onMergeCommitClick: () => void destroyFolder: (path: string) => Promise documents: SidebarDocument[] - currentPath: string | undefined + currentUuid: string | undefined navigateToDocument: (documentUuid: string) => void isDestroying: boolean }) { @@ -156,11 +160,20 @@ export function FilesTree({ const [deletableNode, setDeletable] = useState | null>(null) + const currentPath = useMemo(() => { + if (!currentUuid) return undefined + const currentDocument = documents.find( + (d) => d.documentUuid === currentUuid, + ) + return currentDocument?.path + }, [currentUuid, documents]) + useEffect(() => { if (currentPath) { togglePath(currentPath) } }, [currentPath, togglePath]) + const onConfirmDelete = useCallback( async (deletable: DeletableElement) => { if (deletable.type === DeletableType.File) { @@ -180,11 +193,16 @@ export function FilesTree({ { + onCreateFile={(path) => { createFile({ path }) }} + onRenameFile={({ node, path }) => { + const oldPath = node.path + (node.isFile ? '' : '/') + const newPath = path + (node.isFile ? '' : '/') + renamePaths({ oldPath, newPath }) + }} onDeleteFile={({ node, documentUuid }) => { setDeletable({ type: DeletableType.File, diff --git a/packages/web-ui/src/sections/Document/Sidebar/Files/useTempNodes/index.ts b/packages/web-ui/src/sections/Document/Sidebar/Files/useTempNodes/index.ts index caca7003f..6c1d01968 100644 --- a/packages/web-ui/src/sections/Document/Sidebar/Files/useTempNodes/index.ts +++ b/packages/web-ui/src/sections/Document/Sidebar/Files/useTempNodes/index.ts @@ -178,9 +178,14 @@ export const useTempNodes = create((set, get) => ({ const node = allNodes.find((node) => node.id === id) if (!node) return state - const parentPath = node.path.split('/').slice(0, -1).join('/') - node.name = path - node.path = `${parentPath}/${path}` + const parentPathParts = node.path.split('/').slice(0, -1) + const newPathParts = path.split('/') + const newPath = [...parentPathParts, ...newPathParts].join('/') + + node.path = newPath + node.name = newPathParts.pop()! + + // TODO: Create in-between folders when new path contains nested paths. return state })