[IMP] web: improve property fields modal design

The purpose of this commit is to improve the design of the Property field
modal as it was done to release something fast.

We also introduce a modification of the JS test related to the property
header. Before this commit, the structure of the modal was composed
of a header and a body.

Since we refactored the component in one single block with the class
`.o_modal_container`, we apply the `.o_field_property_definition_header`
class directly on the input rather than targeting an input inside the
header.

task-3084756

closes odoo/odoo#126274

X-original-commit: 12a25449d9f26aec5f14482952ca0db40bf612b9
Related: odoo/enterprise#43091
Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
chrysanthe(chgo)
2023-06-26 13:20:51 +02:00
committed by Chrysanthe (chgo)
parent 8eb8e08258
commit 4833bc84fb
11 changed files with 151 additions and 197 deletions
+14
View File
@@ -3196,6 +3196,20 @@ msgstr ""
msgid "More"
msgstr ""
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/views/fields/properties/property_definition.xml:0
#, python-format
msgid "Move this Property down"
msgstr ""
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/views/fields/properties/property_definition.xml:0
#, python-format
msgid "Move this Property up"
msgstr ""
#. module: web
#. odoo-javascript
#: code:addons/web/static/src/views/fields/statusbar/statusbar_field.js:0
@@ -87,7 +87,6 @@
// put the popover behind datetime component
z-index: $zindex-popover !important;
font-size: inherit;
box-shadow: 0 3rem 5rem rgba($black, .18) !important;
box-sizing: border-box;
}
@@ -9,7 +9,7 @@
t-foreach="propertiesListGroup"
t-as="propertyConfiguration"
t-key="propertyConfiguration.name"
class="o_property_field d-sm-contents mb-3"
class="o_property_field d-contents mb-2"
t-att-property-name="propertyConfiguration.name">
<t t-set="domId" t-value="generateUniqueDomID()"/>
<label
@@ -1,26 +1,10 @@
.o_field_property_definition_delete {
min-width: fit-content;
}
.o_field_property_definition {
max-height: 70vh;
overflow-y: auto;
table {
width: 500px;
}
.o_td_label {
width: 120px;
}
.o_field_property_definition_header {
max-width: 500px;
input {
font-size: 1.2rem;
border-width: 1px;
}
.o_modal_container {
min-width: calc(#{$o-form-view-sheet-max-width} / 3);
max-width: calc(#{$o-form-view-sheet-max-width} / 2);
}
.o_field_property_definition_type {
@@ -29,10 +13,6 @@
height: 20px;
}
.o_field_property_dropdown > img {
margin-right: -18px;
}
.o_input_dropdown {
input {
background-repeat: no-repeat;
@@ -45,13 +25,10 @@
}
}
td {
padding-bottom: 15px;
}
.o_field_property_definition_kanban,
.o_field_property_definition_value {
.form-check-input {
margin-left: 3px;
margin-left: map-get($spacers, 1);
}
}
@@ -1,50 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.PropertyDefinition" owl="1">
<div class="o_field_property_definition o_field_widget d-block" t-ref="propertyDefinition">
<div class="o_field_property_definition_header d-flex flex-row align-items-center justify-content-between m-3">
<h6 t-if="props.readonly" t-out="state.propertyDefinition.string" class="text-truncate"/>
<input
t-else=""
class="o_input px-2 me-3"
type="text"
required=""
placeholder="Property Name"
t-model="state.propertyDefinition.string"
t-on-change="onPropertyLabelChange"
t-on-keypress="onPropertyLabelKeypress"
/>
<div class="d-flex flex-row me-3">
<div class="o_field_property_definition o_field_widget o_form_view d-block" t-ref="propertyDefinition">
<div class="d-flex justify-content-between px-3 border-bottom">
<div class="border-end py-1">
<button
t-attf-class="btn btn-link p-0 m-0 me-2 {{this.isFirst ? 'disabled' : ''}}"
t-on-click="() => this.onPropertyMove('up')">
<i class="oi oi-chevron-up"/>
t-attf-class="btn btn-link ps-0 {{this.isFirst ? 'disabled' : ''}}"
t-on-click="() => this.onPropertyMove('up')" title="Move this Property up">
<i class="oi oi-chevron-up ms-2"/>
</button>
<button
t-attf-class="btn btn-link p-0 m-0 {{this.isLast ? 'disabled' : ''}}"
t-on-click="() => this.onPropertyMove('down')">
<i class="oi oi-chevron-down"/>
</button>
<button
t-if="!props.readonly and props.canChangeDefinition"
class="btn text-danger p-0 float-end o_field_property_definition_delete ms-3"
title="Remove Property"
t-on-click="props.onDelete">
<i class="fa fa-trash"/> Delete
t-attf-class="btn btn-link pe-3 {{this.isLast ? 'disabled' : ''}}"
t-on-click="() => this.onPropertyMove('down')" title="Move this Property down">
<i class="oi oi-chevron-down ms-2"/>
</button>
</div>
<button
t-if="!props.readonly and props.canChangeDefinition"
class="btn pe-0 text-danger float-end o_field_property_definition_delete"
title="Remove Property"
t-on-click="props.onDelete">
<i class="fa fa-trash"/> Delete
</button>
</div>
<table class="m-3">
<tr>
<td class="align-top pe-2">
<label t-att-for="getUniqueDomID('type')">
<b>Field Type</b>
</label>
</td>
<td class="o_field_property_definition_type">
<div class="o_modal_container p-3 o_inner_group grid">
<div class="d-contents mb-3 mb-sm-0">
<label class="o_form_label align-self-center text-900">Label</label>
<h6 t-if="props.readonly" t-out="state.propertyDefinition.string"
class="text-truncate"/>
<input
class="o_input o_field_property_definition_header"
type="text"
required=""
placeholder="Property Name"
t-model="state.propertyDefinition.string"
t-on-change="onPropertyLabelChange"
t-on-keypress="onPropertyLabelKeypress"
/>
</div>
<div class="d-contents mb-3 mb-sm-0">
<label t-att-for="getUniqueDomID('type')"
class="o_form_label align-self-center text-900">
Field Type
</label>
<div class="o_field_property_definition_type">
<div
t-if="props.readonly"
class="d-flex align-items-center">
class="d-flex align-items-center mb-2">
<img class="me-3"
t-attf-src="/web/static/src/views/fields/properties/icons/{{state.propertyDefinition.type}}.png"/>
<span t-out="state.typeLabel"/>
@@ -54,8 +56,9 @@
title="state.typeLabel" togglerClass="'btn btn-link d-flex p-0 w-100'">
<t t-set-slot="toggler">
<div class="o_input_dropdown w-100 o_field_property_dropdown">
<img t-attf-src="/web/static/src/views/fields/properties/icons/{{state.propertyDefinition.type}}.png"/>
<input type="text" class="dropdown text-start w-100 o_input py-1"
<img t-attf-src="/web/static/src/views/fields/properties/icons/{{state.propertyDefinition.type}}.png"
class="position-relative z-index-1 me-n4"/>
<input type="text" class="dropdown text-start w-100 o_input py-1 align-middle"
t-att-id="getUniqueDomID('type')"
t-att-value="state.typeLabel" readonly=""/>
<span class="o_dropdown_button" />
@@ -71,66 +74,48 @@
</DropdownItem>
</t>
</Dropdown>
</td>
</tr>
</div>
</div>
<!-- Add / remove selection labels -->
<tr t-if="state.propertyDefinition.type === 'selection'">
<td class="o_td_label align-top pe-2">
<b>Values</b>
</td>
<td>
<PropertyDefinitionSelection
readonly="props.readonly || !props.canChangeDefinition"
canChangeDefinition="props.canChangeDefinition"
default="state.propertyDefinition.default || ''"
options="state.propertyDefinition.selection || []"
onOptionsChange.bind="onSelectionOptionChange"
onDefaultOptionChange.bind="onDefaultChange"
/>
</td>
</tr>
<tr t-if="state.propertyDefinition.type === 'tags'">
<td class="o_td_label align-top">
<label t-att-for="getUniqueDomID('tags')">
<b>Tags</b>
</label>
</td>
<td>
<PropertyTags
id="getUniqueDomID('tags')"
selectedTags="propertyTagValues"
tags="state.propertyDefinition.tags || []"
readonly="props.readonly"
canChangeTags="props.canChangeDefinition"
checkDefinitionWriteAccess.bind="props.checkDefinitionWriteAccess ? props.checkDefinitionWriteAccess : () => {}"
deleteAction="'tags'"
onTagsChange.bind="onTagsChange"/>
</td>
</tr>
<tr t-if="state.propertyDefinition.type === 'many2one' || state.propertyDefinition.type === 'many2many'" class="o_field_property_definition_model">
<td class="o_td_label align-top pe-2">
<label t-att-for="getUniqueDomID('model')">
<b>Model</b>
</label>
</td>
<td>
<span t-if="props.readonly">
<t t-if="state.resModelDescription" t-out="state.resModelDescription"/>
</span>
<ModelSelector
t-else=""
id="getUniqueDomID('model')"
value="state.resModelDescription"
onModelSelected.bind="onModelChange"
/>
</td>
</tr>
<tr t-if="(state.propertyDefinition.type === 'many2one' || state.propertyDefinition.type === 'many2many')
and state.propertyDefinition.comodel and state.propertyDefinition.comodel.length">
<td class="o_td_label align-top pe-2">
<b>Domain</b>
</td>
<td>
<div t-if="state.propertyDefinition.type === 'selection'" class="d-contents mb-3 mb-sm-0">
<label class="o_form_label text-900">Values</label>
<PropertyDefinitionSelection
readonly="props.readonly || !props.canChangeDefinition"
canChangeDefinition="props.canChangeDefinition"
default="state.propertyDefinition.default || ''"
options="state.propertyDefinition.selection || []"
onOptionsChange.bind="onSelectionOptionChange"
onDefaultOptionChange.bind="onDefaultChange"
/>
</div>
<div t-if="state.propertyDefinition.type === 'tags'" class="d-contents mb-3 mb-sm-0">
<label t-att-for="getUniqueDomID('tags')" class="o_form_label text-900">Tag</label>
<PropertyTags
id="getUniqueDomID('tags')"
selectedTags="propertyTagValues"
tags="state.propertyDefinition.tags || []"
readonly="props.readonly"
canChangeTags="props.canChangeDefinition"
checkDefinitionWriteAccess.bind="props.checkDefinitionWriteAccess ? props.checkDefinitionWriteAccess : () => {}"
deleteAction="'tags'"
onTagsChange.bind="onTagsChange"/>
</div>
<div t-if="state.propertyDefinition.type === 'many2one' || state.propertyDefinition.type === 'many2many'" class="o_field_property_definition_model d-contents mb-3 mb-sm-0">
<label t-att-for="getUniqueDomID('model')" class="o_form_label align-self-center text-900">Model</label>
<span t-if="props.readonly">
<t t-if="state.resModelDescription" t-out="state.resModelDescription"/>
</span>
<ModelSelector
t-else=""
id="getUniqueDomID('model')"
value="state.resModelDescription"
onModelSelected.bind="onModelChange"
/>
</div>
<div t-if="(state.propertyDefinition.type === 'many2one' || state.propertyDefinition.type === 'many2many')
and state.propertyDefinition.comodel and state.propertyDefinition.comodel.length" class="d-contents mb-3 mb-sm-0">
<label class="o_form_label text-900">Domain</label>
<div>
<DomainSelector
resModel="state.propertyDefinition.comodel"
domain="state.propertyDefinition.domain || '[]'"
@@ -146,53 +131,41 @@
<t t-out="state.matchingRecordsCount"/> record(s)
</button>
</div>
</td>
</tr>
<tr t-if="!['tags', 'selection'].includes(state.propertyDefinition.type)
</div>
</div>
<div t-if="!['tags', 'selection'].includes(state.propertyDefinition.type)
and (!['many2one', 'many2many'].includes(state.propertyDefinition.type)
|| (state.propertyDefinition.comodel and state.propertyDefinition.comodel.length))"
class="o_field_property_definition_value">
<td class="o_td_label align-top pe-2">
<label t-att-for="getUniqueDomID('default')">
<b t-if="state.propertyDefinition.type === 'boolean'">
Default State
</b>
<b t-else="">
Default Value
</b>
</label>
</td>
<td>
<PropertyValue
id="getUniqueDomID('default')"
readonly="props.readonly"
canChangeDefinition="props.canChangeDefinition"
type="state.propertyDefinition.type"
string="state.propertyDefinition.string"
value="state.propertyDefinition.default"
comodel="state.propertyDefinition.comodel || ''"
domain="state.propertyDefinition.domain || '[]'"
context="props.context"
onChange.bind="onDefaultChange"
/>
</td>
</tr>
<tr t-if="!props.hideKanbanOption" class="o_field_property_definition_kanban">
<td class="o_td_label align-top pe-2">
<label t-att-for="getUniqueDomID('kanban')">
<b>View In Kanban</b>
</label>
</td>
<td>
<CheckBox
id="getUniqueDomID('kanban')"
value="props.propertyDefinition.view_in_kanban"
disabled="props.readonly"
onChange.bind="onViewInKanbanChange"
/>
</td>
</tr>
</table>
class="o_field_property_definition_value d-contents mb-3 mb-sm-0">
<label t-att-for="getUniqueDomID('default')" class="o_form_label align-self-center text-900">
<b>
<t t-if="state.propertyDefinition.type === 'boolean'">Default State</t>
<t t-else="">Default Value</t>
</b>
</label>
<PropertyValue
id="getUniqueDomID('default')"
readonly="props.readonly"
canChangeDefinition="props.canChangeDefinition"
type="state.propertyDefinition.type"
string="state.propertyDefinition.string"
value="state.propertyDefinition.default"
comodel="state.propertyDefinition.comodel || ''"
domain="state.propertyDefinition.domain || '[]'"
context="props.context"
onChange.bind="onDefaultChange"
/>
</div>
<div t-if="!props.hideKanbanOption" class="o_field_property_definition_kanban d-contents mb-3 mb-sm-0">
<label t-att-for="getUniqueDomID('kanban')" class="o_form_label align-self-center text-900">View In Kanban</label>
<CheckBox
id="getUniqueDomID('kanban')"
value="props.propertyDefinition.view_in_kanban"
disabled="props.readonly"
onChange.bind="onViewInKanbanChange"
/>
</div>
</div>
</div>
</t>
</templates>
@@ -1,10 +1,5 @@
.o_field_property_selection {
input {
border: 0;
background: transparent;
}
.fa-star,
.fa-star-o {
color: gold;
.fa-star {
color: $o-main-favorite-color;
}
}
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.PropertyDefinitionSelection" owl="1">
<div class="d-flex flex-column o_field_property_selection"
<div class="o_field_property_selection d-flex"
t-ref="propertyDefinitionSelection">
<div class="w-100">
<div
t-foreach="optionsVisible" t-as="option" t-key="option[0]"
class="w-100 card d-flex flex-row align-items-center justify-content-between my-2 px-3 rounded o_field_property_selection_option">
class="o_field_property_selection_option d-flex flex-row align-items-center justify-content-between mb-2 rounded">
<span t-if="props.readonly" t-out="option[1]"/>
<input
t-else=""
class="o_input"
class="o_input me-3"
type="text"
placeholder="Option Name"
t-att-value="option[1]"
@@ -19,17 +19,17 @@
t-on-keydown="(event) => this.onOptionKeyDown(event, option_index)"
/>
<button
class="btn btn-link text-secondary p-0 m-0"
class="btn btn-link m-0 p-0 text-muted fs-5"
title="Select Default"
t-on-click="() => this.onOptionSetDefault(option_index)">
<i t-att-class="option[0] === props.default ? 'fa fa-star' : 'fa fa-star-o'"/>
<i t-att-class="option[0] === props.default ? 'fa fa-star align-bottom' : 'fa fa-star-o align-bottom'"/>
</button>
<button
t-if="!props.readonly"
class="btn btn-link p-0 m-0 ms-2"
class="btn btn-link link-danger ms-2 p-0 fs-5"
title="Remove Property"
t-on-click="() => this.onOptionDelete(option_index)">
<i class="text-danger fa fa-times"/>
<i class="fa fa-trash-o align-bottom"/>
</button>
</div>
<button
@@ -2,14 +2,14 @@
<templates xml:space="preserve">
<t t-name="web.PropertyTags" owl="1">
<!-- Copy many2many tags style without duplicating all the CSS -->
<div t-attf-class="o_field_property_tag o_field_widget o_field_many2many_tags d-flex align-items-center {{props.readonly ? 'readonly' : ''}}">
<div t-attf-class="o_field_property_tag o_field_widget o_field_many2many_tags d-flex {{props.readonly ? 'readonly' : ''}}">
<TagsList
tags="tagListItems"
displayBadge="displayBadge"
className="props.canChangeTags ? '' : 'o_field_property_tag_readonly'"/>
<div
t-if="!props.readonly"
class="o_field_property_dropdown_menu o_input_dropdown mt-2">
class="o_field_property_dropdown_menu o_input_dropdown">
<AutoComplete
id="props.id"
value="''"
@@ -27,10 +27,6 @@ input.o_field_property_input[type=number] {
}
.o_field_property_many2many_value {
.o_tag {
color: nth($o-colors, 1) !important;
box-shadow: inset 0 0 0 1px !important;
}
.o_delete {
line-height: 1;
margin-right: 5px;
@@ -113,7 +113,7 @@
</t>
</div>
<t t-elif="props.type === 'many2many'">
<div t-if="props.comodel" class="o_field_property_many2many_value d-flex flex-row ms-0 pe-1 w-100 flex-wrap"
<div t-if="props.comodel" class="o_field_property_many2many_value d-flex flex-wrap flex-row gap-1 ms-0 pe-1 w-100 "
t-attf-class="o_field_widget o_field_many2many_tags {{showAvatar ? 'avatar' : ''}} {{props.readonly ? 'readonly' : ''}}">
<!-- Copy many2many tags style without duplicating all CSS -->
<TagsList tags="propertyValue"/>
@@ -325,14 +325,14 @@ QUnit.module("Fields", (hooks) => {
const popover = target.querySelector(".o_property_field_popover");
assert.ok(popover, "Should have opened the definition popover");
const label = popover.querySelector(".o_field_property_definition_header input");
const label = popover.querySelector(".o_field_property_definition_header");
assert.strictEqual(label.value, "My Char");
const type = popover.querySelector(".o_field_property_definition_type input");
assert.strictEqual(type.value, "Text");
// Change the property type to "Date & Time"
await editInput(target, ".o_field_property_definition_header input", "My Datetime");
await editInput(target, ".o_field_property_definition_header", "My Datetime");
await changeType(target, "datetime");
assert.strictEqual(type.value, "Date & Time", "Should have changed the property type");
@@ -353,7 +353,7 @@ QUnit.module("Fields", (hooks) => {
await closePopover(target);
// Check that the type change have been propagated
const datetimeLabel = field.querySelector(".o_field_property_label b");
const datetimeLabel = field.querySelector(".o_field_property_label");
assert.strictEqual(
datetimeLabel.innerText,
"My Datetime",
@@ -419,7 +419,7 @@ QUnit.module("Fields", (hooks) => {
const popover = target.querySelector(".o_property_field_popover");
assert.ok(popover, "Should have opened the definition popover");
const label = popover.querySelector(".o_field_property_definition_header input");
const label = popover.querySelector(".o_field_property_definition_header");
assert.strictEqual(label.value, "Property 3", "Should have added a default label");
const type = popover.querySelector(".o_field_property_definition_type input");
@@ -562,7 +562,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(options.length, 4, "Should not remove any options");
// Remove the second option
await click(target, ".o_field_property_selection_option:nth-child(2) .fa-times");
await click(target, ".o_field_property_selection_option:nth-child(2) .fa-trash-o");
options = popover.querySelectorAll(".o_field_property_selection_option");
assert.strictEqual(options.length, 3, "Should have removed the second option");
const optionValues = [...options].map((option) => option.querySelector("input").value);