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>
153 lines
9.5 KiB
XML
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>
|