mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Fix desktop preview interactions and connector auth feedback (#864)
* Fix desktop preview modal interactions * Fix connector auth failures surfacing
This commit is contained in:
parent
915c041545
commit
32df17b87b
5 changed files with 100 additions and 9 deletions
|
|
@ -107,6 +107,12 @@ const MAC_WINDOW_CHROME_CSS = `
|
|||
.entry-header [role="button"],
|
||||
.entry-tabs,
|
||||
.entry-tabs *,
|
||||
.ds-modal-header,
|
||||
.ds-modal-header *,
|
||||
.ds-modal-actions,
|
||||
.ds-modal-actions *,
|
||||
.share-menu-popover,
|
||||
.share-menu-popover *,
|
||||
.entry-side-resizer,
|
||||
.avatar-popover,
|
||||
.avatar-popover * {
|
||||
|
|
@ -181,6 +187,15 @@ function isHttpUrl(url: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
function isAllowedChildWindowUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.protocol === "blob:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function installWindowChromeCssHook(window: BrowserWindow): void {
|
||||
window.webContents.on("did-finish-load", () => {
|
||||
void applyWindowChromeCss(window).catch((error: unknown) => {
|
||||
|
|
@ -269,6 +284,7 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
|
|||
window.on("blur", () => showWindowButtons(window));
|
||||
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (isAllowedChildWindowUrl(url)) return { action: "allow" };
|
||||
if (isHttpUrl(url)) void shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -646,7 +646,11 @@ export function EntryView({
|
|||
toolsLoaded={connectorDiscoveryLoaded}
|
||||
composioConfigured={Boolean(config.composio?.apiKeyConfigured)}
|
||||
onOpenSettings={onOpenSettings}
|
||||
onConnect={async (connectorId) => updateConnector(await connectConnector(connectorId))}
|
||||
onConnect={async (connectorId) => {
|
||||
const result = await connectConnector(connectorId);
|
||||
updateConnector(result.connector);
|
||||
return result;
|
||||
}}
|
||||
onDisconnect={async (connectorId) => updateConnector(await disconnectConnector(connectorId))}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -710,7 +714,7 @@ function ConnectorsTab({
|
|||
toolsLoaded: boolean;
|
||||
composioConfigured: boolean;
|
||||
onOpenSettings: (section?: 'execution' | 'media' | 'composio' | 'language' | 'about') => void;
|
||||
onConnect: (connectorId: string) => Promise<void> | void;
|
||||
onConnect: (connectorId: string) => Promise<{ error?: string } | void> | { error?: string } | void;
|
||||
onDisconnect: (connectorId: string) => Promise<void> | void;
|
||||
}) {
|
||||
const t = useT();
|
||||
|
|
@ -720,6 +724,7 @@ function ConnectorsTab({
|
|||
} | null>(null);
|
||||
const [detailConnectorId, setDetailConnectorId] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// Mask the grid whenever no Composio-backed connector has its auth
|
||||
|
|
@ -746,10 +751,14 @@ function ConnectorsTab({
|
|||
|
||||
async function runConnectorAction(connectorId: string, action: 'connect' | 'disconnect') {
|
||||
if (pendingConnectorAction) return;
|
||||
setActionError(null);
|
||||
setPendingConnectorAction({ connectorId, action });
|
||||
try {
|
||||
if (action === 'connect') {
|
||||
await onConnect(connectorId);
|
||||
const result = await onConnect(connectorId);
|
||||
if (result && typeof result === 'object' && 'error' in result && result.error) {
|
||||
setActionError(result.error);
|
||||
}
|
||||
} else {
|
||||
await onDisconnect(connectorId);
|
||||
}
|
||||
|
|
@ -811,6 +820,11 @@ function ConnectorsTab({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{actionError ? (
|
||||
<p className="connector-inline-error" role="alert" data-testid="connectors-action-error">
|
||||
{actionError}
|
||||
</p>
|
||||
) : null}
|
||||
{loading ? (
|
||||
<CenteredLoader label={t('common.loading')} />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -3173,6 +3173,17 @@ code {
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
.connector-inline-error {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in oklab, #ff6b6b 45%, var(--line) 55%);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in oklab, #ff6b6b 10%, var(--panel) 90%);
|
||||
color: color-mix(in oklab, #ff6b6b 70%, var(--fg) 30%);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.connectors-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
|
|
|
|||
|
|
@ -265,7 +265,25 @@ export async function fetchConnectorDiscovery(options: { refresh?: boolean } = {
|
|||
return promise;
|
||||
}
|
||||
|
||||
export async function connectConnector(connectorId: string): Promise<ConnectorDetail | null> {
|
||||
export interface ConnectorActionResult {
|
||||
connector: ConnectorDetail | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function popupBlockedMessage(): string {
|
||||
return 'Popup blocked. Allow popups for Open Design and try again.';
|
||||
}
|
||||
|
||||
async function decodeConnectorError(resp: Response): Promise<string> {
|
||||
try {
|
||||
const payload = (await resp.json()) as { error?: { message?: string } } | null;
|
||||
return payload?.error?.message?.trim() || `Connector request failed (${resp.status})`;
|
||||
} catch {
|
||||
return `Connector request failed (${resp.status})`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function connectConnector(connectorId: string): Promise<ConnectorActionResult> {
|
||||
let authWindow: Window | null = null;
|
||||
try {
|
||||
authWindow = window.open('about:blank', '_blank');
|
||||
|
|
@ -275,22 +293,28 @@ export async function connectConnector(connectorId: string): Promise<ConnectorDe
|
|||
});
|
||||
if (!resp.ok) {
|
||||
authWindow?.close();
|
||||
return null;
|
||||
return { connector: null, error: await decodeConnectorError(resp) };
|
||||
}
|
||||
const json = (await resp.json()) as ConnectorConnectResponse;
|
||||
if (json.auth?.kind === 'redirect_required' && json.auth.redirectUrl) {
|
||||
if (authWindow) {
|
||||
authWindow.location.href = json.auth.redirectUrl;
|
||||
} else {
|
||||
window.open(json.auth.redirectUrl, '_blank');
|
||||
const redirected = window.open(json.auth.redirectUrl, '_blank');
|
||||
if (!redirected) {
|
||||
return { connector: json.connector ?? null, error: popupBlockedMessage() };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
authWindow?.close();
|
||||
}
|
||||
return json.connector ?? null;
|
||||
} catch {
|
||||
return { connector: json.connector ?? null };
|
||||
} catch (err) {
|
||||
authWindow?.close();
|
||||
return null;
|
||||
return {
|
||||
connector: null,
|
||||
error: err instanceof Error && err.message ? err.message : 'Could not start connector authentication.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import {
|
||||
CLOUDFLARE_PAGES_PROVIDER_ID,
|
||||
connectConnector,
|
||||
DEFAULT_DEPLOY_PROVIDER_ID,
|
||||
deployProjectFile,
|
||||
fetchDeployConfig,
|
||||
|
|
@ -132,6 +133,31 @@ describe('fetchConnectorDiscovery', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('connectConnector', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns a user-facing error when the OAuth popup is blocked', async () => {
|
||||
const open = vi.fn(() => null);
|
||||
vi.stubGlobal('window', { open } as unknown as Window & typeof globalThis);
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async () => new Response(JSON.stringify({
|
||||
connector: { id: 'github', name: 'GitHub', status: 'available', tools: [] },
|
||||
auth: { kind: 'redirect_required', redirectUrl: 'https://example.com/oauth' },
|
||||
}), { status: 200 })),
|
||||
);
|
||||
|
||||
await expect(connectConnector('github')).resolves.toEqual({
|
||||
connector: { id: 'github', name: 'GitHub', status: 'available', tools: [] },
|
||||
error: 'Popup blocked. Allow popups for Open Design and try again.',
|
||||
});
|
||||
expect(open).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadProjectFiles', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
|
|
|
|||
Loading…
Reference in a new issue