Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Mongo Modeler Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-extension"
],
"outFiles": ["${workspaceFolder}/packages/vscode-extension/dist/**/*.mjs"]
}
]
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"mongo-modeler.appUrl": "http://localhost:5173/editor.html"
}
2 changes: 2 additions & 0 deletions apps/web/src/core/vscode/env.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isVSCodeEnv = (): boolean =>
new URLSearchParams(window.location.search).get('env') === 'vscode';
1 change: 1 addition & 0 deletions apps/web/src/core/vscode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './use-vscode-sync.hook';
46 changes: 46 additions & 0 deletions apps/web/src/core/vscode/use-vscode-auto-save.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { APP_MESSAGE_TYPE } from '@lemoncode/mongo-modeler-bridge-protocol';
import { useEffect, useRef, type MutableRefObject } from 'react';
import { useCanvasSchemaContext } from '@/core/providers';
import { isVSCodeEnv } from './env.helpers';
import { sendToExtension } from './vscode-bridge.helpers';
import { serializeSchema } from './vscode-sync.helpers';

const AUTO_SAVE_DEBOUNCE_MS = 500;

export const useVSCodeAutoSave = (
hasReceivedFileRef: MutableRefObject<boolean>
): void => {
const { canvasSchema } = useCanvasSchemaContext();

const lastSavedContentRef = useRef<string | null>(null);
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
if (!isVSCodeEnv() || !hasReceivedFileRef.current) return;

const content = serializeSchema(canvasSchema);

if (lastSavedContentRef.current === null) {
lastSavedContentRef.current = content;
return;
}

if (content === lastSavedContentRef.current) return;

debounceTimerRef.current = setTimeout(() => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Debounce timer no se limpia entre renders ni en early-returns

El efecto depende de [canvasSchema]. Cuando el schema cambia rápido, cada render entra en este useEffect, asigna un timer nuevo a debounceTimerRef.current y deja al anterior huérfano (la cleanup function solo corre cuando el efecto se va a re-ejecutar o el componente se desmonta, y para entonces ya hemos pisado la referencia). Además, las early-returns de las líneas 19, 25 y 28 tampoco limpian un timer en vuelo, así que pueden disparar un SAVE con contenido obsoleto capturado en el closure.

Sugerencia: limpiar el timer previo antes de programar el nuevo y al inicio del efecto:

useEffect(() => {
  if (debounceTimerRef.current) {
    clearTimeout(debounceTimerRef.current);
    debounceTimerRef.current = null;
  }
  if (!isVSCodeEnv() || !hasReceivedFileRef.current) return;
  // ...resto igual
});

sendToExtension({
type: APP_MESSAGE_TYPE.SAVE,
payload: { content },
});
lastSavedContentRef.current = content;
debounceTimerRef.current = null;
}, AUTO_SAVE_DEBOUNCE_MS);

return () => {
if (debounceTimerRef.current !== null) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
};
}, [canvasSchema]);
};
50 changes: 50 additions & 0 deletions apps/web/src/core/vscode/use-vscode-file-load.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
useCanvasSchemaContext,
useCanvasViewSettingsContext,
} from '@/core/providers';
import {
APP_MESSAGE_TYPE,
HOST_MESSAGE_TYPE,
type LoadFilePayload,
} from '@lemoncode/mongo-modeler-bridge-protocol';
import { useEffect, useRef, type MutableRefObject } from 'react';
import { isVSCodeEnv } from './env.helpers';
import { onMessage, sendToExtension } from './vscode-bridge.helpers';
import { deserializeSchema } from './vscode-sync.helpers';

export const useVSCodeFileLoad = (): MutableRefObject<boolean> => {
const { loadSchema } = useCanvasSchemaContext();
const { setFilename, setLoadSample } = useCanvasViewSettingsContext();

const loadSchemaRef = useRef(loadSchema);
const setFilenameRef = useRef(setFilename);
const setLoadSampleRef = useRef(setLoadSample);

useEffect(() => {
loadSchemaRef.current = loadSchema;
setFilenameRef.current = setFilename;
setLoadSampleRef.current = setLoadSample;
});

const hasReceivedFileRef = useRef(false);

useEffect(() => {
if (!isVSCodeEnv()) return;

const unsubscribe = onMessage(
HOST_MESSAGE_TYPE.LOAD_FILE,
(payload: LoadFilePayload) => {
hasReceivedFileRef.current = true;
setFilenameRef.current(payload.fileName);
setLoadSampleRef.current(false);
loadSchemaRef.current(deserializeSchema(payload.data));
}
);

sendToExtension({ type: APP_MESSAGE_TYPE.WEBVIEW_READY });

return unsubscribe;
}, []);

return hasReceivedFileRef;
};
13 changes: 13 additions & 0 deletions apps/web/src/core/vscode/use-vscode-sync.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useVSCodeAutoSave } from './use-vscode-auto-save.hook';
import { useVSCodeFileLoad } from './use-vscode-file-load.hook';
import { useVSCodeTheme } from './use-vscode-theme.hook';

