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');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
openExternal: (url: string): Promise<boolean> =>
|
||||||
|
ipcRenderer.invoke('shell:open-external', url),
|
||||||
pickFolder: (): Promise<string | null> =>
|
pickFolder: (): Promise<string | null> =>
|
||||||
ipcRenderer.invoke('dialog:pick-folder'),
|
ipcRenderer.invoke('dialog:pick-folder'),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,10 @@ const MAC_WINDOW_CHROME_CSS = `
|
||||||
.entry-header [role="button"],
|
.entry-header [role="button"],
|
||||||
.entry-tabs,
|
.entry-tabs,
|
||||||
.entry-tabs *,
|
.entry-tabs *,
|
||||||
|
.viewer-toolbar,
|
||||||
|
.viewer-toolbar *,
|
||||||
|
.deck-nav,
|
||||||
|
.deck-nav *,
|
||||||
.ds-modal-header,
|
.ds-modal-header,
|
||||||
.ds-modal-header *,
|
.ds-modal-header *,
|
||||||
.ds-modal-actions,
|
.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
|
// a second handler" on the second createDesktopRuntime() call (e.g. dev
|
||||||
// hot-reload). removeHandler is a no-op when nothing is registered.
|
// hot-reload). removeHandler is a no-op when nothing is registered.
|
||||||
ipcMain.removeHandler("dialog:pick-folder");
|
ipcMain.removeHandler("dialog:pick-folder");
|
||||||
|
ipcMain.removeHandler("shell:open-external");
|
||||||
ipcMain.handle("dialog:pick-folder", async () => {
|
ipcMain.handle("dialog:pick-folder", async () => {
|
||||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||||
return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
|
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 consoleEntries: DesktopConsoleEntry[] = [];
|
||||||
const window = new BrowserWindow({
|
const window = new BrowserWindow({
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import type { ConnectorDetail } from '@open-design/contracts';
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI?: {
|
electronAPI?: {
|
||||||
pickFolder: () => Promise<string | null>;
|
openExternal?: (url: string) => Promise<boolean>;
|
||||||
|
pickFolder?: () => Promise<string | null>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -376,7 +377,7 @@ export function NewProjectPanel({
|
||||||
if (!onImportFolder) return;
|
if (!onImportFolder) return;
|
||||||
let pathToOpen: string;
|
let pathToOpen: string;
|
||||||
if (hasElectronPicker) {
|
if (hasElectronPicker) {
|
||||||
const picked = await window.electronAPI!.pickFolder();
|
const picked = await window.electronAPI!.pickFolder!();
|
||||||
if (!picked) return;
|
if (!picked) return;
|
||||||
pathToOpen = picked;
|
pathToOpen = picked;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,15 @@ import type {
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import type { ArtifactManifest } from '../artifacts/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 DEFAULT_DEPLOY_PROVIDER_ID = 'vercel-self';
|
||||||
export const CLOUDFLARE_PAGES_PROVIDER_ID = 'cloudflare-pages';
|
export const CLOUDFLARE_PAGES_PROVIDER_ID = 'cloudflare-pages';
|
||||||
export const DEPLOY_PROVIDER_IDS = [
|
export const DEPLOY_PROVIDER_IDS = [
|
||||||
|
|
@ -289,9 +298,13 @@ async function decodeConnectorError(resp: Response): Promise<string> {
|
||||||
|
|
||||||
export async function connectConnector(connectorId: string): Promise<ConnectorActionResult> {
|
export async function connectConnector(connectorId: string): Promise<ConnectorActionResult> {
|
||||||
let authWindow: Window | null = null;
|
let authWindow: Window | null = null;
|
||||||
|
const openExternal = window.electronAPI?.openExternal;
|
||||||
|
const useExternalBrowser = typeof openExternal === 'function';
|
||||||
try {
|
try {
|
||||||
authWindow = window.open('about:blank', '_blank');
|
if (!useExternalBrowser) {
|
||||||
renderConnectorAuthLoading(authWindow);
|
authWindow = window.open('about:blank', '_blank');
|
||||||
|
renderConnectorAuthLoading(authWindow);
|
||||||
|
}
|
||||||
const resp = await fetch(`/api/connectors/${encodeURIComponent(connectorId)}/connect`, {
|
const resp = await fetch(`/api/connectors/${encodeURIComponent(connectorId)}/connect`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
|
|
@ -301,7 +314,12 @@ export async function connectConnector(connectorId: string): Promise<ConnectorAc
|
||||||
}
|
}
|
||||||
const json = (await resp.json()) as ConnectorConnectResponse;
|
const json = (await resp.json()) as ConnectorConnectResponse;
|
||||||
if (json.auth?.kind === 'redirect_required' && json.auth.redirectUrl) {
|
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;
|
authWindow.location.href = json.auth.redirectUrl;
|
||||||
} else {
|
} else {
|
||||||
const redirected = window.open(json.auth.redirectUrl, '_blank');
|
const redirected = window.open(json.auth.redirectUrl, '_blank');
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,51 @@ describe('connectConnector', () => {
|
||||||
});
|
});
|
||||||
expect(open).toHaveBeenCalledTimes(2);
|
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', () => {
|
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_ENTRYPOINTS_DIR_NAME = "prebundle-entrypoints";
|
||||||
|
|
||||||
export const MAC_PREBUNDLE_RUNTIME_DEPENDENCIES = {
|
export const MAC_PREBUNDLE_RUNTIME_DEPENDENCIES = {
|
||||||
|
"blake3-wasm": "2.1.5",
|
||||||
"better-sqlite3": "12.9.0",
|
"better-sqlite3": "12.9.0",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -42,9 +43,10 @@ export const MAC_PREBUNDLE_POLICIES = {
|
||||||
label: "packaged main",
|
label: "packaged main",
|
||||||
},
|
},
|
||||||
daemonCli: {
|
daemonCli: {
|
||||||
externals: ["better-sqlite3"],
|
externals: ["better-sqlite3", "blake3-wasm"],
|
||||||
forbiddenInputs: [
|
forbiddenInputs: [
|
||||||
"/node_modules/@open-design/daemon/",
|
"/node_modules/@open-design/daemon/",
|
||||||
|
"/node_modules/blake3-wasm/",
|
||||||
"/node_modules/better-sqlite3/",
|
"/node_modules/better-sqlite3/",
|
||||||
"/node_modules/electron/",
|
"/node_modules/electron/",
|
||||||
"/node_modules/next/",
|
"/node_modules/next/",
|
||||||
|
|
@ -55,9 +57,10 @@ export const MAC_PREBUNDLE_POLICIES = {
|
||||||
label: "daemon cli",
|
label: "daemon cli",
|
||||||
},
|
},
|
||||||
daemonSidecar: {
|
daemonSidecar: {
|
||||||
externals: ["better-sqlite3"],
|
externals: ["better-sqlite3", "blake3-wasm"],
|
||||||
forbiddenInputs: [
|
forbiddenInputs: [
|
||||||
"/node_modules/@open-design/daemon/",
|
"/node_modules/@open-design/daemon/",
|
||||||
|
"/node_modules/blake3-wasm/",
|
||||||
"/node_modules/better-sqlite3/",
|
"/node_modules/better-sqlite3/",
|
||||||
"/node_modules/electron/",
|
"/node_modules/electron/",
|
||||||
"/node_modules/next/",
|
"/node_modules/next/",
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,14 @@ describe("mac standalone prebundle policy", () => {
|
||||||
it("documents the explicit code-level bundle boundaries", () => {
|
it("documents the explicit code-level bundle boundaries", () => {
|
||||||
expect(MAC_PREBUNDLE_ESBUILD_TARGET).toBe("node24");
|
expect(MAC_PREBUNDLE_ESBUILD_TARGET).toBe("node24");
|
||||||
expect(MAC_PREBUNDLE_POLICIES.packagedMain.externals).toEqual(["electron"]);
|
expect(MAC_PREBUNDLE_POLICIES.packagedMain.externals).toEqual(["electron"]);
|
||||||
expect(MAC_PREBUNDLE_POLICIES.daemonCli.externals).toEqual(["better-sqlite3"]);
|
expect(MAC_PREBUNDLE_POLICIES.daemonCli.externals).toEqual(["better-sqlite3", "blake3-wasm"]);
|
||||||
expect(MAC_PREBUNDLE_POLICIES.daemonSidecar.externals).toEqual(["better-sqlite3"]);
|
expect(MAC_PREBUNDLE_POLICIES.daemonSidecar.externals).toEqual(["better-sqlite3", "blake3-wasm"]);
|
||||||
expect(MAC_PREBUNDLE_POLICIES.webSidecar.externals).toEqual([]);
|
expect(MAC_PREBUNDLE_POLICIES.webSidecar.externals).toEqual([]);
|
||||||
expect(MAC_DAEMON_PREBUNDLE_ESM_REQUIRE_BANNER).toContain("createRequire");
|
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_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_DAEMON_SIDECAR_RELATIVE_PATH).toBe("app/prebundled/daemon/daemon-sidecar.mjs");
|
||||||
expect(MAC_PREBUNDLED_WEB_SIDECAR_RELATIVE_PATH).toBe("app/prebundled/web-sidecar.mjs");
|
expect(MAC_PREBUNDLED_WEB_SIDECAR_RELATIVE_PATH).toBe("app/prebundled/web-sidecar.mjs");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue