Skip to content

Commit 2d079b9

Browse files
authored
[codex] Redesign planner sidebar someday sections (#1817)
* feat(web): redesign planner sidebar * chore(web): trim sidebar redesign diff * chore: revert skills lock update * test(web): remove temporary sidebar assertions * test(web): stabilize sidebar ci checks * fix(web): address someday sidebar review
1 parent 0e3897f commit 2d079b9

25 files changed

Lines changed: 653 additions & 121 deletions

e2e/someday/delete-someday-event-mouse.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
expectSomedayEventMissing,
66
expectSomedayEventVisible,
77
fillTitleAndSaveEventForm,
8+
openSomedayEventForEditingWithMouse,
89
openSomedayEventFormWithMouse,
910
prepareCalendarPage,
1011
} from "../utils/event-test-utils";
@@ -21,7 +22,7 @@ test("should delete a someday event using mouse interaction", async ({
2122
await fillTitleAndSaveEventForm(page, title);
2223
await expectSomedayEventVisible(page, title);
2324

24-
await page.locator("#sidebar").getByRole("button", { name: title }).click();
25+
await openSomedayEventForEditingWithMouse(page, title);
2526
await deleteEventWithMouse(page);
2627

2728
await expectSomedayEventMissing(page, title);

e2e/someday/update-someday-event-mouse.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
expectSomedayEventMissing,
55
expectSomedayEventVisible,
66
fillTitleAndSaveEventForm,
7+
openSomedayEventForEditingWithMouse,
78
openSomedayEventFormWithMouse,
89
prepareCalendarPage,
910
updateEventTitle,
@@ -21,7 +22,7 @@ test("should update a someday event using mouse interaction", async ({
2122
await fillTitleAndSaveEventForm(page, title);
2223
await expectSomedayEventVisible(page, title);
2324

24-
await page.locator("#sidebar").getByRole("button", { name: title }).click();
25+
await openSomedayEventForEditingWithMouse(page, title);
2526

2627
const updatedTitle = updateEventTitle("Someday Event");
2728
await fillTitleAndSaveEventForm(page, updatedTitle);

e2e/utils/event-test-utils.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ export const openSomedayEventFormWithMouse = async (
315315
section: SomedaySection,
316316
) => {
317317
await ensureSidebarOpen(page);
318-
const addButtonName = section === "week" ? "Add to week" : "Add to month";
318+
const addButtonName =
319+
section === "week" ? "Add item to week" : "Add item to month";
319320
await page
320321
.locator("#sidebar")
321322
.getByRole("button", { name: addButtonName })
@@ -421,6 +422,24 @@ export const openEventForEditingWithMouse = async (
421422
: new Error(`Unable to open event "${eventTitle}" for mouse editing.`);
422423
};
423424

425+
export const openSomedayEventForEditingWithMouse = async (
426+
page: Page,
427+
eventTitle: string,
428+
) => {
429+
await ensureSidebarOpen(page);
430+
431+
const titleInput = getFormTitleInput(page);
432+
const eventButton = page
433+
.locator("#sidebar")
434+
.getByRole("button", { name: eventTitle })
435+
.last();
436+
437+
await eventButton.waitFor({ state: "visible", timeout: FORM_TIMEOUT });
438+
await eventButton.scrollIntoViewIfNeeded();
439+
await eventButton.click();
440+
await expect(titleInput).toHaveValue(eventTitle, { timeout: FORM_TIMEOUT });
441+
};
442+
424443
export const deleteEventWithMouse = async (page: Page) => {
425444
const form = page.getByRole("form");
426445
await expect(form).toBeVisible();

packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ export type GoogleUiState = "checking" | "repairing" | GoogleConnectionState;
44

55
export type CommandActionIcon = "CloudArrowUpIcon";
66

7+
export type GoogleAccountSummaryStatus = {
8+
label: string;
9+
isHealthy: boolean;
10+
isLoading: boolean;
11+
} | null;
12+
713
export type GoogleUiConfig = {
814
commandAction: {
915
label: string;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getGoogleAccountSummaryStatus } from "./useConnectGoogle.util";
2+
import { describe, expect, it } from "bun:test";
3+
4+
describe("getGoogleAccountSummaryStatus", () => {
5+
it("returns no account summary status when Google is not connected", () => {
6+
expect(getGoogleAccountSummaryStatus("NOT_CONNECTED")).toBeNull();
7+
});
8+
9+
it("returns healthy copy for connected Google", () => {
10+
expect(getGoogleAccountSummaryStatus("HEALTHY")).toEqual({
11+
isHealthy: true,
12+
isLoading: false,
13+
label: "Synced with Google",
14+
});
15+
});
16+
17+
it.each([
18+
"IMPORTING",
19+
"repairing",
20+
] as const)("returns syncing copy for %s", (state) => {
21+
expect(getGoogleAccountSummaryStatus(state)).toEqual({
22+
isHealthy: false,
23+
isLoading: false,
24+
label: "Syncing...",
25+
});
26+
});
27+
28+
it("marks checking as loading", () => {
29+
expect(getGoogleAccountSummaryStatus("checking")).toEqual({
30+
isHealthy: false,
31+
isLoading: true,
32+
label: "Syncing...",
33+
});
34+
});
35+
36+
it("separates reconnect and repair copy", () => {
37+
expect(getGoogleAccountSummaryStatus("RECONNECT_REQUIRED")?.label).toBe(
38+
"Reconnect needed",
39+
);
40+
expect(getGoogleAccountSummaryStatus("ATTENTION")?.label).toBe(
41+
"Repair needed",
42+
);
43+
});
44+
});

packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.util.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { theme } from "@web/common/styles/theme";
22
import {
33
type CommandActionIcon,
4+
type GoogleAccountSummaryStatus,
45
type GoogleUiConfig,
56
type GoogleUiState,
67
} from "./useConnectGoogle.types";
78

89
const COMMAND_ICON: CommandActionIcon = "CloudArrowUpIcon";
10+
const SYNCING_ACCOUNT_SUMMARY_LABEL = "Syncing...";
911
type RepairDialog = NonNullable<GoogleUiConfig["sidebarStatus"]["dialog"]>;
1012

1113
const buildRepairDialog = (onRepairGoogle: () => void): RepairDialog => ({
@@ -118,3 +120,43 @@ export const getGoogleConnectionConfig = (
118120
};
119121
}
120122
};
123+
124+
export const getGoogleAccountSummaryStatus = (
125+
state: GoogleUiState,
126+
): GoogleAccountSummaryStatus => {
127+
switch (state) {
128+
case "HEALTHY":
129+
return {
130+
isHealthy: true,
131+
isLoading: false,
132+
label: "Synced with Google",
133+
};
134+
case "IMPORTING":
135+
case "repairing":
136+
return {
137+
isHealthy: false,
138+
isLoading: false,
139+
label: SYNCING_ACCOUNT_SUMMARY_LABEL,
140+
};
141+
case "checking":
142+
return {
143+
isHealthy: false,
144+
isLoading: true,
145+
label: SYNCING_ACCOUNT_SUMMARY_LABEL,
146+
};
147+
case "RECONNECT_REQUIRED":
148+
return {
149+
isHealthy: false,
150+
isLoading: false,
151+
label: "Reconnect needed",
152+
};
153+
case "ATTENTION":
154+
return {
155+
isHealthy: false,
156+
isLoading: false,
157+
label: "Repair needed",
158+
};
159+
case "NOT_CONNECTED":
160+
return null;
161+
}
162+
};

packages/web/src/components/PlannerSidebar/PlannerAccountSummary/PlannerAccountSummary.test.tsx

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import { render, screen } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
3+
import { type GoogleUiState } from "@web/auth/google/hooks/useConnectGoogle/useConnectGoogle.types";
34
import { beforeEach, describe, expect, it, mock } from "bun:test";
45

56
const mockOpenModal = mock();
67
let mockEmail: string | undefined;
8+
let mockGoogleState: GoogleUiState = "NOT_CONNECTED";
9+
const mockUseConnectGoogle = mock(() => ({
10+
state: mockGoogleState,
11+
}));
712

813
mock.module("@web/auth/compass/user/hooks/useUser", () => ({
914
useUser: () => ({
1015
email: mockEmail,
1116
}),
1217
}));
1318

19+
mock.module("@web/auth/google/hooks/useConnectGoogle/useConnectGoogle", () => ({
20+
useConnectGoogle: mockUseConnectGoogle,
21+
}));
22+
1423
mock.module("@web/components/AuthModal/hooks/useAuthModal", () => ({
1524
useAuthModal: () => ({
1625
openModal: mockOpenModal,
@@ -19,6 +28,7 @@ mock.module("@web/components/AuthModal/hooks/useAuthModal", () => ({
1928

2029
mock.module("@phosphor-icons/react", () => ({
2130
InfoIcon: () => <span aria-hidden="true">info</span>,
31+
PlusIcon: () => <span aria-hidden="true">plus</span>,
2232
}));
2333

2434
const { PlannerAccountSummary } =
@@ -27,7 +37,9 @@ const { PlannerAccountSummary } =
2737
describe("PlannerAccountSummary", () => {
2838
beforeEach(() => {
2939
mockEmail = undefined;
40+
mockGoogleState = "NOT_CONNECTED";
3041
mockOpenModal.mockClear();
42+
mockUseConnectGoogle.mockClear();
3143
});
3244

3345
it("shows a sign up prompt for temporary accounts", async () => {
@@ -44,6 +56,7 @@ describe("PlannerAccountSummary", () => {
4456
expect(screen.getByText("Temporary account")).toBeTruthy();
4557
expect(screen.getByText("Sign up")).toBeTruthy();
4658
expect(mockOpenModal).toHaveBeenCalledWith("signUp");
59+
expect(mockUseConnectGoogle).not.toHaveBeenCalled();
4760
});
4861

4962
it("shows a plain account identity for authenticated accounts", () => {
@@ -52,7 +65,73 @@ describe("PlannerAccountSummary", () => {
5265
render(<PlannerAccountSummary />);
5366

5467
expect(screen.getByText("ugur@example.com")).toBeTruthy();
55-
expect(screen.queryByText("Changes saved")).toBeNull();
5668
expect(screen.queryByRole("button")).toBeNull();
69+
expect(mockUseConnectGoogle).toHaveBeenCalledTimes(1);
70+
});
71+
72+
it("shows the Google sync status without a status landmark when synced", () => {
73+
mockEmail = "ugur@example.com";
74+
mockGoogleState = "HEALTHY";
75+
76+
render(<PlannerAccountSummary />);
77+
78+
expect(screen.getByText("Synced with Google")).toBeTruthy();
79+
expect(screen.queryByRole("status")).toBeNull();
80+
});
81+
82+
it("shows a syncing label while Google is importing", () => {
83+
mockEmail = "ugur@example.com";
84+
mockGoogleState = "IMPORTING";
85+
86+
render(<PlannerAccountSummary />);
87+
88+
expect(screen.getByText("Syncing...")).toBeTruthy();
89+
});
90+
91+
it("shows the syncing label while Google repair is running", () => {
92+
mockEmail = "ugur@example.com";
93+
mockGoogleState = "repairing";
94+
95+
render(<PlannerAccountSummary />);
96+
97+
expect(screen.getByText("Syncing...")).toBeTruthy();
98+
});
99+
100+
it("shows syncing copy before Google metadata loads", () => {
101+
mockEmail = "ugur@example.com";
102+
mockGoogleState = "checking";
103+
104+
render(<PlannerAccountSummary />);
105+
106+
expect(screen.getByText("Syncing...")).toBeTruthy();
107+
});
108+
109+
it("shows repair copy when Google sync needs attention", () => {
110+
mockEmail = "ugur@example.com";
111+
mockGoogleState = "ATTENTION";
112+
113+
render(<PlannerAccountSummary />);
114+
115+
expect(screen.getByText("Repair needed")).toBeTruthy();
116+
expect(screen.queryByText("Reconnect needed")).toBeNull();
117+
});
118+
119+
it("shows reconnect copy only when Google credentials need reconnecting", () => {
120+
mockEmail = "ugur@example.com";
121+
mockGoogleState = "RECONNECT_REQUIRED";
122+
123+
render(<PlannerAccountSummary />);
124+
125+
expect(screen.getByText("Reconnect needed")).toBeTruthy();
126+
});
127+
128+
it("hides the sync line when Google is not connected", () => {
129+
mockEmail = "ugur@example.com";
130+
mockGoogleState = "NOT_CONNECTED";
131+
132+
render(<PlannerAccountSummary />);
133+
134+
expect(screen.queryByText("Synced with Google")).toBeNull();
135+
expect(screen.queryByText("Reconnect needed")).toBeNull();
57136
});
58137
});

0 commit comments

Comments
 (0)