chnages
This commit is contained in:
@@ -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);
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user