From 4213c44e511a4f69ae7bac4a2cdb2784598c6984 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 30 Apr 2026 16:16:14 -0400 Subject: [PATCH] feat(simple-editor): node_type bug fix + inline library authoring + back nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bucket 1 — Generation bug fix - post-migrate.py for 19.0.18.8.0 promotes flat 'step' children of recipes to 'operation' so fp.job._generate_steps() picks them up. Filter is narrow: only direct children of node_type='recipe' get flipped, tree-editor sub-steps (parent.node_type='operation') are untouched. Idempotent. Posts an audit chatter note on each affected recipe. - Simple Editor controller hardcodes node_type='operation' on insert + snapshot-import path so future recipes start correct. Bucket 2 — Inline library authoring - 6 new JSONRPC routes (/fp/simple_recipe/library/load + save + seed_defaults + input/{add,write,remove}, /fp/simple_recipe/tank/list). - + New Step button in the right pane opens an inline form with name / kind / icon / instructions / stations / flags / prompts table. - Pencil icon on each library row reopens the same form prefilled. - Step Kind picker leads with 'Generic — no automatic behaviour'. - 'Seed defaults from kind' calls action_seed_default_inputs server-side for kinds that have curated default prompts. Bucket 3 — Back nav - '← Recipes' button in the header (or '← Part' when opened from Process Composer) mirrors recipe_tree_editor.js, with clearBreadcrumbs:true to avoid stack pollution. Verified on entech: LGPS1104's 19 'step' children now show as 'operation', migration chatter note posted on the recipe, asset cache busted. Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/fusion_plating/__manifest__.py | 2 +- .../controllers/simple_recipe_controller.py | 158 ++++++++- .../migrations/19.0.18.8.0/post-migrate.py | 104 ++++++ .../static/src/js/simple_recipe_editor.js | 235 ++++++++++++++ .../static/src/scss/simple_recipe_editor.scss | 171 ++++++++++ .../static/src/xml/simple_recipe_editor.xml | 307 ++++++++++++++++-- 6 files changed, 954 insertions(+), 23 deletions(-) create mode 100644 fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index 22b28990..08c84a66 100644 --- a/fusion_plating/fusion_plating/__manifest__.py +++ b/fusion_plating/fusion_plating/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating', - 'version': '19.0.18.7.4', + 'version': '19.0.18.8.0', 'category': 'Manufacturing/Plating', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'description': """ diff --git a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py index bac67319..51f8f192 100644 --- a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py @@ -167,6 +167,156 @@ class SimpleRecipeController(http.Controller): tpl.unlink() return {'ok': True, 'soft_deleted': False} + # ---- Inline authoring (Simple Editor right-pane "+ New Step" / pencil) ---- + # + # Endpoints below let a shop foreman create or edit a library template + # (with prompts) without leaving the Simple Editor. Manager-grade + # features (transition form, advanced time/temp targets, common-audit + # seeding) still live on the dedicated form view. + + @http.route('/fp/simple_recipe/library/load', type='jsonrpc', auth='user') + def library_load(self, template_id): + """Return the full payload for one library template.""" + tpl = request.env['fp.step.template'].browse(int(template_id)) + if not tpl.exists(): + return {'ok': False, 'error': 'not_found'} + tpl.check_access('read') + return {'ok': True, 'template': self._library_payload(tpl)} + + def _library_payload(self, tpl): + return { + 'id': tpl.id, + 'name': tpl.name or '', + 'code': tpl.code or '', + 'icon': tpl.icon or 'fa-cog', + 'default_kind': tpl.default_kind or '', + 'description': tpl.description or '', + 'requires_signoff': tpl.requires_signoff, + 'requires_predecessor_done': tpl.requires_predecessor_done, + 'requires_rack_assignment': tpl.requires_rack_assignment, + 'requires_transition_form': tpl.requires_transition_form, + 'tank_ids': [ + {'id': t.id, 'name': t.name, 'code': t.code or ''} + for t in tpl.tank_ids + ], + 'inputs': [ + { + 'id': i.id, + 'name': i.name or '', + 'input_type': i.input_type or 'text', + 'target_min': i.target_min or 0.0, + 'target_max': i.target_max or 0.0, + 'target_unit': i.target_unit or '', + 'required': bool(i.required), + 'sequence': i.sequence or 0, + 'hint': i.hint or '', + 'selection_options': i.selection_options or '', + } + for i in tpl.input_template_ids.sorted('sequence') + ], + } + + @http.route('/fp/simple_recipe/library/save', type='jsonrpc', auth='user') + def library_save(self, template_id, vals): + """Upsert: create when template_id is falsy, write otherwise. + Returns the full template payload so the OWL component can + refresh in one round-trip. + """ + Tpl = request.env['fp.step.template'] + # Whitelist — never trust client-provided write_uid / id / etc. + allowed = { + 'name', 'code', 'icon', 'default_kind', 'description', + 'requires_signoff', 'requires_predecessor_done', + 'requires_rack_assignment', 'requires_transition_form', + 'tank_ids', + } + clean = {k: v for k, v in (vals or {}).items() if k in allowed} + # tank_ids comes in as a plain list of ids from the OWL form; + # translate into the Odoo (6, 0, ids) command form. + if 'tank_ids' in clean: + clean['tank_ids'] = [(6, 0, [int(x) for x in clean['tank_ids']])] + if template_id: + tpl = Tpl.browse(int(template_id)) + tpl.check_access('write') + tpl.write(clean) + else: + tpl = Tpl.create(clean) + return {'ok': True, 'template': self._library_payload(tpl)} + + @http.route('/fp/simple_recipe/library/seed_defaults', type='jsonrpc', auth='user') + def library_seed_defaults(self, template_id): + """Run action_seed_default_inputs on this template. Idempotent — + only adds prompts whose name doesn't already exist. + """ + tpl = request.env['fp.step.template'].browse(int(template_id)) + if not tpl.exists(): + return {'ok': False, 'error': 'not_found'} + tpl.check_access('write') + if not tpl.default_kind: + return {'ok': False, 'error': 'no_kind', + 'message': 'Pick a Step Kind first to seed defaults.'} + tpl.action_seed_default_inputs() + return {'ok': True, 'template': self._library_payload(tpl)} + + @http.route('/fp/simple_recipe/library/input/add', type='jsonrpc', auth='user') + def library_input_add(self, template_id, payload): + tpl = request.env['fp.step.template'].browse(int(template_id)) + tpl.check_access('write') + Input = request.env['fp.step.template.input'] + existing_max = max(tpl.input_template_ids.mapped('sequence') or [0]) + rec = Input.create({ + 'template_id': tpl.id, + 'name': (payload or {}).get('name') or 'New Prompt', + 'input_type': (payload or {}).get('input_type') or 'text', + 'sequence': existing_max + 10, + 'required': bool((payload or {}).get('required')), + }) + return {'ok': True, 'input_id': rec.id, + 'template': self._library_payload(tpl)} + + @http.route('/fp/simple_recipe/library/input/write', type='jsonrpc', auth='user') + def library_input_write(self, input_id, payload): + Input = request.env['fp.step.template.input'] + rec = Input.browse(int(input_id)) + if not rec.exists(): + return {'ok': False, 'error': 'not_found'} + rec.template_id.check_access('write') + allowed = { + 'name', 'input_type', 'target_min', 'target_max', 'target_unit', + 'required', 'sequence', 'selection_options', 'hint', + } + clean = {k: v for k, v in (payload or {}).items() if k in allowed} + if clean: + rec.write(clean) + return {'ok': True, 'template': self._library_payload(rec.template_id)} + + @http.route('/fp/simple_recipe/library/input/remove', type='jsonrpc', auth='user') + def library_input_remove(self, input_id): + Input = request.env['fp.step.template.input'] + rec = Input.browse(int(input_id)) + if not rec.exists(): + return {'ok': False, 'error': 'not_found'} + tpl = rec.template_id + tpl.check_access('write') + rec.unlink() + return {'ok': True, 'template': self._library_payload(tpl)} + + @http.route('/fp/simple_recipe/tank/list', type='jsonrpc', auth='user') + def tank_list(self, query=''): + """Tank picker for the inline library form. Returns active tanks + scoped to the current company. + """ + Tank = request.env['fusion.plating.tank'] + domain = [('active', '=', True)] + if query: + domain += ['|', ('name', 'ilike', query), ('code', 'ilike', query)] + return { + 'tanks': [ + {'id': t.id, 'name': t.name, 'code': t.code or ''} + for t in Tank.search(domain, limit=50) + ], + } + # ------------------------------------------------------------------ step @http.route('/fp/simple_recipe/step/insert', type='jsonrpc', auth='user') def step_insert(self, recipe_id, template_id=False, position=99, vals=None): @@ -175,7 +325,10 @@ class SimpleRecipeController(http.Controller): new_vals = { 'parent_id': recipe.id, - 'node_type': 'step', + # Must be 'operation' — fp.job._generate_steps() only creates + # fp.job.step rows for operation nodes. Flat 'step' children + # of a recipe were silently skipped pre-19.0.18.8.0. + 'node_type': 'operation', 'sequence': target_seq, } tpl = False @@ -291,7 +444,8 @@ class SimpleRecipeController(http.Controller): Node = request.env['fusion.plating.process.node'] new_vals = { 'parent_id': target_recipe.id, - 'node_type': 'step', + # See _SNAPSHOT_FIELDS comment — operation, not step. + 'node_type': 'operation', 'sequence': src_node.sequence, 'source_template_id': src_node.source_template_id.id or False, } diff --git a/fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py new file mode 100644 index 00000000..b035a9c4 --- /dev/null +++ b/fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Post-migration for 19.0.18.8.0 — Simple Editor node_type fix. + +Background +---------- +The Simple Recipe Editor's controller used to insert library templates +into a recipe with `node_type='step'` directly under the recipe root. +But fp.job._generate_steps() (in fusion_plating_jobs/models/fp_job.py) +only creates fp.job.step rows for nodes whose node_type is 'operation'. +Top-level 'step' children of a recipe were silently skipped, meaning +Simple-Editor recipes generated zero job steps — no traveller content, +no shopfloor tablet entries, no CoC moves. + +Fix +--- +Promote every `step` node whose direct parent is a `recipe` to +`node_type='operation'`. Tree-editor authored 'step' nodes (which sit +under `operation` parents) are left untouched — the filter is on +`parent.node_type='recipe'`. + +Idempotent: a second run finds no `step` children of recipes and is +a no-op. Posts a chatter note on each affected recipe so QA / clients +have a paper trail. +""" + +import logging + +_logger = logging.getLogger(__name__) + + +_AUDIT_BODY = ( + '

Recipe migrated to v19.0.18.8.0 step layout.

' + '

Step nodes that were direct children of this recipe (Simple ' + 'Editor authoring) have been promoted to operation nodes so they ' + 'generate work-order steps correctly. No data was lost — only ' + 'node_type changed.

' + '

If this recipe was authored via the Tree Editor with explicit ' + 'sub-process / operation hierarchy, this migration was a no-op ' + 'for it.

' +) + + +def migrate(cr, version): + if not version: + return + from odoo.api import Environment, SUPERUSER_ID + env = Environment(cr, SUPERUSER_ID, {}) + + # Find recipe ids whose direct children include any 'step' rows. + # We need this list BEFORE we flip the rows so we can post chatter + # afterwards. + cr.execute(""" + SELECT DISTINCT parent.id + FROM fusion_plating_process_node parent + JOIN fusion_plating_process_node child + ON child.parent_id = parent.id + WHERE parent.node_type = 'recipe' + AND child.node_type = 'step' + """) + affected_recipe_ids = [r[0] for r in cr.fetchall()] + + if not affected_recipe_ids: + _logger.info( + "Sub Simple-Editor migration: no flat-step recipes found, " + "nothing to do." + ) + return + + # Flip the node_type. Filter is intentionally narrow — only direct + # children of a recipe get promoted. Tree-editor sub-step rows + # (parent.node_type='operation') are untouched. + cr.execute(""" + UPDATE fusion_plating_process_node child + SET node_type = 'operation' + WHERE child.node_type = 'step' + AND child.parent_id IN ( + SELECT id + FROM fusion_plating_process_node + WHERE node_type = 'recipe' + ) + """) + flipped = cr.rowcount + _logger.info( + "Sub Simple-Editor migration: promoted %s step nodes to " + "operation across %s recipes.", + flipped, len(affected_recipe_ids), + ) + + # Post a chatter note on each affected recipe (best-effort). + Node = env['fusion.plating.process.node'] + for recipe in Node.browse(affected_recipe_ids): + try: + recipe.message_post( + body=_AUDIT_BODY, + message_type='notification', + subtype_xmlid='mail.mt_note', + ) + except Exception as e: + _logger.warning( + "Failed to post audit note on recipe %s: %s", + recipe.id, e, + ) diff --git a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js index a3c175a1..49bac759 100644 --- a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js +++ b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js @@ -44,13 +44,25 @@ export class FpSimpleRecipeEditor extends Component { editingStepId: null, editName: "", editInstructions: "", + // Inline library form — open when authoring or editing a + // library template directly from the right pane. null = + // closed; otherwise carries the template payload. + libraryEditor: null, + libraryEditorBusy: false, + tankSearchResults: [], }); this._recipeId = null; + this._partId = null; onMounted(async () => { const ctx = this.props.action?.context || {}; this._recipeId = ctx.recipe_id || null; + this._partId = ctx.part_id || null; + // Mirror onto state so the OWL template can branch on it + // (template scope sees state directly, not arbitrary + // instance properties). + this.state.fromPart = !!this._partId; if (this._recipeId) { await this.loadAll(); } else { @@ -173,6 +185,229 @@ export class FpSimpleRecipeEditor extends Component { }); } + // ----------------------------------------------------- back navigation + /** + * Mirror of recipe_tree_editor.onBackToList. When the editor was + * opened from the part-scoped Process Composer, return to that part + * form; otherwise drop the user back on the Recipes list. + * + * `clearBreadcrumbs: true` is critical — without it, every part → + * composer → editor → back leaves intermediate pages on the + * breadcrumb stack so a second visit shows nonsense. + */ + onBackToList() { + if (this._partId) { + this.action.doAction( + { + type: "ir.actions.act_window", + res_model: "fp.part.catalog", + res_id: this._partId, + views: [[false, "form"]], + target: "current", + }, + { clearBreadcrumbs: true } + ); + return; + } + this.action.doAction("fusion_plating.action_fp_process_recipe", { + clearBreadcrumbs: true, + }); + } + + // -------------------------------------------------- inline library form + /** + * Open the inline form for a NEW library template. Skeleton payload + * mirrors the shape returned by `/fp/simple_recipe/library/load` so + * the same template renders both create + edit. + */ + onOpenLibraryCreate() { + this.state.libraryEditor = { + id: null, // null = create + name: "", + code: "", + icon: "fa-cog", + default_kind: "", + description: "", + requires_signoff: false, + requires_predecessor_done: false, + requires_rack_assignment: false, + requires_transition_form: false, + tank_ids: [], + inputs: [], + }; + this.state.tankSearchResults = []; + } + + async onOpenLibraryEdit(templateId) { + this.state.libraryEditorBusy = true; + const data = await rpc("/fp/simple_recipe/library/load", { + template_id: templateId, + }); + if (data.ok) { + // Defensive copy — OWL useState wraps top-level fields, but + // we want to be able to mutate this.state.libraryEditor.* in + // place without triggering library list re-renders. + this.state.libraryEditor = JSON.parse(JSON.stringify(data.template)); + } else { + this.notification.add( + _t("Could not load library template — it may have been deleted."), + { type: "warning" } + ); + } + this.state.libraryEditorBusy = false; + } + + onCancelLibraryEditor() { + this.state.libraryEditor = null; + this.state.tankSearchResults = []; + } + + async onSaveLibraryEditor() { + const ed = this.state.libraryEditor; + if (!ed) return; + if (!(ed.name || "").trim()) { + this.notification.add(_t("Name is required."), { type: "warning" }); + return; + } + this.state.libraryEditorBusy = true; + const vals = { + name: ed.name, + code: ed.code, + icon: ed.icon, + default_kind: ed.default_kind || false, + description: ed.description, + requires_signoff: !!ed.requires_signoff, + requires_predecessor_done: !!ed.requires_predecessor_done, + requires_rack_assignment: !!ed.requires_rack_assignment, + requires_transition_form: !!ed.requires_transition_form, + tank_ids: (ed.tank_ids || []).map((t) => t.id), + }; + const result = await rpc("/fp/simple_recipe/library/save", { + template_id: ed.id || false, + vals: vals, + }); + if (result.ok) { + // Refresh in place so the Inputs section reflects DB state + // (e.g. id assigned to a freshly-created template). + this.state.libraryEditor = JSON.parse(JSON.stringify(result.template)); + // Refresh the library list in the right pane. + const libData = await rpc("/fp/simple_recipe/library/list", { + query: this.state.librarySearch || "", + }); + this.state.library = libData.templates; + this.notification.add( + ed.id ? _t("Library step updated") : _t("Library step created"), + { type: "success" } + ); + } else { + this.notification.add( + result.error || _t("Save failed"), + { type: "danger" } + ); + } + this.state.libraryEditorBusy = false; + } + + async onSeedLibraryDefaults() { + const ed = this.state.libraryEditor; + if (!ed || !ed.id) { + this.notification.add( + _t("Save the step first, then seed defaults."), + { type: "warning" } + ); + return; + } + if (!ed.default_kind) { + this.notification.add( + _t("Pick a Step Kind first to seed defaults."), + { type: "warning" } + ); + return; + } + const result = await rpc("/fp/simple_recipe/library/seed_defaults", { + template_id: ed.id, + }); + if (result.ok) { + this.state.libraryEditor = JSON.parse(JSON.stringify(result.template)); + this.notification.add(_t("Default prompts seeded"), { type: "success" }); + } else { + this.notification.add( + result.message || _t("Seed failed"), + { type: "warning" } + ); + } + } + + async onAddLibraryInput() { + const ed = this.state.libraryEditor; + if (!ed || !ed.id) { + this.notification.add( + _t("Save the step first, then add prompts."), + { type: "warning" } + ); + return; + } + const result = await rpc("/fp/simple_recipe/library/input/add", { + template_id: ed.id, + payload: { name: _t("New Prompt"), input_type: "text" }, + }); + if (result.ok) { + this.state.libraryEditor = JSON.parse(JSON.stringify(result.template)); + } + } + + async onLibraryInputBlur(inputId, field, ev) { + const value = ev.target.value; + if (field === "name" && !value.trim()) return; + await this._libraryInputWrite(inputId, field, value); + } + + async onLibraryInputChange(inputId, field, value) { + await this._libraryInputWrite(inputId, field, value); + } + + async _libraryInputWrite(inputId, field, value) { + const payload = {}; + payload[field] = value; + const result = await rpc("/fp/simple_recipe/library/input/write", { + input_id: inputId, + payload: payload, + }); + if (result.ok) { + this.state.libraryEditor = JSON.parse(JSON.stringify(result.template)); + } + } + + async onRemoveLibraryInput(inputId) { + const proceed = await this._confirm(_t("Remove this prompt?")); + if (!proceed) return; + const result = await rpc("/fp/simple_recipe/library/input/remove", { + input_id: inputId, + }); + if (result.ok) { + this.state.libraryEditor = JSON.parse(JSON.stringify(result.template)); + } + } + + async onSearchTanks(ev) { + const q = ev.target.value; + const data = await rpc("/fp/simple_recipe/tank/list", { query: q }); + this.state.tankSearchResults = data.tanks; + } + + onAddTank(tank) { + const ed = this.state.libraryEditor; + if (!ed) return; + if (ed.tank_ids.some((t) => t.id === tank.id)) return; + ed.tank_ids.push(tank); + } + + onRemoveTank(tankId) { + const ed = this.state.libraryEditor; + if (!ed) return; + ed.tank_ids = ed.tank_ids.filter((t) => t.id !== tankId); + } + // --------------------------------------------------------- drag & drop onSelectedDragStart(stepId, ev) { diff --git a/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss b/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss index 2dacdeff..c2bcc297 100644 --- a/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss +++ b/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss @@ -321,3 +321,174 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex}); text-align: center; color: $fp-se-muted; } + +// ---- Back button + library header (Bucket 3) ---- +.o_fp_se_back { + color: $fp-se-accent; + margin-right: 1rem; + padding: .25rem .5rem; + font-weight: 500; + + &:hover { + color: $fp-se-accent; + text-decoration: underline; + } +} + +.o_fp_library_header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: .5rem; + + h3 { margin: 0; } +} + +.o_fp_library_item { + .o_fp_library_edit { + background: transparent; + border: 0; + color: $fp-se-muted; + padding: .25rem .5rem; + cursor: pointer; + opacity: 0; + transition: opacity .15s; + + &:hover { color: $fp-se-accent; } + } + + &:hover .o_fp_library_edit { opacity: 1; } +} + +// ---- Inline library editor (Bucket 2) ---- +.o_fp_library_editor { + background: $fp-se-card; + border: 1px solid $fp-se-border; + border-radius: 6px; + padding: 1rem; + max-height: 80vh; + overflow: auto; + + &.o_fp_busy { opacity: .65; pointer-events: none; } +} + +.o_fp_library_editor_header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; + padding-bottom: .75rem; + border-bottom: 1px solid $fp-se-border; + + h3 { margin: 0; font-size: 1.1rem; } +} + +.o_fp_library_editor_body { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.o_fp_le_row { + display: flex; + gap: 1rem; + + .o_fp_le_field { flex: 1; } +} + +.o_fp_le_field { + display: flex; + flex-direction: column; + gap: .25rem; + + label { + font-size: .85rem; + font-weight: 500; + color: $fp-se-muted; + } +} + +.o_fp_le_tank_chips { + display: flex; + flex-wrap: wrap; + gap: .35rem; + margin-bottom: .25rem; +} + +.o_fp_le_tank_chip { + display: inline-flex; + align-items: center; + gap: .35rem; + padding: .15rem .5rem; + background: rgba(0, 100, 200, .1); + border-radius: 12px; + font-size: .85rem; + + .o_fp_le_tank_remove { + background: transparent; + border: 0; + cursor: pointer; + font-size: 1rem; + line-height: 1; + color: $fp-se-muted; + + &:hover { color: red; } + } +} + +.o_fp_le_tank_results { + border: 1px solid $fp-se-border; + border-radius: 4px; + margin-top: .25rem; + max-height: 12rem; + overflow: auto; + background: $fp-se-card; +} + +.o_fp_le_tank_option { + padding: .35rem .5rem; + cursor: pointer; + border-bottom: 1px solid $fp-se-border; + + &:last-child { border-bottom: 0; } + &:hover { background: rgba(0, 100, 200, .08); } +} + +.o_fp_le_flags { + display: grid; + grid-template-columns: 1fr 1fr; + gap: .35rem .75rem; + + label { + display: flex; + align-items: center; + gap: .35rem; + font-size: .9rem; + cursor: pointer; + } +} + +.o_fp_le_prompts { + border-top: 1px solid $fp-se-border; + padding-top: 1rem; +} + +.o_fp_le_prompts_header { + margin-bottom: .5rem; +} + +.o_fp_le_prompt_actions { + display: flex; + gap: .5rem; + margin-top: .25rem; +} + +.o_fp_library_editor_actions { + margin-top: 1rem; + padding-top: .75rem; + border-top: 1px solid $fp-se-border; + display: flex; + gap: .5rem; + justify-content: flex-end; +} + diff --git a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml index 4f8abd8a..da0362c8 100644 --- a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml +++ b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml @@ -4,6 +4,13 @@
+

Recipe:

@@ -256,27 +263,287 @@
-

Step Library

- -
- -
- - - - st. - -
-
-
- No library entries match your search. + + +
+

Step Library

+
-
+ +
+ +
+ + + + st. + + +
+
+
+ No library entries match your search. +
+
+ + + + +
+
+

+ New Library Step

+

Edit:

+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +