[MIG] web_responsive: Migration to 14.0

This commit is contained in:
Sergey Shebanin
2021-02-11 15:07:56 +03:00
committed by Sergey Shebanin
parent 346e2c20e8
commit f7e230d5cc
22 changed files with 1473 additions and 670 deletions

View File

@@ -0,0 +1,97 @@
@include media-breakpoint-down(sm) {
.o_kanban_view.o_kanban_grouped {
display: block;
position: relative;
overflow-x: hidden;
&.o_renderer_with_searchpanel {
width: 100%;
}
.o_kanban_mobile_tabs_container {
position: sticky;
display: flex;
justify-content: space-between;
width: 100%;
top: 0;
z-index: 1;
background-color: #5e5e5e;
.o_kanban_mobile_add_column {
height: $o-kanban-mobile-tabs-height;
padding: 10px;
border-left: grey 1px solid;
color: white;
font-size: 14px;
}
.o_kanban_mobile_tabs {
position: relative;
display: flex;
width: 100%;
height: $o-kanban-mobile-tabs-height;
overflow-x: auto;
.o_kanban_mobile_tab {
height: $o-kanban-mobile-tabs-height;
padding: 10px 20px;
font-size: 14px;
color: white;
&.o_current {
font-weight: bold;
border-bottom: 3px solid $o-brand-primary;
}
.o_column_title {
white-space: nowrap;
text-transform: uppercase;
}
}
}
}
.o_kanban_columns_content {
position: relative;
}
// [class] to get same specificity as elsewhere (kanban_view.less)
&[class] .o_kanban_group:not(.o_column_folded) {
@include o-position-absolute(
$top: $o-kanban-mobile-tabs-height,
$left: 0,
$bottom: 0
);
width: 100%;
padding: 0;
margin-left: 0; // override the margin-left: -1px of the desktop mode
border: none;
&.o_current {
position: inherit;
top: 0;
&.o_kanban_no_records {
// set a default height for clarity when embedded in another view
min-height: $o-kanban-mobile-empty-height;
}
}
.o_kanban_header {
display: none;
}
.o_kanban_record,
.o_kanban_quick_create {
border: none;
border-bottom: 1px solid lightgray;
padding: 10px 16px;
margin: 0;
}
}
}
.o_kanban_view .o_column_quick_create {
.o_quick_create_folded {
display: none !important;
}
.o_quick_create_unfolded {
width: 100%;
}
}
}

View File

@@ -0,0 +1,109 @@
.o_web_client {
.o_mobile_search {
position: fixed;
top: 0;
left: 0;
bottom: 0;
padding: 0;
width: 100%;
background-color: white;
z-index: $zindex-modal;
overflow: auto;
.o_mobile_search_header {
height: 46px;
margin-bottom: 10px;
width: 100%;
background-color: $o-brand-odoo;
color: white;
span:active {
background-color: darken($o-brand-primary, 10%);
}
span {
cursor: pointer;
}
}
.o_searchview_input_container {
display: flex;
padding: 15px 20px 0 20px;
position: relative;
.o_searchview_input {
width: 100%;
margin-bottom: 15px;
border-bottom: 1px solid $o-brand-secondary;
}
.o_searchview_facet {
border-radius: 10px;
display: inline-flex;
order: 1;
.o_searchview_facet_label {
border-radius: 2em 0em 0em 2em;
}
}
.o_searchview_autocomplete {
top: 100%;
> li {
margin: 5px 0px;
}
}
}
.o_mobile_search_filter {
padding-bottom: 15%;
.o_dropdown {
width: 100%;
margin: 15px 5px 0px 5px;
border: solid 1px darken(gray("200"), 20%);
}
.o_dropdown_toggler_btn {
width: 100%;
text-align: left;
&:after {
display: none;
}
}
// We disable the backdrop in this case because it prevents any
// interaction outside of a dropdown while it is open.
.dropdown-backdrop {
z-index: -1;
}
.dropdown-menu {
// Here we use !important because of popper js adding custom style
// to element so to override it use !important
position: relative !important;
width: 100% !important;
transform: translate3d(0, 0, 0) !important;
box-shadow: none;
border: none;
color: gray("600");
.divider {
margin: 0px;
}
> li > a {
padding: 10px 26px;
}
}
}
.o_mobile_search_show_result {
padding: 10px;
font-size: 15px;
}
}
}
// Search panel
@include media-breakpoint-down(sm) {
.o_controller_with_searchpanel {
display: block;
.o_search_panel {
height: auto;
padding: 8px;
border-left: 1px solid $gray-300;
section {
padding: 0px 16px;
}
}
.o_search_panel_summary {
cursor: pointer;
}
}
}

View File

