mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Fix desktop preview and packaged app interactions (#879)
* Fix packaged deck navigation interactions * Fix connector auth in packaged app and localized content coverage * Fix Electron connector browser handoff contract
This commit is contained in:
parent
b9d30aa30e
commit
aec9428b08
7 changed files with 96 additions and 10 deletions
|
|
@ -1,6 +1,8 @@
|
|||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openExternal: (url: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke('shell:open-external', url),
|
||||
pickFolder: (): Promise<string | null> =>
|
||||
ipcRenderer.invoke('dialog:pick-folder'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ const MAC_WINDOW_CHROME_CSS = `
|
|||
.entry-header [role="button"],
|
||||
.entry-tabs,
|
||||
.entry-tabs *,
|
||||
.viewer-toolbar,
|
||||
.viewer-toolbar *,
|
||||
.deck-nav,
|
||||
.deck-nav *,
|
||||
.ds-modal-header,
|
||||
.ds-modal-header *,
|
||||
.ds-modal-actions,
|
||||
|
|
@ -254,10 +258,20 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
|
|||
// a second handler" on the second createDesktopRuntime() call (e.g. dev
|
||||
// hot-reload). removeHandler is a no-op when nothing is registered.
|
||||
ipcMain.removeHandler("dialog:pick-folder");
|
||||
ipcMain.removeHandler("shell:open-external");
|
||||
ipcMain.handle("dialog:pick-folder", async () => {
|
||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
|
||||
});
|
||||
ipcMain.handle("shell:open-external", async (_event, url: string) => {
|
||||
if (!isHttpUrl(url)) return false;
|
||||
try {
|
||||
await shell.openExternal(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const consoleEntries: DesktopConsoleEntry[] = [];
|
||||
const window = new BrowserWindow({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import type { ConnectorDetail } from '@open-design/contracts';
|
|||
declare global {
|
||||
interface Window {
|
||||
electronAPI?: {
|
||||
pickFolder: () => Promise<string | null>;
|
||||
openExternal?: (url: string) => Promise<boolean>;
|
||||
pickFolder?: () => Promise<string | null>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -376,7 +377,7 @@ export function NewProjectPanel({
|
|||
if (!onImportFolder) return;
|
||||
let pathToOpen: string;
|
||||
if (hasElectronPicker) {
|
||||
const picked = await window.electronAPI!.pickFolder();
|
||||
const picked = await window.electronAPI!.pickFolder!();
|
||||
if (!picked) return;
|
||||
pathToOpen = picked;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,15 @@ import type {
|
|||
} from '../types';
|
||||
import type { ArtifactManifest } from '../artifacts/types';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI?: {
|
||||
openExternal?: (url: string) => Promise<boolean>;
|
||||
pickFolder?: () => Promise<string | null>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_DEPLOY_PROVIDER_ID = 'vercel-self';
|
||||
export const CLOUDFLARE_PAGES_PROVIDER_ID = 'cloudflare-pages';
|
||||
export const DEPLOY_PROVIDER_IDS = [
|
||||
|
|
@ -289,9 +298,13 @@ async function decodeConnectorError(resp: Response): Promise<string> {
|
|||
|
||||
export async function connectConnector(connectorId: string): Promise<ConnectorActionResult> {
|
||||
let authWindow: Window | null = null;
|
||||
const openExternal = window.electronAPI?.openExternal;
|
||||
const useExternalBrowser = typeof openExternal === 'function';
|
||||
try {
|
||||
authWindow = window.open('about:blank', '_blank');
|
||||
renderConnectorAuthLoading(authWindow);
|
||||
if (!useExternalBrowser) {
|
||||
authWindow = window.open('about:blank', '_blank');
|
||||
renderConnectorAuthLoading(authWindow);
|
||||
}
|
||||
const resp = await fetch(`/api/connectors/${encodeURIComponent(connectorId)}/connect`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
|
@ -301,7 +314,12 @@ export async function connectConnector(connectorId: string): Promise<ConnectorAc
|
|||
}
|
||||
const json = (await resp.json()) as ConnectorConnectResponse;
|
||||
if (json.auth?.kind === 'redirect_required' && json.auth.redirectUrl) {
|
||||
if (authWindow) {
|
||||
if (useExternalBrowser) {
|
||||
const opened = await openExternal(json.auth.redirectUrl);
|
||||
if (!opened) {
|
||||
return { connector: json.connector ?? null, error: popupBlockedMessage() };
|
||||
}
|
||||
} else if (authWindow) {
|
||||
authWindow.location.href = json.auth.redirectUrl;
|
||||
} else {
|
||||
const redirected = window.open(json.auth.redirectUrl, '_blank');
|
||||
|
|
|
|||
|
|
@ -157,6 +157,51 @@ describe('connectConnector', () => {
|
|||
});
|
||||
expect(open).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('opens connector auth in the system browser when Electron returns a success boolean', async () => {
|
||||
const open = vi.fn();
|
||||
const openExternal = vi.fn(async () => true);
|
||||
vi.stubGlobal('window', {
|
||||
open,
|
||||
electronAPI: { openExternal },
|
||||
} 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: [] },
|
||||
});
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
expect(openExternal).toHaveBeenCalledWith('https://example.com/oauth');
|
||||
});
|
||||
|
||||
it('surfaces an error when Electron cannot confirm that the system browser opened', async () => {
|
||||
const open = vi.fn();
|
||||
const openExternal = vi.fn(async () => false);
|
||||
vi.stubGlobal('window', {
|
||||
open,
|
||||
electronAPI: { openExternal },
|
||||
} 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).not.toHaveBeenCalled();
|
||||
expect(openExternal).toHaveBeenCalledWith('https://example.com/oauth');
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadProjectFiles', () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const MAC_DAEMON_PREBUNDLE_ESM_REQUIRE_BANNER =
|
|||
export const MAC_PREBUNDLE_ENTRYPOINTS_DIR_NAME = "prebundle-entrypoints";
|
||||
|
||||
export const MAC_PREBUNDLE_RUNTIME_DEPENDENCIES = {
|
||||
"blake3-wasm": "2.1.5",
|
||||
"better-sqlite3": "12.9.0",
|
||||
} as const;
|
||||
|
||||
|
|
@ -42,9 +43,10 @@ export const MAC_PREBUNDLE_POLICIES = {
|
|||
label: "packaged main",
|
||||
},
|
||||
daemonCli: {
|
||||
externals: ["better-sqlite3"],
|
||||
externals: ["better-sqlite3", "blake3-wasm"],
|
||||
forbiddenInputs: [
|
||||
"/node_modules/@open-design/daemon/",
|
||||
"/node_modules/blake3-wasm/",
|
||||
"/node_modules/better-sqlite3/",
|
||||
"/node_modules/electron/",
|
||||
"/node_modules/next/",
|
||||
|
|
@ -55,9 +57,10 @@ export const MAC_PREBUNDLE_POLICIES = {
|
|||
label: "daemon cli",
|
||||
},
|
||||
daemonSidecar: {
|
||||
externals: ["better-sqlite3"],
|
||||
externals: ["better-sqlite3", "blake3-wasm"],
|
||||
forbiddenInputs: [
|
||||
"/node_modules/@open-design/daemon/",
|
||||
"/node_modules/blake3-wasm/",
|
||||
"/node_modules/better-sqlite3/",
|
||||
"/node_modules/electron/",
|
||||
"/node_modules/next/",
|
||||
|
|
|
|||
|
|
@ -63,11 +63,14 @@ describe("mac standalone prebundle policy", () => {
|
|||
it("documents the explicit code-level bundle boundaries", () => {
|
||||
expect(MAC_PREBUNDLE_ESBUILD_TARGET).toBe("node24");
|
||||
expect(MAC_PREBUNDLE_POLICIES.packagedMain.externals).toEqual(["electron"]);
|
||||
expect(MAC_PREBUNDLE_POLICIES.daemonCli.externals).toEqual(["better-sqlite3"]);
|
||||
expect(MAC_PREBUNDLE_POLICIES.daemonSidecar.externals).toEqual(["better-sqlite3"]);
|
||||
expect(MAC_PREBUNDLE_POLICIES.daemonCli.externals).toEqual(["better-sqlite3", "blake3-wasm"]);
|
||||
expect(MAC_PREBUNDLE_POLICIES.daemonSidecar.externals).toEqual(["better-sqlite3", "blake3-wasm"]);
|
||||
expect(MAC_PREBUNDLE_POLICIES.webSidecar.externals).toEqual([]);
|
||||
expect(MAC_DAEMON_PREBUNDLE_ESM_REQUIRE_BANNER).toContain("createRequire");
|
||||
expect(MAC_PREBUNDLE_RUNTIME_DEPENDENCIES).toEqual({ "better-sqlite3": "12.9.0" });
|
||||
expect(MAC_PREBUNDLE_RUNTIME_DEPENDENCIES).toEqual({
|
||||
"better-sqlite3": "12.9.0",
|
||||
"blake3-wasm": "2.1.5",
|
||||
});
|
||||
expect(MAC_PREBUNDLED_DAEMON_CLI_RELATIVE_PATH).toBe("app/prebundled/daemon/daemon-cli.mjs");
|
||||
expect(MAC_PREBUNDLED_DAEMON_SIDECAR_RELATIVE_PATH).toBe("app/prebundled/daemon/daemon-sidecar.mjs");
|
||||
expect(MAC_PREBUNDLED_WEB_SIDECAR_RELATIVE_PATH).toBe("app/prebundled/web-sidecar.mjs");
|
||||
|
|
|
|||
Loading…
Reference in a new issue