Skip to content

Commit 800d41d

Browse files
authored
refactor(app): simplify layout hierarchy (#33588)
1 parent 5647ed8 commit 800d41d

3 files changed

Lines changed: 149 additions & 156 deletions

File tree

packages/app/e2e/smoke/session-timeline.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,6 @@ async function navigateToSession(page: Page, directory: string, sessionId: strin
718718
}
719719

720720
async function switchTitlebarSession(page: Page, sessionID: string, title: string) {
721-
console.log(process.env)
722721
const href = `/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`
723722
const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
724723
await expect(tab).toBeVisible()

packages/app/src/app.tsx

Lines changed: 141 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,90 @@ const SessionRoute = Object.assign(
9191

9292
const TargetSessionRoute = Object.assign(
9393
() => {
94-
const sdk = useSDK()
95-
const serverSDK = useServerSDK()
94+
const params = useParams<{ serverKey: string; id: string }>()
95+
const server = useServer()
96+
const conn = createMemo(() => {
97+
const key = requireServerKey(params.serverKey)
98+
return server.list.find((item) => ServerConnection.key(item) === key)
99+
})
100+
96101
return (
97-
<Show when={`${serverSDK().scope}\0${sdk().directory}`} keyed>
98-
<SessionProviders>
99-
<Session />
100-
</SessionProviders>
102+
<Show when={`${params.serverKey}\0${params.id}`} keyed>
103+
<ServerSDKProvider server={conn}>
104+
<ServerSyncProvider server={conn}>
105+
<ResolvedTargetSessionRoute />
106+
</ServerSyncProvider>
107+
</ServerSDKProvider>
101108
</Show>
102109
)
103110
},
104111
{ preload: Session.preload },
105112
)
106113

114+
function ResolvedTargetSessionRoute() {
115+
const params = useParams<{ serverKey: string; id: string }>()
116+
const settings = useSettings()
117+
const tabs = useTabs()
118+
const serverSDK = useServerSDK()
119+
const serverKey = createMemo(() => requireServerKey(params.serverKey))
120+
const resolved = useQuery(() => ({
121+
queryKey: [serverSDK().scope, "session-route", params.id] as const,
122+
queryFn: async () => {
123+
const session = (await serverSDK().client.session.get({ sessionID: params.id })).data!
124+
const root = await rootSession(session, (sessionID) =>
125+
serverSDK()
126+
.client.session.get({ sessionID })
127+
.then((result) => result.data!),
128+
)
129+
return { session, rootID: root.id }
130+
},
131+
}))
132+
const directory = createMemo<string | undefined>((prev) => prev ?? resolved.data?.session.directory)
133+
const targetDirectory = () => directory()!
134+
135+
createEffect(() => {
136+
const current = resolved.data
137+
if (!current) return
138+
tabs.addSessionTab({
139+
server: serverKey(),
140+
sessionId: current.rootID,
141+
})
142+
})
143+
144+
return (
145+
<TargetServerScopedProviders directory={directory} sessionID={() => params.id}>
146+
<Show when={!resolved.error} fallback={<ErrorPage error={resolved.error} />}>
147+
<Show when={directory()}>
148+
<Show
149+
when={settings.general.newLayoutDesigns()}
150+
fallback={<Navigate href={legacySessionHref(directory()!, params.id)} />}
151+
>
152+
<SDKProvider directory={targetDirectory}>
153+
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
154+
<Show when={resolved.data && !resolved.isPlaceholderData}>
155+
<TargetSessionPage />
156+
</Show>
157+
</DirectoryDataProvider>
158+
</SDKProvider>
159+
</Show>
160+
</Show>
161+
</Show>
162+
</TargetServerScopedProviders>
163+
)
164+
}
165+
166+
function TargetSessionPage() {
167+
const sdk = useSDK()
168+
const serverSDK = useServerSDK()
169+
return (
170+
<Show when={`${serverSDK().scope}\0${sdk().directory}`} keyed>
171+
<SessionProviders>
172+
<Session />
173+
</SessionProviders>
174+
</Show>
175+
)
176+
}
177+
107178
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
108179
// server via ServerKey, then provide the server-scoped shell (Permission/Layout/
109180
// Notification/Models + the visual Layout) for that server.
@@ -125,125 +196,42 @@ function LegacyServerLayout(props: ParentProps) {
125196
)
126197
}
127198

128-
// Wraps /new-session. It resolves the draft's target server and provides the
129-
// server-scoped shell for that server — without ServerKey, so the page never depends
130-
// on the globally "selected" server.
131-
function TargetServerLayout(props: ParentProps) {
132-
const server = useServer()
133-
const tabs = useTabs()
134-
const params = useParams<{ serverKey?: string }>()
135-
const [search] = useSearchParams<{ draftId?: string }>()
136-
const conn = createMemo(() => {
137-
if (params.serverKey) {
138-
const key = requireServerKey(params.serverKey)
139-
return server.list.find((item) => ServerConnection.key(item) === key)
140-
}
141-
const id = search.draftId
142-
if (!id) return undefined
143-
const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === id)
144-
if (!draft) return undefined
145-
return server.list.find((c) => ServerConnection.key(c) === draft.server)
146-
})
147-
148-
return (
149-
<ServerSDKProvider server={conn}>
150-
<ServerSyncProvider server={conn}>
151-
<TargetDirectoryLayout>{props.children}</TargetDirectoryLayout>
152-
</ServerSyncProvider>
153-
</ServerSDKProvider>
154-
)
155-
}
156-
157-
function TargetDirectoryLayout(props: ParentProps) {
158-
const params = useParams<{ serverKey?: string; id?: string }>()
159-
const [search] = useSearchParams<{ draftId?: string }>()
160-
const settings = useSettings()
161-
const tabs = useTabs()
162-
const serverSDK = useServerSDK()
163-
const serverKey = createMemo(() => {
164-
if (params.serverKey) return requireServerKey(params.serverKey)
165-
if (!search.draftId) return undefined
166-
return tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)?.server
167-
})
168-
169-
const resolved = useQuery(() => {
170-
const id = params.id
171-
return {
172-
queryKey: [serverSDK().scope, "session-route", id] as const,
173-
enabled: !!params.serverKey && !!params.id,
174-
queryFn: async () => {
175-
const session = (await serverSDK().client.session.get({ sessionID: params.id! })).data!
176-
const root = await rootSession(session, (sessionID) =>
177-
serverSDK()
178-
.client.session.get({ sessionID })
179-
.then((result) => result.data!),
180-
)
181-
return { session, rootID: root.id }
182-
},
183-
}
184-
})
185-
const resolvedDirectory = createMemo(() => {
186-
if (params.serverKey) return resolved.data?.session.directory
187-
if (!search.draftId) return undefined
188-
return tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)?.directory
189-
})
190-
const directory = createMemo<string | undefined>((prev) =>
191-
search.draftId ? resolvedDirectory() : (prev ?? resolvedDirectory()),
192-
)
193-
const home = () => !params.serverKey && !search.draftId
194-
const targetDirectory = () => directory()!
195-
196-
createEffect(() => {
197-
const current = resolved.data
198-
const key = serverKey()
199-
if (!current || !key) return
200-
tabs.addSessionTab({
201-
server: key,
202-
sessionId: current.rootID,
203-
})
204-
})
205-
206-
return (
207-
<NewServerScopedShell directory={() => (home() ? undefined : directory())} sessionID={() => params.id}>
208-
<Show when={!home()} fallback={props.children}>
209-
<Show when={!resolved.error} fallback={<ErrorPage error={resolved.error} />}>
210-
<Show when={directory()}>
211-
<Show
212-
when={!params.serverKey || settings.general.newLayoutDesigns()}
213-
fallback={<Navigate href={legacySessionHref(directory()!, params.id!)} />}
214-
>
215-
<SDKProvider directory={targetDirectory}>
216-
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
217-
<Show when={!params.serverKey || (resolved.data && !resolved.isPlaceholderData)}>
218-
{props.children}
219-
</Show>
220-
</DirectoryDataProvider>
221-
</SDKProvider>
222-
</Show>
223-
</Show>
224-
</Show>
225-
</Show>
226-
</NewServerScopedShell>
227-
)
228-
}
229-
230199
function DraftRoute() {
231200
const [search] = useSearchParams<{ draftId?: string }>()
232201
const tabs = useTabs()
233202
return (
234203
<Show when={tabs.ready()}>
235-
<Show when={search.draftId} keyed fallback={<Navigate href="/" />}>
236-
<ResolvedDraftRoute />
204+
<Show
205+
when={tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === search.draftId)}
206+
keyed
207+
fallback={<Navigate href="/" />}
208+
>
209+
{(draft) => <ResolvedDraftRoute draft={draft} />}
237210
</Show>
238211
</Show>
239212
)
240213
}
241214

