Skip to content

Commit de05b0c

Browse files
authored
fix(web): restore manual sort drag and keep per-group expand state (pingdotgg#2221)
1 parent 6d1505c commit de05b0c

8 files changed

Lines changed: 398 additions & 44 deletions

File tree

apps/web/src/components/Sidebar.logic.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,42 @@ describe("orderItemsByPreferredIds", () => {
309309
ProjectId.make("project-1"),
310310
]);
311311
});
312+
313+
it("honors projectOrder physical keys via getProjectOrderKey", async () => {
314+
// Regression guard for #1904 / the regression introduced by #2055:
315+
// `projectOrder` is populated with physical keys (envId + cwd-derived)
316+
// by the store and by drag-end handlers. Readers must identify projects
317+
// with the same key format, or manual sort silently snaps back.
318+
const { getProjectOrderKey } = await import("../logicalProject");
319+
const projects = [
320+
{
321+
environmentId: EnvironmentId.make("environment-local"),
322+
id: ProjectId.make("id-alpha"),
323+
cwd: "/work/alpha",
324+
},
325+
{
326+
environmentId: EnvironmentId.make("environment-local"),
327+
id: ProjectId.make("id-beta"),
328+
cwd: "/work/beta",
329+
},
330+
{
331+
environmentId: EnvironmentId.make("environment-local"),
332+
id: ProjectId.make("id-gamma"),
333+
cwd: "/work/gamma",
334+
},
335+
];
336+
const ordered = orderItemsByPreferredIds({
337+
items: projects,
338+
preferredIds: [getProjectOrderKey(projects[2]!), getProjectOrderKey(projects[0]!)],
339+
getId: getProjectOrderKey,
340+
});
341+
342+
expect(ordered.map((project) => project.cwd)).toEqual([
343+
"/work/gamma",
344+
"/work/alpha",
345+
"/work/beta",
346+
]);
347+
});
312348
});
313349

314350
describe("resolveAdjacentThreadId", () => {

apps/web/src/components/Sidebar.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ import { CommandDialogTrigger } from "./ui/command";
168168
import { readEnvironmentApi } from "../environmentApi";
169169
import { useSettings, useUpdateSettings } from "~/hooks/useSettings";
170170
import { useServerKeybindings } from "../rpc/serverState";
171-
import { derivePhysicalProjectKey, deriveProjectGroupingOverrideKey } from "../logicalProject";
171+
import {
172+
derivePhysicalProjectKey,
173+
deriveProjectGroupingOverrideKey,
174+
getProjectOrderKey,
175+
} from "../logicalProject";
172176
import {
173177
useSavedEnvironmentRegistryStore,
174178
useSavedEnvironmentRuntimeStore,
@@ -2708,7 +2712,7 @@ export default function Sidebar() {
27082712
return orderItemsByPreferredIds({
27092713
items: projects,
27102714
preferredIds: projectOrder,
2711-
getId: (project) => scopedProjectKey(scopeProjectRef(project.environmentId, project.id)),
2715+
getId: getProjectOrderKey,
27122716
});
27132717
}, [projectOrder, projects]);
27142718

apps/web/src/environments/runtime/service.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ import { useTerminalStateStore } from "~/terminalStateStore";
6161
import { useUiStateStore } from "~/uiStateStore";
6262
import { WsTransport } from "../../rpc/wsTransport";
6363
import { createWsRpcClient, type WsRpcClient } from "../../rpc/wsRpcClient";
64-
import { derivePhysicalProjectKey } from "../../logicalProject";
64+
import {
65+
deriveLogicalProjectKeyFromSettings,
66+
derivePhysicalProjectKey,
67+
} from "../../logicalProject";
68+
import { getClientSettings } from "~/hooks/useSettings";
6569

6670
type EnvironmentServiceState = {
6771
readonly queryClient: QueryClient;
@@ -468,9 +472,11 @@ function coalesceOrchestrationUiEvents(
468472

469473
function syncProjectUiFromStore() {
470474
const projects = selectProjectsAcrossEnvironments(useStore.getState());
475+
const clientSettings = getClientSettings();
471476
useUiStateStore.getState().syncProjects(
472477
projects.map((project) => ({
473478
key: derivePhysicalProjectKey(project),
479+
logicalKey: deriveLogicalProjectKeyFromSettings(project, clientSettings),
474480
cwd: project.cwd,
475481
})),
476482
);
@@ -541,9 +547,11 @@ function applyRecoveredEventBatch(
541547
useStore.getState().applyOrchestrationEvents(uiEvents, environmentId);
542548
if (needsProjectUiSync) {
543549
const projects = selectProjectsAcrossEnvironments(useStore.getState());
550+
const clientSettings = getClientSettings();
544551
useUiStateStore.getState().syncProjects(
545552
projects.map((project) => ({
546553
key: derivePhysicalProjectKey(project),
554+
logicalKey: deriveLogicalProjectKeyFromSettings(project, clientSettings),
547555
cwd: project.cwd,
548556
})),
549557
);

apps/web/src/hooks/useHandleNewThread.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "../composerDraftStore";
1111
import { newDraftId, newThreadId } from "../lib/utils";
1212
import { orderItemsByPreferredIds } from "../components/Sidebar.logic";
13-
import { deriveLogicalProjectKeyFromSettings } from "../logicalProject";
13+
import { deriveLogicalProjectKeyFromSettings, getProjectOrderKey } from "../logicalProject";
1414
import { selectProjectsAcrossEnvironments, useStore } from "../store";
1515
import { createThreadSelectorByRef } from "../storeSelectors";
1616
import { resolveThreadRouteTarget } from "../threadRoutes";
@@ -169,7 +169,7 @@ export function useHandleNewThread() {
169169
return orderItemsByPreferredIds({
170170
items: projects,
171171
preferredIds: projectOrder,
172-
getId: (project) => scopedProjectKey(scopeProjectRef(project.environmentId, project.id)),
172+
getId: getProjectOrderKey,
173173
});
174174
}, [projectOrder, projects]);
175175
const handleNewThread = useNewThreadState();

apps/web/src/hooks/useSettings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ function splitPatch(patch: Partial<UnifiedSettings>): {
123123
* only re-render when the slice they care about changes.
124124
*/
125125

126+
/**
127+
* Non-hook accessor for the current merged client settings snapshot.
128+
* Used by non-React code paths (e.g. runtime services) that need the latest
129+
* settings without subscribing.
130+
*/
131+
export function getClientSettings(): ClientSettings {
132+
return getClientSettingsSnapshot();
133+
}
134+
126135
export function useSettings<T = UnifiedSettings>(selector?: (s: UnifiedSettings) => T): T {
127136
const serverSettings = useServerSettings();
128137
const clientSettings = useSyncExternalStore(

apps/web/src/logicalProject.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ export function deriveProjectGroupingOverrideKey(
6565
return derivePhysicalProjectKey(project);
6666
}
6767

68+
// Key under which a project's manual sort order (projectOrder) is stored.
69+
// Must stay aligned with the writer side in `uiStateStore.syncProjects` and
70+
// the drag handlers in `Sidebar` so readers and writers agree.
71+
export function getProjectOrderKey(project: Pick<Project, "environmentId" | "cwd">): string {
72+
return derivePhysicalProjectKey(project);
73+
}
74+
6875
export function resolveProjectGroupingMode(
6976
project: Pick<Project, "environmentId" | "cwd">,
7077
settings: ProjectGroupingSettings,

0 commit comments

Comments
 (0)