feat(fusion_repairs): flowchart designer node content uses Odoo Wysiwyg

Replace the plain <textarea> in the flowchart designer's node-editor
right-panel with Odoo 19's native rich text editor so admins write
formatted prose / lists / bold / links / inline images without typing
HTML tags. The raw <textarea> stays available behind a toggle for the
power-user case (pasting markup from elsewhere, debugging).

CHANGES

manifest:
  - depends += 'html_editor' (provides @html_editor/wysiwyg)
  - bumped to 19.0.2.2.1

components/flowchart_designer/flowchart_designer.js:
  - import { Wysiwyg } from '@html_editor/wysiwyg'
  - import { MAIN_PLUGINS } from '@html_editor/plugin_sets'
  - register Wysiwyg in static components
  - state.sourceMode boolean (default false = rich text mode)
  - wysiwygConfig getter builds the EditorConfig for the SELECTED node;
    onChange reads editor.getContent() and writes back into the same
    selectedMeta.content_html the rest of the designer already uses,
    so the save path is unchanged
  - onWysiwygLoad(editor) captures the editor instance per dfId so the
    onChange callback can resolve the right one when nodes switch
  - onToggleSource flushes the current editor's content before flipping
    modes so unsaved keystrokes don't get lost

components/flowchart_designer/flowchart_designer.xml:
  - replaced <textarea>...</textarea> with a conditional block:
      sourceMode == false -> <Wysiwyg t-key="'wysiwyg-' + selectedNodeId"
                                       config="wysiwygConfig"
                                       onLoad="onWysiwygLoad.bind(this)"/>
      sourceMode == true  -> <textarea class="font-monospace" rows="10"/>
  - t-key forces the editor to re-mount with the freshly-selected node's
    content; otherwise switching nodes would keep showing the first
    selected node's HTML
  - new toolbar row above the editor has a "HTML Source" / "Rich Text"
    toggle button (eye / code icons) so the user can flip at will
  - hint text updated to reflect what each mode supports

components/flowchart_designer/flowchart_designer.scss:
  - widened the right editor panel from 320px to 360px to give the
    Wysiwyg toolbar room to breathe
  - new .fr-wysiwyg-shell rule frames the embedded editor with the same
    border + background as the other form-controls in the panel, with
    a min-height of 180px and max-height 320px so it scrolls when the
    content grows. Pins .o-we-toolbar inside the shell so it stays in
    view as the user scrolls long content.

The save path, the runtime renderer, and the data model are unchanged -
content_html is still sanitised HTML stored on fusion.repair.flowchart.node.

Verified on local westin-v19:
  - upgrade clean (no errors, no warnings)
  - login serves 200 after restart
  - 4 stale asset bundles flushed; Drawflow JS still served 46KB at
    /fusion_repairs/static/src/lib/drawflow/drawflow.min.js
  - Wysiwyg export confirmed at
    /usr/lib/python3/dist-packages/odoo/addons/html_editor/static/src/wysiwyg.js:25
  - MAIN_PLUGINS export confirmed at plugin_sets.js:103

Bumped to 19.0.2.2.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gsinghpal
2026-05-27 13:18:27 -04:00
parent 134c94fc6c
commit b22bb11b31
4 changed files with 105 additions and 8 deletions

View File

