Some checks failed
Migrated ~68 native browser confirm() calls across 74 files to use the project's confirm_modal/confirm_modal_dynamic Jinja2 macros, providing consistent styled confirmation dialogs instead of plain browser popups. Modules updated: core, tenancy, cms, marketplace, messaging, billing, customers, orders, cart. Uses danger/warning/info variants and double-confirm pattern for destructive delete operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
// app/modules/cms/static/store/js/media.js
|
|
/**
|
|
* Store media library management page logic
|
|
* Upload and manage images, videos, and documents
|
|
*/
|
|
|
|
const storeMediaLog = window.LogConfig.loggers.storeMedia ||
|
|
window.LogConfig.createLogger('storeMedia', false);
|
|
|
|
storeMediaLog.info('Loading...');
|
|
|
|
function storeMedia() {
|
|
storeMediaLog.info('storeMedia() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'media',
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
saving: false,
|
|
|
|
// Media data
|
|
media: [],
|
|
stats: {
|
|
total: 0,
|
|
images: 0,
|
|
videos: 0,
|
|
documents: 0
|
|
},
|
|
|
|
// Filters
|
|
filters: {
|
|
search: '',
|
|
type: '',
|
|
folder: ''
|
|
},
|
|
|
|
// Pagination
|
|
pagination: {
|
|
page: 1,
|
|
per_page: 24,
|
|
total: 0,
|
|
pages: 0
|
|
},
|
|
|
|
// Computed pagination properties required by pagination macro
|
|
get startIndex() {
|
|
if (this.pagination.total === 0) return 0;
|
|
return (this.pagination.page - 1) * this.pagination.per_page + 1;
|
|
},
|
|
|
|
get endIndex() {
|
|
return Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total);
|
|
},
|
|
|
|
get totalPages() {
|
|
return this.pagination.pages;
|
|
},
|
|
|
|
get pageNumbers() {
|
|
const pages = [];
|
|
const total = this.pagination.pages;
|
|
const current = this.pagination.page;
|
|
|
|
if (total <= 7) {
|
|
for (let i = 1; i <= total; i++) pages.push(i);
|
|
} else {
|
|
pages.push(1);
|
|
if (current > 3) pages.push('...');
|
|
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
|
|
pages.push(i);
|
|
}
|
|
if (current < total - 2) pages.push('...');
|
|
pages.push(total);
|
|
}
|
|
return pages;
|
|
},
|
|
|
|
previousPage() {
|
|
if (this.pagination.page > 1) {
|
|
this.pagination.page--;
|
|
this.loadMedia();
|
|
}
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.pagination.page < this.pagination.pages) {
|
|
this.pagination.page++;
|
|
this.loadMedia();
|
|
}
|
|
},
|
|
|
|
goToPage(pageNum) {
|
|
if (pageNum !== '...' && pageNum >= 1 && pageNum <= this.pagination.pages) {
|
|
this.pagination.page = pageNum;
|
|
this.loadMedia();
|
|
}
|
|
},
|
|
|
|
// Modal states
|
|
showUploadModal: false,
|
|
showDetailModal: false,
|
|
showDeleteMediaConfirm: false,
|
|
selectedMedia: null,
|
|
editingMedia: {
|
|
filename: '',
|
|
alt_text: '',
|
|
description: '',
|
|
folder: ''
|
|
},
|
|
|
|
// Upload states
|
|
isDragging: false,
|
|
uploadFolder: 'general',
|
|
uploadingFiles: [],
|
|
|
|
async init() {
|
|
// Load i18n translations
|
|
await I18n.loadModule('cms');
|
|
|
|
// Guard against duplicate initialization
|
|
if (window._storeMediaInitialized) return;
|
|
window._storeMediaInitialized = true;
|
|
|
|
storeMediaLog.info('Initializing media library...');
|
|
|
|
try {
|
|
// IMPORTANT: Call parent init first to set storeCode from URL
|
|
const parentInit = data().init;
|
|
if (parentInit) {
|
|
await parentInit.call(this);
|
|
}
|
|
|
|
// Initialize pagination per_page from PlatformSettings
|
|
if (window.PlatformSettings) {
|
|
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
|
}
|
|
|
|
await this.loadMedia();
|
|
} catch (err) {
|
|
storeMediaLog.error('Failed to initialize media library:', err);
|
|
this.error = err.message || 'Failed to initialize media library';
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async loadMedia() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
skip: (this.pagination.page - 1) * this.pagination.per_page,
|
|
limit: this.pagination.per_page
|
|
});
|
|
|
|
if (this.filters.search) {
|
|
params.append('search', this.filters.search);
|
|
}
|
|
if (this.filters.type) {
|
|
params.append('media_type', this.filters.type);
|
|
}
|
|
if (this.filters.folder) {
|
|
params.append('folder', this.filters.folder);
|
|
}
|
|
|
|
storeMediaLog.info(`Loading media: /api/v1/store/media?${params}`);
|
|
|
|
const response = await apiClient.get(`/store/media?${params.toString()}`);
|
|
|
|
if (response.ok) {
|
|
const data = response.data;
|
|
this.media = data.media || [];
|
|
this.pagination.total = data.total || 0;
|
|
this.pagination.pages = Math.ceil(this.pagination.total / this.pagination.per_page);
|
|
|
|
// Update stats
|
|
await this.loadStats();
|
|
|
|
storeMediaLog.info(`Loaded ${this.media.length} media files`);
|
|
} else {
|
|
throw new Error(response.message || 'Failed to load media');
|
|
}
|
|
} catch (err) {
|
|
storeMediaLog.error('Failed to load media:', err);
|
|
this.error = err.message || 'Failed to load media library';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async loadStats() {
|
|
// Calculate stats from loaded media (simplified)
|
|
// In production, you might have a separate stats endpoint
|
|
try {
|
|
// Get all media without pagination for stats
|
|
const allResponse = await apiClient.get('/store/media?limit=1000');
|
|
if (allResponse.ok) {
|
|
const allMedia = allResponse.data.media || [];
|
|
this.stats.total = allResponse.data.total || 0;
|
|
this.stats.images = allMedia.filter(m => m.media_type === 'image').length;
|
|
this.stats.videos = allMedia.filter(m => m.media_type === 'video').length;
|
|
this.stats.documents = allMedia.filter(m => m.media_type === 'document').length;
|
|
}
|
|
} catch (err) {
|
|
storeMediaLog.warn('Could not load stats:', err);
|
|
}
|
|
},
|
|
|
|
selectMedia(item) {
|
|
this.selectedMedia = item;
|
|
this.editingMedia = {
|
|
filename: item.original_filename || item.filename,
|
|
alt_text: item.alt_text || '',
|
|
description: item.description || '',
|
|
folder: item.folder || 'general'
|
|
};
|
|
this.showDetailModal = true;
|
|
},
|
|
|
|
async saveMediaDetails() {
|
|
if (!this.selectedMedia) return;
|
|
|
|
this.saving = true;
|
|
|
|
try {
|
|
const response = await apiClient.put(`/store/media/${this.selectedMedia.id}`, {
|
|
filename: this.editingMedia.filename,
|
|
alt_text: this.editingMedia.alt_text,
|
|
description: this.editingMedia.description,
|
|
folder: this.editingMedia.folder
|
|
});
|
|
|
|
if (response.ok) {
|
|
Utils.showToast(I18n.t('cms.messages.media_updated_successfully'), 'success');
|
|
this.showDetailModal = false;
|
|
await this.loadMedia();
|
|
} else {
|
|
throw new Error(response.message || 'Failed to update media');
|
|
}
|
|
} catch (err) {
|
|
storeMediaLog.error('Failed to save media:', err);
|
|
this.showToast(err.message || 'Failed to save changes', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async deleteMedia() {
|
|
if (!this.selectedMedia) return;
|
|
|
|
this.saving = true;
|
|
|
|
try {
|
|
const response = await apiClient.delete(`/store/media/${this.selectedMedia.id}`);
|
|
|
|
if (response.ok) {
|
|
Utils.showToast(I18n.t('cms.messages.media_deleted_successfully'), 'success');
|
|
this.showDetailModal = false;
|
|
this.selectedMedia = null;
|
|
await this.loadMedia();
|
|
} else {
|
|
throw new Error(response.message || 'Failed to delete media');
|
|
}
|
|
} catch (err) {
|
|
storeMediaLog.error('Failed to delete media:', err);
|
|
this.showToast(err.message || 'Failed to delete media', 'error');
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
handleDrop(event) {
|
|
this.isDragging = false;
|
|
const files = event.dataTransfer.files;
|
|
if (files.length) {
|
|
this.uploadFiles(files);
|
|
}
|
|
},
|
|
|
|
handleFileSelect(event) {
|
|
const files = event.target.files;
|
|
if (files.length) {
|
|
this.uploadFiles(files);
|
|
}
|
|
// Reset input
|
|
event.target.value = '';
|
|
},
|
|
|
|
async uploadFiles(files) {
|
|
storeMediaLog.info(`Uploading ${files.length} files...`);
|
|
|
|
for (const file of files) {
|
|
const uploadItem = {
|
|
name: file.name,
|
|
status: 'uploading',
|
|
error: null
|
|
};
|
|
this.uploadingFiles.push(uploadItem);
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
// Use apiClient.postFormData for automatic auth handling
|
|
const response = await apiClient.postFormData(
|
|
`/store/media/upload?folder=${this.uploadFolder}`,
|
|
formData
|
|
);
|
|
|
|
if (response.ok) {
|
|
uploadItem.status = 'success';
|
|
storeMediaLog.info(`Uploaded: ${file.name}`);
|
|
} else {
|
|
uploadItem.status = 'error';
|
|
uploadItem.error = response.message || 'Upload failed';
|
|
storeMediaLog.error(`Upload failed for ${file.name}:`, response);
|
|
}
|
|
} catch (err) {
|
|
uploadItem.status = 'error';
|
|
uploadItem.error = err.message || 'Upload failed';
|
|
storeMediaLog.error(`Upload error for ${file.name}:`, err);
|
|
}
|
|
}
|
|
|
|
// Refresh media list after all uploads
|
|
await this.loadMedia();
|
|
|
|
// Clear upload list after a delay
|
|
setTimeout(() => {
|
|
this.uploadingFiles = this.uploadingFiles.filter(f => f.status === 'uploading');
|
|
}, 3000);
|
|
},
|
|
|
|
formatFileSize(bytes) {
|
|
if (!bytes) return '0 B';
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let i = 0;
|
|
while (bytes >= 1024 && i < units.length - 1) {
|
|
bytes /= 1024;
|
|
i++;
|
|
}
|
|
return `${bytes.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
},
|
|
|
|
copyToClipboard(text) {
|
|
if (!text) return;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
Utils.showToast(I18n.t('cms.messages.url_copied_to_clipboard'), 'success');
|
|
}).catch(() => {
|
|
Utils.showToast(I18n.t('cms.messages.failed_to_copy_url'), 'error');
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
storeMediaLog.info('Loaded successfully');
|