feat: embed Twenty CRM via iframe on /crm route

Switch /crm and ?view=app from custom folk-crm-view component to
renderExternalAppShell iframe embedding crm.rspace.online. Also fix
twentyQuery endpoint from /api to /api/graphql for the data proxy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-04 21:26:16 -08:00
parent 99749d8cf2
commit 2ba2034e3a
1 changed files with 10 additions and 12 deletions

View File

@ -7,7 +7,7 @@
*/ */
import { Hono } from "hono"; import { Hono } from "hono";
import { renderShell } from "../../server/shell"; import { renderShell, renderExternalAppShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing"; import { renderLanding } from "./landing";
@ -34,7 +34,7 @@ function getTokenForSpace(space: string): string {
async function twentyQuery(query: string, variables?: Record<string, unknown>, space?: string) { async function twentyQuery(query: string, variables?: Record<string, unknown>, space?: string) {
const token = space ? getTokenForSpace(space) : TWENTY_DEFAULT_TOKEN; const token = space ? getTokenForSpace(space) : TWENTY_DEFAULT_TOKEN;
if (!token) return null; if (!token) return null;
const res = await fetch(`${TWENTY_API_URL}/api`, { const res = await fetch(`${TWENTY_API_URL}/api/graphql`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -269,17 +269,16 @@ routes.get("/api/opportunities", async (c) => {
return c.json({ opportunities }); return c.json({ opportunities });
}); });
// ── CRM sub-route — API-driven CRM view ── // ── CRM sub-route — embed Twenty CRM via iframe ──
routes.get("/crm", (c) => { routes.get("/crm", (c) => {
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
return c.html(renderShell({ return c.html(renderExternalAppShell({
title: `${space} — CRM | rSpace`, title: `${space} — CRM | rSpace`,
moduleId: "rnetwork", moduleId: "rnetwork",
spaceSlug: space, spaceSlug: space,
modules: getModuleInfoList(), modules: getModuleInfoList(),
body: `<folk-crm-view space="${space}"></folk-crm-view>`, appUrl: "https://crm.rspace.online",
scripts: `<script type="module" src="/modules/rnetwork/folk-crm-view.js"></script>`, appName: "Twenty CRM",
styles: `<link rel="stylesheet" href="/modules/rnetwork/network.css">`,
})); }));
}); });
@ -289,14 +288,13 @@ routes.get("/", (c) => {
const view = c.req.query("view"); const view = c.req.query("view");
if (view === "app") { if (view === "app") {
return c.html(renderShell({ return c.html(renderExternalAppShell({
title: `${space} — CRM | rSpace`, title: `${space} — CRM | rSpace`,
moduleId: "rnetwork", moduleId: "rnetwork",
spaceSlug: space, spaceSlug: space,
modules: getModuleInfoList(), modules: getModuleInfoList(),
body: `<folk-crm-view space="${space}"></folk-crm-view>`, appUrl: "https://crm.rspace.online",
scripts: `<script type="module" src="/modules/rnetwork/folk-crm-view.js"></script>`, appName: "Twenty CRM",
styles: `<link rel="stylesheet" href="/modules/rnetwork/network.css">`,
})); }));
} }
@ -321,7 +319,7 @@ export const networkModule: RSpaceModule = {
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,
standaloneDomain: "rnetwork.online", standaloneDomain: "rnetwork.online",
externalApp: { url: "https://demo.rnetwork.online", name: "Twenty CRM" }, externalApp: { url: "https://crm.rspace.online", name: "Twenty CRM" },
feeds: [ feeds: [
{ {
id: "trust-graph", id: "trust-graph",