Templates
Deep dive into DetailList and DetailView — the key components for building admin pages
The two template components — DetailList and DetailView — are the foundation for building admin pages in Deenruv. They provide complete page layouts with built-in support for the plugin injection system, so any plugin can extend your pages with additional columns, tabs, sidebar components, and actions.
DetailList — List Pages
DetailList creates a full-featured list page with:
- Data table with configurable columns
- Pagination (URL search param–based)
- Column sorting
- Filtering
- Bulk actions
- Row actions
- Plugin injection points (
ListViewMarker)
Basic Usage
import { DetailList, apiClient, useTranslation } from '@deenruv/react-ui-devkit';
export function ProductListPage() {
const { t } = useTranslation('catalog');
return (
<DetailList
title={t('products.title')}
listType="products"
route={(options) =>
apiClient('query')({
products: [
{
options: {
take: options.perPage,
skip: (options.page - 1) * options.perPage,
sort: options.sort
? { [options.sort.key]: options.sort.sortDir }
: undefined,
filter: options.filter,
},
},
{
totalItems: true,
items: {
id: true,
name: true,
slug: true,
enabled: true,
featuredAsset: { preview: true },
},
},
],
}).then((r) => r.products)
}
columns={[
{
header: t('products.name'),
accessorKey: 'name',
},
{
header: t('products.slug'),
accessorKey: 'slug',
},
{
header: t('products.enabled'),
accessorKey: 'enabled',
cell: ({ row }) => (
<Badge variant={row.original.enabled ? 'default' : 'secondary'}>
{row.original.enabled ? 'Active' : 'Draft'}
</Badge>
),
},
]}
/>
);
}With Sorting and Filtering
<DetailList
title="Orders"
listType="orders"
route={(options) =>
apiClient('query')({
orders: [
{
options: {
take: options.perPage,
skip: (options.page - 1) * options.perPage,
sort: options.sort
? { [options.sort.key]: options.sort.sortDir }
: undefined,
filter: options.filter,
},
},
{
totalItems: true,
items: {
id: true,
code: true,
state: true,
totalWithTax: true,
orderPlacedAt: true,
customer: { firstName: true, lastName: true },
},
},
],
}).then((r) => r.orders)
}
columns={[
{ header: 'Code', accessorKey: 'code' },
{
header: 'Customer',
accessorKey: 'customer',
cell: ({ row }) => {
const c = row.original.customer;
return c ? `${c.firstName} ${c.lastName}` : '—';
},
},
{
header: 'State',
accessorKey: 'state',
cell: ({ row }) => <OrderStateBadge state={row.original.state} />,
},
{
header: 'Total',
accessorKey: 'totalWithTax',
cell: ({ row }) => formatCurrency(row.original.totalWithTax),
},
]}
/>With Bulk Actions and Row Actions
<DetailList
title="Reviews"
listType="reviews"
route={fetchReviews}
columns={reviewColumns}
bulkActions={[
{
label: 'Approve Selected',
onClick: async (selectedRows) => {
await apiClient('mutation')({
approveReviews: [
{ ids: selectedRows.map((r) => r.id) },
{ success: true },
],
});
},
},
{
label: 'Reject Selected',
onClick: async (selectedRows) => {
await apiClient('mutation')({
rejectReviews: [
{ ids: selectedRows.map((r) => r.id) },
{ success: true },
],
});
},
},
]}
rowActions={[
{
label: 'View',
onClick: (row) => navigate(ROUTES.to(row.id)),
},
{
label: 'Delete',
onClick: async (row) => {
await apiClient('mutation')({
deleteReview: [{ id: row.id }, { success: true }],
});
},
},
]}
/>Plugin Extensibility
DetailList automatically renders a ListViewMarker based on the listType prop. Other plugins can extend your list by targeting the corresponding list location ID:
// In your list page
<DetailList listType="products" ... />
// In another plugin — extending the products list
tables: [
{
locationId: 'products-list-view',
columns: [
{ header: 'Reviews', accessorKey: 'customFields.reviewCount' },
],
},
],DetailView — Detail/Edit Pages
DetailView creates a detail page layout with:
- Tab navigation
- Sidebar area
- Action buttons (inline and dropdown)
- Plugin injection points (
DetailViewMarker) - Form integration
Basic Usage
import {
DetailView, useDeenruvForm, z,
DeenruvForm,
FormField, FormItem, FormLabel, FormControl, FormMessage,
Input, Button, Switch, PageBlock, RichTextEditor, useTranslation, apiClient,
} from '@deenruv/react-ui-devkit';
const productSchema = z.object({
name: z.string().min(1, 'Required'),
slug: z.string(),
description: z.string(),
enabled: z.boolean(),
});
type ProductFormValues = z.infer<typeof productSchema>;
export function ProductDetailPage() {
const { id } = useParams();
const { t } = useTranslation('catalog');
const form = useDeenruvForm({
schema: productSchema,
defaultValues: { name: '', slug: '', description: '', enabled: true },
});
const handleSave = async (data: ProductFormValues) => {
await apiClient('mutation')({
updateProduct: [
{
input: {
id,
enabled: data.enabled,
translations: [{
languageCode: LanguageCode.en,
name: data.name,
slug: data.slug,
description: data.description,
}],
},
},
{ id: true },
],
});
};
return (
<DeenruvForm form={form} onSubmit={handleSave}>
<DetailView
title={form.watch('name') || t('product.new')}
locationId="products-detail-view"
tabs={[
{
id: 'product',
label: t('tabs.product'),
content: (
<PageBlock title={t('product.basicInfo')}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.name')}</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.slug')}</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.description')}</FormLabel>
<FormControl>
<RichTextEditor value={field.value} onChange={field.onChange} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</PageBlock>
),
},
{
id: 'variants',
label: t('tabs.variants'),
content: <VariantsTab productId={id} />,
},
]}
sidebar={
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.enabled')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
}
actions={<Button type="submit">{t('common.save')}</Button>}
/>
</DeenruvForm>
);
}Tab Configuration
Tabs in DetailView support several options:
tabs={[
{
id: 'main',
label: 'Main Info',
content: <MainInfoForm />,
},
{
id: 'media',
label: 'Media',
content: <MediaManager />,
// Hide the sidebar for this tab
hideSidebar: true,
},
{
id: 'seo',
label: 'SEO',
content: <SeoForm />,
// Replace sidebar content for this tab
sidebarReplacement: <SeoSidebar />,
},
]}Plugin Extensibility
DetailView automatically renders DetailViewMarker components based on the locationId. Other plugins can extend your detail page:
// In your detail page
<DetailView locationId="products-detail-view" ... />
// In another plugin — adding a tab
tabs: [
{
locationId: 'products-detail-view',
tab: {
id: 'reviews',
label: 'Reviews',
component: ProductReviewsTab,
},
},
],
// In another plugin — adding a sidebar component
components: [
{
id: 'products-detail-view-sidebar',
tab: 'reviews',
component: ReviewsSidebar,
},
],
// In another plugin — adding action buttons
actions: {
inline: [
{
locationId: 'products-detail-view',
component: ({ entity }) => (
<Button onClick={() => doSomething(entity.id)}>
Custom Action
</Button>
),
},
],
},Combining Templates
A typical plugin uses DetailList for the list page and DetailView for the detail page:
import { createDeenruvUIPlugin } from '@deenruv/react-ui-devkit';
import { ReviewsListPage } from './pages/ReviewsListPage';
import { ReviewDetailPage } from './pages/ReviewDetailPage';
export const ReviewsPlugin = createDeenruvUIPlugin({
name: 'reviews-plugin-ui',
version: '1.0.0',
pages: [
{ path: '', element: <ReviewsListPage /> }, // Uses DetailList
{ path: ':id', element: <ReviewDetailPage /> }, // Uses DetailView
],
// ...
});Both DetailList and DetailView are fully compatible with the plugin injection system. Any registered plugin can extend your pages by targeting the appropriate location IDs.