[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:
fdardenne
2022-11-15 18:25:12 +01:00
parent c513b57c3e
commit 439db9eaeb
9 changed files with 413 additions and 344 deletions
+24 -2
View File
@@ -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"/>
&lt;<t t-esc="siblingView.key"/>&gt;
<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 &lt;Tab&gt; 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;
+1 -1
View File
@@ -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', '=', [])]}" />
+3 -77
View File
@@ -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"/>