Files
Odoo-Modules/fusion_repairs/static/src/components/flowchart_designer/flowchart_designer.xml
gsinghpal b22bb11b31 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>
2026-05-27 13:18:27 -04:00

153 lines
9.5 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_repairs.FlowchartDesigner">
<div class="fr-designer-wrap">
<div class="fr-designer-toolbar">
<span class="fr-designer-title" t-if="state.chart">
<strong t-out="state.chart.name"/>
<span class="text-muted ms-2">v<t t-out="state.chart.version"/></span>
</span>
<div class="fr-designer-toolbar-actions">
<div class="btn-group btn-group-sm me-2">
<button class="btn btn-outline-primary" t-on-click="() => this.onAddNode('question')">
<i class="fa fa-plus"/> Question
</button>
<button class="btn btn-outline-success" t-on-click="() => this.onAddNode('suggestion')">
<i class="fa fa-plus"/> Suggestion
</button>
<button class="btn btn-outline-secondary" t-on-click="() => this.onAddNode('info')">
<i class="fa fa-plus"/> Info
</button>
<button class="btn btn-outline-danger" t-on-click="() => this.onAddNode('outcome')">
<i class="fa fa-plus"/> Outcome
</button>
</div>
<div class="btn-group btn-group-sm me-2">
<button class="btn btn-light" t-on-click="onZoomOut"><i class="fa fa-search-minus"/></button>
<button class="btn btn-light" t-on-click="onZoomReset">100%</button>
<button class="btn btn-light" t-on-click="onZoomIn"><i class="fa fa-search-plus"/></button>
</div>
<button class="btn btn-primary btn-sm"
t-on-click="onSave"
t-att-disabled="state.saving">
<i class="fa fa-save"/>
<t t-if="state.saving">Saving...</t>
<t t-elif="state.dirty">Save *</t>
<t t-else="">Save</t>
</button>
</div>
</div>
<div class="fr-designer-body">
<div class="fr-designer-canvas-wrap">
<div t-ref="canvas" class="drawflow"/>
</div>
<div t-ref="editorPanel" class="fr-designer-editor">
<h6>Node Editor</h6>
<t t-if="!selectedMeta">
<p class="text-muted small">Click a node on the canvas to edit it. Drag from a node's right edge to another node to create an edge.</p>
</t>
<t t-if="selectedMeta">
<div class="mb-2">
<label class="form-label small mb-1">Title</label>
<input class="form-control form-control-sm"
t-att-value="selectedMeta.name"
t-on-change="(ev) => this.onEditorFieldChange('name', ev.target.value)"/>
</div>
<div class="mb-2">
<label class="form-label small mb-1">Type</label>
<select class="form-select form-select-sm"
t-on-change="(ev) => this.onEditorFieldChange('node_type', ev.target.value)">
<option value="question" t-att-selected="selectedMeta.node_type === 'question'">Question</option>
<option value="suggestion" t-att-selected="selectedMeta.node_type === 'suggestion'">Suggestion</option>
<option value="info" t-att-selected="selectedMeta.node_type === 'info'">Info</option>
<option value="outcome" t-att-selected="selectedMeta.node_type === 'outcome'">Outcome</option>
</select>
</div>
<div class="mb-2" t-if="selectedMeta.node_type === 'outcome'">
<label class="form-label small mb-1">Outcome Kind</label>
<select class="form-select form-select-sm"
t-on-change="(ev) => this.onEditorFieldChange('outcome_kind', ev.target.value)">
<option value="resolved" t-att-selected="selectedMeta.outcome_kind === 'resolved'">Resolved on call</option>
<option value="escalate" t-att-selected="selectedMeta.outcome_kind === 'escalate'">Escalate to tech</option>
<option value="order_part" t-att-selected="selectedMeta.outcome_kind === 'order_part'">Order part</option>
</select>
</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">
Media (<t t-out="selectedMeta.media.length"/>)
</label>
<input type="file" class="form-control form-control-sm"
multiple="multiple"
accept="image/*,video/*"
t-on-change="onUploadMedia"/>
<div t-if="selectedMeta.media.length" class="fr-media-thumbs mt-2">
<t t-foreach="selectedMeta.media" t-as="m" t-key="m.id">
<a t-att-href="m.url" target="_blank" class="me-1">
<img t-att-src="m.url" class="fr-media-thumb" t-att-alt="m.name"/>
</a>
</t>
</div>
</div>
<div class="mb-2">
<button class="btn btn-sm btn-outline-warning"
t-on-click="onSetStart"
t-att-disabled="selectedMeta.is_start">
<i class="fa fa-flag"/>
<t t-if="selectedMeta.is_start">Start Node</t>
<t t-else="">Make Start Node</t>
</button>
</div>
<hr/>
<div t-if="outgoingEdgesForSelected.length">
<h6 class="small">Outgoing Edges</h6>
<t t-foreach="outgoingEdgesForSelected" t-as="e" t-key="e.key">
<div class="mb-1">
<label class="form-label small mb-0">to <em t-out="e.target_name"/></label>
<input class="form-control form-control-sm"
placeholder="Label (Yes / No / ...)"
t-att-value="e.label"
t-on-change="(ev) => this.onEdgeLabelChange(ev, e.key)"/>
</div>
</t>
</div>
</t>
</div>
</div>
</div>
</t>
</templates>