[IMP] website, *: add new content systray item

*: website_blog, website_event, website_forum, website_hr_recruitment,
   website_livechat, website_sale, website_slides

The NewContentModal component is added. It displays tiles, which will
install a module if not already installed, or perform an action defined
by the module otherwise. So that modules can patch that component to
define an action when installed (for example, website_sale will handle
the logic of creating a new product), the elements to displayed and
their state (NOT_INSTALLED, INSTALLING, INSTALLED) are listed in the
state of the component.

A key 'isDisplayed' is added on the new content elements that should not
be displayed to the user, depended on his security groups.
By default, the new content elements are displayed to the system user.

See merge commit for more information.

task-2687506
This commit is contained in:
Younn Olivier
2022-06-24 10:28:05 +02:00
committed by qsm-odoo
parent f9d2b7a01f
commit ca2e143d54
23 changed files with 446 additions and 1100 deletions
-1
View File
@@ -235,7 +235,6 @@
'website/static/src/js/menu/debug_menu.js',
'website/static/src/components/wysiwyg_adapter/wysiwyg_adapter.js',
'website/static/src/systray_items/mobile_preview.js',
'website/static/src/systray_items/new_content.js',
'website/static/src/components/dialog/seo.js',
'website/static/src/systray_items/translate_website.js',
'website/static/src/js/set_view_track.js',
+10
View File
@@ -303,6 +303,16 @@ class Website(Home):
]
}
@http.route('/website/get_modules_info', type='json', auth="user")
def get_modules_info(self, xml_ids):
def get_module_info(xml_id):
module_info = request.env.ref(xml_id)
return {'id': module_info.id, 'name': module_info.shortdesc}
modules_info = {}
for xml_id in xml_ids:
modules_info[xml_id] = get_module_info(xml_id)
return modules_info
@http.route('/website/snippet/filters', type='json', auth='public', website=True)
def get_dynamic_filter(self, filter_id, template_key, limit=None, search_domain=None, with_sample=False):
dynamic_filter = request.env['website.snippet.filter'].sudo().search(
@@ -22,6 +22,7 @@ export class WebsitePreview extends Component {
useEffect(() => {
this.websiteService.currentWebsiteId = this.websiteId;
this.websiteService.context.showNewContentModal = this.props.action.context.params && this.props.action.context.params.display_new_content;
return () => this.websiteService.currentWebsiteId = null;
}, () => [this.props.action.context.params]);
}
@@ -83,6 +84,10 @@ export class WebsitePreview extends Component {
this.currentUrl = this.iframe.el.contentDocument.location.href;
history.replaceState({}, this.props.action.display_name, this.currentUrl);
this.websiteService.currentMetadata = {
path: this.currentUrl,
};
// Before leaving the iframe, its content is replicated on an
// underlying iframe, to avoid for white flashes (visible on
// Chrome Windows/Linux).
@@ -3,7 +3,6 @@
import { NavBar } from '@web/webclient/navbar/navbar';
import { useService, useBus } from '@web/core/utils/hooks';
import { registry } from "@web/core/registry";
import { patch } from 'web.utils';
const websiteSystrayRegistry = registry.category('website_systray');
@@ -471,83 +471,6 @@ body .modal {
}
}
// ADD NEW PAGE MODAL
.o_new_content_open {
// Kill the scroll on the body
overflow: hidden;
}
#o_new_content_menu_choices {
@include o-w-preserve-base;
@include o-position-absolute($o-navbar-height, 0, 0, 0);
position: fixed;
display: flex;
overflow: auto;
background-color: rgba(0, 0, 0, 0.8);
font-family: $o-we-font-family;
&::before {
content: " ";
@include o-position-absolute(0, 0, 0, 0);
z-index: -1;
pointer-events: none;
}
.container {
max-width: 720px;
margin: auto;
}
.o_new_content_element {
opacity: 0;
animation: fadeInDownSmall 1s forwards;
.o_uninstalled_module {
filter: brightness(70%) contrast(60%);
}
.module_icon {
display: block;
margin: auto;
}
a {
display: block;
font-size: 34px;
text-align: center;
text-decoration: none;
i {
width: 110px;
height: 110px;
border: 3px solid lighten(#2C2C36, 10%);
border-radius: 100%;
line-height: 104px;
background-color: #2C2C36;
color: white;
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1) 0s;
}
p {
color: white;
margin-top: 0.7em;
font-size: 0.5em;
}
&:hover, &:focus {
text-decoration: none;
outline: none; // remove ugly dotted border on Firefox
i {
border-color: #1cc1a9;
box-shadow: 0 0 10px rgba(28, 193, 169, 0.46);
}
}
}
}
}
// LOGIN FORM
.oe_login_form, .oe_signup_form, .oe_reset_password_form {
max-width: 300px;
@@ -2,6 +2,8 @@
import { registry } from '@web/core/registry';
const { reactive } = owl;
const websiteSystrayRegistry = registry.category('website_systray');
const unslugHtmlDataObject = (repr) => {
@@ -20,6 +22,10 @@ export const websiteService = {
async start(env, { orm, action }) {
let websites = [];
let currentWebsiteId;
let currentMetadata = {};
const context = reactive({
showNewContentModal: false,
});
const setCurrentWebsiteId = id => {
currentWebsiteId = id;
@@ -29,12 +35,22 @@ export const websiteService = {
set currentWebsiteId(id) {
setCurrentWebsiteId(id);
},
set currentMetadata(metadata) {
currentMetadata = metadata;
},
get currentWebsite() {
return websites.find(website => website.id === currentWebsiteId);
const currentWebsite = websites.find(w => w.id === currentWebsiteId);
if (currentWebsite) {
currentWebsite.metadata = currentMetadata;
}
return currentWebsite;
},
get websites() {
return websites;
},
get context() {
return context;
},
goToWebsite({ websiteId = currentWebsiteId || websites[0].id, path = '/' }) {
action.doAction('website.website_preview', {
clearBreadcrumbs: true,
@@ -1,354 +1,204 @@
odoo.define('website.newMenu', function (require) {
'use strict';
/** @odoo-module **/
var core = require('web.core');
var Dialog = require('web.Dialog');
var websiteNavbarData = require('website.navbar');
var wUtils = require('website.utils');
var tour = require('web_tour.tour');
import { registry } from '@web/core/registry';
import { useService } from '@web/core/utils/hooks';
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
const { registry } = require("@web/core/registry");
const { Component, xml, useState, onWillStart } = owl;
const {qweb, _t} = core;
export const MODULE_STATUS = {
NOT_INSTALLED: 'NOT_INSTALLED',
INSTALLING: 'INSTALLING',
FAILED_TO_INSTALL: 'FAILED_TO_INSTALL',
INSTALLED: 'INSTALLED',
};
var enableFlag = 'enable_new_content';
class NewContentElement extends Component {
setup() {
this.MODULE_STATUS = MODULE_STATUS;
}
var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
close_all_widgets: '_handleCloseDemand',
new_page: '_createNewPage',
}),
events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, {
'click': '_onBackgroundClick',
'click [data-module-id]': '_onModuleIdClick',
'keydown': '_onBackgroundKeydown',
}),
// allow text to be customized with inheritance
newContentText: {
failed: _t('Failed to install "%s"'),
installInProgress: _t("The installation of an App is already in progress."),
installNeeded: _t('Do you want to install the "%s" App?'),
installPleaseWait: _t('Installing "%s"'),
},
/**
* Prepare the navigation and find the modules to install.
* Move not installed module buttons after installed modules buttons,
* but keep the original index to be able to move back the pending install
* button at its final position, so the user can click at the same place.
*
* @override
*/
start: function () {
this.pendingInstall = false;
this.$newContentMenuChoices = this.$('#o_new_content_menu_choices');
var $modules = this.$newContentMenuChoices.find('.o_new_content_element');
_.each($modules, function (el, index) {
var $el = $(el);
$el.data('original-index', index);
if ($el.data('module-id')) {
$el.appendTo($el.parent());
$el.find('a i, a p').addClass('o_uninstalled_module');
}
});
this.$firstLink = this.$newContentMenuChoices.find('a:eq(0)');
this.$lastLink = this.$newContentMenuChoices.find('a:last');
if ($.deparam.querystring()[enableFlag] !== undefined) {
Object.keys(tour.tours).forEach(
el => {
let element = tour.tours[el];
if (element.steps[0].trigger == '#new-content-menu > a'
&& !element.steps[0].extra_trigger) {
element.steps[0].auto = true;
}
}
);
this._showMenu();
}
this.$loader = $(qweb.render('website.new_content_loader'));
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Asks the user information about a new page to create, then creates it and
* redirects the user to this new page.
*
* @private
* @returns {Promise} Unresolved if there is a redirection
*/
_createNewPage: function () {
return wUtils.prompt({
id: 'editor_new_page',
window_title: _t("New Page"),
input: _t("Page Title"),
init: function () {
var $group = this.$dialog.find('div.form-group');
$group.removeClass('mb0');
var $add = $('<div/>', {'class': 'form-group mb0 row'})
.append($('<span/>', {'class': 'offset-md-3 col-md-9 text-left'})
.append(qweb.render('website.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")})));
$add.find('input').prop('checked', true);
$group.after($add);
}
}).then(function (result) {
var val = result.val;
var $dialog = result.dialog;
if (!val) {
return;
}
var url = '/website/add/' + encodeURIComponent(val);
wUtils.sendRequest(url, {
add_menu: $dialog.find('input[type="checkbox"]').is(':checked') || '',
});
return new Promise(function () {});
});
},
/**
* @private
*/
_handleCloseDemand: function () {
this._hideMenu();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Set the focus on the first link
*
* @private
*/
_focusFirstLink: function () {
this.$firstLink.focus();
},
/**
* Set the focus on the last link
*
* @private
*/
_focusLastLink: function () {
this.$lastLink.focus();
},
/**
* Hide the menu
*
* @private
*/
_hideMenu: function () {
this.shown = false;
this.$newContentMenuChoices.addClass('o_hidden');
$('body').removeClass('o_new_content_open');
},
/**
* Install a module
*
* @private
* @param {number} moduleId: the module to install
* @return {Promise}
*/
_install: function (moduleId) {
this.pendingInstall = true;
$('body').css('pointer-events', 'none');
return this._rpc({
model: 'ir.module.module',
method: 'button_immediate_install',
args: [[moduleId]],
}).guardedCatch(function () {
$('body').css('pointer-events', '');
});
},
/**
* Show the menu
*
* @private
* @returns {Promise}
*/
_showMenu: function () {
var self = this;
return new Promise(function (resolve, reject) {
self.trigger_up('action_demand', {
actionName: 'close_all_widgets',
onSuccess: resolve,
onFailure: reject,
});
}).then(function () {
self.firstTab = true;
self.shown = true;
self.$newContentMenuChoices.removeClass('o_hidden');
$('body').addClass('o_new_content_open');
self.$('> a').focus();
});
},
/**
* Called to add loader element in DOM.
*
* @param {string} moduleName
* @private
*/
_addLoader(moduleName) {
const newContentLoaderText = _.str.sprintf(_t("Building your %s"), moduleName);
this.$loader.find('#new_content_loader_text').replaceWith(newContentLoaderText);
$('body').append(this.$loader);
},
/**
* @private
*/
_removeLoader() {
this.$loader.remove();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the menu's toggle button is clicked:
* -> Opens the menu and reset the tab navigation (if closed)
* -> Close the menu (if open)
* Called when a click outside the menu's options occurs -> Close the menu
*
* @private
* @param {Event} ev
*/
_onBackgroundClick: function (ev) {
if (this.$newContentMenuChoices.hasClass('o_hidden')) {
this._showMenu();
} else {
this._hideMenu();
}
},
/**
* Called when a keydown occurs:
* ESC -> Closes the modal
* TAB -> Navigation (captured in the modal)
*
* @private
* @param {Event} ev
*/
_onBackgroundKeydown: function (ev) {
if (!this.shown) {
return;
}
switch (ev.which) {
case $.ui.keyCode.ESCAPE:
this._hideMenu();
ev.stopPropagation();
break;
case $.ui.keyCode.TAB:
if (ev.shiftKey) {
if (this.firstTab || document.activeElement === this.$firstLink[0]) {
this._focusLastLink();
ev.preventDefault();
}
} else {
if (this.firstTab || document.activeElement === this.$lastLink[0]) {
this._focusFirstLink();
ev.preventDefault();
}
}
this.firstTab = false;
break;
}
},
/**
* Open the install dialog related to an element:
* - open the dialog depending on access right and another pending install
* - if ok to install, prepare the install action:
* - call the proper action on click
* - change the button text and style
* - handle the result (reload on the same page or error)
*
* @private
* @param {Event} ev
*/
_onModuleIdClick: function (ev) {
var self = this;
var $el = $(ev.currentTarget);
var $i = $el.find('a i');
var $p = $el.find('a p');
var title = $p.text();
var content = '';
var buttons;
var moduleId = $el.data('module-id');
var name = $el.data('module-shortdesc');
ev.stopPropagation();
onClick(ev) {
ev.preventDefault();
ev.stopPropagation();
this.props.onClick();
}
}
NewContentElement.template = "website.NewContentElement";
NewContentElement.props = {
name: { type: String, optional: true },
title: String,
onClick: Function,
status: { type: String, optional: true },
slots: Object,
};
NewContentElement.defaultProps = {
status: MODULE_STATUS.INSTALLED,
};
if (this.pendingInstall) {
content = this.newContentText.installInProgress;
export class NewContentModal extends Component {
setup() {
this.user = useService('user');
this.orm = useService('orm');
this.rpc = useService('rpc');
this.dialogs = useService('dialog');
this.website = useService('website');
this.isSystem = this.user.isSystem;
this.newContentText = {
failed: this.env._t('Failed to install "%s"'),
installInProgress: this.env._t("The installation of an App is already in progress."),
installNeeded: this.env._t('Do you want to install the "%s" App?'),
installPleaseWait: this.env._t('Installing "%s"'),
};
this.state = useState({
newContentElements: [
{
moduleXmlId: 'base.module_website_blog',
status: MODULE_STATUS.NOT_INSTALLED,
icon: xml`<i class="fa fa-rss"/>`,
title: this.env._t('Blog Post'),
},
{
moduleXmlId: 'base.module_website_event',
status: MODULE_STATUS.NOT_INSTALLED,
icon: xml`<i class="fa fa-ticket"/>`,
title: this.env._t('Event'),
},
{
moduleXmlId: 'base.module_website_forum',
status: MODULE_STATUS.NOT_INSTALLED,
icon: xml`<i class="fa fa-comment"/>`,
redirectUrl: '/forum',
title: this.env._t('Forum'),
},
{
moduleXmlId: 'base.module_website_hr_recruitment',
status: MODULE_STATUS.NOT_INSTALLED,
icon: xml`<i class="fa fa-briefcase"/>`,
title: this.env._t('Job Offer'),
},
{
moduleXmlId: 'base.module_website_sale',
status: MODULE_STATUS.NOT_INSTALLED,
icon: xml`<i class="fa fa-shopping-cart"/>`,
title: this.env._t('Product'),
},
{
moduleXmlId: 'base.module_website_slides',
status: MODULE_STATUS.NOT_INSTALLED,
icon: xml`<i class="fa module_icon" style="background-image: url('/website/static/src/img/apps_thumbs/website_slide.svg');background-repeat: no-repeat; background-position: center;"/>`,
title: this.env._t('Course'),
},
{
moduleXmlId: 'base.module_website_livechat',
status: MODULE_STATUS.NOT_INSTALLED,
icon: xml`<i class="fa fa-comments"/>`,
title: this.env._t('Livechat Widget'),
redirectUrl: '/livechat'
},
]
});
this.websiteContext = useState(this.website.context);
onWillStart(async () => {
this.isDesigner = await this.user.hasGroup('website.group_website_designer');
const xmlIds = this.state.newContentElements.filter(({status}) => status === MODULE_STATUS.NOT_INSTALLED).map(({moduleXmlId}) => moduleXmlId);
this.modulesInfo = await this.rpc('/website/get_modules_info', {xml_ids: xmlIds});
});
useHotkey('escape', () => {
if (this.websiteContext.showNewContentModal) {
this.websiteContext.showNewContentModal = false;
}
});
}
get sortedNewContentElements() {
return this.state.newContentElements.filter(({status}) => status !== MODULE_STATUS.NOT_INSTALLED).concat(this.state.newContentElements.filter(({status}) => status === MODULE_STATUS.NOT_INSTALLED));
}
createNewPage() {}
async installModule(id, redirectUrl) {
await this.orm.call(
'ir.module.module',
'button_immediate_install',
[id],
);
if (redirectUrl) {
window.location.replace(redirectUrl);
} else {
content = _.str.sprintf(this.newContentText.installNeeded, name);
buttons = [{
text: _t("Install"),
classes: 'btn-primary',
close: true,
click: function () {
// move the element where it will be after installation
var $finalPosition = self.$newContentMenuChoices
.find('.o_new_content_element:not([data-module-id])')
.filter(function () {
return $(this).data('original-index') < $el.data('original-index');
}).last();
if ($finalPosition) {
$el.fadeTo(400, 0, function () {
// if once installed, button disapeear, don't need to move it.
if (!$el.hasClass('o_new_content_element_once')) {
$el.insertAfter($finalPosition);
}
// change style to use spinner
$i.removeClass()
.addClass('fa fa-spin fa-circle-o-notch fa-spin')
.css('background-image', 'none');
$p.removeClass('o_uninstalled_module')
.text(_.str.sprintf(self.newContentText.installPleaseWait, name));
$el.fadeTo(1000, 1);
self._addLoader(name);
});
}
const { id, metadata: { path } } = this.website.currentWebsite;
// A reload is needed after installing a new module, to instantiate
// a NewContentModal with patches from the installed module.
window.location.replace(`/web#action=website.website_preview&website_id=${id}&path=${encodeURIComponent(path)}&display_new_content=true`);
}
}
self._install(moduleId).then(function () {
var origin = window.location.origin;
var redirectURL = $el.find('a').data('url') || (window.location.pathname + '?' + enableFlag);
window.location.href = origin + redirectURL;
self._removeLoader();
}, function () {
$i.removeClass()
.addClass('fa fa-exclamation-triangle');
$p.text(_.str.sprintf(self.newContentText.failed, name));
});
}
}, {
text: _t("Cancel"),
close: true,
}];
onClickNewContent(element) {
if (element.createNewContent) {
return element.createNewContent();
}
new Dialog(this, {
title: title,
size: 'medium',
$content: $('<div/>', {text: content}),
buttons: buttons
}).open();
},
});
const {id, name} = this.modulesInfo[element.moduleXmlId];
const dialogProps = {
title: this.env._t("Install"),
body: _.str.sprintf(this.newContentText.installNeeded, name),
confirm: async () => {
// Update the NewContentElement with installing icon and text.
this.state.newContentElements = this.state.newContentElements.map(el => {
if (el.moduleXmlId === element.moduleXmlId) {
el.status = MODULE_STATUS.INSTALLING;
el.icon = xml`<i class="fa fa-spin fa-circle-o-notch"/>`;
el.title = _.str.sprintf(this.newContentText.installPleaseWait, name);
}
return el;
});
try {
await this.installModule(id, element.redirectUrl);
} catch (error) {
// Update the NewContentElement with failure icon and text.
this.state.newContentElements = this.state.newContentElements.map(el => {
if (el.moduleXmlId === element.moduleXmlId) {
el.status = MODULE_STATUS.FAILED_TO_INSTALL;
el.icon = xml`<i class="fa fa-exclamation-triangle"/>`;
el.title = _.str.sprintf(this.newContentText.failed, name);
}
return el;
});
console.error(error);
}
},
cancel: () => {},
};
this.dialogs.add(ConfirmationDialog, dialogProps);
}
}
NewContentModal.template = "website.NewContentModal";
NewContentModal.components = { NewContentElement };
registry.category("website_navbar_widgets").add("NewContentMenu", {
Widget: NewContentMenu,
selector: '.o_new_content_menu',
});
class NewContentSystray extends Component {
setup() {
this.rpc = useService('rpc');
this.website = useService('website');
this.websiteContext = useState(this.website.context);
}
return NewContentMenu;
});
onClick() {
this.websiteContext.showNewContentModal = !this.websiteContext.showNewContentModal;
}
}
NewContentSystray.template = "website.NewContentSystray";
NewContentSystray.components = { NewContentModal };
export const systrayItem = {
Component: NewContentSystray,
};
registry.category("website_systray").add("NewContent", systrayItem, { sequence: 10 });
@@ -0,0 +1,80 @@
@keyframes fadeInDownSmall {
0% {
opacity: 0;
transform: translate(0, -5px);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
}
#o_new_content_menu_choices {
@include o-position-absolute(0, 0, 0, 0);
position: fixed;
display: flex;
overflow: auto;
background-color: rgba(0, 0, 0, 0.8);
font-family: $o-we-font-family;
z-index: $zindex-fixed - 1;
&::before {
content: " ";
@include o-position-absolute(0, 0, 0, 0);
z-index: -1;
pointer-events: none;
}
.container {
max-width: 720px;
margin: auto;
}
.o_new_content_element {
opacity: 0;
animation: fadeInDownSmall 1s forwards;
.o_uninstalled_module {
filter: brightness(70%) contrast(60%);
}
.module_icon {
display: block;
margin: auto;
}
a {
display: block;
font-size: 34px;
text-align: center;
text-decoration: none;
i {
width: 110px;
height: 110px;
border: 3px solid lighten(#2C2C36, 10%);
border-radius: 100%;
line-height: 104px;
background-color: #2C2C36;
color: white;
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1) 0s;
}
p {
color: white;
margin-top: 0.7em;
font-size: 0.5em;
}
&:hover, &:focus {
text-decoration: none;
outline: none; // remove ugly dotted border on Firefox
i {
border-color: #1cc1a9;
box-shadow: 0 0 10px rgba(28, 193, 169, 0.46);
}
}
}
}
}
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="website.NewContentElement" owl="1">
<div class="o_new_content_element col-md-4 mb8" t-att-name="props.name">
<a href="#"
t-on-click="onClick"
t-att-class="props.status === MODULE_STATUS.NOT_INSTALLED ? 'o_uninstalled_module' : ''"
t-att-title="props.title"
t-att-aria-label="props.title">
<t t-slot="default"/>
</a>
</div>
</t>
<t t-name="website.NewContentModal" owl="1">
<div id="o_new_content_menu_choices">
<div class="container pt32 pb32">
<div class="row">
<NewContentElement t-if="isDesigner"
name="'New Page'"
onClick="() => createNewPage()"
title="'New Page'">
<i class="fa fa-file-o"/>
<p>Page</p>
</NewContentElement>
<t t-foreach="sortedNewContentElements" t-as="element" t-key="element.moduleXmlId" t-if="'isDisplayed' in element ? element.isDisplayed : isSystem ">
<NewContentElement onClick="() => this.onClickNewContent(element)" status="element.status" title="element.title">
<t t-call="{{ element.icon }}"/>
<p><t t-esc="element.title"/></p>
</NewContentElement>
</t>
</div>
</div>
</div>
</t>
<t t-name="website.NewContentSystray" owl="1">
<div class="o_menu_systray_item o_new_content_container" t-on-click="onClick">
<a href="#" accesskey="c"><span class="fa fa-plus mr-2"/>New</a>
<NewContentModal t-if="websiteContext.showNewContentModal"/>
</div>
</t>
</templates>
+3
View File
@@ -43,6 +43,9 @@
'website_blog/static/src/js/contentshare.js',
'website_blog/static/src/js/website_blog.js',
],
'web.assets_backend': [
'website_blog/static/src/js/systray_items/new_content.js',
],
},
'license': 'LGPL-3',
}
@@ -1,57 +1,16 @@
odoo.define('website_blog.new_blog_post', function (require) {
'use strict';
/** @odoo-module **/
var core = require('web.core');
var wUtils = require('website.utils');
var WebsiteNewMenu = require('website.newMenu');
import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';
import { patch } from 'web.utils';
var _t = core._t;
patch(NewContentModal.prototype, 'website_blog_new_content', {
setup() {
this._super();
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_blog_post: '_createNewBlogPost',
}),
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Asks the user information about a new blog post to create, then creates
* it and redirects the user to this new post.
*
* @private
* @returns {Promise} Unresolved if there is a redirection
*/
_createNewBlogPost: function () {
return this._rpc({
model: 'blog.blog',
method: 'search_read',
args: [wUtils.websiteDomain(this), ['name']],
}).then(function (blogs) {
if (blogs.length === 1) {
document.location = '/blog/' + blogs[0]['id'] + '/post/new';
return new Promise(function () {});
} else if (blogs.length > 1) {
return wUtils.prompt({
id: 'editor_new_blog',
window_title: _t("New Blog Post"),
select: _t("Select Blog"),
init: function (field) {
return _.map(blogs, function (blog) {
return [blog['id'], blog['name']];
});
},
}).then(function (result) {
var blog_id = result.val;
if (!blog_id) {
return;
}
document.location = '/blog/' + blog_id + '/post/new';
return new Promise(function () {});
});
}
});
const newBlogElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_blog');
newBlogElement.createNewContent = () => this.createNewBlogPost();
newBlogElement.status = MODULE_STATUS.INSTALLED;
},
});
createNewBlogPost() {}
});
+3
View File
@@ -66,6 +66,9 @@
'website.assets_wysiwyg': [
'/website_event/static/src/snippets/s_events/options.js',
],
'web.assets_backend': [
'website_event/static/src/js/systray_items/new_content.js',
],
},
'license': 'LGPL-3',
}
@@ -1,277 +1,16 @@
odoo.define('website_event.editor', function (require) {
"use strict";
/** @odoo-module **/
var core = require('web.core');
var Dialog = require('web.Dialog');
var time = require('web.time');
var WebsiteNewMenu = require('website.newMenu');
var _t = core._t;
import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';
import { patch } from 'web.utils';
var EventCreateDialog = Dialog.extend({
xmlDependencies: Dialog.prototype.xmlDependencies.concat(['/website_event/static/src/xml/event_create.xml']),
template: 'website_event.event_create_dialog',
events: _.extend({}, Dialog.prototype.events, {
'change select[name="event_location"]': '_onLocationChanged',
'click .input-group-append': '_onDatetimeClicked',
}),
jsLibs: [
'/web/static/lib/daterangepicker/daterangepicker.js',
'/web/static/src/legacy/js/libs/daterangepicker.js',
],
patch(NewContentModal.prototype, 'website_event_new_content', {
setup() {
this._super();
/**
* @override
* @param {Object} parent
* @param {Object} options
*/
init: function (parent, options) {
options = _.defaults(options || {}, {
title: _t("New Event"),
size: 'medium',
buttons: [
{
text: _t("Create"),
classes: 'btn-primary',
click: this._onClickCreate.bind(this),
},
{
text: _t("Discard"),
close: true
},
]
});
this.eventStart = moment();
this.eventEnd = moment().add(1, "d");
this._super(parent, options);
const newEventElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_event');
newEventElement.createNewContent = () => this.createNewEvent();
newEventElement.status = MODULE_STATUS.INSTALLED;
},
/**
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(() => {
// stat / end datetimes widgets
self.$('input.daterange-input').each(function () {
self._initDateRangePicker($(this));
});
// address select2 configuration
self.$('input.o_wevent_js_address_id').select2(self._getSelect2AddressConfig());
});
},
/**
* @param {*} errors
*/
_display_errors: function (errors) {
this.$("p.text-danger").toggleClass('d-none', true);
for (let i = 0; i < errors.length; i++) {
this.$("#" + errors[i] + " p.text-danger").toggleClass('d-none', false);
}
},
_getSelect2AddressConfig: function () {
var self = this;
return {
allowClear: true,
formatNoMatches: false,
multiple: false,
selection_data: false,
width: '100%',
createSearchChoice: function (name, data) {
var addedPartner = $(this.opts.element).select2('data');
if (_.filter(_.union(addedPartner, data), function (partner) {
return partner.text.toLowerCase().localeCompare(name.toLowerCase()) === 0;
}).length === 0) {
return {
create: true,
id: _.uniqueId('address_'),
name: name,
text: _.str.sprintf(_t('Create "%s"'), name),
};
}
},
fill_data: function (query, data) {
var that = this;
var partners = {results: []};
_.each(data, function (partner) {
var contact_address = partner.contact_address.replace(/[\n]+/g, '').trim();
if (contact_address && that.matcher(query.term, contact_address)) {
partners.results.push({
id: partner.id,
text: contact_address,
});
}
});
query.callback(partners);
},
formatSelection: function (data) {
if (data.name) {
data.text = data.name;
}
return data.text;
},
query: function (query) {
var that = this;
// fetch data only once and store it
if (!this.selection_data) {
self._rpc({
model: 'res.partner',
method: 'search_read',
fields: ['contact_address'],
domain: [],
}).then(function (data) {
that.fill_data(query, data);
that.selection_data = data;
});
} else {
this.fill_data(query, this.selection_data);
}
}
}
},
_getSelect2AddressValues: function () {
var select2Value = this.$('input.o_wevent_js_address_id').select2('data');
if (select2Value) {
if (select2Value.create) {
return [0, {'name': select2Value.name}];
} else {
return [select2Value.id, {}];
}
}
return [];
},
/**
*
* @param {*} $dateGroup
*/
_initDateRangePicker: function ($dateGroup) {
var minDate = moment().subtract(1, "d");
var maxDate = moment().add(200, "y");
var self = this;
var textDirection = _t.database.parameters.direction;
$dateGroup.daterangepicker({
// dates
endDate: this.eventEnd,
maxDate: maxDate,
minDate: minDate,
startDate: this.eventStart,
// display
locale: {
direction: textDirection,
format: time.getLangDatetimeFormat().replace(':ss', ''),
applyLabel: _t('Apply'),
cancelLabel: _t('Cancel'),
weekLabel: 'W',
customRangeLabel: _t('Custom Range'),
daysOfWeek: moment.weekdaysMin(),
monthNames: moment.monthsShort(),
firstDay: moment.localeData().firstDayOfWeek()
},
opens: 'left',
timePicker: true,
timePicker24Hour: true,
viewDate: moment(new Date()).hours(minDate.hours()).minutes(minDate.minutes()).seconds(minDate.seconds()).milliseconds(minDate.milliseconds()),
}, function (start, end, label) {
self.eventStart = start;
self.eventEnd = end;
});
},
/**
* @private
*/
_prepareFormValues: function () {
return {
address_values: this._getSelect2AddressValues(),
event_start: this.eventStart,
event_end: this.eventEnd,
location: this.$('select[name=event_location]').val(),
name: this.$('input[name=name]').val().trim(),
};
},
/**
* @private
* @param {*} values
*/
_validateForm: function (values) {
var errors = [];
if (!values.name){
errors.push('name');
}
if (values.location === 'on_site' && !values.address_values.length) {
errors.push('address');
}
if (!values.event_start || !values.event_end) {
errors.push('event_dates');
}
return errors;
},
/**
* @private
*/
_submitForm: function () {
var eventValues = this._prepareFormValues();
var errors = this._validateForm(eventValues)
console.log(eventValues, errors);
if (errors.length) {
this._display_errors(errors);
return;
}
return this._rpc({
route: '/event/add_event',
params: eventValues,
}).then((url) => {
window.location.href = url;
return new Promise(function () {});
});
},
/**
* @private
*/
_onClickCreate: function () {
return this._submitForm();
},
/**
* @private
* @param {*} ev
*/
_onLocationChanged: function (ev) {
this.$('.show_visibility_address').toggleClass('d-none', ev.target.value === 'online');
},
});
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_event: '_createNewEvent',
}),
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Asks the user information about a new event to create, then creates it
* and redirects the user to this new event.
*
* @private
* @returns {Promise} Unresolved if there is a redirection
*/
_createNewEvent: function () {
var self = this;
var def = new Promise(function (resolve) {
var dialog = new EventCreateDialog(self, {});
dialog.open();
dialog.on('closed', self, resolve);
});
return def;
},
});
createNewEvent() {}
});
+3
View File
@@ -52,6 +52,9 @@ Ask questions, get answers, no distractions
'website_forum/static/src/js/website_forum.js',
'website_forum/static/src/js/website_forum.share.js',
],
'web.assets_backend': [
'website_forum/static/src/js/systray_items/new_content.js',
],
'web.assets_qweb': [
'website_forum/static/src/xml/*.xml',
],
@@ -1,144 +1,16 @@
odoo.define('website_forum.editor', function (require) {
"use strict";
/** @odoo-module **/
var core = require('web.core');
var WebsiteNewMenu = require('website.newMenu');
var Dialog = require('web.Dialog');
import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';
import { patch } from 'web.utils';
var _t = core._t;
patch(NewContentModal.prototype, 'website_forum_new_content', {
setup() {
this._super();
var ForumCreateDialog = Dialog.extend({
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/website_forum/static/src/xml/website_forum_templates.xml']
),
template: 'website_forum.add_new_forum',
events: _.extend({}, Dialog.prototype.events, {
'change input[name="privacy"]': '_onPrivacyChanged',
}),
const newForumElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_forum');
newForumElement.createNewContent = () => this.createNewForum();
newForumElement.status = MODULE_STATUS.INSTALLED;
},
/**
* @override
* @param {Object} parent
* @param {Object} options
*/
init: function (parent, options) {
options = _.defaults(options || {}, {
title: _t("New Forum"),
size: 'medium',
buttons: [
{
text: _t("Create"),
classes: 'btn-primary',
click: this.onCreateClick.bind(this),
},
{
text: _t("Discard"),
close: true
},
]
});
this._super(parent, options);
},
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
var $input = self.$('#group_id');
$input.select2({
width: '100%',
allowClear: true,
formatNoMatches: false,
multiple: false,
selection_data: false,
fill_data: function (query, data) {
var that = this;
var tags = {results: []};
_.each(data, function (obj) {
if (that.matcher(query.term, obj.display_name)) {
tags.results.push({id: obj.id, text: obj.display_name});
}
});
query.callback(tags);
},
query: function (query) {
var that = this;
// fetch data only once and store it
if (!this.selection_data) {
self._rpc({
model: 'res.groups',
method: 'search_read',
args: [[], ['display_name']],
}).then(function (data) {
that.fill_data(query, data);
that.selection_data = data;
});
} else {
this.fill_data(query, this.selection_data);
}
}
});
});
},
onCreateClick: function () {
var $dialog = this.$el;
var $forumName = $dialog.find('input[name=forum_name]');
if (!$forumName.val()) {
$forumName.addClass('border-danger');
return;
}
var $forumPrivacyGroup = $dialog.find('input[name=group_id]');
var forumPrivacy = $dialog.find('input:radio[name=privacy]:checked').val();
if (forumPrivacy === 'private' && !$forumPrivacyGroup.val()) {
this.$("#group-required").removeClass('d-none');
return;
}
var addMenu = ($dialog.find('input[type="checkbox"]').is(':checked'));
var forumMode = $dialog.find('input:radio[name=mode]:checked').val();
return this._rpc({
route: '/forum/new',
params: {
forum_name: $forumName.val(),
forum_mode: forumMode,
forum_privacy: forumPrivacy,
forum_privacy_group: $forumPrivacyGroup.val(),
add_menu: addMenu || "",
},
}).then(function (url) {
window.location.href = url;
return new Promise(function () {});
});
},
/**
* @private
*/
_onPrivacyChanged: function (ev) {
this.$('.show_visibility_group').toggleClass('d-none', ev.target.value !== 'private');
},
});
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_forum: '_createNewForum',
}),
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Asks the user information about a new forum to create, then creates it
* and redirects the user to this new forum.
*
* @private
* @returns {Promise} Unresolved if there is a redirection
*/
_createNewForum: function () {
var self = this;
var def = new Promise(function (resolve) {
var dialog = new ForumCreateDialog(self, {});
dialog.open();
dialog.on('closed', self, resolve);
});
return def;
},
});
createNewForum() {}
});
@@ -27,6 +27,9 @@
'web.assets_frontend': [
'website_hr_recruitment/static/src/scss/**/*',
],
'web.assets_backend': [
'website_hr_recruitment/static/src/js/systray_items/new_content.js',
],
'website.assets_editor': [
'website_hr_recruitment/static/src/js/**/*',
],
@@ -0,0 +1,16 @@
/** @odoo-module **/
import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';
import { patch } from 'web.utils';
patch(NewContentModal.prototype, 'website_hr_recruitment_new_content', {
setup() {
this._super();
const newJobElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_hr_recruitment');
newJobElement.createNewContent = () => this.createNewJob();
newJobElement.status = MODULE_STATUS.INSTALLED;
},
async createNewJob() {}
});
+1
View File
@@ -41,6 +41,7 @@ Allow website visitors to chat with the collaborators. This module also brings a
'website_livechat/static/src/components/*/*.js',
'website_livechat/static/src/components/*/*.scss',
'website_livechat/static/src/models/*.js',
'website_livechat/static/src/js/systray_items/new_content.js',
],
'web.assets_tests': [
'website_livechat/static/tests/tours/**/*',
@@ -1,46 +1,16 @@
/** @odoo-module **/
import { _t } from 'web.core';
import wUtils from 'website.utils';
import WebsiteNewMenu from 'website.newMenu';
import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';
import { patch } from 'web.utils';
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_channel: '_createNewChannel',
}),
patch(NewContentModal.prototype, 'website_livechat_new_content', {
setup() {
this._super();
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Asks the user information about a new channel to create, then creates it
* and redirects the user to this new channel.
*
* @private
* @returns {Promise} Unresolved if there is a redirection
*/
_createNewChannel: function () {
var self = this;
return wUtils.prompt({
window_title: _t("New Channel"),
input: _t("Name"),
}).then(function (result) {
var name = result.val;
if (!name) {
return;
}
return self._rpc({
model: 'im_livechat.channel',
method: 'create_and_get_website_url',
args: [[]],
kwargs: {
name: name,
},
}).then(function (url) {
window.location.href = url;
return new Promise(function () {});
});
});
const newChannelElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_livechat');
newChannelElement.createNewContent = () => this.createNewChannel();
newChannelElement.status = MODULE_STATUS.INSTALLED;
},
createNewChannel() {}
});
+1
View File
@@ -66,6 +66,7 @@
'website_sale/static/src/scss/website_sale_dashboard.scss',
'website_sale/static/src/scss/website_sale_backend.scss',
'website_sale/static/src/js/tours/website_sale_shop_backend.js',
'website_sale/static/src/js/systray_items/new_content.js',
],
'website.assets_wysiwyg': [
'website_sale/static/src/scss/website_sale.editor.scss',
@@ -0,0 +1,16 @@
/** @odoo-module **/
import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';
import { patch } from 'web.utils';
patch(NewContentModal.prototype, 'website_sale_new_content', {
setup() {
this._super();
const newProductElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_sale');
newProductElement.createNewContent = () => this.createNewProduct();
newProductElement.status = MODULE_STATUS.INSTALLED;
},
createNewProduct() {}
});
+1
View File
@@ -70,6 +70,7 @@ Featuring
'website_slides/static/src/js/slide_category_one2many.js',
'website_slides/static/src/js/rating_field_backend.js',
'website_slides/static/src/models/*.js',
'website_slides/static/src/js/systray_items/new_content.js',
],
'web.assets_frontend': [
'website_slides/static/src/scss/website_slides.scss',
@@ -1,183 +1,16 @@
/** @odoo-module **/
import { _t } from 'web.core';
import Dialog from 'web.Dialog';
import WebsiteNewMenu from 'website.newMenu';
import SlidesCourseTagAdd from '@website_slides/js/slides_course_tag_add';
import { NewContentModal, MODULE_STATUS } from '@website/systray_items/new_content';
import { patch } from 'web.utils';
const TagCourseDialog = SlidesCourseTagAdd.TagCourseDialog;
patch(NewContentModal.prototype, 'website_slides_new_content', {
setup() {
this._super();
export const ChannelCreateDialog = Dialog.extend({
template: 'website.slide.channel.create',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/website_slides/static/src/xml/website_slides_channel.xml',
'/website_slides/static/src/xml/website_slides_channel_tag.xml']
),
events: _.extend({}, Dialog.prototype.events, {
'change input#tag_ids' : '_onChangeTag',
}),
custom_events: _.extend({}, Dialog.prototype.custom_events, {
'tag_refresh': '_onTagRefresh',
'tag_remove_new': '_onTagRemoveNew',
}),
/**
* @override
* @param {Object} parent
* @param {Object} options
*/
init: function (parent, options) {
options = _.defaults(options || {}, {
title: _t("New Course"),
size: 'medium',
buttons: [{
text: _t("Create"),
classes: 'btn-primary',
click: this._onClickFormSubmit.bind(this)
}, {
text: _t("Discard"),
close: true
},]
});
this._super(parent, options);
},
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
var $input = self.$('#tag_ids');
$input.select2({
width: '100%',
allowClear: true,
formatNoMatches: false,
multiple: true,
selection_data: false,
formatSelection: function (data) {
if (data.tag) {
data.text = data.tag;
}
return data.text;
},
createSearchChoice: function(term, data) {
var addedTags = $(this.opts.element).select2('data');
if (_.filter(_.union(addedTags, data), function (tag) {
return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0;
}).length === 0) {
if (this.opts.can_create) {
return {
id: _.uniqueId('tag_'),
create: true,
tag: term,
text: _.str.sprintf(_t("Create new Tag '%s'"), term),
};
} else {
return undefined;
}
}
},
fill_data: function (query, data) {
var that = this,
tags = {results: []};
_.each(data, function (obj) {
if (that.matcher(query.term, obj.name)) {
tags.results.push({id: obj.id, text: obj.name});
}
});
query.callback(tags);
},
query: function (query) {
var that = this;
// fetch data only once and store it
if (!this.selection_data) {
self._rpc({
route: '/slides/channel/tag/search_read',
params: {
fields: ['name'],
domain: [],
}
}).then(function (data) {
that.can_create = data.can_create;
that.fill_data(query, data.read_results);
that.selection_data = data.read_results;
});
} else {
this.fill_data(query, this.selection_data);
}
}
});
});
},
_onClickFormSubmit: function (ev) {
var $form = this.$("#slide_channel_add_form");
var $title = this.$("#title");
if (!$title[0].value){
$title.addClass('border-danger');
this.$("#title-required").removeClass('d-none');
} else {
$form.submit();
}
},
_onChangeTag: function (ev) {
var self = this;
var tags = $(ev.currentTarget).select2('data');
tags.forEach(function (element) {
if (element.create) {
new TagCourseDialog(self, { defaultTag: element.text }).open();
}
});
},
/**
* Replace the new tag ID by its real ID
* @param ev
* @private
*/
_onTagRefresh: function (ev) {
var $tag_ids = $('#tag_ids');
var tags = $tag_ids.select2('data');
tags.forEach(function (element) {
if (element.create) {
element.id = ev.data.tag_id;
element.create = false;
}
});
$tag_ids.select2('data', tags);
// Set selection_data to false to force tag reload
$tag_ids.data('select2').opts.selection_data = false;
},
/**
* Remove the created tag if the user clicks on 'Discard' on the create tag Dialog
* @private
*/
_onTagRemoveNew: function () {
var tags = $('#tag_ids').select2('data');
tags = tags.filter(function (value) {
return !value.create;
});
$('#tag_ids').select2('data', tags);
const newSlidesChannelElement = this.state.newContentElements.find(element => element.moduleXmlId === 'base.module_website_slides');
newSlidesChannelElement.createNewContent = () => this.createNewSlidesChannel();
newSlidesChannelElement.status = MODULE_STATUS.INSTALLED;
},
});
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_slide_channel: '_createNewSlideChannel',
}),
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Displays the popup to create a new slide channel,
* and redirects the user to this channel.
*
* @private
* @returns {Promise} Unresolved if there is a redirection
*/
_createNewSlideChannel: function () {
var self = this;
var def = new Promise(function (resolve) {
var dialog = new ChannelCreateDialog(self, {});
dialog.open();
dialog.on('closed', self, resolve);
});
return def;
},
createNewSlidesChannel() {}
});