feat(plating): session 2026-05-23 deploys — F1/F7/S22/S23 + UI fixes
Consolidated commit of session work already deployed to entech and verified via the deep audit + the persona walk: S22 — Signoff gate (fp.job.step.requires_signoff was 100% unenforced, 42/42 done steps had NULL signoff_user_id). Three-piece fix: _fp_autosign_if_required (captures finisher on button_finish), _fp_check_signoff_complete (raises UserError if NULL after autosign), action_signoff (explicit supervisor pre-sign). Bypass: fp_skip_signoff_gate=True. S23 — Transition-form gate (same dormant-field shape as S22, caught preventively before recipe authors flipped requires_transition_form on). Model helpers on fp.job.step.move + controller gate in move_controller (parts commit) + pre-reject in rack commit. F7 — Chatter standardization: _fp_create_qc_check_if_needed, _fp_fire_notification, _fp_create_delivery silent failures now also post to job chatter instead of only logging to file. UI fixes: - Critical Rule 20 documented + applied: OWL templates only expose Math as a global. Calling String(d) inside t-on-click throws 'v2 is not a function'. Fixed pin_pad.xml (string array instead of number array with String() coercion). Also swept parseInt/ parseFloat in recipe_tree_editor + simple_recipe_editor. - Notes panel HTML escape fix: chatter messages off /fp/workspace/load were rendered via t-out, escaping the HTML. Wrap with markup() in job_workspace.js refresh() before assigning to state. Versions: fusion_plating 19.0.20.8.0 → 19.0.20.9.0 fusion_plating_jobs 19.0.10.20.0 → 19.0.10.23.0 fusion_plating_shopfloor 19.0.30.2.0 → 19.0.30.5.0 All deployed to entech (LXC 111) and verified live. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -433,6 +433,31 @@ export class FpRecordInputsDialog extends Component {
|
||||
this.state.rows.splice(idx, 1);
|
||||
}
|
||||
|
||||
// Mirrors fp.job.step.input.wizard.line._has_value() Python helper.
|
||||
// Critical: the wizard SKIPS rows where _has_value() is False when
|
||||
// creating fp.job.step.move.input.value records, so the server-side
|
||||
// required-inputs gate considers them "not recorded". This client
|
||||
// check must match that semantic exactly or the server will reject
|
||||
// saves the operator thought were complete.
|
||||
_fpRowHasValue(row) {
|
||||
if (row.input_type === "photo") return !!row.photo_value;
|
||||
if (row.input_type === "multi_point_thickness") {
|
||||
return !!(row.point_1 || row.point_2 || row.point_3
|
||||
|| row.point_4 || row.point_5);
|
||||
}
|
||||
if (row.input_type === "bath_chemistry_panel") {
|
||||
return !!(row.panel_ph || row.panel_concentration
|
||||
|| row.panel_temperature || row.panel_bath_id);
|
||||
}
|
||||
if (row.input_type === "pass_fail") return !!row._passfail_chosen;
|
||||
// Boolean: value_boolean===true counts; untouched/false is
|
||||
// treated as no-value to match Python `any([..., self.value_
|
||||
// boolean, ...])`. Operators MUST affirmatively check the box.
|
||||
return !!(row.value_text || row.value_number
|
||||
|| row.value_boolean || row.value_date
|
||||
|| row.value_min || row.value_max);
|
||||
}
|
||||
|
||||
// The "current" initials value across all rows — a row counts as a
|
||||
// signature/initials field when ``_fpIsInitialsField`` is true.
|
||||
// Returns the most-recently-set value (last write wins) or empty.
|
||||
@@ -477,6 +502,26 @@ export class FpRecordInputsDialog extends Component {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Required-prompt gate when finishing the step (advanceAfter=true).
|
||||
// Mirrors fp.job.step._fp_check_step_inputs_complete server-side
|
||||
// so the operator sees the missing fields instantly instead of
|
||||
// getting a server roundtrip error after the save commits. Partial
|
||||
// saves are still allowed when the dialog is opened from the
|
||||
// per-row Record button (advanceAfter=false).
|
||||
if (this.props.advanceAfter) {
|
||||
const missing = this.state.rows
|
||||
.filter((r) => r.required && !this._fpRowHasValue(r))
|
||||
.map((r) => r.name || _t("(unnamed)"));
|
||||
if (missing.length) {
|
||||
this.notification.add(
|
||||
_t("Cannot finish step — %n required prompt(s) missing: %list")
|
||||
.replace("%n", missing.length)
|
||||
.replace("%list", missing.map((n) => `"${n}"`).join(", ")),
|
||||
{ type: "danger", sticky: true },
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.state.saving = true;
|
||||
const payload = this.state.rows.map((r) => {
|
||||
// When the prompt expects a range entry (min + max readings),
|
||||
|
||||
Reference in New Issue
Block a user