API reference
Every export of @zanreal/medusa-admin-kit - the column registry, the context builders, the money readers, the query mappers and the types.
Everything below is exported from the package root, @zanreal/medusa-admin-kit.
The package also has a ./admin subpath, which is the built admin bundle Medusa
loads. Nothing in application code imports from it directly.
Column registry
registerVariantColumn
function registerVariantColumn<TProduct extends CatalogProduct = CatalogProduct, TData = unknown>(
def: VariantColumnDef<TProduct, TData>,
): void;Adds a column to the shared Catalog table. Call it at the top level of an admin
extension module, which is the rule Contributing a column
explains. Registering twice with the same id replaces the earlier definition,
which also makes hot module replacement a no-op rather than a duplicate.
Throws a TypeError when id is not a non-empty string, or when cell is not a
function.
getRegisteredVariantColumns
function getRegisteredVariantColumns(): VariantColumnDef[];Every registered column, ordered by ascending priority, ties keeping
registration order. The returned array is a fresh copy, so mutating it does not
touch the registry.
hasVariantColumn, getVariantColumn
function hasVariantColumn(id: string): boolean;
function getVariantColumn(id: string): VariantColumnDef | undefined;unregisterVariantColumn, clearVariantColumns
function unregisterVariantColumn(id: string): boolean;
function clearVariantColumns(): void;unregisterVariantColumn returns whether a column was actually removed.
clearVariantColumns is meant for test isolation and hot-reload resets;
production code should not need it.
Deprecated aliases
registerProductColumn, getRegisteredProductColumns, hasProductColumn,
getProductColumn, unregisterProductColumn and clearProductColumns are kept
as aliases of the Variant names above. They are the same functions writing to
the same store, so a plugin built against the older API keeps working untouched.
Cell context
buildVariantColumnContext
function buildVariantColumnContext<TProduct extends CatalogProduct>(
row: CatalogVariantRow<TProduct>,
): VariantColumnCellContext<TProduct>;Shapes a variant row from GET /admin/product-variants into the context handed
to every cell. The single place normalization happens, so a cell can trust
ctx.variant, ctx.sku and ctx.product without re-deriving them. Pure, so it
needs no admin runtime.
normalizeVariant
function normalizeVariant(variant?: Partial<CatalogVariant> | null): CatalogVariant;Fills a raw variant out to { id, sku, title, thumbnail }. A missing id
becomes ""; missing sku, title and thumbnail become null.
extractSkus
function extractSkus(variants?: Partial<CatalogVariant>[] | null): string[];Every non-empty SKU across a list of variants, de-duplicated and in order. Useful for batching a lookup across a page of rows.
Column resolution
BASE_CATALOG_COLUMN_IDS
const BASE_CATALOG_COLUMN_IDS: readonly [
"thumbnail", "product", "variant", "sku", "status", "price", "srp",
];The kit's own columns, in render order. BaseCatalogColumnId is the union of
those literals.
resolveCatalogColumns
function resolveCatalogColumns<TProduct extends CatalogProduct>(
registered: VariantColumnDef<TProduct>[],
): ResolvedCatalogColumn<TProduct>[];Merges the base columns with the registered ones into the final ordered list: base columns first, then the registered columns in the order they were passed in. Pure, so the ordering contract can be asserted without a dashboard.
ResolvedCatalogColumn is a discriminated union, either
{ id: BaseCatalogColumnId; source: "base" } or
{ id: string; source: "registered"; def: VariantColumnDef<TProduct> }.
renderRegisteredCell
function renderRegisteredCell<TProduct extends CatalogProduct, TData = unknown>(
def: VariantColumnDef<TProduct, TData>,
row: CatalogVariantRow<TProduct>,
async?: VariantColumnAsyncState<TData>,
): ReactNode;The synchronous half of what the route does per row: build the context, hand it
to the column's cell. Passing async directly is how a column's loading, data
and error branches get exercised in a plain Node test.
Money
readAmount
function readAmount(value?: unknown): number | null;Reads an amount out of whatever shape its source stores it in: a number, a
numeric string, a live BigNumber, a bare { value, precision } object, or a
BigNumber flattened into a plain object. Returns the amount in major units, or
null when the value is absent or unreadable. Never 0 as a stand-in for
either.
selectVariantPrice
function selectVariantPrice(
prices?: readonly CatalogPrice[] | null,
): VariantPriceSelection | null;Picks the one price a shop-price cell shows. Plain per-unit prices are preferred,
lowest currency code wins the tie, and a variant with only scoped or tiered
prices uses those rather than rendering nothing. Returns null only when there
is genuinely nothing readable.
VariantPriceSelection is { price: CatalogMoney; otherCount: number }, where
otherCount is how many other readable prices the variant has.
readVariantSrp, readVariantSrpMoney
function readVariantSrp(row: SrpSource, key?: string): number | null;
function readVariantSrpMoney(row: SrpSource, key?: string): CatalogMoney | null;Both read metadata.srp off the variant and fall back to the product's.
readVariantSrp returns a bare number, which is what the server-side consumers of
that figure compare. readVariantSrpMoney also reads the currency from the
sibling key, in the same metadata bag the amount came from and never across the
two, and yields currency: null when no sibling is recorded.
SrpSource is the minimal shape they read: { metadata?, product?: { metadata? } }.
SRP_METADATA_KEY, srpCurrencyKey
const SRP_METADATA_KEY = "srp";
function srpCurrencyKey(key?: string): string;srpCurrencyKey("srp") is "srp_currency". Deriving it from the amount's key
means a store that renamed srp gets the matching sibling for free.
formatAmount
function formatAmount(amount: number): string;toFixed(2), with no locale grouping, so the decimal point lands in the same
place in every cell and in every browser.
CatalogMoney, CatalogPrice
interface CatalogMoney {
amount: number;
currency: string | null;
}
interface CatalogPrice {
amount?: unknown;
currency_code?: string | null;
min_quantity?: number | null;
max_quantity?: number | null;
rules?: Record<string, unknown> | null;
}CatalogMoney.currency is an uppercase ISO code, or null when the source
stores a bare amount with no currency of its own, which is exactly what
metadata.srp is. CatalogPrice.amount is deliberately unknown, because it
cannot be trusted to be a number even where the published type says so.
Medusa's HttpTypes.AdminPrice is structurally assignable to CatalogPrice.
Row links
variantDetailHref
function variantDetailHref(row: CatalogVariantRow<CatalogProduct>): string | null;/products/:product_id/variants/:variant_id, the stock admin's own variant
detail page. Router-relative on purpose, since the admin router is mounted under
the dashboard base path. Returns null when the row has no parent product or no
variant id, so the caller can skip navigation rather than push a broken URL.
unwrapClickedRow
function unwrapClickedRow<TRow>(clicked: TRow | { original: TRow }): TRow;Unwraps whatever @medusajs/ui's DataTable hands an onRowClick handler.
useDataTable types the second argument as TData, but at 4.2.0 the table calls
it with TanStack's Row<TData> wrapper. Reading row.original blind breaks if
that is ever corrected; reading the row blind breaks today. This probes for the
wrapper and accepts both.
Query mapping
VARIANT_LIST_FIELDS
The fields string the table requests, covering the variant, its metadata, its
price set and the parent product.
buildVariantListQuery
function buildVariantListQuery(input: VariantListQueryInput): VariantListQuery;Turns { pageIndex, pageSize, search } into { limit, offset, fields, q? }.
offset is pageIndex * pageSize; a blank or whitespace-only search is omitted
rather than sent as an empty q.
mapVariantListResponse
function mapVariantListResponse<TVariant>(
response: VariantListResponse<TVariant> | null | undefined,
): { variants: TVariant[]; count: number };Normalizes a response, tolerating a missing list or count.
pageCount, DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS
function pageCount(count: number, pageSize: number): number;
const DEFAULT_PAGE_SIZE = 20;
const PAGE_SIZE_OPTIONS: readonly [10, 20, 50, 100];pageCount never returns less than 1.
Types
CatalogVariant, CatalogProduct and CatalogVariantRow describe a row and are
kept structural rather than tied to @medusajs/types, so the registry and its
helpers can be imported and unit-tested without the admin runtime. Medusa's own
HttpTypes.AdminProduct and HttpTypes.AdminProductVariant are structurally
assignable to them.
VariantColumnDef, VariantColumnCellContext and VariantColumnAsyncState are
described in Contributing a column.
ProductColumnVariant, ProductColumnProduct, ProductColumnCellContext,
ProductColumnAsyncState and ProductColumnDef are deprecated aliases of the
above, kept so an unmigrated plugin still compiles.