[REF] website: view_hierarchy: refactor to client action
Before this commit, `view_hierarchy` extended the QWeb view to see the hierarchy of views. With the evolution of the javascript framework, this use case has now more sense in a client action rather than an extension of a QWeb view. This commit refactors `view_hierarchy` into a client action. Part-of: odoo/odoo#103477
This commit is contained in:
@@ -236,9 +236,31 @@ class View(models.Model):
|
||||
})
|
||||
page.menu_ids.filtered(lambda m: m.website_id.id == website.id).page_id = new_page.id
|
||||
|
||||
def _get_top_level_view(self):
|
||||
def get_view_hierarchy(self):
|
||||
self.ensure_one()
|
||||
return self.inherit_id._get_top_level_view() if self.inherit_id else self
|
||||
top_level_view = self
|
||||
while top_level_view.inherit_id:
|
||||
top_level_view = top_level_view.inherit_id
|
||||
top_level_view = top_level_view.with_context(active_test=False)
|
||||
sibling_views = top_level_view.search_read([('key', '=', top_level_view.key), ('id', '!=', top_level_view.id)])
|
||||
return {
|
||||
'sibling_views': sibling_views,
|
||||
'hierarchy': top_level_view._build_hierarchy_datastructure()
|
||||
}
|
||||
|
||||
def _build_hierarchy_datastructure(self):
|
||||
inherit_children = []
|
||||
for child in self.inherit_children_ids:
|
||||
inherit_children.append(child._build_hierarchy_datastructure())
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'inherit_children': inherit_children,
|
||||
'arch_updated': self.arch_updated,
|
||||
'website_name': self.website_id.name if self.website_id else False,
|
||||
'active': self.active,
|
||||
'key': self.key,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_related_views(self, key, bundles=False):
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
odoo.define('website.view_hierarchy', function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require('web.core');
|
||||
const qweb = require('web.qweb');
|
||||
const viewRegistry = require('web.view_registry');
|
||||
|
||||
const _t = core._t;
|
||||
|
||||
const Renderer = qweb.Renderer.extend({
|
||||
events: _.extend({}, qweb.Renderer.prototype.events, {
|
||||
'click .js_fold': '_onCollapseClick',
|
||||
'click .o_website_filter a': '_onWebsiteFilterClick',
|
||||
'click .o_search button': '_onSearchButtonClick',
|
||||
'click .o_show_diff': '_onShowDiffClick',
|
||||
'click .o_load_hierarchy': '_onLoadHierarchyClick',
|
||||
'keydown .o_search input': '_onSearchInputKeyDown',
|
||||
'input .o_search input': '_onSearchInputKeyInput',
|
||||
'change #o_show_inactive': '_onShowActiveClick',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super(...arguments);
|
||||
|
||||
// Search
|
||||
this.cptFound = 0;
|
||||
this.prevSearch = '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this._super(...arguments);
|
||||
|
||||
const self = this;
|
||||
this._handleLastVisibleChild();
|
||||
// Fixed Navbar
|
||||
this.$('.o_tree_container').css({
|
||||
'padding-top': this.$('.o_tree_nav').outerHeight() + 10,
|
||||
});
|
||||
// Website Filters
|
||||
this.$wNodes = this.$("li[data-website_name]");
|
||||
this.$notwNodes = this.$("li:not([data-website_name])");
|
||||
const websiteNames = _.uniq($.map(self.$wNodes, el => el.getAttribute('data-website_name')));
|
||||
for (const websiteName of websiteNames) {
|
||||
this.$('.o_website_filter').append($('<a/>', {
|
||||
'class': 'dropdown-item',
|
||||
'data-website_name': websiteName,
|
||||
'text': websiteName,
|
||||
}));
|
||||
}
|
||||
this.$(`.o_website_filter a[data-website_name="${websiteNames[0] || '*'}"]`).click();
|
||||
// Highlight requested view as google does
|
||||
const reqViewId = this.$('.o_tree_container').data('requested-view-id');
|
||||
const $reqView = $(`[data-id="${reqViewId}"] span.js_fold`).first();
|
||||
$reqView.css({'background-color': 'yellow'});
|
||||
$('.o_content').scrollTo($reqView[0], 300, {offset: -200});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onCollapseClick: function (ev) {
|
||||
const $parent = $(ev.currentTarget).parent();
|
||||
const folded = $parent.find('.o_fold_icon').hasClass('fa-plus-square-o');
|
||||
let $ul, $oFoldIcon;
|
||||
if (folded) { // Unfold only self
|
||||
$ul = $parent.siblings('ul');
|
||||
$oFoldIcon = $parent.find('.o_fold_icon');
|
||||
} else { // Fold all
|
||||
$ul = $parent.parent().find('ul');
|
||||
$oFoldIcon = $parent.parent().find('.o_fold_icon');
|
||||
}
|
||||
$ul.toggleClass('d-none', !folded);
|
||||
$oFoldIcon.toggleClass('fa-minus-square-o', folded).toggleClass('fa-plus-square-o', !folded);
|
||||
this._handleLastVisibleChild();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onShowActiveClick: function (ev) {
|
||||
this.$('.o_is_inactive').toggleClass('d-none', !ev.currentTarget.checked);
|
||||
this._handleLastVisibleChild();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onWebsiteFilterClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
// Update Dropdown Filter
|
||||
const $el = $(ev.currentTarget);
|
||||
$el.addClass('active').siblings().removeClass('active');
|
||||
$el.parent().siblings('.dropdown-toggle').text($el.text());
|
||||
// Show all views
|
||||
const websiteName = $el.data('website_name');
|
||||
this.$wNodes.add(this.$notwNodes).removeClass('d-none');
|
||||
if (websiteName !== '*') {
|
||||
// Hide all website views
|
||||
this.$wNodes.addClass('d-none');
|
||||
// Show selected website views
|
||||
const $selectedWebsiteNodes = this.$(`li[data-website_name="${websiteName}"]`);
|
||||
$selectedWebsiteNodes.removeClass('d-none');
|
||||
// Hide generic siblings
|
||||
$selectedWebsiteNodes.each(function () {
|
||||
$(this).siblings(`li[data-key="${$(this).data('key')}"]:not([data-website_name])`).addClass('d-none');
|
||||
});
|
||||
}
|
||||
// Preserve current inactive toggle state
|
||||
this.$('.o_is_inactive').toggleClass('d-none', !$('#o_show_inactive').prop('checked'));
|
||||
this._handleLastVisibleChild();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onSearchInputKeyDown: function (ev) {
|
||||
// <Tab> or <Enter>
|
||||
if (ev.which === 13 || ev.which === 9) {
|
||||
this._searchScrollTo($(ev.currentTarget).val(), !ev.shiftKey);
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onSearchInputKeyInput: function (ev) {
|
||||
// Useful for input empty either with ms-clear or by typing
|
||||
if (ev.currentTarget.value === "") {
|
||||
this._searchScrollTo("");
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onSearchButtonClick: function (ev) {
|
||||
this._searchScrollTo(this.$('.o_search input').val());
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onShowDiffClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
this.do_action('base.reset_view_arch_wizard_action', {
|
||||
additional_context: {
|
||||
'active_model': 'ir.ui.view',
|
||||
'active_ids': [parseInt(ev.currentTarget.dataset['view_id'])],
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onLoadHierarchyClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
this.do_action('website.action_show_viewhierarchy', {
|
||||
additional_context: {
|
||||
'active_model': 'ir.ui.view',
|
||||
'active_id': parseInt(ev.currentTarget.dataset['view_id']),
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Adds a class to the last visible element of every lists.
|
||||
* This is purely cosmetic to add a right angle dashed `:before` style in
|
||||
* css. This can't be done in css as there is no way to target a last
|
||||
* element by class.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_handleLastVisibleChild: function () {
|
||||
this.$('.o_last_visible_child').removeClass('o_last_visible_child');
|
||||
const lastElements = _.filter(_.map(
|
||||
this.$('ul'), el => $(el).find('> li:visible').last()[0]
|
||||
));
|
||||
$(lastElements).addClass('o_last_visible_child');
|
||||
|
||||
const selector = $('#o_show_inactive').prop('checked') ? '> li' : '> li:not(.o_is_inactive)';
|
||||
this.$('.o_fold_icon').map(function () {
|
||||
let $ico = $(this);
|
||||
let childs = $ico.parent().parent().first().find('ul').find(selector);
|
||||
$ico.toggleClass('d-none', !childs.length);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Searches and scrolls to view entries matching the given text. Exact
|
||||
* matches will be returned first. Search is done on `key`, `id` and `name`
|
||||
* for exact matches, and `key`, `name` for simple matches.
|
||||
*
|
||||
* @private
|
||||
* @param {string} search text to search and scroll to
|
||||
* @param {boolean} [forward] set to false to go to previous find
|
||||
*/
|
||||
_searchScrollTo: function (search, forward = true) {
|
||||
const foundClasses = 'o_search_found border border-info rounded px-2';
|
||||
this.$('.o_search_found').removeClass(foundClasses);
|
||||
this.$('.o_not_found').removeClass('o_not_found');
|
||||
this.$('.o_tab_hint').remove();
|
||||
if (search !== this.prevSearch) {
|
||||
this.prevSearch = search;
|
||||
this.cptFound = -1;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
// Exact match first
|
||||
const exactMatches = $(`[data-key="${search}" i], [data-id="${search}" i], [data-name="${search}" i]`).not(':hidden').get();
|
||||
let matches = $(`[data-key*="${search}" i], [data-name*="${search}" i]`).not(':hidden').not(exactMatches).get();
|
||||
matches = exactMatches.concat(matches);
|
||||
if (!matches.length) {
|
||||
this.$('.o_search input').addClass('o_not_found');
|
||||
} else {
|
||||
if (forward) {
|
||||
this.cptFound++;
|
||||
if (this.cptFound > matches.length - 1) {
|
||||
this.cptFound = 0;
|
||||
}
|
||||
} else {
|
||||
this.cptFound--;
|
||||
if (this.cptFound < 0) {
|
||||
this.cptFound = matches.length - 1;
|
||||
}
|
||||
}
|
||||
const el = matches[this.cptFound];
|
||||
$(el).children('p').addClass(foundClasses).append($('<span/>', {
|
||||
class: 'o_tab_hint text-info ms-auto small fst-italic pe-2',
|
||||
text: _.str.sprintf(_t("Press %s for next %s"), "<Tab>", `[${this.cptFound + 1}/${matches.length}]`),
|
||||
}));
|
||||
$('.o_content').scrollTo(el, 0, {offset: -200});
|
||||
|
||||
this.prevSearch = search;
|
||||
this.$('.o_search input').focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const ViewHierarchy = qweb.View.extend({
|
||||
withSearchBar: false,
|
||||
config: _.extend({}, qweb.View.prototype.config, {
|
||||
Renderer: Renderer,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('view_hierarchy', ViewHierarchy);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { Component, useRef, useState } from "@odoo/owl";
|
||||
|
||||
export class HierarchyNavbar extends Component {
|
||||
setup() {
|
||||
this.searchInput = useRef("search");
|
||||
this.websiteNames = useState(Array.from(this.props.websites.names));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
onInputKeydown(event) {
|
||||
if (event.key === "Enter" || event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
this.props.searchView(event.target.value, !event.shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
onInputClick(event) {
|
||||
this.props.searchView(this.searchInput.el.value, !event.shiftKey);
|
||||
}
|
||||
}
|
||||
|
||||
HierarchyNavbar.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
};
|
||||
HierarchyNavbar.template = "website.hierarchy_navbar";
|
||||
HierarchyNavbar.props = {
|
||||
toggleInactive: Function,
|
||||
websites: Object,
|
||||
selectWebsite: Function,
|
||||
searchView: Function,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website.hierarchy_navbar" owl="1">
|
||||
<nav class="o_tree_nav navbar justify-content-start w-100 bg-white shadow position-sticky top-0" t-ref="navbar">
|
||||
<div class="dropdown ms-2 border border-info rounded">
|
||||
<Dropdown class="'btn-group'" togglerClass="'btn btn-secondary'">
|
||||
<t t-set-slot="toggler">
|
||||
<span t-esc="props.websites.selected"/>
|
||||
</t>
|
||||
<t t-foreach="websiteNames" t-as="websiteName" t-key="websiteName_index">
|
||||
<DropdownItem onSelected="() => props.selectWebsite(websiteName)" t-esc="websiteName"/>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div class="ms-2 form-check form-switch">
|
||||
<input t-on-click="(ev) => props.toggleInactive(ev.target.checked)" id="o_show_inactive" class="form-check-input" type="checkbox"/>
|
||||
<label class="form-check-label" for="o_show_inactive">Show inactive views</label>
|
||||
</div>
|
||||
<div class="o_search input-group ms-auto col-8 col-sm-6 col-md-4 col-xl-3" role="search">
|
||||
<input type="search" name="search" class="form-control border-info" placeholder="Name, id or key" t-on-keydown="onInputKeydown" t-ref="search"/>
|
||||
<button type="submit" class="btn btn-info" aria-label="Search" title="Search" t-on-click="onInputClick">
|
||||
<i class="fa fa-search"/>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,251 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { HierarchyNavbar } from "./hierarchy_navbar";
|
||||
import { Layout } from "@web/search/layout";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component, onWillStart, useEffect, useState } from "@odoo/owl";
|
||||
|
||||
export class ViewHierarchy extends Component {
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.orm = useService("orm");
|
||||
this.router = useService("router");
|
||||
this.state = useState({ showInactive: false, searchedView: {}, viewTree: {} });
|
||||
this.websites = useState({ names: new Set(["All Websites"]), selected: "All Websites" });
|
||||
this.viewId = this.props.action.context.active_id || this.router.current.hash.active_id;
|
||||
this.hideGenericViewByWebsite = {};
|
||||
|
||||
onWillStart(async () => {
|
||||
({
|
||||
sibling_views: this.siblingViews,
|
||||
hierarchy: this.state.viewTree,
|
||||
} = await this.orm.call("ir.ui.view", "get_view_hierarchy", [this.viewId], {}));
|
||||
|
||||
this.setupWebsiteNames();
|
||||
this.setupHideGenericViewByWebsite();
|
||||
this.linkViewsToParent();
|
||||
});
|
||||
|
||||
useEffect(
|
||||
(searchFoundElem) => {
|
||||
if (searchFoundElem) {
|
||||
searchFoundElem.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
},
|
||||
() => [document.querySelector(".o_search_found")]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the treeView by website
|
||||
* @param {String} websiteName
|
||||
*/
|
||||
selectWebsite(websiteName) {
|
||||
this.websites.selected = websiteName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show/hide inactive views
|
||||
* @param {Boolean} checked
|
||||
*/
|
||||
toggleInactive(checked) {
|
||||
this.state.showInactive = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} keyword
|
||||
* @returns {Array} a list of visible views that match the keyword
|
||||
* insensitive case.
|
||||
* The comparison is done on the name, the key and the id of each views.
|
||||
* Priority is given to the exact matches and then to the order
|
||||
*/
|
||||
getSearchResults(keyword) {
|
||||
const exactMatches = [];
|
||||
const matches = [];
|
||||
const lowercaseKeyword = keyword.toLowerCase();
|
||||
this.viewTraversal(
|
||||
this.state.viewTree,
|
||||
(currentView) => {
|
||||
if (
|
||||
this.isViewDisplayed(currentView) &&
|
||||
(currentView.name.toLowerCase() === lowercaseKeyword ||
|
||||
currentView.key.toLowerCase() === lowercaseKeyword ||
|
||||
currentView.id === parseInt(lowercaseKeyword))
|
||||
) {
|
||||
exactMatches.push(currentView);
|
||||
} else if (
|
||||
this.isViewDisplayed(currentView) &&
|
||||
(currentView.name.toLowerCase().includes(lowercaseKeyword) ||
|
||||
currentView.key.toLowerCase().includes(lowercaseKeyword))
|
||||
) {
|
||||
matches.push(currentView);
|
||||
}
|
||||
},
|
||||
(currentView) => this.isViewDisplayed(currentView)
|
||||
);
|
||||
return exactMatches.concat(matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the next visibile view that matches the keyword to the name, the
|
||||
* key and the id of the view
|
||||
* @param {String} keyword
|
||||
* @param {Boolean} forward
|
||||
*/
|
||||
searchView(keyword, forward = true) {
|
||||
const matches = this.getSearchResults(keyword);
|
||||
let index = 0;
|
||||
if (this.state.searchedView.keyword === keyword) {
|
||||
index = matches.findIndex((view) => this.state.searchedView.id === view.id);
|
||||
index = forward ? index + 1 : index - 1;
|
||||
index = index - matches.length * Math.floor(index / matches.length);
|
||||
}
|
||||
|
||||
const view = matches[index];
|
||||
if (view) {
|
||||
this.state.searchedView = {
|
||||
id: view.id,
|
||||
keyword: keyword,
|
||||
total: matches.length,
|
||||
index,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an inorder traversal of the view tree and apply a function at each
|
||||
* node
|
||||
* @param {Object} currentView represent the current view tree
|
||||
* @param {Function} fn function applied at each node with currentView as
|
||||
* parameter
|
||||
* @param {Function} continueRec take the view as argument and decide if
|
||||
* the recursion continue
|
||||
*/
|
||||
viewTraversal(currentView, fn, continueRec = (view) => true) {
|
||||
fn(currentView);
|
||||
if (continueRec(currentView)) {
|
||||
currentView.inherit_children.forEach((childView) => {
|
||||
this.viewTraversal(childView, fn, continueRec);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup website names from the viewTree into this.websites.names
|
||||
*/
|
||||
setupWebsiteNames() {
|
||||
this.viewTraversal(this.state.viewTree, (currentView) => {
|
||||
if (currentView.website_name) {
|
||||
this.websites.names.add(currentView.website_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* States for each website filter if a generic view should be hided or not
|
||||
*/
|
||||
setupHideGenericViewByWebsite() {
|
||||
this.viewTraversal(this.state.viewTree, (currentView) => {
|
||||
if (currentView.website_name) {
|
||||
if (!this.hideGenericViewByWebsite[currentView.website_name]) {
|
||||
this.hideGenericViewByWebsite[currentView.website_name] = {};
|
||||
}
|
||||
this.hideGenericViewByWebsite[currentView.website_name][currentView.name] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Link views in the viewTree to their parent
|
||||
*/
|
||||
linkViewsToParent() {
|
||||
this.viewTraversal(this.state.viewTree, (currentView) => {
|
||||
currentView.inherit_children.forEach((child) => (child.parent = currentView));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse the view to show/hide the children
|
||||
* @param {Object} view
|
||||
*/
|
||||
onCollapseClick(view) {
|
||||
view.collapsed = !view.collapsed;
|
||||
if (view.collapsed) {
|
||||
// When folding a parent, children should also fold
|
||||
this.viewTraversal(view, (child) => {
|
||||
child.collapsed = view.collapsed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} view
|
||||
* @param {Boolean} isCollapsedDisplayed
|
||||
* @returns true if the view is displayed in the view tree, false otherwise
|
||||
*/
|
||||
isViewDisplayed(view, isCollapsedDisplayed = false) {
|
||||
let isCollapsed = view.parent ? view.parent.collapsed : false;
|
||||
if (isCollapsedDisplayed) {
|
||||
isCollapsed = false;
|
||||
}
|
||||
const isActive = this.state.showInactive || view.active;
|
||||
const isWebsiteDisplayed =
|
||||
this.websites.selected === "All Websites" ||
|
||||
view.website_name === this.websites.selected ||
|
||||
(!view.website_name &&
|
||||
!this.hideGenericViewByWebsite[this.websites.selected][view.name]);
|
||||
return !isCollapsed && isActive && isWebsiteDisplayed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} view
|
||||
* @returns true if view has a child to unfold, false otherwise
|
||||
*/
|
||||
hasChildToUnfold(view) {
|
||||
return view.inherit_children.some((child) => this.isViewDisplayed(child, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} viewId
|
||||
*/
|
||||
onShowDiffClick(viewId) {
|
||||
this.action.doAction("base.reset_view_arch_wizard_action", {
|
||||
additionalContext: {
|
||||
active_model: "ir.ui.view",
|
||||
active_ids: [viewId],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} viewId
|
||||
*/
|
||||
openFormView(viewId) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "ir.ui.view",
|
||||
res_id: viewId,
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} viewId
|
||||
*/
|
||||
onShowHierarchy(viewId) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.client",
|
||||
tag: "website_view_hierarchy",
|
||||
name: "View Hierarchy",
|
||||
context: {
|
||||
active_id: viewId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ViewHierarchy.components = { Layout, HierarchyNavbar };
|
||||
ViewHierarchy.template = "website.view_hierarchy";
|
||||
|
||||
registry.category("actions").add("website_view_hierarchy", ViewHierarchy);
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website.view_hierarchy" owl="1">
|
||||
<Layout display="{controlPanel: { 'top-right': false, 'bottom-right': false, 'bottom-left': false }}" className="'overflow-auto h-100'">
|
||||
<HierarchyNavbar
|
||||
toggleInactive.bind="toggleInactive"
|
||||
websites="websites"
|
||||
selectWebsite.bind="selectWebsite"
|
||||
searchView.bind="searchView"
|
||||
/>
|
||||
<div class="o_tree_container ms-2 container mt-2" style="margin-bottom: 50px;">
|
||||
<div t-if="siblingViews.length > 0" class="alert alert-info m-1 p-1">
|
||||
Multiple tree exists for this view
|
||||
<a t-foreach="siblingViews" t-as="siblingView" t-key="siblingView.id" href="#" class="o_load_hierarchy" t-on-click.stop.prevent="() => this.onShowHierarchy(siblingView.id)">
|
||||
<i class="fa fa-arrow-right me-1"/>
|
||||
<t t-esc="siblingView.display_name"/>
|
||||
<<t t-esc="siblingView.key"/>>
|
||||
<t t-if="siblingView.website_id">
|
||||
[<t t-esc="siblingView.website_id[1]"/>]
|
||||
</t>
|
||||
</a>
|
||||
</div>
|
||||
<t t-set="viewTree" t-value="state.viewTree"/>
|
||||
<t t-call="website.report_viewhierarchy_children"/>
|
||||
</div>
|
||||
</Layout>
|
||||
</t>
|
||||
|
||||
<t t-name="website.report_viewhierarchy_children" owl="1">
|
||||
<p t-on-click="() => this.onCollapseClick(viewTree)"
|
||||
t-attf-class="o_tree_entry mb-0 d-flex align-items-center {{viewTree.active ? '': 'text-muted fw-normal'}} {{viewTree.inherit_children.length ? 'o_has_child' : ''}} {{state.searchedView.id === viewTree.id ? 'o_search_found border border-info rounded px-2': ''}}">
|
||||
<i t-if="hasChildToUnfold(viewTree)" t-attf-class="js_fold o_fold_icon fa {{ viewTree.collapsed ? 'fa-plus-square-o' : 'fa-minus-square-o' }} me-1"/>
|
||||
<i t-if="viewTree.arch_updated" class="fa fa-pencil-square me-1 o_text_pink" title="This view arch has been modified"/>
|
||||
<!-- Yellow bg to highlight requested view as google does -->
|
||||
<span class="js_fold" t-att-style="viewId === viewTree.id ? 'background-color: yellow': ''">
|
||||
<t t-esc="viewTree.name"/> (<span class="fw-bold" t-esc="viewTree.key"/>)
|
||||
<span class="o_text_orange" t-if="viewTree.website_name" t-esc="`[${viewTree.website_name}]`"/>
|
||||
</span>
|
||||
<a href="#" t-on-click.stop.prevent="() => this.openFormView(viewTree.id)">
|
||||
<i class="fa fa-eye ms-2 text-muted" title="Go to View"/>
|
||||
</a>
|
||||
<a class="o_show_diff" href="#" t-on-click.stop.prevent="() => this.onShowDiffClick(viewTree.id)">
|
||||
<i class="fa fa-files-o ms-2 text-muted" title="Show Arch Diff"/>
|
||||
</a>
|
||||
<t t-if="state.searchedView.id === viewTree.id">
|
||||
<span class="o_tab_hint text-info ms-auto small fst-italic pe-2">Press <Tab> for next [<t t-esc="state.searchedView.index+1"/>/<t t-esc="state.searchedView.total"/>]</span>
|
||||
</t>
|
||||
</p>
|
||||
|
||||
<ul t-if="viewTree.inherit_children">
|
||||
<t t-foreach="viewTree.inherit_children" t-as="child" t-key="child.id">
|
||||
<li t-if="isViewDisplayed(child)"
|
||||
t-attf-class="{{child.active ? '' : 'o_is_inactive'}}">
|
||||
<t t-call="website.report_viewhierarchy_children">
|
||||
<t t-set="viewTree" t-value="child"/>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -69,8 +69,8 @@
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&.o_last_visible_child:before {
|
||||
background: #fff;
|
||||
&:last-child:before {
|
||||
background: $o-webclient-background-color;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
top: 1em;
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
<label for="key"/>
|
||||
<div class='o_row'>
|
||||
<field name="key"/>
|
||||
<button title="Show site map" attrs="{'invisible': [('type', '!=', 'qweb')]}" name="website.action_show_viewhierarchy" icon="fa-sitemap" type="action" class="btn btn-link"/>
|
||||
<button title="Show site map" attrs="{'invisible': [('type', '!=', 'qweb')]}" name="website.action_website_view_hierarchy" icon="fa-sitemap" type="action" class="btn btn-link"/>
|
||||
</div>
|
||||
<field name="page_ids" invisible="1" />
|
||||
<field name="first_page_id" attrs="{'invisible': [('page_ids', '=', [])]}" />
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
<field name="context" eval="{'search_default_category_id': ref('base.module_category_website_website'), 'searchpanel_default_category_id': ref('base.module_category_website')}"/>
|
||||
</record>
|
||||
|
||||
<record id="action_show_viewhierarchy" model="ir.actions.act_window">
|
||||
<field name="name">Show View Hierarchy</field>
|
||||
<field name="res_model">ir.ui.view</field>
|
||||
<field name="view_mode">qweb</field>
|
||||
<record id="action_website_view_hierarchy" model="ir.actions.client">
|
||||
<field name="name">View Hierarchy</field>
|
||||
<field name="tag">website_view_hierarchy</field>
|
||||
</record>
|
||||
|
||||
<!-- ====== website views ==============================================
|
||||
@@ -172,79 +171,6 @@
|
||||
|
||||
<!-- ====== ir.ui.view views ============================================
|
||||
==================================================================== -->
|
||||
<record id="view_view_qweb" model="ir.ui.view">
|
||||
<field name="name">View Hierarchy</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="model">ir.ui.view</field>
|
||||
<field name="arch" type="xml">
|
||||
<qweb js_class="view_hierarchy">
|
||||
<nav class="o_tree_nav navbar justify-content-start w-100 fixed-top bg-white shadow">
|
||||
<div class="dropdown ms-2 border border-info rounded">
|
||||
<a href="#" role="button" class="btn dropdown-toggle text-info" data-bs-toggle="dropdown">
|
||||
All Websites
|
||||
</a>
|
||||
<div class="dropdown-menu o_website_filter">
|
||||
<a href="#" class="dropdown-item active" data-website_name="*">All Websites</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-2 form-check form-switch">
|
||||
<input id="o_show_inactive" class="form-check-input" type="checkbox"/>
|
||||
<label class="form-check-label" for="o_show_inactive">Show inactive views</label>
|
||||
</div>
|
||||
<div class="o_search input-group ms-auto col-8 col-sm-6 col-md-4 col-xl-3" role="search">
|
||||
<input type="search" name="search" class="form-control border-info" placeholder="Name, id or key"/>
|
||||
<button type="submit" class="btn btn-info" aria-label="Search" title="Search">
|
||||
<i class="fa fa-search"/>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<t t-set="view" t-value="env['ir.ui.view'].browse(context['active_id']).with_context(active_test=False)"/>
|
||||
<t t-set="requested_view" t-value="view"/>
|
||||
<t t-set="view" t-value="view._get_top_level_view()"/>
|
||||
<div class="o_tree_container ms-2" t-att-data-requested-view-id="requested_view.id">
|
||||
<t t-set="sibling_views" t-value="view.search([('key', '=', view.key)]) - view"/>
|
||||
<div t-if="sibling_views" class="alert alert-info m-1 p-1">
|
||||
Multiple tree exists for this view
|
||||
<a t-foreach="sibling_views" t-as="sibling_view" href="#" class="o_load_hierarchy" t-att-data-view_id="sibling_view.id">
|
||||
<i class="fa fa-arrow-right me-1"/>
|
||||
<t t-esc="sibling_view.with_context(display_website=True, display_key=True).display_name"/>
|
||||
</a>
|
||||
</div>
|
||||
<t t-call="website.report_viewhierarchy_children"/>
|
||||
</div>
|
||||
</qweb>
|
||||
</field>
|
||||
</record>
|
||||
<template id="report_viewhierarchy_children">
|
||||
<t t-set="classes_for_search" t-value="'d-flex align-items-center'"/>
|
||||
<p t-attf-class="o_tree_entry mb-0 #{'text-muted fw-normal' if not view.active else ''} #{view.inherit_children_ids and 'o_has_child' or ''} #{classes_for_search}">
|
||||
<i t-if="view.inherit_children_ids" class="js_fold o_fold_icon fa fa-minus-square-o me-1"/>
|
||||
<i t-if="view.arch_updated" class="fa fa-pencil-square me-1 o_text_pink" title="This view arch has been modified"/>
|
||||
<span class="js_fold">
|
||||
<t t-esc="view.name"/> (<span class="fw-bold" t-esc="view.key"/>)
|
||||
<span class="o_text_orange" t-if="view.website_id" t-esc="' [%s]' % view.website_id.name"/>
|
||||
</span>
|
||||
<a type="action" data-model="ir.ui.view" t-att-data-res-id="view.id">
|
||||
<i class="fa fa-eye ms-2 text-muted" title="Go to View"/>
|
||||
</a>
|
||||
<a href="#" class="o_show_diff" t-att-data-view_id="view.id">
|
||||
<i class="fa fa-files-o ms-2 text-muted" title="Show Arch Diff"/>
|
||||
</a>
|
||||
</p>
|
||||
<ul t-if="view.inherit_children_ids">
|
||||
<t t-foreach="view.inherit_children_ids" t-as="child">
|
||||
<li t-att-class="not child.active and 'o_is_inactive d-none'"
|
||||
t-att-data-website_name="child.website_id.name" t-att-data-key="child.key"
|
||||
t-att-data-id="child.id" t-att-data-name="child.name">
|
||||
<t t-call="website.report_viewhierarchy_children">
|
||||
<t t-set="view" t-value="child"/>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<record id="reset_view_arch_wizard_view" model="ir.ui.view">
|
||||
<field name="model">reset.view.arch.wizard</field>
|
||||
<field name="inherit_id" ref="base.reset_view_arch_wizard_view"/>
|
||||
|
||||
Reference in New Issue
Block a user