Skip to content

Commit c5935a7

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(app-shell): zero-roundtrip newTabUrl fast path for opensInNewTab actions (#1675)
Actions that declare newTabUrl (spec: a path template with a {recordId} placeholder whose target endpoint performs all auth/authz itself) now drive the pre-opened popup straight to that URL on click, skipping the action POST entirely. Applied to both server-action paths (list rows via useConsoleActionRuntime, record header via RecordDetailView). The popup paints the existing spinner page and keeps it visible until the (possibly slow) endpoint commits its redirect; the URL is resolved absolute because about:blank gives a bare-relative href no reliable resolution base. Popup-blocked fallback keeps the existing toast. First consumer: cloud sys_environment.sso_as_owner -> GET /sso-open, which re-runs every check the POST half would have done. Removes one full round trip of white-screen latency from every Open click. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c97513f commit c5935a7

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,41 @@ export function useConsoleActionRuntime(opts: ConsoleActionRuntimeOptions): Cons
373373
}
374374
try {
375375
const baseUrl = import.meta.env.VITE_SERVER_URL || '';
376+
// ── Zero-roundtrip fast path ────────────────────────────────────────
377+
// `newTabUrl` names a GET endpoint that performs ALL auth/authz itself
378+
// (e.g. /sso-open re-runs every check the POST half would have done),
379+
// so the POST round trip would add nothing but click latency. Drive the
380+
// pre-opened tab there immediately — the spinner page stays painted
381+
// until the (possibly slow) endpoint commits its redirect.
382+
const newTabUrl = typeof (action as any).newTabUrl === 'string' ? (action as any).newTabUrl as string : '';
383+
if ((action as any).opensInNewTab && newTabUrl) {
384+
if (resolvedRecordId == null) {
385+
if (preOpenedTab) { try { preOpenedTab.close(); } catch { /* ignore */ } }
386+
return { success: false, error: 'This action runs on a single record — no record id available.' };
387+
}
388+
// Absolute URL required: the pre-opened tab is an about:blank document,
389+
// so a bare-relative href has no reliable resolution base.
390+
const directUrl = `${baseUrl || window.location.origin}${newTabUrl.replace('{recordId}', encodeURIComponent(String(resolvedRecordId)))}`;
391+
if (preOpenedTab) {
392+
try { preOpenedTab.location.href = directUrl; }
393+
catch {
394+
try { preOpenedTab.close(); } catch { /* ignore */ }
395+
window.location.href = directUrl;
396+
}
397+
} else {
398+
let popup: Window | null = null;
399+
try { popup = window.open(directUrl, '_blank'); } catch { popup = null; }
400+
if (!popup) {
401+
toast('浏览器拦截了弹窗 / Popup blocked', {
402+
description: '点击在新标签页打开环境',
403+
action: { label: '打开环境', onClick: () => { try { window.open(directUrl, '_blank'); } catch { window.location.href = directUrl; } } },
404+
duration: 10000,
405+
});
406+
}
407+
}
408+
if (action.refreshAfter === true) refresh();
409+
return { success: true };
410+
}
376411
const obj = action.objectName || objApiName || 'global';
377412
const res = await authFetch(
378413
`${baseUrl}/api/v1/actions/${encodeURIComponent(obj)}/${encodeURIComponent(targetName)}`,

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,41 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
496496

497497
try {
498498
const baseUrl = import.meta.env.VITE_SERVER_URL || '';
499+
// ── Zero-roundtrip fast path ────────────────────────────────────────
500+
// `newTabUrl` names a GET endpoint that performs ALL auth/authz itself
501+
// (e.g. /sso-open re-runs every check the POST half would have done),
502+
// so the POST round trip would add nothing but click latency. Drive the
503+
// pre-opened tab there immediately — the spinner page stays painted
504+
// until the (possibly slow) endpoint commits its redirect.
505+
const newTabUrl = typeof (action as any).newTabUrl === 'string' ? (action as any).newTabUrl as string : '';
506+
if ((action as any).opensInNewTab && newTabUrl) {
507+
if (pureRecordId == null) {
508+
if (preOpenedTab) { try { preOpenedTab.close(); } catch { /* ignore */ } }
509+
return { success: false, error: 'This action runs on a single record — no record id available.' };
510+
}
511+
// Absolute URL required: the pre-opened tab is an about:blank document,
512+
// so a bare-relative href has no reliable resolution base.
513+
const directUrl = `${baseUrl || window.location.origin}${newTabUrl.replace('{recordId}', encodeURIComponent(String(pureRecordId)))}`;
514+
if (preOpenedTab) {
515+
try { preOpenedTab.location.href = directUrl; }
516+
catch {
517+
try { preOpenedTab.close(); } catch { /* ignore */ }
518+
window.location.href = directUrl;
519+
}
520+
} else {
521+
let popup: Window | null = null;
522+
try { popup = window.open(directUrl, '_blank'); } catch { popup = null; }
523+
if (!popup) {
524+
toast('浏览器拦截了弹窗 / Popup blocked', {
525+
description: '点击在新标签页打开环境',
526+
action: { label: '打开环境', onClick: () => { try { window.open(directUrl, '_blank'); } catch { window.location.href = directUrl; } } },
527+
duration: 10000,
528+
});
529+
}
530+
}
531+
if (action.refreshAfter === true) setActionRefreshKey(k => k + 1);
532+
return { success: true };
533+
}
499534
const obj = action.objectName || objectName || 'global';
500535
const res = await authFetch(
501536
`${baseUrl}/api/v1/actions/${encodeURIComponent(obj)}/${encodeURIComponent(targetName)}`,

packages/core/src/actions/ActionRunner.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ export interface ActionDef {
127127
* handler can drive it to a returned `redirectUrl` after an awaited fetch
128128
* without tripping popup blockers. Used by actions like `sso_as_owner`. */
129129
opensInNewTab?: boolean;
130+
/** Zero-roundtrip new-tab target: a path template (`{recordId}` placeholder)
131+
* the handler navigates the pre-opened tab to IMMEDIATELY on click, skipping
132+
* the action POST entirely. Only valid with `opensInNewTab`; the endpoint
133+
* must perform all auth/authz itself (e.g. the cloud `/sso-open` endpoint,
134+
* which re-runs every check the POST half would have done). */
135+
newTabUrl?: string;
130136
/** Any additional properties */
131137
[key: string]: any;
132138
}

0 commit comments

Comments
 (0)