Skip to content

Commit 1bcc587

Browse files
committed
refactor(app-shell): keep the newTabUrl routing rule out of the factory-stubbed module
Nineteen app-shell tests stub `utils/consoleServerAction` with a factory that names only `createConsoleServerActionHandler`, so a rule exported from there was undefined under each of them and the record page and the runtime hook threw on mount. The predicate and the api-handler composition now live in `utils/newTabUrlFastPath`, which those stubs do not touch. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSH8FbKjEztmGT1sbB
1 parent 0c1ffeb commit 1bcc587

4 files changed

Lines changed: 67 additions & 45 deletions

File tree

‎ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ import { EnvironmentEntitlementDialog, type EntitlementDialogState } from '../en
5252
import { entitlementDialogFromError, type EntitlementDialogSpec } from '../environment/entitlements.js';
5353
import { resolvePageVarTokens } from '../utils/resolvePageVarTokens.js';
5454
import { launchConsoleFlow } from '../utils/flowLaunch.js';
55-
import { createConsoleServerActionHandler, withNewTabUrlFastPath } from '../utils/consoleServerAction.js';
55+
import { createConsoleServerActionHandler } from '../utils/consoleServerAction.js';
56+
import { withNewTabUrlFastPath } from '../utils/newTabUrlFastPath.js';
5657
import { modalTargetRefusalMessage } from '../utils/modalTargetDiagnostics.js';
5758
import { actionContextOrg } from '../utils/actionContextOrg.js';
5859
import { resolveHostAppSegment } from '../utils/appRoute.js';

‎ui/packages/app-shell/src/utils/consoleServerAction.ts‎

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ import {
4949
resolveServerActionRecordId,
5050
type ActionContext,
5151
type ActionDef,
52-
type ActionResult,
5352
type ServerActionFetch,
5453
type ServerActionHandler,
5554
type ServerActionRecordIdResolver,
5655
} from '@object-ui/core';
56+
import { takesNewTabUrlFastPath } from './newTabUrlFastPath.js';
5757

