Files
orion/static/shared/js/media-picker.js
Samir Boulahtit 65e5c55266 fix: resolve architecture validation errors and warnings
- Fix JS-008: Replace raw fetch() with apiClient in letzshop-vendor-directory.js
- Fix JS-005: Add init guard to letzshop-vendor-directory.js
- Fix JS-004: Increase search region in validator (800→2000 chars) to detect
  currentPage in files with setup code before return statement
- Fix JS-001: Use centralized logger in media-picker.js
- Fix API-002: Move database query from onboarding.py to order_service.py
- Fix FE-001: Add noqa comment to search.html (shop uses custom themed pagination)
- Add audit validator to validate_all.py script
- Update frontend.yaml with vendor exclusion pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 20:36:01 +01:00

308 lines
9.9 KiB
JavaScript

// static/shared/js/media-picker.js
/**
* Media Picker Helper Functions
*
* Provides Alpine.js mixin for media library picker functionality.
* Used in product create/edit forms to select images from vendor's media library.
*
* Usage:
* In your Alpine component:
* return {
* ...mediaPickerMixin(vendorIdGetter, multiSelect),
* // your other data/methods
* }
*/
// Use centralized logger
const mediaPickerLog = window.LogConfig.loggers.mediaPicker ||
window.LogConfig.createLogger('mediaPicker', false);
/**
* Create media picker mixin for Alpine.js components
*
* @param {Function} vendorIdGetter - Function that returns the current vendor ID
* @param {boolean} multiSelect - Allow selecting multiple images
* @returns {Object} Alpine.js mixin object
*/
function mediaPickerMixin(vendorIdGetter, multiSelect = false) {
return {
// Modal visibility
showMediaPicker: false,
showMediaPickerAdditional: false,
// Picker state
mediaPickerState: {
loading: false,
uploading: false,
media: [],
selected: [],
total: 0,
skip: 0,
limit: 24,
search: '',
},
// Which picker is active (main or additional)
activePickerTarget: 'main',
/**
* Open media picker for main image
*/
openMediaPickerMain() {
this.activePickerTarget = 'main';
this.mediaPickerState.selected = [];
this.showMediaPicker = true;
},
/**
* Open media picker for additional images
*/
openMediaPickerAdditional() {
this.activePickerTarget = 'additional';
this.mediaPickerState.selected = [];
this.showMediaPickerAdditional = true;
},
/**
* Load media library from API
*/
async loadMediaLibrary() {
const vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
if (!vendorId) {
mediaPickerLog.warn('No vendor ID available');
return;
}
this.mediaPickerState.loading = true;
this.mediaPickerState.skip = 0;
try {
const params = new URLSearchParams({
skip: '0',
limit: this.mediaPickerState.limit.toString(),
media_type: 'image',
});
if (this.mediaPickerState.search) {
params.append('search', this.mediaPickerState.search);
}
const response = await apiClient.get(
`/admin/media/vendors/${vendorId}?${params.toString()}`
);
this.mediaPickerState.media = response.media || [];
this.mediaPickerState.total = response.total || 0;
} catch (error) {
mediaPickerLog.error('Failed to load media library:', error);
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: 'Failed to load media library', type: 'error' }
}));
} finally {
this.mediaPickerState.loading = false;
}
},
/**
* Load more media (pagination)
*/
async loadMoreMedia() {
const vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
if (!vendorId) return;
this.mediaPickerState.loading = true;
this.mediaPickerState.skip += this.mediaPickerState.limit;
try {
const params = new URLSearchParams({
skip: this.mediaPickerState.skip.toString(),
limit: this.mediaPickerState.limit.toString(),
media_type: 'image',
});
if (this.mediaPickerState.search) {
params.append('search', this.mediaPickerState.search);
}
const response = await apiClient.get(
`/admin/media/vendors/${vendorId}?${params.toString()}`
);
this.mediaPickerState.media = [
...this.mediaPickerState.media,
...(response.media || [])
];
} catch (error) {
mediaPickerLog.error('Failed to load more media:', error);
} finally {
this.mediaPickerState.loading = false;
}
},
/**
* Upload a new media file
*/
async uploadMediaFile(event) {
const file = event.target.files?.[0];
if (!file) return;
const vendorId = typeof vendorIdGetter === 'function' ? vendorIdGetter() : vendorIdGetter;
if (!vendorId) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: 'Please select a vendor first', type: 'error' }
}));
return;
}
// Validate file type
if (!file.type.startsWith('image/')) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: 'Please select an image file', type: 'error' }
}));
return;
}
// Validate file size (10MB max)
if (file.size > 10 * 1024 * 1024) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: 'Image must be less than 10MB', type: 'error' }
}));
return;
}
this.mediaPickerState.uploading = true;
try {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.postFormData(
`/admin/media/vendors/${vendorId}/upload?folder=products`,
formData
);
if (response.success && response.media) {
// Add to beginning of media list
this.mediaPickerState.media.unshift(response.media);
this.mediaPickerState.total++;
// Auto-select the uploaded image
this.toggleMediaSelection(response.media);
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: 'Image uploaded successfully', type: 'success' }
}));
}
} catch (error) {
mediaPickerLog.error('Failed to upload image:', error);
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: error.message || 'Failed to upload image', type: 'error' }
}));
} finally {
this.mediaPickerState.uploading = false;
// Clear the file input
event.target.value = '';
}
},
/**
* Toggle media selection
*/
toggleMediaSelection(media) {
const index = this.mediaPickerState.selected.findIndex(m => m.id === media.id);
if (index > -1) {
// Deselect
this.mediaPickerState.selected.splice(index, 1);
} else {
if (multiSelect) {
// Multi-select: add to selection
this.mediaPickerState.selected.push(media);
} else {
// Single-select: replace selection
this.mediaPickerState.selected = [media];
}
}
},
/**
* Check if media is selected
*/
isMediaSelected(mediaId) {
return this.mediaPickerState.selected.some(m => m.id === mediaId);
},
/**
* Confirm selection and call the appropriate callback
*/
confirmMediaSelection() {
const selected = this.mediaPickerState.selected;
if (selected.length === 0) return;
if (this.activePickerTarget === 'main') {
// Main image: use first selected
this.setMainImage(selected[0]);
this.showMediaPicker = false;
} else {
// Additional images: add all selected
this.addAdditionalImages(selected);
this.showMediaPickerAdditional = false;
}
// Clear selection
this.mediaPickerState.selected = [];
},
/**
* Set the main image (override in your component)
*/
setMainImage(media) {
if (this.form) {
this.form.primary_image_url = media.url;
}
mediaPickerLog.info('Main image set:', media.url);
},
/**
* Add additional images (override in your component)
*/
addAdditionalImages(mediaList) {
if (this.form && Array.isArray(this.form.additional_images)) {
const newUrls = mediaList.map(m => m.url);
this.form.additional_images = [
...this.form.additional_images,
...newUrls
];
}
mediaPickerLog.info('Additional images added:', mediaList.map(m => m.url));
},
/**
* Remove an additional image by index
*/
removeAdditionalImage(index) {
if (this.form && Array.isArray(this.form.additional_images)) {
this.form.additional_images.splice(index, 1);
}
},
/**
* Clear the main image
*/
clearMainImage() {
if (this.form) {
this.form.primary_image_url = '';
}
},
};
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = { mediaPickerMixin };
}