changes
This commit is contained in:
@@ -136,9 +136,16 @@ export class RecipeTreeEditor extends Component {
|
||||
if (result && result.ok) {
|
||||
this.state.recipe = result.recipe;
|
||||
this.state.tree = result.tree;
|
||||
// Auto-expand root node
|
||||
if (result.tree) {
|
||||
this.state.expandedNodes[result.tree.id] = true;
|
||||
// Auto-expand every node on first load so the full
|
||||
// hierarchy is visible. The horizontal bracket layout
|
||||
// works best when everything is open by default;
|
||||
// operators can still collapse individual branches.
|
||||
if (result.tree && Object.keys(this.state.expandedNodes).length === 0) {
|
||||
const expandAll = (n) => {
|
||||
this.state.expandedNodes[n.id] = true;
|
||||
for (const c of (n.children || [])) expandAll(c);
|
||||
};
|
||||
expandAll(result.tree);
|
||||
}
|
||||
// Refresh selected node data if panel is open
|
||||
if (this.state.selectedNodeId) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,41 +3,177 @@
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Recipe Tree Editor — horizontal hierarchical layout (Steelhead-style).
|
||||
Recursive template renders recipe → sub-process → operation → step
|
||||
cards left→right with bracket connectors. Each card carries hover-
|
||||
revealed Add / Delete buttons; a side panel slides in for editing
|
||||
when a node is clicked.
|
||||
-->
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- ====================================================================
|
||||
RECURSIVE NODE TEMPLATE
|
||||
Expects: node, parentNode (or null), isFirst (bool)
|
||||
==================================================================== -->
|
||||
<t t-name="fusion_plating.RecipeTreeNode">
|
||||
<div class="o_fp_re_node">
|
||||
|
||||
<!-- Card -->
|
||||
<div t-att-class="'o_fp_re_card o_fp_re_type_' + node.node_type
|
||||
+ (state.selectedNodeId === node.id ? ' o_fp_re_selected' : '')"
|
||||
t-att-draggable="node.node_type !== 'recipe' ? 'true' : 'false'"
|
||||
t-on-dragstart="(ev) => this.onNodeDragStart(node, parentNode, ev)"
|
||||
t-on-dragend="(ev) => this.onNodeDragEnd(ev)"
|
||||
t-on-dragover="(ev) => this.onNodeDragOver(node, ev)"
|
||||
t-on-dragleave="(ev) => this.onNodeDragLeave(ev)"
|
||||
t-on-drop="(ev) => this.onNodeDrop(node, parentNode, ev)"
|
||||
t-on-click.stop="() => this.selectNode(node)">
|
||||
|
||||
<!-- Drag handle (left grip) -->
|
||||
<i class="o_fp_re_handle fa fa-bars"
|
||||
t-if="node.node_type !== 'recipe'"/>
|
||||
|
||||
<!-- Icon + name -->
|
||||
<i t-attf-class="o_fp_re_icon fa #{ node.icon || 'fa-cog' }"/>
|
||||
<div class="o_fp_re_card_body">
|
||||
<div class="o_fp_re_title" t-esc="node.name"/>
|
||||
<div class="o_fp_re_meta"
|
||||
t-if="node.work_center or node.estimated_duration or node.input_count">
|
||||
<span t-if="node.work_center">
|
||||
<i class="fa fa-building me-1"/><t t-esc="node.work_center"/>
|
||||
</span>
|
||||
<span t-if="node.estimated_duration">
|
||||
· <i class="fa fa-clock-o me-1"/><t t-esc="formatDuration(node.estimated_duration)"/>
|
||||
</span>
|
||||
<span t-if="node.input_count">
|
||||
· <i class="fa fa-keyboard-o me-1"/><t t-esc="node.input_count"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: type pill + capability icons + actions -->
|
||||
<div class="o_fp_re_right">
|
||||
<!-- Capability flags as small icons -->
|
||||
<span class="o_fp_re_flags">
|
||||
<i class="fa fa-check-square" t-if="node.requires_signoff" title="Requires sign-off"/>
|
||||
<i class="fa fa-eye" t-if="node.customer_visible" title="Customer visible"/>
|
||||
<i class="fa fa-magic" t-if="node.auto_complete" title="Auto-complete"/>
|
||||
<i class="fa fa-bolt" t-if="!node.is_manual" title="Automated"/>
|
||||
</span>
|
||||
<!-- Type pill -->
|
||||
<span t-attf-class="o_fp_re_type_pill o_fp_re_type_pill_#{ node.node_type }"
|
||||
t-esc="getNodeTypeMeta(node.node_type).label"/>
|
||||
<!-- Hover-revealed actions -->
|
||||
<span class="o_fp_re_actions">
|
||||
<button class="o_fp_re_btn o_fp_re_btn_add"
|
||||
t-on-click.stop="() => this.startAddChild(node.id)"
|
||||
title="Add child step">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
<button class="o_fp_re_btn o_fp_re_btn_del"
|
||||
t-if="node.node_type !== 'recipe'"
|
||||
t-on-click.stop="() => this.deleteNode(node.id)"
|
||||
title="Delete">
|
||||
<i class="fa fa-trash"/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Children + inline-add form -->
|
||||
<div class="o_fp_re_children"
|
||||
t-if="(node.children and node.children.length and isExpanded(node.id)) or state.addingTo === node.id">
|
||||
<t t-foreach="node.children || []" t-as="child" t-key="child.id">
|
||||
<t t-call="fusion_plating.RecipeTreeNode">
|
||||
<t t-set="node" t-value="child"/>
|
||||
<t t-set="parentNode" t-value="node"/>
|
||||
<t t-set="isFirst" t-value="false"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Inline add form (sits as the last child of `node`) -->
|
||||
<div class="o_fp_re_node o_fp_re_add_form"
|
||||
t-if="state.addingTo === node.id">
|
||||
<div class="o_fp_re_card o_fp_re_card_add">
|
||||
<i class="o_fp_re_icon fa fa-plus-circle"/>
|
||||
<div class="o_fp_re_card_body">
|
||||
<input type="text" class="o_fp_re_add_input"
|
||||
placeholder="New step name..."
|
||||
autofocus="autofocus"
|
||||
t-att-value="state.newNodeName"
|
||||
t-on-input="(ev) => { state.newNodeName = ev.target.value; }"
|
||||
t-on-keydown="onAddNameKey"
|
||||
t-on-click.stop=""/>
|
||||
<div class="o_fp_re_add_row">
|
||||
<select class="o_fp_re_add_select"
|
||||
t-on-change="(ev) => { state.newNodeType = ev.target.value; }"
|
||||
t-on-click.stop="">
|
||||
<t t-foreach="getNodeTypeOptions()" t-as="opt" t-key="opt.value">
|
||||
<option t-att-value="opt.value"
|
||||
t-att-selected="state.newNodeType === opt.value"
|
||||
t-esc="opt.label"/>
|
||||
</t>
|
||||
</select>
|
||||
<button class="o_fp_re_btn o_fp_re_btn_confirm"
|
||||
t-on-click.stop="confirmAdd">
|
||||
<i class="fa fa-check"/>
|
||||
</button>
|
||||
<button class="o_fp_re_btn o_fp_re_btn_cancel"
|
||||
t-on-click.stop="cancelAdd">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsed indicator: small chip showing # of hidden children -->
|
||||
<div class="o_fp_re_collapsed_chip"
|
||||
t-if="node.children and node.children.length and !isExpanded(node.id)"
|
||||
t-on-click.stop="() => this.toggleExpand(node.id)">
|
||||
<i class="fa fa-plus-square-o me-1"/>
|
||||
<t t-esc="node.children.length"/> hidden
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
<!-- ====================================================================
|
||||
ROOT TEMPLATE
|
||||
==================================================================== -->
|
||||
<t t-name="fusion_plating.RecipeTreeEditor">
|
||||
<div class="o_fp_recipe_editor">
|
||||
<div class="o_fp_recipe_editor o_fp_re_v2">
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<div class="o_fp_recipe_header">
|
||||
<div class="o_fp_recipe_header_left">
|
||||
<button class="btn btn-link o_fp_recipe_back_btn"
|
||||
t-on-click="onBackToList" title="Back to list">
|
||||
<i class="fa fa-arrow-left me-1"/> Recipes
|
||||
</button>
|
||||
<h2 class="o_fp_recipe_title" t-if="state.recipe">
|
||||
<i class="fa fa-flask me-2"/>
|
||||
<t t-esc="state.recipe.name"/>
|
||||
<span class="badge rounded-pill o_fp_recipe_version_badge ms-2"
|
||||
t-if="state.recipe.version">
|
||||
<div class="o_fp_re_header">
|
||||
<button class="o_fp_re_back"
|
||||
t-on-click="onBackToList"
|
||||
title="Back to recipes">
|
||||
<i class="fa fa-arrow-left me-2"/>Recipes
|
||||
</button>
|
||||
<div class="o_fp_re_header_title" t-if="state.recipe">
|
||||
<h2 class="o_fp_re_h2 mb-0">
|
||||
<i class="fa fa-flask me-2"/><t t-esc="state.recipe.name"/>
|
||||
<span t-if="state.recipe.version" class="o_fp_re_ver">
|
||||
v<t t-esc="state.recipe.version"/>
|
||||
</span>
|
||||
</h2>
|
||||
<div class="o_fp_re_subtitle" t-if="state.recipe.process_type">
|
||||
<i class="fa fa-tag me-1"/><t t-esc="state.recipe.process_type"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fp_recipe_header_right" t-if="state.recipe">
|
||||
<span class="text-muted small me-3" t-if="state.recipe.process_type">
|
||||
<i class="fa fa-tag me-1"/>
|
||||
<t t-esc="state.recipe.process_type"/>
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||
t-on-click="onDuplicate" title="Duplicate recipe">
|
||||
<i class="fa fa-copy me-1"/> Duplicate
|
||||
<div class="o_fp_re_header_actions" t-if="state.recipe">
|
||||
<button class="o_fp_re_btn_outline"
|
||||
t-on-click="onDuplicate"
|
||||
title="Duplicate recipe">
|
||||
<i class="fa fa-copy me-1"/>Duplicate
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
<button class="o_fp_re_btn_outline"
|
||||
t-on-click="() => this.onOpenForm(state.recipe.id)"
|
||||
title="Edit in form view">
|
||||
<i class="fa fa-pencil me-1"/> Form View
|
||||
<i class="fa fa-pencil me-1"/>Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,17 +184,17 @@
|
||||
<p class="mt-2 text-muted">Loading recipe tree...</p>
|
||||
</div>
|
||||
|
||||
<!-- ========== NO RECIPE ========== -->
|
||||
<div class="text-center py-5" t-if="!state.loading and !_recipeId">
|
||||
<i class="fa fa-exclamation-triangle fa-3x text-muted"/>
|
||||
<p class="mt-3 text-muted">No recipe selected.</p>
|
||||
<!-- ========== EMPTY ========== -->
|
||||
<div class="o_fp_re_empty" t-if="!state.loading and !_recipeId">
|
||||
<i class="fa fa-exclamation-triangle"/>
|
||||
<div>No recipe selected.</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== TREE + PANEL LAYOUT ========== -->
|
||||
<div class="o_fp_recipe_body" t-if="state.tree">
|
||||
<!-- ========== BODY (canvas + side panel) ========== -->
|
||||
<div class="o_fp_re_body" t-if="state.tree">
|
||||
|
||||
<!-- Tree area -->
|
||||
<div class="o_fp_recipe_tree_area">
|
||||
<!-- Tree canvas -->
|
||||
<div class="o_fp_re_canvas">
|
||||
<t t-call="fusion_plating.RecipeTreeNode">
|
||||
<t t-set="node" t-value="state.tree"/>
|
||||
<t t-set="parentNode" t-value="null"/>
|
||||
@@ -67,26 +203,29 @@
|
||||
</div>
|
||||
|
||||
<!-- Side panel -->
|
||||
<div t-att-class="'o_fp_recipe_panel' + (state.showPanel ? ' o_fp_recipe_panel_open' : '')">
|
||||
<div t-att-class="'o_fp_re_panel' + (state.showPanel ? ' o_fp_re_panel_open' : '')">
|
||||
<t t-if="state.showPanel and state.selectedNode">
|
||||
<div class="o_fp_recipe_panel_header">
|
||||
<div class="o_fp_re_panel_head">
|
||||
<h5>
|
||||
<i t-att-class="'fa ' + (state.selectedNode.icon || 'fa-cog') + ' me-2'"/>
|
||||
<i t-attf-class="fa #{ state.selectedNode.icon || 'fa-cog' } me-2"/>
|
||||
Edit Node
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-link" t-on-click="closePanel">
|
||||
<button class="o_fp_re_btn o_fp_re_btn_cancel"
|
||||
t-on-click="closePanel">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_fp_recipe_panel_body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Name</label>
|
||||
<div class="o_fp_re_panel_body">
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Name</label>
|
||||
<input type="text" class="form-control"
|
||||
t-att-value="state.selectedNode.name"
|
||||
t-on-change="(ev) => { state.selectedNode.name = ev.target.value; }"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Type</label>
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Type</label>
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => { state.selectedNode.node_type = ev.target.value; }">
|
||||
<option value="recipe"
|
||||
@@ -99,53 +238,57 @@
|
||||
t-att-selected="state.selectedNode.node_type === 'step'">Step</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Icon</label>
|
||||
<div class="o_fp_recipe_icon_picker">
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Icon</label>
|
||||
<div class="o_fp_re_icon_picker">
|
||||
<t t-foreach="getIconOptions()" t-as="ic" t-key="ic.value">
|
||||
<button t-att-class="'o_fp_recipe_icon_btn' + (state.selectedNode.icon === ic.value ? ' active' : '')"
|
||||
<button t-att-class="'o_fp_re_icon_btn' + (state.selectedNode.icon === ic.value ? ' active' : '')"
|
||||
t-on-click.stop="() => { state.selectedNode.icon = ic.value; }"
|
||||
t-att-title="ic.label">
|
||||
<i t-att-class="'fa ' + ic.value"/>
|
||||
<i t-attf-class="fa #{ ic.value }"/>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Duration (min)</label>
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Estimated Duration (min)</label>
|
||||
<input type="number" class="form-control" min="0" step="1"
|
||||
t-att-value="state.selectedNode.estimated_duration || 0"
|
||||
t-on-change="(ev) => { state.selectedNode.estimated_duration = parseFloat(ev.target.value) || 0; }"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold d-block">Flags</label>
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Flags</label>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_manual"
|
||||
<input type="checkbox" class="form-check-input" id="fp_re_chk_manual"
|
||||
t-att-checked="state.selectedNode.is_manual"
|
||||
t-on-change="(ev) => { state.selectedNode.is_manual = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_manual">Manual operation</label>
|
||||
<label class="form-check-label" for="fp_re_chk_manual">Manual operation</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_auto"
|
||||
<input type="checkbox" class="form-check-input" id="fp_re_chk_auto"
|
||||
t-att-checked="state.selectedNode.auto_complete"
|
||||
t-on-change="(ev) => { state.selectedNode.auto_complete = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_auto">Auto-complete</label>
|
||||
<label class="form-check-label" for="fp_re_chk_auto">Auto-complete</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_signoff"
|
||||
<input type="checkbox" class="form-check-input" id="fp_re_chk_signoff"
|
||||
t-att-checked="state.selectedNode.requires_signoff"
|
||||
t-on-change="(ev) => { state.selectedNode.requires_signoff = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_signoff">Requires sign-off</label>
|
||||
<label class="form-check-label" for="fp_re_chk_signoff">Requires sign-off</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_visible"
|
||||
<input type="checkbox" class="form-check-input" id="fp_re_chk_visible"
|
||||
t-att-checked="state.selectedNode.customer_visible"
|
||||
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_visible">Customer visible</label>
|
||||
<label class="form-check-label" for="fp_re_chk_visible">Customer visible</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Opt In/Out</label>
|
||||
|
||||
<div class="o_fp_re_field">
|
||||
<label>Opt In/Out</label>
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => { state.selectedNode.opt_in_out = ev.target.value; }">
|
||||
<option value="disabled"
|
||||
@@ -156,30 +299,16 @@
|
||||
t-att-selected="state.selectedNode.opt_in_out === 'opt_out'">Opt-Out</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Info -->
|
||||
<div class="text-muted small mb-2" t-if="state.selectedNode.work_center">
|
||||
<i class="fa fa-building me-1"/>
|
||||
<t t-esc="state.selectedNode.work_center"/>
|
||||
</div>
|
||||
<div class="text-muted small mb-2" t-if="state.selectedNode.process_type">
|
||||
<i class="fa fa-tag me-1"/>
|
||||
<t t-esc="state.selectedNode.process_type"/>
|
||||
</div>
|
||||
<div class="text-muted small mb-2"
|
||||
t-if="state.selectedNode.input_count">
|
||||
<i class="fa fa-keyboard-o me-1"/>
|
||||
<t t-esc="state.selectedNode.input_count"/> operator input(s)
|
||||
</div>
|
||||
<!-- Tracking -->
|
||||
<div class="o_fp_recipe_tracking mt-3 pt-3" t-if="state.selectedNode.create_date">
|
||||
<div class="text-muted small mb-1">
|
||||
|
||||
<div class="o_fp_re_tracking" t-if="state.selectedNode.create_date">
|
||||
<div>
|
||||
<i class="fa fa-calendar-plus-o me-1"/>
|
||||
Created <t t-esc="formatTimeAgo(state.selectedNode.create_date)"/>
|
||||
<t t-if="state.selectedNode.create_uid_name">
|
||||
by <strong t-esc="state.selectedNode.create_uid_name"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="text-muted small" t-if="state.selectedNode.write_date">
|
||||
<div t-if="state.selectedNode.write_date">
|
||||
<i class="fa fa-pencil me-1"/>
|
||||
Updated <t t-esc="formatTimeAgo(state.selectedNode.write_date)"/>
|
||||
<t t-if="state.selectedNode.write_uid_name">
|
||||
@@ -187,15 +316,15 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button class="btn btn-primary flex-fill"
|
||||
|
||||
<div class="o_fp_re_panel_actions">
|
||||
<button class="o_fp_re_btn_save"
|
||||
t-on-click="saveNode"
|
||||
t-att-disabled="state.saving">
|
||||
<i t-att-class="state.saving ? 'fa fa-spinner fa-spin me-1' : 'fa fa-check me-1'"/>
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary"
|
||||
<button class="o_fp_re_btn_outline"
|
||||
t-on-click="() => this.onOpenForm(state.selectedNode.id)"
|
||||
title="Open full form">
|
||||
<i class="fa fa-external-link"/>
|
||||
@@ -208,127 +337,4 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- ========== RECURSIVE NODE TEMPLATE ========== -->
|
||||
<t t-name="fusion_plating.RecipeTreeNode">
|
||||
<!-- Connector line (skip for root) -->
|
||||
<div class="o_fp_recipe_connector" t-if="!isFirst"/>
|
||||
|
||||
<!-- Node card -->
|
||||
<div t-att-class="'o_fp_recipe_node'
|
||||
+ (state.selectedNodeId === node.id ? ' o_fp_recipe_node_selected' : '')
|
||||
+ ' o_fp_recipe_node_' + node.node_type"
|
||||
t-att-draggable="node.node_type !== 'recipe' ? 'true' : 'false'"
|
||||
t-on-dragstart="(ev) => this.onNodeDragStart(node, parentNode, ev)"
|
||||
t-on-dragend="(ev) => this.onNodeDragEnd(ev)"
|
||||
t-on-dragover="(ev) => this.onNodeDragOver(node, ev)"
|
||||
t-on-dragleave="(ev) => this.onNodeDragLeave(ev)"
|
||||
t-on-drop="(ev) => this.onNodeDrop(node, parentNode, ev)"
|
||||
t-on-click.stop="() => this.selectNode(node)">
|
||||
|
||||
<!-- Drag handle (non-root only) -->
|
||||
<span class="o_fp_recipe_drag_handle" t-if="node.node_type !== 'recipe'">
|
||||
<i class="fa fa-grip-vertical"/>
|
||||
</span>
|
||||
|
||||
<!-- Node header row -->
|
||||
<div class="o_fp_recipe_node_header">
|
||||
<!-- Expand/collapse toggle -->
|
||||
<button class="o_fp_recipe_toggle_btn"
|
||||
t-if="node.children and node.children.length"
|
||||
t-on-click.stop="() => this.toggleExpand(node.id)">
|
||||
<i t-att-class="isExpanded(node.id) ? 'fa fa-chevron-down' : 'fa fa-chevron-right'"/>
|
||||
</button>
|
||||
<span class="o_fp_recipe_toggle_spacer" t-else=""/>
|
||||
|
||||
<!-- Icon -->
|
||||
<i t-att-class="'o_fp_recipe_node_icon fa ' + (node.icon || 'fa-cog')"/>
|
||||
|
||||
<!-- Name -->
|
||||
<span class="o_fp_recipe_node_name">
|
||||
<t t-esc="node.name"/>
|
||||
</span>
|
||||
|
||||
<!-- Type badge -->
|
||||
<span t-att-class="'badge o_fp_recipe_node_badge ' + getNodeTypeMeta(node.node_type).badgeClass">
|
||||
<t t-esc="getNodeTypeMeta(node.node_type).label"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Meta row: work centre, duration, capability icons -->
|
||||
<div class="o_fp_recipe_node_meta">
|
||||
<span class="o_fp_recipe_node_wc" t-if="node.work_center">
|
||||
<i class="fa fa-building me-1"/>
|
||||
<t t-esc="node.work_center"/>
|
||||
</span>
|
||||
<span class="o_fp_recipe_node_duration" t-if="node.estimated_duration">
|
||||
<i class="fa fa-clock-o me-1"/>
|
||||
<t t-esc="formatDuration(node.estimated_duration)"/>
|
||||
</span>
|
||||
<!-- Capability icons -->
|
||||
<span class="o_fp_recipe_node_icons">
|
||||
<i class="fa fa-hand-paper-o" t-if="node.is_manual" title="Manual"/>
|
||||
<i class="fa fa-bolt" t-if="!node.is_manual" title="Automated"/>
|
||||
<i class="fa fa-check-square" t-if="node.requires_signoff" title="Requires sign-off"/>
|
||||
<i class="fa fa-eye" t-if="node.customer_visible" title="Customer visible"/>
|
||||
<i class="fa fa-magic" t-if="node.auto_complete" title="Auto-complete"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons row -->
|
||||
<div class="o_fp_recipe_node_actions">
|
||||
<button class="btn btn-sm o_fp_recipe_add_btn"
|
||||
t-on-click.stop="() => this.startAddChild(node.id)"
|
||||
title="Add child step">
|
||||
<i class="fa fa-plus me-1"/> Add Step
|
||||
</button>
|
||||
<button class="btn btn-sm o_fp_recipe_delete_btn"
|
||||
t-if="node.node_type !== 'recipe'"
|
||||
t-on-click.stop="() => this.deleteNode(node.id)"
|
||||
title="Delete">
|
||||
<i class="fa fa-trash"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add child inline form -->
|
||||
<div class="o_fp_recipe_add_form" t-if="state.addingTo === node.id">
|
||||
<div class="o_fp_recipe_connector"/>
|
||||
<div class="o_fp_recipe_add_card">
|
||||
<input type="text" class="form-control form-control-sm mb-2"
|
||||
placeholder="New step name..."
|
||||
t-att-value="state.newNodeName"
|
||||
t-on-input="(ev) => { state.newNodeName = ev.target.value; }"
|
||||
t-on-keydown="onAddNameKey"/>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm flex-shrink-1"
|
||||
style="max-width: 140px;"
|
||||
t-on-change="(ev) => { state.newNodeType = ev.target.value; }">
|
||||
<t t-foreach="getNodeTypeOptions()" t-as="opt" t-key="opt.value">
|
||||
<option t-att-value="opt.value"
|
||||
t-att-selected="state.newNodeType === opt.value"
|
||||
t-esc="opt.label"/>
|
||||
</t>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-primary" t-on-click="confirmAdd">
|
||||
<i class="fa fa-check"/>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" t-on-click="cancelAdd">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Children (recursive) -->
|
||||
<div class="o_fp_recipe_children" t-if="node.children and node.children.length and isExpanded(node.id)">
|
||||
<t t-foreach="node.children" t-as="child" t-key="child.id">
|
||||
<t t-call="fusion_plating.RecipeTreeNode">
|
||||
<t t-set="node" t-value="child"/>
|
||||
<t t-set="parentNode" t-value="node"/>
|
||||
<t t-set="isFirst" t-value="false"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
Reference in New Issue
Block a user