[IMP] web: Calendar: mobile UX improvements
The following tweaks have been applied on mobile devices: - remove left and right arrow in control panel - enable swipe navigation (instead of left/right arrows) - replace 'Today' button by a calendar icon - remove static text 'Attendees:' in events - shorten header content in week mode to prevent them from overlapping - ensure that the calendar fits in the screen Also introduce a mobile test suite for the calendar view. Task #31449
This commit is contained in:
@@ -10,6 +10,7 @@ odoo.define('web.CalendarController', function (require) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var AbstractController = require('web.AbstractController');
|
var AbstractController = require('web.AbstractController');
|
||||||
|
var config = require('web.config');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var Dialog = require('web.Dialog');
|
var Dialog = require('web.Dialog');
|
||||||
var dialogs = require('web.view_dialogs');
|
var dialogs = require('web.view_dialogs');
|
||||||
@@ -23,8 +24,10 @@ var CalendarController = AbstractController.extend({
|
|||||||
changeDate: '_onChangeDate',
|
changeDate: '_onChangeDate',
|
||||||
changeFilter: '_onChangeFilter',
|
changeFilter: '_onChangeFilter',
|
||||||
dropRecord: '_onDropRecord',
|
dropRecord: '_onDropRecord',
|
||||||
|
next: '_onNext',
|
||||||
openCreate: '_onOpenCreate',
|
openCreate: '_onOpenCreate',
|
||||||
openEvent: '_onOpenEvent',
|
openEvent: '_onOpenEvent',
|
||||||
|
prev: '_onPrev',
|
||||||
quickCreate: '_onQuickCreate',
|
quickCreate: '_onQuickCreate',
|
||||||
toggleFullWidth: '_onToggleFullWidth',
|
toggleFullWidth: '_onToggleFullWidth',
|
||||||
updateRecord: '_onUpdateRecord',
|
updateRecord: '_onUpdateRecord',
|
||||||
@@ -73,15 +76,16 @@ var CalendarController = AbstractController.extend({
|
|||||||
*/
|
*/
|
||||||
renderButtons: function ($node) {
|
renderButtons: function ($node) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.$buttons = $(QWeb.render("CalendarView.buttons", {'widget': this}));
|
this.$buttons = $(QWeb.render('CalendarView.buttons', {
|
||||||
|
isMobile: config.device.isMobile,
|
||||||
|
}));
|
||||||
this.$buttons.on('click', 'button.o_calendar_button_new', function () {
|
this.$buttons.on('click', 'button.o_calendar_button_new', function () {
|
||||||
self.trigger_up('switch_view', {view_type: 'form'});
|
self.trigger_up('switch_view', {view_type: 'form'});
|
||||||
});
|
});
|
||||||
|
|
||||||
_.each(['prev', 'today', 'next'], function (action) {
|
_.each(['prev', 'today', 'next'], function (action) {
|
||||||
self.$buttons.on('click', '.o_calendar_button_' + action, function () {
|
self.$buttons.on('click', '.o_calendar_button_' + action, function () {
|
||||||
self.model[action]();
|
self._move(action);
|
||||||
self.reload();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
_.each(['day', 'week', 'month'], function (scale) {
|
_.each(['day', 'week', 'month'], function (scale) {
|
||||||
@@ -105,6 +109,18 @@ var CalendarController = AbstractController.extend({
|
|||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Move to the requested direction and reload the view
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} to either 'prev', 'next' or 'today'
|
||||||
|
* @returns {Deferred}
|
||||||
|
*/
|
||||||
|
_move: function (to) {
|
||||||
|
this.model[to]();
|
||||||
|
return this.reload();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
* @param {Object} record
|
* @param {Object} record
|
||||||
* @param {integer} record.id
|
* @param {integer} record.id
|
||||||
* @returns {Deferred}
|
* @returns {Deferred}
|
||||||
@@ -156,6 +172,14 @@ var CalendarController = AbstractController.extend({
|
|||||||
_onDropRecord: function (event) {
|
_onDropRecord: function (event) {
|
||||||
this._updateRecord(event.data);
|
this._updateRecord(event.data);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {OdooEvent} event
|
||||||
|
*/
|
||||||
|
_onNext: function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this._move('next');
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {OdooEvent} event
|
* @param {OdooEvent} event
|
||||||
@@ -305,6 +329,15 @@ var CalendarController = AbstractController.extend({
|
|||||||
};
|
};
|
||||||
open_dialog(true);
|
open_dialog(true);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {OdooEvent} event
|
||||||
|
*/
|
||||||
|
_onPrev: function () {
|
||||||
|
event.stopPropagation();
|
||||||
|
this._move('prev');
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles saving data coming from quick create box
|
* Handles saving data coming from quick create box
|
||||||
*
|
*
|
||||||
@@ -364,8 +397,7 @@ var CalendarController = AbstractController.extend({
|
|||||||
this.$buttons.find('.active').removeClass('active');
|
this.$buttons.find('.active').removeClass('active');
|
||||||
this.$buttons.find('.o_calendar_button_' + this.mode).addClass('active');
|
this.$buttons.find('.o_calendar_button_' + this.mode).addClass('active');
|
||||||
}
|
}
|
||||||
var subtitle = (this.mode === 'week' ? _t('Week ') : '') + event.data.title;
|
this.set({title: this.displayName + ' (' + event.data.title + ')'});
|
||||||
this.set({title: this.displayName + ' (' + subtitle + ')'});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ var Context = require('web.Context');
|
|||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var fieldUtils = require('web.field_utils');
|
var fieldUtils = require('web.field_utils');
|
||||||
var session = require('web.session');
|
var session = require('web.session');
|
||||||
var time = require('web.time');
|
|
||||||
|
|
||||||
var _t = core._t;
|
var _t = core._t;
|
||||||
|
|
||||||
@@ -400,12 +399,6 @@ return AbstractModel.extend({
|
|||||||
weekNumbers: true,
|
weekNumbers: true,
|
||||||
weekNumberTitle: _t("W"),
|
weekNumberTitle: _t("W"),
|
||||||
allDayText: _t("All day"),
|
allDayText: _t("All day"),
|
||||||
views: {
|
|
||||||
week: {
|
|
||||||
columnFormat: 'ddd ' + time.getLangDateFormat(),
|
|
||||||
titleFormat: time.getLangTimeFormat(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
monthNames: moment.months(),
|
monthNames: moment.months(),
|
||||||
monthNamesShort: moment.monthsShort(),
|
monthNamesShort: moment.monthsShort(),
|
||||||
dayNames: moment.weekdays(),
|
dayNames: moment.weekdays(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ odoo.define('web.CalendarRenderer', function (require) {
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var AbstractRenderer = require('web.AbstractRenderer');
|
var AbstractRenderer = require('web.AbstractRenderer');
|
||||||
|
var config = require('web.config');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var Dialog = require('web.Dialog');
|
var Dialog = require('web.Dialog');
|
||||||
var field_utils = require('web.field_utils');
|
var field_utils = require('web.field_utils');
|
||||||
@@ -192,6 +193,9 @@ return AbstractRenderer.extend({
|
|||||||
start: function () {
|
start: function () {
|
||||||
this._initSidebar();
|
this._initSidebar();
|
||||||
this._initCalendar();
|
this._initCalendar();
|
||||||
|
if (config.device.isMobile) {
|
||||||
|
this._bindSwipe();
|
||||||
|
}
|
||||||
return this._super();
|
return this._super();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -289,6 +293,27 @@ return AbstractRenderer.extend({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
|
* Bind handlers to enable swipe navigation
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_bindSwipe: function () {
|
||||||
|
var self = this;
|
||||||
|
var touchStartX;
|
||||||
|
var touchEndX;
|
||||||
|
this.$calendar.on('touchstart', function (event) {
|
||||||
|
touchStartX = event.originalEvent.touches[0].pageX;
|
||||||
|
});
|
||||||
|
this.$calendar.on('touchend', function (event) {
|
||||||
|
touchEndX = event.originalEvent.changedTouches[0].pageX;
|
||||||
|
if (touchStartX - touchEndX > 100) {
|
||||||
|
self.trigger_up('next');
|
||||||
|
} else if (touchStartX - touchEndX < -100) {
|
||||||
|
self.trigger_up('prev');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
* @param {any} event
|
* @param {any} event
|
||||||
* @returns {string} the html for the rendered event
|
* @returns {string} the html for the rendered event
|
||||||
*/
|
*/
|
||||||
@@ -297,6 +322,7 @@ return AbstractRenderer.extend({
|
|||||||
event: event,
|
event: event,
|
||||||
fields: this.state.fields,
|
fields: this.state.fields,
|
||||||
format: this._format.bind(this),
|
format: this._format.bind(this),
|
||||||
|
isMobile: config.device.isMobile,
|
||||||
read_only_mode: this.read_only_mode,
|
read_only_mode: this.read_only_mode,
|
||||||
record: event.record,
|
record: event.record,
|
||||||
user_context: session.user_context,
|
user_context: session.user_context,
|
||||||
@@ -376,11 +402,9 @@ return AbstractRenderer.extend({
|
|||||||
viewRender: function (view) {
|
viewRender: function (view) {
|
||||||
// compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay'
|
// compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay'
|
||||||
var mode = view.name === 'month' ? 'month' : (view.name === 'agendaWeek' ? 'week' : 'day');
|
var mode = view.name === 'month' ? 'month' : (view.name === 'agendaWeek' ? 'week' : 'day');
|
||||||
// compute title: in week mode, display the week number
|
|
||||||
var title = mode === 'week' ? view.intervalStart.week() : view.title;
|
|
||||||
self.trigger_up('viewUpdated', {
|
self.trigger_up('viewUpdated', {
|
||||||
mode: mode,
|
mode: mode,
|
||||||
title: title,
|
title: view.title,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
height: 'parent',
|
height: 'parent',
|
||||||
|
|||||||
@@ -66,9 +66,6 @@
|
|||||||
}
|
}
|
||||||
.fc-view {
|
.fc-view {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
> table {
|
|
||||||
min-width: 600px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,12 +199,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.o_calendar_container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
.o_attendee_head {
|
.o_attendee_head {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
@@ -220,3 +211,31 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mobile rules
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.o_calendar_container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: @screen-xs-max) {
|
||||||
|
.o_control_panel {
|
||||||
|
.o_cp_left {
|
||||||
|
width: 100%;
|
||||||
|
.o_cp_buttons {
|
||||||
|
width: 100%;
|
||||||
|
.o_calendar_button_today {
|
||||||
|
float: right;
|
||||||
|
> .o_calendar_text {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.o_calendar_view {
|
||||||
|
.fc-day-header {
|
||||||
|
word-spacing: 1000px; // force line break in week mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
<t t-foreach="widget.displayFields" t-as="name">
|
<t t-foreach="widget.displayFields" t-as="name">
|
||||||
<div t-attf-class="o_field_#{name} o_field_type_#{fields[name].type}">
|
<div t-attf-class="o_field_#{name} o_field_type_#{fields[name].type}">
|
||||||
<t t-if="widget.displayFields[name].avatar_field">
|
<t t-if="widget.displayFields[name].avatar_field">
|
||||||
<t t-esc="fields[name].string"/>:
|
<t t-if="!isMobile"><t t-esc="fields[name].string"/>:</t>
|
||||||
<div class="o_calendar_avatars pull-right">
|
<div class="o_calendar_avatars pull-right">
|
||||||
<t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,3)" t-as="image"><t t-raw="image"/></t>
|
<t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,3)" t-as="image"><t t-raw="image"/></t>
|
||||||
<span t-if="record[name].length - 3 > 0">+<t t-esc="record[name].length - 3"/></span>
|
<span t-if="record[name].length - 3 > 0">+<t t-esc="record[name].length - 3"/></span>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
@@ -63,10 +63,17 @@
|
|||||||
|
|
||||||
<t t-name="CalendarView.buttons">
|
<t t-name="CalendarView.buttons">
|
||||||
<div class="o_calendar_buttons">
|
<div class="o_calendar_buttons">
|
||||||
<button class="o_calendar_button_prev btn btn-sm btn-primary"><span class="fa fa-arrow-left"/></button>
|
<t t-if="isMobile">
|
||||||
<button class="o_calendar_button_today btn btn-sm btn-primary">Today</button>
|
<span class="fa-stack o_calendar_button_today">
|
||||||
<button class="o_calendar_button_next btn btn-sm btn-primary"><span class="fa fa-arrow-right"/></button>
|
<i class="fa fa-calendar-o fa-stack-2x"></i>
|
||||||
|
<strong class="o_calendar_text fa-stack-1x"><t t-esc="moment().date()"/></strong>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<button class="o_calendar_button_prev btn btn-sm btn-primary"><span class="fa fa-arrow-left"/></button>
|
||||||
|
<button class="o_calendar_button_today btn btn-sm btn-primary">Today</button>
|
||||||
|
<button class="o_calendar_button_next btn btn-sm btn-primary"><span class="fa fa-arrow-right"/></button>
|
||||||
|
</t>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<button type="button" class="o_calendar_button_day btn btn-sm btn-default">Day</button>
|
<button type="button" class="o_calendar_button_day btn btn-sm btn-default">Day</button>
|
||||||
<button type="button" class="o_calendar_button_week btn btn-sm btn-default">Week</button>
|
<button type="button" class="o_calendar_button_week btn btn-sm btn-default">Week</button>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
odoo.define('web.calendar_mobile_tests', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var CalendarView = require('web.CalendarView');
|
||||||
|
var testUtils = require('web.test_utils');
|
||||||
|
|
||||||
|
var createView = testUtils.createView;
|
||||||
|
|
||||||
|
var initialDate = new Date(2016, 11, 12, 8, 0, 0);
|
||||||
|
initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000);
|
||||||
|
|
||||||
|
|
||||||
|
QUnit.module('Views', {
|
||||||
|
beforeEach: function () {
|
||||||
|
this.data = {
|
||||||
|
event: {
|
||||||
|
fields: {
|
||||||
|
id: {string: "ID", type: "integer"},
|
||||||
|
name: {string: "name", type: "char"},
|
||||||
|
start: {string: "start datetime", type: "datetime"},
|
||||||
|
stop: {string: "stop datetime", type: "datetime"},
|
||||||
|
},
|
||||||
|
records: [
|
||||||
|
{id: 1, name: "event 1", start: "2016-12-11 00:00:00", stop: "2016-12-11 00:00:00"},
|
||||||
|
],
|
||||||
|
check_access_rights: function () {
|
||||||
|
return $.when(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, function () {
|
||||||
|
|
||||||
|
QUnit.module('CalendarView Mobile');
|
||||||
|
|
||||||
|
QUnit.test('simple calendar rendering in mobile', function (assert) {
|
||||||
|
assert.expect(3);
|
||||||
|
|
||||||
|
var calendar = createView({
|
||||||
|
View: CalendarView,
|
||||||
|
model: 'event',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<calendar date_start="start" date_stop="stop">' +
|
||||||
|
'<field name="name"/>' +
|
||||||
|
'</calendar>',
|
||||||
|
viewOptions: {
|
||||||
|
initialDate: initialDate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.notOk(calendar.$buttons.find('.o_calendar_button_prev').is(':visible'),
|
||||||
|
"prev button should be hidden");
|
||||||
|
assert.notOk(calendar.$buttons.find('.o_calendar_button_next').is(':visible'),
|
||||||
|
"next button should be hidden");
|
||||||
|
assert.ok(calendar.$buttons.find('.o_calendar_button_today').is(':visible'),
|
||||||
|
"today button should be visible");
|
||||||
|
|
||||||
|
calendar.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -234,7 +234,7 @@ QUnit.module('Views', {
|
|||||||
|
|
||||||
// displays month mode by default
|
// displays month mode by default
|
||||||
assert.strictEqual(actionManager.controlPanel.$('.breadcrumb li').text(),
|
assert.strictEqual(actionManager.controlPanel.$('.breadcrumb li').text(),
|
||||||
'Meetings Test (Week 51)', "should display the current week");
|
'Meetings Test (Dec 11 – 17, 2016)', "should display the current week");
|
||||||
|
|
||||||
// switch to day mode
|
// switch to day mode
|
||||||
actionManager.controlPanel.$('.o_calendar_button_day').click();
|
actionManager.controlPanel.$('.o_calendar_button_day').click();
|
||||||
|
|||||||
@@ -543,6 +543,7 @@
|
|||||||
<script type="text/javascript" src="/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js"></script>
|
<script type="text/javascript" src="/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="/web/static/tests/chrome/action_manager_mobile_tests.js"></script>
|
<script type="text/javascript" src="/web/static/tests/chrome/action_manager_mobile_tests.js"></script>
|
||||||
|
<script type="text/javascript" src="/web/static/tests/views/calendar_mobile_tests.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/tests/views/kanban_mobile_tests.js"></script>
|
<script type="text/javascript" src="/web/static/tests/views/kanban_mobile_tests.js"></script>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user