@@ -328,38 +328,114 @@ html .o_web_client .o_action_manager .o_action {
max-width: 100%;
}
// Control panel (breadcrumbs, search box, buttons...)
@include media-breakpoint-down(sm) {
.o_control_panel {
// Arrange buttons to use space better
.breadcrumb,
.o_cp_buttons,
.o_cp_left,
.o_cp_right,
.o_cp_searchview {
flex: 1 1 100%;
@include media-breakpoint-up(md) {
flex-basis: 50%;
// Make enough space for search panel filters buttons
.o_control_panel {
// There is no media breakpoint for XL upper bound
@include media-breakpoint-up(lg) {
@media (max-width: 1360px) {
.o_cp_top_left,
.o_cp_bottom_left {
width: 40%;
}
.o_cp_top_right,
.o_cp_bottom_right {
width: 60%;
}
}
}
// For FULL HD devices
@media (min-width: 1900px) {
.o_cp_top_left,
.o_cp_bottom_left {
width: 60%;
}
.o_cp_top_right,
.o_cp_bottom_right {
width: 40%;
}
}
@include media-breakpoint-only(md) {
.o_search_options_hide_labels .o_dropdown_title {
display: none;
}
}
.o_cp_bottom_right {
height: 30px;
}
}
.breadcrumb {
// Mobile Control panel (breadcrumbs, search box, buttons...)
@include media-breakpoint-down(sm) {
.o_control_panel {
// Avoid horizontal scrolling of control panel.
// It doesn't work on iOS Safari, but it looks similar as
// without this patch. With this patch it looks better for
// other browsers.
position: sticky;
left: 0;
z-index: 3;
// Arrange buttons to use space better
.o_cp_top_left,
.o_cp_top_right {
flex: 1 1 100%;
}
.o_cp_top_left {
flex-basis: 80%;
}
.o_cp_searchview,
.o_cp_right {
flex-basis: 10%;
.o_cp_top_right {
flex-basis: 20%;
}
.o_cp_left {
flex-basis: 50%;
white-space: nowrap;
.o_cp_bottom {
position: relative; // Necessary for dropdown menu positioning
display: block;
margin: 0;
}
.o_cp_bottom_left {
float: left;
margin: 5px 0;
}
.o_cp_bottom_right {
float: right;
height: 30px;
padding-left: 10px;
margin: 5px 0;
}
.o_cp_bottom_right,
.o_cp_pager {
white-space: nowrap;
}
.o_cp_pager {
margin-bottom: 0;
}
.o_cp_bottom_left > .o_cp_action_menus {
padding-right: 0;
.o_dropdown_title,
.fa-chevron-right,
.fa-chevron-down {
display: none;
}
.o_dropdown_toggler_btn {
margin: 0px 2px;
}
@include media-breakpoint-down(xs) {
.o_dropdown {
position: static;
}
.dropdown-menu {
right: 0;
left: 0;
top: 35px;
}
}
}
// Hide all but 2 last breadcrumbs, and render 2nd-to-last as arrow
.breadcrumb-item {
@@ -379,7 +455,7 @@ html .o_web_client .o_action_manager .o_action {
&:nth-last-of-type(2) {
&::before {
color: var(--primary);
content: "\f048"; // .fa-step-backward
content: "\f060"; // .fa-arrow-left
cursor: pointer;
font-family: FontAwesome;
}
@@ -396,36 +472,67 @@ html .o_web_client .o_action_manager .o_action {
text-overflow: ellipsis;
}
// Empty sidebar should not break layout
.o_cp_sidebar:blank {
display: none;
}
// In case you install `mail`, there is a mess on BS vs inline styles
// we need to fix
.o_cp_buttons .btn.d-block:not(.d-none) {
display: inline-block !important;
}
// Dropdown with buttons to switch the view type
.o_cp_switch_buttons.show {
.dropdown-menu {
align-content: center;
.o_searchview {
padding: 1px 0px 3px 0px;
&.o_searchview_mobile {
cursor: pointer;
}
&.o_searchview_quick {
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 0;
.btn {
border: {
bottom: 0;
radius: 0;
top: 0;
}
flex: 1 1 auto;
align-items: center;
.o_searchview_input_container {
flex: 1 1 auto;
}
}
}
}
.o_calendar_view .o_calendar_widget {
.fc-timeGridDay-view .fc-axis,
.fc-timeGridWeek-view .fc-axis {
padding-left: 0px;
}
.fc-dayGridMonth-view {
padding-left: 0px;
.fc-week-number {
display: none;
}
}
.fc-dayGridYear-view {
padding-left: 0px;
> .fc-month-container > .fc-month {
width: 100%;
}
}
.fc-timeGridDay-view .fc-widget-header {
margin: 0 4px;
}
.fc-timeGridWeek-view .fc-widget-header {
word-spacing: 4em;
white-space: normal;
text-align: center;
}
}
.o_base_settings .o_setting_container {
display: block;
.settings_tab {
flex-flow: row nowrap;
padding-top: 0px;
.tab {
padding-right: 16px;
}
.selected {
background-color: #212529;
box-shadow: inset 0 -5px #7c7bad;
}
}
}
}
// Normal views
@@ -440,13 +547,31 @@ html .o_web_client .o_action_manager .o_action {
overflow-x: auto;
}
.oe_chatter {
.o_FormRenderer_chatterContainer {
padding-top: 0;
.o_Activity_info {
flex-wrap: wrap;
}
.o_ActivityBox_title {
margin-bottom: 0;
}
.o_MessageList_separatorDate {
padding-bottom: 0;
}
}
.o_chatter_topbar {
height: auto;
flex-wrap: wrap-reverse;
// Sided chatter scrolling behavior
.o_Chatter {
height: fit-content;
.o_Chatter_fixedPanel {
position: sticky;
top: 0;
z-index: 1;
background-color: white;
padding-bottom: 10px;
}
.o_Chatter_scrollPanel {
overflow: initial;
}
}
// Sticky statusbar
@@ -458,17 +583,14 @@ html .o_web_client .o_action_manager .o_action {
// Support for long title (with ellipsis)
.oe_title {
span.o_field_widget {
&:not(.oe_inline) {
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: initial;
&:active {
white-space: normal;
}
span.o_field_widget:not(.oe_inline) {
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: initial;
&:active {
white-space: normal;
}
}
}
@@ -520,7 +642,7 @@ html .o_web_client .o_action_manager .o_action {
}
height: 100%;
}
.o_statusbar_buttons > .btn {
.o_statusbar_buttons.dropdown-menu > .btn {
border-radius: 0;
border: 0;
width: 100%;
@@ -541,7 +663,7 @@ html .o_web_client .o_action_manager .o_action {
// Full width in form sheets
.o_form_sheet,
.oe_chatter {
.o_FormRenderer_chatterContainer {
min-width: auto;
max-width: 98%;
}
@@ -553,7 +675,7 @@ html .o_web_client .o_action_manager .o_action {
}
}
.o_chatter {
.o_FormRenderer_chatterContainer {
padding-top: initial;
// Display send button on small screens
@@ -605,7 +727,7 @@ html .o_web_client .o_action_manager .o_action {
}
}
.o_chatter {
.o_FormRenderer_chatterContainer {
border-left: 1px solid gray("400");
flex: 0 0 $chatter_zone_width;
max-width: initial;
@@ -645,7 +767,6 @@ html .o_web_client .o_action_manager .o_action {
.table-responsive {
.o_list_table {
// th & td are here for compatibility with chrome
thead,
thead tr:nth-child(1) th {
position: sticky;
top: 0;
@@ -693,114 +814,105 @@ html .o_web_client .o_action_manager .o_action {
cursor: progress;
}
// Document Viewer
.o_web_client.o_chatter_position_sided {
.o_modal_fullscreen.o_document_viewer {
// On-top of navbar
z-index: 10;
&.o_responsive_document_viewer {
/* Show sided viewer on large screens */
@include media-breakpoint-up(lg) {
width: $chatter_zone_width;
margin-left: auto;
right: 0;
/* Show/Hide control buttons (next, prev, etc..) */
&:hover .arrow,
&:hover .o_viewer_toolbar {
display: flex;
}
.arrow,
.o_viewer_toolbar {
display: none;
}
.o_viewer_img_wrapper {
position: relative;
.o_viewer_pdf {
width: 95%;
}
}
}
.o_minimize_btn {
display: none;
}
// Attachment Viewer
.o_web_client.o_chatter_position_sided .o_Dialog_AttachmentViewer {
/* Show sided viewer on large screens */
@include media-breakpoint-up(lg) {
position: static;
.o_AttachmentViewer_main {
padding-bottom: 20px;
}
&:not(.o_responsive_document_viewer) {
.o_maximize_btn {
.o_AttachmentViewer {
// On-top of navbar
z-index: 10;
position: absolute;
right: 0;
top: 0;
bottom: 0;
margin-left: auto;
background-color: rgba(0, 0, 0, 0.7);
.o_AttachmentViewer_name {
display: contents;
}
width: $chatter_zone_width;
&.o_AttachmentViewer_maximized {
width: 100%;
}
/* Show/Hide control buttons (next, prev, etc..) */
&:hover .o_AttachmentViewer_buttonNavigation,
&:hover .o_AttachmentViewer_toolbar {
display: flex;
}
.o_AttachmentViewer_buttonNavigation,
.o_AttachmentViewer_toolbar {
display: none;
}
}
@include media-breakpoint-down(lg) {
.o_minimize_btn,
.o_maximize_btn {
display: none;
.o_AttachmentViewer_viewIframe {
width: 95%;
}
}
}
}
/* Max/Min buttons only are usefull in sided mode */
.o_web_client:not(.o_chatter_position_sided) {
.o_minimize_btn,
.o_maximize_btn {
display: none;
}
}
// Apply improvements for Document Viewer on all modes
.o_modal_fullscreen .o_viewer_content {
.o_viewer-header {
.o_image_caption {
display: contents;
}
// Now uses a container to have more buttons
.o_buttons {
min-width: 35px;
text-align: right;
// Now close button ('X') it's a fa-icon
> .o_close_btn {
top: unset;
left: unset;
bottom: unset;
right: unset;
font-size: unset;
font-weight: unset;
}
}
}
}
// Search Panel
@include media-breakpoint-down(sm) {
// Hide search panel
.o_controller_with_searchpanel {
.o_search_panel {
@include media-breakpoint-down(md) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none;
}
}
}
/* Attachment Viewer Max/Min buttons only are useful in sided mode */
.o_web_client:not(.o_chatter_position_sided) {
.o_AttachmentViewer_headerItemButtonMinimize,
.o_AttachmentViewer_headerItemButtonMaximize {
display: none;
}
}
.o_control_panel {
// Filter Menu item
.o_filters_menu {
// Filter Menu
// Cut long filters names in the filters menu
.o_filter_menu {
.o_menu_item {
@include o-search-options-dropdown-custom-item;
width: auto;
@include media-breakpoint-up(md) {
max-width: 250px;
}
a {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
// Enable scroll on dropdowns
.o_cp_buttons .dropdown-menu {
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
}
// Dropdown with buttons to switch the view type
.o_cp_switch_buttons.show {
.dropdown-menu {
align-content: center;
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 0;
.btn {
border: {
bottom: 0;
radius: 0;
top: 0;
}
font-size: 1.3em;
}
}
}
}
// Shortcut table ui improvement
.o_shortcut_table {
width: 100%;
}

View File

@@ -1,300 +0,0 @@
/* Copyright Odoo S.A.
* Ported to 13.0 by Copyright 2020 Tecnativa - Alexandre Díaz
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("web_responsive.Discuss", function(require) {
"use strict";
const config = require("web.config");
if (!config.device.isMobile) {
return;
}
const core = require("web.core");
const Discuss = require("mail.Discuss");
const QWeb = core.qweb;
Discuss.include({
contentTemplate: "mail.discuss_mobile",
events: Object.assign({}, Discuss.prototype.events, {
"click .o_mail_mobile_tab": "_onClickMobileTab",
"click .o_mailbox_inbox_item": "_onClickMobileMailboxItem",
"click .o_mail_preview": "_onClickMobileMailPreview",
}),
/**
* @override
*/
init: function() {
this._super.apply(this, arguments);
this._currentState = this._defaultThreadID;
},
/**
* @override
*/
start: function() {
this._$mainContent = this.$(".o_mail_discuss_content");
return this._super
.apply(this, arguments)
.then(this._updateControlPanel.bind(this));
},
/**
* @override
*/
on_attach_callback: function() {
if (this._thread && this._isInInboxTab()) {
this._threadWidget.scrollToPosition(
this._threadsScrolltop[this._thread.getID()]
);
}
},
/**
* @override
*/
on_detach_callback: function() {
if (this._isInInboxTab()) {
this._threadsScrolltop[
this._thread.getID()
] = this._threadWidget.getScrolltop();
}
},
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
/**
* @private
* @returns {Boolean} true iff we currently are in the Inbox tab
*/
_isInInboxTab: function() {
return _.contains(["mailbox_inbox", "mailbox_starred"], this._currentState);
},
/**
* @override
* @private
*/
_renderButtons: function() {
this._super.apply(this, arguments);
_.each(["dm_chat", "multi_user_channel"], type => {
const selector = ".o_mail_discuss_button_" + type;
this.$buttons.on("click", selector, this._onAddThread.bind(this));
});
},
/**
* Overrides to only store the thread state if we are in the Inbox tab, as
* this is the only tab in which we actually have a displayed thread
*
* @override
* @private
*/
_restoreThreadState: function() {
if (this._isInInboxTab()) {
this._super.apply(this, arguments);
}
},
/**
* Overrides to toggle the visibility of the tabs when a message is selected
*
* @override
* @private
*/
_selectMessage: function() {
this._super.apply(this, arguments);
this.$(".o_mail_mobile_tabs").addClass("o_hidden");
},
/**
* @override
* @private
*/
_setThread: function(threadID) {
const thread = this.call("mail_service", "getThread", threadID);
this._thread = thread;
if (thread.getType() !== "mailbox") {
this.call("mail_service", "openThreadWindow", threadID);
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
/**
* Overrides to only store the thread state if we are in the Inbox tab, as
* this is the only tab in which we actually have a displayed thread
*
* @override
* @private
*/
_storeThreadState: function() {
if (this._thread && this._isInInboxTab()) {
this._super.apply(this, arguments);
}
},
/**
* Overrides to toggle the visibility of the tabs when a message is
* unselected
*
* @override
* @private
*/
_unselectMessage: function() {
this._super.apply(this, arguments);
this.$(".o_mail_mobile_tabs").removeClass("o_hidden");
},
/**
* @override
* @private
*/
_updateThreads: function() {
return this._updateContent(this._currentState);
},
/**
* Redraws the content of the client action according to its current state.
*
* @private
* @param {String} type the thread's type to display (e.g. 'mailbox_inbox',
* 'mailbox_starred', 'dm_chat'...).
* @returns {Promise}
*/
_updateContent: function(type) {
const inMailbox = type === "mailbox_inbox" || type === "mailbox_starred";
if (!inMailbox && this._isInInboxTab()) {
// We're leaving the inbox, so store the thread scrolltop
this._storeThreadState();
}
const previouslyInInbox = this._isInInboxTab();
this._currentState = type;
// Fetch content to display
let def = false;
if (inMailbox) {
def = this._fetchAndRenderThread();
} else {
const allChannels = this.call("mail_service", "getChannels");
const channels = _.filter(allChannels, function(channel) {
return channel.getType() === type;
});
def = this.call("mail_service", "getChannelPreviews", channels);
}
return def.then(previews => {
// Update content
if (inMailbox) {
if (!previouslyInInbox) {
this.$(".o_mail_discuss_tab_pane").remove();
this._$mainContent.append(this._threadWidget.$el);
this._$mainContent.append(this._extendedComposer.$el);
}
this._restoreThreadState();
} else {
this._threadWidget.$el.detach();
this._extendedComposer.$el.detach();
const $content = $(
QWeb.render("mail.discuss.MobileTabPane", {
previews: previews,
type: type,
})
);
this._prepareAddThreadInput(
$content.find(".o_mail_add_thread input"),
type
);
this._$mainContent.html($content);
}
// Update control panel
this.$buttons
.find("button")
.removeClass("d-block")
.addClass("d-none");
this.$buttons
.find(".o_mail_discuss_button_" + type)
.removeClass("d-none")
.addClass("d-block");
this.$buttons
.find(".o_mail_discuss_button_mark_all_read")
.toggleClass("d-none", type !== "mailbox_inbox")
.toggleClass("d-block", type === "mailbox_inbox");
this.$buttons
.find(".o_mail_discuss_button_unstar_all")
.toggleClass("d-none", type !== "mailbox_starred")
.toggleClass("d-block", type === "mailbox_starred");
// Update Mailbox page buttons
if (inMailbox) {
this.$(".o_mail_discuss_mobile_mailboxes_buttons").removeClass(
"o_hidden"
);
this.$(".o_mailbox_inbox_item")
.removeClass("btn-primary")
.addClass("btn-secondary");
this.$(".o_mailbox_inbox_item[data-type=" + type + "]")
.removeClass("btn-secondary")
.addClass("btn-primary");
} else {
this.$(".o_mail_discuss_mobile_mailboxes_buttons").addClass(
"o_hidden"
);
}
// Update bottom buttons
this.$(".o_mail_mobile_tab").removeClass("active");
// Mailbox_inbox and mailbox_starred share the same tab
const type_n = type === "mailbox_starred" ? "mailbox_inbox" : type;
this.$(".o_mail_mobile_tab[data-type=" + type_n + "]").addClass(
"active"
);
});
},
// --------------------------------------------------------------------------
// Handlers
// --------------------------------------------------------------------------
/**
* @override
* @private
*/
_onAddThread: function() {
this.$(".o_mail_add_thread")
.show()
.find("input")
.focus();
},
/**
* Switches to the clicked thread in the Inbox page (Inbox or Starred).
*
* @private
* @param {MouseEvent} ev
*/
_onClickMobileMailboxItem: function(ev) {
const mailboxID = $(ev.currentTarget).data("type");
this._setThread(mailboxID);
this._updateContent(this._thread.getID());
},
/**
* Switches to another tab.
*
* @private
* @param {MouseEvent} ev
*/
_onClickMobileTab: function(ev) {
const type = $(ev.currentTarget).data("type");
if (type === "mailbox") {
const inbox = this.call("mail_service", "getMailbox", "inbox");
this._setThread(inbox);
}
this._updateContent(type);
},
/**
* Opens a thread in a chat window (full screen in mobile).
*
* @private
* @param {MouseEvent} ev
*/
_onClickMobileMailPreview: function(ev) {
const threadID = $(ev.currentTarget).data("preview-id");
this.call("mail_service", "openThreadWindow", threadID);
},
});
});

View File

@@ -0,0 +1,494 @@
odoo.define("web_responsive.KanbanRendererMobile", function (require) {
"use strict";
/**
* The purpose of this file is to improve the UX of grouped kanban views in
* mobile. It includes the KanbanRenderer (in mobile only) to only display one
* column full width, and enables the swipe to browse to the other columns.
* Moreover, records in columns are lazy-loaded.
*/
var config = require("web.config");
var core = require("web.core");
var KanbanRenderer = require("web.KanbanRenderer");
var KanbanView = require("web.KanbanView");
var KanbanQuickCreate = require("web.kanban_column_quick_create");
var _t = core._t;
var qweb = core.qweb;
if (!config.device.isMobile) {
return;
}
KanbanQuickCreate.include({
init() {
this._super.apply(this, arguments);
this.isMobile = true;
},
});
KanbanView.include({
init() {
this._super.apply(this, arguments);
this.jsLibs.push("/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js");
},
});
KanbanRenderer.include({
custom_events: _.extend({}, KanbanRenderer.prototype.custom_events || {}, {
quick_create_column_created: "_onColumnAdded",
}),
events: _.extend({}, KanbanRenderer.prototype.events, {
"click .o_kanban_mobile_tab": "_onMobileTabClicked",
"click .o_kanban_mobile_add_column": "_onMobileQuickCreateClicked",
}),
ANIMATE: true, // Allows to disable animations for the tests
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.activeColumnIndex = 0; // Index of the currently displayed column
this._scrollPosition = null;
},
/**
* As this renderer defines its own scrolling area (the column in grouped
* mode), we override this hook to restore the scroll position like it was
* when the renderer has been last detached.
*
* @override
*/
on_attach_callback: function () {
if (
this._scrollPosition &&
this.state.groupedBy.length &&
this.widgets.length
) {
var $column = this.widgets[this.activeColumnIndex].$el;
$column.scrollLeft(this._scrollPosition.left);
$column.scrollTop(this._scrollPosition.top);
}
this._computeTabPosition();
this._super.apply(this, arguments);
},
/**
* As this renderer defines its own scrolling area (the column in grouped
* mode), we override this hook to store the scroll position, so that we can
* restore it if the renderer is re-attached to the DOM later.
*
* @override
*/
on_detach_callback: function () {
if (this.state.groupedBy.length && this.widgets.length) {
var $column = this.widgets[this.activeColumnIndex].$el;
this._scrollPosition = {
left: $column.scrollLeft(),
top: $column.scrollTop(),
};
} else {
this._scrollPosition = null;
}
this._super.apply(this, arguments);
},
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
/**
* Displays the quick create record in the active column
* override to open quick create record in current active column
*
* @override
* @returns {Promise}
*/
addQuickCreate: function () {
if (this._canCreateColumn() && !this.quickCreate.folded) {
this._onMobileQuickCreateClicked();
}
return this.widgets[this.activeColumnIndex].addQuickCreate();
},
/**
* Overrides to restore the left property and the scrollTop on the updated
* column, and to enable the swipe handlers
*
* @override
*/
updateColumn: function (localID) {
var index = _.findIndex(this.widgets, {db_id: localID});
var $column = this.widgets[index].$el;
var scrollTop = $column.scrollTop();
return (
this._super
.apply(this, arguments)
.then(() => this._layoutUpdate(false))
// Required when clicking on 'Load More'
.then(() => $column.scrollTop(scrollTop))
.then(() => this._enableSwipe())
);
},
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
/**
* Check if we use the quick create on mobile
* @returns {Boolean}
* @private
*/
_canCreateColumn: function () {
return this.quickCreateEnabled && this.quickCreate && this.widgets.length;
},
/**
* Update the columns positions
*
* @private
* @param {Boolean} [animate=false] set to true to animate
*/
_computeColumnPosition: function (animate) {
if (this.widgets.length) {
// Check rtl to compute correct css value
const rtl = _t.database.parameters.direction === "rtl";
// Display all o_kanban_group
this.$(".o_kanban_group").show();
const $columnAfter = this._toNode(
this.widgets.filter(
(widget, index) => index > this.activeColumnIndex
)
);
const promiseAfter = this._updateColumnCss(
$columnAfter,
rtl ? {right: "100%"} : {left: "100%"},
animate
);
const $columnBefore = this._toNode(
this.widgets.filter(
(widget, index) => index < this.activeColumnIndex
)
);
const promiseBefore = this._updateColumnCss(
$columnBefore,
rtl ? {right: "-100%"} : {left: "-100%"},
animate
);
const $columnCurrent = this._toNode(
this.widgets.filter(
(widget, index) => index === this.activeColumnIndex
)
);
const promiseCurrent = this._updateColumnCss(
$columnCurrent,
rtl ? {right: "0%"} : {left: "0%"},
animate
);
promiseAfter
.then(promiseBefore)
.then(promiseCurrent)
.then(() => {
$columnAfter.hide();
$columnBefore.hide();
});
}
},
/**
* Define the o_current class to the current selected kanban (column & tab)
*
* @private
*/
_computeCurrentColumn: function () {
if (this.widgets.length) {
var column = this.widgets[this.activeColumnIndex];
if (!column) {
return;
}
var columnID = column.id || column.db_id;
this.$(
".o_kanban_mobile_tab.o_current, .o_kanban_group.o_current"
).removeClass("o_current");
this.$(
'.o_kanban_group[data-id="' +
columnID +
'"], ' +
'.o_kanban_mobile_tab[data-id="' +
columnID +
'"]'
).addClass("o_current");
}
},
/**
* Update the tabs positions
*
* @private
*/
_computeTabPosition: function () {
this._computeTabJustification();
this._computeTabScrollPosition();
},
/**
* Update the tabs positions
*
* @private
*/
_computeTabScrollPosition: function () {
if (this.widgets.length) {
var lastItemIndex = this.widgets.length - 1;
var moveToIndex = this.activeColumnIndex;
var scrollToLeft = 0;
for (var i = 0; i < moveToIndex; i++) {
var columnWidth = this._getTabWidth(this.widgets[i]);
// Apply
if (moveToIndex !== lastItemIndex && i === moveToIndex - 1) {
var partialWidth = 0.75;
scrollToLeft += columnWidth * partialWidth;
} else {
scrollToLeft += columnWidth;
}
}
// Apply the scroll x on the tabs
// XXX in case of RTL, should we use scrollRight?
this.$(".o_kanban_mobile_tabs").scrollLeft(scrollToLeft);
}
},
/**
* Compute the justify content of the kanban tab headers
*
* @private
*/
_computeTabJustification: function () {
if (this.widgets.length) {
var self = this;
// Use to compute the sum of the width of all tab
var widthChilds = this.widgets.reduce(function (total, column) {
return total + self._getTabWidth(column);
}, 0);
// Apply a space around between child if the parent length is higher then the sum of the child width
var $tabs = this.$(".o_kanban_mobile_tabs");
$tabs.toggleClass(
"justify-content-between",
$tabs.outerWidth() >= widthChilds
);
}
},
/**
* Enables swipe event on the current column
*
* @private
*/
_enableSwipe: function () {
var self = this;
var step = _t.database.parameters.direction === "rtl" ? -1 : 1;
this.$el.swipe({
excludedElements: ".o_kanban_mobile_tabs",
swipeLeft: function () {
var moveToIndex = self.activeColumnIndex + step;
if (moveToIndex < self.widgets.length) {
self._moveToGroup(moveToIndex, self.ANIMATE);
}
},
swipeRight: function () {
var moveToIndex = self.activeColumnIndex - step;
if (moveToIndex > -1) {
self._moveToGroup(moveToIndex, self.ANIMATE);
}
},
});
},
/**
* Retrieve the outerWidth of a given widget column
*
* @param {KanbanColumn} column
* @returns {integer} outerWidth of the found column
* @private
*/
_getTabWidth: function (column) {
var columnID = column.id || column.db_id;
return this.$(
'.o_kanban_mobile_tab[data-id="' + columnID + '"]'
).outerWidth();
},
/**
* Update the kanban layout
*
* @private
* @param {Boolean} [animate=false] set to true to animate
*/
_layoutUpdate: function (animate) {
this._computeCurrentColumn();
this._computeTabPosition();
this._computeColumnPosition(animate);
this._enableSwipe();
},
/**
* Moves to the given kanban column
*
* @private
* @param {integer} moveToIndex index of the column to move to
* @param {Boolean} [animate=false] set to true to animate
* @returns {Promise} resolved when the new current group has been loaded
* and displayed
*/
_moveToGroup: function (moveToIndex, animate) {
var self = this;
if (moveToIndex >= 0 && moveToIndex < this.widgets.length) {
this.activeColumnIndex = moveToIndex;
}
var column = this.widgets[this.activeColumnIndex];
this._enableSwipe();
if (!column.data.isOpen) {
this.trigger_up("column_toggle_fold", {
db_id: column.db_id,
onSuccess: () => self._layoutUpdate(animate),
});
} else {
this._layoutUpdate(animate);
}
return Promise.resolve();
},
/**
* @override
* @private
*/
_renderExampleBackground: function () {
// Override to avoid display of example background
},
/**
* @override
* @private
*/
_renderGrouped: function (fragment) {
var self = this;
var newFragment = document.createDocumentFragment();
this._super.apply(this, [newFragment]);
this.defs.push(
Promise.all(this.defs).then(function () {
var data = [];
_.each(self.state.data, function (group) {
if (!group.value) {
group = _.extend({}, group, {value: _t("Undefined")});
data.unshift(group);
} else {
data.push(group);
}
});
var kanbanColumnContainer = document.createElement("div");
kanbanColumnContainer.classList.add("o_kanban_columns_content");
kanbanColumnContainer.appendChild(newFragment);
fragment.appendChild(kanbanColumnContainer);
$(
qweb.render("KanbanView.MobileTabs", {
data: data,
quickCreateEnabled: self._canCreateColumn(),
})
).prependTo(fragment);
})
);
},
/**
* @override
* @private
*/
_renderView: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
if (self.state.groupedBy.length) {
// Force first column for kanban view, because the groupedBy can be changed
return self._moveToGroup(0);
}
if (self._canCreateColumn()) {
self._onMobileQuickCreateClicked();
}
return Promise.resolve();
});
},
/**
* Retrieve the Jquery node (.o_kanban_group) for a list of a given widgets
*
* @private
* @param widgets
* @returns {jQuery} the matching .o_kanban_group widgets
*/
_toNode: function (widgets) {
const selectorCss = widgets
.map(
(widget) =>
'.o_kanban_group[data-id="' + (widget.id || widget.db_id) + '"]'
)
.join(", ");
return this.$(selectorCss);
},
/**
* Update the given column to the updated positions
*
* @private
* @param $column The jquery column
* @param cssProperties Use to update column
* @param {Boolean} [animate=false] set to true to animate
* @returns {Promise}
*/
_updateColumnCss: function ($column, cssProperties, animate) {
if (animate) {
return new Promise((resolve) =>
$column.animate(cssProperties, "fast", resolve)
);
}
$column.css(cssProperties);
return Promise.resolve();
},
// --------------------------------------------------------------------------
// Handlers
// --------------------------------------------------------------------------
/**
* @private
*/
_onColumnAdded: function () {
this._computeTabPosition();
if (this._canCreateColumn() && !this.quickCreate.folded) {
this.quickCreate.toggleFold();
}
},
/**
* @private
*/
_onMobileQuickCreateClicked: function (event) {
if (event) {
event.stopPropagation();
}
this.$(".o_kanban_group").toggle();
this.quickCreate.toggleFold();
},
/**
* @private
* @param {MouseEvent} event
*/
_onMobileTabClicked: function (event) {
if (this._canCreateColumn() && !this.quickCreate.folded) {
this.quickCreate.toggleFold();
}
this._moveToGroup($(event.currentTarget).index(), true);
},
});
});

View File

@@ -1,7 +1,8 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2018 Tecnativa - Sergey Shebanin
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define("web_responsive", function(require) {
odoo.define("web_responsive", function (require) {
"use strict";
const ActionManager = require("web.ActionManager");
@@ -13,16 +14,23 @@ odoo.define("web_responsive", function(require) {
const FormRenderer = require("web.FormRenderer");
const Menu = require("web.Menu");
const RelationalFields = require("web.relational_fields");
const Chatter = require("mail.Chatter");
const ListRenderer = require("web.ListRenderer");
const DocumentViewer = require("mail.DocumentViewer");
const CalendarRenderer = require("web.CalendarRenderer");
const patchMixin = require("web.patchMixin");
const AttachmentViewer = require("mail/static/src/components/attachment_viewer/attachment_viewer.js");
const PatchableAttachmentViewer = patchMixin(AttachmentViewer);
const ControlPanel = require("web.ControlPanel");
const SearchPanel = require("web/static/src/js/views/search_panel.js");
/* global owl */
const {QWeb, Context} = owl;
const {useState, useContext} = owl.hooks;
/* Hide AppDrawer in desktop and mobile modes.
* To avoid delays in pages with a lot of DOM nodes we make
* sub-groups' with 'querySelector' to improve the performance.
*/
function closeAppDrawer() {
_.defer(function() {
_.defer(function () {
// Need close AppDrawer?
var menu_apps_dropdown = document.querySelector(".o_menu_apps .dropdown");
$(menu_apps_dropdown)
@@ -115,7 +123,7 @@ odoo.define("web_responsive", function(require) {
*
* @override
*/
init: function(parent, menuData) {
init: function (parent, menuData) {
this._super.apply(this, arguments);
// Keep base64 icon for main menus
for (const n in this._apps) {
@@ -130,7 +138,7 @@ odoo.define("web_responsive", function(require) {
/**
* @override
*/
start: function() {
start: function () {
this.$search_container = this.$(".search-container");
this.$search_input = this.$(".search-input input");
this.$search_results = this.$(".search-results");
@@ -142,7 +150,7 @@ odoo.define("web_responsive", function(require) {
*
* @override
*/
_onAppsMenuItemClicked: function(ev) {
_onAppsMenuItemClicked: function (ev) {
this._super.apply(this, arguments);
ev.preventDefault();
ev.stopPropagation();
@@ -157,7 +165,7 @@ odoo.define("web_responsive", function(require) {
* @returns {Object}
* Menu definition, plus extra needed keys.
*/
_menuInfo: function(key) {
_menuInfo: function (key) {
const original = this._searchableMenus[key];
return _.extend(
{
@@ -170,7 +178,7 @@ odoo.define("web_responsive", function(require) {
/**
* Autofocus on search field on big screens.
*/
_searchFocus: function() {
_searchFocus: function () {
if (!config.device.isMobile) {
// This timeout is necessary since the menu has a 100ms fading animation
setTimeout(() => this.$search_input.focus(), 100);
@@ -180,7 +188,7 @@ odoo.define("web_responsive", function(require) {
/**
* Reset search input and results
*/
_searchReset: function() {
_searchReset: function () {
this.$search_container.removeClass("has-results");
this.$search_results.empty();
this.$search_input.val("");
@@ -189,8 +197,8 @@ odoo.define("web_responsive", function(require) {
/**
* Schedule a search on current menu items.
*/
_searchMenusSchedule: function() {
this._search_def = new Promise(resolve => {
_searchMenusSchedule: function () {
this._search_def = new Promise((resolve) => {
setTimeout(resolve, 50);
});
this._search_def.then(this._searchMenus.bind(this));
@@ -199,7 +207,7 @@ odoo.define("web_responsive", function(require) {
/**
* Search among available menu items, and render that search.
*/
_searchMenus: function() {
_searchMenus: function () {
const query = this.$search_input.val();
if (query === "") {
this.$search_container.removeClass("has-results");
@@ -224,7 +232,7 @@ odoo.define("web_responsive", function(require) {
*
* @param {jQuery.Event} event
*/
_searchResultChosen: function(event) {
_searchResultChosen: function (event) {
event.preventDefault();
event.stopPropagation();
const $result = $(event.currentTarget),
@@ -238,7 +246,7 @@ odoo.define("web_responsive", function(require) {
previous_menu_id: data.parentId,
});
// Find app that owns the chosen menu
const app = _.find(this._apps, function(_app) {
const app = _.find(this._apps, function (_app) {
return text.indexOf(_app.name + suffix) === 0;
});
// Update navbar menus
@@ -250,7 +258,7 @@ odoo.define("web_responsive", function(require) {
*
* @param {jQuery.Event} event
*/
_searchResultsNavigate: function(event) {
_searchResultsNavigate: function (event) {
// Find current results and active element (1st by default)
const all = this.$search_results.find(".o-menu-search-result"),
pre_focused = all.filter(".active") || $(all[0]);
@@ -301,7 +309,7 @@ odoo.define("web_responsive", function(require) {
/*
* Control if AppDrawer can be closed
*/
_hideAppsMenu: function() {
_hideAppsMenu: function () {
return !this.$("input").is(":focus");
},
});
@@ -313,7 +321,7 @@ odoo.define("web_responsive", function(require) {
*
* @override
*/
canBeDiscarded: function(recordID) {
canBeDiscarded: function (recordID) {
if (this.model.isDirty(recordID || this.handle)) {
closeAppDrawer();
}
@@ -332,7 +340,7 @@ odoo.define("web_responsive", function(require) {
Menu.prototype.events
),
start: function() {
start: function () {
this.$menu_toggle = this.$(".o-menu-toggle");
return this._super.apply(this, arguments);
},
@@ -340,7 +348,7 @@ odoo.define("web_responsive", function(require) {
/**
* Hide menus for current app if you're in mobile
*/
_hideMobileSubmenus: function() {
_hideMobileSubmenus: function () {
if (
config.device.isMobile &&
this.$menu_toggle.is(":visible") &&
@@ -355,7 +363,7 @@ odoo.define("web_responsive", function(require) {
*
* @param {ClickEvent} ev
*/
_onClickMenuItem: function(ev) {
_onClickMenuItem: function (ev) {
ev.stopPropagation();
},
@@ -364,7 +372,7 @@ odoo.define("web_responsive", function(require) {
*
* @override
*/
_updateMenuBrand: function() {
_updateMenuBrand: function () {
if (!config.device.isMobile) {
return this._super.apply(this, arguments);
}
@@ -377,10 +385,10 @@ odoo.define("web_responsive", function(require) {
*
* @override
*/
_setState: function() {
_setState: function () {
this._super.apply(this, arguments);
if (config.device.isMobile) {
_.map(this.status_information, value => {
_.map(this.status_information, (value) => {
value.fold = true;
});
}
@@ -389,7 +397,7 @@ odoo.define("web_responsive", function(require) {
// Sticky Column Selector
ListRenderer.include({
_renderView: function() {
_renderView: function () {
const self = this;
return this._super.apply(this, arguments).then(() => {
const $col_selector = self.$el.find(
@@ -402,7 +410,7 @@ odoo.define("web_responsive", function(require) {
});
},
_onToggleOptionalColumnDropdown: function(ev) {
_onToggleOptionalColumnDropdown: function (ev) {
// FIXME: For some strange reason the 'stopPropagation' call
// in the main method don't work. Invoking here the same method
// does the expected behavior... O_O!
@@ -420,17 +428,16 @@ odoo.define("web_responsive", function(require) {
*
* @override
*/
_renderHeaderButtons: function() {
_renderHeaderButtons: function () {
const $buttons = this._super.apply(this, arguments);
if (
!config.device.isMobile ||
!$buttons.is(":has(>:not(.o_invisible_modifier))")
$buttons.children("button:not(.o_invisible_modifier)").length <= 2
) {
return $buttons;
}
// $buttons must be appended by JS because all events are bound
$buttons.addClass("dropdown-menu");
const $dropdown = $(
core.qweb.render("web_responsive.MenuStatusbarButtons")
);
@@ -439,18 +446,13 @@ odoo.define("web_responsive", function(require) {
},
});
// Chatter Hide Composer
Chatter.include({
_openComposer: function(options) {
if (
this._composer &&
options.isLog === this._composer.options.isLog &&
this._composer.$el.is(":visible")
) {
this._closeComposer(false);
} else {
this._super.apply(this, arguments);
CalendarRenderer.include({
_getFullCalendarOptions: function () {
var options = this._super.apply(this, arguments);
if (config.device.isMobile) {
options.views.dayGridMonth.columnHeaderFormat = "ddd";
}
return options;
},
});
@@ -459,7 +461,7 @@ odoo.define("web_responsive", function(require) {
/**
* @override
*/
_appendController: function() {
_appendController: function () {
this._super.apply(this, arguments);
closeAppDrawer();
},
@@ -495,7 +497,7 @@ odoo.define("web_responsive", function(require) {
* @returns {keyEvent}
* Altered event object
*/
_shiftPressed: function(keyEvent) {
_shiftPressed: function (keyEvent) {
const alt = keyEvent.altKey || keyEvent.key === "Alt",
newEvent = _.extend({}, keyEvent),
shift = keyEvent.shiftKey || keyEvent.key === "Shift";
@@ -509,11 +511,11 @@ odoo.define("web_responsive", function(require) {
return newEvent;
},
_onKeyDown: function(keyDownEvent) {
_onKeyDown: function (keyDownEvent) {
return this._super(this._shiftPressed(keyDownEvent));
},
_onKeyUp: function(keyUpEvent) {
_onKeyUp: function (keyUpEvent) {
return this._super(this._shiftPressed(keyUpEvent));
},
};
@@ -522,44 +524,106 @@ odoo.define("web_responsive", function(require) {
// `KeyboardNavigationMixin` is used upstream
AbstractWebClient.include(KeyboardNavigationShiftAltMixin);
// DocumentViewer: Add support to maximize/minimize
DocumentViewer.include({
// Widget 'keydown' and 'keyup' events are only dispatched when
// this.$el is active, but now the modal have buttons that can obtain
// the focus. For this reason we now listen core events, that are
// dispatched every time.
events: _.extend(
_.omit(DocumentViewer.prototype.events, ["keydown", "keyup"]),
{
"click .o_maximize_btn": "_onClickMaximize",
"click .o_minimize_btn": "_onClickMinimize",
"shown.bs.modal": "_onShownModal",
}
),
start: function() {
core.bus.on("keydown", this, this._onKeydown);
core.bus.on("keyup", this, this._onKeyUp);
return this._super.apply(this, arguments);
},
destroy: function() {
core.bus.off("keydown", this, this._onKeydown);
core.bus.off("keyup", this, this._onKeyUp);
this._super.apply(this, arguments);
},
_onShownModal: function() {
// Disable auto-focus to allow to use controls in edit mode.
// This only affects the active modal.
// More info: https://stackoverflow.com/a/14795256
$(document).off("focusin.modal");
},
_onClickMaximize: function() {
this.$el.removeClass("o_responsive_document_viewer");
},
_onClickMinimize: function() {
this.$el.addClass("o_responsive_document_viewer");
},
// TODO: use default odoo device context when it will be realized
const deviceContext = new Context({
isMobile: config.device.isMobile,
size_class: config.device.size_class,
SIZES: config.device.SIZES,
});
window.addEventListener(
"resize",
owl.utils.debounce(() => {
const state = deviceContext.state;
if (state.isMobile !== config.device.isMobile) {
state.isMobile = !state.isMobile;
}
if (state.size_class !== config.device.size_class) {
state.size_class = config.device.size_class;
}
}, 15)
);
// Patch attachment viewer to add min/max buttons capability
PatchableAttachmentViewer.patch("web_responsive.AttachmentViewer", (T) => {
class AttachmentViewerPatchResponsive extends T {
constructor() {
super(...arguments);
this.state = useState({
maximized: false,
});
}
// Disable auto-close to allow to use form in edit mode.
isCloseable() {
return false;
}
}
return AttachmentViewerPatchResponsive;
});
QWeb.components.AttachmentViewer = PatchableAttachmentViewer;
// Patch control panel to add states for mobile quick search
ControlPanel.patch("web_responsive.ControlPanelMobile", (T) => {
class ControlPanelPatchResponsive extends T {
constructor() {
super(...arguments);
this.state = useState({
mobileSearchMode: "",
});
this.device = useContext(deviceContext);
}
}
return ControlPanelPatchResponsive;
});
// Patch search panel to add functionality for mobile view
SearchPanel.patch("web_responsive.SearchPanelMobile", (T) => {
class SearchPanelPatchResponsive extends T {
constructor() {
super(...arguments);
this.state.mobileSearch = false;
this.device = useContext(deviceContext);
}
getActiveSummary() {
const selection = [];
for (const filter of this.model.get("sections")) {
let filterValues = [];
if (filter.type === "category") {
if (filter.activeValueId) {
const parentIds = this._getAncestorValueIds(
filter,
filter.activeValueId
);
filterValues = [...parentIds, filter.activeValueId].map(
(valueId) => filter.values.get(valueId).display_name
);
}
} else {
let values = [];
if (filter.groups) {
values = [
...filter.groups.values().map((g) => g.values),
].flat();
}
if (filter.values) {
values = [...filter.values.values()];
}
filterValues = values
.filter((v) => v.checked)
.map((v) => v.display_name);
}
if (filterValues.length) {
selection.push({
values: filterValues,
icon: filter.icon,
color: filter.color,
type: filter.type,
});
}
}
return selection;
}
}
return SearchPanelPatchResponsive;
});
return {
deviceContext: deviceContext,
};
});

View File

@@ -7,8 +7,9 @@
<t t-jquery=".o_app" t-operation="attributes">
<attribute
name="t-attf-href"
t-translation="off"
>#menu_id=#{app.menuID}&amp;action_id=#{app.actionID}</attribute>
<attribute name="draggable">false</attribute>
<attribute name="draggable" t-translation="off">false</attribute>
</t>
<!-- App icons should be more than a text -->
<t t-jquery=".o_app &gt; t" t-operation="replace">

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2019 Tecnativa - Alexandre Díaz
Copyright 2021 Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-inherit="mail.Dialog" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_Dialog')]" position="attributes">
<attribute
name="t-attf-class"
t-translation="off"
>o_Dialog_{{dialog.record['constructor'].name}}</attribute>
</xpath>
</t>
<t t-inherit="mail.AttachmentViewer" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_AttachmentViewer')]" position="attributes">
<attribute
name="t-att-class"
t-translation="off"
>state.maximized ? 'o_AttachmentViewer_maximized' : ''</attribute>
</xpath>
<xpath
expr="//div[hasclass('o_AttachmentViewer_header')]/div[hasclass('o-autogrow')]"
position="after"
>
<div
t-if="!state.maximized"
class="o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton o_AttachmentViewer_headerItemButtonMaximize"
t-on-click="state.maximized=true"
role="button"
title="Maximize"
aria-label="Maximize"
>
<i class="fa fa-fw fa-window-maximize" role="img" />
</div>
<div
t-if="state.maximized"
class="o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton o_AttachmentViewer_headerItemButtonMinimize"
t-on-click="state.maximized=false"
role="button"
title="Minimize"
aria-label="Minimize"
>
<i class="fa fa-fw fa-window-minimize" role="img" />
</div>
</xpath>
</t>
</template>

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2021 Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-inherit="web.ControlPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//nav[hasclass('o_cp_switch_buttons')]" position="replace">
<nav
t-if="props.views.length gt 1"
class="btn-group o_cp_switch_buttons"
role="toolbar"
aria-label="View switcher"
>
<t
t-set="collapse_switchview"
t-value="device.size_class &lt;= device.SIZES.LG"
/>
<button
t-if="collapse_switchview"
class="btn btn-link btn-sm"
data-toggle="dropdown"
aria-expanded="false"
>
<span
t-attf-class="fa fa-lg o_switch_view o_{{ env.view.type }} {{ props.views.filter(view => view.type === env.view.type)[0].icon }}"
/>
</button>
<ul
t-if="collapse_switchview"
class="dropdown-menu dropdown-menu-right list-inline"
>
<li t-foreach="props.views" t-as="view" t-key="view.type">
<t t-call="web.ViewSwitcherButton" />
</li>
</ul>
<t
t-if="!collapse_switchview"
t-foreach="props.views"
t-as="view"
t-key="view.type"
>
<t t-call="web.ViewSwitcherButton" />
</t>
</nav>
</xpath>
<xpath expr="//div[hasclass('o_searchview')]" position="replace">
<div
t-if="props.withSearchBar"
class="o_searchview"
t-att-class="state.mobileSearchMode == 'quick' ? 'o_searchview_quick' : 'o_searchview_mobile'"
role="search"
aria-autocomplete="list"
t-on-click.self="state.mobileSearchMode = device.isMobile ? 'quick' : ''"
>
<t t-if="!device.isMobile">
<i
class="o_searchview_icon fa fa-search"
title="Search..."
role="img"
aria-label="Search..."
/>
<SearchBar fields="fields" />
</t>
<t t-if="device.isMobile and state.mobileSearchMode == 'quick'">
<button
class="btn btn-link fa fa-arrow-left"
t-on-click.stop="state.mobileSearchMode = ''"
/>
<SearchBar fields="fields" />
<button
class="btn fa fa-filter"
t-on-click.stop="state.mobileSearchMode = 'full'"
/>
</t>
<t
t-if="device.isMobile and state.mobileSearchMode == 'full'"
t-call="web_responsive.MobileSearchView"
/>
<t t-if="device.isMobile and state.mobileSearchMode == ''">
<button
class="btn btn-link fa fa-search"
t-on-click.stop="state.mobileSearchMode = 'quick'"
/>
</t>
</div>
</xpath>
<xpath expr="//div[hasclass('o_cp_top_left')]" position="attributes">
<attribute
name="t-att-class"
t-translation="off"
>device.isMobile and state.mobileSearchMode == 'quick' ? 'o_hidden' : ''</attribute>
</xpath>
<xpath expr="//div[hasclass('o_search_options')]" position="attributes">
<attribute name="t-if" t-translation="off">!device.isMobile</attribute>
<attribute
name="t-att-class"
t-translation="off"
>device.size_class == device.SIZES.MD ? 'o_search_options_hide_labels' : ''</attribute>
</xpath>
</t>
<t t-name="web_responsive.MobileSearchView" owl="1">
<div class="o_mobile_search">
<div class="o_mobile_search_header">
<span
class="o_mobile_search_close float-left mt16 mb16 mr8 ml16"
t-on-click.stop="state.mobileSearchMode = 'quick'"
>
<i class="fa fa-arrow-left" />
<strong class="float-right ml8">FILTER</strong>
</span>
<span
class="float-right o_mobile_search_clear_facets mt16 mr16"
t-on-click.stop="model.dispatch('clearQuery')"
>
<t>CLEAR</t>
</span>
</div>
<SearchBar fields="fields" />
<div class="o_mobile_search_filter o_search_options mb8 mt8 ml16 mr16">
<FilterMenu
t-if="props.searchMenuTypes.includes('filter')"
class="o_filter_menu"
fields="fields"
/>
<GroupByMenu
t-if="props.searchMenuTypes.includes('groupBy')"
class="o_group_by_menu"
fields="fields"
/>
<ComparisonMenu
t-if="props.searchMenuTypes.includes('comparison') and model.get('filters', f => f.type === 'comparison').length"
class="o_comparison_menu"
/>
<FavoriteMenu
t-if="props.searchMenuTypes.includes('favorite')"
class="o_favorite_menu"
/>
</div>
<div
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
t-on-click.stop="state.mobileSearchMode = ''"
>
<t>SEE RESULT</t>
</div>
</div>
</t>
</templates>

View File

@@ -7,9 +7,10 @@
t-jquery=".o_mail_discuss_button_multi_user_channel"
t-operation="attributes"
>
<attribute
name="class"
>btn btn-secondary o_mail_discuss_button_multi_user_channel d-md-block d-none</attribute>
<attribute name="class" t-translation="off">
btn btn-secondary o_mail_discuss_button_multi_user_channel d-md-block
d-none
</attribute>
</t>
</t>
</template>

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2019 Tecnativa - Alexandre Díaz
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-extend="DocumentViewer">
<t t-jquery=".o_modal_fullscreen" t-operation="attributes">
<attribute
name="class"
>modal o_modal_fullscreen o_document_viewer o_responsive_document_viewer</attribute>
<attribute name="data-backdrop">false</attribute>
</t>
</t>
<t t-extend="DocumentViewer.Content">
<t t-jquery=".o_close_btn" t-operation="replace">
<div class="o_buttons float-right mr8">
<a
role="button"
class="mr8 o_maximize_btn"
tabindex="0"
aria-label="Maximize"
title="Maximize"
>
<i class="fa fa-window-maximize" />
</a>
<a
role="button"
class="mr8 o_minimize_btn"
tabindex="0"
aria-label="Minimize"
title="Minimize"
>
<i class="fa fa-window-minimize" />
</a>
<a
role="button"
class="o_close_btn"
tabindex="0"
aria-label="Close"
title="Close"
>
<i class="fa fa-close" />
</a>
</div>
</t>
</t>
</template>

View File

@@ -98,40 +98,13 @@
</t>
</t>
</t>
<t t-extend="Sidebar">
<!-- Replace some common sections by icons in mobile -->
<t
t-jquery=".o_dropdown_toggler_btn t[t-esc='section.label']"
t-operation="replace"
>
<t t-set="label" t-value="section.label" />
<t t-if="section.name == 'files'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'paperclip'" />
</t>
<t t-extend="CalendarView.navigation_buttons">
<!-- Add responsive icons to buttons -->
<t t-jquery=".o_calendar_button_today" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'calendar-check-o'" />
<t t-set="label">Today</t>
</t>
<t t-elif="section.name == 'print'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'print'" />
</t>
</t>
<t t-elif="section.name == 'other'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'wrench'" />
</t>
</t>
<t t-else="">
<span t-esc="label" />
</t>
</t>
</t>
<t t-extend="mail.Chatter">
<t t-jquery=".o_chatter_topbar" t-operation="replace">
<div class="o_chatter_header_container">
<div class="o_chatter_topbar">
<div class="o_topbar_right_area" />
</div>
</div>
</t>
</t>
</templates>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2017-2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<templates>
<t t-extend="Menu">
<t t-jquery=".o_menu_apps" t-operation="after">
<!-- Hamburger button to show submenus in sm screens -->
@@ -14,4 +14,4 @@
</button>
</t>
</t>
</template>
</templates>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2021 Sergey Shebanin
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates>
<t t-inherit="web.SearchPanel" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o_search_panel')]" position="inside">
<div
t-if="device.isMobile"
class="o_search_panel_summary"
t-on-click.stop="state.mobileSearch = true"
>
<div class="d-flex flex-wrap align-items-center">
<i class="fa fa-fw fa-filter mr-1" />
<t t-set="filters" t-value="getActiveSummary()" />
<span t-foreach="filters" t-as="filter" class="mx-1">
<i
t-if="filter.icon"
t-attf-class="fa {{ filter.icon }} mr-2"
t-att-style="filter.color and ('color: ' + filter.color)"
/>
<t
t-esc="filter.values.join(filter.type == 'category' ? ' / ' : ', ')"
/>
</span>
<t t-if="!filters.length">All</t>
</div>
</div>
<div
class="o_search_panel_content"
t-att-class="device.isMobile ? (state.mobileSearch ? 'o_mobile_search' : 'd-none'): ''"
/>
</xpath>
<xpath expr="//div[hasclass('o_search_panel_content')]" position="inside">
<div t-if="device.isMobile" class="o_mobile_search_header">
<span
class="o_mobile_search_close float-left mt16 mb16 mr8 ml16"
t-on-click.stop="state.mobileSearch = false"
>
<i class="fa fa-arrow-left" />
<strong class="float-right ml8">FILTER</strong>
</span>
</div>
<xpath expr="//section" position="move" />
<div
t-if="device.isMobile"
class="btn btn-primary o_mobile_search_show_result fixed-bottom"
t-on-click.stop="state.mobileSearch = false"
>
<t>SEE RESULT</t>
</div>
</xpath>
</t>
</templates>