[IMP] website: make it possible to create new pages from templates

This commit modifies the "New Page" dialog so that it displays a list
of page templates to pick from in addition to the possibility to create
a blank page.

task-3381714

Part-of: odoo/odoo#126719
Co-authored-by: stefanorigano (SRI) <sri@odoo.com>
This commit is contained in:
Benoit Socias
2023-10-14 03:27:03 +00:00
committed by Romain Derie
co-authored by stefanorigano
parent 928eeca714
commit e0796020ee
11 changed files with 879 additions and 118 deletions
+46 -2
View File
@@ -12,7 +12,7 @@ import werkzeug.utils
import werkzeug.wrappers
from itertools import islice
from lxml import etree
from lxml import etree, html
from textwrap import shorten
from werkzeug.exceptions import NotFound
from xml.etree import ElementTree as ET
@@ -24,6 +24,7 @@ from odoo.exceptions import AccessError
from odoo.http import request, SessionExpiredException
from odoo.osv import expression
from odoo.tools import OrderedSet, escape_psql, html_escape as escape
from odoo.addons.base.models.ir_qweb import QWebException
from odoo.addons.http_routing.models.ir_http import slug, slugify, _guess_mimetype
from odoo.addons.portal.controllers.portal import pager as portal_pager
from odoo.addons.portal.controllers.web import Home
@@ -629,7 +630,7 @@ class Website(Home):
if website_id:
website = request.env['website'].browse(int(website_id))
website._force()
page = request.env['website'].new_page(path, add_menu=add_menu, **template)
page = request.env['website'].new_page(path, add_menu=add_menu, sections_arch=kwargs.get('sections_arch'), **template)
url = page['url']
if redirect:
@@ -641,6 +642,49 @@ class Website(Home):
return json.dumps({'view_id': page.get('view_id')})
return json.dumps({'url': url})
@http.route('/website/get_new_page_templates', type='json', auth='user', website=True)
def get_new_page_templates(self, **kw):
View = request.env['ir.ui.view']
result = []
groups_html = View._render_template("website.new_page_template_groups")
groups_el = etree.fromstring(f'<data>{groups_html}</data>')
for group_el in groups_el.getchildren():
group = {
'id': group_el.attrib['id'],
'title': group_el.text,
'templates': [],
}
for template in View.search([
('mode', '=', 'primary'),
('key', 'like', escape_psql(f'new_page_template_sections_{group["id"]}_')),
], order='key'):
try:
html_tree = html.fromstring(View.with_context(inherit_branding=False)._render_template(
template.key,
))
for section_el in html_tree.xpath("//section[@data-snippet]"):
# data-snippet must be the short general name
snippet = section_el.attrib['data-snippet']
# Because the templates are generated from specific
# t-snippet-calls such as:
# "website.new_page_template_about_0_s_text_block",
# the generated data-snippet looks like:
# "new_page_template_about_0_s_text_block"
# while it should be "s_text_block" only.
if '_s_' in snippet:
section_el.attrib['data-snippet'] = f's_{snippet.split("_s_")[-1]}'
group['templates'].append({
'key': template.key,
'template': html.tostring(html_tree),
})
except QWebException as qe:
# Do not fail if theme is not compatible.
logger.warning("Theme not compatible with template %r: %s", template.key, qe)
if group['templates']:
result.append(group)
return result
@http.route("/website/get_switchable_related_views", type="json", auth="user", website=True)
def get_switchable_related_views(self, key):
views = request.env["ir.ui.view"].get_related_views(key, bundles=False).filtered(lambda v: v.customize_show)
+10 -2
View File
@@ -765,7 +765,7 @@ class Website(models.Model):
copy_menu(submenu, new_top_menu)
@api.model
def new_page(self, name=False, add_menu=False, template='website.default_page', ispage=True, namespace=None, page_values=None, menu_values=None):
def new_page(self, name=False, add_menu=False, template='website.default_page', ispage=True, namespace=None, page_values=None, menu_values=None, sections_arch=None):
""" Create a new website page, and assign it a xmlid based on the given one
:param name: the name of the page
:param add_menu: if True, add a menu for that page
@@ -773,6 +773,7 @@ class Website(models.Model):
:param namespace: module part of the xml_id if none, the template module name is used
:param page_values: default values for the page to be created
:param menu_values: default values for the menu to be created
:param sections_arch: HTML content of sections
"""
if namespace:
template_module = namespace
@@ -788,12 +789,19 @@ class Website(models.Model):
page_key = 'home'
template_record = self.env.ref(template)
arch = template_record.arch
if sections_arch:
tree = html.fromstring(arch)
wrap = tree.xpath('//div[@id="wrap"]')[0]
for section in html.fromstring(f'<wrap>{sections_arch}</wrap>'):
wrap.append(section)
arch = etree.tostring(tree, encoding="unicode")
website_id = self._context.get('website_id')
key = self.get_unique_key(page_key, template_module)
view = template_record.copy({'website_id': website_id, 'key': key})
view.with_context(lang=None).write({
'arch': template_record.arch.replace(template, key),
'arch': arch.replace(template, key),
'name': name,
})
result['view_id'] = view.id
@@ -0,0 +1,436 @@
/** @odoo-module **/
import { isBrowserFirefox } from "@web/core/browser/feature_detection";
import { useAutofocus, useService } from '@web/core/utils/hooks';
import { _t } from "@web/core/l10n/translation";
import { WebsiteDialog } from '@website/components/dialog/dialog';
import { Switch } from '@website/components/switch/switch';
import { useRef, useState, useSubEnv, Component, onWillStart, onMounted } from "@odoo/owl";
import wUtils from '@website/js/utils';
const NO_OP = () => {};
export class AddPageConfirmDialog extends Component {
setup() {
super.setup();
useAutofocus();
this.title = _t("New Page");
this.primaryTitle = _t("Create");
this.switchLabel = _t("Add to menu");
this.website = useService('website');
this.http = useService('http');
this.action = useService('action');
this.state = useState({
addMenu: true,
name: this.props.name,
});
}
onChangeAddMenu(value) {
this.state.addMenu = value;
}
async addPage() {
const params = {'add_menu': this.state.addMenu || '', csrf_token: odoo.csrf_token};
if (this.props.sectionsArch) {
params.sections_arch = this.props.sectionsArch;
}
// Remove any leading slash.
const pageName = this.state.name.replace(/^\/*/, "") || _t("New Page");
const url = `/website/add/${encodeURIComponent(pageName)}`;
params['website_id'] = this.props.websiteId;
const data = await this.http.post(url, params);
if (data.view_id) {
this.action.doAction({
'res_model': 'ir.ui.view',
'res_id': data.view_id,
'views': [[false, 'form']],
'type': 'ir.actions.act_window',
'view_mode': 'form',
});
} else {
this.website.goToWebsite({path: data.url, edition: true, websiteId: this.props.websiteId});
}
this.props.onAddPage(this.state);
}
}
AddPageConfirmDialog.props = {
close: Function,
onAddPage: {
type: Function,
optional: true,
},
websiteId: Number,
sectionsArch: {
type: String,
optional: true,
},
name: String,
};
AddPageConfirmDialog.defaultProps = {
onAddPage: NO_OP,
};
AddPageConfirmDialog.components = {
Switch,
WebsiteDialog,
};
AddPageConfirmDialog.template = "website.AddPageConfirmDialog";
export class AddPageTemplateBlank extends Component {
setup() {
super.setup();
this.holderRef = useRef("holder");
onMounted(async () => {
this.holderRef.el.classList.add("o_ready");
});
}
select() {
this.env.addPage();
}
}
AddPageTemplateBlank.props = {
firstRow: {
type: Boolean,
optional: true,
},
};
AddPageTemplateBlank.template = "website.AddPageTemplateBlank";
export class AddPageTemplatePreview extends Component {
setup() {
super.setup();
this.iframeRef = useRef("iframe");
this.previewRef = useRef("preview");
this.holderRef = useRef("holder");
onMounted(async () => {
const holderEl = this.holderRef.el;
holderEl.classList.add("o_loading");
if (!this.props.template.key) {
return;
}
const previewEl = this.previewRef.el;
const iframeEl = this.iframeRef.el;
// Firefox replaces the built content with about:blank.
const isFirefox = isBrowserFirefox();
if (isFirefox) {
// Make sure empty preview iframe is loaded.
// This event is never triggered on Chrome.
await new Promise(resolve => {
iframeEl.contentDocument.body.onload = resolve;
});
}
// Apply styles.
for (const cssLinkEl of await this.env.getCssLinkEls()) {
const preloadLinkEl = document.createElement("link");
preloadLinkEl.setAttribute("rel", "preload");
preloadLinkEl.setAttribute("href", cssLinkEl.getAttribute("href"));
preloadLinkEl.setAttribute("as", "style");
iframeEl.contentDocument.head.appendChild(preloadLinkEl);
iframeEl.contentDocument.head.appendChild(cssLinkEl.cloneNode(true));
}
// Adjust styles.
const styleEl = document.createElement("style");
// Does not work with fit-content in Firefox.
const carouselHeight = isFirefox ? '450px' : 'fit-content';
// Prevent successive resizes.
const fullHeight = getComputedStyle(document.querySelector(".o_action_manager")).height;
const halfHeight = `${Math.round(parseInt(fullHeight) / 2)}px`;
const css = `
#wrapwrap {
overflow: hidden;
padding-right: 0px;
padding-left: 0px;
}
section {
/* Avoid the zoom's missing pixel. */
transform: scale(101%);
}
section[data-snippet="s_carousel"],
section[data-snippet="s_quotes_carousel"] {
height: ${carouselHeight} !important;
}
section.o_half_screen_height {
min-height: ${halfHeight} !important;
}
section.o_full_screen_height {
min-height: ${fullHeight} !important;
}
section[data-snippet="s_three_columns"] .figure-img[style*="height:50vh"] {
/* In Travel theme. */
height: 170px !important;
}
.o_we_shape {
/* Avoid the zoom's missing pixel. */
transform: scale(101%);
}
`;
const cssText = document.createTextNode(css);
styleEl.appendChild(cssText);
iframeEl.contentDocument.head.appendChild(styleEl);
// Put blocks.
// To preserve styles, the whole #wrapwrap > main > #wrap
// nesting must be reproduced.
const mainEl = document.createElement("main");
const wrapwrapEl = document.createElement("div");
wrapwrapEl.id = "wrapwrap";
wrapwrapEl.appendChild(mainEl);
iframeEl.contentDocument.body.appendChild(wrapwrapEl);
const templateDocument = new DOMParser().parseFromString(this.props.template.template, "text/html");
const wrapEl = templateDocument.getElementById("wrap");
mainEl.appendChild(wrapEl);
// Make image loading eager.
const lazyLoadedImgEls = wrapEl.querySelectorAll("img[loading=lazy]");
for (const imgEl of lazyLoadedImgEls) {
imgEl.setAttribute("loading", "eager");
}
mainEl.appendChild(wrapEl);
await wUtils.onceAllImagesLoaded($(wrapEl));
// Restore image lazy loading.
for (const imgEl of lazyLoadedImgEls) {
imgEl.setAttribute("loading", "lazy");
}
// Wait for fonts.
await iframeEl.contentDocument.fonts.ready;
holderEl.classList.remove("o_loading");
const adjustHeight = () => {
if (!this.previewRef.el) {
// Stop ajusting height when preview is removed.
return;
}
const outerWidth = parseInt(window.getComputedStyle(previewEl).width);
const innerHeight = wrapEl.getBoundingClientRect().height;
const innerWidth = wrapEl.getBoundingClientRect().width;
const ratio = outerWidth / innerWidth;
iframeEl.height = Math.round(innerHeight);
previewEl.style.setProperty("height", `${Math.round(innerHeight * ratio)}px`);
// Sometimes the final height is not ready yet.
setTimeout(adjustHeight, 50);
holderEl.classList.add("o_ready");
};
adjustHeight();
});
}
select() {
if (this.holderRef.el.classList.contains("o_loading")) {
return;
}
const wrapEl = this.iframeRef.el.contentDocument.getElementById("wrap").cloneNode(true);
for (const previewEl of wrapEl.querySelectorAll(".o_new_page_snippet_preview")) {
previewEl.remove();
}
this.env.addPage(wrapEl.innerHTML);
}
}
AddPageTemplatePreview.props = {
template: Object,
animationDelay: Number,
firstRow: {
type: Boolean,
optional: true,
},
};
AddPageTemplatePreview.template = "website.AddPageTemplatePreview";
export class AddPageTemplatePreviews extends Component {
setup() {
super.setup();
}
get columns() {
const result = [[], [], []];
let currentColumnIndex = 0;
for (const template of this.props.templates) {
result[currentColumnIndex].push(template);
currentColumnIndex = (currentColumnIndex + 1) % result.length;
}
return result;
}
}
AddPageTemplatePreviews.props = {
templates: {
type: Array,
element: Object,
},
};
AddPageTemplatePreviews.components = {
AddPageTemplateBlank,
AddPageTemplatePreview,
};
AddPageTemplatePreviews.template = "website.AddPageTemplatePreviews";
export class AddPageTemplates extends Component {
setup() {
super.setup();
this.tabsRef = useRef("tabs");
this.panesRef = useRef("panes");
this.rpc = useService('rpc');
this.state = useState({
pages: [{
Component: AddPageTemplatePreviews,
title: _t("Loading..."),
isPreloading: true,
props: {
id: "basic",
title: _t("Basic"),
// Blank and 5 preloading boxes.
templates: [{ isBlank: true }, {}, {}, {}, {}, {}],
},
}],
});
this.pages = undefined;
onWillStart(() => {
this.preparePages().then(pages => {
this.state.pages = pages;
});
});
}
async preparePages() {
// Forces the correct website if needed before fetching the templates.
// Displaying the correct images in the previews also relies on the
// website id having been forced.
await this.env.getCssLinkEls();
if (this.pages) {
return this.pages;
}
const newPageTemplates = await this.rpc("/website/get_new_page_templates");
newPageTemplates[0].templates.unshift({
isBlank: true,
});
const pages = [];
for (const template of newPageTemplates) {
pages.push({
Component: AddPageTemplatePreviews,
title: template.title,
props: template,
id: `${template.id}`,
});
}
this.pages = pages;
return pages;
}
onTabClick(id) {
for (const page of this.state.pages) {
if (page.id === id) {
page.isAccessed = true;
}
}
const activeTabEl = this.tabsRef.el.querySelector(".active");
const activePaneEl = this.panesRef.el.querySelector(".active");
activeTabEl?.classList?.remove("active");
activePaneEl?.classList?.remove("active");
const tabEl = this.tabsRef.el.querySelector(`[data-id=${id}]`);
const paneEl = this.panesRef.el.querySelector(`[data-id=${id}]`);
tabEl.classList.add("active");
paneEl.classList.add("active");
this.props.onTemplatePageChanged(tabEl.dataset.id === "basic" ? "" : tabEl.textContent);
}
}
AddPageTemplates.props = {
onTemplatePageChanged: Function,
};
AddPageTemplates.components = {
AddPageTemplatePreviews,
};
AddPageTemplates.template = "website.AddPageTemplates";
export class AddPageDialog extends Component {
setup() {
super.setup();
useAutofocus();
this.title = _t("New Page");
this.primaryTitle = _t("Create");
this.switchLabel = _t("Add to menu");
this.website = useService('website');
this.dialogs = useService("dialog");
this.orm = useService('orm');
this.rpc = useService('rpc');
this.http = useService('http');
this.action = useService('action');
this.userService = useService('user');
this.cssLinkEls = undefined;
if (this.props.ready) {
onMounted(() => this.props.ready());
}
this.lastTabName = "";
useSubEnv({
addPage: sectionsArch => this.addPage(sectionsArch),
getCssLinkEls: () => this.getCssLinkEls(),
});
}
onTemplatePageChanged(name) {
this.lastTabName = name;
}
async addPage(sectionsArch) {
const props = this.props;
this.dialogs.add(AddPageConfirmDialog, {
onAddPage: () => {
props.onAddPage();
props.close();
},
websiteId: this.props.websiteId,
sectionsArch: sectionsArch,
name: this.lastTabName,
});
}
getCssLinkEls() {
if (!this.cssLinkEls) {
this.cssLinkEls = new Promise(async resolve => {
let contentDocument;
// Already in DOM ?
const pageIframeEl = document.querySelector("iframe.o_iframe");
if (pageIframeEl?.getAttribute("is-ready") === "true") {
// If there is a fully loaded website preview, use it.
contentDocument = pageIframeEl.contentDocument;
}
if (!contentDocument) {
// If there is no website preview or it was not ready yet, fetch page.
const html = await this.http.get(`/website/force/${this.props.websiteId}?path=/`, "text");
contentDocument = new DOMParser().parseFromString(html, "text/html");
}
resolve(contentDocument.head.querySelectorAll("link[type='text/css']"));
});
}
return this.cssLinkEls;
}
}
AddPageDialog.props = {
close: Function,
onAddPage: {
type: Function,
optional: true,
},
ready: {
type: Function,
optional: true,
},
websiteId: {
type: Number,
},
};
AddPageDialog.defaultProps = {
onAddPage: NO_OP,
};
AddPageDialog.components = {
WebsiteDialog,
AddPageTemplates,
AddPageTemplatePreviews,
};
AddPageDialog.template = "website.AddPageDialog";
@@ -0,0 +1,95 @@
.o_website_page_templates_dialog {
max-height: $modal-lg !important;
.modal-body {
display: flex;
padding: 0;
.nav {
width: 200px;
}
}
.o_website_page_templates_pane.active {
// Avoid using "display: [none/block]" to trigger the tabs behavior
// because doing so would recalculate iframes' height.
z-index: 1;
}
.o_page_template {
$-iframe-width: 1024;
$-preview-width: 275; // Empirically obtained
$-preview-width-xl: 220; // Empirically obtained
$-preview-width-lg: 408; // Empirically obtained
width: #{$-preview-width}px;
@include media-breakpoint-down(xl) {
width: #{$-preview-width-xl}px;
}
@include media-breakpoint-down(lg) {
width: #{$-preview-width-lg}px;
}
.o_page_template_preview {
// Resize iframe:
height: 300px;
padding: 0;
overflow: hidden;
iframe {
width: #{$-iframe-width}px;
transform: scale($-preview-width / $-iframe-width);
@include media-breakpoint-down(xl) {
transform: scale($-preview-width-xl / $-iframe-width);
}
@include media-breakpoint-down(lg) {
transform: scale($-preview-width-lg / $-iframe-width);
}
transform-origin: 0 0;
}
&, iframe {
opacity: 0;
transition: height 300ms ease-in, opacity 300ms ease 300ms;
}
}
.placeholder {
transition: opacity 1s ease;
}
.o_button_area {
opacity: 0;
}
&:empty {
opacity: 0; // Hide empty column filler
}
&.o_loading {
cursor: wait;
}
&.o_ready {
.placeholder {
animation: none;
opacity: 0;
}
.o_page_template_preview, iframe {
opacity: 1;
}
&:hover {
transform: translateY(-0.15rem);
iframe {
opacity: .1;
}
.o_button_area {
opacity: 1;
}
}
}
}
}
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="website.AddPageConfirmDialog">
<WebsiteDialog
title="title"
primaryTitle="primaryTitle"
primaryClick="() => this.addPage()"
close="props.close">
<div class="row gy-4">
<label class="col-form-label col-md-3">
Page Title
</label>
<div class="col-md-9">
<input type="text" t-model="state.name" class="form-control" required="required" t-ref="autofocus"/>
</div>
<Switch extraClasses="'offset-md-3 col-md-9 text-start'" label="switchLabel" value="state.addMenu" onChange="(value) => this.onChangeAddMenu(value)"/>
</div>
</WebsiteDialog>
</t>
<t t-name="website.AddPageTemplatePreviewWrapper">
<div t-ref="holder" class="o_page_template position-relative placeholder-glow mx-auto transition-base" t-att-class="{ 'mt-4': !props.firstRow }">
<div t-ref="preview" class="o_page_template_preview border border-white bg-white rounded shadow" t-out="0"/>
<span class="placeholder position-absolute top-0 w-100 h-100 rounded" t-attf-style="animation-duration: #{props.animationDelay + 500}ms"/>
<div class="o_button_area position-absolute top-0 w-100 h-100 d-flex align-items-center justify-content-center">
<button class="btn btn-primary" t-on-click="select">Use this template</button>
</div>
</div>
</t>
<t t-name="website.AddPageTemplateBlank">
<t t-call="website.AddPageTemplatePreviewWrapper">
<div class="mt-5 h1 text-center">Blank Page</div>
</t>
</t>
<t t-name="website.AddPageTemplatePreview">
<t t-call="website.AddPageTemplatePreviewWrapper">
<iframe class="border-0 pe-none transition-base" t-ref="iframe" src="about:blank"/>
</t>
</t>
<t t-name="website.AddPageTemplatePreviews">
<div class="container-fluid py-3 py-xl-4">
<div class="row g-0">
<t t-foreach="[...this.columns.entries()]" t-as="column" t-key="column[0]">
<div class="col-lg-4 col-12">
<t t-foreach="[...column[1].entries()]" t-as="template" t-key="template[0]">
<AddPageTemplateBlank t-if="template[1].isBlank" firstRow="template_first"/>
<AddPageTemplatePreview t-else="" template="template[1]" firstRow="template_first"
animationDelay="(column_index * 500) + (template_index * 350)"
/>
</t>
<t t-if="column[1].length === 0">
<div class="o_page_template position-relative"/>
</t>
</div>
</t>
</div>
</div>
</t>
<t t-name="website.AddPageTemplates">
<div class="overflow-hidden w-100" t-att-class="{o_loading: state.loading}">
<!-- Do not rely on Notebook to avoid pages from needing reload -->
<div class="d-flex w-100 h-100 vertical flex-row">
<ul t-ref="tabs" class="nav nav-pills flex-column flex-nowrap overflow-y-auto">
<t t-foreach="state.pages" t-as="page" t-key="page.id">
<li class="nav-item">
<a t-att-data-id="page.id"
t-on-click="() => this.onTabClick(page.id)"
class="nav-link p-3 rounded-0"
t-att-class="{active: page_first}"
href="#"
role="tab"
tabindex="0"
>
<span t-if="page.isPreloading" class="fa fa-spin fa-spinner me-1"/>
<t t-out="page.title"/>
</a>
</li>
</t>
</ul>
<div t-ref="panes" class="position-relative flex-grow-1 flex-shrink-1 bg-200">
<t t-foreach="state.pages" t-as="page" t-key="page.id">
<div t-att-data-id="page.id"
class="o_website_page_templates_pane position-absolute top-0 start-0 w-100 h-100 overflow-y-scroll bg-200"
t-att-class="{active: page_first}"
>
<AddPageTemplatePreviews t-if="page_first or page.isAccessed"
templates="page.props.templates"
/>
</div>
</t>
</div>
</div>
</div>
</t>
<t t-name="website.AddPageDialog">
<WebsiteDialog
title="title"
contentClass="'o_website_page_templates_dialog h-100'"
showFooter="false"
footer="false"
size="'xl'"
close="props.close">
<AddPageTemplates
onTemplatePageChanged="name => this.onTemplatePageChanged(name)"
/>
</WebsiteDialog>
</t>
</templates>
@@ -1,11 +1,8 @@
/** @odoo-module **/
import { useAutofocus, useService } from '@web/core/utils/hooks';
import { Dialog } from '@web/core/dialog/dialog';
import { _t } from "@web/core/l10n/translation";
import { Switch } from '@website/components/switch/switch';
import {unslugHtmlDataObject} from '../../services/website_service';
import { xml, useState, Component, onWillStart } from "@odoo/owl";
import { useState, Component } from "@odoo/owl";
const NO_OP = () => {};
@@ -60,10 +57,12 @@ WebsiteDialog.props = {
closeOnClick: { type: Boolean, optional: true },
body: { type: String, optional: true },
slots: { type: Object, optional: true },
showFooter: { type: Boolean, optional: true },
};
WebsiteDialog.defaultProps = {
...Dialog.defaultProps,
title: _t("Confirmation"),
showFooter: true,
primaryTitle: _t("Ok"),
secondaryTitle: _t("Cancel"),
showSecondaryButton: true,
@@ -72,109 +71,3 @@ WebsiteDialog.defaultProps = {
close: NO_OP,
};
WebsiteDialog.template = "website.WebsiteDialog";
export class AddPageDialog extends Component {
setup() {
super.setup();
useAutofocus();
this.title = _t("New Page");
this.primaryTitle = _t("Create");
this.switchLabel = _t("Add to menu");
this.website = useService('website');
this.orm = useService('orm');
this.http = useService('http');
this.action = useService('action');
this.state = useState({
addMenu: true,
name: '',
websiteId: false,
});
onWillStart(async () => {
const [currentWebsite] = await Promise.all([
await this.orm.call('website', 'get_current_website'),
this.website.fetchWebsites(),
]);
this.state.websiteId = unslugHtmlDataObject(currentWebsite).id;
});
}
onChangeAddMenu(value) {
this.state.addMenu = value;
}
async addPage() {
const params = {'add_menu': this.state.addMenu || '', csrf_token: odoo.csrf_token};
// Remove any leading slash.
const pageName = this.state.name.replace(/^\/*/, "");
const url = `/website/add/${encodeURIComponent(pageName)}`;
const websiteId = parseInt(this.state.websiteId);
if (this.props.selectWebsite) {
params['website_id'] = websiteId;
}
const data = await this.http.post(url, params);
if (data.view_id) {
this.action.doAction({
'res_model': 'ir.ui.view',
'res_id': data.view_id,
'views': [[false, 'form']],
'type': 'ir.actions.act_window',
'view_mode': 'form',
});
} else {
this.website.goToWebsite({path: data.url, edition: true, ...(this.props.selectWebsite && {websiteId})});
}
this.props.onAddPage(this.state);
}
}
AddPageDialog.props = {
close: Function,
onAddPage: {
type: Function,
optional: true,
},
selectWebsite: {
type: Boolean,
optional: true,
},
};
AddPageDialog.defaultProps = {
onAddPage: NO_OP,
selectWebsite: false,
};
AddPageDialog.components = {
Switch,
WebsiteDialog,
};
AddPageDialog.template = xml`
<WebsiteDialog
title="title"
primaryTitle="primaryTitle"
primaryClick="() => this.addPage()"
close="props.close">
<div class="row gy-4">
<label class="col-form-label col-md-3">
Page Title
</label>
<div class="col-md-9">
<input type="text" t-model="state.name" class="form-control" required="required" t-ref="autofocus"/>
</div>
<t t-if="props.selectWebsite">
<label class="col-form-label col-md-3">Website</label>
<div class="col-md-9">
<select class="form-select" t-model="state.websiteId">
<option t-foreach="website.websites"
t-as="option"
t-key="option.id"
t-att-value="option.id"
t-esc="option.name"
/>
</select>
</div>
</t>
<Switch extraClasses="'offset-md-3 col-md-9 text-start'" label="switchLabel" value="state.addMenu" onChange="(value) => this.onChangeAddMenu(value)"/>
</div>
</WebsiteDialog>
`;
@@ -2,13 +2,14 @@
<templates xml:space="preserve">
<t t-name="website.WebsiteDialog">
<Dialog contentClass="contentClasses"
footer="props.showFooter ? undefined : false"
size="props.size"
title="props.title">
<t t-set-slot="default">
<t t-if="props.slots and props.slots.default" t-slot="default"/>
<t t-else="" t-esc="props.body"/>
</t>
<t t-set-slot="footer">
<t t-if="props.showFooter" t-set-slot="footer">
<t t-if="props.slots and props.slots.footer" t-slot="footer"/>
<t t-else="">
<button class="btn btn-primary" t-on-click="protectedClick(props.primaryClick)" t-att-disabled="state.disabled">
@@ -21,4 +22,5 @@
</t>
</Dialog>
</t>
</templates>
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import {AddPageDialog} from "../dialog/dialog";
import { AddPageDialog } from "@website/components/dialog/add_page_dialog";
import {useService} from "@web/core/utils/hooks";
import { onWillStart, useState } from "@odoo/owl";
@@ -41,7 +41,15 @@ export const PageControllerMixin = (component) => class extends component {
*/
async createWebsiteContent() {
if (this.props.resModel === 'website.page') {
return this.dialog.add(AddPageDialog, {selectWebsite: true});
if (this.isCreatingNewPage) {
// Prevent opening the "New page" dialog several times.
return;
}
this.isCreatingNewPage = true;
return this.dialog.add(AddPageDialog, {
websiteId: this.state.activeWebsite.id,
ready: () => this.isCreatingNewPage = false,
});
}
const action = this.props.context.create_action;
if (action) {
@@ -3,7 +3,8 @@
import { _t } from "@web/core/l10n/translation";
import { registry } from '@web/core/registry';
import { useService } from '@web/core/utils/hooks';
import { WebsiteDialog, AddPageDialog } from "@website/components/dialog/dialog";
import { WebsiteDialog } from "@website/components/dialog/dialog";
import { AddPageDialog } from "@website/components/dialog/add_page_dialog";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { sprintf } from '@web/core/utils/strings';
import { Component, xml, useState, onWillStart } from "@odoo/owl";
@@ -170,8 +171,15 @@ export class NewContentModal extends Component {
}
createNewPage() {
if (this.isCreatingNewPage) {
// Prevent opening the "New page" dialog several times.
return;
}
this.isCreatingNewPage = true;
this.dialogs.add(AddPageDialog, {
onAddPage: () => this.websiteContext.showNewContentModal = false,
websiteId: this.website.currentWebsite.id,
ready: () => this.isCreatingNewPage = false,
});
}
+6
View File
@@ -40,6 +40,9 @@ wTourUtils.registerWebsitePreviewTour('rte_translator', {
}, {
content: "click on new page",
trigger: '.o_new_content_element a',
}, {
content: "click on Use this template",
trigger: ".o_page_template button.btn-primary",
}, {
content: "insert file name",
trigger: '.modal-dialog input[type="text"]',
@@ -61,6 +64,9 @@ wTourUtils.registerWebsitePreviewTour('rte_translator', {
}, {
content: "click on new page",
trigger: '.o_new_content_element a',
}, {
content: "click on Use this template",
trigger: ".o_page_template button.btn-primary",
}, {
content: "insert page name",
trigger: '.modal-dialog input[type="text"]',
@@ -1,6 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- This template is used to obtain the order and the translations of all
group names.
It is not done in python (eg: a method returning a list of translated terms,
overridden by modules), because:
- it needs to be simple for designers and community: only write XML in themes
- python is ignored when importing modules in Odoo, which is a common flow for
themes
-->
<template id="new_page_template_groups">
<div id="basic">Basic</div>
<div id="about">About</div>
<div id="landing">Landing Pages</div>
<div id="gallery">Gallery</div>
<div id="services">Services</div>
<div id="pricing">Pricing Plans</div>
<div id="team">Team</div>
</template>
<!-- Specific templates are generated from the manifest files:
If the manifest defines:
'new_page_template': {
'somekind': {
'somespecific': ['s_cover'... list of snippets],
},
}
A template named: new_page_template_sections_somekind_somespecific is generated.
Its arch contains t-snippet-calls to each listed snippet, all nested in a div#wrap.
-->
<!-- Previews for blocks that normally generate part of their content upon drop -->
<!-- Defined in website so that every theme can benefit from it -->
<!-- Table of content: define the entries that match the default text -->
<template id="new_page_template_s_table_of_content" inherit_id="s_table_of_content" primary="True">
<xpath expr="//div[hasclass('s_table_of_content_navbar')]" position="attributes">
<attribute name="style">top: 76px; max-height: calc(100vh - 96px);</attribute>
</xpath>
<xpath expr="//div[hasclass('s_table_of_content_navbar')]" position="inside">
<a href="#table_of_content_heading_1_1" class="table_of_content_link list-group-item list-group-item-action py-2 border-0 rounded-0 active">Intuitive system</a>
<a href="#table_of_content_heading_1_2" class="table_of_content_link list-group-item list-group-item-action py-2 border-0 rounded-0">Design features</a>
</xpath>
<xpath expr="//section//section[1]//h1" position="attributes">
<attribute name="id">table_of_content_heading_1_1</attribute>
</xpath>
<xpath expr="//section//section[2]//h1" position="attributes">
<attribute name="id">table_of_content_heading_1_2</attribute>
</xpath>
</template>
<!-- Form: define the default set of form fields -->
<template id="new_page_template_s_website_form" inherit_id="s_website_form" primary="True">
<xpath expr="//form" position="attributes">
<attribute name="data-success-mode">redirect</attribute>
<attribute name="data-success-page">/contactus-thank-you</attribute>
<attribute name="data-model_name">mail.mail</attribute>
</xpath>
<xpath expr="//div[hasclass('s_website_form_submit')]" position="before">
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_dnone">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content"></span>
</label>
<div class="col-sm">
<input type="hidden" class="form-control s_website_form_input" name="email_to" value="info@yourcompany.example.com"/>
</div>
</div>
</div>
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_custom s_website_form_required" data-type="char">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="obij2aulqyau">
<span class="s_website_form_label_content">Your Name</span>
<span class="s_website_form_mark"> *</span>
</label>
<div class="col-sm">
<input class="form-control s_website_form_input" type="text" name="name" required="1" data-fill-with="name" id="obij2aulqyau"/>
</div>
</div>
</div>
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_custom" data-type="tel">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="ozp7022vqhe">
<span class="s_website_form_label_content">Phone Number</span>
</label>
<div class="col-sm">
<input class="form-control s_website_form_input" type="tel" name="phone" data-fill-with="phone" id="ozp7022vqhe"/>
</div>
</div>
</div>
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_model_required" data-type="email">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="oub62hlfgjwf">
<span class="s_website_form_label_content">Your Email</span>
<span class="s_website_form_mark"> *</span>
</label>
<div class="col-sm">
<input class="form-control s_website_form_input" type="email" name="email_from" required="" data-fill-with="email" id="oub62hlfgjwf"/>
</div>
</div>
</div>
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_custom" data-type="char">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="o291di1too2s">
<span class="s_website_form_label_content">Your Company</span>
</label>
<div class="col-sm">
<input class="form-control s_website_form_input" type="text" name="company" data-fill-with="commercial_company_name" id="o291di1too2s"/>
</div>
</div>
</div>
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_model_required" data-type="char">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="oqsf4m51acj">
<span class="s_website_form_label_content">Subject</span>
<span class="s_website_form_mark"> *</span>
</label>
<div class="col-sm">
<input class="form-control s_website_form_input" type="text" name="subject" required="" id="oqsf4m51acj"/>
</div>
</div>
</div>
<div data-name="Field" class="s_website_form_field mb-3 col-12 s_website_form_custom s_website_form_required" data-type="text">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="oyeqnysxh10b">
<span class="s_website_form_label_content">Your Question</span>
<span class="s_website_form_mark"> *</span>
</label>
<div class="col-sm">
<textarea class="form-control s_website_form_input" name="description" required="1" id="oyeqnysxh10b" rows="3">
</textarea>
</div>
</div>
</div>
</xpath>
</template>
<!-- Map: define the embedded iframe -->
<template id="new_page_template_s_map" inherit_id="s_map" primary="True">
<xpath expr="//section" position="attributes">
<attribute name="class" add="o_half_screen_height" separator=" "/>
</xpath>
<xpath expr="//div[hasclass('s_map_color_filter')]" position="before">
<iframe class="s_map_embedded o_not_editable o_new_page_snippet_preview"
width="100%" height="100%" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"
src="https://maps.google.com/maps?q=250%20Executive%20Park%20Blvd%2C%20Suite%203400%20San%20Francisco%20California%20(US)%20United%20States&amp;t=m&amp;z=12&amp;ie=UTF8&amp;iwloc=&amp;output=embed">
</iframe>
</xpath>
</template>
<!-- Using `function` so that it is called during both module install and update -->
<!-- Generate primary snippet templates that are not predefined -->
<function model="ir.module.module" name="_generate_primary_snippet_templates">