Product Editor
The add and edit product page for a store admin. Handles unsaved changes, SKU errors from the server and someone else editing at once.
Product Editor is the page a merchant uses to add or change a product: title and description, media, pricing with live profit and margin, stock per location, shipping, variants, a search result preview, and a rail for status, sales channels, type, vendor, collections and tags. It renders the page frame with breadcrumb, status badge, Preview and a menu for Duplicate, Archive and Delete, and leaves the sidebar to you.
Every edit goes into one draft. The save bar slides up when the draft differs from the saved product and goes away again if you undo your edits by hand, because the check is a deep compare. ⌘S saves, Discard asks first, and Preview or the breadcrumb nudge the bar instead of leaving. A second try within a few seconds asks whether to leave without saving. onSave can return field errors, which are shown on their fields while the first one scrolls into view and gets focus, or a conflict, which turns the bar into “Leo Park changed this product 2 min ago” with Review changes and Overwrite.
MediaUploader takes drops, pastes and the file picker. The first image is the large featured tile. You can drag tiles and the rest move out of the way as you go, or press Space, the arrow keys and Space again. Each upload shows its own progress. A failed one stays in place with Retry, unsupported types and files over the size limit are named in a message, and removed images come back with Undo.
VariantMatrix edits up to three options as value chips that you can reorder, rename and remove, and keeps one row for every combination. Rows are grouped by an option with a summary like “$48–$96 · 12 in stock”. You can set price or stock for a whole group or a selection, delete a combination and restore it with the same SKU, filter rows, and work through hundreds of variants because collapsed groups don’t render and long groups load 25 rows at a time.
Procurez-vous HextaUI Pro
Un seul paiement débloque tous les blocs, y compris les prochains. Voir les tarifs.
Ajouter le registre Pro à components.json
components.json Ajouter votre token
Créez un token sur votre page de compte et placez-le dans
.env.localsous le nomHEXTAUI_PRO_TOKEN.Ajouter le block
pnpm dlx shadcn@latest add @hextaui-pro/product-editor
Save with Shopify’s productSet
Map the draft to a ProductSetInput and send it in one mutation. userErrors come back as field errors, so a taken SKU lands on its variant row, and a 409 from your route becomes the concurrent edit state.
Save through the Medusa admin API
Update the product, then batch the variants. Throw with the API’s message and the save bar shows it with Try again. onNavigate hands guarded links to the Next.js router.
Upload media to S3 with presigned URLs
Ask your server for a presigned PUT URL, send the file with XMLHttpRequest so progress reaches each tile, and return the public URL. Aborting the signal cancels the request when a tile is removed.
Anatomie
Les parties à composer, de l’extérieur vers l’intérieur.
| Partie | Description |
|---|---|
ProductEditor | The page: header, main column of cards, right rail, save bar and dialogs. |
DraftSaveBar | The floating bar for unsaved changes, save progress, errors and conflicts. |
useDraft | Saved value, draft, deep dirty check, validation, save, discard and conflict state. |
MediaUploader | Drop zone and sortable image grid with uploads and alt text. |
VariantMatrix | Options editor and the grouped variant table. |
SeoPreview | A search result snippet with title, description and URL fields. |
TagsInput | Free-form tags as removable chips. |
product-draft helpers | ProductDraft types, combination helpers, validateProduct, getProfit and describeProductChanges. |
ProductEditor
Also takes div props.
| Prop | Type | Par défaut |
|---|---|---|
productThe saved product. A new value replaces the draft when there are no unsaved changes. | ProductDraft | – |
onSaveSave the draft. Return errors keyed by field, such as sku or variant:ID:sku, a conflict, or the saved value. Throw to show the message in the save bar. | (draft, { overwrite, redirectFrom }) => void | { errors?, conflict?, value? } | Promise<…> | – |
conflictSomeone else’s newer version, for example from a live subscription. Without changes, the review dialog works them out from value. | { author; at; value?; changes? } | null | – |
onUploadUpload one image. Without it, images stay as local previews. | (file, { onProgress, signal }) => Promise<{ src; id? }> | – |
locationsStock locations for quantities. | { id; name; description? }[] | – |
channelsSales channels shown as checkboxes. | { id; name; description? }[] | – |
collectionsCollections to choose from. | string[] | – |
storeUsed in the search preview and URL field. | { name; domain } | – |
backHrefThe breadcrumb link. | string | "/products" |
onNavigateCalled for the breadcrumb once it’s safe to leave. | (href: string) => void | – |
onPreview, onDuplicate, onArchive, onDeleteHeader actions. Preview and Duplicate are guarded by unsaved changes. Without onArchive, Archive sets the status in the draft. | () => void | Promise<void> | – |
onDirtyChangeCalled when the draft becomes dirty or clean, for router blockers. | (dirty: boolean) => void | – |
onConflictResolveCalled when the merchant keeps their colleague’s version or overwrites it. | (resolution: "theirs" | "overwrite") => void | – |
localeLocale for money, numbers and relative times. | Intl.LocalesArgument | "en-US" |
saveBarLabelsWords in the save bar. | Partial<SaveBarLabels> | – |
MediaUploader
Also takes div props.
| Prop | Type | Par défaut |
|---|---|---|
valueImages in order. The first is featured. | MediaItem[] | – |
onValueChangeCalled after a reorder, upload, alt text change or removal. | (value: MediaItem[]) => void | – |
onUploadUpload a file and report progress from 0 to 1. Throw to fail the tile with your message. | (file, { onProgress, signal }) => Promise<{ src; id?; width?; height? }> | – |
onUploadingChangeHow many uploads are running. | (count: number) => void | – |
acceptAccepted MIME types. | string[] | JPG, PNG, WebP, GIF, AVIF |
maxSizePlus gros fichier en octets. | number | 20 MB |
maxFilesMost images in total. | number | 250 |
labelsWords for tiles, errors and announcements. | Partial<MediaUploaderLabels> | – |
| Prop | Type | Par défaut |
|---|---|---|
valueThe draft, or any object with these fields. | { options; variants; price; compareAtPrice; sku; barcode; quantities } | – |
onValueChangeCalled with the whole updated value. | (value) => void | – |
locationsLocations for the stock column. | InventoryLocation[] | – |
mediaImages for variant thumbnails. | MediaItem[] | – |
trackQuantityShows the stock column and summaries. | boolean | true |
errorsErrors keyed variant:ID:price or variant:ID:sku. Groups holding them open. | Record<string, string> | – |
pageSizeRows shown per group before Show more. | number | 25 |
| Prop | Type | Par défaut |
|---|---|---|
valueThe saved value. | Value | – |
onSaveSave and return errors, a conflict or the saved value. | (value, { overwrite }) => DraftSaveResult | – |
validateErrors found before saving. | (value) => Record<string, string> | – |
normalizeClean the draft before it is validated and saved. | (value) => Value | – |
conflictAn outside conflict to show. | DraftConflict | null | – |
| Touche | Action |
|---|---|
| MetaS | Saves the product when there are unsaved changes. Ctrl+S on Windows and Linux. |
| Space | On an image or option value, picks it up or drops it. |
| ArrowLeftArrowRightArrowUpArrowDown | Moves a picked-up image or value one place. |
| HomeEnd | Moves a picked-up image or value to the start or end. |
| Escape | Cancels a move and puts the item back. |
| Enter | On an option value, renames it. In the add value field, adds the value and its variants. |
| Backspace | On an option value or tag, removes it. In an empty add field, moves to the last chip. |
| MetaB | In the description, wraps the selection in bold. Meta+I does italic. |
- The save bar is a region named Unsaved changes. A polite live region says its state once, focus is never moved when it appears, and save failures are sent to an alert region.
- Images and option values are sortable buttons with instructions linked through aria-describedby. Pick up, every move, drop and cancel are announced, such as “Image “Ash vase” moved to position 2 of 7.”
- Server errors set aria-invalid on the field, show the message next to it and move focus to the first one. Upload problems and rejected files are announced and named in words, never only by color.
- The variant table uses table, row and cell roles with labelled checkboxes and inputs, such as “Price, Sand / Small”. Group toggles set aria-expanded, and motion falls back to fades or nothing under reduced motion.
Construit avec
Les composants HextaUI gratuits dont Product Editor est constitué. Chacun s’installe séparément.
Code
13 fichiers, ajoutés à components/blocks/product-editor.