Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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(() => {
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';

/**
* 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();
};
39 changes: 39 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,39 @@
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[]> = {
background: ['--bg-canvas', '--background-800', '--background-900'],
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);
}, []);
};
52 changes: 52 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,52 @@
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;
window.parent.postMessage(msg, '*');
};

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);
33 changes: 19 additions & 14 deletions apps/web/src/pods/toolbar/toolbar.pod.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import { isVSCodeEnv } from '@/core/vscode/env.helpers';
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';

export const ToolbarPod: React.FC = () => {
const isVSCode = isVSCodeEnv();

return (
<header className={classes.container}>
<NewButton />
<OpenButton />
<SaveButton />
{!isVSCode && <OpenButton />}
{!isVSCode && <SaveButton />}
<ZoomInButton />
<ZoomOutButton />
<ImportButton />
Expand All @@ -36,7 +39,9 @@ export const ToolbarPod: React.FC = () => {
<DeleteButton />
<CanvasSettingButton />
<AboutButton />
<ThemeToggleButton darkLabel="Dark Mode" lightLabel="Light Mode" />
{!isVSCode && (
<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
10 changes: 9 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/bridge-protocol/src/index.ts

This file was deleted.

11 changes: 11 additions & 0 deletions packages/vscode-bridge-protocol/src/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const HOST_MESSAGE_TYPE = {
SAVED: 'mm:saved',
LOAD_FILE: 'mm:load-file',
THEME: 'mm:theme',
} as const;

export const APP_MESSAGE_TYPE = {
READY: 'mm:ready',
SAVE: 'mm:save',
WEBVIEW_READY: 'mm:webview-ready',
} as const;
2 changes: 2 additions & 0 deletions packages/vscode-bridge-protocol/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './constant';
export * from './model';
25 changes: 25 additions & 0 deletions packages/vscode-bridge-protocol/src/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { APP_MESSAGE_TYPE, HOST_MESSAGE_TYPE } from './constant';

export interface LoadFilePayload {
data: unknown;
fileName: string;
}

export interface ThemePayload {
background: string;
backgroundSecondary: string;
foreground: string;
}

export type HostMessage =
| { type: typeof HOST_MESSAGE_TYPE.SAVED }
| { type: typeof HOST_MESSAGE_TYPE.LOAD_FILE; payload: LoadFilePayload }
| { type: typeof HOST_MESSAGE_TYPE.THEME; payload: ThemePayload };

export type AppMessage =
| { type: typeof APP_MESSAGE_TYPE.READY }
| { type: typeof APP_MESSAGE_TYPE.WEBVIEW_READY }
| { type: typeof APP_MESSAGE_TYPE.SAVE; payload: { content: string } };

export type PayloadOf<U extends { type: string }, T extends U['type']> =
Extract<U, { type: T }> extends { payload: infer P } ? P : undefined;
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './new-diagram';
export * from './register';
Loading