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/ir.model.access.csv',
|
||||
'data/fp_sequence_data.xml',
|
||||
'views/fp_portal_shell.xml',
|
||||
'views/fp_portal_macros.xml',
|
||||
'views/fp_quote_request_views.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