feat(portal): sidebar shell template + portal.portal_layout inherit
fp_portal_shell wraps every /my/* page (FP custom + Odoo default) in a sticky-sidebar shell with no per-template edits. Sidebar markup is a separate fp_portal_sidebar template that reads fp_sidebar_items + fp_partner_display_name from the page context. Approach D ($0 re-emit) used instead of plan's unbalanced-xpath approach: position="replace" on //div[@id='wrap'] with $0 inside <main> causes Odoo's Python inheritance engine to re-emit the original #wrap node (verified in tools/template_inheritance.py lines 162-169). Every xpath block is well-formed XML. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'security/fp_portal_security.xml',
|
'security/fp_portal_security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/fp_sequence_data.xml',
|
'data/fp_sequence_data.xml',
|
||||||
|
'views/fp_portal_shell.xml',
|
||||||
'views/fp_portal_macros.xml',
|
'views/fp_portal_macros.xml',
|
||||||
'views/fp_quote_request_views.xml',
|
'views/fp_quote_request_views.xml',
|
||||||
'views/fp_portal_dashboard.xml',
|
'views/fp_portal_dashboard.xml',
|
||||||
|
|||||||
106
fusion_plating/fusion_plating_portal/views/fp_portal_shell.xml
Normal file
106
fusion_plating/fusion_plating_portal/views/fp_portal_shell.xml
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
|
||||||
|
Wraps every /my/* page (FP custom + Odoo default) in the new
|
||||||
|
sidebar shell. Inherits portal.portal_layout so we don't have
|
||||||
|
to edit every individual page template.
|
||||||
|
|
||||||
|
Implementation note (Approach D, $0 re-emit):
|
||||||
|
|
||||||
|
The plan originally proposed injecting an unbalanced main opening
|
||||||
|
tag in one xpath block and its closing tag in another. QWeb parses
|
||||||
|
each xpath payload as an independent XML fragment, so unbalanced
|
||||||
|
tags are rejected at load time.
|
||||||
|
|
||||||
|
Instead we use position="replace" on //div[@id='wrap'] with $0
|
||||||
|
inside the replacement payload. $0 is supported by Odoo 19's
|
||||||
|
Python view inheritance engine (tools/template_inheritance.py,
|
||||||
|
lines 162-169): any element whose text content is the literal
|
||||||
|
string "$0" has its text cleared and the deep-copied original node
|
||||||
|
appended as a child. This produces a fully balanced replacement tree
|
||||||
|
that nests the original #wrap (and all its Odoo-managed content)
|
||||||
|
inside our .o_fp_portal_main element.
|
||||||
|
|
||||||
|
Verified from portal_templates.xml line 155:
|
||||||
|
div id="wrap" class="o_portal_wrap"
|
||||||
|
div class="container pt-3 pb-5"
|
||||||
|
t t-out="0" (Odoo content slot)
|
||||||
|
/div
|
||||||
|
/div
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- Inherit portal.portal_layout to wrap content in sidebar shell -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<template id="fp_portal_shell"
|
||||||
|
name="FP Portal Shell — Sidebar Wrap"
|
||||||
|
inherit_id="portal.portal_layout"
|
||||||
|
priority="50">
|
||||||
|
<!--
|
||||||
|
Replace #wrap entirely. The $0 text node inside
|
||||||
|
<main class="o_fp_portal_main"> causes Odoo's inheritance
|
||||||
|
engine to re-emit the original #wrap div (with all its
|
||||||
|
children) at that position. Every existing portal page
|
||||||
|
continues to render correctly because Odoo's <t t-out="0"/>
|
||||||
|
content slot inside #wrap is preserved verbatim.
|
||||||
|
-->
|
||||||
|
<xpath expr="//div[@id='wrap']" position="replace">
|
||||||
|
<div class="o_fp_portal_shell">
|
||||||
|
<!-- Mobile hamburger (shown only below 768px via SCSS) -->
|
||||||
|
<button type="button"
|
||||||
|
class="o_fp_portal_hamburger d-md-none"
|
||||||
|
aria-label="Open navigation">
|
||||||
|
<i class="fa fa-bars"/>
|
||||||
|
</button>
|
||||||
|
<!-- Backdrop for mobile drawer (hidden by default) -->
|
||||||
|
<div class="o_fp_portal_backdrop"/>
|
||||||
|
<!-- Sidebar navigation component -->
|
||||||
|
<t t-call="fusion_plating_portal.fp_portal_sidebar"/>
|
||||||
|
<!-- Main content area — original #wrap re-emitted here via $0 -->
|
||||||
|
<main class="o_fp_portal_main">$0</main>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<!-- Sidebar template — rendered by fp_portal_shell -->
|
||||||
|
<!-- ================================================================== -->
|
||||||
|
<template id="fp_portal_sidebar" name="FP Portal Sidebar">
|
||||||
|
<aside class="o_fp_portal_sidebar">
|
||||||
|
<!-- Partner display name header -->
|
||||||
|
<div class="o_fp_sidebar_header">
|
||||||
|
<t t-out="fp_partner_display_name or 'My Account'"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation items, walked from the Python-side data structure.
|
||||||
|
fp_sidebar_items is injected by the controller mixin in Task 4.
|
||||||
|
Guards here handle the case where Task 4 hasn't deployed yet. -->
|
||||||
|
<t t-foreach="fp_sidebar_items or []" t-as="entry">
|
||||||
|
<!-- Section labels render as non-link headers -->
|
||||||
|
<t t-if="entry.get('type') == 'section_label'">
|
||||||
|
<div class="o_fp_sidebar_section_label" t-out="entry['label']"/>
|
||||||
|
</t>
|
||||||
|
<!-- Items render as anchor links -->
|
||||||
|
<t t-elif="entry.get('type') == 'item'">
|
||||||
|
<a t-att-href="entry['url']"
|
||||||
|
t-attf-class="o_fp_sidebar_item #{'o_fp_sidebar_active' if entry.get('active') else ''}">
|
||||||
|
<span class="o_fp_sidebar_icon" t-out="entry.get('icon') or '•'"/>
|
||||||
|
<span t-out="entry['label']"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Footer: sign out link always present -->
|
||||||
|
<div class="o_fp_sidebar_footer">
|
||||||
|
<a href="/web/session/logout?redirect=/" class="o_fp_sidebar_item">
|
||||||
|
<span class="o_fp_sidebar_icon">↪</span>
|
||||||
|
<span>Sign Out</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user