/**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 JSDoc multilínea para algo que el código ya transmite

El proyecto evita JSDoc multilínea cuando el nombre del símbolo es autoexplicativo. useVSCodeSync + el comportamiento de los hooks internos (cada uno hace su propio early-return en !isVSCodeEnv()) ya dicen lo mismo.

Sugerencia: borrar el bloque o reducirlo a una línea (// no-ops when not inside the VS Code webview).

* Wires the VS Code webview bridge. The inner hooks no-op when not running
* inside a VS Code webview, so this can be called unconditionally.
*/
export const useVSCodeSync = (): void => {
const hasReceivedFileRef = useVSCodeFileLoad();
useVSCodeAutoSave(hasReceivedFileRef);
useVSCodeTheme();
};
40 changes: 40 additions & 0 deletions apps/web/src/core/vscode/use-vscode-theme.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
HOST_MESSAGE_TYPE,
type ThemePayload,
} from '@lemoncode/mongo-modeler-bridge-protocol';
import { useEffect } from 'react';
import { isVSCodeEnv } from './env.helpers';
import { onMessage } from './vscode-bridge.helpers';

const CSS_VAR_MAP: Record<keyof ThemePayload, readonly string[]> = {
// VS Code sends a reduced palette; we fan out colors to existing app tokens in a simplified way.
background: ['--bg-canvas', '--background-800', '--background-900'],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mapeo background → tres variables CSS con tonos distintos

--bg-canvas, --background-800 y --background-900 reciben el mismo valor. En el sistema de diseño actual --background-800 y --background-900 son tonos diferentes; aplanarlos al mismo color puede romper jerarquía visual (mismo gris para canvas, paneles y bordes). Lo mismo con backgroundSecondary y sus cinco vars.

Sugerencia: ampliar ThemePayload para exponer más niveles (backgroundElevated, backgroundMuted, etc.) y mapear cada variable a su nivel real; o, si es intencional aplanar la paleta en VS Code, dejar un comentario explicándolo.

backgroundSecondary: [
'--bg-toolbar',
'--bg-table',
'--bg-input',
'--background-700',
'--background-400',
],
foreground: ['--text-color'],
};

const applyTheme = (theme: ThemePayload): void => {
const root = document.documentElement;
for (const [key, cssVars] of Object.entries(CSS_VAR_MAP)) {
const value = theme[key as keyof ThemePayload];
if (!value) continue;
for (const cssVar of cssVars) {
root.style.setProperty(cssVar, value);
}
}
if (theme.background) document.body.style.backgroundColor = theme.background;
if (theme.foreground) document.body.style.color = theme.foreground;
};

export const useVSCodeTheme = (): void => {
useEffect(() => {
if (!isVSCodeEnv()) return;
return onMessage(HOST_MESSAGE_TYPE.THEME, applyTheme);
}, []);
};
54 changes: 54 additions & 0 deletions apps/web/src/core/vscode/vscode-bridge.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {
AppMessage,
HostMessage,
PayloadOf,
} from '@lemoncode/mongo-modeler-bridge-protocol';
import { isVSCodeEnv } from './env.helpers';

type HandlerFor<T extends HostMessage['type']> = (
payload: PayloadOf<HostMessage, T>
) => void;

type AnyHandler = (payload: unknown) => void;

const handlers = new Map<string, Set<AnyHandler>>();

export const sendToExtension = (msg: AppMessage): void => {
if (!isVSCodeEnv()) return;
// In VS Code webviews the parent origin is not a stable app URL.
// We post to parent and rely on origin/source checks in the bridge listeners.
window.parent.postMessage(msg, '*');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 postMessage con origen wildcard

window.parent.postMessage(msg, '*') envía el payload (schema completo, contenido del archivo en cada SAVE) a cualquier frame padre. Si la página padre no es la esperada — o si el webview de VS Code se anida en algún contexto inesperado — el contenido se filtra.

Sugerencia: usar un origen concreto. El webview de VS Code expone el origen del host vía acquireVsCodeApi (lado del bridge en webview/bridge.ts), pero desde el iframe podemos restringir al origen del padre conocido — o, como mínimo, validar event.origin en el listener inverso y reciclar ese mismo origen como target.

};

