This commit is contained in:
gsinghpal
2026-05-04 02:14:34 -04:00
parent 3cc393454d
commit 586f05d567
43 changed files with 3656 additions and 112 deletions

View File

@@ -0,0 +1,58 @@
/** @odoo-module **/
// Sub 14b — visual FontAwesome icon picker for fp.step.kind.icon and any
// other Selection field whose values are FA classes (e.g. 'fa-flask').
//
// Always-visible compact grid with a Search box. Glyph-only tiles
// (label appears on hover via title attribute). Designed for the form
// view; the list shows raw text and opens the form for editing.
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
class FpIconPickerField extends Component {
static template = "fusion_plating.FpIconPicker";
static props = { ...standardFieldProps };
setup() {
this.state = useState({ filter: "" });
}
get options() {
const field = this.props.record.fields[this.props.name];
return (field && field.selection) || [];
}
get filteredOptions() {
const q = (this.state.filter || "").trim().toLowerCase();
if (!q) return this.options;
return this.options.filter(([code, label]) =>
code.toLowerCase().includes(q) ||
(label || "").toLowerCase().includes(q)
);
}
get currentValue() {
return this.props.record.data[this.props.name] || "";
}
get currentLabel() {
const opt = this.options.find((o) => o[0] === this.currentValue);
return opt ? opt[1] : "";
}
async onPick(value, ev) {
if (this.props.readonly) return;
ev.preventDefault();
ev.stopPropagation();
await this.props.record.update({ [this.props.name]: value });
}
}
export const fpIconPickerField = {
component: FpIconPickerField,
displayName: "Icon Picker",
supportedTypes: ["selection"],
};
registry.category("fields").add("fp_icon_picker", fpIconPickerField);

View File

