refactor(cms): migrate store theme UI from tenancy to CMS module
Move store theme admin pages, templates, and JS from tenancy module
to CMS module where the data layer (model, service, API, schemas)
already lives. Eliminates split ownership.
Moved:
- Route handlers: GET /store-themes, GET /stores/{code}/theme
- Templates: store-theme.html, store-themes.html
- JS: store-theme.js, store-themes.js
- Updated static references: tenancy_static → cms_static
Deleted old tenancy files (no remaining references).
Menu item in CMS definition already pointed to correct route.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
332
app/modules/cms/static/admin/js/store-theme.js
Normal file
332
app/modules/cms/static/admin/js/store-theme.js
Normal file
@@ -0,0 +1,332 @@
|
||||
// static/admin/js/store-theme.js (FIXED VERSION)
|
||||
/**
|
||||
* Store Theme Editor - Alpine.js Component
|
||||
* Manages theme customization for store shops
|
||||
*
|
||||
* REQUIRES: log-config.js to be loaded first
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// LOGGING CONFIGURATION (using centralized logger)
|
||||
// ============================================================================
|
||||
|
||||
// Use the pre-configured theme logger from centralized log-config.js
|
||||
const themeLog = window.LogConfig.loggers.storeTheme;
|
||||
|
||||
// ============================================================================
|
||||
// ALPINE.JS COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
function adminStoreTheme() {
|
||||
return {
|
||||
// ✅ CRITICAL: Inherit base layout functionality
|
||||
...data(),
|
||||
|
||||
// ✅ CRITICAL: Set page identifier
|
||||
currentPage: 'store-theme',
|
||||
|
||||
// Page state
|
||||
storeCode: null,
|
||||
store: null,
|
||||
loading: true,
|
||||
saving: false,
|
||||
error: null,
|
||||
showResetThemeModal: false,
|
||||
|
||||
// Theme data structure matching StoreTheme model
|
||||
themeData: {
|
||||
theme_name: 'default',
|
||||
colors: {
|
||||
primary: '#6366f1',
|
||||
secondary: '#8b5cf6',
|
||||
accent: '#ec4899',
|
||||
background: '#ffffff',
|
||||
text: '#1f2937',
|
||||
border: '#e5e7eb'
|
||||
},
|
||||
fonts: {
|
||||
heading: 'Inter, sans-serif',
|
||||
body: 'Inter, sans-serif',
|
||||
size_base: '16px',
|
||||
size_heading: '2rem'
|
||||
},
|
||||
layout: {
|
||||
style: 'grid',
|
||||
header_position: 'fixed',
|
||||
product_card_style: 'card',
|
||||
sidebar_position: 'left'
|
||||
},
|
||||
branding: {
|
||||
logo_url: '',
|
||||
favicon_url: '',
|
||||
banner_url: ''
|
||||
},
|
||||
custom_css: '',
|
||||
social_links: {
|
||||
facebook: '',
|
||||
instagram: '',
|
||||
twitter: '',
|
||||
linkedin: ''
|
||||
}
|
||||
},
|
||||
|
||||
// Available presets
|
||||
presets: [],
|
||||
selectedPreset: null,
|
||||
|
||||
// ====================================================================
|
||||
// INITIALIZATION
|
||||
// ====================================================================
|
||||
|
||||
async init() {
|
||||
// Load i18n translations
|
||||
await I18n.loadModule('tenancy');
|
||||
|
||||
// Guard against multiple initialization
|
||||
if (window._adminStoreThemeInitialized) return;
|
||||
window._adminStoreThemeInitialized = true;
|
||||
|
||||
themeLog.info('Initializing store theme editor');
|
||||
|
||||
// Start performance timer
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
// Extract store code from URL
|
||||
const urlParts = window.location.pathname.split('/');
|
||||
this.storeCode = urlParts[urlParts.indexOf('stores') + 1];
|
||||
|
||||
themeLog.debug('Store code from URL:', this.storeCode);
|
||||
|
||||
if (!this.storeCode) {
|
||||
throw new Error('Store code not found in URL');
|
||||
}
|
||||
|
||||
// Load data in parallel
|
||||
themeLog.group('Loading theme data');
|
||||
|
||||
await Promise.all([
|
||||
this.loadStore(),
|
||||
this.loadTheme(),
|
||||
this.loadPresets()
|
||||
]);
|
||||
|
||||
themeLog.groupEnd();
|
||||
|
||||
// Log performance
|
||||
const duration = performance.now() - startTime;
|
||||
window.LogConfig.logPerformance('Theme Editor Init', duration);
|
||||
|
||||
themeLog.info('Theme editor initialized successfully');
|
||||
|
||||
} catch (error) {
|
||||
// Use centralized error logger
|
||||
window.LogConfig.logError(error, 'Theme Editor Init');
|
||||
|
||||
this.error = error.message || 'Failed to initialize theme editor';
|
||||
Utils.showToast(this.error, 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ====================================================================
|
||||
// DATA LOADING
|
||||
// ====================================================================
|
||||
|
||||
async loadStore() {
|
||||
themeLog.info('Loading store data');
|
||||
|
||||
const url = `/admin/stores/${this.storeCode}`;
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
try {
|
||||
// ✅ FIX: apiClient returns data directly, not response.data
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
// ✅ Direct assignment - response IS the data
|
||||
this.store = response;
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, this.store, 'response');
|
||||
themeLog.debug('Store loaded:', this.store);
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to load store:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTheme() {
|
||||
themeLog.info('Loading theme data');
|
||||
|
||||
const url = `/admin/store-themes/${this.storeCode}`;
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
try {
|
||||
// ✅ FIX: apiClient returns data directly
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
// Merge with default theme data
|
||||
this.themeData = {
|
||||
...this.themeData,
|
||||
...response
|
||||
};
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, this.themeData, 'response');
|
||||
themeLog.debug('Theme loaded:', this.themeData);
|
||||
|
||||
} catch (error) {
|
||||
themeLog.warn('Failed to load theme, using defaults:', error);
|
||||
// Continue with default theme
|
||||
}
|
||||
},
|
||||
|
||||
async loadPresets() {
|
||||
themeLog.info('Loading theme presets');
|
||||
|
||||
const url = '/admin/store-themes/presets';
|
||||
window.LogConfig.logApiCall('GET', url, null, 'request');
|
||||
|
||||
try {
|
||||
// ✅ FIX: apiClient returns data directly
|
||||
const response = await apiClient.get(url);
|
||||
|
||||
// ✅ Access presets directly from response, not response.data.presets
|
||||
this.presets = response.presets || [];
|
||||
|
||||
window.LogConfig.logApiCall('GET', url, response, 'response');
|
||||
themeLog.debug(`Loaded ${this.presets.length} presets`);
|
||||
|
||||
} catch (error) {
|
||||
themeLog.error('Failed to load presets:', error);
|
||||
this.presets = [];
|
||||
}
|
||||
},
|
||||
|
||||
// ====================================================================
|
||||
// THEME OPERATIONS
|
||||
// ====================================================================
|
||||
|
||||
async saveTheme() {
|
||||
if (this.saving) return;
|
||||
|
||||
themeLog.info('Saving theme changes');
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const url = `/admin/store-themes/${this.storeCode}`;
|
||||
window.LogConfig.logApiCall('PUT', url, this.themeData, 'request');
|
||||
|
||||
// ✅ FIX: apiClient returns data directly
|
||||
const response = await apiClient.put(url, this.themeData);
|
||||
|
||||
window.LogConfig.logApiCall('PUT', url, response, 'response');
|
||||
|
||||
const duration = performance.now() - startTime;
|
||||
window.LogConfig.logPerformance('Save Theme', duration);
|
||||
|
||||
themeLog.info('Theme saved successfully');
|
||||
Utils.showToast(I18n.t('tenancy.messages.theme_saved_successfully'), 'success');
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Save Theme');
|
||||
this.error = 'Failed to save theme';
|
||||
Utils.showToast(this.error, 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async applyPreset(presetName) {
|
||||
themeLog.info(`Applying preset: ${presetName}`);
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const url = `/admin/store-themes/${this.storeCode}/preset/${presetName}`;
|
||||
window.LogConfig.logApiCall('POST', url, null, 'request');
|
||||
|
||||
// ✅ FIX: apiClient returns data directly
|
||||
const response = await apiClient.post(url);
|
||||
|
||||
window.LogConfig.logApiCall('POST', url, response, 'response');
|
||||
|
||||
// ✅ FIX: Access theme directly from response, not response.data.theme
|
||||
if (response && response.theme) {
|
||||
this.themeData = {
|
||||
...this.themeData,
|
||||
...response.theme
|
||||
};
|
||||
}
|
||||
|
||||
themeLog.info(`Preset '${presetName}' applied successfully`);
|
||||
Utils.showToast(`Applied ${presetName} preset`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Apply Preset');
|
||||
Utils.showToast(I18n.t('tenancy.messages.failed_to_apply_preset'), 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async resetTheme() {
|
||||
themeLog.warn('Resetting theme to default');
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const url = `/admin/store-themes/${this.storeCode}`;
|
||||
window.LogConfig.logApiCall('DELETE', url, null, 'request');
|
||||
|
||||
await apiClient.delete(url);
|
||||
|
||||
window.LogConfig.logApiCall('DELETE', url, null, 'response');
|
||||
|
||||
// Reload theme data
|
||||
await this.loadTheme();
|
||||
|
||||
themeLog.info('Theme reset successfully');
|
||||
Utils.showToast(I18n.t('tenancy.messages.theme_reset_to_default'), 'success');
|
||||
|
||||
} catch (error) {
|
||||
window.LogConfig.logError(error, 'Reset Theme');
|
||||
Utils.showToast(I18n.t('tenancy.messages.failed_to_reset_theme'), 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ====================================================================
|
||||
// UTILITY METHODS
|
||||
// ====================================================================
|
||||
|
||||
previewTheme() {
|
||||
themeLog.debug('Opening theme preview');
|
||||
const previewUrl = `/store/${this.store?.subdomain || this.storeCode}`;
|
||||
window.open(previewUrl, '_blank');
|
||||
},
|
||||
|
||||
updateColor(key, value) {
|
||||
themeLog.debug(`Color updated: ${key} = ${value}`);
|
||||
this.themeData.colors[key] = value;
|
||||
},
|
||||
|
||||
updateFont(type, value) {
|
||||
themeLog.debug(`Font updated: ${type} = ${value}`);
|
||||
this.themeData.fonts[type] = value;
|
||||
},
|
||||
|
||||
updateLayout(key, value) {
|
||||
themeLog.debug(`Layout updated: ${key} = ${value}`);
|
||||
this.themeData.layout[key] = value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MODULE LOADED
|
||||
// ============================================================================
|
||||
|
||||
themeLog.info('Store theme editor module loaded');
|
||||
171
app/modules/cms/static/admin/js/store-themes.js
Normal file
171
app/modules/cms/static/admin/js/store-themes.js
Normal file
@@ -0,0 +1,171 @@
|
||||
// noqa: js-006 - async init pattern is safe, loadData has try/catch
|
||||
// static/admin/js/store-themes.js
|
||||
/**
|
||||
* Admin store themes selection page
|
||||
*/
|
||||
|
||||
// ✅ Use centralized logger
|
||||
const storeThemesLog = window.LogConfig.loggers.storeTheme;
|
||||
|
||||
storeThemesLog.info('Loading...');
|
||||
|
||||
function adminStoreThemes() {
|
||||
storeThemesLog.debug('adminStoreThemes() called');
|
||||
|
||||
return {
|
||||
// Inherit base layout state
|
||||
...data(),
|
||||
|
||||
// Set page identifier
|
||||
currentPage: 'store-theme',
|
||||
|
||||
// State
|
||||
loading: false,
|
||||
error: '',
|
||||
stores: [],
|
||||
selectedStoreCode: '',
|
||||
|
||||
// Selected store for filter (Tom Select)
|
||||
selectedStore: null,
|
||||
storeSelector: null,
|
||||
|
||||
// Search/filter
|
||||
searchQuery: '',
|
||||
|
||||
async init() {
|
||||
// Guard against multiple initialization
|
||||
if (window._adminStoreThemesInitialized) {
|
||||
return;
|
||||
}
|
||||
window._adminStoreThemesInitialized = true;
|
||||
|
||||
storeThemesLog.info('Store Themes init() called');
|
||||
|
||||
// Initialize store selector (Tom Select)
|
||||
this.$nextTick(() => {
|
||||
this.initStoreSelector();
|
||||
});
|
||||
|
||||
// Check localStorage for saved store
|
||||
const savedStoreId = localStorage.getItem('store_themes_selected_store_id');
|
||||
if (savedStoreId) {
|
||||
storeThemesLog.info('Restoring saved store:', savedStoreId);
|
||||
await this.loadStores();
|
||||
// Restore store after stores are loaded
|
||||
setTimeout(async () => {
|
||||
await this.restoreSavedStore(parseInt(savedStoreId));
|
||||
}, 200);
|
||||
} else {
|
||||
await this.loadStores();
|
||||
}
|
||||
|
||||
storeThemesLog.info('Store Themes initialization complete');
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore saved store from localStorage
|
||||
*/
|
||||
async restoreSavedStore(storeId) {
|
||||
try {
|
||||
const store = await apiClient.get(`/admin/stores/${storeId}`);
|
||||
if (this.storeSelector && store) {
|
||||
// Use the store selector's setValue method
|
||||
this.storeSelector.setValue(store.id, store);
|
||||
|
||||
// Set the filter state
|
||||
this.selectedStore = store;
|
||||
|
||||
storeThemesLog.info('Restored store:', store.name);
|
||||
}
|
||||
} catch (error) {
|
||||
storeThemesLog.warn('Failed to restore saved store, clearing localStorage:', error);
|
||||
localStorage.removeItem('store_themes_selected_store_id');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize store selector with Tom Select
|
||||
*/
|
||||
initStoreSelector() {
|
||||
if (!this.$refs.storeSelect) {
|
||||
storeThemesLog.warn('Store select element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.storeSelector = initStoreSelector(this.$refs.storeSelect, {
|
||||
placeholder: 'Search store...',
|
||||
onSelect: (store) => {
|
||||
storeThemesLog.info('Store selected:', store);
|
||||
this.selectedStore = store;
|
||||
// Save to localStorage
|
||||
localStorage.setItem('store_themes_selected_store_id', store.id.toString());
|
||||
},
|
||||
onClear: () => {
|
||||
storeThemesLog.info('Store filter cleared');
|
||||
this.selectedStore = null;
|
||||
// Clear from localStorage
|
||||
localStorage.removeItem('store_themes_selected_store_id');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear store filter
|
||||
*/
|
||||
clearStoreFilter() {
|
||||
if (this.storeSelector) {
|
||||
this.storeSelector.clear();
|
||||
}
|
||||
this.selectedStore = null;
|
||||
// Clear from localStorage
|
||||
localStorage.removeItem('store_themes_selected_store_id');
|
||||
},
|
||||
|
||||
async loadStores() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/admin/stores?limit=1000');
|
||||
this.stores = response.stores || [];
|
||||
storeThemesLog.debug('Loaded stores:', this.stores.length);
|
||||
} catch (error) {
|
||||
storeThemesLog.error('Failed to load stores:', error);
|
||||
this.error = error.message || 'Failed to load stores';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Computed: Filtered stores based on search and selected store
|
||||
*/
|
||||
get filteredStores() {
|
||||
let filtered = this.stores;
|
||||
|
||||
// If a store is selected via Tom Select, show only that store
|
||||
if (this.selectedStore) {
|
||||
filtered = this.stores.filter(v => v.id === this.selectedStore.id);
|
||||
}
|
||||
// Otherwise filter by search query
|
||||
else if (this.searchQuery) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
filtered = this.stores.filter(v =>
|
||||
v.name.toLowerCase().includes(query) ||
|
||||
(v.store_code && v.store_code.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
navigateToTheme() {
|
||||
if (!this.selectedStoreCode) {
|
||||
return;
|
||||
}
|
||||
window.location.href = `/admin/stores/${this.selectedStoreCode}/theme`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
storeThemesLog.info('Module loaded');
|
||||
Reference in New Issue
Block a user