Add CMMS embedding hooks: load a .scad attachment via window.CMMS_SCAD_CONFIG,
Some checks failed
Test Build / Node v22 (push) Has been cancelled
Test Build / Node v24 (push) Has been cancelled
Test Build / Node v26 (push) Has been cancelled

save the current source back to CMMS via a new "Save to CMMS" button

Mirrors the window.CAD_EDITOR_CONFIG pattern already used by the cad-editor
fork. Load: on boot, if CMMS_SCAD_CONFIG.loadUrl is set, fetch it and seed
the editor with that content instead of restoring the app's own URL-
fragment/localStorage state - reloading always shows the current CMMS
attachment, not a stale local edit. Save: POSTs the current editor source
as plain text to CMMS_SCAD_CONFIG.saveUrl (public/attachment.php's new
scad_save/scad_save_new action) - the server re-renders the DXF companion
itself via render_scad_to_dxf(), so there's one OpenSCAD render
implementation, not a second one duplicated in the browser's WASM engine.

Every new code path is gated on window.CMMS_SCAD_CONFIG being present, so
standalone/upstream use is unaffected.
This commit is contained in:
AI Assistant 2026-08-29 17:23:55 +10:00
commit f471e0245a
2 changed files with 88 additions and 3 deletions

View file

@ -13,6 +13,41 @@ import SettingsMenu from './SettingsMenu.tsx';
import MultimaterialColorsDialog from './MultimaterialColorsDialog.tsx'; import MultimaterialColorsDialog from './MultimaterialColorsDialog.tsx';
// CMMS "Save to CMMS" action (patch, not upstream) - POSTs the current
// editor source text back to public/attachment.php's scad_save/
// scad_save_new action (see public/vendor/openscad-playground/edit.php and
// CLAUDE.md). Deliberately posts only the raw .scad text, not a rendered
// DXF - the server re-renders the DXF companion itself via the same
// render_scad_to_dxf() the AI Assistant's generate_cad_drawing tool uses,
// so there's one render implementation, not a second one duplicated here
// in the browser's WASM engine.
async function saveToCmms(model: any, toast: React.RefObject<Toast>) {
const cfg = window.CMMS_SCAD_CONFIG;
if (!cfg) return;
const state = model.state;
const content = state.params.sources.find((s: any) => s.path === state.params.activePath)?.content ?? '';
const form = new FormData();
form.set('csrf_token', cfg.csrfToken);
form.set('job_id', cfg.jobId);
if (cfg.attachmentId) {
form.set('action', 'scad_save');
form.set('id', cfg.attachmentId);
} else {
form.set('action', 'scad_save_new');
form.set('original_name', cfg.originalName);
}
form.set('file', new Blob([content], {type: 'text/plain'}), cfg.originalName);
try {
const resp = await fetch(cfg.saveUrl, {method: 'POST', credentials: 'same-origin', body: form});
const data = await resp.json();
if (!resp.ok || !data.ok) throw new Error(data.error || `HTTP ${resp.status}`);
if (data.id) cfg.attachmentId = String(data.id);
toast.current?.show({severity: 'success', summary: 'Saved to CMMS', life: 3000});
} catch (e: any) {
toast.current?.show({severity: 'error', summary: 'Save failed', detail: String(e?.message ?? e), life: 6000});
}
}
export default function Footer({style}: {style?: CSSProperties}) { export default function Footer({style}: {style?: CSSProperties}) {
const model = useContext(ModelContext); const model = useContext(ModelContext);
if (!model) throw new Error('No model'); if (!model) throw new Error('No model');
@ -73,6 +108,13 @@ export default function Footer({style}: {style?: CSSProperties}) {
) : undefined ) : undefined
} }
<MultimaterialColorsDialog /> <MultimaterialColorsDialog />
{window.CMMS_SCAD_CONFIG &&
<Button
icon="pi pi-cloud-upload"
className="p-button-sm"
label="Save to CMMS"
onClick={() => saveToCmms(model, toast)}
/>}
{/* <Button {/* <Button
icon="pi pi-bolt" icon="pi pi-bolt"
onClick={() => model.render({isPreview: false, now: true})} onClick={() => model.render({isPreview: false, now: true})}

View file

@ -7,7 +7,7 @@ import { createEditorFS } from './fs/filesystem.ts';
import { registerOpenSCADLanguage } from './language/openscad-register-language.ts'; import { registerOpenSCADLanguage } from './language/openscad-register-language.ts';
import { zipArchives } from './fs/zip-archives.ts'; import { zipArchives } from './fs/zip-archives.ts';
import {readStateFromFragment} from './state/fragment-state.ts' import {readStateFromFragment} from './state/fragment-state.ts'
import { createInitialState } from './state/initial-state.ts'; import { createInitialState, defaultSourcePath } from './state/initial-state.ts';
import './index.css'; import './index.css';
import debug from 'debug'; import debug from 'debug';
@ -31,6 +31,25 @@ if (process.env.NODE_ENV !== 'production') {
declare var BrowserFS: BrowserFSInterface declare var BrowserFS: BrowserFSInterface
// CMMS embedding hook (patch, not upstream) - set only by
// public/vendor/openscad-playground/edit.php when this app is loaded inside
// the CMMS. Mirrors the window.CAD_EDITOR_CONFIG pattern already used by the
// vendored cad-editor fork: a real file (an existing .scad attachment) is
// fetched from loadUrl on boot instead of restoring the URL-fragment/
// localStorage state this app normally persists between visits - reloading
// this editor should always show the current CMMS-side attachment, not a
// stale local edit. See Footer.tsx for the matching "Save to CMMS" action.
interface CmmsScadConfig {
loadUrl?: string;
saveUrl: string;
attachmentId?: string;
jobId: string;
csrfToken: string;
originalName: string;
}
declare global {
interface Window { CMMS_SCAD_CONFIG?: CmmsScadConfig; }
}
window.addEventListener('load', async () => { window.addEventListener('load', async () => {
//* //*
@ -68,7 +87,31 @@ window.addEventListener('load', async () => {
let statePersister: StatePersister; let statePersister: StatePersister;
let persistedState: State | null = null; let persistedState: State | null = null;
if (isInStandaloneMode()) { const cmmsConfig = window.CMMS_SCAD_CONFIG;
if (cmmsConfig) {
// No-op persister: a CMMS-embedded session is explicitly saved via the
// "Save to CMMS" button (Footer.tsx), never via this app's own
// fragment/localStorage auto-persist - same "load fresh every time"
// rule as the vendored cad-editor.
statePersister = { set: async () => {} };
if (cmmsConfig.loadUrl) {
try {
const resp = await fetch(cmmsConfig.loadUrl, {credentials: 'same-origin'});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const content = await resp.text();
const initialState = createInitialState(null, {content, path: cmmsConfig.originalName || defaultSourcePath});
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App initialState={initialState} statePersister={statePersister} fs={fs} />
</React.StrictMode>
);
return;
} catch (e) {
console.error('CMMS: failed to load attachment from', cmmsConfig.loadUrl, e);
}
}
} else if (isInStandaloneMode()) {
const fs: FS = BrowserFS.BFSRequire('fs') const fs: FS = BrowserFS.BFSRequire('fs')
try { try {
const data = JSON.parse(new TextDecoder("utf-8").decode(fs.readFileSync('/state.json'))); const data = JSON.parse(new TextDecoder("utf-8").decode(fs.readFileSync('/state.json')));