[IMP] website, website_links: add Promote menu and SEO dialog

The media dialog is added in a later commit.

See merge commit for more information.

task-2687506
This commit is contained in:
Younn Olivier
2022-06-24 10:28:05 +02:00
committed by qsm-odoo
parent f260e3c144
commit ac55f2bb11
9 changed files with 676 additions and 1023 deletions
@@ -2,13 +2,13 @@
import { registry } from '@web/core/registry';
import { useService } from '@web/core/utils/hooks';
import { unslugHtmlDataObject } from '../../services/website_service';
const { Component, onWillStart, useRef, useEffect } = owl;
export class WebsitePreview extends Component {
setup() {
this.websiteService = useService('website');
this.title = useService('title');
this.iframeFallbackUrl = '/website/iframefallback';
@@ -24,7 +24,10 @@ export class WebsitePreview extends Component {
useEffect(() => {
this.websiteService.currentWebsiteId = this.websiteId;
this.websiteService.context.showNewContentModal = this.props.action.context.params && this.props.action.context.params.display_new_content;
return () => this.websiteService.currentWebsiteId = null;
return () => {
this.websiteService.currentWebsiteId = null;
this.websiteService.pageDocument = null;
};
}, () => [this.props.action.context.params]);
}
@@ -83,16 +86,11 @@ export class WebsitePreview extends Component {
// This replaces the browser url (/web#action=website...) with
// the iframe's url (it is clearer for the user).
this.currentUrl = this.iframe.el.contentDocument.location.href;
history.replaceState({}, this.props.action.display_name, this.currentUrl);
this.currentTitle = this.iframe.el.contentDocument.title;
history.replaceState({}, this.currentTitle, this.currentUrl);
this.title.setParts({ action: this.currentTitle });
const { mainObject, isPublished, canPublish, editableInBackend } = this.iframe.el.contentDocument.documentElement.dataset;
this.websiteService.currentMetadata = {
path: this.currentUrl,
mainObject: unslugHtmlDataObject(mainObject),
isPublished: isPublished === 'True',
canPublish: canPublish === 'True',
editableInBackend: editableInBackend === 'True',
};
this.websiteService.pageDocument = this.iframe.el.contentDocument;
// Before leaving the iframe, its content is replicated on an
// underlying iframe, to avoid for white flashes (visible on
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
.oe_seo_configuration {
#language-box {
padding-right: 25px;
background-color: white;
}
select {
background-position: bottom 50% right 1em;
}
.o_seo_og_image {
.o_meta_img {
position: relative;
transition: border-color 200ms;
display: inline-block;
border: 2px solid gray('400');
> img {
width: 70px;
height: 70px;
object-fit: cover;
cursor: pointer;
}
&:hover {
border-color: $o-brand-primary;
}
&.o_active_image {
border-color: $o-brand-primary;
&:before {
@include o-position-absolute($right: 0);
content: '';
border: 16px solid rgba($o-brand-primary, 0.8);
border-left-color: transparent;
border-bottom-color: transparent;
}
&:after {
@include o-position-absolute(2px, 3px);
display: inline-block;
content: "\f00c";
font-family: FontAwesome;
color: white;
font-size: 12px;
}
}
.o-custom-label {
@include o-position-absolute($bottom: 0px);
cursor: pointer;
background: rgba(gray('800'), 0.6);
font-size: 12px;
}
}
.o_meta_img_upload {
cursor: pointer;
transition: 200ms;
display: inline-block;
padding: 23px 27px;
border: 3px dashed lighten(gray('700'), 30%);
vertical-align: top;
color: lighten(gray('600'), 30%);
&:hover {
border-color: $o-brand-primary;
color: $o-brand-primary;
}
}
.o_meta_active_img {
height: 240px;
object-fit: cover;
}
}
div.oe_seo_preview_g {
list-style: none;
font-family: arial, sans-serif;
.r {
cursor: pointer;
color:#1a0dab;
font-size: 18px;
overflow: hidden;
text-overflow: ellipsis;
-webkit-text-overflow: ellipsis;
white-space: nowrap;
}
.s {
font-size: 13px;
line-height: 18px;
color: #545454;
.kv {
color: #006621;
font-size: 14px;
line-height: 18px;
}
}
}
td.o_seo_keyword_suggestion span.o_seo_suggestion.badge {
cursor: pointer;
}
}
@@ -1,118 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="Configurator.language_promote">
<t t-foreach="language" t-as="lang">
<option t-att-value="lang[0]" t-att-selected="lang[0] == def_lang ? 'selected' : null"><t t-esc="lang[2]" /></option>
</t>
</t>
<div t-name="website.seo_configuration" role="form">
<section class="js_seo_meta_title_description"/>
<section class="js_seo_meta_keywords"/>
<section class="js_seo_image"/>
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="website.MetaImage" owl="1">
<div t-att-class="props.active ? 'o_active_image o_meta_img m-1' : 'o_meta_img m-1'" t-on-click="() => props.selectImage(props.src)">
<span t-if="props.custom" class="o-custom-label w-100 text-white text-center">Custom</span>
<img t-att-src="props.src"/>
</div>
</t>
<t t-name="website.seo_suggestion_list">
<ul class="list-inline mb0">
<!-- filled in JS -->
</ul>
</t>
<t t-name="website.seo_list">
<tbody>
<!-- filled in JS -->
</tbody>
</t>
<t t-name="website.seo_keyword">
<tr class="js_seo_keyword" t-att-data-keyword="widget.keyword">
<td t-esc="widget.keyword"/>
<td class="text-center"><i t-if="widget.used_h1" class="fa fa-check" t-attf-title="{{ widget.keyword }} is used in page first level heading"/></td>
<td class="text-center"><i t-if="widget.used_h2" class="fa fa-check" t-attf-title="{{ widget.keyword }} is used in page second level heading"/></td>
<td class="text-center"><i class="js_seo_keyword_title fa fa-check" style="visibility: hidden;" t-attf-title="{{ widget.keyword }} is used in page title"/></td>
<td class="text-center"><i class="js_seo_keyword_description fa fa-check" style="visibility: hidden;" t-attf-title="{{ widget.keyword }} is used in page description"/></td>
<td class="text-center"><i t-if="widget.used_content" class="fa fa-check" t-attf-title="{{ widget.keyword }} is used in page content"/></td>
<td class="o_seo_keyword_suggestion"/>
<td class="text-center"><a href="#" class="oe_remove" data-action="remove-keyword" t-attf-title="Remove {{ widget.keyword }}"><i class="fa fa-trash"/></a></td>
</tr>
</t>
<t t-name="website.seo_suggestion">
<li class="list-inline-item">
<span class="o_seo_suggestion badge badge-info" t-att-data-keyword="widget.keyword" t-attf-title="Add {{ widget.keyword }}" t-esc="widget.keyword"/>
</li>
</t>
<t t-name="website.seo_preview">
<div class="oe_seo_preview_g">
<div class="rc">
<div class="r"><t t-esc="widget.title"/></div>
<div class="s">
<div class="kv"><t t-esc="widget.url"/></div>
<div class="st"><t t-esc="widget.description"/></div>
<t t-name="website.ImageSelector" owl="1">
<section>
<div class="o_seo_og_image">
<h4><small>Select an image for social share</small></h4>
<div class="row">
<div class="col-lg-6">
<t t-foreach="state.images" t-as="image" t-key="image.src">
<MetaImage active="image.active" custom="image.custom" src="image.src" selectImage="src => this.selectImage(src)"/>
</t>
<div class="o_meta_img_upload m-1" title="Click to choose more images">
<i class="fa fa-upload"/>
</div>
</div>
</div>
</div>
</t>
<div t-name="website.seo_meta_title_description">
<div class="row">
<div class="col-lg-6">
<div class="form-group">
<label for="website_meta_title">
Title <i class="fa fa-question-circle-o" title="The title will take a default value unless you specify one."/>
</label>
<input type="text" name="website_meta_title" id="website_meta_title" class="form-control" placeholder="Keep empty to use default value" maxlength="70" size="70"/>
</div>
<div class="form-group">
<label for="website_meta_description">
Description <i class="fa fa-question-circle-o" t-att-title="widget.previewDescription"/>
</label>
<textarea name="website_meta_description" id="website_meta_description" placeholder="Keep empty to use default value" class="form-control"/>
<div class="alert alert-warning mt16 mb0 small" id="website_meta_description_warning" style="display: none;"/>
</div>
<div class="form-group" t-if='widget.canEditUrl'>
<label for="website_seo_name">
Custom Url <i class="fa fa-question-circle-o" t-att-title="widget.seoNameHelp" />
</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text seo_name_pre"></span>
</div>
<input type="text" name="website_seo_name" id="website_seo_name" class="form-control" t-att-placeholder="widget.seoNameDefault" />
<div class="input-group-append" title="Unalterable unique identifier">
<span class="input-group-text seo_name_post"></span>
<div class="col-lg-6">
<div class="card p-0 mb16">
<div class="card-header">Social Preview</div>
<img class="card-img-top o_meta_active_img" t-att-src="activeMetaImage"/>
<div class="card-body px-3 py-2">
<h6 class="text-primary card-title mb0"><t t-esc="title"/></h6>
<small class="card-subtitle text-muted"><t t-esc="props.url"/></small>
<p t-esc="description"/>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card-header">Preview</div>
<div class="card mb0 p-0">
<div class="card-body">
<div class="js_seo_preview"/>
</div>
</div>
</div>
</div>
</div>
</section>
</t>
<t t-name="website.seo_meta_keywords">
<t t-name="website.Keyword" owl="1">
<tr>
<td t-esc="props.keyword"/>
<td class="text-center"><i t-if="usedInH1" class="fa fa-check" t-att-title="props.keyword + ' is used in page first level heading'"/></td>
<td class="text-center"><i t-if="usedInH2" class="fa fa-check" t-att-title="props.keyword + ' is used in page second level heading'"/></td>
<td class="text-center"><i t-if="usedInTitle" class="fa fa-check" t-att-title="props.keyword + ' is used in page title'"/></td>
<td class="text-center"><i t-if="usedInDescription" class=" fa fa-check" t-att-title="props.keyword + ' is used in page description'"/></td>
<td class="text-center"><i t-if="usedInContent" class="fa fa-check" t-att-title="props.keyword + ' is used in page content'"/></td>
<td class="o_seo_keyword_suggestion">
<ul class="list-inline mb0">
<span t-if="!state.suggestions.length">Loading...</span>
<t t-foreach="state.suggestions" t-as="suggestion" t-key="suggestion">
<li class="list-inline-item" t-on-click="() => props.addKeyword(suggestion)">
<span class="o_seo_suggestion badge badge-info" t-att-title="'Add ' + suggestion" t-esc="suggestion"/>
</li>
</t>
</ul>
</td>
<td class="text-center" t-on-click="() => props.removeKeyword(props.keyword)"><a href="#" class="oe_remove" t-att-title="'Remove ' + props.keyword"><i class="fa fa-trash"/></a></td>
</tr>
</t>
<t t-name="website.MetaKeywords" owl="1">
<section>
<label for="website_meta_keywords">
Keywords
</label>
<div class="form-inline" role="form">
<div class="input-group">
<input type="text" name="website_meta_keywords" id="website_meta_keywords" class="form-control" placeholder="Keyword" maxlength="30"/>
<input t-model="state.keyword" type="text" class="form-control" t-att-placeholder="isFull ? 'Remove a keyword first' : 'Keyword'" t-att-readonly="isFull" maxlength="30" t-on-keyup="onKeyup"/>
<span title="The language of the keyword and related keywords." class="input-group-append">
<select name="seo_page_language" id="language-box" class="btn form-control"/>
<select t-model="state.language" class="btn btn-outline-primary pr-5 form-control">
<t t-foreach="languages" t-as="lang" t-key="lang[0]">
<option t-att-value="lang[0]"><t t-esc="lang[2]"/></option>
</t>
</select>
</span>
<span class="input-group-append">
<button data-action="add" class="btn btn-primary btn-sm" type="button">Add</button>
<button t-on-click="() => this.addKeyword(state.keyword)" t-att-disabled="isFull" class="btn btn-primary btn-sm" type="button">Add</button>
</span>
</div>
</div>
<div class="table-responsive mt16">
<div t-if="seoContext.keywords.length" class="table-responsive mt16">
<table class="table table-sm">
<thead>
<tr>
@@ -126,43 +92,94 @@
<th class="text-center"></th>
</tr>
</thead>
<!-- body inserted in JS -->
<t t-foreach="seoContext.keywords" t-as="keyword" t-key="keyword">
<Keyword language="state.language" keyword="keyword" addKeyword="(keyword) => this.addKeyword(keyword)" removeKeyword="(keyword) => this.removeKeyword(keyword)"/>
</t>
</table>
</div>
</t>
</section>
</t>
<div t-name="website.seo_meta_image_selector" class="o_seo_og_image">
<t t-call="website.og_image_body"/>
<t t-name="website.SEOPreview" owl="1">
<div class="oe_seo_preview_g">
<div class="rc">
<t t-if="props.isIndexed">
<div class="r"><t t-esc="props.title"/></div>
<div class="s">
<div class="kv"><t t-esc="props.url"/></div>
<div class="st"><t t-esc="description"/></div>
</div>
</t>
<t t-else="">
<div class="s">
<div class="st">You have hidden this page from search results. It won't be indexed by search engines.</div>
</div>
</t>
</div>
</div>
</t>
<t t-name="website.og_image_body">
<h4><small>Select an image for social share</small></h4>
<t t-name="website.TitleDescription" owl="1">
<section>
<div class="row">
<div class="col-lg-6">
<t t-foreach="widget.images" t-as="image">
<div t-attf-class="o_meta_img mt4 #{new window.URL(image, window.location.origin).pathname === new window.URL(widget.activeMetaImg, window.location.origin).pathname and ' o_active_image' or ''}">
<img t-att-src="image"/>
</div>
</t>
<div t-if="widget.customImgUrl" t-attf-class="o_meta_img mt4 #{widget.customImgUrl === widget.activeMetaImg and ' o_active_image' or ''}">
<span class="o-custom-label w-100 text-white text-center">Custom</span>
<img t-att-src="widget.customImgUrl"/>
<div class="form-group">
<label for="website_meta_title">
Title <i class="fa fa-question-circle-o" title="The title will take a default value unless you specify one."/>
</label>
<input type="text" t-model="seoContext.title" t-att-disabled="!props.canEditTitle" class="form-control" placeholder="Keep empty to use default value" maxlength="70" size="70"/>
</div>
<div class="o_meta_img_upload mt4" title="Click to choose more images">
<i class="fa fa-upload"/>
<div class="form-group">
<label for="website_meta_description">
Description <i class="fa fa-question-circle-o" t-att-title="props.previewDescription"/>
</label>
<textarea t-model="seoContext.description" t-att-disabled="!props.canEditDescription" name="website_meta_description" placeholder="Keep empty to use default value" class="form-control"/>
<div t-if="descriptionWarning.length" class="alert alert-warning mt16 mb0 small">
<span t-esc="descriptionWarning"/>
</div>
</div>
<div class="form-group" t-if='props.canEditUrl'>
<label for="website_seo_name">
Custom Url <i class="fa fa-question-circle-o" t-att-title="props.seoNameHelp" />
</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text seo_name_pre" t-esc="seoNamePre"/>
</div>
<input type="text" t-model="seoContext.seoName" class="form-control" t-att-placeholder="props.seoNameDefault" />
<div class="input-group-append" title="Unalterable unique identifier">
<span class="input-group-text seo_name_post" t-esc="seoNamePost"/>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card p-0 mb16">
<div class="card-header">Social Preview</div>
<img class="card-img-top o_meta_active_img" t-att-src="widget.activeMetaImg"/>
<div class="card-body px-3 py-2">
<h6 class="text-primary card-title mb0"><t t-esc="widget.metaTitle"/></h6>
<small class="card-subtitle text-muted"><t t-esc="widget.serverUrl"/></small>
<p t-esc="widget.metaDescription"/>
</div>
<div class="card-header">Preview</div>
<div class="card mb0 p-0">
<div class="card-body">
<SEOPreview isIndexed="props.isIndexed" title="title" description="description" url="url"/>
</div>
</div>
</div>
</div>
</t>
</section>
</t>
<t t-name="website.OptimizeSEODialog" owl="1">
<TitleDescription canEditDescription="canEditDescription"
canEditUrl="canEditUrl"
canEditTitle="canEditTitle"
seoNameHelp="seoNameHelp"
seoNameDefault="seoNameDefault"
isIndexed="isIndexed"
defaultTitle="defaultTitle"
previewDescription="previewDescription"
url="url"/>
<MetaKeywords/>
<ImageSelector previewDescription="socialPreviewDescription"
defaultTitle="defaultTitle"
hasSocialDefaultImage="hasSocialDefaultImage"
pageImages="pageImages"
url="url"/>
</t>
</templates>
@@ -5,6 +5,7 @@ import { useService, useBus } from '@web/core/utils/hooks';
import { registry } from "@web/core/registry";
import { patch } from 'web.utils';
import { EditMenuDialog } from '@website/components/dialog/edit_menu';
import { OptimizeSEODialog } from '@website/components/dialog/seo';
const websiteSystrayRegistry = registry.category('website_systray');
@@ -26,14 +27,21 @@ patch(NavBar.prototype, 'website_navbar', {
useBus(websiteSystrayRegistry, 'CONTENT-UPDATED', () => this.render(true));
this.websiteDialogMenus = {
'website.menu_edit_menu': EditMenuDialog,
'website.menu_edit_menu': {
component: EditMenuDialog,
isDisplayed: () => !!this.websiteService.currentWebsite,
},
'website.menu_optimize_seo': {
component: OptimizeSEODialog,
isDisplayed: () => this.websiteService.currentWebsite && !!this.websiteService.currentWebsite.metadata.mainObject,
},
};
},
filterWebsiteMenus(sections) {
const filteredSections = [];
for (const section of sections) {
if (!this.websiteDialogMenus[section.xmlid]) {
if (!this.websiteDialogMenus[section.xmlid] || this.websiteDialogMenus[section.xmlid].isDisplayed()) {
let subSections = [];
if (section.childrenTree.length) {
subSections = this.filterWebsiteMenus(section.childrenTree);
@@ -63,7 +71,7 @@ patch(NavBar.prototype, 'website_navbar', {
*/
get currentAppSections() {
const currentAppSections = this._super();
if (this.currentApp && this.currentApp.xmlid === 'website.menu_website_configuration' && !this.websiteService.currentWebsite) {
if (this.currentApp && this.currentApp.xmlid === 'website.menu_website_configuration') {
return this.filterWebsiteMenus(currentAppSections).filter(section => section.childrenTree.length);
}
return currentAppSections;
@@ -74,7 +82,7 @@ patch(NavBar.prototype, 'website_navbar', {
*/
onNavBarDropdownItemSelection(menu) {
if (this.websiteDialogMenus[menu.xmlid]) {
return this.dialogService.add(this.websiteDialogMenus[menu.xmlid]);
return this.dialogService.add(this.websiteDialogMenus[menu.xmlid].component);
}
return this._super(menu);
}
@@ -26,6 +26,7 @@ export const websiteService = {
let currentWebsiteId;
let currentMetadata = {};
let fullscreen;
let pageDocument;
const context = reactive({
showNewContentModal: false,
});
@@ -51,10 +52,6 @@ export const websiteService = {
set currentWebsiteId(id) {
setCurrentWebsiteId(id);
},
set currentMetadata(metadata) {
currentMetadata = metadata;
websiteSystrayRegistry.trigger('CONTENT-UPDATED');
},
get currentWebsite() {
const currentWebsite = websites.find(w => w.id === currentWebsiteId);
if (currentWebsite) {
@@ -68,6 +65,26 @@ export const websiteService = {
get context() {
return context;
},
set pageDocument(document) {
pageDocument = document;
if (!document) {
return;
}
const { mainObject, seoObject, isPublished, canPublish, editableInBackend } = document.documentElement.dataset;
currentMetadata = {
path: document.location.href,
mainObject: unslugHtmlDataObject(mainObject),
seoObject: unslugHtmlDataObject(seoObject),
isPublished: isPublished === 'True',
canPublish: canPublish === 'True',
editableInBackend: editableInBackend === 'True',
title: document.title,
};
websiteSystrayRegistry.trigger('CONTENT-UPDATED');
},
get pageDocument() {
return pageDocument;
},
goToWebsite({ websiteId = currentWebsiteId || websites[0].id, path = '/' } = {}) {
action.doAction('website.website_preview', {
clearBreadcrumbs: true,
@@ -64,10 +64,6 @@
</div>
<!-- Promote -->
<div class="dropdown" id="promote-menu">
<a class="dropdown-toggle o-no-caret waves" data-toggle="dropdown" data-display="static" href="#">Promote</a>
<div class="dropdown-menu oe_promote_menu" role="menu">
<a role="menuitem" data-action="promote-current-page" href="#" title="Promote page on the web" class="dropdown-item">Optimize SEO</a>
</div>
</div>
<t t-call="website.website_publisher" groups="website.group_website_publisher">
<t t-set="extra_classes" t-translation="off">d-flex d-sm-flex d-md-none</t>
+11
View File
@@ -535,6 +535,11 @@
sequence="1"
parent="website.menu_website_configuration"/>
<menuitem id="menu_promote"
name="Promote"
sequence="5"
parent="website.menu_website_configuration"/>
<menuitem id="menu_reporting"
name="Reporting"
sequence="99"
@@ -571,6 +576,12 @@
parent="menu_reporting"
action="website.ir_actions_server_website_google_analytics"/>
<menuitem id="menu_optimize_seo"
name="Optimize SEO"
sequence="1"
parent="website.menu_promote"
action="website.website_preview"/>
<!-- Configurator actions -->
<record id="start_configurator_act_url" model="ir.actions.act_url">
<field name="name">Website Configurator</field>
@@ -2,7 +2,7 @@
<odoo>
<template id="share_page_menu" inherit_id="website.user_navbar">
<xpath expr="//a[@data-action='promote-current-page']" position="after">
<xpath expr="//div[@id='promote-menu']" position="inside">
<a href="/r" id="o_website_links_share_page" class="dropdown-item">
<span title="Track this page to count clicks">Link Tracker</span>
</a>