[REF] *: convert legacy dialogs
The goal of this commit is to convert a bunch of legacy dialogs into owl dialogs. closes odoo/odoo#131278 Task-id: 3453920 Related: odoo/enterprise#45484 Signed-off-by: Samuel Degueldre (sad) <sad@odoo.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { markup } from "@odoo/owl";
|
||||
import { InputConfirmationDialog } from "@portal/js/components/input_confirmation_dialog/input_confirmation_dialog";
|
||||
import { handleCheckIdentity } from "@portal/js/portal_security";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import {handleCheckIdentity} from "@portal/js/portal_security";
|
||||
|
||||
/**
|
||||
* Replaces specific <field> elements by normal HTML, strip out the rest entirely
|
||||
@@ -131,67 +132,6 @@ function fixupViewBody(oldNode, record) {
|
||||
return [node, qrcode, code]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a backend <button> element and a bunch of metadata into a structure
|
||||
* which can kinda be of use to Dialog.
|
||||
*/
|
||||
class Button {
|
||||
constructor(parent, model, record_id, input_node, button_node) {
|
||||
this._parent = parent;
|
||||
this.model = model;
|
||||
this.record_id = record_id;
|
||||
this.input = input_node;
|
||||
this.text = button_node.getAttribute('string');
|
||||
this.classes = button_node.getAttribute('class') || null;
|
||||
this.action = button_node.getAttribute('name');
|
||||
if (button_node.getAttribute('special') === 'cancel') {
|
||||
this.close = true;
|
||||
this.click = null;
|
||||
} else {
|
||||
this.close = false;
|
||||
// because Dialog doesnt' call() click on the descriptor object
|
||||
this.click = this._click.bind(this);
|
||||
}
|
||||
}
|
||||
async _click() {
|
||||
if (!this.input.reportValidity()) {
|
||||
this.input.classList.add('is-invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.callAction(this.record_id, {code: this.input.value});
|
||||
} catch (e) {
|
||||
this.input.classList.add('is-invalid');
|
||||
// show custom validity error message
|
||||
this.input.setCustomValidity(e.message);
|
||||
this.input.reportValidity();
|
||||
return;
|
||||
}
|
||||
this.input.classList.remove('is-invalid');
|
||||
// reloads page, avoid window.location.reload() because it re-posts forms
|
||||
window.location = window.location;
|
||||
}
|
||||
async callAction(id, update) {
|
||||
try {
|
||||
await this._parent._rpc({model: this.model, method: 'write', args: [id, update]});
|
||||
await handleCheckIdentity(
|
||||
this._parent.proxy('_rpc'),
|
||||
this._parent._rpc({model: this.model, method: this.action, args: [id]})
|
||||
);
|
||||
} catch(e) {
|
||||
// avoid error toast (crashmanager)
|
||||
e.event.preventDefault();
|
||||
// try to unwrap mess of an error object to a usable error message
|
||||
throw new Error(
|
||||
!e.message ? e.toString()
|
||||
: !e.message.data ? e.message.message
|
||||
: e.message.data.message || _t("Operation failed for unknown reason.")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publicWidget.registry.TOTPButton = publicWidget.Widget.extend({
|
||||
selector: '#auth_totp_portal_enable',
|
||||
events: {
|
||||
@@ -205,7 +145,7 @@ publicWidget.registry.TOTPButton = publicWidget.Widget.extend({
|
||||
model: 'res.users',
|
||||
method: 'action_totp_enable_wizard',
|
||||
args: [this.getSession().user_id]
|
||||
}));
|
||||
}), (...args) => this.call("dialog", "add", ...args));
|
||||
|
||||
if (!w) {
|
||||
// TOTP probably already enabled, just reload page
|
||||
@@ -225,27 +165,49 @@ publicWidget.registry.TOTPButton = publicWidget.Widget.extend({
|
||||
);
|
||||
|
||||
const xmlBody = doc.querySelector('sheet *');
|
||||
const [body, , codeInput] = fixupViewBody(xmlBody, record);
|
||||
// remove custom validity error message any time the field changes
|
||||
// otherwise it sticks and browsers suppress submit
|
||||
codeInput.addEventListener('input', () => codeInput.setCustomValidity(''));
|
||||
const [body, ,] = fixupViewBody(xmlBody, record);
|
||||
|
||||
const buttons = [];
|
||||
for(const button of doc.querySelectorAll('footer button')) {
|
||||
buttons.push(new Button(this, model, record.id, codeInput, button));
|
||||
}
|
||||
this.call("dialog", "add", InputConfirmationDialog, {
|
||||
body: markup(body.outerHTML),
|
||||
onInput: ({ inputEl }) => {
|
||||
inputEl.setCustomValidity("");
|
||||
},
|
||||
confirmLabel: _t("Activate"),
|
||||
confirm: async ({ inputEl }) => {
|
||||
if (!inputEl.reportValidity()) {
|
||||
inputEl.classList.add("is-invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
// wrap in a root host of .modal-body otherwise it breaks our neat flex layout
|
||||
const $content = document.createElement('form');
|
||||
$content.appendChild(body);
|
||||
// implicit submission by pressing [return] from within input
|
||||
$content.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
// sadness: footer not available as normal element
|
||||
dialog.$footer.find('.btn-primary').click();
|
||||
try {
|
||||
await this._rpc({
|
||||
model,
|
||||
method: "write",
|
||||
args: [record.id, { code: inputEl.value }],
|
||||
});
|
||||
await handleCheckIdentity(
|
||||
this.proxy("_rpc"),
|
||||
this._rpc({ model, method: "enable", args: [record.id] }),
|
||||
(...args) => this.call("dialog", "add", ...args)
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMessage = (
|
||||
!e.message ? e.toString()
|
||||
: !e.message.data ? e.message.message
|
||||
: e.message.data.message || _t("Operation failed for unknown reason.")
|
||||
);
|
||||
inputEl.classList.add("is-invalid");
|
||||
// show custom validity error message
|
||||
inputEl.setCustomValidity(errorMessage);
|
||||
inputEl.reportValidity();
|
||||
return false;
|
||||
}
|
||||
// reloads page, avoid window.location.reload() because it re-posts forms
|
||||
window.location = window.location;
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
var dialog = new Dialog(this, {$content, buttons}).open();
|
||||
}
|
||||
},
|
||||
});
|
||||
publicWidget.registry.DisableTOTPButton = publicWidget.Widget.extend({
|
||||
selector: '#auth_totp_portal_disable',
|
||||
@@ -257,7 +219,8 @@ publicWidget.registry.DisableTOTPButton = publicWidget.Widget.extend({
|
||||
e.preventDefault();
|
||||
await handleCheckIdentity(
|
||||
this.proxy('_rpc'),
|
||||
this._rpc({model: 'res.users', method: 'action_totp_disable', args: [this.getSession().user_id]})
|
||||
this._rpc({model: 'res.users', method: 'action_totp_disable', args: [this.getSession().user_id]}),
|
||||
(...args) => this.call("dialog", "add", ...args)
|
||||
)
|
||||
window.location = window.location;
|
||||
}
|
||||
@@ -276,7 +239,8 @@ publicWidget.registry.RevokeTrustedDeviceButton = publicWidget.Widget.extend({
|
||||
model: 'auth_totp.device',
|
||||
method: 'remove',
|
||||
args: [parseInt(this.el.id)]
|
||||
})
|
||||
}),
|
||||
(...args) => this.call("dialog", "add", ...args)
|
||||
);
|
||||
window.location = window.location;
|
||||
}
|
||||
@@ -295,7 +259,8 @@ publicWidget.registry.RevokeAllTrustedDevicesButton = publicWidget.Widget.extend
|
||||
model: 'res.users',
|
||||
method: 'revoke_all_devices',
|
||||
args: [this.getSession().user_id]
|
||||
})
|
||||
}),
|
||||
(...args) => this.call("dialog", "add", ...args)
|
||||
);
|
||||
window.location = window.location;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { formatDuration } from "@web/core/l10n/dates";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { formatDuration } from "@web/views/fields/formatters";
|
||||
import {
|
||||
preloadStatusBar,
|
||||
statusBarField,
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
'payment/static/src/js/manage_form.js',
|
||||
'payment/static/src/js/payment_form_mixin.js',
|
||||
'payment/static/src/js/post_processing.js',
|
||||
'payment/static/src/xml/payment_post_processing.xml',
|
||||
'payment/static/src/xml/*.xml',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'payment/static/src/scss/payment_provider.scss',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { renderToMarkup } from "@web/core/utils/render";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
|
||||
import paymentFormMixin from "@payment/js/payment_form_mixin";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
@@ -68,49 +69,24 @@ import { debounce } from "@web/core/utils/timing";
|
||||
},
|
||||
|
||||
/**
|
||||
* Build the confirmation dialog based on the linked records' information.
|
||||
* Build and open the confirmation dialog based on the linked records' information.
|
||||
*
|
||||
* @private
|
||||
* @param {Array} linkedRecordsInfo - The list of information about linked records.
|
||||
* @param confirmCallback - The callback method called when the user clicks on the
|
||||
* confirmation button.
|
||||
* @return {object}
|
||||
*/
|
||||
_buildConfirmationDialog: function (linkedRecordsInfo, confirmCallback) {
|
||||
const $dialogContentMessage = $(
|
||||
'<span>', {text: _t("Are you sure you want to delete this payment method?")}
|
||||
_openConfirmationDialog: function (linkedRecordsInfo, confirmCallback) {
|
||||
const body = renderToMarkup(
|
||||
"payment.ManageFormConfirmationDialog",
|
||||
{ linkedRecordsInfo }
|
||||
);
|
||||
if (linkedRecordsInfo.length > 0) { // List the documents linked to the token.
|
||||
$dialogContentMessage.append($('<br>'));
|
||||
$dialogContentMessage.append($(
|
||||
'<span>', {text: _t("It is currently linked to the following documents:")}
|
||||
));
|
||||
const $documentInfoList = $('<ul>');
|
||||
linkedRecordsInfo.forEach(documentInfo => {
|
||||
$documentInfoList.append($('<li>').append($(
|
||||
'<a>', {
|
||||
href: documentInfo.url,
|
||||
target: '_blank',
|
||||
title: documentInfo.description,
|
||||
text: documentInfo.name
|
||||
}
|
||||
)));
|
||||
});
|
||||
$dialogContentMessage.append($documentInfoList);
|
||||
}
|
||||
return new Dialog(this, {
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
title: _t("Warning!"),
|
||||
size: 'medium',
|
||||
$content: $('<div>').append($dialogContentMessage),
|
||||
buttons: [
|
||||
{
|
||||
text: _t("Confirm Deletion"), classes: 'btn-primary', close: true,
|
||||
click: confirmCallback,
|
||||
},
|
||||
{
|
||||
text: _t("Cancel"), close: true
|
||||
},
|
||||
],
|
||||
body,
|
||||
confirmLabel: _t("Confirm Deletion"),
|
||||
confirm: confirmCallback,
|
||||
cancel: () => {},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -154,7 +130,7 @@ import { debounce } from "@web/core/utils/timing";
|
||||
method: 'get_linked_records_info',
|
||||
args: [tokenId],
|
||||
}).then(linkedRecordsInfo => {
|
||||
this._buildConfirmationDialog(linkedRecordsInfo, execute).open();
|
||||
this._openConfirmationDialog(linkedRecordsInfo, execute);
|
||||
}).guardedCatch(error => {
|
||||
this._displayError(
|
||||
_t("Server Error"),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { escape } from "@web/core/utils/strings";
|
||||
import core from "@web/legacy/js/services/core";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export default {
|
||||
@@ -70,18 +69,14 @@
|
||||
* @param {string} title - The title of the error
|
||||
* @param {string} description - The description of the error
|
||||
* @param {string} error - The raw error message
|
||||
* @return {(Dialog|undefined)} A dialog showing the error if no payment option is selected,
|
||||
* undefined otherwise.
|
||||
*/
|
||||
_displayError: function (title, description = '', error = '') {
|
||||
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
|
||||
if ($checkedRadios.length !== 1) { // Cannot find selected payment option, show dialog
|
||||
return new Dialog(null, {
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
title: _t("Error: %s", title),
|
||||
size: 'medium',
|
||||
$content: `<p>${escape(description) || ''}</p>`,
|
||||
buttons: [{text: _t("Ok"), close: true}]
|
||||
}).open();
|
||||
body: description || '',
|
||||
});
|
||||
} else { // Show error in inline form
|
||||
this._hideError(); // Remove any previous error
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="payment.ManageFormConfirmationDialog" owl="1">
|
||||
<div>
|
||||
<span>Are you sure you want to delete this payment method?</span>
|
||||
<t t-if="linkedRecordsInfo.length > 0">
|
||||
<br/>
|
||||
<span>It is currently linked to the following documents:</span>
|
||||
<ul>
|
||||
<t t-foreach="linkedRecordsInfo" t-as="documentInfo" t-key="documentInfo_index">
|
||||
<li t-att-title="documentInfo.description" t-att-href="documentInfo.url" t-esc="documentInfo.name"/>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -47,6 +47,7 @@ a dependency towards website editing and customization capabilities.""",
|
||||
'portal/static/src/xml/portal_signature.xml',
|
||||
'portal/static/src/js/portal_sidebar.js',
|
||||
'portal/static/src/xml/portal_security.xml',
|
||||
'portal/static/src/js/components/**/*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'portal/static/tests/**/*',
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useEffect } from "@odoo/owl";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
export class InputConfirmationDialog extends ConfirmationDialog {
|
||||
static props = {
|
||||
...ConfirmationDialog.props,
|
||||
onInput: Function,
|
||||
};
|
||||
static template = "portal.InputConfirmationDialog";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
const onInput = () => {
|
||||
if (this.props.onInput) {
|
||||
this.props.onInput({ inputEl: this.inputEl });
|
||||
}
|
||||
};
|
||||
const onKeydown = (ev) => {
|
||||
if (ev.key && ev.key.toLowerCase() === "enter") {
|
||||
ev.preventDefault();
|
||||
this._confirm();
|
||||
}
|
||||
};
|
||||
useEffect(
|
||||
(inputEl) => {
|
||||
this.inputEl = inputEl;
|
||||
if (this.inputEl) {
|
||||
this.inputEl.focus();
|
||||
this.inputEl.addEventListener("keydown", onKeydown);
|
||||
this.inputEl.addEventListener("input", onInput);
|
||||
return () => {
|
||||
this.inputEl.removeEventListener("keydown", onKeydown);
|
||||
this.inputEl.removeEventListener("input", onInput);
|
||||
};
|
||||
}
|
||||
},
|
||||
() => [this.modalRef.el?.querySelector("input")]
|
||||
);
|
||||
}
|
||||
|
||||
_confirm() {
|
||||
this.execButton(() => this.props.confirm({ inputEl: this.inputEl }));
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="portal.InputConfirmationDialog" t-inherit="web.ConfirmationDialog" owl="1">
|
||||
<xpath expr="//p[hasclass('text-prewrap')]" position="attributes">
|
||||
<attribute name="class"></attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,10 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';
|
||||
import { renderToMarkup } from "@web/core/utils/render";
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import { _t } from "@web/legacy/js/services/core";
|
||||
import { renderToString } from "@web/core/utils/render";
|
||||
import session from "web.session";
|
||||
import { InputConfirmationDialog } from './components/input_confirmation_dialog/input_confirmation_dialog';
|
||||
|
||||
publicWidget.registry.NewAPIKeyButton = publicWidget.Widget.extend({
|
||||
selector: '.o_portal_new_api_key',
|
||||
@@ -23,46 +24,40 @@ publicWidget.registry.NewAPIKeyButton = publicWidget.Widget.extend({
|
||||
model: 'res.users',
|
||||
method: 'api_key_wizard',
|
||||
args: [session.user_id],
|
||||
}));
|
||||
const self = this;
|
||||
const d_description = new Dialog(self, {
|
||||
title: _t('New API Key'),
|
||||
$content: renderToString('portal.keydescription'),
|
||||
buttons: [{text: _t('Confirm'), classes: 'btn-primary', close: true, click: async () => {
|
||||
var description = d_description.el.querySelector('[name="description"]').value;
|
||||
var wizard_id = await this._rpc({
|
||||
model: 'res.users.apikeys.description',
|
||||
method: 'create',
|
||||
args: [{name: description}],
|
||||
}), (...args) => this.call("dialog", "add", ...args));
|
||||
|
||||
this.call("dialog", "add", InputConfirmationDialog, {
|
||||
title: _t("New API Key"),
|
||||
body: renderToMarkup("portal.keydescription"),
|
||||
confirmLabel: _t("Confirm"),
|
||||
confirm: async ({ inputEl }) => {
|
||||
const description = inputEl.value;
|
||||
const wizard_id = await this._rpc({
|
||||
model: "res.users.apikeys.description",
|
||||
method: "create",
|
||||
args: [{ name: description }],
|
||||
});
|
||||
var res = await handleCheckIdentity(
|
||||
const res = await handleCheckIdentity(
|
||||
this.proxy('_rpc'),
|
||||
this._rpc({
|
||||
model: 'res.users.apikeys.description',
|
||||
method: 'make_key',
|
||||
args: [wizard_id],
|
||||
})
|
||||
}),
|
||||
(...args) => this.call("dialog", "add", ...args)
|
||||
);
|
||||
const d_show = new Dialog(self, {
|
||||
title: _t('API Key Ready'),
|
||||
$content: renderToString('portal.keyshow', {key: res.context.default_key}),
|
||||
buttons: [{text: _t('Close'), clases: 'btn-primary', close: true}],
|
||||
});
|
||||
d_show.on('closed', this, () => {
|
||||
window.location = window.location;
|
||||
});
|
||||
d_show.open();
|
||||
}}, {text: _t('Discard'), close: true}],
|
||||
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
title: _t("API Key Ready"),
|
||||
body: renderToMarkup("portal.keyshow", { key: res.context.default_key }),
|
||||
confirmLabel: _t("Close"),
|
||||
}, {
|
||||
onClose: () => {
|
||||
window.location = window.location;
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
d_description.opened(() => {
|
||||
const input = d_description.el.querySelector('[name="description"]');
|
||||
input.focus();
|
||||
d_description.el.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
d_description.$footer.find('.btn-primary').click();
|
||||
});
|
||||
});
|
||||
d_description.open();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -80,7 +75,8 @@ publicWidget.registry.RemoveAPIKeyButton = publicWidget.Widget.extend({
|
||||
model: 'res.users.apikeys',
|
||||
method: 'remove',
|
||||
args: [parseInt(this.el.id)]
|
||||
})
|
||||
}),
|
||||
(...args) => this.call("dialog", "add", ...args)
|
||||
);
|
||||
window.location = window.location;
|
||||
}
|
||||
@@ -121,72 +117,49 @@ publicWidget.registry.RevokeSessionsButton = publicWidget.Widget.extend({
|
||||
},
|
||||
|
||||
async _onClick() {
|
||||
const rpc = this.proxy('_rpc');
|
||||
const wrapped = this._rpc({
|
||||
const { res_id: checkId } = await this._rpc({
|
||||
model: 'res.users',
|
||||
method: 'api_key_wizard',
|
||||
args: [session.user_id],
|
||||
});
|
||||
await handleRevokeSessions(rpc, wrapped);
|
||||
window.location.href = "/web/session/logout?redirect=/";
|
||||
this.call("dialog", "add", InputConfirmationDialog, {
|
||||
title: _t("Log out from all devices?"),
|
||||
body: renderToMarkup("portal.revoke_all_devices_popup_template"),
|
||||
confirmLabel: _t("Log out from all devices"),
|
||||
confirm: async ({ inputEl }) => {
|
||||
if (!inputEl.reportValidity()) {
|
||||
inputEl.classList.add("is-invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleRevokeSessions(rpc, wrapped) {
|
||||
return wrapped.then((inst) => {
|
||||
const check_id = inst.res_id
|
||||
var $content = $(renderToString("portal.revoke_all_devices_popup_template"));
|
||||
return new Promise((resolve) => {
|
||||
var dialog = new Dialog(this, {
|
||||
title: _t("Log out from all devices?"),
|
||||
$content,
|
||||
buttons: [{
|
||||
text: _t("Log out from all devices"), classes: 'btn btn-primary',
|
||||
// nb: if click & close, waits for click to resolve before closing
|
||||
async click() {
|
||||
const password_input = this.el.querySelector('[name=password]');
|
||||
if (!password_input.reportValidity()) {
|
||||
password_input.classList.add('is-invalid');
|
||||
return;
|
||||
};
|
||||
await rpc({
|
||||
model: 'res.users.identitycheck',
|
||||
method: 'write',
|
||||
args: [check_id, {password: password_input.value}]
|
||||
});
|
||||
await rpc({
|
||||
model: 'res.users.identitycheck',
|
||||
method: 'revoke_all_devices',
|
||||
args: [check_id]
|
||||
}).then((inst) => {
|
||||
this.close();
|
||||
resolve(inst);
|
||||
}, (err) => {
|
||||
err.event.preventDefault(); // suppress crashmanager
|
||||
password_input.classList.add('is-invalid');
|
||||
password_input.setCustomValidity(_t("Check failed"));
|
||||
password_input.reportValidity();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
text: _t('Cancel'), close: true
|
||||
}]
|
||||
await this._rpc({
|
||||
model: "res.users.identitycheck",
|
||||
method: "write",
|
||||
args: [checkId, { password: inputEl.value }],
|
||||
});
|
||||
try {
|
||||
await this._rpc({
|
||||
model: "res.users.identitycheck",
|
||||
method: "revoke_all_devices",
|
||||
args: [checkId],
|
||||
});
|
||||
dialog.opened(() => {
|
||||
const password_input = dialog.el.querySelector('[name="password"]');
|
||||
password_input.focus();
|
||||
password_input.addEventListener('input', () => {
|
||||
password_input.classList.remove('is-invalid');
|
||||
password_input.setCustomValidity('');
|
||||
});
|
||||
dialog.el.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
dialog.$footer.find('.btn-primary').click();
|
||||
});
|
||||
});
|
||||
dialog.open();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
inputEl.classList.add("is-invalid");
|
||||
inputEl.setCustomValidity(_t("Check failed"));
|
||||
inputEl.reportValidity();
|
||||
return false;
|
||||
}
|
||||
|
||||
window.location.href = "/web/session/logout?redirect=/";
|
||||
return true;
|
||||
},
|
||||
cancel: () => {},
|
||||
onInput: ({ inputEl }) => {
|
||||
inputEl.classList.remove("is-invalid");
|
||||
inputEl.setCustomValidity("");
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -200,65 +173,52 @@ publicWidget.registry.RevokeSessionsButton = publicWidget.Widget.extend({
|
||||
*
|
||||
* @param {Function} rpc Widget#_rpc bound do the widget
|
||||
* @param {Promise} wrapped promise to check for an identity check request
|
||||
* @param {Function} addDialog add a dialog to the dialog service
|
||||
* @returns {Promise} result of the original call
|
||||
*/
|
||||
export function handleCheckIdentity(rpc, wrapped) {
|
||||
export async function handleCheckIdentity(rpc, wrapped, addDialog) {
|
||||
return wrapped.then((r) => {
|
||||
if (!(r.type === "ir.actions.act_window" && r.res_model === "res.users.identitycheck")) {
|
||||
return r;
|
||||
}
|
||||
const check_id = r.res_id;
|
||||
return new Promise((resolve, reject) => {
|
||||
const d = new Dialog(null, {
|
||||
const checkId = r.res_id;
|
||||
return new Promise((resolve) => {
|
||||
addDialog(InputConfirmationDialog, {
|
||||
title: _t("Security Control"),
|
||||
$content: renderToString('portal.identitycheck'),
|
||||
buttons: [{
|
||||
text: _t("Confirm Password"), classes: 'btn btn-primary',
|
||||
// nb: if click & close, waits for click to resolve before closing
|
||||
click() {
|
||||
const password_input = this.el.querySelector('[name=password]');
|
||||
if (!password_input.reportValidity()) {
|
||||
password_input.classList.add('is-invalid');
|
||||
return;
|
||||
}
|
||||
return rpc({
|
||||
model: 'res.users.identitycheck',
|
||||
method: 'write',
|
||||
args: [check_id, {password: password_input.value}]
|
||||
}).then(() => rpc({
|
||||
model: 'res.users.identitycheck',
|
||||
method: 'run_check',
|
||||
args: [check_id]
|
||||
})).then((r) => {
|
||||
this.close();
|
||||
resolve(r);
|
||||
}, (err) => {
|
||||
err.event.preventDefault(); // suppress crashmanager
|
||||
password_input.classList.add('is-invalid');
|
||||
password_input.setCustomValidity(_t("Check failed"));
|
||||
password_input.reportValidity();
|
||||
});
|
||||
body: renderToMarkup("portal.identitycheck"),
|
||||
confirmLabel: _t("Confirm Password"),
|
||||
confirm: async ({ inputEl }) => {
|
||||
if (!inputEl.reportValidity()) {
|
||||
inputEl.classList.add("is-invalid");
|
||||
return false;
|
||||
}
|
||||
}, {
|
||||
text: _t('Cancel'), close: true
|
||||
}]
|
||||
}).on('close', null, () => {
|
||||
// unlink wizard object?
|
||||
reject();
|
||||
let result;
|
||||
await rpc({
|
||||
model: "res.users.identitycheck",
|
||||
method: "write",
|
||||
args: [checkId, { password: inputEl.value }],
|
||||
});
|
||||
try {
|
||||
result = await rpc({
|
||||
model: "res.users.identitycheck",
|
||||
method: "run_check",
|
||||
args: [checkId],
|
||||
});
|
||||
} catch {
|
||||
inputEl.classList.add("is-invalid");
|
||||
inputEl.setCustomValidity(_t("Check failed"));
|
||||
inputEl.reportValidity();
|
||||
return false;
|
||||
}
|
||||
resolve(result);
|
||||
return true;
|
||||
},
|
||||
cancel: () => {},
|
||||
onInput: ({ inputEl }) => {
|
||||
inputEl.classList.remove("is-invalid");
|
||||
inputEl.setCustomValidity("");
|
||||
},
|
||||
});
|
||||
d.opened(() => {
|
||||
const pw = d.el.querySelector('[name="password"]');
|
||||
pw.focus();
|
||||
pw.addEventListener('input', () => {
|
||||
pw.classList.remove('is-invalid');
|
||||
pw.setCustomValidity('');
|
||||
});
|
||||
d.el.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
d.$footer.find('.btn-primary').click();
|
||||
});
|
||||
});
|
||||
d.open();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="portal.identitycheck">
|
||||
<t t-name="portal.identitycheck" owl="1">
|
||||
<form string="Security Control">
|
||||
<h3><strong>Please enter your password to confirm you own this account</strong></h3>
|
||||
<br/>
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="/web/reset_password/" class="btn btn-link" role="button">Forgot password?</a>
|
||||
</form>
|
||||
</t>
|
||||
<t t-name="portal.keydescription">
|
||||
<t t-name="portal.keydescription" owl="1">
|
||||
<form string="Key Description">
|
||||
<h3><strong>Name your key</strong></h3>
|
||||
<p>Enter a description of and purpose for the key.</p>
|
||||
@@ -23,7 +23,7 @@
|
||||
</p>
|
||||
</form>
|
||||
</t>
|
||||
<t t-name="portal.keyshow">
|
||||
<t t-name="portal.keyshow" owl="1">
|
||||
<div>
|
||||
<h3><strong>Write down your key</strong></h3>
|
||||
<p>
|
||||
@@ -39,23 +39,19 @@
|
||||
</div>
|
||||
</t>
|
||||
<!-- Popup's view !-->
|
||||
<t t-name="portal.revoke_all_devices_popup_template">
|
||||
<t t-name="portal.revoke_all_devices_popup_template" owl="1">
|
||||
<section>
|
||||
<div>
|
||||
You are about to log out from all devices that currently have access to this user's account.
|
||||
</div><br/>
|
||||
<t t-set="path">password</t>
|
||||
<form action="/my/security" method="post" class="oe_reset_password_form" >
|
||||
<input type="hidden" name="op" value="revoke"/>
|
||||
<div class="mb-3">
|
||||
<label for="current">Type in your password to confirm :</label>
|
||||
<input type="password" t-attf-class="form-control form-control-sm"
|
||||
id="current" name="password"
|
||||
autocomplete="current-password" required="required"/>
|
||||
<div class="invalid-feedback">
|
||||
<t t-esc="error"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="op" value="revoke"/>
|
||||
</form>
|
||||
</section>
|
||||
</t>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** @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 config from "@web/legacy/js/services/config";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import dom from "@web/legacy/js/core/dom";
|
||||
import {getCookie, setCookie, deleteCookie} from "@web/legacy/js/core/cookie_utils";
|
||||
|
||||
@@ -1055,14 +1055,22 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
|
||||
locale: moment.locale(),
|
||||
allowInputToggle: true,
|
||||
});
|
||||
$dateGroup.on('error.datetimepicker', function (err) {
|
||||
$dateGroup.on('error.datetimepicker', (err) => {
|
||||
if (err.date) {
|
||||
if (err.date < minDate) {
|
||||
Dialog.alert(this, _t('The date you selected is lower than the minimum date: ') + minDate.format(datetimepickerFormat));
|
||||
this.call("dialog", "add", AlertDialog, {
|
||||
body:
|
||||
_t("The date you selected is lower than the minimum date: ") +
|
||||
minDate.format(datetimepickerFormat),
|
||||
});
|
||||
}
|
||||
|
||||
if (err.date > maxDate) {
|
||||
Dialog.alert(this, _t('The date you selected is greater than the maximum date: ') + maxDate.format(datetimepickerFormat));
|
||||
this.call("dialog", "add", AlertDialog, {
|
||||
body:
|
||||
_t("The date you selected is greater than the maximum date: ") +
|
||||
maxDate.format(datetimepickerFormat),
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -10,48 +10,47 @@ export class ConfirmationDialog extends Component {
|
||||
setup() {
|
||||
this.env.dialogData.close = () => this._cancel();
|
||||
this.modalRef = useChildRef();
|
||||
this.isConfirmedOrCancelled = false; // ensures we do not confirm and/or cancel twice
|
||||
this.isProcess = false;
|
||||
}
|
||||
|
||||
async _cancel() {
|
||||
if (this.isConfirmedOrCancelled) {
|
||||
return;
|
||||
}
|
||||
this.isConfirmedOrCancelled = true;
|
||||
this.disableButtons();
|
||||
if (this.props.cancel) {
|
||||
try {
|
||||
await this.props.cancel();
|
||||
} catch (e) {
|
||||
this.props.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.props.close();
|
||||
this.execButton(this.props.cancel);
|
||||
}
|
||||
|
||||
async _confirm() {
|
||||
if (this.isConfirmedOrCancelled) {
|
||||
return;
|
||||
}
|
||||
this.isConfirmedOrCancelled = true;
|
||||
this.disableButtons();
|
||||
if (this.props.confirm) {
|
||||
try {
|
||||
await this.props.confirm();
|
||||
} catch (e) {
|
||||
this.props.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.props.close();
|
||||
this.execButton(this.props.confirm);
|
||||
}
|
||||
disableButtons() {
|
||||
|
||||
setButtonsDisabled(disabled) {
|
||||
this.isProcess = disabled;
|
||||
if (!this.modalRef.el) {
|
||||
return; // safety belt for stable versions
|
||||
}
|
||||
for (const button of [...this.modalRef.el.querySelectorAll(".modal-footer button")]) {
|
||||
button.disabled = true;
|
||||
button.disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
async execButton(callback) {
|
||||
if (this.isProcess) {
|
||||
return;
|
||||
}
|
||||
this.setButtonsDisabled(true);
|
||||
if (callback) {
|
||||
let shouldClose;
|
||||
try {
|
||||
shouldClose = await callback();
|
||||
} catch (e) {
|
||||
this.props.close();
|
||||
throw e;
|
||||
}
|
||||
if (shouldClose === false) {
|
||||
this.setButtonsDisabled(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
ConfirmationDialog.template = "web.ConfirmationDialog";
|
||||
ConfirmationDialog.components = { Dialog };
|
||||
|
||||
@@ -21,4 +21,4 @@
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
</templates>
|
||||
|
||||
@@ -293,10 +293,55 @@ export function formatDateTime(value, options = {}) {
|
||||
return value.setZone("default").toFormat(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given duration in seconds into a human-readable format.
|
||||
*
|
||||
* The function takes a duration in seconds and converts it into a human-readable form,
|
||||
* such as "1h" or "1 hour, 30 minutes", depending on the value of the `showFullDuration` parameter.
|
||||
* If the `showFullDuration` is set to true, the function will display up to two non-zero duration
|
||||
* components in long form (e.g: hours, minutes).
|
||||
* Otherwise, it will show just the largest non-zero duration component in narrow form (e.g: y or h).
|
||||
* Luxon takes care of translations given the current locale.
|
||||
*
|
||||
* @param {number} seconds - The duration in seconds to be converted.
|
||||
* @param {boolean} showFullDuration - If true, the output will have two components in long form.
|
||||
* Otherwise, just one component will be displayed in narrow form.
|
||||
*
|
||||
* @returns {string} A human-readable string representation of the duration.
|
||||
*
|
||||
* @example
|
||||
* // Sample usage
|
||||
* const durationInSeconds = 7320; // 2 hours and 2 minutes (2 * 3600 + 2 * 60)
|
||||
* const fullDuration = humanizeDuration(durationInSeconds, true);
|
||||
* console.log(fullDuration); // Output: "2 hours, 2 minutes"
|
||||
*
|
||||
* const shortDuration = humanizeDuration(durationInSeconds, false);
|
||||
* console.log(shortDuration); // Output: "2h"
|
||||
*/
|
||||
export function formatDuration(seconds, showFullDuration) {
|
||||
const displayStyle = showFullDuration ? "long" : "narrow";
|
||||
const numberOfValuesToDisplay = showFullDuration ? 2 : 1;
|
||||
const durationKeys = ["years", "months", "days", "hours", "minutes"];
|
||||
|
||||
if (seconds < 60) {
|
||||
seconds = 60;
|
||||
}
|
||||
seconds -= seconds % 60;
|
||||
|
||||
let duration = luxon.Duration.fromObject({ seconds: seconds }).shiftTo(...durationKeys);
|
||||
duration = duration.shiftTo(...durationKeys.filter((key) => duration.get(key)));
|
||||
const durationSplit = duration.toHuman({ unitDisplay: displayStyle }).split(",");
|
||||
|
||||
if (!showFullDuration && duration.loc.locale.includes("en") && duration.months > 0) {
|
||||
durationSplit[0] = durationSplit[0].replace("m", "M");
|
||||
}
|
||||
return durationSplit.slice(0, numberOfValuesToDisplay).join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given DateTime to the server date format.
|
||||
* @param {DateTime} value
|
||||
* @returns {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeDate(value) {
|
||||
if (!dateCache.has(value)) {
|
||||
@@ -308,7 +353,7 @@ export function serializeDate(value) {
|
||||
/**
|
||||
* Formats the given DateTime to the server datetime format.
|
||||
* @param {DateTime} value
|
||||
* @returns {string}
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeDateTime(value) {
|
||||
if (!dateTimeCache.has(value)) {
|
||||
|
||||
@@ -23,13 +23,13 @@ import { setLoadXmlDefaultApp, loadJS, templates } from '@web/core/assets';
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { browser } from '@web/core/browser/browser';
|
||||
import { jsonrpc } from '@web/core/network/rpc_service';
|
||||
import { renderToString } from "@web/core/utils/render";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { omit } from "@web/core/utils/objects";
|
||||
import { Component, App, whenReady } from "@odoo/owl";
|
||||
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
import { Component, App, whenReady } from "@odoo/owl";
|
||||
import { omit } from '@web/core/utils/objects';
|
||||
import { renderToString } from '@web/core/utils/render';
|
||||
|
||||
// Load localizations outside the PublicRoot to not wait for DOM ready (but
|
||||
// wait for them in PublicRoot)
|
||||
|
||||
@@ -77,51 +77,6 @@ export function formatChar(value, options) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given duration in seconds into a human-readable format.
|
||||
*
|
||||
* The function takes a duration in seconds and converts it into a human-readable form,
|
||||
* such as "1h" or "1 hour, 30 minutes", depending on the value of the `showFullDuration` parameter.
|
||||
* If the `showFullDuration` is set to true, the function will display up to two non-zero duration
|
||||
* components in long form (e.g: hours, minutes).
|
||||
* Otherwise, it will show just the largest non-zero duration component in narrow form (e.g: y or h).
|
||||
* Luxon takes care of translations given the current locale.
|
||||
*
|
||||
* @param {number} seconds - The duration in seconds to be converted.
|
||||
* @param {boolean} showFullDuration - If true, the output will have two components in long form.
|
||||
* Otherwise, just one component will be displayed in narrow form.
|
||||
*
|
||||
* @returns {string} A human-readable string representation of the duration.
|
||||
*
|
||||
* @example
|
||||
* // Sample usage
|
||||
* const durationInSeconds = 7320; // 2 hours and 2 minutes (2 * 3600 + 2 * 60)
|
||||
* const fullDuration = humanizeDuration(durationInSeconds, true);
|
||||
* console.log(fullDuration); // Output: "2 hours, 2 minutes"
|
||||
*
|
||||
* const shortDuration = humanizeDuration(durationInSeconds, false);
|
||||
* console.log(shortDuration); // Output: "2h"
|
||||
*/
|
||||
export function formatDuration(seconds, showFullDuration) {
|
||||
const displayStyle = showFullDuration ? "long" : "narrow";
|
||||
const numberOfValuesToDisplay = showFullDuration ? 2 : 1;
|
||||
const durationKeys = ["years", "months", "days", "hours", "minutes"];
|
||||
|
||||
if (seconds < 60) {
|
||||
seconds = 60;
|
||||
}
|
||||
seconds -= seconds % 60;
|
||||
|
||||
let duration = luxon.Duration.fromObject({ seconds: seconds }).shiftTo(...durationKeys);
|
||||
duration = duration.shiftTo(...durationKeys.filter((key) => duration.get(key)));
|
||||
const durationSplit = duration.toHuman({ unitDisplay: displayStyle }).split(",");
|
||||
|
||||
if (!showFullDuration && duration.loc.locale.includes("en") && duration.months > 0) {
|
||||
durationSplit[0] = durationSplit[0].replace("m", "M");
|
||||
}
|
||||
return durationSplit.slice(0, numberOfValuesToDisplay).join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing a float. The result takes into account the
|
||||
* user settings (to display the correct decimal separator).
|
||||
|
||||
@@ -4,12 +4,13 @@ import config from "@web/legacy/js/services/config";
|
||||
import concurrency from "@web/legacy/js/core/concurrency";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import dom from "@web/legacy/js/core/dom";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import Widget from "@web/legacy/js/core/widget";
|
||||
import localStorage from "@web/legacy/js/core/local_storage";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { sortBy } from "@web/core/utils/arrays";
|
||||
import { pick } from "@web/core/utils/objects";
|
||||
import { AlertDialog, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* Formats a content-check result (@see checkXML, checkSCSS).
|
||||
@@ -683,20 +684,23 @@ var ViewEditor = Widget.extend({
|
||||
}).bind(this)
|
||||
);
|
||||
|
||||
var self = this;
|
||||
return Promise.all(defs).guardedCatch(function (results) {
|
||||
// some overrides handle errors themselves
|
||||
if (results === undefined) {
|
||||
return;
|
||||
}
|
||||
var error = results[1];
|
||||
Dialog.alert(self, '', {
|
||||
const content = _t(
|
||||
"A server error occured. Please check you correctly signed in and that the file you are saving is correctly formatted."
|
||||
);
|
||||
const body = markup(`
|
||||
<div>${content}</div>
|
||||
<br/>
|
||||
${error}
|
||||
`);
|
||||
this.call("dialog", "add", AlertDialog, {
|
||||
title: _t("Server error"),
|
||||
$content: $('<div/>').html(
|
||||
_t("A server error occured. Please check you correctly signed in and that the file you are saving is correctly formatted.")
|
||||
+ '<br/>'
|
||||
+ error
|
||||
)
|
||||
body,
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -1004,12 +1008,13 @@ var ViewEditor = Widget.extend({
|
||||
* @private
|
||||
*/
|
||||
_onResetClick: function () {
|
||||
var self = this;
|
||||
Dialog.confirm(this, _t("If you reset this file, all your customizations will be lost as it will be reverted to the default file."), {
|
||||
title: _t("Careful!"),
|
||||
confirm_callback: function () {
|
||||
self._resetResource(self._getSelectedResource());
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
title: _t("Careful"),
|
||||
body: _t("If you reset this file, all your customizations will be lost as it will be reverted to the default file."),
|
||||
confirm: () => {
|
||||
this._resetResource(this._getSelectedResource());
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
'website_event_exhibitor/static/src/js/event_exhibitor_connect.js',
|
||||
'website_event_exhibitor/static/src/js/event_sponsor_search.js',
|
||||
'website_event_exhibitor/static/src/xml/event_exhibitor_connect.xml',
|
||||
'website_event_exhibitor/static/src/components/exhibitor_connect_closed_dialog/**/*',
|
||||
],
|
||||
'web.report_assets_common': [
|
||||
'/website_event_exhibitor/static/src/scss/event_full_page_ticket_report.scss',
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component, onWillStart, markup } from "@odoo/owl";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { formatDuration } from "@web/core/l10n/dates";
|
||||
|
||||
export class ExhibitorConnectClosedDialog extends Component {
|
||||
static template = "website_event_exhibitor.ExhibitorConnectClosedDialog";
|
||||
static components = { Dialog };
|
||||
static props = {
|
||||
sponsorId: Number,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
|
||||
onWillStart(() => this.fetchSponsor());
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async fetchSponsor() {
|
||||
const sponsorData = await this.rpc(
|
||||
`/event_sponsor/${encodeURIComponent(this.props.sponsorId)}/read`
|
||||
);
|
||||
sponsorData.website_description = markup(sponsorData.website_description);
|
||||
this.formatEventStartRemaining = formatDuration(sponsorData.event_start_remaining, true);
|
||||
this.sponsorData = sponsorData;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website_event_exhibitor.ExhibitorConnectClosedDialog" owl="1">
|
||||
<Dialog size="'md'" header="false" footer="false" >
|
||||
<div class="o_wesponsor_js_connect_modal_main container">
|
||||
<div class="row mt-2">
|
||||
<t t-if="! sponsorData.event_is_ongoing">
|
||||
<div class="col-12 alert alert-warning text-center" role="alert"
|
||||
t-if="sponsorData.event_is_done">
|
||||
Event <span t-out="sponsorData.event_name" class="fw-bold"/> is over.
|
||||
|
||||
<br/>
|
||||
<span>Join us next time to meet <b t-out="sponsorData.name"/>!</span>
|
||||
</div>
|
||||
<div class="col-12 alert alert-warning text-center" role="alert"
|
||||
t-else="">
|
||||
<span t-out="sponsorData.name" class="fw-bold"/> is not available right now.<br />
|
||||
Event <span t-out="sponsorData.event_name" class="fw-bold"/>
|
||||
<span t-if="sponsorData.event_start_today">
|
||||
starts in
|
||||
<span t-if="sponsorData.event_start_remaining >= 1" t-out="formatEventStartRemaining"/>
|
||||
<t t-else="">
|
||||
a few seconds
|
||||
</t>.
|
||||
</span>
|
||||
<span class="my-0" t-else="">
|
||||
starts on <span t-out="sponsorData.event_date_begin_located"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<div class="col-12 alert alert-warning text-center" role="alert"
|
||||
t-else="">
|
||||
<span t-out="sponsorData.name" class="fw-bold"/> is not available right now.<br />
|
||||
Come back between
|
||||
<strong>
|
||||
<t t-out="sponsorData.hour_from_str"/>
|
||||
-
|
||||
<t t-out="sponsorData.hour_to_str"/>
|
||||
</strong> (<span t-out="sponsorData.event_date_tz"/>)
|
||||
to meet them!
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<img class="img" style="max-width: 100%;"
|
||||
t-att-src="sponsorData.website_image_url"
|
||||
t-att-alt="sponsorData.name"/>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="d-flex align-items-top mb-3">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 t-out="sponsorData.name" class="mb4"/>
|
||||
<span class="badge text-bg-primary ms-2"
|
||||
t-out="sponsorData.sponsor_type_name"/>
|
||||
</div>
|
||||
<span class="text-muted" t-if="sponsorData.subtitle" t-out="sponsorData.subtitle"/>
|
||||
<span t-if="sponsorData.url">
|
||||
<i class="fa fa-home me-2"/><a t-att-href="sponsorData.url"><span t-out="sponsorData.url"/></a>
|
||||
</span>
|
||||
<span t-if="sponsorData.email">
|
||||
<i class="fa fa-envelope me-2"/><a t-att-mailto="sponsorData.email"><span t-out="sponsorData.email"/></a>
|
||||
</span>
|
||||
<span t-if="sponsorData.phone">
|
||||
<i class="fa fa-phone me-2"/><span t-out="sponsorData.phone"/>
|
||||
</span>
|
||||
</div>
|
||||
<img t-if="sponsorData.country_flag_url"
|
||||
class="img ms-auto"
|
||||
style="max-height: 36px;"
|
||||
t-att-src="sponsorData.country_flag_url"
|
||||
t-att-alt="sponsorData.country_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12" t-if="sponsorData.website_description" t-out="sponsorData.website_description"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,60 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { Markup } from "@web/legacy/js/core/utils";
|
||||
|
||||
var ExhibitorConnectClosedDialog = Dialog.extend({
|
||||
events: Object.assign({}, Dialog.prototype.events, {
|
||||
'click .o_wesponsor_js_connect_modal_contry': '_onClickCountryFlag',
|
||||
}),
|
||||
template: 'exhibitor.connect.closed.modal',
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} parent;
|
||||
* @param {Object} options holding a sponsorData obj with required values to
|
||||
* display (see .xml for details);
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
options = Object.assign({
|
||||
size: 'medium',
|
||||
renderHeader: false,
|
||||
renderFooter: false,
|
||||
backdrop: true,
|
||||
}, options || {});
|
||||
this.sponsorId = options.sponsorId;
|
||||
this._super(parent, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Wait for fetching sponsor data;
|
||||
*/
|
||||
willStart: function () {
|
||||
return Promise.all([
|
||||
this._super(...arguments),
|
||||
this._fetchSponsor()
|
||||
]);
|
||||
},
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Private
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _fetchSponsor() {
|
||||
const sponsorData = await this._rpc({
|
||||
route: `/event_sponsor/${encodeURIComponent(this.sponsorId)}/read`
|
||||
});
|
||||
sponsorData.website_description = Markup(sponsorData.website_description);
|
||||
this.sponsorData = sponsorData;
|
||||
},
|
||||
});
|
||||
|
||||
import { ExhibitorConnectClosedDialog } from "../components/exhibitor_connect_closed_dialog/exhibitor_connect_closed_dialog";
|
||||
|
||||
publicWidget.registry.eventExhibitorConnect = publicWidget.Widget.extend({
|
||||
selector: '.o_wesponsor_connect_button',
|
||||
@@ -113,17 +61,12 @@ publicWidget.registry.eventExhibitorConnect = publicWidget.Widget.extend({
|
||||
|
||||
_openClosedDialog: function ($element) {
|
||||
const sponsorId = this.$el.data('sponsorId');
|
||||
return new ExhibitorConnectClosedDialog(
|
||||
this, {
|
||||
sponsorId: sponsorId,
|
||||
}
|
||||
).open();
|
||||
this.call("dialog", "add", ExhibitorConnectClosedDialog, { sponsorId });
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
export default {
|
||||
ExhibitorConnectClosedDialog: ExhibitorConnectClosedDialog,
|
||||
eventExhibitorConnect: publicWidget.registry.eventExhibitorConnect,
|
||||
};
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="exhibitor.connect.closed.modal">
|
||||
<div class="o_wesponsor_js_connect_modal_main container">
|
||||
<div class="row mt-2">
|
||||
<t t-if="! widget.sponsorData.event_is_ongoing">
|
||||
<div class="col-12 alert alert-warning text-center" role="alert"
|
||||
t-if="widget.sponsorData.event_is_done">
|
||||
Event <span t-out="widget.sponsorData.event_name" class="fw-bold"/> is over.
|
||||
|
||||
<br/>
|
||||
<span>Join us next time to meet <b t-out="widget.sponsorData.name"/>!</span>
|
||||
</div>
|
||||
<div class="col-12 alert alert-warning text-center" role="alert"
|
||||
t-else="">
|
||||
<span t-out="widget.sponsorData.name" class="fw-bold"/> is not available right now.<br />
|
||||
Event <span t-out="widget.sponsorData.event_name" class="fw-bold"/>
|
||||
<span t-if="widget.sponsorData.event_start_today">
|
||||
starts in
|
||||
<span t-if="widget.sponsorData.event_start_remaining >= 1" t-out="widget.sponsorData.event_start_remaining"
|
||||
t-options="{'widget': 'duration', 'digital': False, 'unit': 'minute', 'round': 'minute'}"/>
|
||||
<t t-else="">
|
||||
a few seconds
|
||||
</t>.
|
||||
</span>
|
||||
<span class="my-0" t-else="">
|
||||
starts on <span t-out="widget.sponsorData.event_date_begin_located"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<div class="col-12 alert alert-warning text-center" role="alert"
|
||||
t-else="">
|
||||
<span t-out="widget.sponsorData.name" class="fw-bold"/> is not available right now.<br />
|
||||
Come back between
|
||||
<strong>
|
||||
<t t-out="widget.sponsorData.hour_from_str"/>
|
||||
-
|
||||
<t t-out="widget.sponsorData.hour_to_str"/>
|
||||
</strong> (<span t-out="widget.sponsorData.event_date_tz"/>)
|
||||
to meet them!
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<img class="img" style="max-width: 100%;"
|
||||
t-att-src="widget.sponsorData.website_image_url"
|
||||
t-att-alt="widget.sponsorData.name"/>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="d-flex align-items-top mb-3">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 t-out="widget.sponsorData.name" class="mb4"/>
|
||||
<span class="badge text-bg-primary ms-2"
|
||||
t-out="widget.sponsorData.sponsor_type_name"/>
|
||||
</div>
|
||||
<span class="text-muted" t-if="widget.sponsorData.subtitle" t-out="widget.sponsorData.subtitle"/>
|
||||
<span t-if="widget.sponsorData.url">
|
||||
<i class="fa fa-home me-2"/><a t-att-href="widget.sponsorData.url"><span t-out="widget.sponsorData.url"/></a>
|
||||
</span>
|
||||
<span t-if="widget.sponsorData.email">
|
||||
<i class="fa fa-envelope me-2"/><a t-att-mailto="widget.sponsorData.email"><span t-out="widget.sponsorData.email"/></a>
|
||||
</span>
|
||||
<span t-if="widget.sponsorData.phone">
|
||||
<i class="fa fa-phone me-2"/><span t-out="widget.sponsorData.phone"/>
|
||||
</span>
|
||||
</div>
|
||||
<img t-if="widget.sponsorData.country_flag_url"
|
||||
class="img ms-auto"
|
||||
style="max-height: 36px;"
|
||||
t-att-src="widget.sponsorData.country_flag_url"
|
||||
t-att-alt="widget.sponsorData.country_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12" t-if="widget.sponsorData.website_description" t-out="widget.sponsorData.website_description"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
|
||||
publicWidget.registry.websiteEventMeetingRoom = publicWidget.Widget.extend({
|
||||
selector: '.o_wevent_meeting_room_card',
|
||||
@@ -31,23 +31,20 @@ publicWidget.registry.websiteEventMeetingRoom = publicWidget.Widget.extend({
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
Dialog.confirm(
|
||||
this,
|
||||
_t("Are you sure you want to close this room?"),
|
||||
{
|
||||
confirm_callback: async () => {
|
||||
await this._rpc({
|
||||
model: 'event.meeting.room',
|
||||
method: 'write',
|
||||
args: [this.meetingRoomId, {is_published: false}],
|
||||
context: this.context,
|
||||
});
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
body: _t("Are you sure you want to close this room?"),
|
||||
confirm: async () => {
|
||||
await this._rpc({
|
||||
model: "event.meeting.room",
|
||||
method: "write",
|
||||
args: [this.meetingRoomId, { is_published: false }],
|
||||
context: this.context,
|
||||
});
|
||||
|
||||
// remove the element so we do not need to refresh the page
|
||||
this.$el.remove();
|
||||
}
|
||||
// remove the element so we do not need to refresh the page
|
||||
this.$el.remove();
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -58,22 +55,19 @@ publicWidget.registry.websiteEventMeetingRoom = publicWidget.Widget.extend({
|
||||
_onDuplicateClick: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
Dialog.confirm(
|
||||
this,
|
||||
_t("Are you sure you want to duplicate this room?"),
|
||||
{
|
||||
confirm_callback: async () => {
|
||||
await this._rpc({
|
||||
model: 'event.meeting.room',
|
||||
method: 'copy',
|
||||
args: [this.meetingRoomId],
|
||||
context: this.context,
|
||||
});
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
body: _t("Are you sure you want to duplicate this room?"),
|
||||
confirm: async () => {
|
||||
await this._rpc({
|
||||
model: "event.meeting.room",
|
||||
method: "copy",
|
||||
args: [this.meetingRoomId],
|
||||
context: this.context,
|
||||
});
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
window.location.reload();
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,7 @@ Ask questions, get answers, no distractions
|
||||
'website_forum/static/src/js/website_forum.js',
|
||||
'website_forum/static/src/js/website_forum.share.js',
|
||||
'website_forum/static/src/xml/public_templates.xml',
|
||||
'website_forum/static/src/components/flag_mark_as_offensive/**/*',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useEffect } from "@odoo/owl";
|
||||
import { useChildRef } from "@web/core/utils/hooks";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
|
||||
export class FlagMarkAsOffensiveDialog extends Component {
|
||||
static template = "website_forum.FlagMarkAsOffensiveDialog";
|
||||
static components = { Dialog };
|
||||
|
||||
setup() {
|
||||
this.modalRef = useChildRef();
|
||||
|
||||
const onClickDiscard = (ev) => {
|
||||
ev.preventDefault();
|
||||
this.props.close();
|
||||
};
|
||||
|
||||
useEffect(
|
||||
(discardButton) => {
|
||||
if (discardButton) {
|
||||
discardButton.addEventListener("click", onClickDiscard);
|
||||
return () => {
|
||||
discardButton.removeEventListener("click", onClickDiscard);
|
||||
};
|
||||
}
|
||||
},
|
||||
() => [this.modalRef.el?.querySelector(".btn-link")]
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website_forum.FlagMarkAsOffensiveDialog" owl="1">
|
||||
<Dialog size="'md'" title="props.title" footer="false" modalRef="modalRef" >
|
||||
<t t-out="props.body"/>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,8 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { markup } from "@odoo/owl";
|
||||
import { FlagMarkAsOffensiveDialog } from "../components/flag_mark_as_offensive/flag_mark_as_offensive";
|
||||
import dom from "@web/legacy/js/core/dom";
|
||||
import {setCookie} from "@web/legacy/js/core/cookie_utils";
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import { loadWysiwygFromTextarea } from "@web_editor/js/frontend/loadWysiwygFromTextarea";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { Markup } from '@web/legacy/js/core/utils';
|
||||
@@ -572,17 +573,9 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
const template = await this._rpc({
|
||||
route: $(ev.currentTarget).data('action'),
|
||||
});
|
||||
const dialog = new Dialog(this, {
|
||||
size: 'medium',
|
||||
this.call("dialog", "add", FlagMarkAsOffensiveDialog, {
|
||||
title: _t("Offensive Post"),
|
||||
$content: template,
|
||||
renderFooter: false,
|
||||
}).open();
|
||||
dialog.opened().then(() => {
|
||||
dialog.$(".btn-link:contains('Discard')").click((ev) => {
|
||||
ev.preventDefault();
|
||||
dialog.close();
|
||||
});
|
||||
body: markup(template),
|
||||
});
|
||||
},
|
||||
_displayAccessDeniedNotification(message) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import Dialog from "@web/legacy/js/core/dialog";
|
||||
import options from "@web_editor/js/editor/snippets.options";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { renderToElement } from "@web/core/utils/render";
|
||||
import options from "@web_editor/js/editor/snippets.options";
|
||||
|
||||
options.registry.mailing_list_subscribe = options.Class.extend({
|
||||
/**
|
||||
@@ -14,17 +14,19 @@ options.registry.mailing_list_subscribe = options.Class.extend({
|
||||
if (this.mailingLists.length) {
|
||||
this.$target.attr("data-list-id", this.mailingLists[0][0]);
|
||||
} else {
|
||||
Dialog.confirm(this, _t("No mailing list found, do you want to create a new one? This will save all your changes, are you sure you want to proceed?"), {
|
||||
confirm_callback: () => {
|
||||
this.trigger_up('request_save', {
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
body: _t("No mailing list found, do you want to create a new one? This will save all your changes, are you sure you want to proceed?"),
|
||||
confirm: () => {
|
||||
this.trigger_up("request_save", {
|
||||
reload: false,
|
||||
onSuccess: () => {
|
||||
window.location.href = '/web#action=mass_mailing.action_view_mass_mailing_lists';
|
||||
window.location.href =
|
||||
"/web#action=mass_mailing.action_view_mass_mailing_lists";
|
||||
},
|
||||
});
|
||||
},
|
||||
cancel_callback: () => {
|
||||
this.trigger_up('remove_snippet', {
|
||||
cancel: () => {
|
||||
this.trigger_up("remove_snippet", {
|
||||
$snippet: this.$target,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="website_slides.slides.course.unsubscribe.modal.leave" t-inherit-mode="extension">
|
||||
<xpath expr="//p[last()]" position="after">
|
||||
<t t-if="widget.enroll === 'payment'">
|
||||
<t t-inherit="website_slides.SlideUnsubscribeDialog" t-inherit-mode="extension">
|
||||
<xpath expr="//t[@t-if="state.mode === 'leave'"] //p[last()]" position="after">
|
||||
<t t-if="props.enroll === 'payment'">
|
||||
<p class="alert alert-warning">
|
||||
<i class="fa fa-exclamation-triangle fa-3x float-start me-3"></i>
|
||||
This course is paid.<br/>
|
||||
|
||||
@@ -109,12 +109,12 @@ Featuring
|
||||
'website_slides/static/src/xml/website_slides_fullscreen.xml',
|
||||
'website_slides/static/src/xml/website_slides_share.xml',
|
||||
'website_slides/static/src/xml/website_slides_channel_tag.xml',
|
||||
'website_slides/static/src/xml/website_slides_unsubscribe.xml',
|
||||
'website_slides/static/src/xml/slide_management.xml',
|
||||
'website_slides/static/src/xml/slide_course_join.xml',
|
||||
'website_slides/static/src/xml/slide_course_prerequisite.xml',
|
||||
'website_slides/static/src/xml/slide_quiz_create.xml',
|
||||
'website_slides/static/src/xml/slide_quiz.xml',
|
||||
'website_slides/static/src/js/public/**/*',
|
||||
],
|
||||
'website.assets_editor': [
|
||||
'website_slides/static/src/js/systray_items/*.js',
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
|
||||
export class CategoryAddDialog extends ConfirmationDialog {
|
||||
static template = "website_slides.CategoryAddDialog";
|
||||
static props = {
|
||||
...ConfirmationDialog.props,
|
||||
channelId: Number,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.inputRef = useAutofocus();
|
||||
this.csrf_token = odoo.csrf_token;
|
||||
this.lastInputValue;
|
||||
}
|
||||
|
||||
_confirm() {
|
||||
this.execButton(() => {
|
||||
if (this.inputRef.el.value === this.lastInputValue) {
|
||||
return;
|
||||
}
|
||||
this.lastInputValue = this.inputRef.el.value;
|
||||
return this.props.confirm({ formEl: this.modalRef.el.querySelector("form") });
|
||||
});
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website_slides.CategoryAddDialog" t-inherit="web.ConfirmationDialog" owl="1">
|
||||
<xpath expr="//p[hasclass('text-prewrap')]" position="replace">
|
||||
<div>
|
||||
<form t-on-submit.prevent="_confirm" action="/slides/category/add" method="POST" id="slide_category_add_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
|
||||
<input type="hidden" name="channel_id" t-att-value="props.channelId"/>
|
||||
<div class="mb-3 row">
|
||||
<label for="section_name" class="col-sm-3 col-form-label">Section name</label>
|
||||
<div class="col-sm-9">
|
||||
<input t-ref="autofocus" type="text" autocomplete="off" class="form-control" name="name" id="section_name" required="required" placeholder='e.g. "Introduction"'/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { CheckBox } from "@web/core/checkbox/checkbox";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class SlideUnsubscribeDialog extends Component {
|
||||
static template = "website_slides.SlideUnsubscribeDialog";
|
||||
static components = { CheckBox, Dialog };
|
||||
static props = {
|
||||
channelId: Number,
|
||||
isFollower: String,
|
||||
visibility: String,
|
||||
enroll: { type: String, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
|
||||
this.state = useState({
|
||||
buttonDisabled: false,
|
||||
});
|
||||
this.channelID = parseInt(this.props.channelId, 10);
|
||||
this.isFollower = this.props.isFollower === "True";
|
||||
this.updateState("subscription");
|
||||
this.isChecked = this.isFollower;
|
||||
}
|
||||
|
||||
updateState(mode) {
|
||||
if (mode === "subscription") {
|
||||
this.state.title = this.isFollower ? _t("Subscribe") : _t("Notifications");
|
||||
this.state.mode = "subscription";
|
||||
} else if (mode === "leave") {
|
||||
this.state.title = _t("Leave the course");
|
||||
this.state.mode = "leave";
|
||||
}
|
||||
}
|
||||
|
||||
onChangeCheckbox(isChecked) {
|
||||
this.isChecked = isChecked;
|
||||
}
|
||||
|
||||
onClickLeaveCourse() {
|
||||
this.updateState("leave");
|
||||
}
|
||||
|
||||
onClickLeaveCourseCancel() {
|
||||
this.updateState("subscription");
|
||||
}
|
||||
|
||||
async onClickLeaveCourseSubmit() {
|
||||
if (this.state.buttonDisabled) {
|
||||
return;
|
||||
}
|
||||
this.state.buttonDisabled = true;
|
||||
|
||||
await this.rpc("/slides/channel/leave", { channel_id: this.channelID });
|
||||
if (this.props.visibility === "public" || this.props.visibility === "connected") {
|
||||
window.location.reload();
|
||||
} else {
|
||||
window.location.href = "/slides";
|
||||
}
|
||||
}
|
||||
|
||||
async onClickSubscriptionSubmit() {
|
||||
if (this.state.buttonDisabled) {
|
||||
return;
|
||||
}
|
||||
this.state.buttonDisabled = true;
|
||||
|
||||
if (this.isFollower === this.isChecked) {
|
||||
this.props.close();
|
||||
} else {
|
||||
await this.rpc(`/slides/channel/${this.isChecked ? "subscribe" : "unsubscribe"}`, {
|
||||
channel_id: this.channelID,
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website_slides.SlideUnsubscribeDialog" owl="1">
|
||||
<Dialog size="'md'" title="state.title">
|
||||
<div>
|
||||
<t t-if="state.mode === 'subscription'">
|
||||
<form class="clearfix">
|
||||
<div class="controls mt8">
|
||||
<CheckBox value="isChecked" name="subscribed" onChange="(isChecked) => this.onChangeCheckbox(isChecked)">
|
||||
Be notified when a new content is added.
|
||||
</CheckBox>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
<t t-if="state.mode === 'leave'">
|
||||
<p>Do you really want to leave the course?</p>
|
||||
<p>All progress will be lost until you rejoin this course.</p>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<t t-set-slot="footer" owl="1">
|
||||
<t t-if="state.mode === 'subscription'">
|
||||
<button class="btn btn-primary" t-att-disabled="state.buttonDisabled" t-on-click="() => this.onClickSubscriptionSubmit()">Save</button>
|
||||
<button class="btn" t-att-disabled="state.buttonDisabled" t-on-click="() => this.props.close()">Discard</button>
|
||||
<button class="btn btn-danger ms-auto" t-att-disabled="state.buttonDisabled" t-on-click="() => this.onClickLeaveCourse()">or Leave the course</button>
|
||||
</t>
|
||||
<t t-if="state.mode === 'leave'">
|
||||
<button class="btn btn-danger" t-att-disabled="state.buttonDisabled" t-on-click="() => this.onClickLeaveCourseSubmit()">Leave the course</button>
|
||||
<button class="btn" t-att-disabled="state.buttonDisabled" t-on-click="() => this.onClickLeaveCourseCancel()">Discard</button>
|
||||
</t>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,68 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import Dialog from '@web/legacy/js/core/dialog';
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
var CategoryAddDialog = Dialog.extend({
|
||||
template: 'slides.category.add',
|
||||
events: Object.assign(
|
||||
{ "submit form#slide_category_add_form": "_onClickFormSubmit" },
|
||||
Dialog.events
|
||||
),
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
options = Object.assign({
|
||||
title: _t('Add a section'),
|
||||
size: 'medium',
|
||||
buttons: [{
|
||||
text: _t('Save'),
|
||||
classes: 'btn-primary o_js_website_slides_add_category_btn',
|
||||
click: this._onClickFormSubmit.bind(this),
|
||||
}, {
|
||||
text: _t('Back'),
|
||||
close: true
|
||||
}]
|
||||
}, options || {});
|
||||
|
||||
this.csrf_token = odoo.csrf_token;
|
||||
this.channelId = options.channelId;
|
||||
this._super(parent, options);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_formValidate: function ($form) {
|
||||
$form.addClass('was-validated');
|
||||
return $form[0].checkValidity();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle form submission on click button or press ENTER key. Prevent multiple submissions
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onClickFormSubmit: function (ev) {
|
||||
ev.preventDefault();
|
||||
const form = document.getElementById("slide_category_add_form");
|
||||
if (!form.classList.contains("o_js_website_slides_form_submitted")) {
|
||||
if (this._formValidate($(form))) {
|
||||
form.classList.add("o_js_website_slides_form_submitted");
|
||||
document.querySelector("button.o_js_website_slides_add_category_btn").disabled = true;
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import { CategoryAddDialog } from "@website_slides/js/public/components/category_add_dialog/category_add_dialog";
|
||||
|
||||
publicWidget.registry.websiteSlidesCategoryAdd = publicWidget.Widget.extend({
|
||||
selector: '.o_wslides_js_slide_section_add',
|
||||
@@ -75,7 +15,21 @@ publicWidget.registry.websiteSlidesCategoryAdd = publicWidget.Widget.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_openDialog: function (channelId) {
|
||||
new CategoryAddDialog(this, {channelId: channelId}).open();
|
||||
this.call("dialog", "add", CategoryAddDialog, {
|
||||
title: _t("Add a section"),
|
||||
confirmLabel: _t("Save"),
|
||||
confirm: ({ formEl }) => {
|
||||
if (!formEl.checkValidity()) {
|
||||
return false;
|
||||
}
|
||||
formEl.classList.add("was-validated");
|
||||
formEl.submit();
|
||||
return true;
|
||||
},
|
||||
cancelLabel: _t("Back"),
|
||||
cancel: () => {},
|
||||
channelId,
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -93,6 +47,5 @@ publicWidget.registry.websiteSlidesCategoryAdd = publicWidget.Widget.extend({
|
||||
});
|
||||
|
||||
export default {
|
||||
categoryAddDialog: CategoryAddDialog,
|
||||
websiteSlidesCategoryAdd: publicWidget.registry.websiteSlidesCategoryAdd
|
||||
};
|
||||
|
||||
@@ -1,58 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import Dialog from '@web/legacy/js/core/dialog';
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
const categoryDeleteDialog = Dialog.extend({
|
||||
template: 'slides.category.delete',
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Widget} parent
|
||||
* @param {DOM Object} slideTarget
|
||||
*/
|
||||
init(parent, slideTarget) {
|
||||
this.categoryId = parseInt(slideTarget.dataset.categoryId);
|
||||
this._super(parent, {
|
||||
title: _t('Delete Category'),
|
||||
size: 'medium',
|
||||
buttons: [{
|
||||
text: _t('Delete'),
|
||||
classes: 'btn-primary',
|
||||
click: () => this._onClickDelete(),
|
||||
}, {
|
||||
text: _t('Cancel'),
|
||||
close: true
|
||||
}]
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Calls 'unlink' method on slides.slide to delete the category and
|
||||
* reloads page after deletion to re-arrange the content on UI
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onClickDelete() {
|
||||
this._rpc({
|
||||
model: 'slide.slide',
|
||||
method: 'unlink',
|
||||
args: [this.categoryId],
|
||||
}).then(function () {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
publicWidget.registry.websiteSlidesCategoryDelete = publicWidget.Widget.extend({
|
||||
selector: '.o_wslides_js_category_delete',
|
||||
selector: ".o_wslides_js_category_delete",
|
||||
events: {
|
||||
'click': '_onClickDeleteCateogry',
|
||||
click: "_onClickDeleteCateogry",
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -64,11 +19,28 @@ publicWidget.registry.websiteSlidesCategoryDelete = publicWidget.Widget.extend({
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickDeleteCateogry(ev) {
|
||||
new categoryDeleteDialog(this, ev.currentTarget).open();
|
||||
const categoryId = parseInt(ev.currentTarget.dataset.categoryId);
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
title: _t("Delete Category"),
|
||||
body: _t("Are you sure you want to delete this category?"),
|
||||
confirmLabel: _t("Delete"),
|
||||
confirm: async () => {
|
||||
/**
|
||||
* Calls 'unlink' method on slides.slide to delete the category and
|
||||
* reloads page after deletion to re-arrange the content on UI
|
||||
*/
|
||||
await this._rpc({
|
||||
model: "slide.slide",
|
||||
method: "unlink",
|
||||
args: [categoryId],
|
||||
});
|
||||
window.location.reload();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
categoryDeleteDialog: categoryDeleteDialog,
|
||||
websiteSlidesCategoryDelete: publicWidget.registry.websiteSlidesCategoryDelete
|
||||
websiteSlidesCategoryDelete: publicWidget.registry.websiteSlidesCategoryDelete,
|
||||
};
|
||||
|
||||
@@ -1,135 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import Dialog from '@web/legacy/js/core/dialog';
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { renderToElement } from "@web/core/utils/render";
|
||||
|
||||
var SlideUnsubscribeDialog = Dialog.extend({
|
||||
template: 'slides.course.unsubscribe.modal',
|
||||
_texts: {
|
||||
titleSubscribe: _t("Subscribe"),
|
||||
titleUnsubscribe: _t("Notifications"),
|
||||
titleLeaveCourse: _t("Leave the course")
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} parent
|
||||
* @param {Object} options
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
options = Object.assign({
|
||||
title: options.isFollower === 'True' ? this._texts.titleSubscribe : this._texts.titleUnsubscribe,
|
||||
size: 'medium',
|
||||
}, options || {});
|
||||
this._super(parent, options);
|
||||
|
||||
this.set('state', '_subscription');
|
||||
this.on('change:state', this, this._onChangeType);
|
||||
|
||||
this.channelID = parseInt(options.channelId, 10);
|
||||
this.isFollower = options.isFollower === 'True';
|
||||
this.enroll = options.enroll;
|
||||
this.visibility = options.visibility;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.$('input#subscribed').prop('checked', self.isFollower);
|
||||
self._resetModal();
|
||||
});
|
||||
},
|
||||
|
||||
getSubscriptionState: function () {
|
||||
return this.$('input#subscribed').prop('checked');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
_getModalButtons: function () {
|
||||
var btnList = [];
|
||||
var state = this.get('state');
|
||||
if (state === '_subscription') {
|
||||
btnList.push({text: _t("Save"), classes: "btn-primary", click: this._onClickSubscriptionSubmit.bind(this)});
|
||||
btnList.push({text: _t("Discard"), close: true});
|
||||
btnList.push({text: _t("or Leave the course"), classes: "btn-danger ms-auto", click: this._onClickLeaveCourse.bind(this)});
|
||||
} else if (state === '_leave') {
|
||||
btnList.push({text: _t("Leave the course"), classes: "btn-danger", click: this._onClickLeaveCourseSubmit.bind(this)});
|
||||
btnList.push({text: _t("Discard"), click: this._onClickLeaveCourseCancel.bind(this)});
|
||||
}
|
||||
return btnList;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_resetModal: function () {
|
||||
var state = this.get('state');
|
||||
if (state === '_subscription') {
|
||||
this.set_title(this.isFollower ? this._texts.titleUnsubscribe : this._texts.titleSubscribe);
|
||||
this.$('input#subscribed').prop('checked', this.isFollower);
|
||||
}
|
||||
else if (state === '_leave') {
|
||||
this.set_title(this._texts.titleLeaveCourse);
|
||||
}
|
||||
this.set_buttons(this._getModalButtons());
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
_onClickLeaveCourse: function () {
|
||||
this.set('state', '_leave');
|
||||
},
|
||||
|
||||
_onClickLeaveCourseCancel: function () {
|
||||
this.set('state', '_subscription');
|
||||
},
|
||||
|
||||
_onClickLeaveCourseSubmit: async function () {
|
||||
await this._rpc({
|
||||
route: '/slides/channel/leave',
|
||||
params: {channel_id: this.channelID},
|
||||
});
|
||||
if (this.visibility === 'public' || this.visibility === 'connected') {
|
||||
window.location.reload();
|
||||
} else {
|
||||
window.location.href = '/slides';
|
||||
}
|
||||
},
|
||||
|
||||
_onClickSubscriptionSubmit: function () {
|
||||
if (this.isFollower === this.getSubscriptionState()) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
this._rpc({
|
||||
route: this.getSubscriptionState() ? '/slides/channel/subscribe' : '/slides/channel/unsubscribe',
|
||||
params: {channel_id: this.channelID},
|
||||
}).then(function () {
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
_onChangeType: function () {
|
||||
var currentType = this.get('state');
|
||||
var tmpl;
|
||||
if (currentType === '_subscription') {
|
||||
tmpl = 'slides.course.unsubscribe.modal.subscription';
|
||||
} else if (currentType === '_leave') {
|
||||
tmpl = 'slides.course.unsubscribe.modal.leave';
|
||||
}
|
||||
this.$('.o_w_slide_unsubscribe_modal_container').empty();
|
||||
this.$('.o_w_slide_unsubscribe_modal_container').append(renderToElement(tmpl, {widget: this}));
|
||||
|
||||
this._resetModal();
|
||||
},
|
||||
});
|
||||
import { SlideUnsubscribeDialog } from "./public/components/slide_unsubscribe_dialog/slide_unsubscribe_dialog";
|
||||
|
||||
publicWidget.registry.websiteSlidesUnsubscribe = publicWidget.Widget.extend({
|
||||
selector: '.o_wslides_js_channel_unsubscribe',
|
||||
@@ -142,7 +14,7 @@ publicWidget.registry.websiteSlidesUnsubscribe = publicWidget.Widget.extend({
|
||||
|
||||
_openDialog: function ($element) {
|
||||
var data = $element.data();
|
||||
return new SlideUnsubscribeDialog(this, data).open();
|
||||
this.call("dialog", "add", SlideUnsubscribeDialog, data);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -160,6 +32,5 @@ publicWidget.registry.websiteSlidesUnsubscribe = publicWidget.Widget.extend({
|
||||
});
|
||||
|
||||
export default {
|
||||
SlideUnsubscribeDialog: SlideUnsubscribeDialog,
|
||||
websiteSlidesUnsubscribe: publicWidget.registry.websiteSlidesUnsubscribe
|
||||
};
|
||||
|
||||
@@ -1,77 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from '@web/legacy/js/public/public_widget';
|
||||
import Dialog from '@web/legacy/js/core/dialog';
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
var SlideArchiveDialog = Dialog.extend({
|
||||
template: 'slides.slide.archive',
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
options = Object.assign({
|
||||
title: _t('Archive Content'),
|
||||
size: 'medium',
|
||||
buttons: [{
|
||||
text: _t('Archive'),
|
||||
classes: 'btn-primary',
|
||||
click: this._onClickArchive.bind(this)
|
||||
}, {
|
||||
text: _t('Cancel'),
|
||||
close: true
|
||||
}]
|
||||
}, options || {});
|
||||
|
||||
this.$slideTarget = options.slideTarget;
|
||||
this.slideId = this.$slideTarget.data('slideId');
|
||||
this._super(parent, options);
|
||||
},
|
||||
_checkForEmptySections: function (){
|
||||
$('.o_wslides_slide_list_category').each(function (){
|
||||
var $categoryHeader = $(this).find('.o_wslides_slide_list_category_header');
|
||||
var categorySlideCount = $(this).find('.o_wslides_slides_list_slide:not(.o_not_editable)').length;
|
||||
var $emptyFlagContainer = $categoryHeader.find('.o_wslides_slides_list_drag').first();
|
||||
var $emptyFlag = $emptyFlagContainer.find('small');
|
||||
if (categorySlideCount === 0 && $emptyFlag.length === 0){
|
||||
$emptyFlagContainer.append($('<small>', {
|
||||
'class': "ms-1 text-muted fw-bold",
|
||||
text: _t("(empty)")
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Calls 'archive' on slide controller and then visually removes the slide dom element
|
||||
*/
|
||||
_onClickArchive: function () {
|
||||
var self = this;
|
||||
|
||||
this._rpc({
|
||||
route: '/slides/slide/archive',
|
||||
params: {
|
||||
slide_id: this.slideId
|
||||
},
|
||||
}).then(function (isArchived) {
|
||||
if (isArchived){
|
||||
self.$slideTarget.closest('.o_wslides_slides_list_slide').remove();
|
||||
self._checkForEmptySections();
|
||||
}
|
||||
self.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||
|
||||
publicWidget.registry.websiteSlidesSlideArchive = publicWidget.Widget.extend({
|
||||
selector: '.o_wslides_js_slide_archive',
|
||||
selector: ".o_wslides_js_slide_archive",
|
||||
events: {
|
||||
'click': '_onArchiveSlideClick',
|
||||
click: "_onArchiveSlideClick",
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -79,7 +15,45 @@ publicWidget.registry.websiteSlidesSlideArchive = publicWidget.Widget.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_openDialog: function ($slideTarget) {
|
||||
new SlideArchiveDialog(this, {slideTarget: $slideTarget}).open();
|
||||
const slideId = $slideTarget.data("slideId");
|
||||
this.call("dialog", "add", ConfirmationDialog, {
|
||||
title: _t("Archive Content"),
|
||||
body: _t("Are you sure you want to archive this content?"),
|
||||
confirmLabel: _t("Archive"),
|
||||
confirm: async () => {
|
||||
/**
|
||||
* Calls 'archive' on slide controller and then visually removes the slide dom element
|
||||
*/
|
||||
const isArchived = await this._rpc({
|
||||
route: "/slides/slide/archive",
|
||||
params: {
|
||||
slide_id: slideId,
|
||||
},
|
||||
});
|
||||
if (isArchived) {
|
||||
$slideTarget.closest(".o_wslides_slides_list_slide").remove();
|
||||
$(".o_wslides_slide_list_category").each(function () {
|
||||
var $categoryHeader = $(this).find(".o_wslides_slide_list_category_header");
|
||||
var categorySlideCount = $(this).find(
|
||||
".o_wslides_slides_list_slide:not(.o_not_editable)"
|
||||
).length;
|
||||
var $emptyFlagContainer = $categoryHeader
|
||||
.find(".o_wslides_slides_list_drag")
|
||||
.first();
|
||||
var $emptyFlag = $emptyFlagContainer.find("small");
|
||||
if (categorySlideCount === 0 && $emptyFlag.length === 0) {
|
||||
$emptyFlagContainer.append(
|
||||
$("<small>", {
|
||||
class: "ms-1 text-muted fw-bold",
|
||||
text: _t("(empty)"),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -98,6 +72,5 @@ publicWidget.registry.websiteSlidesSlideArchive = publicWidget.Widget.extend({
|
||||
});
|
||||
|
||||
export default {
|
||||
slideArchiveDialog: SlideArchiveDialog,
|
||||
websiteSlidesSlideArchive: publicWidget.registry.websiteSlidesSlideArchive
|
||||
websiteSlidesSlideArchive: publicWidget.registry.websiteSlidesSlideArchive,
|
||||
};
|
||||
|
||||
@@ -13,19 +13,4 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="slides.category.add">
|
||||
<div>
|
||||
<form action="/slides/category/add" method="POST" id="slide_category_add_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="widget.csrf_token"/>
|
||||
<input type="hidden" name="channel_id" t-att-value="widget.channelId"/>
|
||||
<div class="mb-3 row">
|
||||
<label for="section_name" class="col-sm-3 col-form-label">Section name</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" autocomplete="off" class="form-control" name="name" id="section_name" required="required" placeholder='e.g. "Introduction"'/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="slides.course.unsubscribe.modal">
|
||||
<div>
|
||||
<div class="o_w_slide_unsubscribe_modal_container">
|
||||
<t t-call="slides.course.unsubscribe.modal.subscription"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="slides.course.unsubscribe.modal.subscription">
|
||||
<form class="clearfix">
|
||||
<div class="controls mt8">
|
||||
<input id="subscribed" name="subscribed" type="checkbox"/>
|
||||
<label for="subscribed" class="col-form-label fw-normal">Be notified when a new content is added.</label>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
<t t-name="slides.course.unsubscribe.modal.leave">
|
||||
<p>Do you really want to leave the course?</p>
|
||||
<p>All progress will be lost until you rejoin this course.</p>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user