[REF] web, *: remove tempusdominus date(time) picker lib

As tempusdominus relies on jQuery, we have written our own date(time)
picker in owl. This commit converts the remaining uses of the
tempusdominus date(time) picker and replaces them by our own.

closes odoo/odoo#133349

Signed-off-by: Julien Mougenot (jum) <jum@odoo.com>
This commit is contained in:
Samuel Degueldre
2023-09-04 09:19:08 +00:00
parent 6580a435a9
commit 910897fc97
33 changed files with 729 additions and 3986 deletions
+1 -1
View File
@@ -167,7 +167,7 @@ class CustomerPortal(portal.CustomerPortal):
return request.render("purchase.portal_my_purchase_order_update_date", values)
return request.render("purchase.portal_my_purchase_order", values)
@http.route(['/my/purchase/<int:order_id>/update'], type='http', methods=['POST'], auth="public", website=True)
@http.route(['/my/purchase/<int:order_id>/update'], type='json', auth="public", website=True)
def portal_my_purchase_order_update_dates(self, order_id=None, access_token=None, **kw):
"""User update scheduled date on purchase order line.
"""
@@ -1,6 +1,26 @@
$(function () {
$('input.o-purchase-datetimepicker').datetimepicker();
$('input.o-purchase-datetimepicker').on("hide.datetimepicker", function () {
$(this).parents('form').submit();
});
})
/** @odoo-module */
import PublicWidget from "@web/legacy/js/public/public_widget";
export const PurchaseDatePicker = PublicWidget.Widget.extend({
selector: ".o-purchase-datetimepicker",
start() {
this.call("datetime_picker", "create", {
target: this.el,
onChange: (newDate) => {
const { accessToken, orderId, lineId } = this.el.dataset;
this._rpc({
route: `/my/purchase/${orderId}/update?access_token=${accessToken}`,
params: {
[lineId]: newDate.toISODate(),
},
});
},
pickerProps: {
type: "date",
value: luxon.DateTime.fromISO(this.el.dataset.value),
},
}).enable();
},
});
PublicWidget.registry.PurchaseDatePicker = PurchaseDatePicker;
+7 -7
View File
@@ -262,7 +262,6 @@
<th class="text-start">Products</th>
<th class="text-end">Quantity</th>
<th t-if="update_dates" class="text-end">Scheduled Date</th>
<th t-if="update_dates" class="text-end"><strong>Update Here</strong></th>
<th t-if="not update_dates and order.state in ['purchase', 'done']" t-attf-class="text-end {{ 'd-none d-sm-table-cell' if report_type == 'html' else '' }}">Unit Price</th>
<th t-if="not update_dates and order.state in ['purchase', 'done']" t-attf-class="text-end {{ 'd-none d-md-table-cell' if report_type == 'html' else '' }}">
<span>Taxes</span>
@@ -296,19 +295,20 @@
<span t-field="line.product_uom" groups="uom.group_uom"/>
</div>
</td>
<td t-if="update_dates" class="text-end"><span t-esc="line.date_planned.date()"/></td>
<td t-if="update_dates" class="text-end">
<form t-attf-action="/my/purchase/#{order.id}/update?access_token=#{order.access_token}" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="container">
<div class="mb-3">
<div class="input-group date">
<input type="text" class="form-control datetimepicker-input o-purchase-datetimepicker" t-attf-id="datetimepicker_#{line.id}" t-att-name="line.id"
data-toggle="datetimepicker" data-date-format="YYYY-MM-DD" t-attf-data-target="#datetimepicker_#{line.id}"/>
<input type="text"
class="form-control datetimepicker-input o-purchase-datetimepicker text-end"
t-att-data-access-token="order.access_token"
t-att-data-order-id="order.id"
t-att-data-line-id="line.id"
t-att-data-value="line.date_planned.isoformat()"
/>
</div>
</div>
</div>
</form>
</td>
<td t-if="not update_dates and order.state in ['purchase', 'done']" t-attf-class="text-end {{ 'd-none d-sm-table-cell' if report_type == 'html' else '' }}">
<div
+31 -106
View File
@@ -1,9 +1,6 @@
/** @odoo-module **/
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import publicWidget from "@web/legacy/js/public/public_widget";
import { formatDateTime, parseDate, parseDateTime} from "@web/legacy/js/core/dates";
import time from "@web/legacy/js/core/time";
import { _t } from "@web/core/l10n/translation";
import dom from "@web/legacy/js/core/dom";
import {getCookie, setCookie, deleteCookie} from "@web/legacy/js/core/cookie_utils";
@@ -11,9 +8,17 @@ import { utils as uiUtils } from "@web/core/ui/ui_service";
import SurveyPreloadImageMixin from "@survey/js/survey_preload_image_mixin";
import { SurveyImageZoomer } from "@survey/js/survey_image_zoomer";
import {
deserializeDate,
deserializeDateTime,
parseDateTime,
parseDate,
serializeDateTime,
serializeDate,
} from "@web/core/l10n/dates";
const { DateTime } = luxon;
var isMac = navigator.platform.toUpperCase().includes('MAC');
const { DateTime, Settings } = luxon;
publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloadImageMixin, {
selector: '.o_survey_form',
@@ -56,9 +61,6 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
self._initTimer();
self._initBreadcrumb();
}
self.$('div.o_survey_form_date').each(function () {
self._initDateTimePicker($(this));
});
self._initChoiceItems();
self._initTextArea();
self._focusOnFirstInput();
@@ -472,9 +474,8 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
options.isFinish = true;
}
this.$('div.o_survey_form_date').each(function () {
self._initDateTimePicker($(this));
});
// Start datetime pickers
self.trigger_up("widgets_start_request");
if (this.options.isStartScreen || (options && options.initTimer)) {
this._initTimer();
this.options.isStartScreen = false;
@@ -602,16 +603,20 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
if (questionRequired && !data[questionId]) {
errors[questionId] = constrErrorMsg;
} else if (data[questionId]) {
var datetimepickerFormat = $input.data('questionType') === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat();
var momentDate = moment($input.val(), datetimepickerFormat);
if (!momentDate.isValid()) {
const [parse, deserialize] =
$input.data("questionType") === "date"
? [parseDate, deserializeDate]
: [parseDateTime, deserializeDateTime];
const date = parse($input.val());
if (!date || !date.isValid) {
errors[questionId] = validationDateMsg;
} else {
var $dateDiv = $questionWrapper.find('.o_survey_form_date');
var maxDate = $dateDiv.data('maxdate');
var minDate = $dateDiv.data('mindate');
if ((maxDate && momentDate.isAfter(moment(maxDate)))
|| (minDate && momentDate.isBefore(moment(minDate)))) {
const maxDate = deserialize($input.data('max-date'));
const minDate = deserialize($input.data('min-date'));
if (
(maxDate.isValid && date > maxDate) ||
(minDate.isValid && date < minDate)
) {
errors[questionId] = validationErrorMsg;
}
}
@@ -691,11 +696,15 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
params[this.name] = this.value;
break;
case 'date':
params = self._prepareSubmitDates(params, this.name, this.value, false);
break;
case 'datetime':
params = self._prepareSubmitDates(params, this.name, this.value, true);
case 'datetime':{
const [parse, serialize] =
$(this).data("questionType") === "date"
? [parseDate, serializeDate]
: [parseDateTime, serializeDateTime];
const date = parse(this.value);
params[this.name] = date ? serialize(date) : "";
break;
}
case 'simple_choice_radio':
case 'multiple_choice':
params = self._prepareSubmitChoices(params, $(this), $(this).data('name'));
@@ -706,19 +715,6 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
}
});
},
/**
* Prepare date answer before submitting form.
* Convert date value from client current timezone to UTC Date to correspond to the server format.
* return params = { 'dateQuestionId' : '2019-05-23', 'datetimeQuestionId' : '2019-05-23 14:05:12' }
*/
_prepareSubmitDates: function (params, questionId, value, isDateTime) {
var momentDate = isDateTime ? parseDateTime(value, null, {timezone: true}) : parseDate(value);
var formattedDate = momentDate ? momentDate.toJSON() : '';
params[questionId] = formattedDate;
return params;
},
/**
* Prepare choice answer before submitting form.
* If the answer is not the 'comment selection' (=Other), calls the _prepareSubmitAnswer method to add the answer to the params
@@ -919,77 +915,6 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
}
},
/**
* Initialize datetimepicker in correct format and with constraints
*/
_initDateTimePicker: function ($dateGroup) {
var disabledDates = [];
var questionType = $dateGroup.find('input').data('questionType');
var minDateData = $dateGroup.data('mindate');
var maxDateData = $dateGroup.data('maxdate');
var datetimepickerFormat = questionType === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat();
var minDate = minDateData
? this._formatDateTime(minDateData, datetimepickerFormat)
: moment({ y: 1000 });
var maxDate = maxDateData
? this._formatDateTime(maxDateData, datetimepickerFormat)
: moment().add(200, "y");
if (questionType === 'date') {
// Include min and max date in selectable values
maxDate = moment(maxDate).add(1, "d");
minDate = moment(minDate).subtract(1, "d");
disabledDates = [minDate, maxDate];
}
$dateGroup.datetimepicker({
format: datetimepickerFormat,
minDate: minDate,
maxDate: maxDate,
disabledDates: disabledDates,
useCurrent: false,
viewDate: moment(new Date()).hours(minDate.hours()).minutes(minDate.minutes()).seconds(minDate.seconds()).milliseconds(minDate.milliseconds()),
calendarWeeks: true,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
next: 'oi oi-chevron-right',
previous: 'oi oi-chevron-left',
up: 'oi oi-chevron-up',
down: 'oi oi-chevron-down',
},
locale: Settings.defaultLocale,
allowInputToggle: true,
});
$dateGroup.on('error.datetimepicker', (err) => {
if (err.date) {
if (err.date < minDate) {
this.call("dialog", "add", AlertDialog, {
body:
_t("The date you selected is lower than the minimum date: ") +
minDate.format(datetimepickerFormat),
});
}
if (err.date > maxDate) {
this.call("dialog", "add", AlertDialog, {
body:
_t("The date you selected is greater than the maximum date: ") +
maxDate.format(datetimepickerFormat),
});
}
}
return false;
});
},
_formatDateTime: function (datetimeValue, format) {
return moment(formatDateTime(moment(datetimeValue), null, {timezone: true}), format);
},
_initResultWidget: function () {
var $result = this.$('.o_survey_result');
if ($result.length) {
@@ -114,12 +114,6 @@ _::-webkit-full-page-media, _:future, :root .o_survey_wrap {
}
}
.o_survey_form_date [data-toggle="datetimepicker"] {
right: 0;
bottom: 5px;
top: auto;
}
.o_survey_choice_btn {
transition: background-color 0.3s ease;
flex: 1 0 300px;
+12 -12
View File
@@ -371,28 +371,28 @@
</template>
<template id="question_date" name="Question: date box">
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_date"
t-att-data-maxdate="question.validation_max_date">
<div class="input-group o_survey_form_date">
<input type="text" class="form-control datetimepicker-input o_survey_question_date bg-transparent text-dark rounded-0 p-0"
t-attf-data-target="#datetimepicker_#{question.id}"
t-att-name="question.id" t-att-placeholder="question.question_placeholder"
t-att-value="format_date(answer_lines[0].value_date) if answer_lines else None"
t-att-data-question-type="question.question_type"/>
<div t-if="not survey_form_readonly" class="position-absolute input-group-text text-primary border-0 bg-transparent p-0" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker"><i class="fa fa-calendar"></i></div>
t-att-data-question-type="question.question_type"
data-widget="datetime-picker" data-widget-type="date"
t-att-data-min-date="question.validation_min_date"
t-att-data-max-date="question.validation_max_date"/>
<div t-if="not survey_form_readonly" class="position-absolute input-group-text text-primary border-0 bg-transparent p-0 end-0 pe-none"><i class="fa fa-calendar"></i></div>
</div>
</template>
<template id="question_datetime" name="Question: datetime box">
<div class="input-group o_survey_form_date" t-attf-id="datetimepicker_#{question.id}" data-target-input="nearest"
t-att-data-mindate="question.validation_min_datetime"
t-att-data-maxdate="question.validation_max_datetime">
<div class="input-group o_survey_form_date">
<input type="text" class="form-control datetimepicker-input o_survey_question_datetime bg-transparent text-dark rounded-0 p-0"
t-attf-data-target="#datetimepicker_#{question.id}"
t-att-name="question.id" t-att-placeholder="question.question_placeholder"
t-att-value="format_datetime(answer_lines[0].value_datetime) if answer_lines else None"
t-att-data-question-type="question.question_type"/>
<div t-if="not survey_form_readonly" class="position-absolute input-group-text text-primary border-0 bg-transparent p-0" t-attf-data-target="#datetimepicker_#{question.id}" data-toggle="datetimepicker"><i class="fa fa-calendar"></i></div>
t-att-data-question-type="question.question_type"
data-widget="datetime-picker" data-widget-type="datetime"
t-att-data-min-date="question.validation_min_datetime"
t-att-data-max-date="question.validation_max_datetime"/>
<div t-if="not survey_form_readonly" class="position-absolute input-group-text text-primary border-0 bg-transparent p-0 end-0 pe-none"><i class="fa fa-calendar"></i></div>
</div>
</template>
+1 -4
View File
@@ -182,8 +182,6 @@ This module provides the core of the Odoo Web Client.
('include', 'web._assets_bootstrap_frontend'),
'web/static/src/legacy/scss/tempusdominus_overridden.scss',
'web/static/lib/tempusdominus/tempusdominus.scss',
'web/static/lib/jquery.ui/jquery-ui.css',
'web/static/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/odoo_ui_icons/*',
@@ -234,7 +232,6 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/bootstrap/js/dist/scrollspy.js',
'web/static/lib/bootstrap/js/dist/tab.js',
'web/static/lib/bootstrap/js/dist/toast.js',
'web/static/lib/tempusdominus/tempusdominus.js',
'web/static/lib/select2/select2.js',
'web/static/lib/clipboard/clipboard.js',
'web/static/lib/jSignature/jSignatureCustom.js',
@@ -268,6 +265,7 @@ This module provides the core of the Odoo Web Client.
('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'),
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
'web/static/src/public/error_notifications.js',
'web/static/src/public/datetime_picker_widget.js',
'web/static/src/libs/pdfjs.js',
'web/static/src/legacy/utils.js',
@@ -512,7 +510,6 @@ This module provides the core of the Odoo Web Client.
('remove', 'web/static/tests/webclient/**/helpers.js'),
# Legacy
'web/static/lib/tempusdominus/tempusdominus.js',
'web/static/tests/legacy/**/*.js',
('remove', 'web/static/tests/legacy/helpers/**/*.js'),
('remove', 'web/static/tests/legacy/legacy_setup.js'),
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Tempus Dominus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because it is too large Load Diff
@@ -1,342 +0,0 @@
$bs-datetimepicker-timepicker-font-size: var(--bs-datetimepicker-timepicker-font-size, 1.2em) !default;
$bs-datetimepicker-active-bg: var(--bs-datetimepicker-active-bg, #{$blue}) !default;
$bs-datetimepicker-active-color: var(--bs-datetimepicker-active-color, #{$white}) !default;
$bs-datetimepicker-border-radius: var(--bs-datetimepicker-border-radius, #{$border-radius}) !default;
$bs-datetimepicker-btn-hover-bg: var(--bs-datetimepicker-btn-hover-bg, #{$gray-200}) !default;
$bs-datetimepicker-disabled-color: var(--bs-datetimepicker-disabled-color, #{$gray-600}) !default;
$bs-datetimepicker-alternate-color: var(--bs-datetimepicker-alternate-color, #{$gray-600}) !default;
$bs-datetimepicker-secondary-border-color: var(--bs-datetimepicker-secondary-border-color, #ccc) !default;
$bs-datetimepicker-secondary-border-color-rgba: var(--bs-datetimepicker-secondary-border-color-rgba, rgba(0, 0, 0, 0.2)) !default;
$bs-datetimepicker-primary-border-color: var(--bs-datetimepicker-primary-border-color, white) !default;
$bs-datetimepicker-text-shadow: var(--bs-datetimepicker-text-shadow, 0 -1px 0 rgba(0, 0, 0, 0.25)) !default;
.bootstrap-datetimepicker-widget {
list-style: none;
&.dropdown-menu {
display: block;
margin: 2px 0;
padding: 4px;
width: 14rem;
&.timepicker-sbs {
@media (min-width: map-get($grid-breakpoints, 'sm')) {
width: 38em;
}
@media (min-width: map-get($grid-breakpoints, 'md')) {
width: 38em;
}
@media (min-width: map-get($grid-breakpoints, 'lg')) {
width: 38em;
}
}
&:before, &:after {
content: '';
display: inline-block;
position: absolute;
}
&.bottom {
&:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid $bs-datetimepicker-secondary-border-color;
border-bottom-color: $bs-datetimepicker-secondary-border-color-rgba;
top: -7px;
left: 7px;
}
&:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid $bs-datetimepicker-primary-border-color;
top: -6px;
left: 8px;
}
}
&.top {
&:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 7px solid $bs-datetimepicker-secondary-border-color;
border-top-color: $bs-datetimepicker-secondary-border-color-rgba;
bottom: -7px;
left: 6px;
}
&:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid $bs-datetimepicker-primary-border-color;
bottom: -6px;
left: 7px;
}
}
&.float-right {
&:before {
left: auto;
right: 6px;
}
&:after {
left: auto;
right: 7px;
}
}
&.wider {
width: 16rem;
}
}
.list-unstyled {
margin: 0;
}
a[data-action] {
padding: 6px 0;
}
a[data-action]:active {
box-shadow: none;
}
.timepicker-hour, .timepicker-minute, .timepicker-second {
width: 54px;
font-weight: bold;
font-size: $bs-datetimepicker-timepicker-font-size;
margin: 0;
}
button[data-action] {
padding: 6px;
}
.btn[data-action="incrementHours"]::after {
@extend .sr-only;
content: "Increment Hours";
}
.btn[data-action="incrementMinutes"]::after {
@extend .sr-only;
content: "Increment Minutes";
}
.btn[data-action="decrementHours"]::after {
@extend .sr-only;
content: "Decrement Hours";
}
.btn[data-action="decrementMinutes"]::after {
@extend .sr-only;
content: "Decrement Minutes";
}
.btn[data-action="showHours"]::after {
@extend .sr-only;
content: "Show Hours";
}
.btn[data-action="showMinutes"]::after {
@extend .sr-only;
content: "Show Minutes";
}
.btn[data-action="togglePeriod"]::after {
@extend .sr-only;
content: "Toggle AM/PM";
}
.btn[data-action="clear"]::after {
@extend .sr-only;
content: "Clear the picker";
}
.btn[data-action="today"]::after {
@extend .sr-only;
content: "Set the date to today";
}
.picker-switch {
text-align: center;
&::after {
@extend .sr-only;
content: "Toggle Date and Time Screens";
}
td {
padding: 0;
margin: 0;
height: auto;
width: auto;
line-height: inherit;
span {
line-height: 2.5;
height: 2.5em;
width: 100%;
}
}
}
table {
width: 100%;
margin: 0;
& td,
& th {
text-align: center;
border-radius: $bs-datetimepicker-border-radius;
}
& th {
height: 20px;
line-height: 20px;
width: 20px;
&.picker-switch {
width: 145px;
}
&.disabled,
&.disabled:hover {
background: none;
color: $bs-datetimepicker-disabled-color;
cursor: not-allowed;
}
&.prev::after {
@extend .sr-only;
content: "Previous Month";
}
&.next::after {
@extend .sr-only;
content: "Next Month";
}
}
& thead tr:first-child th {
cursor: pointer;
&:hover {
background: $bs-datetimepicker-btn-hover-bg;
}
}
& td {
height: 54px;
line-height: 54px;
width: 54px;
&.cw {
font-size: .8em;
height: 20px;
line-height: 20px;
color: $bs-datetimepicker-alternate-color;
}
&.day {
height: 20px;
line-height: 20px;
width: 20px;
}
&.day:hover,
&.hour:hover,
&.minute:hover,
&.second:hover {
background: $bs-datetimepicker-btn-hover-bg;
cursor: pointer;
}
&.old,
&.new {
color: $bs-datetimepicker-alternate-color;
}
&.today {
position: relative;
&:before {
content: '';
display: inline-block;
border: solid transparent;
border-width: 0 0 7px 7px;
border-bottom-color: $bs-datetimepicker-active-bg;
border-top-color: $bs-datetimepicker-secondary-border-color-rgba;
position: absolute;
bottom: 4px;
right: 4px;
}
}
&.active,
&.active:hover {
background-color: $bs-datetimepicker-active-bg;
color: $bs-datetimepicker-active-color;
text-shadow: $bs-datetimepicker-text-shadow;
}
&.active.today:before {
border-bottom-color: #fff;
}
&.disabled,
&.disabled:hover {
background: none;
color: $bs-datetimepicker-disabled-color;
cursor: not-allowed;
}
span {
display: inline-block;
width: 54px;
height: 54px;
line-height: 54px;
margin: 2px 1.5px;
cursor: pointer;
border-radius: $bs-datetimepicker-border-radius;
&:hover {
background: $bs-datetimepicker-btn-hover-bg;
}
&.active {
background-color: $bs-datetimepicker-active-bg;
color: $bs-datetimepicker-active-color;
text-shadow: $bs-datetimepicker-text-shadow;
}
&.old {
color: $bs-datetimepicker-alternate-color;
}
&.disabled,
&.disabled:hover {
background: none;
color: $bs-datetimepicker-disabled-color;
cursor: not-allowed;
}
}
}
}
&.usetwentyfour {
td.hour {
height: 27px;
line-height: 27px;
}
}
}
.input-group [data-toggle="datetimepicker"] {
cursor: pointer;
}
@@ -1,440 +1,30 @@
/** @odoo-module **/
/** @odoo-module */
import { markRaw, onPatched, onWillRender, reactive, useEffect, useEnv, useRef } from "@odoo/owl";
import { areDatesEqual } from "../l10n/dates";
import { usePopover } from "../popover/popover_hook";
import { registry } from "../registry";
import { ensureArray, zip, zipWith } from "../utils/arrays";
import { deepCopy, shallowEqual } from "../utils/objects";
import { DateTimePicker } from "./datetime_picker";
import { DateTimePickerPopover } from "./datetime_picker_popover";
import { onPatched, onWillRender, useEffect, useRef } from "@odoo/owl";
import { usePopover } from "@web/core/popover/popover_hook";
import { useService } from "@web/core/utils/hooks";
/**
* @typedef {luxon.DateTime} DateTime
*
* @typedef DateTimePickerHookParams
* @property {string} [format]
* @property {(value: DateTimePickerProps["value"]) => any} [onChange] callback
* invoked every time the hook updates the reactive value, either through the inputs
* or the picker.
* @property {(value: DateTimePickerProps["value"]) => any} [onApply] callback
* invoked once the value is committed: this is either when all inputs received
* a "change" event or when the datetime picker popover has been closed.
* @property {DateTimePickerProps} [pickerProps]
* @property {string | ReturnType<typeof useRef>} [target]
*
* @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps
*/
/**
* @template {HTMLElement} T
* @typedef {{ el: T | null }} OwlRef
*/
/** @type {typeof shallowEqual} */
const arePropsEqual = (obj1, obj2) =>
shallowEqual(obj1, obj2, (a, b) => areDatesEqual(a, b) || shallowEqual(a, b));
const FOCUS_CLASSNAME = "text-primary";
/**
* @param {DateTimePickerHookParams} hookParams
*/
export const useDateTimePicker = (hookParams) => {
// Hook methods
/**
* @param {HTMLElement} el
* @param {string} type
* @param {(ev: Event) => any} listener
*/
const addListener = (el, type, listener) => {
el.addEventListener(type, listener);
cleanups.push(() => el.removeEventListener(type, listener));
};
/**
* Wrapper method on the "onApply" callback to only call it when the
* value has changed, and set other internal variables accordingly.
*/
const apply = () => {
const serializedValue = JSON.stringify(pickerProps.value);
if (areDatesEqual(lastInitialProps?.value, serializedValue)) {
return;
}
lastInitialProps = null; // Next pickerProps are considered final
inputsChanged = ensureArray(pickerProps.value).map(() => false);
onApply?.(pickerProps.value);
};
const computeBasePickerProps = () => {
const nextInitialProps = markValuesRaw(hookParams.pickerProps);
const serializedProps = deepCopy(nextInitialProps);
if (lastInitialProps && arePropsEqual(lastInitialProps, serializedProps)) {
return;
}
lastInitialProps = serializedProps;
inputsChanged = ensureArray(lastInitialProps.value).map(() => false);
for (const [key, value] of Object.entries(nextInitialProps)) {
if (pickerProps[key] !== value && !areDatesEqual(pickerProps[key], value)) {
pickerProps[key] = value;
}
}
};
/**
* Ensures the current focused input (indicated by `pickerProps.focusedDateIndex`)
* is actually focused.
*/
const focusActiveInput = () => {
const inputEl = getInput(pickerProps.focusedDateIndex);
if (!inputEl) {
shouldFocusOnPatched = true;
return;
}
const { activeElement } = inputEl.ownerDocument;
if (activeElement !== inputEl) {
inputEl.focus();
}
setInputFocus(inputEl);
};
/**
* @param {number} valueIndex
* @returns {HTMLInputElement | null}
*/
const getInput = (valueIndex) => {
const el = inputRefs[valueIndex]?.el;
if (el && document.body.contains(el)) {
return el;
}
return null;
};
/**
* Returns the appropriate root element to attach the popover:
* - if the value is a range: the closest common parent of the two inputs
* - if not: the first input
*/
const getPopoverTarget = () => {
if (targetRef?.el) {
return targetRef.el;
}
if (pickerProps.range) {
let parentElement = getInput(0).parentElement;
const inputEls = inputRefs.map((ref) => ref.el);
while (parentElement && !inputEls.every((inputEl) => parentElement.contains(inputEl))) {
parentElement = parentElement.parentElement;
}
return parentElement || getInput(0);
} else {
return getInput(0);
}
};
/**
* @template {object} T
* @param {T} obj
*/
const markValuesRaw = (obj) => {
/** @type {T} */
const copy = {};
for (const [key, value] of Object.entries(obj)) {
if (value && typeof value === "object") {
copy[key] = markRaw(value);
} else {
copy[key] = value;
}
}
return copy;
};
/**
* Inputs "change" event handler. This will trigger an "onApply" callback if
* one of the following is true:
* - there is only one input;
* - the popover is closed;
* - the other input has also changed.
*
* @param {Event} ev
*/
const onInputChange = (ev) => {
updateValueFromInputs();
inputsChanged[ev.target === getInput(1) ? 1 : 0] = true;
if (!popover.isOpen || inputsChanged.every(Boolean)) {
saveAndClose();
}
};
/**
* @param {PointerEvent} ev
*/
const onInputClick = ({ target }) => {
openPicker(target === getInput(1) ? 1 : 0);
};
/**
* @param {FocusEvent} ev
*/
const onInputFocus = ({ target }) => {
pickerProps.focusedDateIndex = target === getInput(1) ? 1 : 0;
setInputFocus(target);
};
/**
* @param {KeyboardEvent} ev
*/
const onInputKeydown = (ev) => {
switch (ev.key) {
case "Enter":
case "Escape": {
return saveAndClose();
}
case "Tab": {
if (!getInput(0) || !getInput(1) || ev.target !== getInput(ev.shiftKey ? 1 : 0)) {
return saveAndClose();
}
}
}
};
/**
* @param {number} inputIndex Input from which to open the picker
*/
const openPicker = (inputIndex) => {
pickerProps.focusedDateIndex = inputIndex;
if (!popover.isOpen) {
const popoverTarget = getPopoverTarget();
if (env.isSmall) {
popoverTarget.scrollIntoView(true);
}
popover.open(popoverTarget, { pickerProps });
}
focusActiveInput();
};
/**
* @template {"format" | "parse"} T
* @param {T} operation
* @param {T extends "format" ? DateTime : string} value
* @returns {[T extends "format" ? string : DateTime, null] | [null, Error]}
*/
const safeConvert = (operation, value) => {
const { type } = pickerProps;
const convertFn = registry
.category(operation === "format" ? "formatters" : "parsers")
.get(type);
try {
return [convertFn(value, { format }), null];
} catch (error) {
if (error?.name === "ConversionError") {
return [null, error];
} else {
throw error;
}
}
};
/**
* Wrapper method to ensure the "onApply" callback is called, either:
* - by closing the popover (if any);
* - or by directly calling "apply", without updating the values.
*/
const saveAndClose = () => {
if (popover.isOpen) {
// apply will be done in the "onClose" callback
popover.close();
} else {
apply();
}
};
/**
* Updates class names on given inputs according to the currently selected input.
*
* @param {HTMLInputElement | null} input
*/
const setFocusClass = (input) => {
for (const { el } of inputRefs) {
if (el) {
el.classList.toggle(FOCUS_CLASSNAME, popover.isOpen && el === input);
}
}
};
/**
* Applies class names to all inputs according to whether they are focused or not.
*
* @param {HTMLInputElement} inputEl
*/
const setInputFocus = (inputEl) => {
inputEl.selectionStart = 0;
inputEl.selectionEnd = inputEl.value.length;
setFocusClass(inputEl);
shouldFocusOnPatched = false;
};
/**
* Synchronizes the given input with the given value.
*
* @param {HTMLInputElement} el
* @param {DateTime} value
*/
const updateInput = (el, value) => {
const [formattedValue] = safeConvert("format", value);
el.value = formattedValue || "";
};
/**
* @param {DateTimePickerProps["value"]} value
*/
const updateValue = (value) => {
const previousValue = pickerProps.value;
pickerProps.value = value;
if (areDatesEqual(previousValue, pickerProps.value)) {
return;
}
if (pickerProps.range) {
// When in range: compare each individual value
const [prevStart, prevEnd] = ensureArray(previousValue);
const [nextStart, nextEnd] = ensureArray(pickerProps.value);
if (
(pickerProps.focusedDateIndex === 0 && areDatesEqual(prevEnd, nextEnd)) ||
(pickerProps.focusedDateIndex === 1 && areDatesEqual(prevStart, nextStart))
) {
pickerProps.focusedDateIndex = pickerProps.focusedDateIndex === 1 ? 0 : 1;
}
}
onChange?.(pickerProps.value);
};
const updateValueFromInputs = () => {
const values = zipWith(
inputRefs,
ensureArray(pickerProps.value),
({ el }, currentValue) => {
if (!el) {
return currentValue;
}
const [parsedValue, error] = safeConvert("parse", el.value);
if (error) {
updateInput(el, currentValue);
return currentValue;
} else {
return parsedValue;
}
}
);
updateValue(values.length === 2 ? values : values[0]);
};
// Hook variables
const { format, onApply, onChange, target } = hookParams;
const targetRef = typeof target === "string" ? useRef(target) : target;
/** @type {ReturnType<typeof useRef<HTMLInputElement>>[]} */
export function useDateTimePicker(hookParams) {
const datetimePicker = useService("datetime_picker");
if (typeof hookParams.target === "string") {
const target = useRef(hookParams.target);
Object.defineProperty(hookParams, "target", {
get() {
return target.el;
},
});
}
const inputRefs = [useRef("start-date"), useRef("end-date")];
const env = useEnv();
const popover = usePopover(DateTimePickerPopover, {
onClose: () => {
if (!allowOnClose) {
return;
}
updateValueFromInputs();
apply();
setFocusClass(null);
},
});
/** @type {DateTimePickerProps} */
const rawPickerProps = {
...DateTimePicker.defaultProps,
onSelect: (value) => {
value &&= markRaw(value);
updateValue(value);
if (!pickerProps.range && pickerProps.type === "date") {
saveAndClose();
}
},
...markValuesRaw(hookParams.pickerProps),
};
const pickerProps = reactive(rawPickerProps, () => {
// Resets the popover position when switching from single date to a range
// or vice-versa
const currentIsRange = pickerProps.range;
if (popover.isOpen && lastIsRange !== currentIsRange) {
allowOnClose = false;
popover.open(getPopoverTarget(), { pickerProps });
allowOnClose = true;
}
lastIsRange = currentIsRange;
// Update inputs
for (const [{ el }, value] of zip(inputRefs, ensureArray(pickerProps.value), true)) {
if (el) {
updateInput(el, value);
}
}
shouldFocusOnPatched = true;
});
/** Decides whether the popover 'onClose' callback can be called */
let allowOnClose = true;
/** @type {boolean[]} */
let inputsChanged = [];
/** @type {DateTimePickerProps | null} */
let lastInitialProps = null;
let lastIsRange = pickerProps.range;
let shouldFocusOnPatched = false;
const { computeBasePickerProps, state, open, focusIfNeeded, enable } = datetimePicker.create(
hookParams,
() => inputRefs.map((ref) => ref?.el),
usePopover
);
onWillRender(computeBasePickerProps);
const cleanups = [];
useEffect(() => {
let editableInputs = 0;
for (const [{ el }, value] of zip(inputRefs, ensureArray(pickerProps.value), true)) {
if (el && !el.disabled && !el.readOnly) {
addListener(el, "change", onInputChange);
addListener(el, "click", onInputClick);
addListener(el, "focus", onInputFocus);
addListener(el, "keydown", onInputKeydown);
updateInput(el, value);
editableInputs++;
}
}
if (!editableInputs) {
saveAndClose();
}
return () => {
while (cleanups.length) {
cleanups.pop()();
}
};
});
useEffect(enable);
// Note: this `onPatched` callback must be called after the `useEffect` since
// the effect may change input values that will be selected by the patch callback.
onPatched(() => {
if (popover.isOpen && shouldFocusOnPatched) {
focusActiveInput();
}
});
return { state: pickerProps, open: openPicker };
};
onPatched(focusIfNeeded);
return { state, open };
}
@@ -0,0 +1,470 @@
/** @odoo-module **/
import { markRaw, reactive } from "@odoo/owl";
import { areDatesEqual, formatDate, formatDateTime, parseDate, parseDateTime } from "../l10n/dates";
import { makePopover } from "../popover/popover_hook";
import { registry } from "../registry";
import { ensureArray, zip, zipWith } from "../utils/arrays";
import { deepCopy, shallowEqual } from "../utils/objects";
import { DateTimePicker } from "./datetime_picker";
import { DateTimePickerPopover } from "./datetime_picker_popover";
/**
* @typedef {luxon.DateTime} DateTime
*
* @typedef DateTimePickerHookParams
* @property {string} [format]
* @property {(value: DateTimePickerProps["value"]) => any} [onChange] callback
* invoked every time the hook updates the reactive value, either through the inputs
* or the picker.
* @property {(value: DateTimePickerProps["value"]) => any} [onApply] callback
* invoked once the value is committed: this is either when all inputs received
* a "change" event or when the datetime picker popover has been closed.
* @property {DateTimePickerProps} [pickerProps]
* @property {string | ReturnType<typeof import("@odoo/owl").useRef>} [target]
*
* @typedef {import("./datetime_picker").DateTimePickerProps} DateTimePickerProps
*/
/**
* @template {HTMLElement} T
* @typedef {{ el: T | null }} OwlRef
*/
/** @type {typeof shallowEqual} */
const arePropsEqual = (obj1, obj2) =>
shallowEqual(obj1, obj2, (a, b) => areDatesEqual(a, b) || shallowEqual(a, b));
const FOCUS_CLASSNAME = "text-primary";
const formatters = {
date: formatDate,
datetime: formatDateTime,
};
const parsers = {
date: parseDate,
datetime: parseDateTime,
};
export const datetimePickerService = {
dependencies: ["popover"],
start(env, { popover: popoverService }) {
return {
/**
* @param {DateTimePickerHookParams} hookParams
*/
create: (
hookParams,
getInputs = () => [hookParams.target, null],
createPopover = (...args) => makePopover(popoverService, ...args)
) => {
const popover = createPopover(DateTimePickerPopover, {
onClose: () => {
if (!allowOnClose) {
return;
}
updateValueFromInputs();
apply();
setFocusClass(null);
},
});
// Hook methods
/**
* Wrapper method on the "onApply" callback to only call it when the
* value has changed, and set other internal variables accordingly.
*/
const apply = () => {
const serializedValue = JSON.stringify(pickerProps.value);
if (areDatesEqual(lastInitialProps?.value, serializedValue)) {
return;
}
lastInitialProps = null; // Next pickerProps are considered final
inputsChanged = ensureArray(pickerProps.value).map(() => false);
hookParams.onApply?.(pickerProps.value);
};
const computeBasePickerProps = () => {
const nextInitialProps = markValuesRaw(hookParams.pickerProps);
const serializedProps = deepCopy(nextInitialProps);
if (lastInitialProps && arePropsEqual(lastInitialProps, serializedProps)) {
return;
}
lastInitialProps = serializedProps;
inputsChanged = ensureArray(lastInitialProps.value).map(() => false);
for (const [key, value] of Object.entries(nextInitialProps)) {
if (pickerProps[key] !== value && !areDatesEqual(pickerProps[key], value)) {
pickerProps[key] = value;
}
}
};
/**
* Ensures the current focused input (indicated by `pickerProps.focusedDateIndex`)
* is actually focused.
*/
const focusActiveInput = () => {
const inputEl = getInput(pickerProps.focusedDateIndex);
if (!inputEl) {
shouldFocus = true;
return;
}
const { activeElement } = inputEl.ownerDocument;
if (activeElement !== inputEl) {
inputEl.focus();
}
setInputFocus(inputEl);
};
/**
* @param {number} valueIndex
* @returns {HTMLInputElement | null}
*/
const getInput = (valueIndex) => {
const el = getInputs()[valueIndex];
if (el && document.body.contains(el)) {
return el;
}
return null;
};
/**
* Returns the appropriate root element to attach the popover:
* - if the value is a range: the closest common parent of the two inputs
* - if not: the first input
*/
const getPopoverTarget = () => {
if (hookParams.target) {
return hookParams.target;
}
if (pickerProps.range) {
let parentElement = getInput(0).parentElement;
const inputEls = getInputs();
while (
parentElement &&
!inputEls.every((inputEl) => parentElement.contains(inputEl))
) {
parentElement = parentElement.parentElement;
}
return parentElement || getInput(0);
} else {
return getInput(0);
}
};
/**
* @template {object} T
* @param {T} obj
*/
const markValuesRaw = (obj) => {
/** @type {T} */
const copy = {};
for (const [key, value] of Object.entries(obj)) {
if (value && typeof value === "object") {
copy[key] = markRaw(value);
} else {
copy[key] = value;
}
}
return copy;
};
/**
* Inputs "change" event handler. This will trigger an "onApply" callback if
* one of the following is true:
* - there is only one input;
* - the popover is closed;
* - the other input has also changed.
*
* @param {Event} ev
*/
const onInputChange = (ev) => {
updateValueFromInputs();
inputsChanged[ev.target === getInput(1) ? 1 : 0] = true;
if (!popover.isOpen || inputsChanged.every(Boolean)) {
saveAndClose();
}
};
/**
* @param {PointerEvent} ev
*/
const onInputClick = ({ target }) => {
openPicker(target === getInput(1) ? 1 : 0);
};
/**
* @param {FocusEvent} ev
*/
const onInputFocus = ({ target }) => {
pickerProps.focusedDateIndex = target === getInput(1) ? 1 : 0;
setInputFocus(target);
};
/**
* @param {KeyboardEvent} ev
*/
const onInputKeydown = (ev) => {
switch (ev.key) {
case "Enter":
case "Escape": {
return saveAndClose();
}
case "Tab": {
if (
!getInput(0) ||
!getInput(1) ||
ev.target !== getInput(ev.shiftKey ? 1 : 0)
) {
return saveAndClose();
}
}
}
};
/**
* @param {number} inputIndex Input from which to open the picker
*/
const openPicker = (inputIndex) => {
pickerProps.focusedDateIndex = inputIndex;
if (!popover.isOpen) {
const popoverTarget = getPopoverTarget();
if (env.isSmall) {
popoverTarget.scrollIntoView(true);
}
popover.open(popoverTarget, { pickerProps });
}
focusActiveInput();
};
/**
* @template {"format" | "parse"} T
* @param {T} operation
* @param {T extends "format" ? DateTime : string} value
* @returns {[T extends "format" ? string : DateTime, null] | [null, Error]}
*/
const safeConvert = (operation, value) => {
const { type } = pickerProps;
const convertFn = (operation === "format" ? formatters : parsers)[type];
try {
return [convertFn(value, { format: hookParams.format }), null];
} catch (error) {
if (error?.name === "ConversionError") {
return [null, error];
} else {
throw error;
}
}
};
/**
* Wrapper method to ensure the "onApply" callback is called, either:
* - by closing the popover (if any);
* - or by directly calling "apply", without updating the values.
*/
const saveAndClose = () => {
if (popover.isOpen) {
// apply will be done in the "onClose" callback
popover.close();
} else {
apply();
}
};
/**
* Updates class names on given inputs according to the currently selected input.
*
* @param {HTMLInputElement | null} input
*/
const setFocusClass = (input) => {
for (const el of getInputs()) {
if (el) {
el.classList.toggle(FOCUS_CLASSNAME, popover.isOpen && el === input);
}
}
};
/**
* Applies class names to all inputs according to whether they are focused or not.
*
* @param {HTMLInputElement} inputEl
*/
const setInputFocus = (inputEl) => {
inputEl.selectionStart = 0;
inputEl.selectionEnd = inputEl.value.length;
setFocusClass(inputEl);
shouldFocus = false;
};
/**
* Synchronizes the given input with the given value.
*
* @param {HTMLInputElement} el
* @param {DateTime} value
*/
const updateInput = (el, value) => {
const [formattedValue] = safeConvert("format", value);
el.value = formattedValue || "";
};
/**
* @param {DateTimePickerProps["value"]} value
*/
const updateValue = (value) => {
const previousValue = pickerProps.value;
pickerProps.value = value;
if (areDatesEqual(previousValue, pickerProps.value)) {
return;
}
if (pickerProps.range) {
// When in range: compare each individual value
const [prevStart, prevEnd] = ensureArray(previousValue);
const [nextStart, nextEnd] = ensureArray(pickerProps.value);
if (
(pickerProps.focusedDateIndex === 0 &&
areDatesEqual(prevEnd, nextEnd)) ||
(pickerProps.focusedDateIndex === 1 &&
areDatesEqual(prevStart, nextStart))
) {
pickerProps.focusedDateIndex =
pickerProps.focusedDateIndex === 1 ? 0 : 1;
}
}
hookParams.onChange?.(pickerProps.value);
};
const updateValueFromInputs = () => {
const values = zipWith(
getInputs(),
ensureArray(pickerProps.value),
(el, currentValue) => {
if (!el) {
return currentValue;
}
const [parsedValue, error] = safeConvert("parse", el.value);
if (error) {
updateInput(el, currentValue);
return currentValue;
} else {
return parsedValue;
}
}
);
updateValue(values.length === 2 ? values : values[0]);
};
// Hook variables
/** @type {DateTimePickerProps} */
const rawPickerProps = {
...DateTimePicker.defaultProps,
onSelect: (value) => {
value &&= markRaw(value);
updateValue(value);
if (!pickerProps.range && pickerProps.type === "date") {
saveAndClose();
}
},
...markValuesRaw(hookParams.pickerProps),
};
const pickerProps = reactive(rawPickerProps, () => {
// Resets the popover position when switching from single date to a range
// or vice-versa
const currentIsRange = pickerProps.range;
if (popover.isOpen && lastIsRange !== currentIsRange) {
allowOnClose = false;
popover.open(getPopoverTarget(), { pickerProps });
allowOnClose = true;
}
lastIsRange = currentIsRange;
// Update inputs
for (const [el, value] of zip(
getInputs(),
ensureArray(pickerProps.value),
true
)) {
if (el) {
updateInput(el, value);
}
}
shouldFocus = true;
});
/** Decides whether the popover 'onClose' callback can be called */
let allowOnClose = true;
/** @type {boolean[]} */
let inputsChanged = [];
/** @type {DateTimePickerProps | null} */
let lastInitialProps = null;
let lastIsRange = pickerProps.range;
let shouldFocus = false;
/**
* @param {HTMLElement} el
* @param {string} type
* @param {(ev: Event) => any} listener
*/
const addListener = (el, type, listener) => {
el.addEventListener(type, listener);
return () => el.removeEventListener(type, listener);
};
return {
state: pickerProps,
open: openPicker,
computeBasePickerProps,
focusIfNeeded() {
if (popover.isOpen && shouldFocus) {
focusActiveInput();
}
},
enable() {
let editableInputs = 0;
const cleanups = [];
for (const [el, value] of zip(
getInputs(),
ensureArray(pickerProps.value),
true
)) {
if (el && !el.disabled && !el.readOnly) {
cleanups.push(addListener(el, "change", onInputChange));
cleanups.push(addListener(el, "click", onInputClick));
cleanups.push(addListener(el, "focus", onInputFocus));
cleanups.push(addListener(el, "keydown", onInputKeydown));
updateInput(el, value);
editableInputs++;
}
}
if (!editableInputs) {
saveAndClose();
}
return () => cleanups.forEach((cleanup) => cleanup());
},
get isOpen() {
return popover.isOpen;
},
};
},
};
},
};
registry.category("services").add("datetime_picker", datetimePickerService);
@@ -123,9 +123,9 @@ export const rpcService = {
/**
* @param {string} route
* @param {Object} params
* @param {Object} settings
* @param {Object} [settings]
* @param {boolean} settings.silent
* @param {XmlHttpRequest} settings.xhr
* @param {XMLHttpRequest} settings.xhr
*/
return function rpc(route, params = {}, settings) {
return jsonrpc(env, rpcId++, route, params, settings);
@@ -15,6 +15,28 @@ import { onWillUnmount, status, useComponent } from "@odoo/owl";
* - Whether the popover is currently open.
*/
export function makePopover(popoverService, component, options) {
let removeFn = null;
function close() {
removeFn?.();
}
return {
open(target, props) {
close();
const newOptions = Object.create(options);
newOptions.onClose = () => {
removeFn = null;
options.onClose?.();
};
removeFn = popoverService.add(target, component, props, newOptions);
},
close,
get isOpen() {
return Boolean(removeFn);
},
};
}
/**
* Manages a component to be used as a popover.
*
@@ -23,28 +45,15 @@ import { onWillUnmount, status, useComponent } from "@odoo/owl";
* @returns {PopoverHookReturnType}
*/
export function usePopover(component, options = {}) {
let removeFn = null;
const popover = useService("popover");
const popoverService = useService("popover");
const owner = useComponent();
function close() {
removeFn?.();
}
onWillUnmount(close);
return {
open(target, props) {
close();
const newOptions = Object.create(options);
newOptions.onClose = function () {
removeFn = null;
if (status(owner) !== "destroyed") {
options.onClose?.();
}
};
removeFn = popover.add(target, component, props, newOptions);
},
close,
get isOpen() {
return Boolean(removeFn);
},
const newOptions = Object.create(options);
newOptions.onClose = () => {
if (status(owner) !== "destroyed") {
options.onClose?.();
}
};
const popover = makePopover(popoverService, component, newOptions);
onWillUnmount(popover.close);
return popover;
}
@@ -101,8 +101,6 @@ export const PublicRoot = publicWidget.RootWidget.extend({
this.el.scrollTop = +window.location.hash.match(/scrollTop=([0-9]+)/)[1];
}
this.$el.children().on('error.datetimepicker', this._onDateTimePickerError.bind(this));
return Promise.all(defs);
},
@@ -0,0 +1,36 @@
/** @odoo-module */
import {
deserializeDate,
deserializeDateTime,
parseDate,
parseDateTime,
} from "@web/core/l10n/dates";
import PublicWidget from "@web/legacy/js/public/public_widget";
export const DateTimePickerWidget = PublicWidget.Widget.extend({
selector: "[data-widget='datetime-picker']",
start() {
this._super(...arguments);
const { widgetType, minDate, maxDate } = this.el.dataset;
const type = widgetType || "datetime";
const { value } = this.el;
const [parse, deserialize] =
type === "date" ? [parseDate, deserializeDate] : [parseDateTime, deserializeDateTime];
this.disable = this.call("datetime_picker", "create", {
target: this.el,
pickerProps: {
type,
minDate: minDate && deserialize(minDate),
maxDate: maxDate && deserialize(maxDate),
value: parse(value),
},
}).enable();
},
destroy() {
this.disable();
return this._super(...arguments);
},
});
PublicWidget.registry.DateTimePickerWidget = DateTimePickerWidget;
@@ -4,6 +4,7 @@ import { Component, reactive, useState, xml } from "@odoo/owl";
import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { editInput, getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
import { useDateTimePicker } from "@web/core/datetime/datetime_hook";
import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
@@ -57,7 +58,8 @@ QUnit.module("Components", ({ beforeEach }) => {
})
)
.add("popover", popoverService)
.add("ui", uiService);
.add("ui", uiService)
.add("datetime_picker", datetimePickerService);
fixture = getFixture();
});
@@ -24,6 +24,7 @@ import {
getTimePickers,
zoomOut,
} from "./datetime_test_helpers";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
const { DateTime } = luxon;
@@ -74,7 +75,8 @@ QUnit.module("Components", ({ beforeEach }) => {
})
)
.add("popover", popoverService)
.add("ui", uiService);
.add("ui", uiService)
.add("datetime_picker", datetimePickerService);
fixture = getFixture();
});
@@ -31,6 +31,7 @@ import { openModelFieldSelectorPopover } from "./model_field_selector_tests";
import { nameService } from "@web/core/name_service";
import { dialogService } from "@web/core/dialog/dialog_service";
import { browser } from "@web/core/browser/browser";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
let serverData;
let target;
@@ -159,6 +160,7 @@ QUnit.module("Components", (hooks) => {
registry.category("services").add("field", fieldService);
registry.category("services").add("name", nameService);
registry.category("services").add("dialog", dialogService);
registry.category("services").add("datetime_picker", datetimePickerService);
target = getFixture();
});
@@ -25,6 +25,7 @@ import {
} from "../helpers/utils";
import { makeParent } from "./tooltip/tooltip_service_tests";
import { getPickerCell } from "./datetime/datetime_test_helpers";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
const serviceRegistry = registry.category("services");
@@ -1204,6 +1205,7 @@ QUnit.module("Components", ({ beforeEach }) => {
QUnit.test(
"Dropdown with a date picker inside do not close when a click occurs in date picker",
async (assert) => {
registry.category("services").add("datetime_picker", datetimePickerService);
class MyComponent extends owl.Component {}
MyComponent.template = owl.xml`
<Dropdown>
@@ -26,6 +26,7 @@ import { viewService } from "@web/views/view_service";
import { actionService } from "@web/webclient/actions/action_service";
import { MainComponentsContainer } from "@web/core/main_components_container";
import { nameService } from "@web/core/name_service";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
const serviceRegistry = registry.category("services");
const favoriteMenuRegistry = registry.category("favoriteMenu");
@@ -41,6 +42,7 @@ export function setupControlPanelServiceRegistry() {
serviceRegistry.add("popover", popoverService);
serviceRegistry.add("view", viewService);
serviceRegistry.add("command", commandService);
serviceRegistry.add("datetime_picker", datetimePickerService);
}
export function setupControlPanelFavoriteMenuRegistry() {
@@ -31,6 +31,7 @@ import { overlayService } from "@web/core/overlay/overlay_service";
import { Component, xml } from "@odoo/owl";
import { fieldService } from "@web/core/field_service";
import { nameService } from "@web/core/name_service";
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
const actionRegistry = registry.category("actions");
const serviceRegistry = registry.category("services");
@@ -77,6 +78,7 @@ export function setupWebClientRegistries() {
user: () => makeFakeUserService(),
view: () => viewService,
company: () => fakeCompanyService,
datetime_picker: () => datetimePickerService,
};
for (const serviceName in services) {
if (!serviceRegistry.contains(serviceName)) {
-5
View File
@@ -194,11 +194,6 @@ Odoo Web Editor widget.
('include', 'web_editor.assets_media_dialog'),
# TODO: remove when refactoring 'DatetimePickerUserValueWidget'
'web/static/src/legacy/scss/tempusdominus_overridden.scss',
'web/static/lib/tempusdominus/tempusdominus.scss',
'web/static/lib/tempusdominus/tempusdominus.js',
'web_editor/static/src/scss/web_editor.common.scss',
'web_editor/static/src/scss/web_editor.backend.scss',
@@ -1758,6 +1758,7 @@ var SnippetsMenu = Widget.extend({
'.modal .btn-close',
'.o_we_crop_widget',
'.transfo-container',
'.o_datetime_picker',
].join(', ');
this.loadingTimers = {};
@@ -5,7 +5,6 @@ import { MediaDialogWrapper } from "@web_editor/components/media_dialog/media_di
import Dialog from "@web/legacy/js/core/dialog";
import dom from "@web/legacy/js/core/dom";
import rpc from "@web/legacy/js/core/rpc";
import time from "@web/legacy/js/core/time";
import { throttleForAnimation, debounce } from "@web/core/utils/timing";
import utils from "@web/legacy/js/core/utils";
import Widget from "@web/legacy/js/core/widget";
@@ -34,7 +33,6 @@ import {
} from "@web_editor/js/editor/image_processing";
import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor";
import {SIZES, MEDIAS_BREAKPOINTS} from "@web/core/ui/ui_service";
import { uniqueId } from "@web/core/utils/functions";
import { pick } from "@web/core/utils/objects";
import { _t } from "@web/core/l10n/translation";
import {
@@ -1837,11 +1835,9 @@ const VideopickerUserValueWidget = MediapickerUserValueWidget.extend({
const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
events: { // Explicitely not consider all InputUserValueWidget events
'blur input': '_onInputBlur',
'change.datetimepicker': '_onDateTimePickerChange',
'error.datetimepicker': '_onDateTimePickerError',
'input input': '_onDateInputInput',
},
defaultFormat: time.getLangDatetimeFormat(),
pickerType: 'datetime',
/**
* @override
@@ -1849,7 +1845,6 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
init: function () {
this._super(...arguments);
this._value = DateTime.now().toUnixInteger().toString();
this.__libInput = 0;
},
/**
* @override
@@ -1857,51 +1852,21 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
start: async function () {
await this._super(...arguments);
const datetimePickerId = uniqueId("datetimepicker");
this.el.classList.add('o_we_large');
this.inputEl.classList.add('datetimepicker-input', 'mx-0', 'text-start');
this.inputEl.setAttribute('id', datetimePickerId);
this.inputEl.setAttribute('data-target', '#' + datetimePickerId);
const datepickersOptions = {
minDate: moment({ y: 1000 }),
maxDate: moment().add(200, 'y'),
calendarWeeks: true,
defaultDate: moment().format(),
icons: {
close: 'fa fa-check primary',
this.picker = this.call("datetime_picker", "create", {
target: this.inputEl,
onChange: this._onDateTimePickerChange.bind(this),
pickerProps: {
type: this.pickerType,
minDate: DateTime.fromObject({ year: 1000 }),
maxDate: DateTime.now().plus({ year: 200 }),
value: DateTime.fromSeconds(parseInt(this._value)),
rounding: 0,
},
locale: moment.locale(),
format: this.defaultFormat,
sideBySide: true,
buttons: {
showClear: true,
showClose: true,
showToday: true,
},
widgetParent: 'body',
// Open the datetimepicker on focus not on click. This allows to
// take care of a bug which is due to the wysiwyg editor:
// sometimes, the datetimepicker loses the focus then get it back
// in the same execution flow. This was making the datepicker close
// for no apparent reason. Now, it only closes then reopens directly
// without it be possible to notice.
allowInputToggle: true,
};
this.__libInput++;
const $input = $(this.inputEl);
$input.datetimepicker(datepickersOptions);
this.__libInput--;
// Monkey-patch the library option to add custom classes on the pickers
const libObject = $input.data('datetimepicker');
const oldFunc = libObject._getTemplate;
libObject._getTemplate = function () {
const $template = oldFunc.call(this, ...arguments);
$template.addClass('o_we_no_overlay o_we_datetimepicker');
return $template;
};
});
this.picker.enable();
},
//--------------------------------------------------------------------------
@@ -1920,23 +1885,21 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
* @override
*/
isPreviewed: function () {
return this._super(...arguments) || !!$(this.inputEl).data('datetimepicker').widget;
return this._super(...arguments) || this.picker.isOpen;
},
/**
* @override
*/
async setValue() {
await this._super(...arguments);
let momentObj = null;
let dateTime = "";
if (this._value) {
momentObj = moment.unix(this._value);
if (!momentObj.isValid()) {
momentObj = moment();
dateTime = DateTime.fromSeconds(parseInt(this._value))
if (!dateTime.isValid) {
dateTime = DateTime.now();
}
}
this.__libInput++;
$(this.inputEl).datetimepicker('date', momentObj);
this.__libInput--;
this.picker.state.value = dateTime;
},
//--------------------------------------------------------------------------
@@ -1947,23 +1910,13 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
* @private
* @param {Event} ev
*/
_onDateTimePickerChange: function (ev) {
if (this.__libInput > 0) {
return;
}
if (!ev.date || !ev.date.isValid()) {
_onDateTimePickerChange: function (newDateTime) {
if (!newDateTime || !newDateTime.isValid) {
this._value = '';
} else {
this._value = ev.date.unix().toString();
this._value = newDateTime.toUnixInteger().toString();
}
this._onUserValuePreview(ev);
},
/**
* Prevents crash manager to throw CORS error. Note that library already
* clears the wrong date format.
*/
_onDateTimePickerError: function (ev) {
ev.stopPropagation();
this._onUserValuePreview();
},
/**
* Handles the clear button of the datepicker.
@@ -1980,7 +1933,7 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
});
const DatePickerUserValueWidget = DatetimePickerUserValueWidget.extend({
defaultFormat: time.getLangDateFormat(),
pickerType: 'date',
});
const ListUserValueWidget = UserValueWidget.extend({
@@ -262,7 +262,7 @@ body.editor_enable {
}
}
.s_website_form {
[data-toggle="datetimepicker"], textarea:not(.o_translatable_attribute):not(.o_translatable_text) {
textarea:not(.o_translatable_attribute):not(.o_translatable_text) {
pointer-events: none;
}
}
@@ -1,6 +1,5 @@
/** @odoo-module **/
import time from "@web/legacy/js/core/time";
import {ReCaptcha} from "@google_recaptcha/js/recaptcha";
import { session } from "@web/session";
import ajax from "@web/legacy/js/core/ajax";
@@ -10,8 +9,15 @@
import { debounce } from "@web/core/utils/timing";
import { _t } from "@web/core/l10n/translation";
import { renderToElement } from "@web/core/utils/render";
import { formatDate, formatDateTime } from "@web/core/l10n/dates";
const { DateTime } = luxon;
import {
formatDate,
formatDateTime,
parseDate,
parseDateTime,
serializeDate,
serializeDateTime,
} from "@web/core/l10n/dates";
const { DateTime } = luxon;
publicWidget.registry.EditModeWebsiteForm = publicWidget.Widget.extend({
selector: '.s_website_form form, form.s_website_form', // !compatibility
@@ -97,32 +103,18 @@
this._onFieldInputDebounced = debounce(this._onFieldInput.bind(this), 400);
this.$el.on('input.s_website_form', '.s_website_form_field', this._onFieldInputDebounced);
// Initialize datetimepickers
var datepickers_options = {
minDate: moment({y: 1000}),
maxDate: moment({y: 9999, M: 11, d: 31}),
calendarWeeks: true,
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
next: 'oi oi-chevron-right',
previous: 'oi oi-chevron-left',
up: 'oi oi-chevron-up',
down: 'oi oi-chevron-down',
},
locale: moment.locale(),
format: time.getLangDatetimeFormat(),
extraFormats: ['X'],
};
const $datetimes = this.$el.find('.s_website_form_datetime, .o_website_form_datetime'); // !compatibility
$datetimes.datetimepicker(datepickers_options);
// Adapt options to date-only pickers
datepickers_options.format = time.getLangDateFormat();
const $dates = this.$el.find('.s_website_form_date, .o_website_form_date'); // !compatibility
$dates.datetimepicker(datepickers_options);
this.$allDates = $datetimes.add($dates);
this.$allDates = this.$el.find('.s_website_form_datetime, .o_website_form_datetime, .s_website_form_date, .o_website_form_date');
for (const field of this.$allDates) {
const input = field.querySelector("input");
const defaultValue = input.getAttribute("value");
this.call("datetime_picker", "create", {
target: input,
pickerProps: {
type: field.matches('.s_website_form_date, .o_website_form_date') ? 'date' : 'datetime',
value: defaultValue && DateTime.fromSeconds(parseInt(defaultValue)),
},
}).enable();
}
this.$allDates.addClass('s_website_form_datepicker_initialized');
// Display form values from tag having data-for attribute
@@ -339,20 +331,14 @@
this.$el.find('.s_website_form_field:not(.s_website_form_custom)')
.find('.s_website_form_date, .s_website_form_datetime').each(function () {
const inputEl = this.querySelector('input');
// Datetimepicker('viewDate') will return `new Date()` if the
// input is empty but we want to keep the empty value
if (!inputEl.value) {
const { value } = inputEl;
if (!value) {
return;
}
var date = $(this).datetimepicker('viewDate').clone().locale('en');
var format = 'YYYY-MM-DD';
if ($(this).hasClass('s_website_form_datetime')) {
date = date.utc();
format = 'YYYY-MM-DD HH:mm:ss';
}
form_values[inputEl.getAttribute('name')] = date.format(format);
form_values[inputEl.getAttribute("name")] = this.matches(".s_website_form_date")
? serializeDate(parseDate(value))
: serializeDateTime(parseDateTime(value));
});
if (this._recaptchaLoaded) {
@@ -524,11 +510,13 @@
// consider checking the date inputs are not disabled before
// saying they are invalid (see checkValidity used here))
} else if ($(input).hasClass('s_website_form_date') || $(input).hasClass('o_website_form_date')) { // !compatibility
if (!self.is_datetime_valid(input.value, 'date')) {
const date = parseDate(input.value);
if (!date || !date.isValid) {
return true;
}
} else if ($(input).hasClass('s_website_form_datetime') || $(input).hasClass('o_website_form_datetime')) { // !compatibility
if (!self.is_datetime_valid(input.value, 'datetime')) {
const date = parseDateTime(input.value);
if (!date || !date.isValid) {
return true;
}
} else if (input.type === "file" && !self.isFileInputValid(input)) {
@@ -567,42 +555,6 @@
return form_valid;
},
is_datetime_valid: function (value, type_of_date) {
if (value === "") {
return true;
} else {
try {
this.parse_date(value, type_of_date);
return true;
} catch {
return false;
}
}
},
// This is a stripped down version of format.js parse_value function
parse_date: function (value, type_of_date, value_if_empty) {
var date_pattern = time.getLangDateFormat(),
time_pattern = time.getLangTimeFormat();
var date_pattern_wo_zero = date_pattern.replace('MM', 'M').replace('DD', 'D'),
time_pattern_wo_zero = time_pattern.replace('HH', 'H').replace('mm', 'm').replace('ss', 's');
switch (type_of_date) {
case 'datetime':
var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero], true);
if (datetime.isValid()) {
return time.datetime_to_str(datetime.toDate());
}
throw new Error(_t("'%s' is not a correct datetime", value));
case 'date':
var date = moment(value, [date_pattern, date_pattern_wo_zero], true);
if (date.isValid()) {
return time.date_to_str(date.toDate());
}
throw new Error(_t("'%s' is not a correct date", value));
}
return value;
},
update_status: function (status, message) {
if (status !== 'success') { // Restore send button behavior if result is an error
this.$el.find('.s_website_form_send, .o_website_form_send')
@@ -720,13 +672,7 @@
return value.name === '';
}
// Date & Date Time comparison requires formatting the value
if (value.includes(':')) {
const datetimeFormat = time.getLangDatetimeFormat();
value = moment(value, datetimeFormat)._d.getTime() / 1000;
} else {
const dateFormat = time.getLangDateFormat();
value = moment(value, dateFormat)._d.getTime() / 1000;
}
value = (value.includes(':') ? parseDateTime(value) : parseDate(value)).toUnixInteger();
comparable = parseInt(comparable);
between = parseInt(between) || '';
switch (comparator) {
@@ -176,19 +176,17 @@
<!-- Date Field -->
<t t-name="website.form_field_date">
<t t-call="website.form_field">
<t t-set="datepickerID" t-value="'datepicker' + Math.random().toString().substring(2)"/>
<div class="s_website_form_date input-group date" t-att-id="datepickerID" data-target-input="nearest">
<div class="s_website_form_date input-group date">
<input
type="text"
class="form-control datetimepicker-input s_website_form_input"
t-attf-data-target="##{datepickerID}"
t-att-name="field.name"
t-att-required="field.required || field.modelRequired || None"
t-att-value="field.value"
t-att-placeholder="field.placeholder"
t-att-id="field.id"
/>
<div class="input-group-text" t-attf-data-target="##{datepickerID}" data-toggle="datetimepicker"><i class="fa fa-calendar"></i></div>
<div class="input-group-text pe-none"><i class="fa fa-calendar"></i></div>
</div>
</t>
</t>
@@ -196,19 +194,17 @@
<!-- Datetime Field -->
<t t-name="website.form_field_datetime">
<t t-call="website.form_field">
<t t-set="datetimepickerID" t-value="'datetimepicker' + Math.random().toString().substring(2)"/>
<div class="s_website_form_datetime input-group date" t-att-id="datetimepickerID" data-target-input="nearest">
<div class="s_website_form_datetime input-group date">
<input
type="text"
class="form-control datetimepicker-input s_website_form_input"
t-attf-data-target="##{datetimepickerID}"
t-att-name="field.name"
t-att-required="field.required || field.modelRequired || None"
t-att-value="field.value"
t-att-placeholder="field.placeholder"
t-att-id="field.id"
/>
<div class="input-group-text" t-attf-data-target="##{datetimepickerID}" data-toggle="datetimepicker"><i class="fa fa-calendar"></i></div>
<div class="input-group-text pe-none"><i class="fa fa-calendar"></i></div>
</div>
</t>
</t>
@@ -86,9 +86,14 @@ registry.category("web_tour.tours").add("website_form_editor_tour_submit", {
":has(.s_website_form_field:has(label:contains('Invoice Scan')):not(.o_has_error))",
trigger: "input[name=Products][value='Wiko Stairway']"
},
{
content: "Open datetime picker",
trigger: ".s_website_form_datetime input",
run: "click",
},
{
content: "Complete Date field",
trigger: ".s_website_form_datetime [data-toggle='datetimepicker']",
trigger: ".o_date_picker .o_today",
},
{
content: "Check another product",
@@ -2,8 +2,7 @@
import { _t } from "@web/core/l10n/translation";
import publicWidget from "@web/legacy/js/public/public_widget";
import time from "@web/legacy/js/core/time";
import { parseDate, formatDate } from "@web/core/l10n/dates";
import { parseDate, formatDate, serializeDate } from "@web/core/l10n/dates";
const { DateTime } = luxon;
publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
@@ -18,7 +17,6 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
'click .edit_opp_confirm': '_onEditOppConfirm',
'change .edit_opp_form .next_activity': '_onChangeNextActivity',
'change #new-opp-dialog .contact_name': '_onChangeContactName',
'click div.input-group.date[data-target-input="nearest"]': '_onCalendarInputGroupClick',
},
//--------------------------------------------------------------------------
@@ -259,32 +257,11 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
$('.edit_opp_form .activity_date_deadline').val( formatDate(date) );
}
},
/**
* @private
* @param {Event} ev
*/
_onCalendarInputGroupClick: function (ev) {
const $calendarInputGroup = $(ev.currentTarget);
const calendarOptions = {
format : time.getLangDateFormat(),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'oi oi-chevron-up',
down: 'oi oi-chevron-down',
},
};
$calendarInputGroup.datetimepicker(calendarOptions);
},
_parse_date: function (value) {
console.log(value);
var date = parseDate(value);
if (date.isValid && date.year >= 1900) {
return formatDate(date);
}
else {
if (!date.isValid || date.year < 1900) {
return false;
}
return serializeDate(date);
},
});
@@ -759,10 +759,10 @@
</div>
<div class="col-md-7">
<label>Expected Closing:</label>
<div class="input-group date" id="exp_closing_div" data-target-input="nearest">
<div class="input-group date" id="exp_closing_div">
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="opportunity.date_deadline"/></t>
<input type="text" name="date_deadline" data-target="#exp_closing_div" t-att-value="date_formatted" class="datetimepicker-input form-control date_deadline" t-att-name="prefix"/>
<span class="input-group-text" data-target="#exp_closing_div" data-toggle="datetimepicker">
<input type="text" data-widget="datetime-picker" data-widget-type="date" name="date_deadline" t-att-value="date_formatted" class="datetimepicker-input form-control date_deadline" t-att-name="prefix"/>
<span class="input-group-text">
<span class="fa fa-calendar" role="img" aria-label="Calendar"></span>
</span>
</div>
@@ -784,10 +784,10 @@
</div>
<div class="col-md-7">
<label class="col-form-label" for="activity_date_deadline">Next Activity Date</label>
<div class="input-group date" id="next_activity_div" data-target-input="nearest">
<div class="input-group date" id="next_activity_div" >
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="user_activity.date_deadline"/></t>
<input type="text" name="activity_date_deadline" data-target="#next_activity_div" t-att-value="date_formatted" class="form-control activity_date_deadline datetimepicker-input" t-att-name="prefix"/>
<span class="input-group-text" data-target="#next_activity_div" data-toggle="datetimepicker">
<input type="text" data-widget="datetime-picker" data-widget-type="date" name="activity_date_deadline" t-att-value="date_formatted" class="form-control activity_date_deadline datetimepicker-input" t-att-name="prefix"/>
<span class="input-group-text">
<span class="fa fa-calendar" role="img" aria-label="Calendar" title="Calendar"></span>
</span>
</div>
-3
View File
@@ -130,8 +130,6 @@ Featuring
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/src/legacy/scss/tempusdominus_overridden.scss',
'web/static/lib/tempusdominus/tempusdominus.scss',
'web/static/lib/jquery.ui/jquery-ui.css',
'web/static/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/odoo_ui_icons/*',
@@ -184,7 +182,6 @@ Featuring
'web/static/lib/bootstrap/js/dist/scrollspy.js',
'web/static/lib/bootstrap/js/dist/tab.js',
'web/static/lib/bootstrap/js/dist/toast.js',
'web/static/lib/tempusdominus/tempusdominus.js',
'web/static/lib/select2/select2.js',
'web/static/lib/clipboard/clipboard.js',
'web/static/lib/jSignature/jSignatureCustom.js',
-4
View File
@@ -318,10 +318,6 @@ Files: addons/web/static/lib/select2/*
Copyright: 2014-2015 Igor Vaynberg
License: Apache-2.0
Files: addons/web/static/lib/tempusdominus/*
Copyright: 2016 Tempus Dominus; 2016 Jonathan Peterson
License: MIT
Files: addons/web/static/lib/underscore.string/*
Copyright: Esa-Matti Suuronen, Alexandru Marasteanu
License: MIT