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:
@@ -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'),
|
||||
|
||||
@@ -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 },
|
||||
|
||||
148
frontend/src/components/ui-new/dialogs/RenameWorkspaceDialog.tsx
Normal file
148
frontend/src/components/ui-new/dialogs/RenameWorkspaceDialog.tsx
Normal 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);
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -129,7 +129,15 @@
|
||||
"archived": "アーカイブ済み",
|
||||
"loading": "読み込み中...",
|
||||
"selectToStart": "ワークスペースを選択して開始",
|
||||
"draft": "下書き"
|
||||
"draft": "下書き",
|
||||
"rename": {
|
||||
"title": "ワークスペースの名前を変更",
|
||||
"description": "このワークスペースの新しい名前を入力してください。",
|
||||
"nameLabel": "名前",
|
||||
"placeholder": "ワークスペース名を入力",
|
||||
"action": "名前を変更",
|
||||
"renaming": "名前を変更中..."
|
||||
}
|
||||
},
|
||||
"fileTree": {
|
||||
"searchPlaceholder": "ファイルを検索...",
|
||||
|
||||
@@ -129,7 +129,15 @@
|
||||
"archived": "보관됨",
|
||||
"loading": "로딩 중...",
|
||||
"selectToStart": "워크스페이스를 선택하여 시작",
|
||||
"draft": "초안"
|
||||
"draft": "초안",
|
||||
"rename": {
|
||||
"title": "워크스페이스 이름 변경",
|
||||
"description": "이 워크스페이스의 새 이름을 입력하세요.",
|
||||
"nameLabel": "이름",
|
||||
"placeholder": "워크스페이스 이름 입력",
|
||||
"action": "이름 변경",
|
||||
"renaming": "이름 변경 중..."
|
||||
}
|
||||
},
|
||||
"fileTree": {
|
||||
"searchPlaceholder": "파일 검색...",
|
||||
|
||||
@@ -129,7 +129,15 @@
|
||||
"archived": "已归档",
|
||||
"loading": "加载中...",
|
||||
"selectToStart": "选择一个工作区开始",
|
||||
"draft": "草稿"
|
||||
"draft": "草稿",
|
||||
"rename": {
|
||||
"title": "重命名工作区",
|
||||
"description": "输入此工作区的新名称。",
|
||||
"nameLabel": "名称",
|
||||
"placeholder": "输入工作区名称",
|
||||
"action": "重命名",
|
||||
"renaming": "正在重命名..."
|
||||
}
|
||||
},
|
||||
"fileTree": {
|
||||
"searchPlaceholder": "搜索文件...",
|
||||
|
||||
@@ -129,7 +129,15 @@
|
||||
"archived": "已封存",
|
||||
"loading": "載入中...",
|
||||
"selectToStart": "選擇一個工作區開始",
|
||||
"draft": "草稿"
|
||||
"draft": "草稿",
|
||||
"rename": {
|
||||
"title": "重新命名工作區",
|
||||
"description": "輸入此工作區的新名稱。",
|
||||
"nameLabel": "名稱",
|
||||
"placeholder": "輸入工作區名稱",
|
||||
"action": "重新命名",
|
||||
"renaming": "正在重新命名..."
|
||||
}
|
||||
},
|
||||
"fileTree": {
|
||||
"searchPlaceholder": "搜尋檔案...",
|
||||
|
||||
Reference in New Issue
Block a user