From b743f849f72219ee9f740a9591b5dcfa709d20dd Mon Sep 17 00:00:00 2001 From: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:06:29 +0100 Subject: [PATCH] Add rename workspace functionality (Vibe Kanban) (#1868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * I've added the rename workspace functionality. Here's a summary of the changes: ## Summary The backend already had rename workspace support via the `PUT /api/task-attempts/{attemptId}` endpoint with the `name` field. I added the frontend UI to expose this functionality. ### Files Created 1. **`frontend/src/components/ui-new/dialogs/RenameWorkspaceDialog.tsx`** - New dialog component for renaming workspaces - Shows the current name (or branch name as fallback) - Allows user to input a new name - Handles mutation with proper error handling and cache invalidation ### Files Modified 1. **`frontend/src/components/ui-new/actions/index.ts`** - Added `PencilSimpleIcon` import - Added `RenameWorkspaceDialog` import - Added new `RenameWorkspace` action that shows the rename dialog 2. **`frontend/src/components/ui-new/actions/pages.ts`** - Added `Actions.RenameWorkspace` to the `workspaceActions` page (at the top of the list) 3. **`frontend/src/i18n/locales/en/common.json`** - Added translations for the rename workspace dialog under `workspaces.rename` ### How to Use Users can now rename a workspace by: 1. Clicking the three-dot menu (⋯) on any workspace in the sidebar 2. Selecting "Rename" from the command bar 3. Entering a new name in the dialog 4. Clicking "Rename" to save * Cleanup script changes for workspace 4eb64331-cc43-43af-849e-3731664e53b9 * Added the missing translations for the rename workspace dialog to all locale files: - **es** (Spanish) - **ja** (Japanese) - **ko** (Korean) - **zh-Hans** (Simplified Chinese) - **zh-Hant** (Traditional Chinese) --- .../src/components/ui-new/actions/index.ts | 16 ++ .../src/components/ui-new/actions/pages.ts | 1 + .../ui-new/dialogs/RenameWorkspaceDialog.tsx | 148 ++++++++++++++++++ frontend/src/i18n/locales/en/common.json | 10 +- frontend/src/i18n/locales/es/common.json | 10 +- frontend/src/i18n/locales/ja/common.json | 10 +- frontend/src/i18n/locales/ko/common.json | 10 +- frontend/src/i18n/locales/zh-Hans/common.json | 10 +- frontend/src/i18n/locales/zh-Hant/common.json | 10 +- 9 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/ui-new/dialogs/RenameWorkspaceDialog.tsx diff --git a/frontend/src/components/ui-new/actions/index.ts b/frontend/src/components/ui-new/actions/index.ts index db2bf8ea..0a3f6e76 100644 --- a/frontend/src/components/ui-new/actions/index.ts +++ b/frontend/src/components/ui-new/actions/index.ts @@ -29,6 +29,7 @@ import { ArrowsClockwiseIcon, CrosshairIcon, DesktopIcon, + PencilSimpleIcon, } from '@phosphor-icons/react'; import { useDiffViewStore } from '@/stores/useDiffViewStore'; import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore'; @@ -40,6 +41,7 @@ import { workspaceSummaryKeys } from '@/components/ui-new/hooks/useWorkspaces'; import { ConfirmDialog } from '@/components/ui-new/dialogs/ConfirmDialog'; import { ChangeTargetDialog } from '@/components/ui-new/dialogs/ChangeTargetDialog'; import { RebaseDialog } from '@/components/ui-new/dialogs/RebaseDialog'; +import { RenameWorkspaceDialog } from '@/components/ui-new/dialogs/RenameWorkspaceDialog'; import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog'; import { getIdeName } from '@/components/ide/IdeIcon'; import { EditorSelectionDialog } from '@/components/dialogs/tasks/EditorSelectionDialog'; @@ -191,6 +193,20 @@ export const Actions = { }, }, + RenameWorkspace: { + id: 'rename-workspace', + label: 'Rename', + icon: PencilSimpleIcon, + requiresTarget: true, + execute: async (ctx, workspaceId) => { + const workspace = getWorkspaceFromCache(ctx.queryClient, workspaceId); + await RenameWorkspaceDialog.show({ + workspaceId, + currentName: workspace.name || workspace.branch, + }); + }, + }, + PinWorkspace: { id: 'pin-workspace', label: (workspace?: Workspace) => (workspace?.pinned ? 'Unpin' : 'Pin'), diff --git a/frontend/src/components/ui-new/actions/pages.ts b/frontend/src/components/ui-new/actions/pages.ts index 45cc4ee9..49c24305 100644 --- a/frontend/src/components/ui-new/actions/pages.ts +++ b/frontend/src/components/ui-new/actions/pages.ts @@ -91,6 +91,7 @@ export const Pages: Record = { type: 'group', label: 'Workspace', items: [ + { type: 'action', action: Actions.RenameWorkspace }, { type: 'action', action: Actions.DuplicateWorkspace }, { type: 'action', action: Actions.PinWorkspace }, { type: 'action', action: Actions.ArchiveWorkspace }, diff --git a/frontend/src/components/ui-new/dialogs/RenameWorkspaceDialog.tsx b/frontend/src/components/ui-new/dialogs/RenameWorkspaceDialog.tsx new file mode 100644 index 00000000..8fb73e7a --- /dev/null +++ b/frontend/src/components/ui-new/dialogs/RenameWorkspaceDialog.tsx @@ -0,0 +1,148 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import NiceModal, { useModal } from '@ebay/nice-modal-react'; +import { defineModal } from '@/lib/modals'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { attemptsApi } from '@/lib/api'; +import { attemptKeys } from '@/hooks/useAttempt'; +import { workspaceSummaryKeys } from '@/components/ui-new/hooks/useWorkspaces'; + +export interface RenameWorkspaceDialogProps { + workspaceId: string; + currentName: string; +} + +export type RenameWorkspaceDialogResult = { + action: 'confirmed' | 'canceled'; + name?: string; +}; + +const RenameWorkspaceDialogImpl = NiceModal.create( + ({ workspaceId, currentName }) => { + const modal = useModal(); + const { t } = useTranslation(['common']); + const queryClient = useQueryClient(); + const [name, setName] = useState(currentName); + const [error, setError] = useState(null); + + useEffect(() => { + setName(currentName); + setError(null); + }, [currentName]); + + const renameMutation = useMutation({ + mutationFn: async (newName: string) => { + return attemptsApi.update(workspaceId, { name: newName }); + }, + onSuccess: (_, newName) => { + queryClient.invalidateQueries({ + queryKey: attemptKeys.byId(workspaceId), + }); + queryClient.invalidateQueries({ queryKey: workspaceSummaryKeys.all }); + modal.resolve({ + action: 'confirmed', + name: newName, + } as RenameWorkspaceDialogResult); + modal.hide(); + }, + onError: (err: unknown) => { + setError( + err instanceof Error ? err.message : 'Failed to rename workspace' + ); + }, + }); + + const handleConfirm = () => { + const trimmedName = name.trim(); + + if (trimmedName === currentName) { + modal.resolve({ action: 'canceled' } as RenameWorkspaceDialogResult); + modal.hide(); + return; + } + + setError(null); + renameMutation.mutate(trimmedName); + }; + + const handleCancel = () => { + modal.resolve({ action: 'canceled' } as RenameWorkspaceDialogResult); + modal.hide(); + }; + + const handleOpenChange = (open: boolean) => { + if (!open) { + handleCancel(); + } + }; + + return ( + + + + {t('workspaces.rename.title')} + + {t('workspaces.rename.description')} + + + +
+
+ + { + setName(e.target.value); + setError(null); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && !renameMutation.isPending) { + handleConfirm(); + } + }} + placeholder={t('workspaces.rename.placeholder')} + disabled={renameMutation.isPending} + autoFocus + /> + {error &&

{error}

} +
+
+ + + + + +
+
+ ); + } +); + +export const RenameWorkspaceDialog = defineModal< + RenameWorkspaceDialogProps, + RenameWorkspaceDialogResult +>(RenameWorkspaceDialogImpl); diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 02ee3c6f..3b9af1fe 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -129,7 +129,15 @@ "archived": "Archived", "loading": "Loading...", "selectToStart": "Select a workspace to get started", - "draft": "Draft" + "draft": "Draft", + "rename": { + "title": "Rename Workspace", + "description": "Enter a new name for this workspace.", + "nameLabel": "Name", + "placeholder": "Enter workspace name", + "action": "Rename", + "renaming": "Renaming..." + } }, "fileTree": { "searchPlaceholder": "Search files...", diff --git a/frontend/src/i18n/locales/es/common.json b/frontend/src/i18n/locales/es/common.json index ef53c579..4982a45d 100644 --- a/frontend/src/i18n/locales/es/common.json +++ b/frontend/src/i18n/locales/es/common.json @@ -129,7 +129,15 @@ "archived": "Archivado", "loading": "Cargando...", "selectToStart": "Selecciona un espacio de trabajo para comenzar", - "draft": "Borrador" + "draft": "Borrador", + "rename": { + "title": "Renombrar espacio de trabajo", + "description": "Ingresa un nuevo nombre para este espacio de trabajo.", + "nameLabel": "Nombre", + "placeholder": "Ingresa el nombre del espacio de trabajo", + "action": "Renombrar", + "renaming": "Renombrando..." + } }, "fileTree": { "searchPlaceholder": "Buscar archivos...", diff --git a/frontend/src/i18n/locales/ja/common.json b/frontend/src/i18n/locales/ja/common.json index 2ace03bc..709a0fde 100644 --- a/frontend/src/i18n/locales/ja/common.json +++ b/frontend/src/i18n/locales/ja/common.json @@ -129,7 +129,15 @@ "archived": "アーカイブ済み", "loading": "読み込み中...", "selectToStart": "ワークスペースを選択して開始", - "draft": "下書き" + "draft": "下書き", + "rename": { + "title": "ワークスペースの名前を変更", + "description": "このワークスペースの新しい名前を入力してください。", + "nameLabel": "名前", + "placeholder": "ワークスペース名を入力", + "action": "名前を変更", + "renaming": "名前を変更中..." + } }, "fileTree": { "searchPlaceholder": "ファイルを検索...", diff --git a/frontend/src/i18n/locales/ko/common.json b/frontend/src/i18n/locales/ko/common.json index 19743c0d..521640dd 100644 --- a/frontend/src/i18n/locales/ko/common.json +++ b/frontend/src/i18n/locales/ko/common.json @@ -129,7 +129,15 @@ "archived": "보관됨", "loading": "로딩 중...", "selectToStart": "워크스페이스를 선택하여 시작", - "draft": "초안" + "draft": "초안", + "rename": { + "title": "워크스페이스 이름 변경", + "description": "이 워크스페이스의 새 이름을 입력하세요.", + "nameLabel": "이름", + "placeholder": "워크스페이스 이름 입력", + "action": "이름 변경", + "renaming": "이름 변경 중..." + } }, "fileTree": { "searchPlaceholder": "파일 검색...", diff --git a/frontend/src/i18n/locales/zh-Hans/common.json b/frontend/src/i18n/locales/zh-Hans/common.json index 664e7cf1..a7c84d33 100644 --- a/frontend/src/i18n/locales/zh-Hans/common.json +++ b/frontend/src/i18n/locales/zh-Hans/common.json @@ -129,7 +129,15 @@ "archived": "已归档", "loading": "加载中...", "selectToStart": "选择一个工作区开始", - "draft": "草稿" + "draft": "草稿", + "rename": { + "title": "重命名工作区", + "description": "输入此工作区的新名称。", + "nameLabel": "名称", + "placeholder": "输入工作区名称", + "action": "重命名", + "renaming": "正在重命名..." + } }, "fileTree": { "searchPlaceholder": "搜索文件...", diff --git a/frontend/src/i18n/locales/zh-Hant/common.json b/frontend/src/i18n/locales/zh-Hant/common.json index efab1a78..6cf661e6 100644 --- a/frontend/src/i18n/locales/zh-Hant/common.json +++ b/frontend/src/i18n/locales/zh-Hant/common.json @@ -129,7 +129,15 @@ "archived": "已封存", "loading": "載入中...", "selectToStart": "選擇一個工作區開始", - "draft": "草稿" + "draft": "草稿", + "rename": { + "title": "重新命名工作區", + "description": "輸入此工作區的新名稱。", + "nameLabel": "名稱", + "placeholder": "輸入工作區名稱", + "action": "重新命名", + "renaming": "正在重新命名..." + } }, "fileTree": { "searchPlaceholder": "搜尋檔案...",