feat(app): make pinned workspaces sortable (#3341)

Reuse the existing workspace drag interaction for pinned rows and persist their local order across grouping modes. Keep pressed and dragged row scrims aligned with the active backdrop, and preserve memoized row rendering on web.
This commit is contained in:
Mohamed Boudra
2026-08-13 23:48:01 +02:00
committed by GitHub
parent 373a98c64d
commit b03dc804eb
16 changed files with 602 additions and 95 deletions
@@ -16,9 +16,18 @@ describe("migrateSidebarOrderState", () => {
expect(migrated).toEqual({
projectOrder: ["project-a"],
pinnedWorkspaceOrder: [],
workspaceOrderByProject: {
"project-a": ["host-a:main", "host-a:feature", "host-b:main"],
},
});
});
it("normalizes pinned workspace order", () => {
const migrated = migrateSidebarOrderState({
pinnedWorkspaceOrder: [" host-a:one ", "host-a:one", "", "host-b:two"],
});
expect(migrated.pinnedWorkspaceOrder).toEqual(["host-a:one", "host-b:two"]);
});
});
+19 -2
View File
@@ -6,15 +6,19 @@ import { createValidatedPersistStorage } from "@/storage/validated-persist-stora
interface SidebarOrderStoreState {
projectOrder: string[];
pinnedWorkspaceOrder: string[];
workspaceOrderByProject: Record<string, string[]>;
getProjectOrder: () => string[];
setProjectOrder: (keys: string[]) => void;
getPinnedWorkspaceOrder: () => string[];
setPinnedWorkspaceOrder: (keys: string[]) => void;
getWorkspaceOrder: (projectViewKey: string) => string[];
setWorkspaceOrder: (projectViewKey: string, keys: string[]) => void;
}
interface SidebarOrderPersistedState {
projectOrder?: string[];
pinnedWorkspaceOrder?: string[];
workspaceOrderByProject?: Record<string, string[]>;
projectOrderByServerId?: Record<string, string[]>;
workspaceOrderByServerAndProject?: Record<string, string[]>;
@@ -23,6 +27,7 @@ interface SidebarOrderPersistedState {
const StringArrayRecordSchema = z.record(z.string(), z.array(z.string()));
const SidebarOrderPersistedStateSchema = z.strictObject({
projectOrder: z.array(z.string()).optional(),
pinnedWorkspaceOrder: z.array(z.string()).optional(),
workspaceOrderByProject: StringArrayRecordSchema.optional(),
projectOrderByServerId: StringArrayRecordSchema.optional(),
workspaceOrderByServerAndProject: StringArrayRecordSchema.optional(),
@@ -79,11 +84,12 @@ function normalizeLegacyWorkspaceKey(serverId: string, rawWorkspaceKey: string):
export function migrateSidebarOrderState(persistedState: unknown): {
projectOrder: string[];
pinnedWorkspaceOrder: string[];
workspaceOrderByProject: Record<string, string[]>;
} {
const result = SidebarOrderPersistedStateSchema.safeParse(persistedState);
if (!result.success) {
return { projectOrder: [], workspaceOrderByProject: {} };
return { projectOrder: [], pinnedWorkspaceOrder: [], workspaceOrderByProject: {} };
}
const state: SidebarOrderPersistedState = result.data;
@@ -113,19 +119,29 @@ export function migrateSidebarOrderState(persistedState: unknown): {
workspaceOrderByProject[scope.projectViewKey] = merged;
}
return { projectOrder, workspaceOrderByProject };
return {
projectOrder,
pinnedWorkspaceOrder: normalizeKeys(state.pinnedWorkspaceOrder ?? []),
workspaceOrderByProject,
};
}
export const useSidebarOrderStore = create<SidebarOrderStoreState>()(
persist(
(set, get) => ({
projectOrder: [],
pinnedWorkspaceOrder: [],
workspaceOrderByProject: {},
getProjectOrder: () => get().projectOrder,
setProjectOrder: (keys) => {
const normalized = normalizeKeys(keys);
set({ projectOrder: normalized });
},
getPinnedWorkspaceOrder: () => get().pinnedWorkspaceOrder,
setPinnedWorkspaceOrder: (keys) => {
const normalized = normalizeKeys(keys);
set({ pinnedWorkspaceOrder: normalized });
},
getWorkspaceOrder: (projectViewKey) => {
const scope = projectViewKey.trim();
if (!scope) return [];
@@ -148,6 +164,7 @@ export const useSidebarOrderStore = create<SidebarOrderStoreState>()(
storage: createValidatedPersistStorage(AsyncStorage, SidebarOrderPersistedStateSchema),
partialize: (state) => ({
projectOrder: state.projectOrder,
pinnedWorkspaceOrder: state.pinnedWorkspaceOrder,
workspaceOrderByProject: state.workspaceOrderByProject,
}),
version: 1,