feat(app): auto-focus terminal on create/switch, stabilize sidebar ordering for new items
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { test, expect, type Page } from "./fixtures";
|
import { test, expect, type Page } from "./fixtures";
|
||||||
|
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||||
import {
|
import {
|
||||||
createAgent,
|
createAgent,
|
||||||
createAgentInRepo,
|
createAgentInRepo,
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
setWorkingDirectory,
|
setWorkingDirectory,
|
||||||
} from "./helpers/app";
|
} from "./helpers/app";
|
||||||
import { createTempGitRepo } from "./helpers/workspace";
|
import { createTempGitRepo } from "./helpers/workspace";
|
||||||
|
import { getWorkspaceTabTestIds } from "./helpers/workspace-tabs";
|
||||||
import { switchWorkspaceViaSidebar } from "./helpers/workspace-ui";
|
import { switchWorkspaceViaSidebar } from "./helpers/workspace-ui";
|
||||||
|
|
||||||
function visibleTestId(page: Page, testId: string) {
|
function visibleTestId(page: Page, testId: string) {
|
||||||
@@ -264,6 +266,23 @@ async function readCurrentTerminalBuffer(page: Page): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectTerminalFocused(page: Page): Promise<void> {
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
return await page.evaluate(() => {
|
||||||
|
const surface = document.querySelector<HTMLElement>(
|
||||||
|
'[data-testid="terminal-surface"]'
|
||||||
|
);
|
||||||
|
if (!surface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const active = document.activeElement;
|
||||||
|
return active instanceof HTMLElement && surface.contains(active);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
async function expectCurrentTerminalBufferToContain(page: Page, marker: string): Promise<void> {
|
async function expectCurrentTerminalBufferToContain(page: Page, marker: string): Promise<void> {
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => await readCurrentTerminalBuffer(page), { timeout: 30000 })
|
.poll(async () => await readCurrentTerminalBuffer(page), { timeout: 30000 })
|
||||||
@@ -376,11 +395,7 @@ test("new terminal does not inherit output from the previously selected terminal
|
|||||||
directory: repo.path,
|
directory: repo.path,
|
||||||
prompt: "hello",
|
prompt: "hello",
|
||||||
});
|
});
|
||||||
await switchWorkspaceViaSidebar({
|
await page.goto(buildHostWorkspaceRoute(serverId, repo.path));
|
||||||
page,
|
|
||||||
serverId,
|
|
||||||
targetWorkspacePath: repo.path,
|
|
||||||
});
|
|
||||||
await expect(page.getByTestId("workspace-new-terminal-tab").first()).toBeVisible({
|
await expect(page.getByTestId("workspace-new-terminal-tab").first()).toBeVisible({
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
@@ -403,6 +418,74 @@ test("new terminal does not inherit output from the previously selected terminal
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("workspace terminal tabs auto-focus on create and switch on desktop web", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const repo = await createTempGitRepo("paseo-e2e-terminal-focus-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serverId = process.env.E2E_SERVER_ID;
|
||||||
|
if (!serverId) {
|
||||||
|
throw new Error("E2E_SERVER_ID is not set.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await createAgentInRepo(page, {
|
||||||
|
directory: repo.path,
|
||||||
|
prompt: "hello",
|
||||||
|
});
|
||||||
|
await page.goto(buildHostWorkspaceRoute(serverId, repo.path));
|
||||||
|
await expect(page.getByTestId("workspace-new-terminal-tab").first()).toBeVisible({
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("workspace-new-terminal-tab").first().click();
|
||||||
|
await waitForTerminalAttachToSettle(page);
|
||||||
|
await expectTerminalFocused(page);
|
||||||
|
|
||||||
|
const firstMarker = `terminal-focus-one-${Date.now()}`;
|
||||||
|
await page.keyboard.type(`echo ${firstMarker}`, { delay: 1 });
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
await expectCurrentTerminalBufferToContain(page, firstMarker);
|
||||||
|
|
||||||
|
const terminalTabIdsBeforeSecondCreate = (await getWorkspaceTabTestIds(page)).filter((id) =>
|
||||||
|
id.startsWith("workspace-tab-terminal_")
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByTestId("workspace-new-terminal-tab").first().click();
|
||||||
|
await waitForTerminalAttachToSettle(page);
|
||||||
|
await expectTerminalFocused(page);
|
||||||
|
|
||||||
|
const terminalTabIdsAfterSecondCreate = (await getWorkspaceTabTestIds(page)).filter((id) =>
|
||||||
|
id.startsWith("workspace-tab-terminal_")
|
||||||
|
);
|
||||||
|
const secondTerminalTabId = terminalTabIdsAfterSecondCreate.find(
|
||||||
|
(id) => !terminalTabIdsBeforeSecondCreate.includes(id)
|
||||||
|
);
|
||||||
|
const firstTerminalTabId = terminalTabIdsBeforeSecondCreate[0];
|
||||||
|
|
||||||
|
if (!firstTerminalTabId || !secondTerminalTabId) {
|
||||||
|
throw new Error("Expected two distinct terminal tabs to exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondMarker = `terminal-focus-two-${Date.now()}`;
|
||||||
|
await page.keyboard.type(`echo ${secondMarker}`, { delay: 1 });
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
await expectCurrentTerminalBufferToContain(page, secondMarker);
|
||||||
|
|
||||||
|
await page.getByTestId(firstTerminalTabId).first().click();
|
||||||
|
await waitForTerminalAttachToSettle(page);
|
||||||
|
await expectTerminalFocused(page);
|
||||||
|
await expectCurrentTerminalBufferToContain(page, firstMarker);
|
||||||
|
|
||||||
|
await page.getByTestId(secondTerminalTabId).first().click();
|
||||||
|
await waitForTerminalAttachToSettle(page);
|
||||||
|
await expectTerminalFocused(page);
|
||||||
|
await expectCurrentTerminalBufferToContain(page, secondMarker);
|
||||||
|
} finally {
|
||||||
|
await repo.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("terminal reattaches cleanly after heavy output and tab switches", async ({ page }) => {
|
test("terminal reattaches cleanly after heavy output and tab switches", async ({ page }) => {
|
||||||
const repo = await createTempGitRepo("paseo-e2e-terminal-reattach-");
|
const repo = await createTempGitRepo("paseo-e2e-terminal-reattach-");
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "@xterm/xterm/css/xterm.css";
|
|||||||
import type { ITheme } from "@xterm/xterm";
|
import type { ITheme } from "@xterm/xterm";
|
||||||
import type { PendingTerminalModifiers } from "../utils/terminal-keys";
|
import type { PendingTerminalModifiers } from "../utils/terminal-keys";
|
||||||
import { TerminalEmulatorRuntime } from "../terminal/runtime/terminal-emulator-runtime";
|
import { TerminalEmulatorRuntime } from "../terminal/runtime/terminal-emulator-runtime";
|
||||||
|
import { focusWithRetries } from "../utils/web-focus";
|
||||||
import {
|
import {
|
||||||
summarizeTerminalText,
|
summarizeTerminalText,
|
||||||
terminalDebugLog,
|
terminalDebugLog,
|
||||||
@@ -343,7 +344,19 @@ export default function TerminalEmulator({
|
|||||||
if (focusRequestToken <= 0) {
|
if (focusRequestToken <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runtimeRef.current?.focus();
|
return focusWithRetries({
|
||||||
|
focus: () => {
|
||||||
|
runtimeRef.current?.focus();
|
||||||
|
},
|
||||||
|
isFocused: () => {
|
||||||
|
const root = rootRef.current;
|
||||||
|
if (!root) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const active = typeof document !== "undefined" ? document.activeElement : null;
|
||||||
|
return active instanceof HTMLElement && root.contains(active);
|
||||||
|
},
|
||||||
|
});
|
||||||
}, [focusRequestToken]);
|
}, [focusRequestToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export function TerminalPane({
|
|||||||
const selectedTerminalIdRef = useRef<string | null>(selectedTerminalId);
|
const selectedTerminalIdRef = useRef<string | null>(selectedTerminalId);
|
||||||
const pendingTerminalInputRef = useRef<PendingTerminalInput[]>([]);
|
const pendingTerminalInputRef = useRef<PendingTerminalInput[]>([]);
|
||||||
const keyboardRefitTimeoutsRef = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
const keyboardRefitTimeoutsRef = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
||||||
|
const lastAutoFocusKeyRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const updateSelectedTerminalId = useCallback(
|
const updateSelectedTerminalId = useCallback(
|
||||||
(next: string | null) => {
|
(next: string | null) => {
|
||||||
@@ -279,6 +280,21 @@ export function TerminalPane({
|
|||||||
setResizeRequestToken((current) => current + 1);
|
setResizeRequestToken((current) => current + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMobile || !isScreenFocused || !selectedTerminalId) {
|
||||||
|
lastAutoFocusKeyRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextFocusKey = `${scopeKey}:${selectedTerminalId}`;
|
||||||
|
if (lastAutoFocusKeyRef.current === nextFocusKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAutoFocusKeyRef.current = nextFocusKey;
|
||||||
|
requestTerminalFocus();
|
||||||
|
}, [isMobile, isScreenFocused, requestTerminalFocus, scopeKey, selectedTerminalId]);
|
||||||
|
|
||||||
const clearKeyboardRefitTimeouts = useCallback(() => {
|
const clearKeyboardRefitTimeouts = useCallback(() => {
|
||||||
if (keyboardRefitTimeoutsRef.current.length === 0) {
|
if (keyboardRefitTimeoutsRef.current.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
|
appendMissingOrderKeys,
|
||||||
applyStoredOrdering,
|
applyStoredOrdering,
|
||||||
buildSidebarProjectsFromWorkspaces,
|
buildSidebarProjectsFromWorkspaces,
|
||||||
} from './use-sidebar-workspaces-list'
|
} from './use-sidebar-workspaces-list'
|
||||||
@@ -46,6 +47,28 @@ describe('applyStoredOrdering', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('appendMissingOrderKeys', () => {
|
||||||
|
it('appends unseen keys while preserving existing order', () => {
|
||||||
|
const result = appendMissingOrderKeys({
|
||||||
|
currentOrder: ['project-b', 'project-a'],
|
||||||
|
visibleKeys: ['project-a', 'project-b', 'project-c'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(['project-b', 'project-a', 'project-c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the same array when there are no unseen keys', () => {
|
||||||
|
const currentOrder = ['project-a', 'project-b']
|
||||||
|
|
||||||
|
const result = appendMissingOrderKeys({
|
||||||
|
currentOrder,
|
||||||
|
visibleKeys: ['project-b', 'project-a'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toBe(currentOrder)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('buildSidebarProjectsFromWorkspaces', () => {
|
describe('buildSidebarProjectsFromWorkspaces', () => {
|
||||||
it('uses workspace descriptor name and status directly', () => {
|
it('uses workspace descriptor name and status directly', () => {
|
||||||
const workspaces: WorkspaceDescriptor[] = [
|
const workspaces: WorkspaceDescriptor[] = [
|
||||||
@@ -70,4 +93,148 @@ describe('buildSidebarProjectsFromWorkspaces', () => {
|
|||||||
expect(projects[0]?.workspaces[0]?.name).toBe('feat/hard-cut')
|
expect(projects[0]?.workspaces[0]?.name).toBe('feat/hard-cut')
|
||||||
expect(projects[0]?.workspaces[0]?.statusBucket).toBe('failed')
|
expect(projects[0]?.workspaces[0]?.statusBucket).toBe('failed')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('preserves stored project order even when activity changes', () => {
|
||||||
|
const initialWorkspaces: WorkspaceDescriptor[] = [
|
||||||
|
{
|
||||||
|
id: '/repo/b',
|
||||||
|
projectId: 'project-b',
|
||||||
|
name: 'feat/b',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '/repo/a',
|
||||||
|
projectId: 'project-a',
|
||||||
|
name: 'feat/a',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const seededOrder = appendMissingOrderKeys({
|
||||||
|
currentOrder: [],
|
||||||
|
visibleKeys: buildSidebarProjectsFromWorkspaces({
|
||||||
|
serverId: 'srv',
|
||||||
|
workspaces: initialWorkspaces,
|
||||||
|
projectOrder: [],
|
||||||
|
workspaceOrderByScope: {},
|
||||||
|
}).map((project) => project.projectKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedProjects = buildSidebarProjectsFromWorkspaces({
|
||||||
|
serverId: 'srv',
|
||||||
|
workspaces: [
|
||||||
|
{
|
||||||
|
id: '/repo/b',
|
||||||
|
projectId: 'project-b',
|
||||||
|
name: 'feat/b',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '/repo/a',
|
||||||
|
projectId: 'project-a',
|
||||||
|
name: 'feat/a',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-03T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectOrder: seededOrder,
|
||||||
|
workspaceOrderByScope: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(updatedProjects.map((project) => project.projectKey)).toEqual(['project-b', 'project-a'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends new projects after the stored project order', () => {
|
||||||
|
const projects = buildSidebarProjectsFromWorkspaces({
|
||||||
|
serverId: 'srv',
|
||||||
|
workspaces: [
|
||||||
|
{
|
||||||
|
id: '/repo/c',
|
||||||
|
projectId: 'project-c',
|
||||||
|
name: 'feat/c',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-04T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '/repo/b',
|
||||||
|
projectId: 'project-b',
|
||||||
|
name: 'feat/b',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '/repo/a',
|
||||||
|
projectId: 'project-a',
|
||||||
|
name: 'feat/a',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectOrder: ['project-b', 'project-a', 'project-c'],
|
||||||
|
workspaceOrderByScope: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(projects.map((project) => project.projectKey)).toEqual(['project-b', 'project-a', 'project-c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves stored workspace order when workspace activity changes', () => {
|
||||||
|
const initialProjects = buildSidebarProjectsFromWorkspaces({
|
||||||
|
serverId: 'srv',
|
||||||
|
workspaces: [
|
||||||
|
{
|
||||||
|
id: '/repo/main',
|
||||||
|
projectId: 'project-1',
|
||||||
|
name: 'main',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '/repo/feature',
|
||||||
|
projectId: 'project-1',
|
||||||
|
name: 'feature',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectOrder: ['project-1'],
|
||||||
|
workspaceOrderByScope: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const seededWorkspaceOrder = appendMissingOrderKeys({
|
||||||
|
currentOrder: [],
|
||||||
|
visibleKeys: initialProjects[0]?.workspaces.map((workspace) => workspace.workspaceKey) ?? [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const projects = buildSidebarProjectsFromWorkspaces({
|
||||||
|
serverId: 'srv',
|
||||||
|
workspaces: [
|
||||||
|
{
|
||||||
|
id: '/repo/main',
|
||||||
|
projectId: 'project-1',
|
||||||
|
name: 'main',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-02T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '/repo/feature',
|
||||||
|
projectId: 'project-1',
|
||||||
|
name: 'feature',
|
||||||
|
status: 'running',
|
||||||
|
activityAt: new Date('2026-01-03T00:00:00.000Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectOrder: ['project-1'],
|
||||||
|
workspaceOrderByScope: {
|
||||||
|
'srv::project-1': seededWorkspaceOrder,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(projects[0]?.workspaces.map((workspace) => workspace.workspaceId)).toEqual([
|
||||||
|
'/repo/main',
|
||||||
|
'/repo/feature',
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'
|
||||||
import { useSessionStore } from '@/stores/session-store'
|
import { useSessionStore } from '@/stores/session-store'
|
||||||
import { getHostRuntimeStore } from '@/runtime/host-runtime'
|
import { getHostRuntimeStore } from '@/runtime/host-runtime'
|
||||||
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
|
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
|
||||||
@@ -217,6 +217,23 @@ export function applyStoredOrdering<T>(input: {
|
|||||||
return ordered
|
return ordered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function appendMissingOrderKeys(input: {
|
||||||
|
currentOrder: string[]
|
||||||
|
visibleKeys: string[]
|
||||||
|
}): string[] {
|
||||||
|
if (input.visibleKeys.length === 0) {
|
||||||
|
return input.currentOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingKeys = new Set(input.currentOrder)
|
||||||
|
const missingKeys = input.visibleKeys.filter((key) => !existingKeys.has(key))
|
||||||
|
if (missingKeys.length === 0) {
|
||||||
|
return input.currentOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...input.currentOrder, ...missingKeys]
|
||||||
|
}
|
||||||
|
|
||||||
function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string {
|
function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string {
|
||||||
return `${serverId.trim()}::${projectKey.trim()}`
|
return `${serverId.trim()}::${projectKey.trim()}`
|
||||||
}
|
}
|
||||||
@@ -296,6 +313,35 @@ export function useSidebarWorkspacesList(options?: {
|
|||||||
})
|
})
|
||||||
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, serverId, sessionWorkspaces])
|
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, serverId, sessionWorkspaces])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!serverId || projects.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextProjectOrder = appendMissingOrderKeys({
|
||||||
|
currentOrder: persistedProjectOrder,
|
||||||
|
visibleKeys: projects.map((project) => project.projectKey),
|
||||||
|
})
|
||||||
|
if (nextProjectOrder !== persistedProjectOrder) {
|
||||||
|
useSidebarOrderStore.getState().setProjectOrder(serverId, nextProjectOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const project of projects) {
|
||||||
|
const workspaceOrderScopeKey = getWorkspaceOrderScopeKey(serverId, project.projectKey)
|
||||||
|
const persistedWorkspaceOrder =
|
||||||
|
persistedWorkspaceOrderByScope[workspaceOrderScopeKey] ?? EMPTY_ORDER
|
||||||
|
const nextWorkspaceOrder = appendMissingOrderKeys({
|
||||||
|
currentOrder: persistedWorkspaceOrder,
|
||||||
|
visibleKeys: project.workspaces.map((workspace) => workspace.workspaceKey),
|
||||||
|
})
|
||||||
|
if (nextWorkspaceOrder !== persistedWorkspaceOrder) {
|
||||||
|
useSidebarOrderStore
|
||||||
|
.getState()
|
||||||
|
.setWorkspaceOrder(serverId, project.projectKey, nextWorkspaceOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, projects, serverId])
|
||||||
|
|
||||||
const refreshAll = useCallback(() => {
|
const refreshAll = useCallback(() => {
|
||||||
if (!isActive || !serverId || connectionStatus !== 'online' || !enabled) {
|
if (!isActive || !serverId || connectionStatus !== 'online' || !enabled) {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user