mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
Eight actions were only reachable through the sidebar workspace menu, the workspace header menu, or a keybind you had to remember: Rename workspace, Copy workspace path, Copy branch name, Pin/Unpin, the three layout toggles, and Show setup. All eight are now in the command center, plus a new one: label assignment. Type a label's name to assign or unassign it on the active workspace without leaving the keyboard. Rename needed more than a palette entry. Its dialog lived inside the sidebar row component, so it disappeared whenever that row was not rendered: a collapsed project or status group, a collapsed Pinned section, or focus mode. It now has a global host keyed on the active workspace selection, mirroring use-global-workspace-pin-action.ts. Each entry reuses the handler the existing menu item already calls, so behaviour matches the menu. Entries that cannot act are omitted rather than present and failing: Copy branch name without a branch, Pin when the host lacks workspacePinning, Show setup when there is nothing to show, and labels when the catalog has not loaded. Toggle Explorer sidebar and Toggle focus mode are registered as workspace contributions, not global ones. Their handlers live in workspace-screen.tsx behind an isRouteFocused gate, so a global registration would have listed two entries that close the palette and do nothing on /settings, /sessions, /schedules and Home. The copy-path, copy-branch, and rename-dialog logic was duplicated across three sidebar files. It moves into one hook and one component so the palette is not a fourth copy. That also fixes drift in sidebar-status-list.tsx, where Copy branch name copied the workspace name instead of the branch and four strings were hardcoded English. This supersedes #3383, which fixes the same bug directly without the shared-hook refactor. Toggle focus mode also had a hidden duplicate: buildPaneContributions already dispatches the same action once split panes are available, so the standalone entry now steps aside there instead of listing the toggle twice. No new i18n keys.
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { WorkspaceRenameModal } from "@/components/workspace-rename-modal";
|
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
|
import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
|
|
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
|
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
|
|
|
const WORKSPACE_RENAME_ACTIONS: readonly KeyboardActionId[] = ["workspace.rename"];
|
|
|
|
/**
|
|
* The rename dialog used to live only on the sidebar row, so it was unreachable whenever that row
|
|
* was not rendered — a collapsed project or status group, a collapsed Pinned section, or focus
|
|
* mode. This is the same fix `use-global-workspace-pin-action.ts` applies to pin: one registration
|
|
* keyed on the active route selection.
|
|
*
|
|
* Sidebar rows keep their own modal instances. They rename *their* workspace, not the active one.
|
|
*/
|
|
export function WorkspaceRenameHost() {
|
|
const selection = useActiveWorkspaceSelection();
|
|
const serverId = selection?.serverId ?? null;
|
|
const routeWorkspaceId = selection?.workspaceId ?? null;
|
|
// Narrow projection so the dialog doesn't re-render on every gitRuntime/diffStat tick.
|
|
// `id` is projected rather than reusing the route id: the route carries an opaque workspace id
|
|
// that is not guaranteed to equal the descriptor id, and setWorkspaceTitle needs the descriptor.
|
|
const fields = useWorkspaceFields(serverId, routeWorkspaceId, (workspace) => ({
|
|
id: workspace.id,
|
|
name: workspace.name,
|
|
title: workspace.title ?? null,
|
|
}));
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const openFrameRef = useRef<number | null>(null);
|
|
|
|
const cancelPendingOpen = useCallback(() => {
|
|
if (openFrameRef.current !== null) {
|
|
cancelAnimationFrame(openFrameRef.current);
|
|
openFrameRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => cancelPendingOpen, [cancelPendingOpen]);
|
|
|
|
// The command center closes and dispatches in the same React batch, so opening synchronously
|
|
// would mount this modal while the palette is still unmounting — on Android the portal stacking
|
|
// lands the dialog behind it, and on web the palette's teardown steals focus back from the
|
|
// input. Wait a frame. (The caller has already cleared the focus-restore element.)
|
|
const handle = useCallback(() => {
|
|
if (!fields) return false;
|
|
cancelPendingOpen();
|
|
openFrameRef.current = requestAnimationFrame(() => {
|
|
openFrameRef.current = null;
|
|
setIsOpen(true);
|
|
});
|
|
return true;
|
|
}, [cancelPendingOpen, fields]);
|
|
|
|
const handleClose = useCallback(() => {
|
|
cancelPendingOpen();
|
|
setIsOpen(false);
|
|
}, [cancelPendingOpen]);
|
|
|
|
// Closing on workspace change avoids renaming whatever the user navigated to instead.
|
|
useEffect(() => {
|
|
cancelPendingOpen();
|
|
setIsOpen(false);
|
|
}, [cancelPendingOpen, serverId, routeWorkspaceId]);
|
|
|
|
useKeyboardActionHandler({
|
|
handlerId: "workspace-rename-global",
|
|
actions: WORKSPACE_RENAME_ACTIONS,
|
|
enabled: serverId !== null && fields !== null,
|
|
priority: 0,
|
|
handle,
|
|
});
|
|
|
|
const workspace = useMemo(
|
|
() =>
|
|
serverId && fields
|
|
? { serverId, workspaceId: fields.id, name: fields.name, title: fields.title }
|
|
: null,
|
|
[fields, serverId],
|
|
);
|
|
|
|
if (!workspace) return null;
|
|
|
|
return (
|
|
<WorkspaceRenameModal
|
|
visible={isOpen}
|
|
workspace={workspace}
|
|
onClose={handleClose}
|
|
testID="workspace-rename-modal-global"
|
|
/>
|
|
);
|
|
}
|