5858
/**
5959
* i18next-style translate function: `t(key, englishDefault)` returns the
@@ -166,48 +166,6 @@ function openInTab(preOpenedTab: Window | null, url: string, t: ConsoleServerAct
166166
}
167167
}
168168

169-
/**
170-
* Whether `action` takes the zero-roundtrip `newTabUrl` fast path:
171-
* `opensInNewTab` plus a non-empty `newTabUrl`. The spec gives that pair one
172-
* meaning whatever the action's `type` — navigate the pre-opened tab, no
173-
* request — so a host's `type: 'api'` handler asks this and hands such an
174-
* action to the handler {@link createConsoleServerActionHandler} builds,
175-
* instead of sending its own request to `target`.
176-
*/
177-
export function takesNewTabUrlFastPath(action: ActionDef): action is ActionDef & { newTabUrl: string } {
178-
return !!action.opensInNewTab && typeof action.newTabUrl === 'string' && action.newTabUrl !== '';
179-
}
180-
181-
/**
182-
* A host's registered `type: 'api'` handler, made to honour the `newTabUrl`
183-
* fast path: an action that {@link takesNewTabUrlFastPath} goes to
184-
* `consoleHandler` (the handler {@link createConsoleServerActionHandler}
185-
* built for the same surface, so the record id resolves the way that surface
186-
* resolves it), and every other action to `apiHandler` unchanged.
187-
*
188-
* Without it the console's api handlers sent their request to `target` and
189-
* never read the pair, so a `type: 'api'` action declaring `opensInNewTab` +
190-
* `newTabUrl` opened no tab and POSTed instead, from the record header, its
191-
* more-menu and the list toolbar alike (objectstack#26747). The pair's early
192-
* row-count refusal is the fast path's own, forwarded only for those actions;
193-
* an ordinary api action keeps having none.
194-
*/
195-
export function withNewTabUrlFastPath(
196-
apiHandler: (action: ActionDef, context?: ActionContext) => Promise<ActionResult>,
197-
consoleHandler: ServerActionHandler,
198-
): ServerActionHandler {
199-
return Object.assign(
200-
(action: ActionDef, context?: ActionContext) => (takesNewTabUrlFastPath(action)
201-
? consoleHandler(action, context)
202-
: apiHandler(action, context)),
203-
{
204-
refuseBeforeInput: (action: ActionDef, context?: ActionContext) => (takesNewTabUrlFastPath(action)
205-
? consoleHandler.refuseBeforeInput?.(action, context)
206-
: undefined),
207-
},
208-
);
209-
}
210-
211169
export function createConsoleServerActionHandler(opts: ConsoleServerActionOptions): ServerActionHandler {
212170
const dispatch = createServerActionHandler({
213171
fetch: opts.fetch,
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* The `newTabUrl` fast path's routing rule, shared by the console server-action
11+
* handler (`consoleServerAction`) and the hosts that register a `type: 'api'`
12+
* handler beside it (`useConsoleActionRuntime`, `RecordDetailView`).
13+
*
14+
* It lives apart from `consoleServerAction` on purpose: the hosts' tests stub
15+
* that module with a factory, and a rule kept there would vanish under every
16+
* such stub. Kept here, an api action is routed by the same rule under test as
17+
* in the console.
18+
*/
19+
20+
import type { ActionContext, ActionDef, ActionResult, ServerActionHandler } from '@object-ui/core';
21+
22+
/**
23+
* Whether `action` takes the zero-roundtrip `newTabUrl` fast path:
24+
* `opensInNewTab` plus a non-empty `newTabUrl`. The spec gives that pair one
25+
* meaning whatever the action's `type` — navigate the pre-opened tab, no
26+
* request — so a host's `type: 'api'` handler asks this and hands such an
27+
* action to the handler `createConsoleServerActionHandler` builds,
28+
* instead of sending its own request to `target`.
29+
*/
30+
export function takesNewTabUrlFastPath(action: ActionDef): action is ActionDef & { newTabUrl: string } {
31+
return !!action.opensInNewTab && typeof action.newTabUrl === 'string' && action.newTabUrl !== '';
32+
}
33+
34+
/**
35+
* A host's registered `type: 'api'` handler, made to honour the `newTabUrl`
36+
* fast path: an action that {@link takesNewTabUrlFastPath} goes to
37+
* `consoleHandler` (the handler `createConsoleServerActionHandler`
38+
* built for the same surface, so the record id resolves the way that surface
39+
* resolves it), and every other action to `apiHandler` unchanged.
40+
*
41+
* Without it the console's api handlers sent their request to `target` and
42+
* never read the pair, so a `type: 'api'` action declaring `opensInNewTab` +
43+
* `newTabUrl` opened no tab and POSTed instead, from the record header, its
44+
* more-menu and the list toolbar alike (objectstack#26747). The pair's early
45+
* row-count refusal is the fast path's own, forwarded only for those actions;
46+
* an ordinary api action keeps having none.
47+
*/
48+
export function withNewTabUrlFastPath(
49+
apiHandler: (action: ActionDef, context?: ActionContext) => Promise<ActionResult>,
50+
consoleHandler: ServerActionHandler,
51+
): ServerActionHandler {
52+
return Object.assign(
53+
(action: ActionDef, context?: ActionContext) => (takesNewTabUrlFastPath(action)
54+
? consoleHandler(action, context)
55+
: apiHandler(action, context)),
56+
{
57+
refuseBeforeInput: (action: ActionDef, context?: ActionContext) => (takesNewTabUrlFastPath(action)
58+
? consoleHandler.refuseBeforeInput?.(action, context)
59+
: undefined),
60+
},
61+
);
62+
}

‎ui/packages/app-shell/src/views/RecordDetailView.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import { RelatedRecordActionsBridge } from './RelatedRecordActionsBridge.js';
3535
import { withPageTabsUrlSync } from '../utils/pageTabsUrlSync.js';
3636
import { RECORD_DETAIL_TAB_PARAM, RECORD_TRAIL_PARAM, decodeRecordTrail, buildRecordTrailHref } from '../urlParams.js';
3737
import { resolveActionParams } from '@object-ui/fields';
38-
import { createConsoleServerActionHandler, withNewTabUrlFastPath } from '../utils/consoleServerAction.js';
38+
import { createConsoleServerActionHandler } from '../utils/consoleServerAction.js';
39+
import { withNewTabUrlFastPath } from '../utils/newTabUrlFastPath.js';
3940
import { modalTargetRefusalMessage } from '../utils/modalTargetDiagnostics.js';
4041
import { actionContextOrg } from '../utils/actionContextOrg.js';
4142
import { resolveHostAppSegment } from '../utils/appRoute.js';

0 commit comments

Comments
 (0)