/** * The admin's extension points, for a plugin that extends them. * * Copy this file into your own project. It is hand-maintained rather than * published to npm: eight hooks and six interfaces do not justify a package * boundary between two plugins that release on different days. * * These are plain exported interfaces, not `declare module` ambient types — * there is no `warranty-cart-admin` package for a `declare module` block to * attach to once this file lives in your project instead of ours, so that * form would declare a module nobody imports. Reference the interfaces * directly, the way the worked example at * https://github.com/mralaminahamed/warranty-cart/blob/trunk/docs/developer-guide/extending-the-admin.md * does — linked there and not as a relative path, because that guide is a * `.md` file and `.distignore` keeps every `.md` out of the zip this `.d.ts` * ships in, so a relative path would point at a file the reader who copied * this one does not have. * * Every filter is read through a guard, most callbacks that build a screen's * contents are run through a second one, and a component that throws only * once React is actually rendering it — a route's `element`, an icon, or one * of those same callbacks handing back a component rather than a plain node * — is caught by an error boundary around the whole admin instead. A callback * that throws is logged and ignored, not left to blank somebody's admin — * but "logged and ignored" still means your registration silently did * nothing, so check the browser console when one seems not to fire. The * per-hook notes below say exactly what each guard drops. * * Requires Warranty Cart 2.2.0 or later. */ import { type ReactNode } from 'react'; import { type RouteObject } from 'react-router'; /** * The shape every nav and tab icon is called with. * * Real icons in this admin are Hugeicons wrapped as components, and their * full prop type comes from `@hugeicons/react` — a dependency this file * will not ask your project to install. What actually reaches an icon at * render time is narrower than that full type: the sidebar and the tab * strip only ever pass a Tailwind sizing class and `aria-hidden`, never a * numeric `size`, so that is all this type promises. */ export type IconComponent = ( props: { className?: string; 'aria-hidden'?: boolean | 'true' | 'false'; } ) => ReactNode; export interface NavItem { label: string; path: string; icon: IconComponent; } export interface NavGroup { /** Rendered as a small-caps label above the group; omit for an unlabelled one. */ label?: string; items: NavItem[]; } /** A column appended after the built-in ones and before the actions column. */ export interface ExtraColumn< T > { /** Stable, unique among the columns on that screen. Used as the React key. */ id: string; header: string; cell: ( row: T ) => ReactNode; align?: 'left' | 'right'; } /** A card appended to a detail screen. */ export interface DetailPanel< T > { id: string; title: string; render: ( subject: T ) => ReactNode; } /** An entry in a row's action menu. */ export interface RowAction< T > { id: string; label: string; onSelect: ( row: T ) => void; destructive?: boolean; } /** An action applied to every selected row. */ export interface BulkAction< T > { id: string; label: string; onSelect: ( rows: T[] ) => void | Promise< void >; } /** A tab appended to the Settings screen. */ export interface SettingsTab { /** * Prefix this with your plugin's slug — an unprefixed id can collide with * one of the six built-in ids (`selling`, `after-sale`, `checkout`, * `notifications`, `payments`, `import`) or with one of the three * retired ids Settings still redirects from old bookmarks (`storefront`, * `general`, `uploads`), and the two fail in opposite ways: * * - A built-in collision is not deduped — it renders. The tab strip gets * two buttons sharing one DOM id, both marked selected, and (if your * tab carries `render`) the panel area gets two panels sharing another, * both visible at once. * - A `RENAMED_TABS` collision is redirected away before it ever * renders. Settings resolves the URL's tab id through that map before * checking it against the tab list, so your tab is unreachable by a * click or a bookmark alike. * * A colliding id is a different mistake from forgetting to append: see * `warrantyCart.settings.tabs` in the hook table below for what happens * to the six built-in tabs if your callback returns only its own. */ id: string; label: string; icon?: IconComponent; /** A count or short status shown after the label. */ badge?: ReactNode; /** * The tab's own contents. Omit it and the tab appears in the strip with * nothing behind it — the built-in tabs render their panel from Settings' * own state, which your add-on has no access to, so this is how you * supply yours. */ render?: () => ReactNode; } /** * The eight hooks, their signatures, and what the first argument actually * contains — which differs between them and matters more than the * signature does. * * Four hooks hand you the *whole* current list — built-ins included — and * expect the whole list back. Append; do not replace. Return only your own * entries from any of these four and the built-ins disappear for every * store running your add-on: `warrantyCart.admin.routes` (every existing * screen orphaned), `warrantyCart.admin.nav` (the whole sidebar but your own * group), `warrantyCart.claims.bulkActions` (the free "Export selected" * action gone) and `warrantyCart.settings.tabs` (all six built-in tabs * gone). * * These same four hooks treat an empty `[]` result the same way they treat a * thrown error or a forgotten `return`: reverted to the base list, silently, * whenever the base was not itself empty. That is deliberate for `nav`, * `routes` and `settings.tabs` — zeroing the whole sidebar, route tree or * tab strip is never what a callback actually meant to do — but it is a real * limit on `warrantyCart.claims.bulkActions`: the built-in `export` action * is the *only* entry in the list until some add-on appends its own, so * `bulkActions.filter( ( a ) => 'export' !== a.id )` — the only way to * remove it — comes back `[]` and is reverted to the built-in list you were * trying to drop, with nothing logged to say so. There is currently no way * to remove the built-in export action through this hook. * * The other four hand you only what previous callbacks have already added, * starting from an empty array, because the built-in content on those * screens is not routed through the filter at all — it is written directly * into the surrounding component. Append to what you are given; do not * assume it is empty, since another add-on's callback may have already run: * `warrantyCart.records.extraColumns`, `warrantyCart.claims.extraColumns`, * `warrantyCart.claims.detail.panels` and `warrantyCart.claims.rowActions`. * * | Hook | Signature | Payload | * |---------------------------------------|---------------------------------------------------------------------------|---------------------| * | `warrantyCart.admin.routes` | `( routes: RouteObject[] ) => RouteObject[]` | whole tree | * | `warrantyCart.admin.nav` | `( nav: NavGroup[] ) => NavGroup[]` | whole nav | * | `warrantyCart.records.extraColumns` | `( columns: ExtraColumn[] ) => ExtraColumn[]` | added columns only | * | `warrantyCart.claims.extraColumns` | `( columns: ExtraColumn[] ) => ExtraColumn[]` | added columns only | * | `warrantyCart.claims.detail.panels` | `( panels: DetailPanel[], claim: Claim ) => DetailPanel[]` | added panels only | * | `warrantyCart.claims.rowActions` | `( actions: RowAction[], claim: Claim ) => RowAction[]` | added actions only | * | `warrantyCart.claims.bulkActions` | `( actions: BulkAction[] ) => BulkAction[]` | whole list, built-in export included | * | `warrantyCart.settings.tabs` | `( tabs: SettingsTab[] ) => SettingsTab[]` | whole list, six built-in tabs included | * * `WarrantyRecord` and `Claim` are the shapes the admin already renders — * a purchased warranty and a filed claim, respectively — not declared here * because copying them would drift from the real ones the moment either * gained a field. Type your callback loosely (`any`, or your own narrow * slice of the fields you read) rather than reproducing them. */ export type { RouteObject };