Add rename workspace functionality (Vibe Kanban) (#1868)

* 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)
This commit is contained in:
Anastasiia Solop
2026-01-09 10:06:29 +01:00
committed by GitHub
parent 078e7fc372
commit b743f849f7
9 changed files with 219 additions and 6 deletions

View File

@@ -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'),

View File

@@ -91,6 +91,7 @@ export const Pages: Record<PageId, CommandBarPage> = {
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 },

View File

@@ -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<RenameWorkspaceDialogProps>(
({ workspaceId, currentName }) => {
const modal = useModal();
const { t } = useTranslation(['common']);
const queryClient = useQueryClient();
const [name, setName] = useState<string>(currentName);
const [error, setError] = useState<string | null>(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 (
<Dialog open={modal.visible} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('workspaces.rename.title')}</DialogTitle>
<DialogDescription>
{t('workspaces.rename.description')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="workspace-name" className="text-sm font-medium">
{t('workspaces.rename.nameLabel')}
</label>
<Input
id="workspace-name"
type="text"
value={name}
onChange={(e) => {
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 && <p className="text-sm text-destructive">{error}</p>}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={handleCancel}
disabled={renameMutation.isPending}
>
{t('buttons.cancel')}
</Button>
<Button onClick={handleConfirm} disabled={renameMutation.isPending}>
{renameMutation.isPending
? t('workspaces.rename.renaming')
: t('workspaces.rename.action')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
);
export const RenameWorkspaceDialog = defineModal<
RenameWorkspaceDialogProps,
RenameWorkspaceDialogResult
>(RenameWorkspaceDialogImpl);

View File

@@ -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...",

View File

@@ -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...",

View File

@@ -129,7 +129,15 @@
"archived": "アーカイブ済み",
"loading": "読み込み中...",
"selectToStart": "ワークスペースを選択して開始",
"draft": "下書き"
"draft": "下書き",
"rename": {
"title": "ワークスペースの名前を変更",
"description": "このワークスペースの新しい名前を入力してください。",
"nameLabel": "名前",
"placeholder": "ワークスペース名を入力",
"action": "名前を変更",
"renaming": "名前を変更中..."
}
},
"fileTree": {
"searchPlaceholder": "ファイルを検索...",

View File

@@ -129,7 +129,15 @@
"archived": "보관됨",
"loading": "로딩 중...",
"selectToStart": "워크스페이스를 선택하여 시작",
"draft": "초안"
"draft": "초안",
"rename": {
"title": "워크스페이스 이름 변경",
"description": "이 워크스페이스의 새 이름을 입력하세요.",
"nameLabel": "이름",
"placeholder": "워크스페이스 이름 입력",
"action": "이름 변경",
"renaming": "이름 변경 중..."
}
},
"fileTree": {
"searchPlaceholder": "파일 검색...",

View File

@@ -129,7 +129,15 @@
"archived": "已归档",
"loading": "加载中...",
"selectToStart": "选择一个工作区开始",
"draft": "草稿"
"draft": "草稿",
"rename": {
"title": "重命名工作区",
"description": "输入此工作区的新名称。",
"nameLabel": "名称",
"placeholder": "输入工作区名称",
"action": "重命名",
"renaming": "正在重命名..."
}
},
"fileTree": {
"searchPlaceholder": "搜索文件...",

View File

@@ -129,7 +129,15 @@
"archived": "已封存",
"loading": "載入中...",
"selectToStart": "選擇一個工作區開始",
"draft": "草稿"
"draft": "草稿",
"rename": {
"title": "重新命名工作區",
"description": "輸入此工作區的新名稱。",
"nameLabel": "名稱",
"placeholder": "輸入工作區名稱",
"action": "重新命名",
"renaming": "正在重新命名..."
}
},
"fileTree": {
"searchPlaceholder": "搜尋檔案...",