feat: complete dynamic menu system across all frontends
All checks were successful
CI / ruff (push) Successful in 11s
CI / pytest (push) Successful in 44m40s
CI / validate (push) Successful in 22s
CI / dependency-scanning (push) Successful in 28s
CI / docs (push) Successful in 39s
CI / deploy (push) Successful in 49s

- Add "Merchant Frontend" tab to admin menu-config page
- Merchant render endpoint now respects AdminMenuConfig visibility
  via get_merchant_primary_platform_id() platform resolution
- New store menu render endpoint (GET /store/core/menu/render/store)
  with platform-scoped visibility and store_code interpolation
- Store sidebar migrated from hardcoded Jinja2 macros to dynamic
  Alpine.js x-for rendering with loading skeleton and fallback
- Store init-alpine.js: add loadMenuConfig(), expandSectionForCurrentPage()
- Include store page route fixes, login template updates, and tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 02:14:42 +01:00
parent be248222bc
commit 506171503d
14 changed files with 1364 additions and 158 deletions

View File

@@ -21,14 +21,20 @@ function getStoreSidebarSectionsFromStorage() {
} catch (e) {
console.warn('[STORE INIT-ALPINE] Failed to load sidebar state from localStorage:', e);
}
// Default: all sections open
return {
products: true,
sales: true,
customers: true,
shop: true,
account: true
};
// Default: empty (populated dynamically from API response)
return {};
}
function findSectionForPage(menuData, pageId) {
if (!menuData?.sections) return null;
for (const section of menuData.sections) {
for (const item of (section.items || [])) {
if (item.id === pageId) {
return section.id;
}
}
}
return null;
}
function saveStoreSidebarSectionsToStorage(sections) {
@@ -51,6 +57,10 @@ function data() {
store: null,
storeCode: null,
// Dynamic menu state
menuData: null,
menuLoading: false,
// Sidebar collapsible sections state
openSections: getStoreSidebarSectionsFromStorage(),
@@ -81,8 +91,9 @@ function data() {
this.dark = true;
}
// Load store info
// Load store info and dynamic menu
this.loadStoreInfo();
this.loadMenuConfig();
// Save last visited page (for redirect after login)
// Exclude login, logout, onboarding, error pages
@@ -111,6 +122,38 @@ function data() {
}
},
async loadMenuConfig() {
if (this.menuData || this.menuLoading) return;
if (typeof apiClient === 'undefined') return;
if (!localStorage.getItem('store_token')) return;
this.menuLoading = true;
try {
this.menuData = await apiClient.get('/store/core/menu/render/store');
// Initialize section open state from response
for (const section of (this.menuData?.sections || [])) {
if (this.openSections[section.id] === undefined) {
this.openSections[section.id] = true;
}
}
saveStoreSidebarSectionsToStorage(this.openSections);
this.expandSectionForCurrentPage();
} catch (e) {
console.debug('Menu config not loaded, using fallback:', e?.message || e);
} finally {
this.menuLoading = false;
}
},
expandSectionForCurrentPage() {
if (!this.menuData) return;
const sectionId = findSectionForPage(this.menuData, this.currentPage);
if (sectionId && !this.openSections[sectionId]) {
this.openSections[sectionId] = true;
saveStoreSidebarSectionsToStorage(this.openSections);
}
},
toggleSideMenu() {
this.isSideMenuOpen = !this.isSideMenuOpen;
},