fix(jobs): v3 wizard — chrome on <td>, verified from Odoo source (16.3)
I was wrong about the DOM. Verified from Odoo 19 source on entech:
web/static/src/views/fields/float/float_field.xml
web/static/src/views/fields/char/char_field.xml
web/static/src/views/list/list_renderer.xml
Float/Char fields render as a BARE <span> (read mode) or BARE
<input class="o_input"> (edit mode) directly inside the <td>.
There is NO .o_field_widget wrapper. So all my prior CSS targeting
.o_field_widget matched nothing.
Also discovered: Odoo's getCellClass() in list_renderer.js calls
canUseFormatter() which strips custom <field> classes when the
column has widget="..." set:
canUseFormatter(column, record) {
if (column.widget) {
return false; // ← class stripped here
}
...
}
So o_fp_iw_value class doesn't even land on cells with
widget="boolean_toggle"/"image". Those cells render natively;
boolean toggle and image styling now targets the widgets directly
wherever they appear (.o_boolean_toggle, .o_field_image).
Fix: put visible chrome (border, bg, padding, min-height) on the
<td> itself for prompt/meta/value/extras cells. Make inner span
and input transparent + inherit. Focus ring travels up via
:focus-within on the td.
Cells now look like obvious input boxes from first paint, regardless
of whether the user has clicked into edit mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Native Jobs',
|
'name': 'Fusion Plating — Native Jobs',
|
||||||
'version': '19.0.8.16.2',
|
'version': '19.0.8.16.3',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||||
'author': 'Nexa Systems Inc.',
|
'author': 'Nexa Systems Inc.',
|
||||||
|
|||||||
@@ -207,28 +207,37 @@ $fp-iw-pill-bg : var(--fp-pill-bg, #{$_fp-iw-pill-bg-hex});
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Card header — prompt name ----------
|
// ---------- Card header — prompt name ----------
|
||||||
|
// Char field renders as bare <span> (read) or <input> (edit)
|
||||||
|
// directly in the td. Style typography on the td and make
|
||||||
|
// the inner element inherit + transparent.
|
||||||
td.o_fp_iw_prompt {
|
td.o_fp_iw_prompt {
|
||||||
grid-area: prompt;
|
grid-area: prompt;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $fp-iw-ink;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
input, .o_field_widget {
|
> span,
|
||||||
font-size: 1rem;
|
> input,
|
||||||
font-weight: 600;
|
> input.o_input {
|
||||||
color: $fp-iw-ink;
|
display: block;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
color: inherit !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
font: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
|
min-height: 0 !important;
|
||||||
|
height: auto !important;
|
||||||
|
|
||||||
&[readonly], &:disabled {
|
&[readonly], &:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required asterisk — driven by data-required attribute
|
|
||||||
// OR a server-side compute. We can't easily inspect the
|
|
||||||
// model field here, so the asterisk is rendered by the
|
|
||||||
// XML view via a span sibling (.o_fp_iw_required_marker).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Meta pills — type + unit each in its OWN column ----
|
// ---------- Meta pills — type + unit each in its OWN column ----
|
||||||
@@ -238,99 +247,116 @@ $fp-iw-pill-bg : var(--fp-pill-bg, #{$_fp-iw-pill-bg-hex});
|
|||||||
td.o_fp_iw_meta_type { grid-area: type; }
|
td.o_fp_iw_meta_type { grid-area: type; }
|
||||||
td.o_fp_iw_meta_unit { grid-area: unit; }
|
td.o_fp_iw_meta_unit { grid-area: unit; }
|
||||||
|
|
||||||
|
// Meta pills — input_type (Selection) and target_unit
|
||||||
|
// (Selection) render as bare <span> (read mode) or <select>
|
||||||
|
// (edit mode) directly inside the td. Style the td as a
|
||||||
|
// pill, make the inner element transparent.
|
||||||
td.o_fp_iw_meta {
|
td.o_fp_iw_meta {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
width: auto !important;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: $fp-iw-ink-mute;
|
padding: 4px 10px !important;
|
||||||
|
background-color: $fp-iw-pill-bg !important;
|
||||||
|
color: $fp-iw-ink-soft !important;
|
||||||
|
border: 1px solid $fp-iw-border !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
|
||||||
.o_field_widget {
|
> span,
|
||||||
width: auto;
|
> select,
|
||||||
}
|
> input,
|
||||||
|
> input.o_input {
|
||||||
// The pill itself — applied to whatever input/select
|
|
||||||
// Odoo renders for the field type (Selection → select).
|
|
||||||
select,
|
|
||||||
input,
|
|
||||||
.o_input,
|
|
||||||
.o_field_widget > span {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.75rem;
|
background: transparent !important;
|
||||||
padding: 4px 10px !important;
|
color: inherit !important;
|
||||||
background-color: $fp-iw-pill-bg !important;
|
border: none !important;
|
||||||
color: $fp-iw-ink-soft !important;
|
padding: 0 !important;
|
||||||
border: 1px solid $fp-iw-border !important;
|
margin: 0 !important;
|
||||||
border-radius: 999px !important;
|
font: inherit !important;
|
||||||
line-height: 1.2 !important;
|
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
|
line-height: inherit !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Value — the live widget for this row's type --------
|
// ---------- Value — the live widget for this row's type --------
|
||||||
//
|
//
|
||||||
// KEY INSIGHT: Odoo's editable list shows each cell as a
|
// VERIFIED FROM ODOO 19 SOURCE
|
||||||
// read-mode span/text until the user clicks it (then a real
|
// (web/static/src/views/fields/float/float_field.xml +
|
||||||
// <input> swaps in). Targeting `input { ... }` only styles
|
// web/static/src/views/fields/char/char_field.xml +
|
||||||
// the focused state, leaving every other cell looking like
|
// web/static/src/views/list/list_renderer.xml)
|
||||||
// bare un-clickable text. So we put the input chrome on
|
//
|
||||||
// the .o_field_widget wrapper (always in DOM, both modes)
|
// Float/Char/Date fields render as a BARE <span> (read mode)
|
||||||
// and make the inner <input> transparent so it inherits.
|
// or BARE <input class="o_input"> (edit mode) directly inside
|
||||||
|
// the <td>. There is NO .o_field_widget wrapper. So the
|
||||||
|
// visible "input box" chrome must go on the <td> itself.
|
||||||
|
// The inner span/input is then made transparent.
|
||||||
|
//
|
||||||
|
// Cells with widget="boolean_toggle"/"image" don't get our
|
||||||
|
// o_fp_iw_value class at all (Odoo's canUseFormatter strips
|
||||||
|
// custom classes when column.widget is set), so they render
|
||||||
|
// natively — handled in their own rules below.
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
td.o_fp_iw_value {
|
td.o_fp_iw_value {
|
||||||
grid-area: value;
|
grid-area: value;
|
||||||
position: relative;
|
// Override the global "td { display: block; }" rule above —
|
||||||
|
// we need flex layout so the inner span/input centers.
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 420px;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 10px 14px !important;
|
||||||
|
background-color: $fp-iw-page !important;
|
||||||
|
color: $fp-iw-ink !important;
|
||||||
|
border: 1px solid $fp-iw-ink-faint !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
cursor: text;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: border-color 120ms ease,
|
||||||
|
background-color 120ms ease,
|
||||||
|
box-shadow 120ms ease;
|
||||||
|
|
||||||
// The wrapper IS the visible "input box" — both in
|
&:hover {
|
||||||
// display mode (showing a span) and in edit mode
|
border-color: $fp-iw-ink-mute !important;
|
||||||
// (showing an actual input).
|
|
||||||
> .o_field_widget,
|
|
||||||
> div.o_field_widget {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 420px;
|
|
||||||
min-height: 48px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
background-color: $fp-iw-page;
|
|
||||||
color: $fp-iw-ink;
|
|
||||||
border: 1px solid $fp-iw-ink-faint;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: text;
|
|
||||||
transition: border-color 120ms ease,
|
|
||||||
background-color 120ms ease,
|
|
||||||
box-shadow 120ms ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: $fp-iw-ink-mute;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus ring travels up from the inner input to the
|
// Focus ring travels up from the inner input to the
|
||||||
// wrapper via :focus-within so the visible chrome
|
// td via :focus-within (works even though the input
|
||||||
// glows when the user clicks in.
|
// is the focused element, not the td).
|
||||||
> .o_field_widget:focus-within,
|
&:focus-within {
|
||||||
> div.o_field_widget:focus-within {
|
|
||||||
border-color: $fp-iw-border-focus !important;
|
border-color: $fp-iw-border-focus !important;
|
||||||
background-color: $fp-iw-card !important;
|
background-color: $fp-iw-card !important;
|
||||||
box-shadow: 0 0 0 3px
|
box-shadow: 0 0 0 3px
|
||||||
color-mix(in srgb,
|
color-mix(in srgb,
|
||||||
#{$fp-iw-border-focus} 25%, transparent);
|
#{$fp-iw-border-focus} 25%, transparent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner inputs / spans inherit from wrapper — fully
|
// Inner span (read mode) — fills the cell, left-aligned,
|
||||||
// transparent so they don't double-up on chrome.
|
// inherits typography from the td.
|
||||||
input,
|
> span {
|
||||||
input[type="text"],
|
display: block;
|
||||||
input[type="number"],
|
width: 100%;
|
||||||
input[type="datetime-local"],
|
text-align: left;
|
||||||
input.o_input,
|
color: inherit;
|
||||||
.o_field_widget > span,
|
font: inherit;
|
||||||
.o_field_widget > div {
|
}
|
||||||
|
|
||||||
|
// Inner input (edit mode) — same treatment as the span,
|
||||||
|
// fully transparent so the td chrome shows through.
|
||||||
|
> input,
|
||||||
|
> input.o_input,
|
||||||
|
> input[type="text"],
|
||||||
|
> input[type="number"],
|
||||||
|
> input[type="datetime-local"] {
|
||||||
|
flex: 1 1 auto;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
@@ -338,9 +364,8 @@ $fp-iw-pill-bg : var(--fp-pill-bg, #{$_fp-iw-pill-bg-hex});
|
|||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
font-size: inherit !important;
|
font: inherit !important;
|
||||||
font-weight: inherit !important;
|
line-height: inherit !important;
|
||||||
line-height: 1.4 !important;
|
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
@@ -351,101 +376,86 @@ $fp-iw-pill-bg : var(--fp-pill-bg, #{$_fp-iw-pill-bg-hex});
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Special widgets — opt out of the input chrome ----
|
// ---------- Boolean toggle cells — render bare ---------------
|
||||||
//
|
// value_boolean has widget="boolean_toggle" so canUseFormatter
|
||||||
// Boolean toggle, photo upload, multi-point and panel
|
// returns false → our o_fp_iw_value class is NOT added.
|
||||||
// widgets each have their own visual treatment. They
|
// We target the cell via the type flag column's td position
|
||||||
// shouldn't sit inside an "input box" — they should
|
// (4th visible td when is_boolean_type) — easier: target the
|
||||||
// render bare with their own chrome.
|
// toggle directly anywhere it appears.
|
||||||
> .o_field_widget:has(.o_boolean_toggle),
|
.o_boolean_toggle,
|
||||||
> .o_field_widget:has(.form-switch),
|
.form-switch {
|
||||||
> .o_field_widget.o_field_image,
|
transform: scale(1.5);
|
||||||
> .o_field_widget:has(.o_form_image_controls) {
|
transform-origin: left center;
|
||||||
background: transparent !important;
|
margin: 12px 0 12px 16px;
|
||||||
border: none !important;
|
}
|
||||||
padding: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
|
|
||||||
&:focus-within {
|
// ---------- Image / photo cells — constrain ------------------
|
||||||
background: transparent !important;
|
// Same canUseFormatter issue — widget="image" strips our class.
|
||||||
box-shadow: none !important;
|
// Target the o_field_image natively wherever it lands.
|
||||||
}
|
.o_field_image {
|
||||||
}
|
max-width: 240px;
|
||||||
|
|
||||||
// Boolean toggle — bigger pill for fat fingers
|
img,
|
||||||
.o_boolean_toggle,
|
.o_image,
|
||||||
.form-switch {
|
.o_form_uri,
|
||||||
transform: scale(1.5);
|
.o_form_image_controls {
|
||||||
transform-origin: left center;
|
|
||||||
margin: 12px 0 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image / photo widget — constrain the WHOLE field
|
|
||||||
// (upload area + preview) so it doesn't blow up.
|
|
||||||
.o_field_image {
|
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
|
max-height: 180px;
|
||||||
img,
|
border-radius: 8px;
|
||||||
.o_image,
|
border: 1px solid $fp-iw-border;
|
||||||
.o_form_uri,
|
|
||||||
.o_form_image_controls {
|
|
||||||
max-width: 240px;
|
|
||||||
max-height: 180px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid $fp-iw-border;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Extras — composite types (multi-point, panel) ----
|
// ---------- Extras — composite types (multi-point, panel) ----
|
||||||
// Same display/edit-mode trick as the value cell: chrome
|
// Same approach as value cells: chrome on the td itself,
|
||||||
// on the wrapper, transparent input inside.
|
// because Float fields render as bare span/input.
|
||||||
td.o_fp_iw_extra {
|
td.o_fp_iw_extra {
|
||||||
grid-area: extras;
|
grid-area: extras;
|
||||||
display: inline-flex;
|
display: inline-flex !important;
|
||||||
gap: 6px;
|
align-items: center !important;
|
||||||
align-items: center;
|
width: 80px !important;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 6px 10px !important;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
background-color: $fp-iw-page !important;
|
||||||
|
color: $fp-iw-ink !important;
|
||||||
|
border: 1px solid $fp-iw-ink-faint !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
cursor: text;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
&::before {
|
&:focus-within {
|
||||||
content: attr(data-label);
|
border-color: $fp-iw-border-focus !important;
|
||||||
display: inline-block;
|
background-color: $fp-iw-card !important;
|
||||||
font-size: 0.75rem;
|
|
||||||
color: $fp-iw-ink-mute;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .o_field_widget,
|
// Per-cell label (R1/R2/.../pH/Conc/Temp/Bath) is the
|
||||||
> div.o_field_widget {
|
// field's `string=` attribute — Odoo renders it inside
|
||||||
display: inline-flex;
|
// the cell when in form mode but skips it in list mode.
|
||||||
align-items: center;
|
// We surface it via the data-label attr if present, or
|
||||||
width: 80px;
|
// fall back to no-label (composite cells stack inline
|
||||||
min-height: 38px;
|
// and the visual order makes them obvious).
|
||||||
padding: 6px 10px;
|
|
||||||
background-color: $fp-iw-page;
|
|
||||||
color: $fp-iw-ink;
|
|
||||||
border: 1px solid $fp-iw-ink-faint;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: text;
|
|
||||||
|
|
||||||
&:focus-within {
|
> span,
|
||||||
border-color: $fp-iw-border-focus;
|
> input,
|
||||||
background-color: $fp-iw-card;
|
> input.o_input {
|
||||||
}
|
display: block;
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
|
font: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
|
height: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user