diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 46baaeed00b..47eec868503 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -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'{groups_html}')
+ 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)
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index f64bd14803b..fcd58c69432 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -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'{sections_arch}'):
+ 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
diff --git a/addons/website/static/src/components/dialog/add_page_dialog.js b/addons/website/static/src/components/dialog/add_page_dialog.js
new file mode 100644
index 00000000000..783344168e1
--- /dev/null
+++ b/addons/website/static/src/components/dialog/add_page_dialog.js
@@ -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";
diff --git a/addons/website/static/src/components/dialog/add_page_dialog.scss b/addons/website/static/src/components/dialog/add_page_dialog.scss
new file mode 100644
index 00000000000..43d39c7304e
--- /dev/null
+++ b/addons/website/static/src/components/dialog/add_page_dialog.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/addons/website/static/src/components/dialog/add_page_dialog.xml b/addons/website/static/src/components/dialog/add_page_dialog.xml
new file mode 100644
index 00000000000..b9ef5466bbe
--- /dev/null
+++ b/addons/website/static/src/components/dialog/add_page_dialog.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Blank Page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/website/static/src/components/dialog/dialog.js b/addons/website/static/src/components/dialog/dialog.js
index 31962cfcef3..468d4ac3063 100644
--- a/addons/website/static/src/components/dialog/dialog.js
+++ b/addons/website/static/src/components/dialog/dialog.js
@@ -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`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/addons/website/static/src/components/dialog/dialog.xml b/addons/website/static/src/components/dialog/dialog.xml
index 2b83a1ccb62..77ece7edbb0 100644
--- a/addons/website/static/src/components/dialog/dialog.xml
+++ b/addons/website/static/src/components/dialog/dialog.xml
@@ -2,13 +2,14 @@
+
diff --git a/addons/website/static/src/components/views/page_views_mixin.js b/addons/website/static/src/components/views/page_views_mixin.js
index 501f4b510d7..a394930981d 100644
--- a/addons/website/static/src/components/views/page_views_mixin.js
+++ b/addons/website/static/src/components/views/page_views_mixin.js
@@ -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) {
diff --git a/addons/website/static/src/systray_items/new_content.js b/addons/website/static/src/systray_items/new_content.js
index 1f09a9094af..607c5e2be68 100644
--- a/addons/website/static/src/systray_items/new_content.js
+++ b/addons/website/static/src/systray_items/new_content.js
@@ -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,
});
}
diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js
index c4ade3ac829..ade431016cb 100644
--- a/addons/website/static/tests/tours/rte.js
+++ b/addons/website/static/tests/tours/rte.js
@@ -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"]',
diff --git a/addons/website/views/new_page_template_templates.xml b/addons/website/views/new_page_template_templates.xml
index 83017b75f5f..a9bc35b69fa 100644
--- a/addons/website/views/new_page_template_templates.xml
+++ b/addons/website/views/new_page_template_templates.xml
@@ -1,6 +1,153 @@
+
+
+ Basic
+ About
+ Landing Pages
+ Gallery
+ Services
+ Pricing Plans
+ Team
+
+
+
+
+
+
+
+
+
+ top: 76px; max-height: calc(100vh - 96px);
+
+
+ Intuitive system
+ Design features
+
+
+ table_of_content_heading_1_1
+
+
+ table_of_content_heading_1_2
+
+
+
+
+
+
+ redirect
+ /contactus-thank-you
+ mail.mail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+