@@ -4,7 +4,7 @@
{
'name': 'Fusion Repairs',
'version': '19.0.2.2.0',
'version': '19.0.2.2.1',
'category': 'Inventory/Repairs',
'summary': 'Guided medical equipment repair intake, dispatch, maintenance, and self-service portal',
'description': """
@@ -52,6 +52,7 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'depends': [
'base',
'mail',
'html_editor',
'portal',
'website',
'sale_management',

View File

@@ -24,6 +24,8 @@ import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
import { loadJS, loadCSS } from "@web/core/assets";
import { Wysiwyg } from "@html_editor/wysiwyg";
import { MAIN_PLUGINS } from "@html_editor/plugin_sets";
const DRAWFLOW_JS = "/fusion_repairs/static/src/lib/drawflow/drawflow.min.js";
const DRAWFLOW_CSS = "/fusion_repairs/static/src/lib/drawflow/drawflow.min.css";
@@ -43,6 +45,7 @@ const OUTCOME_COLORS = {
export class FlowchartDesigner extends Component {
static template = "fusion_repairs.FlowchartDesigner";
static components = { Wysiwyg };
static props = ["*"];
setup() {
@@ -63,6 +66,8 @@ export class FlowchartDesigner extends Component {
selectedNodeId: null,
dirty: false,
saving: false,
// Default to rich-text mode; the user can toggle to raw HTML.
sourceMode: false,
// client_id -> drawflow numeric id mapping for new nodes (those
// without a DB id yet, created via "Add Node")
clientToDfId: {},
@@ -70,6 +75,54 @@ export class FlowchartDesigner extends Component {
// outcome_kind, media_ids}
nodeMeta: {},
});
// Per-selected-node Wysiwyg editor instance captured via onLoad.
// Keyed by dfId so switching nodes correctly reads the right editor.
this.wysiwygEditors = {};
}
// ------------------------------------------------------------------
// RICH TEXT EDITOR INTEGRATION
// ------------------------------------------------------------------
/**
* Build the config object the <Wysiwyg> component expects for the
* currently selected node. Recomputed each render so switching nodes
* re-mounts the editor with the right content (via the t-key on the
* template).
*/
get wysiwygConfig() {
const meta = this.selectedMeta;
return {
content: meta?.content_html || "",
Plugins: MAIN_PLUGINS,
// onChange fires after each edit step - read the current HTML
// from the editor and persist it to selectedMeta + dirty flag.
onChange: () => {
if (!this.state.selectedNodeId) return;
const editor = this.wysiwygEditors[this.state.selectedNodeId];
if (!editor) return;
const html = editor.getContent();
this.onEditorFieldChange("content_html", html);
},
};
}
onWysiwygLoad(editor) {
if (this.state.selectedNodeId) {
this.wysiwygEditors[this.state.selectedNodeId] = editor;
}
}
onToggleSource() {
// Before flipping modes, persist the current editor's content so we
// don't lose unsaved keystrokes.
if (!this.state.sourceMode && this.state.selectedNodeId) {
const editor = this.wysiwygEditors[this.state.selectedNodeId];
if (editor) {
this.onEditorFieldChange("content_html", editor.getContent());
}
}
this.state.sourceMode = !this.state.sourceMode;
}
onMounted(async () => {
await Promise.all([loadJS(DRAWFLOW_JS), loadCSS(DRAWFLOW_CSS)]);

View File

@@ -60,7 +60,7 @@ $fr-panel: var(--fr-panel-bg, #{$_fr_panel-hex});
}
.fr-designer-editor {
width: 320px;
width: 360px;
border-left: 1px solid $fr-border;
background: $fr-panel;
padding: 12px 14px;
@@ -68,6 +68,26 @@ $fr-panel: var(--fr-panel-bg, #{$_fr_panel-hex});
h6 { margin-bottom: 8px; font-weight: 700; }
}
// Frame the embedded Odoo Wysiwyg so it visually matches the other
// form-control fields in the right panel (border, padding, scrollable).
.fr-wysiwyg-shell {
border: 1px solid $fr-border;
border-radius: 4px;
background: $fr-card;
min-height: 180px;
max-height: 320px;
overflow-y: auto;
.odoo-editor-editable, .fr-wysiwyg-content {
min-height: 160px;
padding: 8px 10px;
outline: none;
}
.o-we-toolbar, .o_we_toolbar {
// Pin the Wysiwyg toolbar inside the shell so it scrolls with content.
z-index: 2;
}
}
// ----- Node card styling (inside Drawflow's drawflow_content_node) -----
.drawflow .drawflow-node {
background: transparent !important;

View File

@@ -75,12 +75,35 @@
<option value="order_part" t-att-selected="selectedMeta.outcome_kind === 'order_part'">Order part</option>
</select>
</div>
<div class="mb-2">
<label class="form-label small mb-1">Content (shown to CS)</label>
<textarea class="form-control form-control-sm" rows="6"
t-on-change="(ev) => this.onEditorFieldChange('content_html', ev.target.value)"
t-out="selectedMeta.content_html"/>
<div class="form-text">HTML allowed: lists, bold, links.</div>
<div class="mb-2 fr-content-editor">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label small mb-0">Content (shown to CS)</label>
<button type="button"
class="btn btn-link btn-sm p-0"
t-on-click="onToggleSource">
<i t-att-class="state.sourceMode ? 'fa fa-eye' : 'fa fa-code'"/>
<t t-if="state.sourceMode"> Rich Text</t>
<t t-else=""> HTML Source</t>
</button>
</div>
<!-- Rich text (default) - re-mounts when selecting a different node via t-key. -->
<t t-if="!state.sourceMode">
<div class="fr-wysiwyg-shell">
<Wysiwyg t-key="'wysiwyg-' + state.selectedNodeId"
config="wysiwygConfig"
onLoad="onWysiwygLoad.bind(this)"
contentClass="'fr-wysiwyg-content'"/>
</div>
<div class="form-text">Bold, italic, lists, links and inline images supported. Click <em>HTML Source</em> to paste raw markup.</div>
</t>
<!-- HTML source mode (toggle) - power user escape hatch. -->
<t t-else="">
<textarea class="form-control form-control-sm font-monospace"
rows="10"
t-on-change="(ev) => this.onEditorFieldChange('content_html', ev.target.value)"
t-out="selectedMeta.content_html"/>
<div class="form-text">Raw HTML mode - what you type is rendered as-is.</div>
</t>
</div>
<div class="mb-2">
<label class="form-label small mb-1">