242-
function ResolvedDraftRoute() {
215+
function ResolvedDraftRoute(props: { draft: DraftTab }) {
216+
const server = useServer()
217+
const conn = createMemo(() => server.list.find((item) => ServerConnection.key(item) === props.draft.server))
218+
const directory = () => props.draft.directory
219+
const serverKey = () => props.draft.server
220+
243221
return (
244-
<DraftProviders>
245-
<NewSession />
246-
</DraftProviders>
222+
<ServerSDKProvider server={conn}>
223+
<ServerSyncProvider server={conn}>
224+
<TargetServerScopedProviders directory={directory}>
225+
<SDKProvider directory={directory}>
226+
<DirectoryDataProvider directory={directory} server={serverKey}>
227+
<DraftProviders>
228+
<NewSession />
229+
</DraftProviders>
230+
</DirectoryDataProvider>
231+
</SDKProvider>
232+
</TargetServerScopedProviders>
233+
</ServerSyncProvider>
234+
</ServerSDKProvider>
247235
)
248236
}
249237

@@ -306,9 +294,7 @@ function SharedProviders(props: ParentProps) {
306294
)
307295
}
308296

309-
// Server-scoped providers plus the visual Layout (tabs/sidebar). These live inside
310-
// each per-route server layout so they resolve to that route's server (selected vs
311-
// draft). The Layout remounts when crossing between those groups.
297+
// Server-scoped providers shared by the legacy shell and the top-level new shell.
312298
type ServerScopedShellProps = ParentProps<{
313299
directory?: () => string | undefined
314300
sessionID?: () => string | undefined
@@ -334,11 +320,23 @@ function LegacyServerScopedShell(props: ServerScopedShellProps) {
334320
)
335321
}
336322

