Store Products Admin
The merchant side of the catalogue: a product list with bulk edit, a product editor with a generated variant matrix, media manager and search preview, and a collections manager with manual and rule-based collections.
Code
<ProductAdminList products={products} currency="USD" onOpen={(p) => router.push(`/products/${p.id}`)} onCreate={() => router.push("/products/new")} onBulkEdit={async (ids, edit) => api.bulkEdit(ids, edit)}/><ProductEditor initial={productToDraft(product, { cost: 4000 })} currency="USD" siteUrl="https://shop.example" onSave={async (draft) => api.saveProduct(draft)}/>Store Admin · beta
Live examples and controls: Store Products Admin in the lab.
Install
npx shadcn@latest add https://docs.nasaqui.com/r/store-products-admin.jsonEverything a merchant needs to manage the catalogue: a product list with search, filters and bulk edit of price, stock and status;
a product editor with pictures, price and margin, inventory, options that build a variant matrix, and a search listing preview;
and a collections manager where a collection is picked by hand or filled by rules with a live match preview.
Money is integer minor units. The components hold no data and call no API: you pass the catalogue and save in on* callbacks.
When to use
- A store back office: products, variants, stock, collections.
When not to use
- Showing products to shoppers: use
product-card,product-detailand the listing components. - Stock movements and warehouses: use
stock-ledger.
Import
import { ProductAdminList, ProductEditor, CollectionsManager, productToDraft } from "@fadymondy/nasaq/web";Quick start
<ProductAdminList
products={products}
currency="USD"
onOpen={(p) => router.push(`/products/${p.id}`)}
onCreate={() => router.push("/products/new")}
onBulkEdit={async (ids, edit) => api.bulkEdit(ids, edit)}
/>
<ProductEditor
initial={productToDraft(product, { cost: 4000 })}
currency="USD"
siteUrl="https://shop.example"
onSave={async (draft) => api.saveProduct(draft)}
/>Anatomy
ProductAdminList [data-slot=product-list] DataTable + bulk edit dialog + delete confirm
ProductEditor [data-slot=product-editor] form of cards, sticky save bar
MediaManager [data-slot=media-manager] sortable picture tiles [data-slot=media-tile]
OptionsEditor [data-slot=options-editor]
VariantMatrix [data-slot=variant-matrix]
CollectionsManager [data-slot=collections-manager] cards + editor dialog with RuleBuilder and match previewAPI
ProductAdminList
| Prop | Type | Default | Description |
|---|---|---|---|
products | readonly CommerceProduct[] | required | The catalogue. |
currency | string | "USD" ("SAR" in Arabic) | ISO 4217 code. |
onOpen | (product) => void | none | Row click, Enter, or Edit in the row menu. |
onCreate | () => void | none | Shows "New product". |
onBulkEdit | (ids, edit: ProductBulkEdit) => Promise<ProductAdminResult> | none | Without it the bulk button is hidden. |
onStatusChange | (product, status) => Promise<ProductAdminResult> | none | Activate, draft, archive. |
onDelete | (product) => Promise<ProductAdminResult> | none | Delete after confirmation. |
lowStockAt | number | 5 | Stock at or below is low. |
loading, error, onRetry | States. error is true or a message. | ||
labels | StoreProductsAdminLabels | en / ar | Override any string. |
ProductEditor
| Prop | Type | Default | Description |
|---|---|---|---|
initial | ProductDraft | empty draft | Build with productToDraft(product, { cost, visibility, seoTitle, seoDescription }). |
currency | string | "USD" ("SAR" in Arabic) | ISO 4217 code. |
siteUrl | string | https://store.example | Origin for the search preview URL. |
onSave | (draft) => Promise<ProductAdminResult> | required | Resolve { error } to keep the edits. |
onCancel | () => void | none | Called when Discard is pressed with nothing changed. |
suggestions | { brands?, categories?, tags? } | none | Autocomplete lists. |
loading | boolean | false | Skeleton. |
labels | StoreProductsAdminLabels | en / ar | Strings. |
Changing options regenerates the variants with generateVariants: a variant whose option values still exist keeps its price, compare-at, SKU, stock and image. Removed combinations are named in a notice; the matrix is capped at 100.
MediaManager, OptionsEditor, VariantMatrix
MediaManager({ images, onImagesChange, maxImages = 12, disabled, labels }),
OptionsEditor({ options, onOptionsChange, maxOptions = 3, disabled, labels }),
VariantMatrix({ options, variants, onVariantsChange, currency, images, disabled, labels }). All are controlled.
CollectionsManager
| Prop | Type | Description |
|---|---|---|
collections | readonly CollectionDef[] | { id, title, kind: "manual" | "rules", productIds?, conditions? }. |
products | readonly CommerceProduct[] | For the picker and live preview. |
currency | string | Rule prices are typed in major units of it. |
onSave | (collection) => Promise<ProductAdminResult> | New collections arrive with a fresh id. |
onDelete | (collection) => Promise<ProductAdminResult> | Adds Delete. |
loading, error, onRetry, labels |
Logic (pure, also exported)
generateVariants, bulkFillVariants, bulkEditProducts, bulkPrice, marginFromCost, priceForMargin, stockSummary, matchCollection, collectionsOfProduct, validateProductDraft, productToDraft, draftToProduct, draftChanged, emptyProductDraft, decimalToMinor, optionCombinations, variantLabel, duplicateSkus. They use integer maths only.
Examples
// A rule collection: products tagged "summer" under 500
const collection: CollectionDef = {
id: "c1",
title: "Summer under 500",
kind: "rules",
conditions: { kind: "group", id: "g", join: "and", children: [
{ kind: "condition", id: "a", field: "tag", op: "is", value: "summer" },
{ kind: "condition", id: "b", field: "price", op: "lt", value: "500" },
] },
};
matchCollection(products, collection, { minorPerMajor: 100 });Accessibility
| Key | Action |
|---|---|
| Space, arrows, Space | Pick up, move and drop a picture (also the move earlier / later buttons). |
| Enter on a list row | Open the product. |
| Shift+F10, Menu key, context-click | Row actions. |
Moves are announced in a live region. Missing alt text is counted and flagged. Every icon-only button has a label.
RTL & i18n
English and Arabic built in through useOptionalNasaq(). SKUs, prices and numbers stay LTR inside dir="ltr" or <bdi>. Chevrons flip in RTL.
Styling & tokens
Uses --nq-* tokens only. Target data-slot names above; data-dragging marks a picture being dragged.
Do / Don't
- Do pass prices in minor units. Don't pass floats.
- Do give every picture alt text.
- Don't rely on client validation alone: check again on the server.
Related
Lab
https://docs.nasaqui.com/?path=/docs/components-store-admin-store-products-admin--docs
StoreOrdersList
Store admin orders. A data-table order list with status, payment and fulfilment chips, filters, saved views and bulk actions; an order detail with partial fulfilment and tracking, refunds by line or amount with restock, cancel, a notes timeline and customer cards; printable invoices and packing slips; abandoned carts with a recovery email. The refund, fulfilment and status maths is pure and tested.
Store Settings
The merchant's store configuration: shipping zones and rates with local pickup, tax rates (inclusive or exclusive), discounts with a basket simulator, and gift cards with issue, ledger history and a checkout redeem field. Pure, tested logic for rate resolution, discount evaluation and gift card balances.