-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+ Click the + button to add more images from the media library
+
+
+
+ {{ media_picker_modal(
+ id='media-picker-main',
+ show_var='showMediaPicker',
+ vendor_id_var='product?.vendor_id',
+ title='Select Main Image'
+ ) }}
+
+ {{ media_picker_modal(
+ id='media-picker-additional',
+ show_var='showMediaPickerAdditional',
+ vendor_id_var='product?.vendor_id',
+ multi_select=true,
+ title='Select Additional Images'
+ ) }}
+
@@ -349,5 +425,6 @@
{% endblock %}
{% block extra_scripts %}
+
{% endblock %}
diff --git a/app/templates/shared/macros/modals.html b/app/templates/shared/macros/modals.html
index 064ac67c..7962c73d 100644
--- a/app/templates/shared/macros/modals.html
+++ b/app/templates/shared/macros/modals.html
@@ -699,3 +699,216 @@
{% endmacro %}
+
+
+{#
+ Media Picker Modal
+ ==================
+ A modal for selecting images from the vendor's media library.
+ Supports browsing existing media, uploading new files, and single/multi-select.
+
+ Parameters:
+ - id: Unique modal ID (default: 'mediaPicker')
+ - show_var: Alpine.js variable controlling visibility (default: 'showMediaPicker')
+ - vendor_id_var: Variable containing vendor ID (default: 'vendorId')
+ - on_select: Callback function when images are selected (default: 'onMediaSelected')
+ - multi_select: Allow selecting multiple images (default: false)
+ - title: Modal title (default: 'Select Image')
+
+ Required Alpine.js state:
+ - showMediaPicker: boolean
+ - vendorId: number
+ - mediaPickerState: object (managed by initMediaPicker())
+ - onMediaSelected(images): callback function
+
+ Usage:
+ {% from 'shared/macros/modals.html' import media_picker_modal %}
+ {{ media_picker_modal(vendor_id_var='form.vendor_id', on_select='setMainImage', multi_select=false) }}
+#}
+{% macro media_picker_modal(id='mediaPicker', show_var='showMediaPicker', vendor_id_var='vendorId', on_select='onMediaSelected', multi_select=false, title='Select Image') %}
+
+ {# Backdrop #}
+
+
+ {# Modal Container #}
+
+
+ {# Header #}
+
+
+ {{ title }}
+ {% if multi_select %}
+ (select multiple)
+ {% endif %}
+
+
+
+
+ {# Toolbar #}
+
+
+ {# Search #}
+
+
+ {# Upload Button #}
+
+
+
+
+
+ {# Upload Progress #}
+
+
+
+ {# Media Grid #}
+
+ {# Loading State #}
+
+
+
+
+ {# Empty State #}
+
+
+
No images found
+
Upload an image to get started
+
+
+ {# Image Grid #}
+
+
+
+
![]()
+ {# Selection Indicator #}
+
+ {# Filename tooltip #}
+
+
+
+
+
+ {# Pagination #}
+
+
+
+
+
+ {# Footer #}
+
+
+
+ selected
+
+
+
+
+
+
+
+
+
+
+{% endmacro %}
diff --git a/models/schema/vendor_product.py b/models/schema/vendor_product.py
index cfffca01..cec20768 100644
--- a/models/schema/vendor_product.py
+++ b/models/schema/vendor_product.py
@@ -107,6 +107,7 @@ class VendorProductDetail(BaseModel):
availability_source: str | None = None
primary_image_url: str | None = None
primary_image_url_source: str | None = None
+ additional_images: list[str] | None = None
is_digital: bool | None = None
product_type: str | None = None
# Vendor-specific fields
@@ -138,6 +139,10 @@ class VendorProductDetail(BaseModel):
# Translations
marketplace_translations: dict | None = None
vendor_translations: dict | None = None
+ # Convenience fields for UI display
+ title: str | None = None
+ description: str | None = None
+ image_url: str | None = None # Alias for primary_image_url
# Timestamps
created_at: str | None = None
updated_at: str | None = None
@@ -177,8 +182,9 @@ class VendorProductCreate(BaseModel):
tax_rate_percent: int | None = 17 # Default Luxembourg VAT
availability: str | None = None
- # Image
+ # Images
primary_image_url: str | None = None
+ additional_images: list[str] | None = None
# Status
is_active: bool = True
@@ -227,6 +233,7 @@ class VendorProductUpdate(BaseModel):
# Images
primary_image_url: str | None = None
+ additional_images: list[str] | None = None
# Optional supplier info
supplier: str | None = None
diff --git a/static/admin/js/vendor-product-create.js b/static/admin/js/vendor-product-create.js
index 610f7e86..4465af20 100644
--- a/static/admin/js/vendor-product-create.js
+++ b/static/admin/js/vendor-product-create.js
@@ -24,6 +24,9 @@ function adminVendorProductCreate() {
// Inherit base layout state
...data(),
+ // Include media picker functionality (vendor ID getter will be bound via loadMediaLibrary override)
+ ...mediaPickerMixin(() => null, false),
+
// Set page identifier
currentPage: 'vendor-products',
@@ -53,8 +56,9 @@ function adminVendorProductCreate() {
currency: 'EUR',
tax_rate_percent: 17,
availability: '',
- // Image
+ // Images
primary_image_url: '',
+ additional_images: [],
// Status
is_active: true,
is_featured: false,
@@ -204,8 +208,10 @@ function adminVendorProductCreate() {
tax_rate_percent: this.form.tax_rate_percent !== null
? parseInt(this.form.tax_rate_percent) : 17,
availability: this.form.availability || null,
- // Image
+ // Images
primary_image_url: this.form.primary_image_url?.trim() || null,
+ additional_images: this.form.additional_images?.length > 0
+ ? this.form.additional_images : null,
// Status
is_active: this.form.is_active,
is_featured: this.form.is_featured,
@@ -230,6 +236,168 @@ function adminVendorProductCreate() {
} finally {
this.saving = false;
}
+ },
+
+ // === Media Picker Overrides ===
+ // These override the mixin methods to use proper form context
+
+ /**
+ * Load media library for the selected vendor
+ */
+ async loadMediaLibrary() {
+ const vendorId = this.form?.vendor_id;
+
+ if (!vendorId) {
+ adminVendorProductCreateLog.warn('Media picker: No vendor ID selected');
+ 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) {
+ adminVendorProductCreateLog.error('Failed to load media library:', error);
+ Utils.showToast('Failed to load media library', 'error');
+ } finally {
+ this.mediaPickerState.loading = false;
+ }
+ },
+
+ /**
+ * Load more media (pagination)
+ */
+ async loadMoreMedia() {
+ const vendorId = this.form?.vendor_id;
+ 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) {
+ adminVendorProductCreateLog.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 = this.form?.vendor_id;
+
+ if (!vendorId) {
+ Utils.showToast('Please select a vendor first', 'error');
+ return;
+ }
+
+ if (!file.type.startsWith('image/')) {
+ Utils.showToast('Please select an image file', 'error');
+ return;
+ }
+
+ if (file.size > 10 * 1024 * 1024) {
+ Utils.showToast('Image must be less than 10MB', '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) {
+ this.mediaPickerState.media.unshift(response.media);
+ this.mediaPickerState.total++;
+ this.toggleMediaSelection(response.media);
+ Utils.showToast('Image uploaded successfully', 'success');
+ }
+ } catch (error) {
+ adminVendorProductCreateLog.error('Failed to upload image:', error);
+ Utils.showToast(error.message || 'Failed to upload image', 'error');
+ } finally {
+ this.mediaPickerState.uploading = false;
+ event.target.value = '';
+ }
+ },
+
+ /**
+ * Set main image from media picker
+ */
+ setMainImage(media) {
+ this.form.primary_image_url = media.url;
+ adminVendorProductCreateLog.info('Main image set:', media.url);
+ },
+
+ /**
+ * Add additional images from media picker
+ */
+ addAdditionalImages(mediaList) {
+ const newUrls = mediaList.map(m => m.url);
+ this.form.additional_images = [
+ ...this.form.additional_images,
+ ...newUrls
+ ];
+ adminVendorProductCreateLog.info('Additional images added:', newUrls);
+ },
+
+ /**
+ * Remove an additional image by index
+ */
+ removeAdditionalImage(index) {
+ this.form.additional_images.splice(index, 1);
+ },
+
+ /**
+ * Clear the main image
+ */
+ clearMainImage() {
+ this.form.primary_image_url = '';
}
};
}
diff --git a/static/admin/js/vendor-product-edit.js b/static/admin/js/vendor-product-edit.js
index c2aa2b8c..846d4894 100644
--- a/static/admin/js/vendor-product-edit.js
+++ b/static/admin/js/vendor-product-edit.js
@@ -28,6 +28,9 @@ function adminVendorProductEdit() {
// Inherit base layout state
...data(),
+ // Include media picker functionality (vendor ID comes from loaded product)
+ ...mediaPickerMixin(() => null, false),
+
// Set page identifier
currentPage: 'vendor-products',
@@ -60,8 +63,9 @@ function adminVendorProductEdit() {
currency: 'EUR',
tax_rate_percent: 17,
availability: '',
- // Image
+ // Images
primary_image_url: '',
+ additional_images: [],
// Product type & status
is_digital: false,
is_active: true,
@@ -127,8 +131,9 @@ function adminVendorProductEdit() {
currency: response.currency || 'EUR',
tax_rate_percent: response.tax_rate_percent ?? 17,
availability: response.availability || '',
- // Image
+ // Images
primary_image_url: response.primary_image_url || '',
+ additional_images: response.additional_images || [],
// Product type & status
is_digital: response.is_digital ?? false,
is_active: response.is_active ?? true,
@@ -241,8 +246,10 @@ function adminVendorProductEdit() {
tax_rate_percent: this.form.tax_rate_percent !== null && this.form.tax_rate_percent !== ''
? parseInt(this.form.tax_rate_percent) : null,
availability: this.form.availability || null,
- // Image
+ // Images
primary_image_url: this.form.primary_image_url?.trim() || null,
+ additional_images: this.form.additional_images?.length > 0
+ ? this.form.additional_images : null,
// Status
is_digital: this.form.is_digital,
is_active: this.form.is_active,
@@ -271,6 +278,168 @@ function adminVendorProductEdit() {
} finally {
this.saving = false;
}
+ },
+
+ // === Media Picker Overrides ===
+ // These override the mixin methods to use proper form/product context
+
+ /**
+ * Load media library for the product's vendor
+ */
+ async loadMediaLibrary() {
+ const vendorId = this.product?.vendor_id;
+
+ if (!vendorId) {
+ adminVendorProductEditLog.warn('Media picker: 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) {
+ adminVendorProductEditLog.error('Failed to load media library:', error);
+ Utils.showToast('Failed to load media library', 'error');
+ } finally {
+ this.mediaPickerState.loading = false;
+ }
+ },
+
+ /**
+ * Load more media (pagination)
+ */
+ async loadMoreMedia() {
+ const vendorId = this.product?.vendor_id;
+ 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) {
+ adminVendorProductEditLog.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 = this.product?.vendor_id;
+
+ if (!vendorId) {
+ Utils.showToast('No vendor associated with this product', 'error');
+ return;
+ }
+
+ if (!file.type.startsWith('image/')) {
+ Utils.showToast('Please select an image file', 'error');
+ return;
+ }
+
+ if (file.size > 10 * 1024 * 1024) {
+ Utils.showToast('Image must be less than 10MB', '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) {
+ this.mediaPickerState.media.unshift(response.media);
+ this.mediaPickerState.total++;
+ this.toggleMediaSelection(response.media);
+ Utils.showToast('Image uploaded successfully', 'success');
+ }
+ } catch (error) {
+ adminVendorProductEditLog.error('Failed to upload image:', error);
+ Utils.showToast(error.message || 'Failed to upload image', 'error');
+ } finally {
+ this.mediaPickerState.uploading = false;
+ event.target.value = '';
+ }
+ },
+
+ /**
+ * Set main image from media picker
+ */
+ setMainImage(media) {
+ this.form.primary_image_url = media.url;
+ adminVendorProductEditLog.info('Main image set:', media.url);
+ },
+
+ /**
+ * Add additional images from media picker
+ */
+ addAdditionalImages(mediaList) {
+ const newUrls = mediaList.map(m => m.url);
+ this.form.additional_images = [
+ ...this.form.additional_images,
+ ...newUrls
+ ];
+ adminVendorProductEditLog.info('Additional images added:', newUrls);
+ },
+
+ /**
+ * Remove an additional image by index
+ */
+ removeAdditionalImage(index) {
+ this.form.additional_images.splice(index, 1);
+ },
+
+ /**
+ * Clear the main image
+ */
+ clearMainImage() {
+ this.form.primary_image_url = '';
}
};
}
diff --git a/static/shared/js/media-picker.js b/static/shared/js/media-picker.js
new file mode 100644
index 00000000..9e9306fa
--- /dev/null
+++ b/static/shared/js/media-picker.js
@@ -0,0 +1,303 @@
+// 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
+ * }
+ */
+
+/**
+ * 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) {
+ console.warn('Media picker: 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) {
+ console.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) {
+ console.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) {
+ console.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;
+ }
+ console.log('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
+ ];
+ }
+ console.log('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 };
+}