[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:
committed by
Romain Derie
co-authored by
stefanorigano
parent
928eeca714
commit
e0796020ee
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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&t=m&z=12&ie=UTF8&iwloc=&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">
|
||||
|
||||
Reference in New Issue
Block a user