Skip to content

Commit ba55204

Browse files
robinlejFrancoisGe
authored andcommitted
Add New Content systray
1 parent bb9a5ca commit ba55204

13 files changed

+398
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component } from "@odoo/owl";
2+
import { _t } from "@web/core/l10n/translation";
3+
import { WebsiteDialog } from "@website/components/dialog/dialog";
4+
5+
export class InstallModuleDialog extends Component {
6+
static components = { WebsiteDialog };
7+
static template = "html_builder.InstallModuleDialog";
8+
static props = {
9+
title: String,
10+
installationText: String,
11+
installModule: Function,
12+
close: Function,
13+
};
14+
15+
setup() {
16+
this.installButtonTitle = _t("Install");
17+
}
18+
19+
onClickInstall() {
20+
this.props.close();
21+
this.props.installModule();
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="html_builder.InstallModuleDialog">
5+
<WebsiteDialog close="props.close"
6+
title="props.title"
7+
primaryTitle="installButtonTitle"
8+
primaryClick="() => this.onClickInstall()">
9+
<t t-esc="props.installationText"/>
10+
</WebsiteDialog>
11+
</t>
12+
13+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Component } from "@odoo/owl";
2+
3+
export const MODULE_STATUS = {
4+
NOT_INSTALLED: "NOT_INSTALLED",
5+
INSTALLING: "INSTALLING",
6+
FAILED_TO_INSTALL: "FAILED_TO_INSTALL",
7+
INSTALLED: "INSTALLED",
8+
};
9+
10+
export class NewContentElement extends Component {
11+
static template = "html_builder.NewContentElement";
12+
static props = {
13+
name: { type: String, optional: true },
14+
title: String,
15+
onClick: Function,
16+
status: { type: String, optional: true },
17+
moduleXmlId: { type: String, optional: true },
18+
slots: Object,
19+
};
20+
static defaultProps = {
21+
status: MODULE_STATUS.INSTALLED,
22+
};
23+
24+
setup() {
25+
this.MODULE_STATUS = MODULE_STATUS;
26+
}
27+
28+
onClick(ev) {
29+
this.props.onClick();
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="html_builder.NewContentElement">
5+
<div class="o_new_content_element col-md-4 mb8" t-att-name="props.name">
6+
<button
7+
t-on-click.prevent.stop="onClick"
8+
class="btn w-100"
9+
t-att-class="props.status === MODULE_STATUS.NOT_INSTALLED ? 'o_uninstalled_module' : ''"
10+
t-att-title="props.title"
11+
t-att-aria-label="props.title"
12+
t-att-data-module-xml-id="props.moduleXmlId">
13+
<t t-slot="default"/>
14+
</button>
15+
</div>
16+
</t>
17+
18+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { InstallModuleDialog } from "@html_builder/website_preview/install_module_dialog";
2+
import {
3+
MODULE_STATUS,
4+
NewContentElement,
5+
} from "@html_builder/website_preview/new_content_element";
6+
import { Component, onWillStart, useState, xml } from "@odoo/owl";
7+
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
8+
import { _t } from "@web/core/l10n/translation";
9+
import { rpc } from "@web/core/network/rpc";
10+
import { useActiveElement } from "@web/core/ui/ui_service";
11+
import { user } from "@web/core/user";
12+
import { useService } from "@web/core/utils/hooks";
13+
import { sprintf } from "@web/core/utils/strings";
14+
import { redirect } from "@web/core/utils/urls";
15+
16+
export class NewContentModal extends Component {
17+
static template = "html_builder.NewContentModal";
18+
static components = { NewContentElement };
19+
static props = {
20+
onNewPage: Function,
21+
};
22+
23+
setup() {
24+
this.orm = useService("orm");
25+
this.dialogs = useService("dialog");
26+
this.website = useService("website");
27+
this.action = useService("action");
28+
this.isSystem = user.isSystem;
29+
useActiveElement("modalRef");
30+
31+
this.newContentText = {
32+
failed: _t('Failed to install "%s"'),
33+
installInProgress: _t("The installation of an App is already in progress."),
34+
installNeeded: _t('Do you want to install the "%s" App?'),
35+
installPleaseWait: _t('Installing "%s"'),
36+
};
37+
38+
this.state = useState({
39+
newContentElements: [
40+
{
41+
moduleName: "website_blog",
42+
moduleXmlId: "base.module_website_blog",
43+
status: MODULE_STATUS.NOT_INSTALLED,
44+
icon: xml`<i class="fa fa-newspaper-o"/>`,
45+
title: _t("Blog Post"),
46+
},
47+
{
48+
moduleName: "website_event",
49+
moduleXmlId: "base.module_website_event",
50+
status: MODULE_STATUS.NOT_INSTALLED,
51+
icon: xml`<i class="fa fa-ticket"/>`,
52+
title: _t("Event"),
53+
},
54+
{
55+
moduleName: "website_forum",
56+
moduleXmlId: "base.module_website_forum",
57+
status: MODULE_STATUS.NOT_INSTALLED,
58+
icon: xml`<i class="fa fa-comment"/>`,
59+
redirectUrl: "/forum",
60+
title: _t("Forum"),
61+
},
62+
{
63+
moduleName: "website_hr_recruitment",
64+
moduleXmlId: "base.module_website_hr_recruitment",
65+
status: MODULE_STATUS.NOT_INSTALLED,
66+
icon: xml`<i class="fa fa-briefcase"/>`,
67+
title: _t("Job Position"),
68+
},
69+
{
70+
moduleName: "website_sale",
71+
moduleXmlId: "base.module_website_sale",
72+
status: MODULE_STATUS.NOT_INSTALLED,
73+
icon: xml`<i class="fa fa-shopping-cart"/>`,
74+
title: _t("Product"),
75+
},
76+
{
77+
moduleName: "website_slides",
78+
moduleXmlId: "base.module_website_slides",
79+
status: MODULE_STATUS.NOT_INSTALLED,
80+
icon: xml`<i class="fa module_icon" style="background-image: url('/website/static/src/img/apps_thumbs/website_slide.svg');background-repeat: no-repeat; background-position: center;"/>`,
81+
title: _t("Course"),
82+
},
83+
{
84+
moduleName: "website_livechat",
85+
moduleXmlId: "base.module_website_livechat",
86+
status: MODULE_STATUS.NOT_INSTALLED,
87+
icon: xml`<i class="fa fa-comments"/>`,
88+
title: _t("Livechat Widget"),
89+
redirectUrl: "/livechat",
90+
},
91+
],
92+
});
93+
94+
this.websiteContext = useState(this.website.context);
95+
useHotkey("escape", () => {
96+
if (this.websiteContext.showNewContentModal) {
97+
this.websiteContext.showNewContentModal = false;
98+
}
99+
});
100+
101+
onWillStart(this.onWillStart.bind(this));
102+
}
103+
104+
async onWillStart() {
105+
this.isDesigner = await user.hasGroup("website.group_website_designer");
106+
this.canInstall = await user.isAdmin;
107+
if (this.canInstall) {
108+
const moduleNames = this.state.newContentElements
109+
.filter(({ status }) => status === MODULE_STATUS.NOT_INSTALLED)
110+
.map(({ moduleName }) => moduleName);
111+
this.modulesInfo = {};
112+
for (const record of await this.orm.searchRead(
113+
"ir.module.module",
114+
[["name", "in", moduleNames]],
115+
["id", "name", "shortdesc"]
116+
)) {
117+
this.modulesInfo[record.name] = { id: record.id, name: record.shortdesc };
118+
}
119+
}
120+
const modelsToCheck = [];
121+
const elementsToUpdate = {};
122+
for (const element of this.state.newContentElements) {
123+
if (element.model) {
124+
modelsToCheck.push(element.model);
125+
elementsToUpdate[element.model] = element;
126+
}
127+
}
128+
const accesses = await rpc("/website/check_new_content_access_rights", {
129+
models: modelsToCheck,
130+
});
131+
for (const [model, access] of Object.entries(accesses)) {
132+
elementsToUpdate[model].isDisplayed = access;
133+
}
134+
}
135+
136+
get sortedNewContentElements() {
137+
return this.state.newContentElements
138+
.filter(({ status }) => status !== MODULE_STATUS.NOT_INSTALLED)
139+
.concat(
140+
this.state.newContentElements.filter(
141+
({ status }) => status === MODULE_STATUS.NOT_INSTALLED
142+
)
143+
);
144+
}
145+
146+
async installModule(id, redirectUrl) {
147+
await this.orm.silent.call("ir.module.module", "button_immediate_install", [id]);
148+
if (redirectUrl) {
149+
this.website.prepareOutLoader();
150+
window.location.replace(redirectUrl);
151+
} else {
152+
const {
153+
id,
154+
metadata: { path, viewXmlid },
155+
} = this.website.currentWebsite;
156+
const url = new URL(path);
157+
if (viewXmlid === "website.page_404") {
158+
url.pathname = "";
159+
}
160+
// A reload is needed after installing a new module, to instantiate
161+
// a NewContentModal with patches from the installed module.
162+
this.website.prepareOutLoader();
163+
redirect(
164+
`/odoo/action-website.website_preview?website_id=${id}&path=${encodeURIComponent(
165+
url.toString()
166+
)}&display_new_content=true`
167+
);
168+
}
169+
}
170+
171+
onClickNewContent(element) {
172+
if (element.createNewContent) {
173+
return element.createNewContent();
174+
}
175+
176+
const { id, name } = this.modulesInfo[element.moduleName];
177+
const dialogProps = {
178+
title: element.title,
179+
installationText: sprintf(this.newContentText.installNeeded, name),
180+
installModule: async () => {
181+
// Update the NewContentElement with installing icon and text.
182+
this.state.newContentElements = this.state.newContentElements.map((el) => {
183+
if (el.moduleXmlId === element.moduleXmlId) {
184+
el.status = MODULE_STATUS.INSTALLING;
185+
el.icon = xml`<i class="fa fa-spin fa-circle-o-notch"/>`;
186+
el.title = sprintf(this.newContentText.installPleaseWait, name);
187+
}
188+
return el;
189+
});
190+
this.website.showLoader({ title: _t("Building your %s", name) });
191+
try {
192+
await this.installModule(id, element.redirectUrl);
193+
} catch (error) {
194+
this.website.hideLoader();
195+
// Update the NewContentElement with failure icon and text.
196+
this.state.newContentElements = this.state.newContentElements.map((el) => {
197+
if (el.moduleXmlId === element.moduleXmlId) {
198+
el.status = MODULE_STATUS.FAILED_TO_INSTALL;
199+
el.icon = xml`<i class="fa fa-exclamation-triangle"/>`;
200+
el.title = sprintf(this.newContentText.failed, name);
201+
}
202+
return el;
203+
});
204+
console.error(error);
205+
}
206+
},
207+
};
208+
this.dialogs.add(InstallModuleDialog, dialogProps);
209+
}
210+
211+
/**
212+
* This method registers the action to perform when a new content is
213+
* saved. The path must be computed once the record is saved, to
214+
* perform the 'ir.act_window_close' action, which will be used when
215+
* the dialog is closed to go to the correct website page.
216+
*/
217+
async onAddContent(action, edition = false, context = null) {
218+
this.action.doAction(action, {
219+
additionalContext: context ? context : {},
220+
onClose: (infos) => {
221+
if (infos) {
222+
this.website.goToWebsite({ path: infos.path, edition: edition });
223+
}
224+
},
225+
props: {
226+
onSave: (record, params) => {
227+
if (record.resId) {
228+
const path = params.computePath();
229+
this.action.doAction({
230+
type: "ir.actions.act_window_close",
231+
infos: { path },
232+
});
233+
}
234+
},
235+
},
236+
});
237+
}
238+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="html_builder.NewContentModal">
5+
<div id="o_new_content_menu_choices" t-ref="modalRef" role="dialog" aria-modal="true" aria-label="New Content" tabindex="-1">
6+
<div class="container pt32 pb32">
7+
<div class="row">
8+
<NewContentElement t-if="isDesigner"
9+
name.translate="New Page"
10+
onClick="() => props.onNewPage()"
11+
title.translate="New Page">
12+
<i class="fa fa-file-o"/>
13+
<p>Page</p>
14+
</NewContentElement>
15+
16+
<t t-foreach="sortedNewContentElements" t-as="element" t-key="element.moduleXmlId" t-if="'isDisplayed' in element ? element.isDisplayed : isSystem ">
17+
<NewContentElement onClick="() => this.onClickNewContent(element)"
18+
status="element.status"
19+
title="element.title"
20+
moduleXmlId="element.moduleXmlId">
21+
<t t-call="{{ element.icon }}"/>
22+
<p><t t-esc="element.title"/></p>
23+
</NewContentElement>
24+
</t>
25+
</div>
26+
</div>
27+
</div>
28+
</t>
29+
30+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NewContentModal } from "@html_builder/website_preview/new_content_modal";
2+
import { Component, useState } from "@odoo/owl";
3+
import { useService } from "@web/core/utils/hooks";
4+
5+
export class NewContentSystrayItem extends Component {
6+
static template = "html_builder.NewContentSystrayItem";
7+
static components = { NewContentModal };
8+
static props = {
9+
onNewPage: Function,
10+
};
11+
12+
setup() {
13+
this.website = useService("website");
14+
this.websiteContext = useState(this.website.context);
15+
}
16+
17+
onClick() {
18+
this.websiteContext.showNewContentModal = !this.websiteContext.showNewContentModal;
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="html_builder.NewContentSystrayItem">
5+
<div class="o_menu_systray_item o_new_content_container d-none d-md-flex" t-on-click="onClick">
6+
<button accesskey="c" class="o-website-btn-custo-secondary btn d-flex align-items-center rounded-0 border-0 px-3">New</button>
7+
<NewContentModal t-if="websiteContext.showNewContentModal" onNewPage="props.onNewPage"/>
8+
</div>
9+
</t>
10+
11+
</templates>

0 commit comments

Comments
 (0)