Bulk administration
Keep selection across pages and submit only chosen fields.
What you will build
Combine cross-page selection, validated bulk patches, row permissions and an application-owned Archive action.
Start with the React setup or Vue setup. Use the complete files below with the shared recipe modules. The browser-memory store resets on reload; an API integration must preserve the same contracts.
Required files: shared/products.ts, shared/store.ts, shared/admin-mutations.ts, product-config.ts, list-records.ts, view-actions.ts, editable-actions.ts, admin-actions.ts, product-form.ts, get-form-config.ts, admin-config.ts, recipe-admin.
Copy the shared mutation store and adapter and the product form catalog. The recipe deliberately uses two rows per page to make cross-page selection visible.
admin-config.ts
import { defineTableConfig } from "@/components/ui/yayaw-table/config/helpers";
import { productConfig } from "./product-config";
export const adminConfig = defineTableConfig({
...productConfig,
table: {
...productConfig.table,
layoutPreset: "admin",
defaultPageSize: 2,
pageSizeOptions: [2, 10, 20],
enableRowSelection: true,
enableMultiRowSelection: true,
preserveSelectionOnQuery: true,
allowEdit: true,
allowBulkEdit: true,
canEditRow: (row) => row.status !== "archived",
canSelectRow: (row) => row.status !== "archived",
bulkExport: true,
},
form: { editFormType: "products" },
});import { defineTableConfig } from "@/components/ui/yayaw-table-vue/config";
import { productConfig } from "./product-config";
export const adminConfig = defineTableConfig({
...productConfig,
table: {
...productConfig.table,
layoutPreset: "admin",
defaultPageSize: 2,
pageSizeOptions: [2, 10, 20],
enableRowSelection: true,
enableMultiRowSelection: true,
preserveSelectionOnQuery: true,
allowEdit: true,
allowBulkEdit: true,
canEditRow: (row) => row.status !== "archived",
canSelectRow: (row) => row.status !== "archived",
bulkExport: true,
},
form: { editFormType: "products" },
});Entry component
Sample data. Changes stay in this preview.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Archive, type LucideProps } from "lucide-react";
import { NuqsAdapter } from "nuqs/adapters/react";
import { useState } from "react";
import { Toaster } from "sonner";
import { DataTable } from "@/components/ui/yayaw-table";
import { adminMutations } from "../shared/admin-mutations";
import { getAdminActions } from "./admin-actions";
import { adminConfig } from "./admin-config";
import { getFormConfig } from "./get-form-config";
import type { ExamplePresentation } from "./presentation";
const getConfig = () => adminConfig;
// Custom bulk actions accept a function component for their icon.
const ArchiveIcon = (props: LucideProps) => <Archive {...props} />;
export default function AdminRecipe(presentation: ExamplePresentation = {}) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<DataTable
{...presentation}
customBulkActions={(selection) => [
{
id: "archive-products",
icon: ArchiveIcon,
label: "Archive",
confirm: {
title: "Archive selected products?",
description:
"Archived products remain visible but cannot be edited or selected.",
confirmLabel: "Archive",
},
onClick: async () => {
const result = adminMutations.bulkUpdate(
selection.selectedOriginalRows.map((row) => String(row.id)),
{ status: "archived" }
);
await queryClient.invalidateQueries({
queryKey: ["tableData", "recipe-admin"],
});
return {
...result,
clearSelection: result.success,
closeMenu: result.success,
};
},
},
]}
getFormConfig={getFormConfig}
getRowId={(row) => String(row.id)}
getTableActions={getAdminActions}
getTableConfig={getConfig}
tableId="recipe-admin"
tableType="products"
title={presentation.title ?? "Admin"}
/>
<Toaster />
</NuqsAdapter>
</QueryClientProvider>
);
}Sample data. Changes stay in this preview.
<script setup lang="ts">
import type { ExamplePresentation } from "./presentation";
defineProps<ExamplePresentation>();
import { Archive } from "lucide-vue-next";
import { YayawDataTable } from "@/components/ui/yayaw-table-vue";
import type { BulkAction } from "@/components/ui/yayaw-table-vue/types";
import { adminMutations } from "../shared/admin-mutations";
import { getAdminActions } from "./admin-actions";
import { adminConfig } from "./admin-config";
import { getFormConfig } from "./get-form-config";
const customBulkActions: BulkAction[] = [
{
id: "archive-products",
label: "Archive",
icon: Archive,
confirm: {
title: "Archive selected products?",
description:
"Archived products remain visible but cannot be edited or selected.",
confirmLabel: "Archive",
},
handler: async (context) => {
const result = adminMutations.bulkUpdate(context.selectedIds, {
status: "archived",
});
await context.refresh();
return {
...result,
clearSelection: result.success,
closeMenu: result.success,
};
},
},
];
const getRowId = (row: Record<string, unknown>) => String(row.id);
</script>
<template>
<YayawDataTable v-bind="$props" table-type="products" table-id="recipe-admin" :title="title ?? 'Admin'"
:config="adminConfig" :get-table-actions="getAdminActions" :custom-bulk-actions="customBulkActions" :get-row-id="getRowId"
:get-form-config="getFormConfig" />
</template>Verify the result
Select one record on page one.
Go to page two and select another: the selection contains two IDs.
Open bulk edit and choose Status only.
Set Archived and submit: both selected records change; stock and price stay unchanged.
Exercise
bulkUpdatewith one missing ID:failedIdsreports that ID, while successful changes remain saved.Verify Name is excluded from the bulk editor by
bulkEdit: false.
Use the existing playground to explore the library’s controls.
Enforce the record policy in mutations
The same rule is checked again at write time. This demo is not authentication: replace it with your server’s actor and tenant checks. Unknown or archived targets are reported as failed IDs, so successful writes are not retried by the built-in editor.
import { type createProductStore, productStore } from "./store";
/** Demo record policy. A real server must also authenticate and scope every request. */
export function createAdminMutations(
store: ReturnType<typeof createProductStore>
) {
const update = (id: string, patch: Record<string, unknown>) => {
if (store.rows.find((row) => row.id === id)?.status === "archived") {
return { success: false, error: "Archived products are read-only." };
}
return store.update(id, patch);
};
const bulkUpdate = (ids: string[], patch: Record<string, unknown>) => {
const failedIds: string[] = [];
for (const id of new Set(ids)) {
if (!update(id, patch).success) {
failedIds.push(id);
}
}
return {
success: failedIds.length === 0,
failedIds,
...(failedIds.length
? {
error:
"Some products cannot be edited. Retry the remaining records.",
}
: {}),
};
};
return { update, bulkUpdate };
}
export const adminMutations = createAdminMutations(productStore);import type { TableActions } from "@/components/ui/yayaw-table/providers/table-provider";
import { adminMutations } from "../shared/admin-mutations";
import { editableActions } from "./editable-actions";
const adminActions: TableActions = {
...editableActions,
update: (id, patch) => Promise.resolve(adminMutations.update(id, patch)),
bulkUpdate: (ids, patch) =>
Promise.resolve(adminMutations.bulkUpdate(ids, patch)),
};
export const getAdminActions = (tableType: string) =>
tableType === "products" ? adminActions : undefined;import type { TableActions } from "@/components/ui/yayaw-table-vue/types";
import { adminMutations } from "../shared/admin-mutations";
import { editableActions } from "./editable-actions";
const adminActions: TableActions = {
...editableActions,
update: (id, patch) => Promise.resolve(adminMutations.update(id, patch)),
bulkUpdate: (ids, patch) =>
Promise.resolve(adminMutations.bulkUpdate(ids, patch)),
};
export const getAdminActions = (tableType: string) =>
tableType === "products" ? adminActions : undefined;Walk through the result
| Action | Expected result |
|---|---|
| Select two products and open Edit. | The property list starts empty and Apply is disabled. |
Add Stock, type 24, then apply. | Only Stock changes on those two targets; names, prices and the third product remain unchanged. |
| Add and remove Price before applying another property. | Price is absent from the submitted patch. |
| Select a product, choose Archive and confirm. | Its status becomes Archived; the row can no longer be selected or edited. |
| Export the selected rows before archiving. | The CSV contains the selected records, rather than only the current page. |
On mobile, use View settings for presentation, the Data menu for export, and the search button in the toolbar. Resetting a view does not undo writes. Try the keyboard as well as pointer input.