[REF] web,*: ControlPanel layout

This commit brings the ControlPanel into a single line with 3 main
sections:
- buttons & breadcrumb
- layout related actions (ie. the SearchBar in multirecord view or
  ButtonBox in formView)
- navigation (pager, switch view...)

Add new search bar menu, this is a merge of the following components
into one big component display in column:
* comparison_menu
* favorite_menu
* filter_menu
* group_by_menu

Also adapt navigation hook.

Part-of: odoo/odoo#116641
This commit is contained in:
Pierre Paridans
2023-05-12 22:59:16 +02:00
parent b2ef35a431
commit caef16ee4e
66 changed files with 865 additions and 851 deletions
@@ -51,13 +51,13 @@
</t>
<t t-name="account.ListView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//*[@class='btn btn-primary o_list_button_add']" position="after">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<t t-call="account.AccountViewUploadButton"/>
</xpath>
</t>
<t t-name="account.KanbanView.Buttons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//*[@class='btn btn-primary o-kanban-button-new']" position="after">
<xpath expr="//div[hasclass('o_cp_buttons')]" position="inside">
<t t-call="account.AccountViewUploadButton"/>
</xpath>
</t>
@@ -63,11 +63,7 @@ export class ImportAction extends Component {
get display() {
return {
controlPanel: {
"top-right": false,
"bottom-left": true,
"bottom-right": false,
},
controlPanel: {},
};
}
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="DataRecycle.buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-if='props.showButtons']">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<t t-if="nbSelected">
<button type="button" class="btn btn-primary o_data_recycle_validate_button" t-on-click="onValidateClick">
Validate
@@ -18,20 +18,14 @@
</t>
<t t-name="hr_expense.KanbanButtons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-if='canCreate']" position="after">
<xpath expr="//div[hasclass('o_cp_buttons')]" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput" />
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Upload
</button>
</xpath>
<xpath expr="//t[@t-if='canCreate']" position="before">
<button type="button" class="d-inline d-md-none o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Scan
</button>
</xpath>
<xpath expr="//div" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput" />
</xpath>
</t>
</templates>
+2 -18
View File
@@ -3,7 +3,8 @@
<t t-name="hr_expense.ListButtons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<!-- hr.expense and hr.expense.sheet -->
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput"/>
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Scan
</button>
@@ -11,32 +12,15 @@
t-on-click="() => this.action_show_expenses_to_submit()">
Create Report
</button>
</xpath>
<xpath expr="//div" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput"/>
</xpath>
<!-- hr.expense.sheet -->
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displaySubmit()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_submit_sheet')">
Submit
</button>
</xpath>
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayApprove()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_approve_expense_sheets')">
Approve Report
</button>
</xpath>
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayPost()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_sheet_move_create')">
Post Entries
</button>
</xpath>
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayPayment()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_register_payment')">
Register Payment
</button>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="hr_fleet.HrFleetKanbanController.Buttons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//button[hasclass('o-kanban-button-new')]" position="replace">
<xpath expr="//div[hasclass('o_cp_buttons')]" position="inside">
<input type="file" multiple="true" t-ref="uploadFileInput" class="o_input_file o_hidden" t-on-change.stop="onInputChange"/>
<button type="button" t-att-class="'d-none d-md-block btn' + (!env.isSmall ? ' btn-primary' : 'btn-secondary')" t-on-click="() => this.uploadFileInput.el.click()">
Upload
@@ -57,8 +57,7 @@
</t>
<t t-name="loyalty.LoyaltyCardListView.buttons" owl="1" t-inherit-mode="primary" t-inherit="web.ListView.Buttons">
<xpath expr="//button[hasclass('o_list_button_add')]" position="replace"/>
<xpath expr="//t[contains(@t-if, 'isExportEnable')]" position="before">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<t t-set="supportedProgramTypes" t-value="['coupons', 'gift_card', 'ewallet']"/>
<button t-if="supportedProgramTypes.includes(props.context.program_type)" type="button" class="btn btn-primary o_loyalty_card_list_button_generate" t-attf-data-tooltip="Generate {{props.context.program_item_name}}"
t-on-click.stop.prevent="() => this.actionService.doAction('loyalty.loyalty_generate_wizard_action', { additionalContext: this.props.context, onClose: () => {this.model.load()} })">
@@ -10,9 +10,10 @@ import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog
import { sprintf } from "@web/core/utils/strings";
import { useModel } from "@web/views/model";
import { useService } from "@web/core/utils/hooks";
import { SearchBar } from "@web/search/search_bar/search_bar";
export class ActivityController extends Component {
static components = { Layout };
static components = { Layout, SearchBar };
static props = {
...standardViewProps,
Model: Function,
@@ -3,6 +3,9 @@
<t t-name="mail.ActivityController" owl="1">
<div t-att-class="props.className + (env.isSmall ? ' o_action_delegate_scroll': '')">
<Layout display="props.display">
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
<t t-component="props.Renderer" t-props="rendererProps" />
</Layout>
</div>
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="mrp.MrpDocumentsKanbanView.Buttons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
<div role="toolbar" position="inside">
<xpath expr="//div[hasclass('o_cp_buttons')]" position="inside">
<input type="file" multiple="true" t-ref="uploadFileInput" class="o_input_file o_hidden" t-on-change.stop="onFileInputChange"/>
<button type="button" t-attf-class="btn btn-primary o_mrp_documents_kanban_upload"
t-on-click.stop.prevent="() => this.uploadFileInputRef.el.click()">
Upload
</button>
</div>
</xpath>
</t>
</templates>
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mrp_subcontracting.PickingFormController" t-inherit="web.FormView" t-inherit-mode="primary" owl="1">
<xpath expr="//ActionMenus" position="replace"/>
<t t-set-slot="control-panel-additional-actions"/>
</t>
</templates>
@@ -10,45 +10,18 @@
<t t-name="project.ProjectControlPanelContent" owl="1">
<t t-if="showProjectUpdate">
<li t-if="isProjectUser" class="o_project_updates_breadcrumb ps-3" t-on-click="onStatusClick">
<div t-if="isProjectUser" class="o_project_updates_breadcrumb" t-on-click="onStatusClick">
<t t-call="project.ProjectControlPanelContentBadge"></t>
</li>
<li t-else="" class="o_project_updates_breadcrumb ps-3">
</div>
<div t-else="" class="o_project_updates_breadcrumb">
<t t-call="project.ProjectControlPanelContentBadge"></t>
</li>
</div>
</t>
</t>
<t t-name="project.Breadcrumbs" t-inherit="web.Breadcrumbs" t-inherit-mode="primary" owl="1">
<xpath expr="//ol" position="inside">
<t t-call="project.ProjectControlPanelContent"/>
</xpath>
</t>
<t t-name="project.Breadcrumbs.Small" t-inherit="web.Breadcrumbs.Small" t-inherit-mode="primary" owl="1">
<xpath expr="//ol" position="inside">
<t t-call="project.ProjectControlPanelContent"/>
</xpath>
</t>
<t t-name="project.ProjectControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-call='web.Breadcrumbs']" position="replace">
<t t-call="project.Breadcrumbs"/>
</xpath>
</t>
<t t-name="project.ProjectControlPanel.Small" t-inherit="web.ControlPanel.Small" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-call='web.Breadcrumbs.Small']" position="replace">
<t t-call="project.Breadcrumbs.Small"/>
</xpath>
</t>
<t t-name="project.ProjectControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
<t t-call="project.ProjectControlPanel.Regular"/>
</xpath>
<xpath expr="//t[@t-call='web.ControlPanel.Small']" position="replace">
<t t-call="project.ProjectControlPanel.Small"/>
<xpath expr="//t[@t-call='web.Breadcrumbs']" position="after">
<t t-call="project.ProjectControlPanelContent"/>
</xpath>
</t>
@@ -17,12 +17,7 @@ import { Component, onWillStart, useState, useEffect } from "@odoo/owl";
export class SpreadsheetDashboardAction extends Component {
setup() {
this.Status = Status;
this.controlPanelDisplay = {
"top-left": true,
"top-right": true,
"bottom-left": false,
"bottom-right": false,
};
this.controlPanelDisplay = {};
this.orm = useService("orm");
this.router = useService("router");
// Use the non-protected orm service (`this.env.services.orm` instead of `useService("orm")`)
@@ -63,7 +63,7 @@ export class TraceabilityReport extends Component {
});
this.display = {
controlPanel: { "top-right": false, "bottom-right": false },
controlPanel: {},
searchPanel: false,
};
}
@@ -9,10 +9,7 @@ const { Component, onWillStart, useState } = owl;
export class ReceptionReportMain extends Component {
setup() {
this.controlPanelDisplay = {
"top-right": false,
"bottom-right": false,
};
this.controlPanelDisplay = {};
this.ormService = useService("orm");
this.actionService = useService("action");
this.reportName = "stock.report_reception";
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="InventoryReport.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="(//div/*)[last()]" position="after">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button t-if="props.context.inventory_mode and !props.context.inventory_report_mode and nbSelected == 0"
class="btn btn-secondary" type="button" t-on-click="onClickApplyAll">
Apply All
@@ -2,10 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="stock.StockOrderpoint.Buttons" t-inherit="web.ListView.Buttons" owl="1">
<xpath expr="//button[hasclass('o_list_button_add')]" position="attributes">
<attribute name="class">btn btn-secondary o_list_button_add</attribute>
</xpath>
<xpath expr="//t[@t-if='props.showButtons']" position="before">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button t-if="nbSelected" type="button" t-on-click="onClickOrder"
class="o_button_order btn btn-primary me-1">
Order
@@ -2,8 +2,7 @@
<templates>
<t t-name="transifex.CodeTranslationListView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<!-- Before the export button -->
<xpath expr="//t[contains(@t-if, 'isExportEnable')]" position="before">
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button type="button" class="btn btn-primary" t-on-click="onClickReloadCodeTranslations">Reload</button>
</xpath>
</t>
@@ -29,7 +29,7 @@
t-att-aria-expanded="state.open ? 'true' : 'false'"
t-ref="togglerRef"
>
<t t-slot="toggler" />
<t t-slot="toggler" open="state.open"/>
</button>
<div
t-if="state.open"
@@ -21,7 +21,12 @@ import { useComponent, useEffect, useRef } from "@odoo/owl";
*/
const ACTIVE_MENU_ELEMENT_CLASS = "focus";
const MENU_ELEMENTS_SELECTORS = [":scope > .dropdown-item", ":scope > .dropdown"];
const MENU_ELEMENTS_SELECTORS = [
":scope > .dropdown-item",
":scope > .dropdown",
":scope > .o_dropdown_container > .dropdown-item",
":scope > .o_dropdown_container > .dropdown",
];
const NEXT_ACTIVE_INDEX_FNS = {
FIRST: () => 0,
LAST: (list) => list.length - 1,
+1 -1
View File
@@ -18,7 +18,7 @@
<span class="o_pager_limit" t-esc="props.total"/>
</t>
</span>
<span class="btn-group" aria-atomic="true">
<span class="btn-group d-print-none" aria-atomic="true">
<!-- accesskeys not wanted in X2Many widgets -->
<button type="button" class="fa fa-chevron-left btn btn-secondary o_pager_previous px-2 rounded-start" aria-label="Previous" data-tooltip="Previous" tabindex="-1" t-att-data-hotkey="props.withAccessKey ? 'p' : false" t-att-disabled="state.isDisabled or isSinglePage" t-on-click.stop="() => this.navigate(-1)" />
<button type="button" class="fa fa-chevron-right btn btn-secondary o_pager_next px-2 rounded-end" aria-label="Next" data-tooltip="Next" tabindex="-1" t-att-data-hotkey="props.withAccessKey ? 'n' : false" t-att-disabled="state.isDisabled or isSinglePage" t-on-click.stop="() => this.navigate(1)" />
+2 -2
View File
@@ -4,8 +4,8 @@
<!-- Owl Templates -->
<t t-name="web.Legacy.ControlPanel" owl="1">
<div class="o_control_panel" data-command-category="actions">
<div class="o_cp_top">
<div class="o_control_panel px-3" data-command-category="actions">
<div class="o_cp_top mb-1">
<div class="o_cp_top_left">
<ol t-if="props.withBreadcrumbs" class="breadcrumb" role="navigation">
<li t-foreach="props.breadcrumbs" t-as="bc" t-key="bc.controllerID"
@@ -197,6 +197,11 @@ $utilities: map-merge(
}
}
// Hide empty elements
.d-empty-none:empty {
display: none !important;
}
// Define a smaller font-size
.smaller {
font-size: $o-font-size-base-smaller;
@@ -2,8 +2,8 @@
<templates xml:space="preserve">
<t t-name="web.ActionMenus" owl="1">
<div class="o_cp_action_menus pe-2">
<Dropdown t-if="printItems.length" class="'d-inline-block'" togglerClass="'btn btn-light'" hotkey="'shift+u'">
<div class="o_cp_action_menus d-flex pe-2 gap-1">
<Dropdown t-if="printItems.length" class="'d-inline-block'" togglerClass="'btn btn-secondary'" hotkey="'shift+u'">
<t t-set-slot="toggler">
<i class="me-md-1 fa fa-print"/>
<span class="o_dropdown_title">Print</span>
@@ -15,7 +15,7 @@
</t>
</Dropdown>
<Dropdown t-if="actionItems.length" class="'d-inline-block'" togglerClass="'btn btn-light'" hotkey="'u'">
<Dropdown t-if="actionItems.length" class="'d-inline-block'" togglerClass="'btn btn-secondary'" hotkey="'u'">
<t t-set-slot="toggler">
<i class="me-md-1 fa fa-cog"/>
<span class="o_dropdown_title">Action</span>
@@ -0,0 +1,19 @@
/** @odoo-module **/
import { ActionMenus } from "@web/search/action_menus/action_menus";
/**
* Combined Action menus (or Action/Print bar, previously called 'Sidebar')
*
* This is a variation of the ActionMenus, combined into a single DropDown.
*
* The side bar is the group of dropdown menus located on the left side of the
* control panel. Its role is to display a list of items depending on the view
* type and selected records and to execute a set of actions on active records.
* It is made out of 2 dropdown: Print and Action.
*
* @extends Component
*/
export class CombinedActionMenus extends ActionMenus {
static template = "web.CombinedActionMenus";
}
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.CombinedActionMenus" owl="1">
<div class="o_cp_action_menus d-flex pe-2 gap-1">
<Dropdown togglerClass="'d-print-none btn btn-text ms-1 lh-sm'">
<t t-set-slot="toggler">
<i class="fa fa-cog"/>
</t>
<t t-if="printItems.length">
<div class="dropdown-menu_group dropdown-header">
<i class="me-md-1 fa fa-print"/>
Print
</div>
<t t-foreach="printItems" t-as="item" t-key="item.key">
<DropdownItem class="'o_menu_item'" onSelected="() => this.onItemSelected(item)">
<t t-esc="item.description"/>
</DropdownItem>
</t>
</t>
<t t-if="actionItems.length">
<div class="dropdown-menu_group dropdown-header">
<i class="me-md-1 fa fa-cog"/>
Action
</div>
<t t-foreach="actionItems" t-as="item" t-key="item.key">
<t t-if="item.Component" t-component="item.Component" t-props="item.props" />
<DropdownItem t-else="" class="'o_menu_item'" onSelected="() => this.onItemSelected(item)">
<t t-esc="item.description"/>
</DropdownItem>
</t>
</t>
</Dropdown>
</div>
</t>
</templates>
@@ -4,48 +4,19 @@ import { browser } from "@web/core/browser/browser";
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { Pager } from "@web/core/pager/pager";
import { useService } from "@web/core/utils/hooks";
import { ComparisonMenu } from "../comparison_menu/comparison_menu";
import { FavoriteMenu } from "../favorite_menu/favorite_menu";
import { FilterMenu } from "../filter_menu/filter_menu";
import { GroupByMenu } from "../group_by_menu/group_by_menu";
import { SearchBar } from "../search_bar/search_bar";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { useCommand } from "@web/core/commands/command_hook";
import { sprintf } from "@web/core/utils/strings";
import { Dialog } from "@web/core/dialog/dialog";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import {
Component,
useState,
onMounted,
useExternalListener,
useRef,
useEffect,
useSubEnv,
} from "@odoo/owl";
const MAPPING = {
filter: FilterMenu,
groupBy: GroupByMenu,
comparison: ComparisonMenu,
favorite: FavoriteMenu,
};
import { Component, useState, onMounted, useExternalListener, useRef, useEffect } from "@odoo/owl";
const STICKY_CLASS = "o_mobile_sticky";
export class ControlPanelSearchDialog extends Component {
setup() {
useSubEnv(this.props.env);
}
}
ControlPanelSearchDialog.template = "web.ControlPanelSearchDialog";
ControlPanelSearchDialog.props = ["close", "slots?", "display", "env", "searchMenus"];
ControlPanelSearchDialog.components = { Dialog, SearchBar };
export class ControlPanel extends Component {
setup() {
this.actionService = useService("action");
this.dialog = useService("dialog");
this.pagerProps = this.env.config.pagerProps
? useState(this.env.config.pagerProps)
: undefined;
@@ -55,6 +26,7 @@ export class ControlPanel extends Component {
this.state = useState({
showSearchBar: false,
showMobileSearch: false,
showViewSwitcher: false,
});
@@ -110,6 +82,7 @@ export class ControlPanel extends Component {
resetSearchState() {
Object.assign(this.state, {
showSearchBar: false,
showMobileSearch: false,
showViewSwitcher: false,
});
}
@@ -118,49 +91,10 @@ export class ControlPanel extends Component {
* @returns {Object}
*/
get display() {
const display = Object.assign(
{
"top-left": true,
"top-right": true,
"bottom-left": true,
"bottom-left-buttons": true,
"bottom-right": true,
},
this.props.display
);
display.top = display["top-left"] || display["top-right"];
display.bottom = display["bottom-left"] || display["bottom-right"];
return display;
}
/**
* @returns {Component[]}
*/
get searchMenus() {
const searchMenus = [];
for (const key of this.env.searchModel.searchMenuTypes) {
// look in display instead?
if (
key === "comparison" &&
this.env.searchModel.getSearchItems((i) => i.type === "comparison").length === 0
) {
continue;
}
searchMenus.push({ Component: MAPPING[key], key });
}
return searchMenus;
}
openSearchDialog() {
this.dialog.add(ControlPanelSearchDialog, {
slots: this.props.slots,
display: this.display,
searchMenus: this.searchMenus,
env: {
searchModel: this.env.searchModel,
config: this.env.config,
},
});
return {
layoutActions: true,
...this.props.display,
};
}
/**
@@ -245,10 +179,10 @@ export class ControlPanel extends Component {
}
ControlPanel.components = {
...Object.values(MAPPING),
Pager,
SearchBar,
Dropdown,
DropdownItem,
};
ControlPanel.template = "web.ControlPanel";
ControlPanel.props = {
@@ -1,211 +1,31 @@
.o_control_panel {
border-bottom: var(--ControlPanel-border-bottom, #{$o-control-panel-border-bottom});
@include o-webclient-padding($top: map-get($spacers, 2), $bottom: map-get($spacers, 2));
background-color: $o-control-panel-background-color;
> div {
display: flex;
min-height: $o-statusbar-height;
}
@include media-breakpoint-up(md) {
.o_cp_top_left, .o_cp_top_right,
.o_cp_bottom_left, .o_cp_bottom_right {
width: 50%;
}
.o_control_panel_breadcrumbs {
min-width: 200px;
}
@include media-breakpoint-down(md) {
> div.o_cp_bottom {
justify-content: space-between;
min-height: auto;
}
&.o_mobile_sticky {
@include o-position-sticky();
z-index: 10;
}
}
.o_cp_action_menus {
.o_dropdown_title {
@include visually-hidden;
}
.o_dropdown_chevron, .o_dropdown_caret {
display: none;
}
@include media-breakpoint-up(lg) {
.o_control_panel_breadcrumbs, .o_control_panel_navigation {
flex: 1
}
.o_cp_searchview {
min-height: 35px;
&.o_searchview_quick {
display: flex;
align-items: flex-start;
width: 100%;
> .o_searchview {
display: flex;
flex: 1 1 auto;
align-items: center;
border-bottom: 1px solid $o-brand-secondary;
> .o_searchview_input_container {
flex: 1 1 auto;
}
> .o_enable_searchview {
margin: 0;
padding-left: 0;
&, &:hover {
color: $gray-600;
}
}
}
}
}
.o_cp_switch_buttons {
&.show > .dropdown-menu {
display: inline-flex;
min-width: 0px;
}
.o_control_panel_actions {
min-width: MIN(500px, 33%);
}
}
.breadcrumb {
font-size: 18px;
min-width: 0;
> li {
@include o-text-overflow();
}
@include media-breakpoint-down(md) {
width: 100%;
overflow: hidden;
flex-wrap: nowrap;
> li {
margin: 0;
&:before {
display: none;
}
&.o_back_button {
flex-shrink: 0;
&, & ~ li {
margin: auto 0;
}
&:before {
font-family: FontAwesome;
content: ""; // fa-arrow-left
display: inline-block;
padding: 0; // override bootstrap
color: inherit;
}
&.btn {
padding: $o-cp-button-sm-no-border-padding;
}
> a {
display: none;
}
}
}
}
}
.o_cp_top_right {
min-height: $o-cp-breadcrumb-height;
}
.o_cp_bottom_left {
display: flex;
flex-wrap: wrap;
gap: map-get($spacers, 3) map-get($spacers, 2);
> .o_cp_action_menus {
margin-left: auto;
padding-right: 0;
@include media-breakpoint-up(md) {
padding-right: 10px;
}
@include media-breakpoint-up(sm) {
display: flex;
padding-right: 25px;
> .btn-group {
margin: auto 0;
display: block;
}
.dropdown-toggle {
margin-right: 0;
}
}
.o_hidden_input_file {
position: relative;
input.o_input_file {
position: absolute;
top: 1px;
opacity: 0;
width: 100%;
height: 26px;
}
.o_form_binary_form span {
padding: 3px 25px;
color: $o-brand-primary;
}
.o_form_binary_form:hover {
background-color: $table-hover-bg;
}
}
.o_sidebar_delete_attachment {
padding: 0px;
position: absolute;
top: 5px;
right: 10px;
}
.dropdown-toggle {
margin-right: 0;
@include media-breakpoint-up(md) {
margin-right: 15px;
}
}
}
}
.o_cp_bottom_right {
display: flex;
flex-wrap: wrap;
@include media-breakpoint-up(md) {
column-gap: $o-horizontal-padding;
}
> .o_form_status_indicator {
@include media-breakpoint-down(md) {
.o_form_status_indicator_buttons > .btn {
padding: $o-cp-button-sm-no-border-padding;
}
}
}
> .o_cp_pager {
padding-left: 5px;
text-align: center;
user-select: none;
.o_pager_limit_fetch:not(.disabled), .o_pager_value {
cursor: pointer;
}
@include media-breakpoint-up(xl) {
.o_control_panel_actions {
min-width: MIN(600px, 33%);
}
}
}
@@ -2,206 +2,95 @@
<templates xml:space="preserve">
<t t-name="web.ControlPanel" owl="1">
<div class="o_control_panel" t-ref="root">
<t t-if="env.isSmall">
<t t-call="web.ControlPanel.Small" />
</t>
<t t-else="">
<t t-call="web.ControlPanel.Regular" />
</t>
</div>
</t>
<t t-name="web.ControlPanel.Regular" owl="1">
<div t-if="display['top']" class="o_cp_top">
<div class="o_cp_top_left">
<t t-slot="control-panel-top-left" t-if="display['top-left']">
<t t-call="web.Breadcrumbs" t-if="!env.config.noBreadcrumbs"/>
</t>
</div>
<div class="o_cp_top_right">
<t t-slot="control-panel-top-right" t-if="display['top-right']">
<SearchBar/>
</t>
</div>
</div>
<div t-if="display['bottom']" class="o_cp_bottom">
<div class="o_cp_bottom_left" t-on-keydown="onBottomLeftKeydown">
<t t-slot="control-panel-bottom-left-buttons" t-if="display['bottom-left'] and display['bottom-left-buttons']" />
<t t-slot="control-panel-bottom-left" t-if="display['bottom-left']"/>
</div>
<div t-if="display['bottom-right']" class="o_cp_bottom_right">
<t t-slot="control-panel-bottom-right">
<div class="btn-group o_search_options position-static" role="search">
<t t-foreach="searchMenus" t-as="menu" t-key="menu.key">
<t t-component="menu.Component"/>
</t>
<div class="o_control_panel d-flex flex-column gap-3 gap-lg-1 px-3 pt-2 pb-3" t-ref="root">
<div class="o_control_panel_main d-flex flex-wrap flex-lg-nowrap justify-content-between align-items-lg-start gap-3">
<div class="o_control_panel_breadcrumbs d-flex align-items-center w-100 w-md-50 w-lg-auto">
<div class="o_control_panel_create_button d-empty-none d-print-none me-2">
<t t-slot="control-panel-create-button"/>
<t t-slot="control-panel-always-buttons"/>
</div>
</t>
<div t-if="pagerProps and pagerProps.total > 0" class="o_cp_pager" role="search">
<Pager t-props="pagerProps"/>
<t t-if="env.config.noBreadcrumbs">
<section class="o_control_panel_breadcrumbs_actions d-contents">
<t t-slot="control-panel-additional-actions"/>
<t t-slot="control-panel-status-indicator" />
</section>
</t>
<t t-else="">
<t t-call="web.Breadcrumbs"/>
</t>
<span class="d-none d-xl-block me-auto"/> <!-- Spacer -->
</div>
<t t-if="env.config.viewSwitcherEntries and env.config.viewSwitcherEntries.length > 1">
<nav class="btn-group o_cp_switch_buttons ms-auto">
<div t-if="display.layoutActions" class="o_control_panel_actions d-flex align-items-center justify-content-around w-100 w-lg-auto order-3 order-lg-2">
<t t-slot="layout-actions"/>
</div>
<div class="o_control_panel_navigation d-flex flex-wrap flex-md-nowrap align-items-center justify-content-between justify-content-md-end w-100 w-md-auto gap-3 gap-lg-1 gap-xl-3 order-2 order-lg-3">
<span class="d-none d-xl-block ms-auto"/> <!-- Spacer -->
<div t-if="pagerProps and pagerProps.total > 0" class="o_cp_pager text-nowrap order-2 order-md-1" role="search">
<Pager t-props="pagerProps"/>
</div>
<nav t-if="env.config.viewSwitcherEntries?.length > 1"
class="o_cp_switch_buttons d-print-none btn-group order-1"
t-att-class="{'w-100 w-sm-auto': env.config.viewSwitcherEntries?.length > 5}">
<t t-foreach="env.config.viewSwitcherEntries" t-as="view" t-key="view.type">
<button class="btn btn-light o_switch_view "
<button class="btn btn-secondary o_switch_view "
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
t-att-data-tooltip="view.name"
t-on-click="() => this.onViewClicked(view.type)"
/>
</t>
</nav>
</t>
</div>
</div>
<div class="o_control_panel_secondary d-flex gap-2">
<t t-slot="layout-buttons"/>
</div>
</div>
</t>
<t t-name="web.ControlPanel.Small" owl="1">
<div t-if="display['top']" class="o_cp_top">
<t t-if="display['top-left'] and !state.showSearchBar">
<t t-slot="control-panel-top-left">
<t t-call="web.Breadcrumbs.Small" />
</t>
</t>
<t t-if="display['top-right']">
<button type="button" class="o_enable_searchview btn btn-link"
t-att-class="state.showSearchBar ? 'fa fa-arrow-left' : 'oi oi-search'"
t-on-click="() => state.showSearchBar = !state.showSearchBar"
/>
<t t-if="state.showSearchBar or !display['top-left']">
<t t-slot="control-panel-top-right">
<SearchBar />
<button
type="button"
class="o_toggle_searchview_full btn fa fa-filter"
t-on-click="() => this.openSearchDialog()"
/>
</t>
</t>
</t>
</div>
<div t-if="display['bottom']" class="o_cp_bottom">
<div t-if="display['bottom-left']" class="o_cp_bottom_left">
<t t-slot="control-panel-bottom-left-buttons" t-if="display['bottom-left-buttons']"/>
<t t-slot="control-panel-bottom-left"/>
</div>
<div t-if="display['bottom-right']" class="o_cp_bottom_right">
<div t-if="pagerProps and pagerProps.total > 0" class="o_cp_pager" role="search">
<Pager t-props="pagerProps"/>
</div>
<t t-if="env.config.viewSwitcherEntries and env.config.viewSwitcherEntries.length > 1">
<nav class="btn-group o_cp_switch_buttons">
<t t-set="view" t-value="env.config.viewSwitcherEntries.find((v) => v.active)" />
<Dropdown
position="'bottom-end'"
menuClass="'d-inline-flex'"
togglerClass="'btn btn-link'"
>
<t t-set-slot="toggler">
<i
class="fa-lg o_switch_view"
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
/>
</t>
<t t-foreach="env.config.viewSwitcherEntries" t-as="view" t-key="view.type">
<button class="btn btn-light fa-lg o_switch_view"
t-attf-class="o_{{view.type}} {{view.icon}} {{view.active ? 'active' : ''}}"
t-att-data-tooltip="view.name"
t-on-click="() => this.onViewClicked(view.type)"
/>
</t>
</Dropdown>
</nav>
</t>
<t t-slot="control-panel-bottom-right"/>
</div>
</div>
</t>
<t t-name="web.ControlPanelSearchDialog" owl="1">
<Dialog>
<div class="o_searchview o_mobile_search">
<div class="o_mobile_search_header">
<button
type="button"
class="o_mobile_search_button btn"
t-on-click="() => this.props.close()"
>
<i class="fa fa-arrow-left"/>
<strong class="ms-2">FILTER</strong>
</button>
<button
type="button"
class="o_mobile_search_button btn"
t-on-click="() => env.searchModel.clearQuery()"
>
CLEAR
</button>
</div>
<div class="o_mobile_search_content">
<t t-if="props.display['top-right']">
<t t-slot="control-panel-top-right">
<SearchBar/>
</t>
</t>
<t t-if="props.display['bottom-right']">
<t t-slot="control-panel-bottom-right">
<div class="o_mobile_search_filter o_search_options">
<t t-foreach="props.searchMenus" t-as="menu" t-key="menu.key">
<t t-component="menu.Component"/>
</t>
</div>
</t>
</t>
</div>
<button
type="button"
class="btn btn-primary o_mobile_search_footer"
t-on-click="() => this.props.close()"
>
SEE RESULT
</button>
</div>
</Dialog>
</t>
<t t-name="web.Breadcrumbs" owl="1">
<ol class="breadcrumb">
<t t-foreach="breadcrumbs" t-as="breadcrumb" t-key="breadcrumb.jsId">
<t t-set="isPenultimate" t-value="breadcrumb_index === breadcrumbs.length - 2"/>
<li t-if="!breadcrumb_last" class="breadcrumb-item" t-att-data-hotkey="isPenultimate and 'b'" t-att-class="{ o_back_button: isPenultimate}" t-on-click.prevent="() => this.onBreadcrumbClicked(breadcrumb.jsId)">
<a href="#">
<t t-if="breadcrumb.name" t-esc="breadcrumb.name"/>
<em t-else="" class="text-warning">Unnamed</em>
</a>
<t t-set="currentBreadcrumbs" t-value="breadcrumbs.slice(-1)"/>
<t t-set="visiblePathBreadcrumbs" t-value="breadcrumbs.slice(-3, -1)"/>
<t t-set="collapsedBreadcrumbs" t-value="breadcrumbs.slice(0, -3)"/>
<t t-set="breadcrumb" t-value="currentBreadcrumbs[0] || {}"/>
<div t-if="collapsedBreadcrumbs.length || visiblePathBreadcrumbs.length" class="d-flex flex-column align-self-stretch justify-content-between min-w-0">
<ol class="breadcrumb flex-nowrap text-nowrap small lh-1">
<li t-if="collapsedBreadcrumbs.length" class="breadcrumb-item d-inline-flex min-w-0">
<Dropdown togglerClass="'btn btn-light btn-sm p-0'">
<t t-set-slot="toggler">
<i class="fa fa-ellipsis-h"/>
</t>
<t t-foreach="collapsedBreadcrumbs" t-as="breadcrumb" t-key="breadcrumb.jsId">
<DropdownItem onSelected="() => this.onBreadcrumbClicked(breadcrumb.jsId)">
<t t-call="web.Breadcrumb.Name"/>
</DropdownItem>
</t>
</Dropdown>
</li>
<li t-else="" class="breadcrumb-item active d-flex align-items-center">
<span class="text-truncate" t-if="breadcrumb.name" t-esc="breadcrumb.name"/>
<em t-else="" class="text-warning">Unnamed</em>
<t t-slot="control-panel-status-indicator" />
</li>
</t>
</ol>
<t t-foreach="visiblePathBreadcrumbs" t-as="breadcrumb" t-key="breadcrumb.jsId">
<li class="breadcrumb-item d-inline-flex min-w-0" t-att-data-hotkey="breadcrumb_last and 'b'" t-on-click.prevent="() => this.onBreadcrumbClicked(breadcrumb.jsId)">
<a href="#" class="fw-bold text-truncate" t-att-data-tooltip="'Back to &quot;' + breadcrumb.name + '&quot;'"><t t-call="web.Breadcrumb.Name"/></a>
</li>
</t>
</ol>
<div class="active d-flex gap-2 align-items-center min-w-0 lh-sm">
<span class="min-w-0 text-truncate" t-call="web.Breadcrumb.Name"/>
</div>
</div>
<div t-else="" class="active d-flex fs-4 min-w-0">
<span class="min-w-0 text-truncate" t-call="web.Breadcrumb.Name"/>
</div>
<t t-slot="control-panel-additional-actions"/>
<t t-slot="control-panel-status-indicator" />
</t>
<t t-name="web.Breadcrumbs.Small" owl="1">
<ol class="breadcrumb">
<t t-if="breadcrumbs.length > 1">
<t t-set="breadcrumb" t-value="breadcrumbs[breadcrumbs.length - 2]" />
<li class="breadcrumb-item o_back_button btn btn-secondary"
t-on-click.prevent="() => this.onBreadcrumbClicked(breadcrumb.jsId)"
/>
</t>
<li t-if="breadcrumbs.length > 0" class="breadcrumb-item active">
<t t-set="breadcrumb" t-value="breadcrumbs[breadcrumbs.length - 1]" />
<t t-if="breadcrumb.name" t-esc="breadcrumb.name"/>
<em t-else="" class="text-warning">Unnamed</em>
</li>
</ol>
<t t-name="web.Breadcrumb.Name" owl="1">
<t t-if="breadcrumb.name" t-out="breadcrumb.name"/>
<em t-else="" class="text-warning">Unnamed</em>
</t>
</templates>
+1 -7
View File
@@ -26,8 +26,6 @@ export class Layout extends Component {
}
get controlPanelSlots() {
const slots = { ...this.props.slots };
slots["control-panel-bottom-left-buttons"] = slots["layout-buttons"];
delete slots["layout-buttons"];
delete slots.default;
return slots;
}
@@ -38,11 +36,7 @@ export class Layout extends Component {
}
return {
...this.props.display,
controlPanel: {
...controlPanel,
"top-left": false,
"bottom-left-buttons": false,
},
controlPanel,
};
}
}
@@ -8,8 +8,16 @@ import { useAutofocus, useBus, useService } from "@web/core/utils/hooks";
import { DomainSelectorDialog } from "@web/core/domain_selector_dialog/domain_selector_dialog";
import { fuzzyTest } from "@web/core/utils/search";
import { _t } from "@web/core/l10n/translation";
import { SearchBarMenu } from "../search_bar_menu/search_bar_menu";
import { Component, useExternalListener, useRef, useState } from "@odoo/owl";
import {
Component,
onMounted,
onWillUnmount,
useExternalListener,
useRef,
useState,
} from "@odoo/owl";
const parsers = registry.category("parsers");
const CHAR_FIELDS = ["char", "html", "many2many", "many2one", "one2many", "text", "properties"];
@@ -29,6 +37,7 @@ export class SearchBar extends Component {
expanded: [],
focusedIndex: 0,
query: "",
showSearchBar: !this.env.isSmall,
});
// derived state
@@ -363,9 +372,9 @@ export class SearchBar extends Component {
// Handlers
//---------------------------------------------------------------------
onFacetClick(target, facet) {
onFacetLabelClick(target, facet) {
const { domain, groupId } = facet;
if (!target.classList.contains("o_searchview_facet_label") || !domain) {
if (!domain) {
return;
}
const { resModel } = this.env.searchModel;
@@ -543,6 +552,10 @@ export class SearchBar extends Component {
}
}
onToggleSearchBar() {
this.state.showSearchBar = !this.state.showSearchBar;
}
/**
* @param {MouseEvent} ev
*/
@@ -563,4 +576,7 @@ export class SearchBar extends Component {
}
SearchBar.template = "web.SearchBar";
SearchBar.components = {
SearchBarMenu,
};
SearchBar.props = {};
@@ -2,16 +2,12 @@
// ============================================================================
.o_searchview_facet {
cursor: default;
.o_facet_values {
border: $border-width solid var(--SearchBar-facet-background, #{$o-brand-odoo});
@keyframes animate {
from {
opacity: .5;
transform: translateY(20%);
}
}
.o_searchview_facet_label {
background: var(--SearchBar-facet-background, #{$o-brand-odoo});
color: var(--SearchBar-facet-color, #{$o-white});
}
&.o_facet_with_domain:hover .o_searchview_facet_label {
cursor: pointer;
background: var(--SearchBar-facet-background, $primary);
}
}
animation: animate .25s ease-out;
}
@@ -3,41 +3,45 @@
<t t-name="web.SearchBar.Facets" owl="1">
<t t-foreach="env.searchModel.facets" t-as="facet" t-key="facet_index">
<div class="o_searchview_facet"
<div class="o_searchview_facet d-inline-flex align-items-stretch text-nowrap position-relative"
t-att-class="{o_facet_with_domain: facet.domain }"
role="img"
role="listitem"
aria-label="search"
tabindex="0"
t-on-keydown="ev => this.onFacetKeydown(facet, facet_index, ev)"
t-on-click="(ev) => this.onFacetClick(ev.target, facet)"
>
<t t-if="facet.icon">
<span t-attf-class="o_searchview_facet_label {{ facet.icon }}"/>
</t>
<t t-else="">
<span class="o_searchview_facet_label" t-esc="facet.title"/>
</t>
<div class="o_facet_values border-start-0">
<t t-foreach="facet.values" t-as="facetValue" t-key="facetValue_index">
<t t-if="!facetValue_first">
<span class="o_facet_values_sep" t-esc="facet.separator"/>
</t>
<span class="o_facet_value" t-esc="facetValue"/>
</t>
<div class="o_searchview_facet_label rounded-start-2"
t-on-click="(ev) => this.onFacetLabelClick(ev.target, facet)"
t-att-role="facet.type != 'groupBy' ? 'button' : 'img'"
t-att-class="{
'text-bg-action d-flex align-items-center': facet.type == 'groupBy',
'btn btn-primary rounded-end-0 p-0': facet.type == 'field' || facet.type == 'filter',
'btn btn-favourite rounded-end-0 p-0': facet.type == 'favorite'
}"
>
<i t-if="facet.icon" class="px-1 small" t-att-class="facet.icon" role="image"/>
<small t-else="" class="px-2" t-esc="facet.title"/>
</div>
<div class="o_facet_values d-flex align-items-center ps-2 bg-200 rounded-end-2">
<t t-foreach="facet.values" t-as="facetValue" t-key="facetValue_index">
<em t-if="!facetValue_first" class="o_facet_values_sep small fw-bold mx-1 opacity-50" t-esc="facet.separator"/>
<small class="o_facet_value" t-esc="facetValue"/>
</t>
<button class="o_facet_remove oi oi-close btn btn-link py-0 px-2 opacity-50 opacity-100-hover text-900"
role="button"
aria-label="Remove"
title="Remove"
t-on-click="() => this.onFacetRemove(facet)"
/>
</div>
<i class="o_facet_remove oi oi-close btn btn-link opacity-50 opacity-100-hover text-900"
role="img"
aria-label="Remove"
title="Remove"
t-on-click="() => this.onFacetRemove(facet)"
/>
</div>
</t>
</t>
<t t-name="web.SearchBar.Input" owl="1">
<input type="text"
class="o_searchview_input"
class="o_searchview_input d-print-none flex-grow-1 w-auto border-0"
accesskey="Q"
placeholder="Search..."
role="searchbox"
@@ -76,14 +80,14 @@
</t>
<t t-name="web.SearchBar" owl="1">
<div class="o_cp_searchview d-flex flex-grow-1" role="search" t-ref="root">
<div class="o_searchview pb-1 align-self-center border-bottom flex-grow-1" role="search" aria-autocomplete="list">
<i class="o_searchview_icon oi oi-search"
<div t-if="state.showSearchBar" class="o_cp_searchview d-flex input-group" role="search" t-ref="root">
<div class="o_searchview form-control d-print-contents d-flex align-items-center py-1 border-end-0" role="search" aria-autocomplete="list">
<i class="o_searchview_icon d-print-none oi oi-search me-2"
role="img"
aria-label="Search..."
title="Search..."
/>
<div class="o_searchview_input_container">
<div class="o_searchview_input_container d-flex flex-grow-1 flex-wrap gap-1">
<t t-call="web.SearchBar.Facets"/>
<t t-call="web.SearchBar.Input"/>
<t t-if="items.length">
@@ -91,7 +95,13 @@
</t>
</div>
</div>
<SearchBarMenu/>
</div>
<t t-if="env.isSmall" t-portal="'.o_control_panel_navigation'">
<button class="btn btn-secondary" t-on-click="onToggleSearchBar">
<i class="fa fa-fw fa-search"/>
</button>
</t>
</t>
</templates>
@@ -0,0 +1,210 @@
/** @odoo-module **/
import { AdvancedSearchDialog } from "@web/search/filter_menu/advanced_search_dialog";
import { Component } from "@odoo/owl";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { Domain } from "@web/core/domain";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { registry } from "@web/core/registry";
import { sortBy } from "@web/core/utils/arrays";
import { useBus, useService } from "@web/core/utils/hooks";
import { CustomGroupByItem } from "@web/search/group_by_menu/custom_group_by_item";
import { SearchDropdownItem } from "@web/search/search_dropdown_item/search_dropdown_item";
import { FACET_ICONS, GROUPABLE_TYPES } from "@web/search/utils/misc";
const favoriteMenuRegistry = registry.category("favoriteMenu");
export class SearchBarMenu extends Component {
static template = "web.SearchBarMenu";
static components = {
Dropdown,
DropdownItem,
SearchDropdownItem,
CustomGroupByItem,
};
static props = {};
setup() {
this.facet_icons = FACET_ICONS;
// Filter
this.dialogService = useService("dialog");
this.notification = useService("notification");
// GroupBy
const fields = [];
for (const [fieldName, field] of Object.entries(this.env.searchModel.searchViewFields)) {
if (this.validateField(fieldName, field)) {
fields.push(Object.assign({ name: fieldName }, field));
}
}
this.fields = sortBy(fields, "string");
// Favorite
this.dialogService = useService("dialog");
useBus(this.env.searchModel, "update", this.render);
}
// Filter Panel
get filterItems() {
return this.env.searchModel.getSearchItems((searchItem) =>
["filter", "dateFilter"].includes(searchItem.type)
);
}
onAdvancedSearchClick() {
const domains = [this.env.searchModel._getDomain({ withGlobal: false })];
if (this.env.searchModel.comparison && !this.env.searchModel.globalComparison) {
const { range } = this.env.searchModel.getFullComparison();
domains.push(range);
}
const domain = Domain.and(domains).toString();
this.dialogService.add(AdvancedSearchDialog, {
domain,
onConfirm: (domain) => this.onConfirm(domain),
resModel: this.env.searchModel.resModel,
isDebugMode: !!this.env.debug,
});
}
async onConfirm(domain) {
const isValid = await this.env.searchModel.isValidDomain(domain);
if (!isValid) {
this.notification.add(this.env._t("Domain is invalid. Please correct it"), {
type: "danger",
});
return false;
}
this.env.searchModel.createAdvancedDomain(domain);
return true;
}
/**
* @param {Object} param0
* @param {number} param0.itemId
* @param {number} [param0.optionId]
*/
onFilterSelected({ itemId, optionId }) {
if (optionId) {
this.env.searchModel.toggleDateFilter(itemId, optionId);
} else {
this.env.searchModel.toggleSearchItem(itemId);
}
}
// GroupBy Panel
/**
* @returns {boolean}
*/
get hideCustomGroupBy() {
return this.env.searchModel.hideCustomGroupBy || false;
}
/**
* @returns {Object[]}
*/
get groupByItems() {
return this.env.searchModel.getSearchItems((searchItem) =>
["groupBy", "dateGroupBy"].includes(searchItem.type)
);
}
/**
* @param {string} fieldName
* @param {Object} field
* @returns {boolean}
*/
validateField(fieldName, field) {
const { sortable, store, type } = field;
return (
(type === "many2many" ? store : sortable) &&
fieldName !== "id" &&
GROUPABLE_TYPES.includes(type)
);
}
/**
* @param {Object} param0
* @param {number} param0.itemId
* @param {number} [param0.optionId]
*/
onGroupBySelected({ itemId, optionId }) {
if (optionId) {
this.env.searchModel.toggleDateGroupBy(itemId, optionId);
} else {
this.env.searchModel.toggleSearchItem(itemId);
}
}
/**
* @param {string} fieldName
*/
onAddCustomGroup(fieldName) {
this.env.searchModel.createNewGroupBy(fieldName);
}
// Comparison Panel
get showComparisonMenu() {
return (
this.env.searchModel.searchMenuTypes.has("comparison") &&
this.env.searchModel.getSearchItems((i) => i.type === "comparison").length > 0
);
}
get comparisonItems() {
return this.env.searchModel.getSearchItems(
(searchItem) => searchItem.type === "comparison"
);
}
/**
* @param {number} itemId
*/
onComparisonSelected(itemId) {
this.env.searchModel.toggleSearchItem(itemId);
}
// Favorite Panel
/**
* @returns {Array}
*/
get favoriteItems() {
const favorites = this.env.searchModel.getSearchItems(
(searchItem) => searchItem.type === "favorite"
);
const registryMenus = [];
for (const item of favoriteMenuRegistry.getAll()) {
if ("isDisplayed" in item ? item.isDisplayed(this.env) : true) {
registryMenus.push({
Component: item.Component,
groupNumber: item.groupNumber,
key: item.Component.name,
});
}
}
return [...favorites, ...registryMenus];
}
/**
* @param {number} itemId
*/
onFavoriteSelected(itemId) {
this.env.searchModel.toggleSearchItem(itemId);
}
/**
* @param {number} itemId
*/
openConfirmationDialog(itemId) {
const { userId } = this.favoriteItems.find((item) => item.id === itemId);
const dialogProps = {
title: this.env._t("Warning"),
body: userId
? this.env._t("Are you sure that you want to remove this filter?")
: this.env._t(
"This filter is global and will be removed for everybody if you continue."
),
confirm: () => this.env.searchModel.deleteFavorite(itemId),
cancel: () => {},
};
this.dialogService.add(ConfirmationDialog, dialogProps);
}
}
@@ -0,0 +1,7 @@
// SearchBar Menu
// ============================================================================
.o_search_bar_menu {
.o_dropdown_container {
border-color: $dropdown-divider-bg !important;
}
}
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.SearchBarMenu" owl="1">
<Dropdown togglerClass="'o_searchview_dropdown_toggler d-print-none btn btn-outline-secondary o-no-caret rounded-start-0 h-100'"
menuClass="'o_search_bar_menu d-flex flex-wrap flex-lg-nowrap mt-2 py-3'"
position="'bottom-end'"
t-if="this.env.searchModel.searchMenuTypes.size">
<t t-set-slot="toggler" t-slot-scope="scope">
<i class="fa fa-caret-down" aria-hidden="true"/>
</t>
<!-- Filter -->
<t t-if="this.env.searchModel.searchMenuTypes.has('filter')">
<div class="o_dropdown_container o_filter_menu w-100 w-lg-auto px-3 mb-4 mb-lg-0 border-end">
<div class="px-3 fs-5 mb-2">
<i class="me-2 text-primary" t-att-class="facet_icons.filter"/>
<h5 class="o_dropdown_title d-inline">Filters</h5>
</div>
<t t-set="currentGroup" t-value="null"/>
<t t-foreach="filterItems" t-as="item" t-key="item.id">
<t t-if="currentGroup !== null and currentGroup !== item.groupNumber">
<div class="dropdown-divider" role="separator"/>
</t>
<t t-if="item.options">
<Dropdown togglerClass="'o_menu_item' + (item.isActive ? ' selected' : '')">
<t t-set-slot="toggler">
<t t-esc="item.description"/>
</t>
<t t-set="subGroup" t-value="null"/>
<t t-foreach="item.options" t-as="option" t-key="option.id">
<t t-if="subGroup !== null and subGroup !== option.groupNumber">
<div class="dropdown-divider" role="separator"/>
</t>
<SearchDropdownItem class="{ o_item_option: true, selected: option.isActive }"
t-esc="option.description"
checked="option.isActive"
parentClosingMode="'none'"
onSelected="() => this.onFilterSelected({ itemId: item.id, optionId: option.id })"
/>
<t t-set="subGroup" t-value="option.groupNumber"/>
</t>
</Dropdown>
</t>
<t t-else="">
<SearchDropdownItem class="{ o_menu_item: true, selected: item.isActive }"
checked="item.isActive"
parentClosingMode="'none'"
t-esc="item.description"
onSelected="() => this.onFilterSelected({ itemId: item.id })"
/>
</t>
<t t-set="currentGroup" t-value="item.groupNumber"/>
</t>
<t t-if="filterItems.length">
<div role="separator" class="dropdown-divider"/>
</t>
<DropdownItem onSelected.bind="onAdvancedSearchClick">Advanced Search</DropdownItem>
</div>
</t>
<!-- GroupBy -->
<t t-if="this.env.searchModel.searchMenuTypes.has('groupBy')">
<div class="o_dropdown_container o_group_by_menu w-100 w-lg-auto px-3 mb-4 mb-lg-0 border-end">
<div class="px-3 fs-5 mb-2">
<i class="me-2 text-action" t-att-class="facet_icons.groupBy"/>
<h5 class="o_dropdown_title d-inline">Group By</h5>
</div>
<t t-set="currentGroup" t-value="null"/>
<t t-foreach="groupByItems" t-as="item" t-key="item.id">
<t t-if="currentGroup !== null and currentGroup !== item.groupNumber">
<div class="dropdown-divider" role="separator"/>
</t>
<t t-if="item.options">
<Dropdown togglerClass="'o_menu_item' + (item.isActive ? ' selected' : '')">
<t t-set-slot="toggler">
<t t-esc="item.description"/>
</t>
<t t-set="subGroup" t-value="null"/>
<t t-foreach="item.options" t-as="option" t-key="option.id">
<t t-if="subGroup !== null and subGroup !== option.groupNumber">
<div class="dropdown-divider" role="separator"/>
</t>
<SearchDropdownItem class="{ o_item_option: true, selected: option.isActive }"
checked="option.isActive ? true : false"
parentClosingMode="'none'"
t-esc="option.description"
onSelected="() => this.onGroupBySelected({ itemId: item.id, optionId: option.id})"
/>
<t t-set="subGroup" t-value="option.groupNumber"/>
</t>
</Dropdown>
</t>
<t t-else="">
<SearchDropdownItem class="{ o_menu_item: true, selected: item.isActive }"
checked="item.isActive"
parentClosingMode="'none'"
t-esc="item.description"
onSelected="() => this.onGroupBySelected({ itemId: item.id })"
/>
</t>
<t t-set="currentGroup" t-value="item.groupNumber"/>
</t>
<t t-if="!hideCustomGroupBy and fields.length">
<div t-if="groupByItems.length" role="separator" class="dropdown-divider"/>
<CustomGroupByItem fields="fields" onAddCustomGroup.bind="onAddCustomGroup"/>
</t>
</div>
</t>
<!-- Comparison -->
<t t-if="showComparisonMenu">
<div class="o_dropdown_container o_comparison_menu w-100 w-lg-auto px-3 border-end">
<div class="px-3 fs-5 mb-2">
<i class="me-2 text-danger" t-att-class="facet_icons.comparison"/>
<h5 class="o_dropdown_title d-inline">Comparison</h5>
</div>
<t t-foreach="comparisonItems" t-as="item" t-key="item.id">
<SearchDropdownItem class="{ o_menu_item: true, selected: item.isActive }"
checked="item.isActive"
parentClosingMode="'none'"
t-esc="item.description"
onSelected="() => this.onComparisonSelected(item.id)"
/>
</t>
</div>
</t>
<!-- Favorite -->
<t t-if="this.env.searchModel.searchMenuTypes.has('favorite')">
<div class="o_dropdown_container o_favorite_menu w-100 w-lg-auto px-3">
<div class="px-3 fs-5 mb-2">
<i class="me-2 text-favourite" t-att-class="facet_icons.favorite"/>
<h5 class="o_dropdown_title d-inline">Favorites</h5>
</div>
<t t-set="currentGroup" t-value="null"/>
<t t-foreach="favoriteItems" t-as="item" t-key="item.id or item.key">
<t t-if="currentGroup !== null and currentGroup !== item.groupNumber">
<div role="separator" class="dropdown-divider"/>
</t>
<t t-if="item.type ==='favorite'">
<SearchDropdownItem class="{ o_menu_item: true, selected: item.isActive }"
checked="item.isActive"
parentClosingMode="'none'"
onSelected="() => this.onFavoriteSelected(item.id)"
>
<span class="d-flex p-0 align-items-center justify-content-between">
<t t-esc="item.description"/>
<i class="ms-1 o_icon_right fa fa-trash-o"
title="Delete item"
t-on-click.stop="() => this.openConfirmationDialog(item.id)"
/>
</span>
</SearchDropdownItem>
</t>
<t t-else="">
<t t-component="item.Component"/>
</t>
<t t-set="currentGroup" t-value="item.groupNumber"/>
</t>
</div>
</t>
</Dropdown>
</t>
</templates>
+3 -1
View File
@@ -16,7 +16,8 @@ import {
rankInterval,
yearSelected,
} from "./utils/dates";
import { FACET_ICONS } from "./utils/misc";
import { FACET_ICONS, FACET_COLORS } from "./utils/misc";
import { EventBus, toRaw } from "@odoo/owl";
import { toDomain, toTree } from "@web/core/domain_tree";
import { extractPathsFromDomain } from "@web/core/domain_selector/utils";
@@ -1752,6 +1753,7 @@ export class SearchModel extends EventBus {
facet.title = title;
} else {
facet.icon = FACET_ICONS[type];
facet.color = FACET_COLORS[type];
}
if (groupActiveItemDomains.length) {
facet.domain = Domain.or(groupActiveItemDomains).toString();
@@ -1,80 +1,28 @@
.o_searchview {
align-items: flex-end;
padding: 0 20px 1px 0;
position: relative;
background-color: $input-focus-bg;
border: $input-border-width solid $border-color;
@include media-breakpoint-up(md) {
border-bottom: 1px solid $border-color;
padding-bottom: 3px;
&:focus-within {
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
color: $input-focus-color;
&:focus-within {
border-bottom-color: $primary;
}
}
@include media-breakpoint-down(md) {
margin-right: 5px; // o_switch_view_button_icon alignment
padding-right: 0;
}
.o_searchview_input_container {
display: flex;
flex-flow: row wrap;
position: relative;
}
.o_searchview_facet {
display: flex;
flex: 0 0 auto;
margin: 1px 3px 0 0;
max-width: 100%;
position: relative;
$o-searchview-facet-remove-width: 18px;
@include media-breakpoint-down(md) {
flex-flow: row wrap;
}
.o_searchview_facet_label {
align-items: center;
flex: 0 0 auto;
padding: 0 3px;
@include o-text-overflow($display: flex);
}
.o_facet_values {
direction: ltr#{"/*rtl:ignore*/"};
padding: 0 $o-searchview-facet-remove-width 0 5px;
.o_facet_values_sep {
font-style: italic;
margin: 0 0.3rem;
}
}
.o_facet_remove {
align-items: center;
cursor: pointer;
display: flex;
flex: 0 0 auto;
justify-content: center;
width: $o-searchview-facet-remove-width;
@include o-position-absolute(0, 0, 0);
@include o-hover-text-color($text-muted, $o-brand-odoo);
+ .o-dropdown .o_searchview_dropdown_toggler {
border-top-color: $input-focus-border-color;
border-right-color: $input-focus-border-color;
border-bottom-color: $input-focus-border-color;
}
}
.o_searchview_input {
flex: 1 0 auto;
width: 75px;
border: none;
outline: none;
background-color: var(--o-input-background-color, #{$input-bg});
appearance: none;
color: inherit;
background: inherit;
}
.o_searchview_autocomplete {
@include o-position-absolute(calc(100% + #{$border-width * 3}), 0);
width: 100%;
@include o-position-absolute(100%, $left: auto);
.o_menu_item {
align-items: center;
@@ -97,10 +45,6 @@
}
}
}
.o_searchview_icon {
@include o-position-absolute($top: 4px, $right: 0);
}
}
.o_search_options {
@@ -7,6 +7,13 @@ export const FACET_ICONS = {
comparison: "fa fa-adjust",
};
export const FACET_COLORS = {
filter: "primary",
groupBy: "action",
favorite: "warning",
comparison: "danger",
};
export const GROUPABLE_TYPES = [
"boolean",
"char",
@@ -13,6 +13,7 @@ import { CalendarDatePicker } from "./date_picker/calendar_date_picker";
import { CalendarFilterPanel } from "./filter_panel/calendar_filter_panel";
import { CalendarMobileFilterPanel } from "./mobile_filter_panel/calendar_mobile_filter_panel";
import { CalendarQuickCreate } from "./quick_create/calendar_quick_create";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { Component, useState } from "@odoo/owl";
@@ -221,5 +222,6 @@ CalendarController.components = {
QuickCreate: CalendarQuickCreate,
Layout,
ViewScaleSelector,
SearchBar,
};
CalendarController.template = "web.CalendarController";
@@ -7,7 +7,9 @@
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
<div class="o_calendar_container">
<MobileFilterPanel t-if="env.isSmall" t-props="mobileFilterPanelProps" />
<div t-if="showSideBar" class="o_calendar_sidebar_container d-md-block">
@@ -13,7 +13,7 @@
isDebugMode="!!env.debug"
className="props.readonly ? 'o_read_mode' : 'o_edit_mode'"
/>
<div class="o_field_domain_panel" style="height:22px;">
<div class="o_field_domain_panel d-flex align-items-center gap-2 mt-1">
<i class="fa fa-arrow-right" role="img" aria-label="Domain" title="Domain" />
<t t-if="state.isValid === null and state.recordCount === null">
<i class="fa fa-circle-o-notch fa-spin ms-2" role="img" aria-label="Loading" title="Loading" />
@@ -1,6 +0,0 @@
/** @odoo-module **/
import { ControlPanel } from "@web/search/control_panel/control_panel";
export class FormControlPanel extends ControlPanel {}
FormControlPanel.template = "web.FormControlPanel";
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.FormControlPanel" owl="1">
<div class="o_control_panel" t-ref="root">
<div t-if="display['top']" class="o_cp_top" t-att-class="{ 'flex-wrap': env.isSmall }">
<div class="o_cp_top_left d-flex flex-grow-1 align-items-center" t-att-class="{ 'w-100': env.isSmall }">
<t t-if="display['top-left']">
<t t-slot="control-panel-breadcrumb">
<t t-if="env.isSmall">
<t t-call="web.Breadcrumbs.Small" t-if="!env.config.noBreadcrumbs"/>
</t>
<t t-else="">
<t t-call="web.Breadcrumbs" t-if="!env.config.noBreadcrumbs"/>
</t>
</t>
</t>
</div>
<div class="o_cp_bottom_right w-auto flex-shrink-0 justify-content-between align-items-center"
t-att-class="{ 'flex-grow-1' : env.isSmall }">
<t t-if="env.isSmall">
<t t-slot="control-panel-status-indicator" />
</t>
<t t-slot="control-panel-action-menu" t-if="display['bottom-left']"/>
<div t-if="pagerProps and pagerProps.total > 0" class="o_cp_pager" role="search">
<Pager t-props="pagerProps"/>
</div>
<t t-slot="control-panel-create-button" />
</div>
</div>
</div>
</t>
</templates>
@@ -9,7 +9,7 @@ import { SIZES } from "@web/core/ui/ui_service";
import { useBus, useService } from "@web/core/utils/hooks";
import { omit } from "@web/core/utils/objects";
import { createElement } from "@web/core/utils/xml";
import { ActionMenus } from "@web/search/action_menus/action_menus";
import { CombinedActionMenus } from "@web/search/combined_action_menus/combined_action_menus";
import { Layout } from "@web/search/layout";
import { usePager } from "@web/search/pager_hook";
import { useModel } from "@web/views/model";
@@ -496,7 +496,7 @@ export class FormController extends Component {
FormController.template = `web.FormView`;
FormController.components = {
ActionMenus,
CombinedActionMenus,
FormStatusIndicator,
Layout,
ButtonBox,
@@ -5,17 +5,32 @@
<div t-att-class="className" t-ref="root">
<div class="o_form_view_container">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="display">
<t t-set-slot="layout-buttons">
<t t-if="footerArchInfo and env.inDialog">
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="footerArchInfo" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons"/>
</t>
<t t-else="">
<t t-call="{{ props.buttonTemplate }}"/>
<t t-set-slot="control-panel-create-button">
<t t-if="canCreate">
<button type="button" class="btn btn-secondary o_form_button_create" data-hotkey="c" t-on-click.stop="create" t-att-disabled="state.isDisabled">New</button>
</t>
</t>
<t t-set-slot="control-panel-action-menu">
<t t-set-slot="layout-buttons">
<t t-if="env.inDialog">
<t t-if="footerArchInfo">
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="footerArchInfo" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons"/>
</t>
<t t-else="">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
</t>
</t>
<t t-set-slot="layout-actions">
<t t-if="buttonBoxTemplate">
<t t-call="{{ buttonBoxTemplate }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this, props: { ...this.props, record: this.model.root } }) }"/>
</t>
</t>
<t t-set-slot="control-panel-additional-actions">
<t t-if="props.info.actionMenus">
<ActionMenus
<CombinedActionMenus
getActiveIds="() => model.root.isNew ? [] : [model.root.resId]"
context="props.context"
items="actionMenuItems"
@@ -27,6 +42,7 @@
/>
</t>
</t>
<t t-set-slot="control-panel-status-indicator">
<t t-if="canEdit">
<FormStatusIndicator model="model" discard.bind="discard" save.bind="saveButtonClicked" isDisabled="state.isDisabled" />
@@ -37,7 +53,7 @@
<button type="button" class="btn btn-outline-primary o_form_button_create" data-hotkey="c" t-on-click.stop="create" t-att-disabled="state.isDisabled">New</button>
</t>
</t>
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="archInfo" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons" onNotebookPageChange.bind="onNotebookPageChange" activeNotebookPages="props.state and props.state.activeNotebookPages"/>
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="archInfo" translateAlert="translateAlert" enableViewButtons.bind="enableButtons" disableViewButtons.bind="disableButtons" onNotebookPageChange.bind="onNotebookPageChange" activeNotebookPages="props.state and props.state.activeNotebookPages"/>
</Layout>
</div>
</div>
@@ -2,11 +2,11 @@
<templates xml:space="preserve">
<t t-name="web.FormStatusIndicator" owl="1">
<div class="o_form_status_indicator d-md-flex align-items-center" t-att-class="{ o_form_status_indicator_new_record: props.model.root.isNew }">
<div class="o_form_status_indicator d-md-flex align-items-center align-self-md-end me-auto" t-att-class="{ o_form_status_indicator_new_record: props.model.root.isNew }">
<div class="o_form_status_indicator_buttons d-flex" t-att-class="{ invisible: !(props.model.root.isNew or displayButtons) }">
<button
type="button"
class="o_form_button_save btn btn-light px-1 lh-sm"
class="o_form_button_save btn btn-light px-1 py-0 lh-sm"
t-att-disabled="props.isDisabled || !props.model.root.isNew and indicatorMode === 'invalid'"
data-hotkey="s"
t-on-click.stop="save"
@@ -16,7 +16,7 @@
</button>
<button
type="button"
class="o_form_button_cancel btn btn-light px-1 lh-sm"
class="o_form_button_cancel btn btn-light px-1 py-0 lh-sm"
t-att-disabled="props.isDisabled"
data-hotkey="j"
t-on-click.stop="discard"
@@ -6,14 +6,12 @@ import { RelationalModel } from "../basic_relational_model";
import { FormArchParser } from "./form_arch_parser";
import { FormController } from "./form_controller";
import { FormCompiler } from "./form_compiler";
import { FormControlPanel } from "./control_panel/form_control_panel";
export const formView = {
type: "form",
display_name: "Form",
multiRecord: false,
searchMenuTypes: [],
ControlPanel: FormControlPanel,
Controller: FormController,
Renderer: FormRenderer,
ArchParser: FormArchParser,
@@ -4,9 +4,11 @@ import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
import { GroupByMenu } from "@web/search/group_by_menu/group_by_menu";
import { useModel } from "@web/views/model";
import { standardViewProps } from "@web/views/standard_view_props";
import { useSetupView } from "@web/views/view_hook";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { Component, useRef } from "@odoo/owl";
@@ -127,7 +129,7 @@ export class GraphController extends Component {
}
GraphController.template = "web.GraphView";
GraphController.components = { Dropdown, DropdownItem, Layout };
GraphController.components = { Dropdown, DropdownItem, Layout, GroupByMenu, SearchBar };
GraphController.props = {
...standardViewProps,
@@ -9,41 +9,41 @@
</t>
</div>
<div class="btn-group" role="toolbar" aria-label="Change graph">
<button class="btn btn-light fa fa-bar-chart o_graph_button" data-tooltip="Bar Chart" aria-label="Bar Chart" data-mode="bar"
<button class="btn btn-secondary fa fa-bar-chart o_graph_button" data-tooltip="Bar Chart" aria-label="Bar Chart" data-mode="bar"
t-on-click="() => this.onModeSelected('bar')"
t-att-class="{ active: model.metaData.mode === 'bar' }"
/>
<button class="btn btn-light fa fa-line-chart o_graph_button" data-tooltip="Line Chart" aria-label="Line Chart" data-mode="line"
<button class="btn btn-secondary fa fa-line-chart o_graph_button" data-tooltip="Line Chart" aria-label="Line Chart" data-mode="line"
t-on-click="() => this.onModeSelected('line')"
t-att-class="{ active: model.metaData.mode === 'line' }"
/>
<button class="btn btn-light fa fa-pie-chart o_graph_button" data-tooltip="Pie Chart" aria-label="Pie Chart" data-mode="pie"
<button class="btn btn-secondary fa fa-pie-chart o_graph_button" data-tooltip="Pie Chart" aria-label="Pie Chart" data-mode="pie"
t-on-click="() => this.onModeSelected('pie')"
t-att-class="{ active: model.metaData.mode === 'pie' }"
/>
</div>
<div t-if="model.metaData.mode === 'bar'" class="btn-group" role="toolbar" aria-label="Change graph">
<button class="btn btn-light fa fa-database o_graph_button" data-tooltip="Stacked" aria-label="Stacked"
<button class="btn btn-secondary fa fa-database o_graph_button" data-tooltip="Stacked" aria-label="Stacked"
t-on-click="toggleStacked"
t-att-class="{ active: model.metaData.stacked }"
/>
</div>
<div t-if="model.metaData.mode === 'line'" class="btn-group" role="toolbar" aria-label="Change graph">
<button class="btn btn-light fa fa-database o_graph_button" data-tooltip="Stacked" aria-label="Stacked"
<button class="btn btn-secondary fa fa-database o_graph_button" data-tooltip="Stacked" aria-label="Stacked"
t-on-click="toggleStacked"
t-att-class="{ active: model.metaData.stacked }"
/>
<button class="btn btn-light fa fa-signal o_graph_button" data-tooltip="Cumulative" aria-label="Cumulative"
<button class="btn btn-secondary fa fa-signal o_graph_button" data-tooltip="Cumulative" aria-label="Cumulative"
t-on-click="toggleCumulated"
t-att-class="{ active: model.metaData.cumulated }"
/>
</div>
<div t-if="model.metaData.mode !== 'pie' and model.metaData.domains.length === 1" class="btn-group" role="toolbar" aria-label="Sort graph" name="toggleOrderToolbar">
<button class="btn btn-light fa fa-sort-amount-desc o_graph_button" data-tooltip="Descending" aria-label="Descending"
<button class="btn btn-secondary fa fa-sort-amount-desc o_graph_button" data-tooltip="Descending" aria-label="Descending"
t-on-click="() => this.toggleOrder('DESC')"
t-att-class="{ active: model.metaData.order === 'DESC' }"
/>
<button class="btn btn-light fa fa-sort-amount-asc o_graph_button" data-tooltip="Ascending" aria-label="Ascending"
<button class="btn btn-secondary fa fa-sort-amount-asc o_graph_button" data-tooltip="Ascending" aria-label="Ascending"
t-on-click="() => this.toggleOrder('ASC')"
t-att-class="{ active: model.metaData.order === 'ASC' }"
/>
@@ -56,6 +56,9 @@
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
<t t-if="model.data">
<t t-if="model.useSampleModel and props.info.noContentHelp" t-call="web.ActionHelper">
<t t-set="noContentHelp" t-value="props.info.noContentHelp"/>
@@ -9,6 +9,7 @@ import { MultiRecordViewButton } from "@web/views/view_button/multi_record_view_
import { useViewButtons } from "@web/views/view_button/view_button_hook";
import { useSetupView } from "@web/views/view_hook";
import { KanbanRenderer } from "./kanban_renderer";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { Component, reactive, useRef } from "@odoo/owl";
@@ -174,7 +175,7 @@ export class KanbanController extends Component {
}
KanbanController.template = `web.KanbanView`;
KanbanController.components = { Layout, KanbanRenderer, MultiRecordViewButton };
KanbanController.components = { Layout, KanbanRenderer, MultiRecordViewButton, SearchBar };
KanbanController.props = {
...standardViewProps,
defaultGroupBy: { validate: (dgb) => !dgb || typeof dgb === "string", optional: true },
@@ -4,9 +4,34 @@
<t t-name="web.KanbanView" owl="1">
<div t-att-class="className" t-ref="root">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="props.display">
<t t-set-slot="control-panel-create-button">
<t t-if="canCreate">
<button type="button" class="btn btn-primary o-kanban-button-new" accesskey="c" t-on-click="() => this.createRecord()" data-bounce-button="">
New
</button>
</t>
</t>
<t t-set-slot="control-panel-always-buttons">
<t t-foreach="headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
t-if="button.display === 'always'"
list="model.root"
className="button.className"
clickParams="button.clickParams"
defaultRank="'btn-secondary'"
domain="props.domain"
icon="button.icon"
string="button.string"
title="button.title"
/>
</t>
</t>
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
<t t-component="props.Renderer"
list="model.root"
archInfo="props.archInfo"
@@ -19,32 +44,12 @@
canQuickCreate="canQuickCreate"
quickCreateState="quickCreateState"
/>
</Layout>
</div>
</t>
<t t-name="web.KanbanView.Buttons" owl="1">
<div t-if="props.showButtons" class="o_cp_buttons d-flex align-items-baseline" role="toolbar" aria-label="Main actions">
<t t-if="canCreate">
<button type="button" class="btn btn-primary o-kanban-button-new" accesskey="c" t-on-click="() => this.createRecord()" data-bounce-button="">
New
</button>
</t>
<t t-foreach="headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
t-if="button.display === 'always'"
list="model.root"
className="button.className"
clickParams="button.clickParams"
defaultRank="'btn-secondary'"
domain="props.domain"
icon="button.icon"
string="button.string"
title="button.title"
/>
</t>
</div>
<div t-if="props.showButtons" class="o_cp_buttons d-flex align-items-baseline" role="toolbar" aria-label="Main actions"/>
</t>
</templates>
@@ -6,7 +6,7 @@
<div class="o_kanban_header_title position-relative d-flex lh-lg" t-on-mouseenter="onTitleMouseEnter" t-on-mouseleave="onTitleMouseLeave">
<div t-if="!env.isSmall and group.isFolded" class="o_column_title d-flex align-items-center pt-1 fs-4 lh-1 text-nowrap opacity-50 opacity-100-hover">
<t t-esc="groupName"></t>
<span class="badge text-bg-500 rounded-pill ms-2" t-esc="group.count"></span>
<span class="badge text-bg-500 rounded-pill lh-1 ms-2" t-esc="group.count"></span>
</div>
<span t-if="!env.isSmall and !group.isFolded"
t-esc="groupName"
@@ -20,6 +20,9 @@ import { useViewButtons } from "@web/views/view_button/view_button_hook";
import { ExportDataDialog } from "@web/views/view_dialogs/export_data_dialog";
import { useSetupView } from "@web/views/view_hook";
import { ListConfirmationDialog } from "./list_confirmation_dialog";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { Component, onMounted, onWillStart, useEffect, useRef, useSubEnv } from "@odoo/owl";
@@ -371,15 +374,12 @@ export class ListController extends Component {
}
get display() {
if (!this.env.isSmall) {
return this.props.display;
}
const { controlPanel } = this.props.display;
return {
...this.props.display,
controlPanel: {
...controlPanel,
"bottom-right": !this.nbSelected,
layoutActions: !this.nbSelected,
},
};
}
@@ -589,7 +589,7 @@ export class ListController extends Component {
}
ListController.template = `web.ListView`;
ListController.components = { ActionMenus, Layout, ViewButton, MultiRecordViewButton };
ListController.components = { ActionMenus, Layout, ViewButton, MultiRecordViewButton, SearchBar, Dropdown, DropdownItem };
ListController.props = {
...standardViewProps,
allowSelectors: { type: Boolean, optional: true },
@@ -4,26 +4,76 @@
<t t-name="web.ListView" owl="1">
<div t-att-class="className" t-ref="root">
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="display">
<t t-set-slot="layout-buttons">
<t t-if="env.isSmall and nbSelected">
<t t-call="web.ListView.Selection" />
</t>
<t t-else="">
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel buttons" t-ref="buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</div>
<t t-set-slot="control-panel-create-button">
<t t-if="!model.root.editedRecord and activeActions.create and (!model.root.isGrouped or !editable) and !env.inDialog">
<button type="button" class="btn btn-primary o_list_button_add" data-hotkey="c" t-on-click="onClickCreate" data-bounce-button="">
New
</button>
</t>
</t>
<t t-set-slot="control-panel-bottom-left">
<t t-if="props.info.actionMenus and model.root.selection.length">
<ActionMenus
getActiveIds="() => model.root.selection.map((r) => r.resId)"
context="props.context"
<t t-set-slot="control-panel-always-buttons">
<t t-foreach="archInfo.headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
t-if="button.display === 'always'"
list="model.root"
className="button.className"
clickParams="button.clickParams"
defaultRank="'btn-secondary'"
domain="props.domain"
items="actionMenuItems"
isDomainSelected="model.root.isDomainSelected"
resModel="model.root.resModel"
onActionExecuted="() => model.load()"/>
icon="button.icon"
string="button.string"
title="button.title"
/>
</t>
</t>
<t t-set-slot="layout-buttons">
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel buttons" t-ref="buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</div>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
<t t-set-slot="control-panel-additional-actions">
<div t-if="nbSelected" class="d-flex ms-3">
<t t-call="web.ListView.Selection"/>
<t t-foreach="archInfo.headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
t-if="button.display !== 'always'"
list="model.root"
className="button.className"
clickParams="button.clickParams"
defaultRank="'btn-secondary'"
domain="props.domain"
icon="button.icon"
string="button.string"
title="button.title"
/>
</t>
<t t-if="props.info.actionMenus">
<ActionMenus
getActiveIds="() => model.root.selection.map((r) => r.resId)"
context="props.context"
domain="props.domain"
items="actionMenuItems"
isDomainSelected="model.root.isDomainSelected"
resModel="model.root.resModel"
onActionExecuted="() => model.load()"/>
</t>
</div>
<t t-else="">
<Dropdown togglerClass="'d-print-none btn btn-text ms-1 lh-sm'">
<t t-set-slot="toggler">
<i class="fa fa-cog"/>
</t>
<DropdownItem t-if="nbTotal and !nbSelected and activeActions.exportXlsx and isExportEnable" onSelected.bind="onDirectExportData">
Export All
</DropdownItem>
</Dropdown>
</t>
</t>
<t t-component="props.Renderer" list="model.root" activeActions="activeActions" archInfo="archInfo" allowSelectors="props.allowSelectors" editable="editable" openRecord.bind="openRecord" noContentHelp="props.info.noContentHelp" onAdd.bind="createRecord"/>
@@ -32,56 +82,32 @@
</t>
<t t-name="web.ListView.Buttons" owl="1">
<div class="o_list_buttons d-flex flex-wrap" role="toolbar" aria-label="Main actions">
<t t-if="props.showButtons">
<t t-if="model.root.editedRecord">
<button type="button" class="btn btn-primary o_list_button_save" data-hotkey="s" t-on-click.stop="onClickSave">
Save
</button>
<button type="button" class="btn btn-secondary o_list_button_discard" data-hotkey="j" t-on-click="onClickDiscard" t-on-mousedown="onMouseDownDiscard">
Discard
</button>
</t>
<t t-elif="activeActions.create and (!model.root.isGrouped or !editable)">
<button type="button" class="btn btn-primary o_list_button_add" data-hotkey="c" t-on-click="onClickCreate" data-bounce-button="">
New
</button>
</t>
<t t-foreach="archInfo.headerButtons" t-as="button" t-key="button.id">
<MultiRecordViewButton
t-if="nbSelected || (button.display === 'always')"
list="model.root"
className="button.className"
clickParams="button.clickParams"
defaultRank="'btn-secondary'"
domain="props.domain"
icon="button.icon"
string="button.string"
title="button.title"
/>
</t>
<t t-if="nbTotal and !nbSelected and activeActions.exportXlsx and isExportEnable and !env.isSmall">
<button type="button" class="btn btn-secondary fa fa-download o_list_export_xlsx" data-tooltip="Export All" aria-label="Export All" t-on-click="onDirectExportData"/>
</t>
</t>
<t t-if="!env.isSmall and nbSelected">
<t t-call="web.ListView.Selection"/>
<div t-if="props.showButtons" class="o_list_buttons d-flex d-empty-none align-items-baseline" role="toolbar" aria-label="Main actions">
<t t-if="model.root.editedRecord">
<button type="button" class="btn btn-primary o_list_button_save" data-hotkey="s" t-on-click.stop="onClickSave">
Save
</button>
<button type="button" class="btn btn-secondary o_list_button_discard" data-hotkey="j" t-on-click="onClickDiscard" t-on-mousedown="onMouseDownDiscard">
Discard
</button>
</t>
</div>
</t>
<t t-name="web.ListView.Selection" owl="1">
<div class="o_list_selection_box alert alert-info d-inline-flex align-items-center p-1" role="alert">
<span t-if="isDomainSelected" class="p-1">All <t t-esc="nbTotal"/> selected</span>
<t t-else="">
<span class="me-1" t-esc="nbSelected"/> selected
<a t-if="isPageSelected and nbTotal > nbSelected" href="#" class="o_list_select_domain ms-2 btn btn-sm btn-info p-1 border-0 fw-normal" t-on-click="onSelectDomain">
<i class="fa fa-fw fa-arrow-right"/> Select all <span t-esc="nbTotal"/>
<div class="o_list_selection_box list-group flex-row me-2" role="alert">
<span class="list-group-item active d-flex align-items-center alisgn-self-stretch pe-0 py-0 rounded-1 lh-1">
<span t-if="isDomainSelected">All <b t-esc="nbTotal"/> selected</span>
<t t-else="">
<b class="me-1" t-esc="nbSelected"/> selected
<a t-if="isPageSelected and nbTotal > nbSelected" href="#" class="o_list_select_domain ms-3 btn btn-sm btn-info p-1 me-n2 border-0 fw-normal" t-on-click="onSelectDomain">
<i class="fa fa-fw fa-arrow-right"/> Select all <span t-esc="nbTotal"/>
</a>
</t>
<a href="#" title="Unselect All" class="o_list_unselect_all btn btn-link py-0 ms-2" t-on-click="onUnselectAll">
<i class="oi oi-close oi-large"/>
</a>
</t>
<a t-if="nbSelected > 1" href="#" class="o_list_unselect_all ms-2 btn btn-sm btn-info p-1 border-0 fw-normal" t-att-class="{'btn-link': env.isSmall}" t-on-click="onUnselectAll">
<i class="fa fa-fw fa-times"/><t t-if="!env.isSmall"> Unselect all</t>
</a>
</span>
</div>
</t>
@@ -8,6 +8,7 @@ import { Layout } from "@web/search/layout";
import { useModel } from "@web/views/model";
import { standardViewProps } from "@web/views/standard_view_props";
import { useSetupView } from "@web/views/view_hook";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { Component, useRef } from "@odoo/owl";
@@ -131,7 +132,7 @@ export class PivotController extends Component {
}
PivotController.template = "web.PivotView";
PivotController.components = { Dropdown, DropdownItem, Layout };
PivotController.components = { Dropdown, DropdownItem, Layout, SearchBar };
PivotController.props = {
...standardViewProps,
@@ -22,6 +22,9 @@
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar/>
</t>
<t t-set="displayNoContent" t-value="
props.info.noContentHelp !== false and (
!(model.hasData() and model.metaData.activeMeasures.length) or
@@ -109,7 +109,7 @@
<div class="o_right_field_panel h-100 px-2 overflow-auto border">
<ul class="o_fields_list list-unstyled" t-ref="draggable">
<t t-foreach="state.exportList" t-as="field" t-key="field.id">
<li t-attf-class="o_export_field {{ state.isSmall ? '' : 'o_export_field_sortable' }}" t-att-data-field_id="field.id">
<li t-attf-class="o_export_field d-inline-block w-100 {{ state.isSmall ? '' : 'o_export_field_sortable' }}" t-att-data-field_id="field.id">
<span t-if="!state.isSmall" class="fa fa-sort o_sort_field mx-1" t-attf-style="opacity:{{ state.exportList.length === 1 ? 0 : 1 }}" />
<span t-esc="isDebug and field.id ? `${field.string} (${field.id})` : field.string" />
<span class="fa fa-trash m-1 pe-2 float-end o_remove_field cursor-pointer" t-att-title="removeFieldText" t-on-click.stop="() => this.onRemoveItemExportList(field.id)" />
@@ -15,6 +15,7 @@ export class SettingsFormController extends formView.Controller {
this.state = useState({ displayNoContent: false });
this.searchState = useState({ value: "" });
this.rootRef = useRef("root");
this.canCreate = false;
useSubEnv({ searchState: this.searchState });
useEffect(
() => {
@@ -4,8 +4,8 @@
<xpath expr="./div[@t-ref='root']" position="attributes">
<attribute name="class">o-settings-form-view o_field_highlight</attribute>
</xpath>
<xpath expr="//Layout" position="inside">
<t t-set-slot="control-panel-top-right">
<xpath expr="//Layout/t[@t-set-slot='layout-actions']" position="replace">
<t t-set-slot="layout-actions">
<div class="o_cp_searchview d-flex flex-grow-1" role="search">
<div class="o_searchview pb-1 align-self-center border-bottom flex-grow-1" role="search" aria-autocomplete="list">
<i class="o_searchview_icon oi oi-search" role="img" aria-label="Search..." title="Search..." />
@@ -28,6 +28,11 @@
</t>
</t>
</xpath>
<xpath expr="//Layout/t[@t-set-slot='layout-buttons']" position="replace">
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
</xpath>
</t>
<t t-name="web.SettingsFormView.Buttons" t-inherit="web.FormView.Buttons" t-inherit-mode="primary" owl="1">
@@ -23,16 +23,16 @@
</xpath>
</t>
<t t-name="website.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="extension" owl="1">
<!-- <t t-name="website.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_search_options')]/*[last()]" position="after">
<t t-slot="control-panel-website-extra-actions"/>
</xpath>
</t>
</t> -->
<t t-name="website.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
<!-- <xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
<t t-call="website.ControlPanel.Regular"/>
</xpath>
</xpath> -->
</t>
<t t-name="website.RecordFilter" owl="1">
@@ -16,7 +16,7 @@
</div>
</t>
<t t-name="website.ThemePreviewForm.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
<!-- <t t-name="website.ThemePreviewForm.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_cp_bottom_right')]" position="replace">
<div class="o_cp_bottom_right" role="group">
<input class="btn-check" id="themeViewerDesktop" type="radio" name="viewer" data-mode='desktop' autocomplete="off" checked='checked'/>
@@ -25,15 +25,15 @@
<label class="btn btn-secondary" for="themeViewerMobile" t-on-click="this.onMobileClick">Mobile</label>
</div>
</xpath>
</t>
</t> -->
<t t-name="website.ThemePreviewForm.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
<!-- <xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
<t t-call="website.ThemePreviewForm.ControlPanel.Regular"/>
</xpath>
</xpath> -->
</t>
<t t-name="website.ThemePreviewKanban.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
<!-- <t t-name="website.ThemePreviewKanban.ControlPanel.Regular" t-inherit="web.ControlPanel.Regular" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_cp_top_left')]" position="replace">
<div class="o_cp_top_left">
<ol class="breadcrumb" role="navigation">
@@ -51,12 +51,12 @@
</a>
</div>
</xpath>
</t>
</t> -->
<t t-name="website.ThemePreviewKanban.ControlPanel" t-inherit="web.ControlPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
<!-- <xpath expr="//t[@t-call='web.ControlPanel.Regular']" position="replace">
<t t-call="website.ThemePreviewKanban.ControlPanel.Regular"/>
</xpath>
</xpath> -->
</t>
</templates>
@@ -93,10 +93,7 @@ ThemePreviewFormControlPanel.template = 'website.ThemePreviewForm.ControlPanel';
const ThemePreviewFormView = {
...formView,
display: {
controlPanel: {
'top-right': false,
'bottom-right': true,
}
controlPanel: {}
},
buttonTemplate: 'website.ThemePreview.Buttons',
Controller: ThemePreviewFormController,
@@ -33,10 +33,7 @@ const ThemePreviewKanbanView = {
Controller: ThemePreviewKanbanController,
ControlPanel: ThemePreviewControlPanel,
display: {
controlPanel: {
'bottom-left': false,
'bottom-right': false,
},
controlPanel: {},
},
};
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="website.EditInBackendSystray" owl="1">
<div class="o_website_edit_in_backend">
<a href="#" accesskey="e" t-on-click="editInBackend">
<div class="o_website_edit_in_backend d-flex">
<a href="#" class="d-flex align-items-center px-2" accesskey="e" t-on-click="editInBackend">
<span class="fa fa-cog"/>
<span class= "d-none d-md-block ms-2" t-esc="state.mainObjectName"/>
</a>
@@ -62,7 +62,7 @@
<button class="btn btn-secondary js_date_range" data-date="month">Last Month</button>
<button class="btn btn-secondary js_date_range" data-date="year">Last Year</button>
</div>
<div class="btn-group d-none d-md-inline-block float-end" style="margin-right: 20px;">
<div class="btn-group d-none d-md-inline-block float-end">
<t t-foreach="widget.websites" t-as="website">
<button t-attf-class="btn btn-secondary js_website #{website.selected ? 'active' : ''}"
t-att-data-website-id="website.id">