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:
@@ -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',
|
||||
|
||||
@@ -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)]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user