-
Notifications
You must be signed in to change notification settings - Fork 65
feat(shell): restore the last app screen after restart #3965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
478c0e4
Restore the last app location on startup
puemos 2038b65
Tighten startup location boundaries
puemos 969b86a
Simplify startup location restoration
puemos 5939e4e
Clarify initial route loading
puemos 1547521
Keep personal channel setup in canvas
puemos 9781a3d
Use a concise state storage name
puemos 412ed35
fix(shell): scope personal channel startup cache
puemos f10c73a
chore(visual): update storybook baselines
posthog[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,46 +1,82 @@ | ||
| import type { Channel } from "@posthog/ui/features/canvas/hooks/useChannels"; | ||
| import type { PostHogAPIClient } from "@posthog/api-client/posthog-client"; | ||
| import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels"; | ||
|
|
||
| export interface PersonalChannel { | ||
| id: string; | ||
| name: string; | ||
| } | ||
|
|
||
| export type PersonalChannelClient = Pick< | ||
| PostHogAPIClient, | ||
| "createDesktopFileSystemChannel" | "getDesktopFileSystemChannels" | ||
| >; | ||
|
|
||
| // The "me" folder is provisioned on first use, and folder creation is not | ||
| // server-side idempotent by path — so two callers racing before the first | ||
| // create lands in the channels cache would each make their own "me". The entry | ||
| // points are trivially concurrent (Cmd+T's new tab, the sidebar row, its "+" | ||
| // menu), so they share one in-flight create rather than guarding separately: | ||
| // per-caller guards would still race each other. | ||
| let inFlight: Promise<Channel> | null = null; | ||
| interface PersonalChannelState { | ||
| inFlight: Promise<PersonalChannel> | null; | ||
| created: PersonalChannel | null; | ||
| } | ||
|
|
||
| const sharedScope = {}; | ||
| const stateByScope = new WeakMap<object, PersonalChannelState>(); | ||
| // The in-flight promise alone isn't enough: it settles the moment the POST | ||
| // returns, but callers pass the `channels` from their last render, which hasn't | ||
| // re-rendered with the seeded cache yet. A click landing in that gap sees | ||
| // neither an existing "me" nor an in-flight create, and makes a second one. | ||
| // Remember what was created until the list catches up. | ||
| let created: Channel | null = null; | ||
|
|
||
| /** | ||
| * The user's "me" folder, creating it once if it doesn't exist yet. Concurrent | ||
| * callers await the same create. Rejects if the create fails; callers own the | ||
| * messaging. | ||
| */ | ||
| export async function ensurePersonalChannel( | ||
| channels: readonly Channel[], | ||
| createChannel: (name: string) => Promise<Channel>, | ||
| ): Promise<Channel> { | ||
| channels: readonly PersonalChannel[], | ||
| createChannel: (name: string) => Promise<PersonalChannel>, | ||
| scope: object = sharedScope, | ||
| ): Promise<PersonalChannel> { | ||
| const state = stateByScope.get(scope) ?? { inFlight: null, created: null }; | ||
| stateByScope.set(scope, state); | ||
| const existing = channels.find((c) => c.name === PERSONAL_CHANNEL_NAME); | ||
| if (existing) { | ||
| // The list is authoritative once it carries the folder: drop the memo, so a | ||
| // deleted-then-recreated "me" resolves fresh rather than to a dead id. | ||
| created = null; | ||
| state.created = null; | ||
| return existing; | ||
| } | ||
| if (created) return created; | ||
| if (!inFlight) { | ||
| inFlight = createChannel(PERSONAL_CHANNEL_NAME) | ||
| if (state.created) return state.created; | ||
| if (!state.inFlight) { | ||
| state.inFlight = createChannel(PERSONAL_CHANNEL_NAME) | ||
| .then((channel) => { | ||
| created = channel; | ||
| state.created = channel; | ||
| return channel; | ||
| }) | ||
| .finally(() => { | ||
| inFlight = null; | ||
| state.inFlight = null; | ||
| }); | ||
| } | ||
| return inFlight; | ||
| return state.inFlight; | ||
| } | ||
|
|
||
| export async function ensurePersonalChannelFromClient( | ||
| client: PersonalChannelClient, | ||
| ): Promise<PersonalChannel> { | ||
| const toPersonalChannel = ({ id, path }: { id: string; path: string }) => ({ | ||
| id, | ||
| name: path.replace(/^\/+/, ""), | ||
| }); | ||
| const channels = (await client.getDesktopFileSystemChannels()) | ||
| .filter((channel) => channel.type === "folder") | ||
| .map(toPersonalChannel); | ||
| return await ensurePersonalChannel( | ||
| channels, | ||
| async (name) => | ||
| toPersonalChannel(await client.createDesktopFileSystemChannel(name)), | ||
| client, | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { stateStorage } from "@posthog/ui/shell/rendererStorage"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { resolveStartupLocation } from "./startupLocation"; | ||
|
|
||
| describe("startup location", () => { | ||
| afterEach(() => vi.restoreAllMocks()); | ||
|
|
||
| it("restores the exact last location", async () => { | ||
| vi.spyOn(stateStorage, "getItem").mockResolvedValue("/code"); | ||
| const client = { | ||
| getDesktopFileSystemChannels: vi.fn(), | ||
| createDesktopFileSystemChannel: vi.fn(), | ||
| }; | ||
|
|
||
| await expect(resolveStartupLocation("project", client)).resolves.toBe( | ||
| "/code", | ||
| ); | ||
| expect(client.getDesktopFileSystemChannels).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("opens a new task in me when there is no saved location", async () => { | ||
| vi.spyOn(stateStorage, "getItem").mockResolvedValue(null); | ||
| const client = { | ||
| getDesktopFileSystemChannels: vi | ||
| .fn() | ||
| .mockResolvedValue([{ id: "me-id", path: "me", type: "folder" }]), | ||
| createDesktopFileSystemChannel: vi.fn(), | ||
| }; | ||
|
|
||
| await expect(resolveStartupLocation("project", client)).resolves.toBe( | ||
| "/website/me-id/new", | ||
| ); | ||
| expect(client.createDesktopFileSystemChannel).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { | ||
| ensurePersonalChannelFromClient, | ||
| type PersonalChannelClient, | ||
| } from "@posthog/ui/features/canvas/ensurePersonalChannel"; | ||
| import { stateStorage } from "@posthog/ui/shell/rendererStorage"; | ||
|
|
||
| const storageKey = (identity: string): string => `startup-location:${identity}`; | ||
|
|
||
| export async function resolveStartupLocation( | ||
| identity: string, | ||
| client: PersonalChannelClient, | ||
| ): Promise<string> { | ||
| const saved = await stateStorage.getItem(storageKey(identity)); | ||
| if (saved) return saved; | ||
| const personal = await ensurePersonalChannelFromClient(client); | ||
| return `/website/${personal.id}/new`; | ||
| } | ||
|
puemos marked this conversation as resolved.
|
||
|
|
||
| export function rememberStartupLocation(identity: string, href: string): void { | ||
| void stateStorage.setItem(storageKey(identity), href); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.