odoo18/addons_extensions/knowledge/static/src/js/knowledge_controller.js

261 lines
8.4 KiB
JavaScript

/** @odoo-module **/
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { FormController } from '@web/views/form/form_controller';
import { KnowledgeSidebar } from '@knowledge/components/sidebar/sidebar';
import { useBus, useService } from "@web/core/utils/hooks";
import { Deferred } from "@web/core/utils/concurrency";
import {
onMounted,
onWillStart,
useChildSubEnv,
useEffect,
useExternalListener,
useRef,
} from "@odoo/owl";
export class KnowledgeArticleFormController extends FormController {
static template = "knowledge.ArticleFormView";
// Open articles in edit mode by default
static defaultProps = {
...FormController.defaultProps,
mode: "edit",
};
static components = {
...FormController.components,
KnowledgeSidebar,
};
setup() {
super.setup();
this.root = useRef('root');
this.orm = useService('orm');
this.actionService = useService('action');
this.dialogService = useService("dialog");
/*
Because of the way OWL is designed we are never sure when OWL finishes mounting this component.
Thus, we added this deferred promise in order for us to know when it is done.
It is necessary to have this because the comments handler needs to notify the topbar when
it has detected comments so that it can show the comments panel's button.
*/
this.topbarMountedPromise = new Deferred();
useChildSubEnv({
createArticle: this.createArticle.bind(this),
ensureArticleName: this.ensureArticleName.bind(this),
openArticle: this.openArticle.bind(this),
renameArticle: this.renameArticle.bind(this),
toggleAsideMobile: this.toggleAsideMobile.bind(this),
topbarMountedPromise: this.topbarMountedPromise,
save: this.save.bind(this),
discard: this.discard.bind(this),
});
useBus(this.env.bus, 'KNOWLEDGE:OPEN_ARTICLE', (event) => {
this.openArticle(event.detail.id);
});
// Unregister the current candidate recordInfo for Knowledge macros in
// case of breadcrumbs mismatch.
onWillStart(() => {
if (
!this.env.inDialog &&
this.env.config.breadcrumbs &&
this.env.config.breadcrumbs.length
) {
// Unregister the current candidate recordInfo in case of
// breadcrumbs mismatch.
this.knowledgeCommandsService.unregisterCommandsRecordInfo(this.env.config.breadcrumbs);
}
});
onMounted(() => {
this.topbarMountedPromise.resolve();
});
useExternalListener(document.documentElement, 'mouseleave', async () => {
if (await this.model.root.isDirty()) {
await this.model.root.save();
}
});
useEffect(
() => {
const scrollView = this.root.el?.querySelector(".o_scroll_view_lg");
if (scrollView) {
scrollView.scrollTop = 0;
}
const mobileScrollView = this.root.el?.querySelector(".o_knowledge_main_view");
if (mobileScrollView) {
mobileScrollView.scrollTop = 0;
}
},
() => [this.model.root.resId]
);
}
/**
* Ensure that the title is set @see beforeUnload
* Dirty check is sometimes necessary in cases where the user leaves
* the article from inside an article (i.e. embedded views/links) very
* shortly after a mutation (i.e. in tours). At that point, the
* html_field may not have notified the model from the change.
* @override
*/
async beforeLeave() {
if (this.model.root.resId) {
await this.ensureArticleName();
}
await this.model.root.isDirty();
return super.beforeLeave();
}
/**
* Check that the title is set or not before closing the tab and
* save the whole article, if the current article exists (it does
* not exist if there are no articles to show, in which case the no
* content helper is displayed).
* @override
*/
async beforeUnload(ev) {
if (this.model.root.resId) {
await this.ensureArticleName();
if (await this.model.root.isDirty()) {
await super.beforeUnload(ev); // triggers an urgent save
}
}
}
/**
* If the article has no name set, tries to rename it.
*/
ensureArticleName() {
const recordData = this.model.root.data;
if (
!recordData.name &&
!(recordData.is_locked || !recordData.user_can_write || !recordData.active)
) {
return this.renameArticle();
}
}
get resId() {
return this.model.root.resId;
}
/**
* Create a new article and open it.
* @param {String} category - Category of the new article
* @param {integer} targetParentId - Id of the parent of the new article (optional)
*/
async createArticle(category, targetParentId) {
const articleId = await this.orm.call(
"knowledge.article",
"article_create",
[],
{
is_private: category === 'private',
parent_id: targetParentId ? targetParentId : false
}
);
this.openArticle(articleId);
}
getHtmlTitle() {
const titleEl = this.root.el.querySelector('#body_0 h1');
if (titleEl) {
const title = titleEl.textContent.trim();
if (title) {
return title;
}
}
}
displayName() {
return this.model.root.data.name || _t("New");
}
/**
* Callback executed before the record save (if the record is valid).
* When an article has no name set, use the title (first h1 in the
* body) to try to save the article with a name.
* @overwrite
*/
async onWillSaveRecord(record, changes) {
if (!record.data.name) {
const title = this.getHtmlTitle();
if (title) {
changes.name = title;
}
}
}
/**
* @param {integer} - resId: id of the article to open
*/
async openArticle(resId) {
if (!resId || resId === this.resId) {
return;
}
// blur to remove focus on the active element
document.activeElement.blur();
// load the new record
try {
if (this.model.root.isNew) {
await this.model.load({ resId });
} else {
await this.ensureArticleName();
if (await this.model.root.isDirty()) {
await this.model.root.save({
onError: this.onSaveError.bind(this),
nextId: resId,
});
} else {
await this.model.load({ resId });
}
}
} catch {
this.dialogService.add(AlertDialog, {
title: _t("Access Denied"),
body: _t(
"The article you are trying to open has either been removed or is inaccessible.",
),
confirmLabel: _t("Close"),
});
}
this.toggleAsideMobile(false);
}
/*
* Rename the article using the given name, or using the article title if
* no name is given (first h1 in the body). If no title is found, the
* article is kept untitled.
* @param {string} name - new name of the article
*/
renameArticle(name) {
if (!name) {
const title = this.root.el.querySelector(".note-editable.odoo-editor-editable h1");
if (title) {
name = title.textContent.trim();
if (!name) {
return;
}
}
}
return this.model.root.update({ name });
}
/**
* Toggle the aside menu on mobile devices (< 576px).
* @param {boolean} force
*/
toggleAsideMobile(force) {
const container = this.root.el.querySelector('.o_knowledge_form_view');
container.classList.toggle('o_toggle_aside', force);
}
}