export const onMessage = <T extends HostMessage['type']>(
type: T,
handler: HandlerFor<T>
): (() => void) => {
if (!isVSCodeEnv()) return () => { };

const existing = handlers.get(type) ?? new Set<AnyHandler>();
existing.add(handler as AnyHandler);
handlers.set(type, existing);

return () => {
const set = handlers.get(type);
if (!set) return;
set.delete(handler as AnyHandler);
if (set.size === 0) handlers.delete(type);
};
};

if (typeof window !== 'undefined' && isVSCodeEnv()) {
window.addEventListener('message', (event: MessageEvent) => {
if (event.source !== window.parent) return;

const msg = event.data as Partial<HostMessage> | undefined;
if (!msg?.type) return;

const set = handlers.get(msg.type);
if (!set) return;

const payload = (msg as { payload?: unknown }).payload;
for (const handler of set) handler(payload);
});
}
8 changes: 8 additions & 0 deletions apps/web/src/core/vscode/vscode-sync.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type DatabaseSchemaVm } from '@/core/providers';
import { mapSchemaToLatestVersion } from '@/core/providers/canvas-schema/canvas-schema.mapper';

export const deserializeSchema = (data: unknown): DatabaseSchemaVm =>
mapSchemaToLatestVersion(data);

export const serializeSchema = (schema: DatabaseSchemaVm): string =>
JSON.stringify(schema, null, 2);
27 changes: 15 additions & 12 deletions apps/web/src/pods/toolbar/toolbar.pod.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { isVSCodeEnv } from '@/core/vscode/env.helpers';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Import order y comentario huérfano

Dos cosas en este reorder:

  1. isVSCodeEnv (interno, @/core/vscode/...) queda antes que React (externo). El resto del repo agrupa externos primero, luego internos con alias @/.
  2. La línea 17 deja un comentario // CanvasSettingButton, dentro del bloque de imports — ya estaba antes, pero este es buen momento para limpiarlo.

Sugerencia: mover import React arriba del todo y borrar el comentario huérfano.

import React from 'react';
import {
// CanvasSettingButton,
ZoomInButton,
ZoomOutButton,
ThemeToggleButton,
AboutButton,
CanvasSettingButton,
CopyButton,
DeleteButton,
ExportButton,
ImportButton,
NewButton,
OpenButton,
PasteButton,
RedoButton,
SaveButton,
ThemeToggleButton,
UndoButton,
RedoButton,
DeleteButton,
AboutButton,
CanvasSettingButton,
CopyButton,
PasteButton,
ImportButton,
// CanvasSettingButton,
ZoomInButton,
ZoomOutButton,
} from './components';
import classes from './toolbar.pod.module.css';

Expand All @@ -36,7 +37,9 @@ export const ToolbarPod: React.FC = () => {
<DeleteButton />
<CanvasSettingButton />
<AboutButton />
<ThemeToggleButton darkLabel="Dark Mode" lightLabel="Light Mode" />
{!isVSCodeEnv() && (
<ThemeToggleButton darkLabel="Dark Mode" lightLabel="Light Mode" />
)}
</header>
);
};
12 changes: 8 additions & 4 deletions apps/web/src/scenes/main.scene.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { ModalDialog } from '@/common/components';
import { useDeviceContext, useModalDialogContext } from '@/core/providers';
import { useVSCodeSync } from '@/core/vscode';
import { CanvasPod } from '@/pods/canvas/canvas.pod';
import { FloatingBarPod } from '@/pods/floating-bar';
import { FooterPod } from '@/pods/footer';
import { ToolbarPod } from '@/pods/toolbar/toolbar.pod';
import { useDeviceContext, useModalDialogContext } from '@/core/providers';
import { ModalDialog } from '@/common/components';
import classes from './main.scene.module.css';
import { FooterPod } from '@/pods/footer';
import { FloatingBarPod } from '@/pods/floating-bar';

export const MainScene: React.FC = () => {
const { modalDialog } = useModalDialogContext();
const { isTabletOrMobileDevice } = useDeviceContext();

useVSCodeSync();

return (
<>
<div className={classes.container} aria-hidden={modalDialog.isOpen}>
Expand Down
Loading
Loading