@@ -243,6 +243,7 @@ export class FpSimpleRecipeEditor extends Component {
*/
async onOpenLibraryCreate() {
await this._fpEnsureWorkflowStatesLoaded();
await this._fpEnsureKindOptionsLoaded();
this.state.libraryEditor = {
id: null, // null = create
name: "",
@@ -282,9 +283,67 @@ export class FpSimpleRecipeEditor extends Component {
}
}
/**
* Sub 14b — fetch the user-extensible Step Kind catalog once per
* editor session, cache on this.state.kindOptions. Used by both
* create + edit flows to populate the "Step Kind" dropdown so
* user-added kinds appear without a page reload.
*/
async _fpEnsureKindOptionsLoaded() {
if (this.state.kindOptions && this.state.kindOptions.length) {
return;
}
try {
const data = await rpc("/fp/simple_recipe/kinds/list", {});
this.state.kindOptions = data.kinds || [];
} catch (err) {
this.state.kindOptions = [];
}
}
/**
* Sub 14b — handler for Step Kind dropdown change. Special-cases
* the "+ Add a new kind…" sentinel: prompt the user for a name,
* round-trip to /kinds/create, refresh the cached options, then
* select the newly-created kind.
*/
async onKindChange(ev) {
const code = ev.target.value;
if (code !== "__new__") {
this.state.libraryEditor.default_kind = code || "";
return;
}
// Reset the dropdown so it doesn't stay on the sentinel if the
// user cancels the prompt.
ev.target.value = this.state.libraryEditor.default_kind || "";
const name = window.prompt(
"Name your new Step Kind (e.g. 'Passivation', 'Shot Peen')",
""
);
if (!name || !name.trim()) {
return;
}
try {
const data = await rpc("/fp/simple_recipe/kinds/create", {
name: name.trim(),
});
if (!data.ok) {
alert(data.error || "Could not create Step Kind.");
return;
}
// Drop the cached list so the next ensure() refetches it.
this.state.kindOptions = null;
await this._fpEnsureKindOptionsLoaded();
this.state.libraryEditor.default_kind = data.code;
} catch (err) {
alert("Could not create Step Kind: " + (err.message || err));
}
}
async onOpenLibraryEdit(templateId) {
this.state.libraryEditorBusy = true;
await this._fpEnsureWorkflowStatesLoaded();
await this._fpEnsureKindOptionsLoaded();
const data = await rpc("/fp/simple_recipe/library/load", {
template_id: templateId,
});

View File

@@ -0,0 +1,150 @@
// Sub 14b — Visual icon picker for fp.step.kind.icon and similar
// Selection fields whose values are FontAwesome class names.
//
// Compact 12-column grid with Search filter. Glyph-only tiles
// (label appears on hover via the browser tooltip). Capped at ~280px
// scrollable height so it doesn't dominate the form.
//
// Dark-mode aware via $o-webclient-color-scheme branch (see CLAUDE.md).
$o-webclient-color-scheme: bright !default;
$_fp-icon-picker-bg-hex: #ffffff;
$_fp-icon-picker-border-hex: #d8dadd;
$_fp-icon-picker-hover-hex: #f3f4f6;
$_fp-icon-picker-active-hex: #2c89e9;
$_fp-icon-picker-text-hex: #21252b;
$_fp-icon-picker-muted-hex: #6c757d;
@if $o-webclient-color-scheme == dark {
$_fp-icon-picker-bg-hex: #22262d !global;
$_fp-icon-picker-border-hex: #3a3f47 !global;
$_fp-icon-picker-hover-hex: #2c313a !global;
$_fp-icon-picker-active-hex: #4ea3ff !global;
$_fp-icon-picker-text-hex: #e6e9ef !global;
$_fp-icon-picker-muted-hex: #9aa3ad !global;
}
$fp-icon-picker-bg: var(--fp-icon-picker-bg, $_fp-icon-picker-bg-hex);
$fp-icon-picker-border: var(--fp-icon-picker-border, $_fp-icon-picker-border-hex);
$fp-icon-picker-hover: var(--fp-icon-picker-hover, $_fp-icon-picker-hover-hex);
$fp-icon-picker-active: var(--fp-icon-picker-active, $_fp-icon-picker-active-hex);
$fp-icon-picker-text: var(--fp-icon-picker-text, $_fp-icon-picker-text-hex);
$fp-icon-picker-muted: var(--fp-icon-picker-muted, $_fp-icon-picker-muted-hex);
// Force full sheet width even when Odoo wraps the field in a fixed-width
// .o_field_widget cell. Selecting both the wrapper and the inline root
// belt-and-suspenders any group container that tries to clip us.
.o_field_widget[name="icon"]:has(.o_fp_icon_picker_inline) {
width: 100% !important;
max-width: none !important;
flex: 1 1 100% !important;
}
.o_fp_icon_picker_inline {
display: flex;
flex-direction: column;
gap: 0.4rem;
width: 100%;
.o_fp_icon_picker_top {
display: flex;
align-items: center;
gap: 0.5rem;
}
.o_fp_icon_picker_current {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.25rem 0.6rem;
background-color: $fp-icon-picker-bg;
border: 1px solid $fp-icon-picker-border;
border-radius: 4px;
color: $fp-icon-picker-text;
font-size: 0.85rem;
flex: 1;
min-width: 0;
}
.o_fp_icon_picker_current_glyph {
font-size: 1rem;
color: $fp-icon-picker-active;
width: 1.2em;
text-align: center;
}
.o_fp_icon_picker_current_empty {
color: $fp-icon-picker-muted;
}
.o_fp_icon_picker_current_label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.o_fp_icon_picker_filter {
flex: 0 0 140px;
height: 28px;
font-size: 0.8rem;
padding: 0.2rem 0.5rem;
}
.o_fp_icon_picker_inline_grid {
// Auto-fill tracks of ~36px each → grid expands to fill
// whatever width the form column gives it.
display: grid;
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
gap: 2px;
padding: 0.4rem;
background-color: $fp-icon-picker-bg;
border: 1px solid $fp-icon-picker-border;
border-radius: 4px;
max-height: 240px;
overflow-y: auto;
}
.o_fp_icon_picker_tile {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
color: $fp-icon-picker-text;
height: 30px;
width: 100%;
&:hover:not(:disabled) {
background-color: $fp-icon-picker-hover;
border-color: $fp-icon-picker-border;
}
&.o_fp_icon_picker_active {
border-color: $fp-icon-picker-active;
color: $fp-icon-picker-active;
background-color: rgba(44, 137, 233, 0.10);
}
&:disabled {
cursor: default;
opacity: 0.55;
}
}
.o_fp_icon_picker_tile_glyph {
font-size: 1rem;
}
.o_fp_icon_picker_empty_results {
grid-column: 1 / -1;
padding: 0.5rem;
text-align: center;
color: $fp-icon-picker-muted;
font-size: 0.8rem;
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_plating.FpIconPicker">
<div class="o_fp_icon_picker_inline">
<div class="o_fp_icon_picker_top">
<div class="o_fp_icon_picker_current">
<i t-if="currentValue" t-att-class="'fa ' + currentValue + ' o_fp_icon_picker_current_glyph'"/>
<i t-if="!currentValue" class="fa fa-square-o o_fp_icon_picker_current_glyph o_fp_icon_picker_current_empty"/>
<span class="o_fp_icon_picker_current_label" t-esc="currentLabel || 'Pick an icon'"/>
</div>
<input type="text" class="o_fp_icon_picker_filter form-control"
placeholder="Search…"
t-att-value="state.filter"
t-on-input="(ev) => state.filter = ev.target.value"
t-att-disabled="props.readonly"/>
</div>
<div class="o_fp_icon_picker_inline_grid">
<button type="button"
t-foreach="filteredOptions" t-as="opt" t-key="opt[0]"
class="o_fp_icon_picker_tile"
t-att-class="{ 'o_fp_icon_picker_active': opt[0] === currentValue }"
t-att-title="opt[1]"
t-att-disabled="props.readonly"
t-on-click="(ev) => this.onPick(opt[0], ev)">
<i t-att-class="'fa ' + opt[0] + ' o_fp_icon_picker_tile_glyph'"/>
</button>
<div t-if="filteredOptions.length === 0" class="o_fp_icon_picker_empty_results">
No icons match "<t t-esc="state.filter"/>"
</div>
</div>
</div>
</t>
</templates>

View File

@@ -414,32 +414,15 @@
title="Picking a kind auto-seeds prompts and turns on workflow gates (Contract Review, Racking, Bake). Leave blank for plain generic steps."/>
</label>
<select class="form-select"
t-model="state.libraryEditor.default_kind">
t-on-change="(ev) => this.onKindChange(ev)"
t-att-value="state.libraryEditor.default_kind">
<option value="">Generic — no automatic behaviour</option>
<option value="receiving">Receiving / Incoming Inspection</option>
<option value="contract_review">Contract Review (QA-005)</option>
<option value="racking">Racking</option>
<option value="mask">Masking</option>
<option value="cleaning">Cleaning</option>
<option value="electroclean">Electroclean</option>
<option value="etch">Etch / Activation</option>
<option value="rinse">Rinse</option>
<option value="strike">Strike (Wood's Nickel / Activation)</option>
<option value="plate">Plating</option>
<option value="replenishment">Tank Replenishment</option>
<option value="wbf_test">Water Break Free Test</option>
<option value="dry">Drying</option>
<option value="bake">Bake (HE Relief / Stress Relief)</option>
<option value="demask">De-Masking</option>
<option value="derack">De-Racking</option>
<option value="inspect">Inspection</option>
<option value="hardness_test">Hardness Test</option>
<option value="adhesion_test">Adhesion Test</option>
<option value="salt_spray">Salt Spray / Corrosion Test</option>
<option value="final_inspect">Final Inspection</option>
<option value="packaging">Packaging / Pre-Ship</option>
<option value="ship">Shipping</option>
<option value="gating">Gating</option>
<t t-foreach="state.kindOptions || []" t-as="k" t-key="k.id">
<option t-att-value="k.code" t-att-selected="k.code === state.libraryEditor.default_kind">
<t t-esc="k.name"/>
</option>
</t>
<option value="__new__">+ Add a new kind…</option>
</select>
</div>
<div class="o_fp_le_field">