Skip to content

Commit 87c4372

Browse files
committed
fix(web): preserve archive scope until changed
1 parent 511761e commit 87c4372

8 files changed

Lines changed: 24 additions & 171 deletions

File tree

apps/web/src/archiveProjectFiltering.test.ts

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import { describe, expect, it } from "vite-plus/test";
77

88
import {
99
buildArchivedProjectModel,
10-
canValidateArchivedProjectKey,
1110
filterArchivedProjectGroups,
12-
resolveArchivedProjectSelection,
1311
validateArchivedThreadsSearch,
1412
} from "./archiveProjectFiltering";
1513

@@ -149,59 +147,7 @@ describe("archive project filtering", () => {
149147
expect(model.projectGroups.map((group) => group.displayName)).toEqual(["Removed project"]);
150148
});
151149

152-
it("only falls back to All for a stale project key after sources are ready", () => {
153-
const project = makeProject();
154-
const model = buildModel([project], [makeThread(project)]);
155-
const projectKey = model.projectGroups[0]!.projectKey;
156-
157-
expect(
158-
resolveArchivedProjectSelection({
159-
canValidateProjectKey: false,
160-
projectGroups: model.projectGroups,
161-
requestedProjectKey: "missing-project",
162-
}),
163-
).toEqual({
164-
selectedProjectKey: "missing-project",
165-
shouldClearRequestedProjectKey: false,
166-
});
167-
expect(
168-
resolveArchivedProjectSelection({
169-
canValidateProjectKey: true,
170-
projectGroups: model.projectGroups,
171-
requestedProjectKey: "missing-project",
172-
}),
173-
).toEqual({ selectedProjectKey: null, shouldClearRequestedProjectKey: true });
174-
expect(
175-
resolveArchivedProjectSelection({
176-
canValidateProjectKey: true,
177-
projectGroups: model.projectGroups,
178-
requestedProjectKey: projectKey,
179-
}),
180-
).toEqual({ selectedProjectKey: projectKey, shouldClearRequestedProjectKey: false });
181-
});
182-
183-
it("waits for every archive project-key source before validating", () => {
184-
const ready = {
185-
archiveError: null,
186-
archivesReady: true,
187-
environmentsReady: true,
188-
environmentTopologyReady: true,
189-
isLoadingArchive: false,
190-
settingsHydrated: true,
191-
};
192-
193-
expect(canValidateArchivedProjectKey(ready)).toBe(true);
194-
expect(canValidateArchivedProjectKey({ ...ready, archivesReady: false })).toBe(false);
195-
expect(canValidateArchivedProjectKey({ ...ready, environmentsReady: false })).toBe(false);
196-
expect(canValidateArchivedProjectKey({ ...ready, environmentTopologyReady: false })).toBe(
197-
false,
198-
);
199-
expect(canValidateArchivedProjectKey({ ...ready, settingsHydrated: false })).toBe(false);
200-
expect(canValidateArchivedProjectKey({ ...ready, isLoadingArchive: true })).toBe(false);
201-
expect(canValidateArchivedProjectKey({ ...ready, archiveError: "Failed" })).toBe(false);
202-
});
203-
204-
it("does not widen an unresolved project filter to All", () => {
150+
it("does not widen an unavailable project filter to All", () => {
205151
const project = makeProject();
206152
const model = buildModel([project], [makeThread(project)]);
207153

apps/web/src/archiveProjectFiltering.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ export interface ArchivedThreadsSearch {
2626
readonly project?: string;
2727
}
2828

29-
export interface ArchivedProjectSelection {
30-
readonly selectedProjectKey: string | null;
31-
readonly shouldClearRequestedProjectKey: boolean;
32-
}
33-
3429
export function validateArchivedThreadsSearch(raw: Record<string, unknown>): ArchivedThreadsSearch {
3530
return typeof raw.project === "string" && raw.project ? { project: raw.project } : {};
3631
}
@@ -95,44 +90,6 @@ export function buildArchivedProjectModel(input: {
9590
};
9691
}
9792

98-
export function canValidateArchivedProjectKey(input: {
99-
readonly archiveError: string | null;
100-
readonly archivesReady: boolean;
101-
readonly environmentsReady: boolean;
102-
readonly environmentTopologyReady: boolean;
103-
readonly isLoadingArchive: boolean;
104-
readonly settingsHydrated: boolean;
105-
}): boolean {
106-
return (
107-
input.environmentsReady &&
108-
input.environmentTopologyReady &&
109-
input.settingsHydrated &&
110-
input.archivesReady &&
111-
!input.isLoadingArchive &&
112-
input.archiveError === null
113-
);
114-
}
115-
116-
export function resolveArchivedProjectSelection(input: {
117-
readonly canValidateProjectKey: boolean;
118-
readonly projectGroups: ReadonlyArray<SidebarProjectSnapshot>;
119-
readonly requestedProjectKey: string | null;
120-
}): ArchivedProjectSelection {
121-
if (input.requestedProjectKey === null) {
122-
return { selectedProjectKey: null, shouldClearRequestedProjectKey: false };
123-
}
124-
if (
125-
input.projectGroups.some((group) => group.projectKey === input.requestedProjectKey) ||
126-
!input.canValidateProjectKey
127-
) {
128-
return {
129-
selectedProjectKey: input.requestedProjectKey,
130-
shouldClearRequestedProjectKey: false,
131-
};
132-
}
133-
return { selectedProjectKey: null, shouldClearRequestedProjectKey: true };
134-
}
135-
13693
export function filterArchivedProjectGroups(
13794
archivedGroups: ReadonlyArray<ArchivedProjectGroup>,
13895
projectKey: string | null,

apps/web/src/components/ProjectScopeBreadcrumb.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
WorkspaceBreadcrumbSeparator,
1111
} from "./WorkspaceBreadcrumb";
1212

13-
const ALL_PROJECTS_MENU_ID = "__all_projects__";
13+
const ALL_PROJECTS_MENU_ID = "all";
1414

1515
export interface ProjectScopeBreadcrumbItem {
1616
readonly id: string;
@@ -36,17 +36,27 @@ export function ProjectScopeBreadcrumb(props: {
3636
if (!api) return;
3737

3838
const rect = event.currentTarget.getBoundingClientRect();
39+
const projectKeyByMenuId = new Map<string, string>(
40+
props.items.map((item, index) => [`project:${index}`, item.id] as const),
41+
);
3942
const items: ContextMenuItem<string>[] = [
4043
...(props.allLabel
4144
? [{ id: ALL_PROJECTS_MENU_ID, label: props.allLabel } satisfies ContextMenuItem<string>]
4245
: []),
43-
...props.items,
46+
...props.items.map((item, index) => ({ id: `project:${index}`, label: item.label })),
4447
];
4548
void settlePromise(() =>
4649
api.contextMenu.show(items, { x: rect.left, y: rect.bottom + 4 }),
4750
).then((clicked) => {
4851
if (clicked._tag === "Failure" || clicked.value === null) return;
49-
props.onSelect(clicked.value === ALL_PROJECTS_MENU_ID ? null : clicked.value);
52+
if (clicked.value === ALL_PROJECTS_MENU_ID) {
53+
props.onSelect(null);
54+
return;
55+
}
56+
const projectKey = projectKeyByMenuId.get(clicked.value);
57+
if (projectKey !== undefined) {
58+
props.onSelect(projectKey);
59+
}
5060
});
5161
};
5262

apps/web/src/components/settings/SettingsBreadcrumb.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { useNavigate, useSearch } from "@tanstack/react-router";
2-
import { useEffect } from "react";
32

4-
import { resolveArchivedProjectSelection } from "../../archiveProjectFiltering";
53
import { useArchivedProjectModel } from "../../lib/archivedThreadsState";
64
import { ProjectScopeBreadcrumb } from "../ProjectScopeBreadcrumb";
75
import {
@@ -46,17 +44,7 @@ export function SettingsBreadcrumb({ pathname }: { pathname: string }) {
4644
function ArchivedThreadsBreadcrumb() {
4745
const search = useSearch({ from: "/settings/archived" });
4846
const navigate = useNavigate({ from: "/settings/archived" });
49-
const { canValidateProjectKey, isLoading, projectGroups } = useArchivedProjectModel();
50-
const selection = resolveArchivedProjectSelection({
51-
canValidateProjectKey,
52-
projectGroups,
53-
requestedProjectKey: search.project ?? null,
54-
});
55-
56-
useEffect(() => {
57-
if (!selection.shouldClearRequestedProjectKey) return;
58-
void navigate({ search: {}, replace: true, hashScrollIntoView: false });
59-
}, [navigate, selection.shouldClearRequestedProjectKey]);
47+
const { isLoading, projectGroups } = useArchivedProjectModel();
6048

6149
return (
6250
<ProjectScopeBreadcrumb
@@ -71,7 +59,7 @@ function ArchivedThreadsBreadcrumb() {
7159
});
7260
}}
7361
rootLabel="Archive"
74-
selectedKey={selection.selectedProjectKey}
62+
selectedKey={search.project ?? null}
7563
unavailableLabel={isLoading ? "Loading project" : "Unavailable project"}
7664
/>
7765
);

apps/web/src/components/settings/SettingsPanels.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,7 @@ import { ensureLocalApi, readLocalApi } from "../../localApi";
7878
import { isMacPlatform } from "../../lib/utils";
7979
import { primaryServerObservabilityAtom, primaryServerProvidersAtom } from "../../state/server";
8080
import { useArchivedProjectModel } from "../../lib/archivedThreadsState";
81-
import {
82-
filterArchivedProjectGroups,
83-
resolveArchivedProjectSelection,
84-
} from "../../archiveProjectFiltering";
81+
import { filterArchivedProjectGroups } from "../../archiveProjectFiltering";
8582
import { formatRelativeTimeLabel } from "../../timestampFormat";
8683
import { Button } from "../ui/button";
8784
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
@@ -2379,24 +2376,18 @@ export function ArchivedThreadsPanel({ projectKey }: { projectKey: string | null
23792376
const { unarchiveThread, confirmAndDeleteThread } = useThreadActions();
23802377
const {
23812378
archivedGroups,
2382-
canValidateProjectKey,
23832379
error: archiveError,
23842380
isLoading: isLoadingArchive,
23852381
projectGroups,
23862382
refresh: refreshArchivedThreads,
23872383
} = useArchivedProjectModel();
2388-
const selection = resolveArchivedProjectSelection({
2389-
canValidateProjectKey,
2390-
projectGroups,
2391-
requestedProjectKey: projectKey,
2392-
});
23932384
const selectedProject =
2394-
selection.selectedProjectKey === null
2385+
projectKey === null
23952386
? null
2396-
: (projectGroups.find((group) => group.projectKey === selection.selectedProjectKey) ?? null);
2387+
: (projectGroups.find((group) => group.projectKey === projectKey) ?? null);
23972388
const visibleArchivedGroups = useMemo(
2398-
() => filterArchivedProjectGroups(archivedGroups, selection.selectedProjectKey),
2399-
[archivedGroups, selection.selectedProjectKey],
2389+
() => filterArchivedProjectGroups(archivedGroups, projectKey),
2390+
[archivedGroups, projectKey],
24002391
);
24012392

24022393
const handleArchivedThreadContextMenu = useCallback(

apps/web/src/lib/archivedThreadsState.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import {
77
import type { EnvironmentId } from "@t3tools/contracts";
88
import { useCallback, useMemo } from "react";
99

10-
import {
11-
buildArchivedProjectModel,
12-
canValidateArchivedProjectKey,
13-
} from "../archiveProjectFiltering";
10+
import { buildArchivedProjectModel } from "../archiveProjectFiltering";
1411
import { selectProjectGroupingSettings } from "../logicalProject";
1512
import { useClientSettings, useClientSettingsHydrated } from "../hooks/useSettings";
1613
import { orchestrationEnvironment } from "../state/orchestration";
@@ -39,7 +36,6 @@ export function useArchivedThreadSnapshots(environmentIds: ReadonlyArray<Environ
3936
readonly snapshots: ReadonlyArray<ArchivedSnapshotEntry>;
4037
readonly error: string | null;
4138
readonly isLoading: boolean;
42-
readonly isReady: boolean;
4339
readonly refresh: () => void;
4440
} {
4541
const environmentKey = useMemo(
@@ -100,24 +96,11 @@ export function useArchivedProjectModel() {
10096
}, [archiveState.snapshots, environmentLabelById, primaryEnvironmentId, projectGroupingSettings]);
10197
const environmentTopologyReady = isHostedStaticApp() || primaryEnvironmentId !== null;
10298
const isLoading =
103-
archiveState.isLoading ||
104-
!archiveState.isReady ||
105-
!environmentsReady ||
106-
!environmentTopologyReady ||
107-
!settingsHydrated;
108-
const canValidateProjectKey = canValidateArchivedProjectKey({
109-
archiveError: archiveState.error,
110-
archivesReady: archiveState.isReady,
111-
environmentsReady,
112-
environmentTopologyReady,
113-
isLoadingArchive: archiveState.isLoading,
114-
settingsHydrated,
115-
});
99+
archiveState.isLoading || !environmentsReady || !environmentTopologyReady || !settingsHydrated;
116100

117101
return {
118102
...archiveState,
119103
...model,
120-
canValidateProjectKey,
121104
isLoading,
122105
};
123106
}

packages/client-runtime/src/state/archivedThreads.test.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,6 @@ it("does not expose an archived snapshot failure message", () => {
3434
snapshots: [],
3535
error: "Failed to load archived threads.",
3636
isLoading: false,
37-
isReady: true,
38-
});
39-
40-
registry.dispose();
41-
});
42-
43-
it("does not mark an initial archived snapshot as ready", () => {
44-
const environmentId = EnvironmentId.make("env-initial");
45-
const snapshotsAtom = createArchivedThreadSnapshotsAtomFamily<Error>({
46-
getSnapshotAtom: () => Atom.make(AsyncResult.initial<OrchestrationShellSnapshot, Error>(false)),
47-
labelPrefix: "test:archived-thread-snapshots",
48-
});
49-
const registry = AtomRegistry.make();
50-
51-
expect(registry.get(snapshotsAtom(makeArchivedThreadsEnvironmentKey([environmentId])))).toEqual({
52-
snapshots: [],
53-
error: null,
54-
isLoading: false,
55-
isReady: false,
5637
});
5738

5839
registry.dispose();

packages/client-runtime/src/state/archivedThreads.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface ArchivedThreadSnapshotsState {
1414
readonly snapshots: ReadonlyArray<ArchivedSnapshotEntry>;
1515
readonly error: string | null;
1616
readonly isLoading: boolean;
17-
readonly isReady: boolean;
1817
}
1918

2019
const ARCHIVED_THREADS_ENVIRONMENT_KEY_SEPARATOR = "\u001f";
@@ -49,12 +48,10 @@ export function createArchivedThreadSnapshotsAtomFamily<E>(options: {
4948
const snapshots: ArchivedSnapshotEntry[] = [];
5049
let error: string | null = null;
5150
let isLoading = false;
52-
let isReady = true;
5351

5452
for (const environmentId of parseArchivedThreadsEnvironmentKey(environmentKey)) {
5553
const result = get(options.getSnapshotAtom(environmentId));
5654
isLoading ||= result.waiting;
57-
isReady &&= result._tag !== "Initial";
5855

5956
const snapshot = Option.getOrNull(AsyncResult.value(result));
6057
if (snapshot !== null) {
@@ -66,7 +63,7 @@ export function createArchivedThreadSnapshotsAtomFamily<E>(options: {
6663
}
6764
}
6865

69-
return { snapshots, error, isLoading, isReady };
66+
return { snapshots, error, isLoading };
7067
}).pipe(Atom.withLabel(`${options.labelPrefix}:${environmentKey}`)),
7168
);
7269
}

0 commit comments

Comments
 (0)