337-
function NewServerScopedShell(props: ServerScopedShellProps) {
323+
function NewAppLayout(props: ParentProps) {
338324
return (
339-
<ServerScopedProviders directory={props.directory} sessionID={props.sessionID}>
340-
<NewLayout>{props.children}</NewLayout>
341-
</ServerScopedProviders>
325+
<SelectedServerProviders>
326+
<ServerScopedProviders>
327+
<NewLayout>{props.children}</NewLayout>
328+
</ServerScopedProviders>
329+
</SelectedServerProviders>
330+
)
331+
}
332+
333+
function TargetServerScopedProviders(props: ServerScopedShellProps) {
334+
return (
335+
<PermissionProvider directory={props.directory}>
336+
<NotificationProvider directory={props.directory} sessionID={props.sessionID}>
337+
<ModelsProvider>{props.children}</ModelsProvider>
338+
</NotificationProvider>
339+
</PermissionProvider>
342340
)
343341
}
344342

@@ -524,11 +522,9 @@ export function AppInterface(props: {
524522
router?: Component<BaseRouterProps>
525523
disableHealthCheck?: boolean
526524
}) {
527-
// The shared shell holds only server-agnostic providers (QueryClient + Settings/
528-
// Command/Highlights) and stays mounted across every route. The server-scoped
529-
// providers and the visual Layout live in the per-route layouts below, so they
530-
// resolve to that route's server (selected for most routes, the draft's server for
531-
// /new-session). appChildren is server-agnostic, so it renders here once.
525+
// The visual new layout lives in the router root so it remains mounted across
526+
// route changes. Draft and session routes override only their server-bound data
527+
// providers beneath it.
532528
const ServerShell = (shellProps: ParentProps) => (
533529
<QueryProvider>
534530
<SharedProviders>
@@ -552,7 +548,11 @@ export function AppInterface(props: {
552548
component={props.router ?? Router}
553549
root={(routerProps) => (
554550
<TabsProvider>
555-
<ServerShell>{routerProps.children}</ServerShell>
551+
<ServerShell>
552+
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
553+
<NewAppLayout>{routerProps.children}</NewAppLayout>
554+
</Show>
555+
</ServerShell>
556556
</TabsProvider>
557557
)}
558558
>
@@ -578,28 +578,20 @@ function Routes() {
578578
<Route path="/session/:id?" component={SessionRoute} />
579579
</Route>
580580
</Route>
581-
<Route component={TargetServerLayout}>
582-
<Show when={settings.general.newLayoutDesigns()}>
583-
{
584-
<>
585-
<Route path="/" component={NewHome} />
586-
<Route path="/:dir" component={DirectoryLayout}>
587-
<Route
588-
path="/session/:id"
589-
component={() => {
590-
const server = useServer()
591-
const { id } = useParams()
592-
593-
return <Navigate href={`/server/${server.key}/session/${id}`} />
594-
}}
595-
/>
596-
</Route>
597-
</>
598-
}
599-
</Show>
600-
<Route path="/new-session" component={DraftRoute} />
601-
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
602-
</Route>
581+
<Show when={settings.general.newLayoutDesigns()}>
582+
<Route path="/" component={NewHome} />
583+
<Route
584+
path="/:dir/session/:id"
585+
component={() => {
586+
const server = useServer()
587+
const { id } = useParams()
588+
589+
return <Navigate href={`/server/${server.key}/session/${id}`} />
590+
}}
591+
/>
592+
</Show>
593+
<Route path="/new-session" component={DraftRoute} />
594+
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
603595
</>
604596
)
605597
}

0 commit comments

Comments
 (0)