Group and clear workspace status in the sidebar (#1317)

* Add status grouping to the workspace sidebar

* Mark workspaces as read from the sidebar

* Clean package builds before release checks

* Fix sidebar status follow-up checks
This commit is contained in:
Mohamed Boudra
2026-06-04 01:20:20 +08:00
committed by GitHub
parent 71ce96b913
commit 31ae545289
65 changed files with 3758 additions and 290 deletions
@@ -7,21 +7,26 @@ import {
serializeCollapsedProjects,
setProjectCollapsed,
toggleProjectCollapsed,
toggleStatusGroupCollapsed,
} from "./state";
interface SidebarCollapsedSectionsState extends CollapsedProjectsState {
toggleProjectCollapsed: (projectKey: string) => void;
setProjectCollapsed: (projectKey: string, collapsed: boolean) => void;
toggleStatusGroupCollapsed: (statusGroupKey: string) => void;
}
export const useSidebarCollapsedSectionsStore = create<SidebarCollapsedSectionsState>()(
persist(
(set) => ({
collapsedProjectKeys: new Set(),
collapsedStatusGroupKeys: new Set(),
toggleProjectCollapsed: (projectKey) =>
set((state) => toggleProjectCollapsed(state, projectKey)),
setProjectCollapsed: (projectKey, collapsed) =>
set((state) => setProjectCollapsed(state, projectKey, collapsed)),
toggleStatusGroupCollapsed: (statusGroupKey) =>
set((state) => toggleStatusGroupCollapsed(state, statusGroupKey)),
}),
{
name: "sidebar-collapsed-sections",
@@ -5,10 +5,11 @@ import {
serializeCollapsedProjects,
setProjectCollapsed,
toggleProjectCollapsed,
toggleStatusGroupCollapsed,
} from "@/stores/sidebar-collapsed-sections-store/state";
function emptyState(): CollapsedProjectsState {
return { collapsedProjectKeys: new Set() };
return { collapsedProjectKeys: new Set(), collapsedStatusGroupKeys: new Set() };
}
describe("sidebar collapsed projects transitions", () => {
@@ -18,17 +19,21 @@ describe("sidebar collapsed projects transitions", () => {
state = setProjectCollapsed(state, "project-a", true);
state = toggleProjectCollapsed(state, "project-b");
state = toggleProjectCollapsed(state, "project-a");
state = toggleStatusGroupCollapsed(state, "running");
expect(Array.from(state.collapsedProjectKeys)).toEqual(["project-b"]);
expect(Array.from(state.collapsedStatusGroupKeys)).toEqual(["running"]);
});
it("serializes collapsed project keys for preference storage", () => {
const state: CollapsedProjectsState = {
collapsedProjectKeys: new Set(["project-a", "project-b"]),
collapsedStatusGroupKeys: new Set(["running"]),
};
expect(serializeCollapsedProjects(state)).toEqual({
collapsedProjectKeys: ["project-a", "project-b"],
collapsedStatusGroupKeys: ["running"],
});
});
@@ -39,6 +44,7 @@ describe("sidebar collapsed projects transitions", () => {
);
expect(Array.from(restored.collapsedProjectKeys)).toEqual(["project-a", "project-b"]);
expect(Array.from(restored.collapsedStatusGroupKeys)).toEqual([]);
});
it("keeps the existing state object when persisted preferences do not change collapsed keys", () => {
@@ -1,9 +1,11 @@
export interface CollapsedProjectsState {
collapsedProjectKeys: Set<string>;
collapsedStatusGroupKeys: Set<string>;
}
export interface PersistedCollapsedProjects {
collapsedProjectKeys?: unknown;
collapsedStatusGroupKeys?: unknown;
}
export function toggleProjectCollapsed(
@@ -16,7 +18,20 @@ export function toggleProjectCollapsed(
} else {
next.add(projectKey);
}
return { collapsedProjectKeys: next };
return { ...state, collapsedProjectKeys: next };
}
export function toggleStatusGroupCollapsed(
state: CollapsedProjectsState,
statusGroupKey: string,
): CollapsedProjectsState {
const next = new Set(state.collapsedStatusGroupKeys);
if (next.has(statusGroupKey)) {
next.delete(statusGroupKey);
} else {
next.add(statusGroupKey);
}
return { ...state, collapsedStatusGroupKeys: next };
}
export function setProjectCollapsed(
@@ -30,13 +45,17 @@ export function setProjectCollapsed(
} else {
next.delete(projectKey);
}
return { collapsedProjectKeys: next };
return { ...state, collapsedProjectKeys: next };
}
export function serializeCollapsedProjects(state: CollapsedProjectsState): {
collapsedProjectKeys: string[];
collapsedStatusGroupKeys: string[];
} {
return { collapsedProjectKeys: Array.from(state.collapsedProjectKeys) };
return {
collapsedProjectKeys: Array.from(state.collapsedProjectKeys),
collapsedStatusGroupKeys: Array.from(state.collapsedStatusGroupKeys),
};
}
export function mergePersistedCollapsedProjects<S extends CollapsedProjectsState>(
@@ -44,16 +63,24 @@ export function mergePersistedCollapsedProjects<S extends CollapsedProjectsState
current: S,
): S {
if (!persisted?.collapsedProjectKeys) {
if (!persisted?.collapsedStatusGroupKeys) return current;
}
const restoredProjects = deserializeCollapsedKeys(persisted.collapsedProjectKeys);
const restoredStatusGroups = deserializeCollapsedKeys(persisted.collapsedStatusGroupKeys);
if (
areSetsEqual(current.collapsedProjectKeys, restoredProjects) &&
areSetsEqual(current.collapsedStatusGroupKeys, restoredStatusGroups)
) {
return current;
}
const restored = deserializeCollapsedProjectKeys(persisted.collapsedProjectKeys);
if (areSetsEqual(current.collapsedProjectKeys, restored)) {
return current;
}
return { ...current, collapsedProjectKeys: restored };
return {
...current,
collapsedProjectKeys: restoredProjects,
collapsedStatusGroupKeys: restoredStatusGroups,
};
}
function deserializeCollapsedProjectKeys(value: unknown): Set<string> {
function deserializeCollapsedKeys(value: unknown): Set<string> {
if (!Array.isArray(value)) {
return new Set();
}