Appearance
Meduse product images, files, and Vimeo workflows
This guide follows the media controls on the product grid through their API, stored catalog references, and source-defined connector work. The interface map and API inventory already list the screens and routes; the backend, queues, and connector map cover their wider request and worker context.
The product grid has two separate media dialogs. Images manages the gallery and image roles. Files manages a dimensions GIF, a PDF notice, and the product's unboxing-video URL. Vimeo Stories is a separate screen with per-store video contexts. The unboxing URL field is not a Vimeo Story record.
Product images
The image dialog stages uploads before saving catalog references. Its multipart request carries product_id and one or more images[] entries to POST /api/upload-images. The backend writes UUID-named files to the public disk under media/catalog/product/tmp/{product_id} and returns temporary paths. The dialog then submits the add, update, and removal groups to POST /api/save-images.
View diagram source
sequenceDiagram
actor Editor
participant UI as Product image dialog
participant API as Meduse API
participant Disk as Public media storage
participant Catalog as catalog_product.raw_values
participant Event as ProductImagesUpdated
participant Job as Tenant image handler
Editor->>UI: Choose image files
UI->>API: POST upload-images (product_id, images[])
API->>Disk: Write under tmp/{product_id}/{uuid}
API-->>UI: Return temporary paths
Editor->>UI: Reorder, replace, edit roles or mark removals
UI->>API: POST save-images (toAddImages, toUpdateImages, toRemoveImages)
API->>Disk: Move newly added staged files to product media
API->>Catalog: Save image paths and metadata
API->>Event: Dispatch changed product IDs and tenant context
Event->>Job: Select the matching tenant handler
In the Gestion view, drag-and-drop changes each image's position. Position zero is the UI's Décor image. It is separate from the image role, which the UI labels Coloris and keeps unique across the gallery. The role selector can also assign thumbnail, small_image, rollover_image, swatch_image, matelas_image, has_human_or_animal, and other_images. The backend stores each role under the product's <all_channels> raw value; other_images is a list, while the other roles point to an image record. Each record carries its path, label, position, and hideFromProductPage flag.
The UI's position metadata is not the same as export array order. The update handler replaces matching other_images entries in their existing array slots; the Menzzo and MeublePromo image processors serialize that stored array without sorting by position. A drag-and-drop change therefore does not prove the exported image sequence changed. Also, the update handler's role-cleanup list omits small_image and swatch_image: changing an image out of either role can leave its previous reference behind. Inspect those raw-value slots as well as the dialog after a role change. Update/cleanup handler
New images begin as other_images. Edits to labels, roles, hide state, and order are debounced and sent as update operations. A temporary path submitted in the update group is normalized into an add operation, which is how the replace control stages a replacement image. Removals are buffered until save; restoring a marked image in the dialog cancels that removal. On the server, removal clears matching role references from raw_values; it does not delete the stored binary. A replacement or removal therefore does not, by itself, prove that old files were physically removed.
The browser's chooser accepts image/* and advertises a 20 MB limit. The backend is narrower: up to 20 files per request, each at most 5 MiB, with JPG, JPEG, PNG, or WEBP extensions and matching JPEG, PNG, or WEBP MIME types. Treat the request validator as the effective contract. Invalid uploads receive HTTP 422.
The image save response must be checked for its JSON success value. Validation errors return 422, while caught or per-image save errors can return HTTP 200 with success: false. The dialog uses that JSON value to choose its toast state. For saved product groups, a successful response means the catalog write and image event path completed; it does not prove the connector completed. A success: false response is not a no-op: per-image errors can coexist with persisted changes and an event dispatched for the collected product IDs. Read back the affected product and job/file state before retrying. Partial-write and response path
Product files
The Files dialog uploads GIF and PDF separately through POST /api/upload-file. Its multipart payload contains product_id, action (gif or pdf), and files[]. The server stages a UUID-named file under media/catalog/product/tmp/files/{product_id}. Enregistrer sends one POST /api/save-files payload shaped as:
json
{
"data": {
"gif": { "toAddGif": {}, "toRemoveGif": [] },
"pdf": { "toAddPdf": {}, "toRemovePdf": [] },
"video": { "toAddVideo": {}, "toRemoveVideo": [] }
}
}The staged paths must match the product's temporary directory and UUID format, exist on the public disk, and have an allowed MIME type. The backend accepts one file up to 10 MiB per upload: gif means .gif / image/gif; pdf means .pdf / application/pdf. The GIF chooser currently advertises GIF, JPG, and PNG, but the server accepts GIF only, so the UI hint does not guarantee that a JPG or PNG will pass validation.
When saved, a GIF or PDF is moved into product media storage and its path is written to catalog_product.raw_values under url_visuel_dimensions_produit or pdf_notice. Removing either clears its <all_channels> reference without deleting the stored binary. The video group is a string URL stored under url_video_deballage; it is not uploaded as a file and is not the Vimeo Stories workflow.
For the upload paths reviewed here, the controllers persist references in raw_values; they do not create a file_storage_file_info row. That model and table are used by a separate image-migration command in this source revision, so they are not the upload receipt to look for. Caught save errors can return HTTP 200 with success: false; check the JSON result as well as the HTTP status.
Tenant-specific connector work
The upload requests run through the API's tenant middleware. The backend media handlers choose work from that tenant context; a selected store/channel in the UI is not the same value as the tenant ID used by these handlers.
| Tenant context in source | Image save result | Product-file save result |
|---|---|---|
No tenant (null, the default path) | ProductImagesUpdated is handled by the Menzzo provider. It dispatches ProductImagesProcessor on export-product-images; the job writes an image-reference CSV to the Menzzo FTP pending directory. | The controller pushes Menzzo ProductFilesProcessor on export-product-files; it writes the GIF/PDF/unboxing-URL references to the Menzzo FTP pending directory. |
meuble_promo | The MeublePromo event listener dispatches its image processor to export-product-images, which writes its tenant-specific pending CSV. | The controller pushes the MeublePromo file processor to export-product-files, which writes its tenant-specific pending CSV. |
mosaikasa | The Mosaikasa event listener dispatches its image processor. That processor invokes UpdateShopifyProductImagesJob synchronously for each product. | The reviewed save-files controller has no Mosaikasa branch, so it does not establish a Mosaikasa file export. |
| Any other tenant ID | No matching image listener appears in the reviewed Menzzo, MeublePromo, and Mosaikasa providers. | No matching product-file processor branch appears in the reviewed controller. |
These are source-defined dispatches and destinations, not proof that an FTP consumer imported a CSV, Shopify accepted every image, or a storefront rendered the result. The effective queue connection and worker state also matter; see queues and job processing before interpreting a dispatch or tracked-job status.
Vimeo Stories
The Vimeo Stories screen lists product records and lets an operator add or replace Vimeo IDs in one or more contexts. The UI contexts are Toutes les boutiques (default) plus fr, fr_be, de, de_at, it, nl, nl_be, es, and pt. The backend maps default to all nine configured store codes; each named context maps to its corresponding store code. When adding, the operator can select multiple contexts. The edit modal submits one context, but the backend's modify action removes the matching old video ID from every stored context before adding its replacement under the selected context. Editing one copy can therefore remove the old ID from other store contexts. Delete and restore are per video and per context. Cross-context replacement
The product list posts page, perPage, filters, sortData, and the selected channel to POST /api/vimeo-stories/products. In the reviewed controller, the page, filters, and sort are consumed, but channel is not applied to the query. Do not treat that request field as server-side tenant or store scoping. The existing interface map also records the Vimeo menu/route tenant-guard mismatch.
Tester le lien checks the entered value for a six-or-more-digit ID and opens a constructed https://vimeo.com/{id} page in a new tab. That is a format check and browser handoff; the button does not call the Vimeo API or prove that the video exists, is public, or is accessible to the configured API account. Saving posts action, context, url, and video_id to POST /api/vimeo-stories/products/{product}/videos. The frontend sends one request per selected context and uses Promise.allSettled: it reports partial success when some context writes succeed and others fail. The backend validates the action, known context, Vimeo ID format, and product/video lookup, then stores video metadata in catalog_product_stories.contexts and maps it to the product's raw_values.product_stories value. This save handler does not enqueue the separate sync job.
Delete is a soft-delete: the server marks the context entry with deleted and deleted_at; restore removes those flags. The UI says deleted videos will not be synchronized to the site. These actions update Meduse records; they do not delete the video from Vimeo.
The Synchroniser depuis Vimeo controls use a different path. They post either { "product": productId } or {} to POST /api/vimeo-stories/sync. The API returns 202 with a job ID after pushing SyncVimeoProductStoriesJob to vimeo-stories. That tracked job invokes menzzo:vimeo:sync-product-stories, which reads Vimeo folders/videos and updates Meduse story data. The response and UI toast report submission through the configured queue connection, which may execute inline or through a worker. They do not establish that Vimeo data was fetched successfully or a storefront changed.
The detail panel shows context coverage (OK when local data has videos, otherwise Manquant), a last-sync label, and a status label. The source statuses are updated, no_changes, no_videos, and failed. Keep two caveats in mind when reading them:
- Manual add, edit, delete, and restore all persist through a helper that also sets
last_synced_atandlast_sync_status = updated. The displayed “last sync” and “Succès” can therefore reflect a Meduse save rather than a completed Vimeo API sync. - The tracked job marks failure when the Artisan call throws, but does not inspect its numeric exit code. The command can return a nonzero code when the service reports failure, and the service can record per-product
failedstatuses while finishing the wider run. Check both the tracked job and the product's sync status; neither alone proves every product succeeded.
Invalid action, context, URL/ID, or missing video data returns a validation error (typically HTTP 422); a missing product or video/context can return 404. The UI performs local checks first and shows a generic save/sync error for rejected requests, so use the response body and product-level status when diagnosing a failed test.
Nonproduction validation checkpoints
Use a disposable product and isolated nonproduction tenant with nonproduction storage and connector settings. These are checkpoints for a human-led test; no production command or live credential is part of this guide.
- Upload one small JPEG or WEBP, confirm the staging response contains a temporary path, then save a changed order, label, and role. Verify the final file path and image metadata in the test product's
raw_values, compare stored/exported array order separately fromposition, and check for stalesmall_image/swatch_imagerole references. Then confirm the expected tenant-specific event/job path. Test removal on a disposable file and verify that the catalog reference clears; do not expect physical deletion from this handler. - Upload one GIF and one PDF in separate tests. Confirm that matching extensions and MIME types pass, and that a JPG/PNG submitted as the GIF action fails validation. After save, verify the two stored references and the relevant product-file export record or sandbox pending file.
- In a test Vimeo account or mocked nonproduction integration, add the same video to two store contexts, modify one copy and check both contexts for the backend-wide removal behavior. Also test a partial multi-context save, soft-delete and restore one entry, and request a product sync. Verify the context JSON and product
product_storiesmapping, then compare the tracked job withlast_sync_status. A green save toast, a Vimeo tab opened by Tester le lien, or an accepted job ID is only an intermediate signal. - Confirm connector completion at the nonproduction destination: inspect the sandbox FTP consumer result or Shopify test-store state, as applicable. A Meduse save, queue dispatch, or job-finished label is not storefront acceptance.
Do not use production products, storage, queues, FTP, Shopify stores, or Vimeo accounts for these checks. For broader production diagnostic or recovery procedures, follow the individual-command approval boundary in documentation maintenance.