feat: add platform detail/edit admin UI and service enhancements
- Add platform detail and edit admin pages with templates and JS - Add ContentPageService methods: list_all_platform_pages, list_all_vendor_defaults - Deprecate /admin/platform-homepage route (redirects to /admin/platforms) - Add migration to fix content_page nullable columns - Refine platform and vendor context middleware - Add platform context middleware unit tests - Update platforms.js with improved functionality - Add section-based homepage plan documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,13 +44,19 @@ function contentPagesManager() {
|
||||
]);
|
||||
contentPagesLog.groupEnd();
|
||||
|
||||
// Check for platform filter in URL
|
||||
// Check for platform filter in URL (support both 'platform' and 'platform_code')
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const platformParam = urlParams.get('platform');
|
||||
const platformParam = urlParams.get('platform_code') || urlParams.get('platform');
|
||||
if (platformParam) {
|
||||
this.selectedPlatform = platformParam;
|
||||
}
|
||||
|
||||
// Check for slug param - if specified, redirect to edit page
|
||||
const slugParam = urlParams.get('slug');
|
||||
if (slugParam && platformParam) {
|
||||
await this.redirectToEditIfSlugMatches(platformParam, slugParam);
|
||||
}
|
||||
|
||||
contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
@@ -162,6 +168,30 @@ function contentPagesManager() {
|
||||
}
|
||||
},
|
||||
|
||||
// Redirect to edit page if a specific slug is requested
|
||||
async redirectToEditIfSlugMatches(platformCode, slug) {
|
||||
contentPagesLog.info(`Looking for page with platform=${platformCode}, slug=${slug}`);
|
||||
|
||||
// Find the page matching the platform and slug
|
||||
const matchingPage = this.allPages.find(page =>
|
||||
page.platform_code === platformCode && page.slug === slug
|
||||
);
|
||||
|
||||
if (matchingPage) {
|
||||
contentPagesLog.info(`Found matching page: ${matchingPage.id}, redirecting to edit...`);
|
||||
window.location.href = `/admin/content-pages/${matchingPage.id}/edit`;
|
||||
} else {
|
||||
contentPagesLog.warn(`No page found for platform=${platformCode}, slug=${slug}`);
|
||||
// Show a toast and offer to create
|
||||
if (slug === 'home') {
|
||||
// Offer to create homepage
|
||||
if (confirm(`No homepage found for ${platformCode}. Would you like to create one?`)) {
|
||||
window.location.href = `/admin/content-pages/create?platform_code=${platformCode}&slug=home&is_platform_page=true`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Get page tier label (three-tier system)
|
||||
getPageTierLabel(page) {
|
||||
if (page.vendor_id) {
|
||||
|
||||
139
static/admin/js/platform-detail.js
Normal file
139
static/admin/js/platform-detail.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Platform Detail - Alpine.js Component
|
||||
*
|
||||
* Displays platform overview, stats, and quick actions.
|
||||
*/
|
||||
|
||||
const platformDetailLog = window.LogConfig.createLogger('PLATFORM_DETAIL');
|
||||
|
||||
function platformDetail() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Page identification
|
||||
currentPage: 'platform-detail',
|
||||
|
||||
// State
|
||||
platform: null,
|
||||
stats: null,
|
||||
recentPages: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
platformCode: null,
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
platformDetailLog.info('=== PLATFORM DETAIL PAGE INITIALIZING ===');
|
||||
|
||||
// Duplicate initialization guard
|
||||
if (window._platformDetailInitialized) {
|
||||
platformDetailLog.warn('Platform detail page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._platformDetailInitialized = true;
|
||||
|
||||
try {
|
||||
// Extract platform code from URL
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/\/admin\/platforms\/([^\/]+)$/);
|
||||
|
||||
if (match) {
|
||||
this.platformCode = match[1];
|
||||
platformDetailLog.info('Viewing platform:', this.platformCode);
|
||||
await Promise.all([
|
||||
this.loadPlatform(),
|
||||
this.loadRecentPages(),
|
||||
]);
|
||||
} else {
|
||||
platformDetailLog.error('No platform code in URL');
|
||||
this.error = 'Platform code not found in URL';
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
platformDetailLog.info('=== PLATFORM DETAIL PAGE INITIALIZATION COMPLETE ===');
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Platform Detail Init');
|
||||
this.error = 'Failed to initialize page';
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// API Methods
|
||||
async loadPlatform() {
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/platforms/${this.platformCode}`);
|
||||
this.platform = response;
|
||||
platformDetailLog.info(`Loaded platform: ${this.platformCode}`);
|
||||
} catch (err) {
|
||||
platformDetailLog.error('Error loading platform:', err);
|
||||
this.error = err.message || 'Failed to load platform';
|
||||
throw err;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadRecentPages() {
|
||||
try {
|
||||
// Load recent content pages for this platform
|
||||
const response = await apiClient.get(`/admin/content-pages?platform_code=${this.platformCode}&limit=5`);
|
||||
this.recentPages = response.items || response || [];
|
||||
platformDetailLog.info(`Loaded ${this.recentPages.length} recent pages`);
|
||||
} catch (err) {
|
||||
platformDetailLog.error('Error loading recent pages:', err);
|
||||
// Non-fatal - don't throw
|
||||
this.recentPages = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Helper Methods
|
||||
getPlatformIcon(code) {
|
||||
const icons = {
|
||||
main: 'home',
|
||||
oms: 'clipboard-list',
|
||||
loyalty: 'star',
|
||||
sitebuilder: 'template',
|
||||
};
|
||||
return icons[code] || 'globe-alt';
|
||||
},
|
||||
|
||||
getPageTypeLabel(page) {
|
||||
if (page.is_platform_page) return 'Marketing';
|
||||
if (page.vendor_id) return 'Vendor Override';
|
||||
return 'Vendor Default';
|
||||
},
|
||||
|
||||
getPageTypeBadgeClass(page) {
|
||||
if (page.is_platform_page) {
|
||||
return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
|
||||
}
|
||||
if (page.vendor_id) {
|
||||
return 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200';
|
||||
}
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '—';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-LU', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
},
|
||||
|
||||
getPlatformUrl() {
|
||||
if (!this.platform) return '#';
|
||||
if (this.platform.domain) {
|
||||
return `https://${this.platform.domain}`;
|
||||
}
|
||||
// Development URL
|
||||
if (this.platform.code === 'main') {
|
||||
return '/';
|
||||
}
|
||||
return `/platforms/${this.platform.code}/`;
|
||||
},
|
||||
};
|
||||
}
|
||||
230
static/admin/js/platform-edit.js
Normal file
230
static/admin/js/platform-edit.js
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Platform Edit - Alpine.js Component
|
||||
*
|
||||
* Handles platform editing for multi-platform CMS.
|
||||
*/
|
||||
|
||||
const platformEditLog = window.LogConfig.createLogger('PLATFORM_EDIT');
|
||||
|
||||
function platformEdit() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Page identification
|
||||
currentPage: 'platform-edit',
|
||||
|
||||
// State
|
||||
platform: null,
|
||||
loading: true,
|
||||
saving: false,
|
||||
error: null,
|
||||
success: null,
|
||||
platformCode: null,
|
||||
|
||||
// Form data
|
||||
formData: {
|
||||
name: '',
|
||||
description: '',
|
||||
domain: '',
|
||||
path_prefix: '',
|
||||
logo: '',
|
||||
logo_dark: '',
|
||||
favicon: '',
|
||||
default_language: 'fr',
|
||||
supported_languages: ['fr', 'de', 'en'],
|
||||
is_active: true,
|
||||
is_public: true,
|
||||
theme_config: {},
|
||||
settings: {},
|
||||
},
|
||||
|
||||
errors: {},
|
||||
|
||||
// Available languages
|
||||
availableLanguages: [
|
||||
{ code: 'fr', name: 'French' },
|
||||
{ code: 'de', name: 'German' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'lu', name: 'Luxembourgish' },
|
||||
],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
platformEditLog.info('=== PLATFORM EDIT PAGE INITIALIZING ===');
|
||||
|
||||
// Duplicate initialization guard
|
||||
if (window._platformEditInitialized) {
|
||||
platformEditLog.warn('Platform edit page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._platformEditInitialized = true;
|
||||
|
||||
try {
|
||||
// Extract platform code from URL
|
||||
const path = window.location.pathname;
|
||||
const match = path.match(/\/admin\/platforms\/([^\/]+)\/edit/);
|
||||
|
||||
if (match) {
|
||||
this.platformCode = match[1];
|
||||
platformEditLog.info('Editing platform:', this.platformCode);
|
||||
await this.loadPlatform();
|
||||
} else {
|
||||
platformEditLog.error('No platform code in URL');
|
||||
this.error = 'Platform code not found in URL';
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
platformEditLog.info('=== PLATFORM EDIT PAGE INITIALIZATION COMPLETE ===');
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Platform Edit Init');
|
||||
this.error = 'Failed to initialize page';
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// API Methods
|
||||
async loadPlatform() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/admin/platforms/${this.platformCode}`);
|
||||
this.platform = response;
|
||||
|
||||
// Populate form data
|
||||
this.formData = {
|
||||
name: response.name || '',
|
||||
description: response.description || '',
|
||||
domain: response.domain || '',
|
||||
path_prefix: response.path_prefix || '',
|
||||
logo: response.logo || '',
|
||||
logo_dark: response.logo_dark || '',
|
||||
favicon: response.favicon || '',
|
||||
default_language: response.default_language || 'fr',
|
||||
supported_languages: response.supported_languages || ['fr', 'de', 'en'],
|
||||
is_active: response.is_active ?? true,
|
||||
is_public: response.is_public ?? true,
|
||||
theme_config: response.theme_config || {},
|
||||
settings: response.settings || {},
|
||||
};
|
||||
|
||||
platformEditLog.info(`Loaded platform: ${this.platformCode}`);
|
||||
} catch (err) {
|
||||
platformEditLog.error('Error loading platform:', err);
|
||||
this.error = err.message || 'Failed to load platform';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
this.success = null;
|
||||
this.errors = {};
|
||||
|
||||
try {
|
||||
// Build update payload (only changed fields)
|
||||
const payload = {
|
||||
name: this.formData.name,
|
||||
description: this.formData.description || null,
|
||||
domain: this.formData.domain || null,
|
||||
path_prefix: this.formData.path_prefix || null,
|
||||
logo: this.formData.logo || null,
|
||||
logo_dark: this.formData.logo_dark || null,
|
||||
favicon: this.formData.favicon || null,
|
||||
default_language: this.formData.default_language,
|
||||
supported_languages: this.formData.supported_languages,
|
||||
is_active: this.formData.is_active,
|
||||
is_public: this.formData.is_public,
|
||||
};
|
||||
|
||||
const response = await apiClient.put(
|
||||
`/admin/platforms/${this.platformCode}`,
|
||||
payload
|
||||
);
|
||||
|
||||
this.platform = response;
|
||||
this.success = 'Platform updated successfully';
|
||||
platformEditLog.info(`Updated platform: ${this.platformCode}`);
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.success = null;
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
platformEditLog.error('Error updating platform:', err);
|
||||
this.error = err.message || 'Failed to update platform';
|
||||
|
||||
// Handle validation errors
|
||||
if (err.details) {
|
||||
this.errors = err.details;
|
||||
}
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async toggleActive() {
|
||||
try {
|
||||
this.formData.is_active = !this.formData.is_active;
|
||||
await this.handleSubmit();
|
||||
} catch (err) {
|
||||
platformEditLog.error('Error toggling active status:', err);
|
||||
// Revert on error
|
||||
this.formData.is_active = !this.formData.is_active;
|
||||
}
|
||||
},
|
||||
|
||||
async togglePublic() {
|
||||
try {
|
||||
this.formData.is_public = !this.formData.is_public;
|
||||
await this.handleSubmit();
|
||||
} catch (err) {
|
||||
platformEditLog.error('Error toggling public status:', err);
|
||||
// Revert on error
|
||||
this.formData.is_public = !this.formData.is_public;
|
||||
}
|
||||
},
|
||||
|
||||
// Helper Methods
|
||||
isLanguageSupported(code) {
|
||||
return this.formData.supported_languages.includes(code);
|
||||
},
|
||||
|
||||
toggleLanguage(code) {
|
||||
const index = this.formData.supported_languages.indexOf(code);
|
||||
if (index > -1) {
|
||||
// Don't allow removing the last language
|
||||
if (this.formData.supported_languages.length > 1) {
|
||||
this.formData.supported_languages.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
this.formData.supported_languages.push(code);
|
||||
}
|
||||
},
|
||||
|
||||
getPlatformIcon(code) {
|
||||
const icons = {
|
||||
main: 'home',
|
||||
oms: 'clipboard-list',
|
||||
loyalty: 'star',
|
||||
sitebuilder: 'template',
|
||||
};
|
||||
return icons[code] || 'globe-alt';
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '—';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-LU', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -60,15 +60,15 @@ function platformHomepageManager() {
|
||||
// Handle response - API returns array directly
|
||||
const pages = Array.isArray(response) ? response : (response.data || response.items || []);
|
||||
|
||||
// Find the platform_homepage page
|
||||
const homepage = pages.find(page => page.slug === 'platform_homepage');
|
||||
// Find the homepage page (slug='home')
|
||||
const homepage = pages.find(page => page.slug === 'home');
|
||||
|
||||
if (!homepage) {
|
||||
platformHomepageLog.warn('Platform homepage not found, creating default...');
|
||||
// Initialize with default values
|
||||
this.page = {
|
||||
id: null,
|
||||
slug: 'platform_homepage',
|
||||
slug: 'home',
|
||||
title: 'Welcome to Our Multi-Vendor Marketplace',
|
||||
content: '<p>Connect vendors with customers worldwide. Build your online store and reach millions of shoppers.</p>',
|
||||
template: 'default',
|
||||
|
||||
@@ -8,6 +8,12 @@ const platformsLog = window.LogConfig.createLogger('PLATFORMS');
|
||||
|
||||
function platformsManager() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Page identification
|
||||
currentPage: "platforms",
|
||||
|
||||
// State
|
||||
platforms: [],
|
||||
loading: true,
|
||||
@@ -15,12 +21,22 @@ function platformsManager() {
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
platformsLog.info('=== PLATFORMS PAGE INITIALIZING ===');
|
||||
|
||||
// Duplicate initialization guard
|
||||
if (window._adminPlatformsInitialized) return;
|
||||
if (window._adminPlatformsInitialized) {
|
||||
platformsLog.warn('Platforms page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._adminPlatformsInitialized = true;
|
||||
|
||||
this.currentPage = "platforms";
|
||||
await this.loadPlatforms();
|
||||
try {
|
||||
await this.loadPlatforms();
|
||||
platformsLog.info('=== PLATFORMS PAGE INITIALIZATION COMPLETE ===');
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Platforms Init');
|
||||
this.error = 'Failed to initialize page';
|
||||
}
|
||||
},
|
||||
|
||||
// API Methods
|
||||
|
||||
Reference in New Issue
Block a user