Actions Provider API
Wire CRUD actions to the table via provider callbacks
Original row context and concurrent edits
React and Vue call update(id, patch, context) and delete(id, context) with an
optional TableMutationContext. Its row contains the original displayed record,
including application version fields. The patch contains only submitted changes.
Existing actions accepting two update arguments or one delete argument remain valid.
update: async (id, patch, context) => {
const expectedVersion = context?.row.dataVersion;
if (typeof expectedVersion !== "number") {
return { success: false, error: "Reload the record before editing." };
}
return updateRecord({ id, patch, expectedVersion });
},Forward the expected version to your server and compare it atomically with the stored version before applying changes. Resolve permissions and allowed fields on the server; row context is client input and does not grant access. Return a failed action result for a conflict so the form or inline editor retains the user's draft. Do not obtain the expected version from an unrelated preloaded page.
Catalogue forms, inline edits, Kanban moves, single-record deletion and per-record
bulk-delete fallbacks receive this context. A custom bulkUpdate or bulkDelete
keeps its existing contract: capture versions from the selected rows in your host
adapter. Context is separate from editable fields and must not be persisted as
business data.
A typed action adapter
The following complete example builds on the quick start and shared recipe files. The reference below explains individual options and integration fragments.
import type { TableActions } from "@/components/ui/yayaw-table/providers/table-provider";
import { productStore } from "../shared/store";
import { listRecords } from "./list-records";
import { viewActions } from "./view-actions";
export const editableActions: TableActions = {
views: viewActions,
list: (params) => listRecords(productStore.rows, params),
update: (id, patch) => Promise.resolve(productStore.update(id, patch)),
bulkUpdate: (ids, patch) =>
Promise.resolve(productStore.bulkUpdate(ids, patch)),
};
export const getEditableActions = (tableType: string) =>
tableType === "products" ? editableActions : undefined;import type { TableActions } from "@/components/ui/yayaw-table-vue/types";
import { productStore } from "../shared/store";
import { listRecords } from "./list-records";
import { viewActions } from "./view-actions";
export const editableActions: TableActions = {
views: viewActions,
list: (params) => listRecords(productStore.rows, params),
update: (id, patch) => Promise.resolve(productStore.update(id, patch)),
bulkUpdate: (ids, patch) =>
Promise.resolve(productStore.bulkUpdate(ids, patch)),
};
export const getEditableActions = (tableType: string) =>
tableType === "products" ? editableActions : undefined;Actions API
Provide actions per tableType via getTableActions (passed to DataTable). These connect the table to your data layer. In Next.js you can pass Server Actions so that list, create, update, delete, and bulk operations run on the server. See Server-side & Server Actions for a full example.
Shape
getTableActions: (tableType: string) => ({
list: async (params) => {
// params: { filters, advancedFilters, limit, orderBy, page (1-based), search }
return {
data: [],
meta: { pageCount: 1, totalCount: 0 },
};
},
aggregate: async (params) => {
// params: { filters, advancedFilters, search, calculations, locale }
// Chart view: also groupBy, metrics, timeZone, weekStartsOn; answer { groups }
return {
results: {
price: { raw: 820.64, label: "820,64" },
},
meta: { totalCount: 50 },
};
},
create: async (data) => ({ success: true, data }),
update: async (id, data) => ({ success: true, data }),
delete: async (id) => ({ success: true }),
duplicate: async (id) => ({ success: true }),
bulkDelete: async (ids) => ({ success: true }),
bulkCopy: async (ids) => ({ success: true, data: ids }),
bulkUpdate: async (ids, updateData) => ({ success: true }),
destinations: [],
})destinations
Declare custom connect and share destinations (webhooks, n8n, connectors) alongside the CRUD actions: { id, label, kind: "connect" | "share", icon?, hidden?, requiresSelection?, run(context) } ("sync" and "export" are still accepted as aliases of "connect"). They appear in the Data menu: connect destinations under a Connect › row (hidden when there are none) and share destinations under Share ›, after the built-in "Copy link". A connect destination can also declare schedule (frequencies?, load, save, status?) so it can run on a schedule per view; see Schedule a Connect destination. It can declare connector (targets, allowTargetInput?, describe, modes?, load?, save?, push, labels?, help?) to open the table's send screen instead of running run, which then becomes optional; see Connector screens. With directions, conflictRules?, preview? and sync, the same screen also imports from the target or keeps both in sync; see Sync from the connector screen. See Custom destinations for the full run(context) shape, the n8n and share examples, and the security note about keeping credentials on your backend.
import
Declare import to configure Data › Import: { csv?, sources?, importRows?, lookup?, allowNewOptions?, batchSize? }, all optional. Without it, CSV imports look up keys through list and write each row through create or update. importRows(batch, context) writes a batch of { creates, updates } on your server and returns { created?, updated?, failures? }; lookup({ columnId, keys }) returns the record ids by key value; sources adds sources after CSV ({ id, label, description?, load(context) }); allowNewOptions keeps unknown select options; batchSize sets the rows per write (default 50); csv: false hides CSV. See Import actions and the server-side example.
formLinks
Declare formLinks to share Form views on public links your application serves: { status(viewId), publish(viewId, snapshot), unpublish(viewId), setAcceptingResponses?(viewId, accepting) }. With it, a saved Form view shows Share form (publish to the web, copy or open the link, accept responses, update the public form). publish receives a PublicFormSnapshot and returns { url }; status returns { published, url?, acceptsResponses? } or null. The table never serves the form itself: your server stores the snapshot, renders YayawTableForm on a public route and re-validates each response with acceptPublicFormResponse. See Share on a public link and your application's responsibilities.
exportFile
Declare exportFile(request) alongside the CRUD actions to build the file for the Export screen on the server, server-first:
exportFile: async (request) => {
const response = await fetch("/api/exports", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error("Export failed");
}
return response.json(); // { url: "https://.../export-2026-09-23.xlsx" }
},request carries the chosen format ("csv" | "xlsx" | "pdf"), scope ("view" | "selection"), formatted (the Values setting), fileName (with extension), viewId, the view's query in the list shape, the chosen columns in order, and selectedRowIds when scope is "selection". Return { url } (a download link, for example a signed URL) or { blob }; when provided, exportFile handles every format, and the server loads the records itself with no row-count limit from the browser. Without it the browser writes the CSV or prints the PDF page. See Export screen for the full request shape and the fallback behavior.
tree
Declare tree for the File tree view: { path?, move?, createFolder? }, all optional. path(id) returns the ancestors of a node, root first, for breadcrumbs and links to a folder. move({ ids, parentId }) moves records in one batch (parentId: null is the root) and returns { moved?, failed? }, where failed is [{ id, error? }]: failed items go back where they were and the first error is shown. createFolder({ parentId, name }) creates a folder and returns its record. Without them, the tree walks the parent column over the loaded rows, moves with update(id, { [parentColumn]: parentId }) and creates folders with create. Your server re-checks permissions, cycles and name clashes. See Loading from your server.
views
Declare views to keep saved views on your server: { list, create, update, delete, getFavorite, setFavorite, setOrder? }. list answers { data, order? } (TableViewListResult), where order is the person's order of their views; setOrder({ tableId, tableType, viewIds }) stores it and answers { success, data: { viewIds } }. Without views, views stay in the browser. See Saved views and Order of views.
tags
Declare tags for tags columns: { list, create?, update?, merge?, remove? }, each called with { tableId, tableType, columnId }. list answers the catalog [{ id, name, color? }]; create, update, merge and remove change it, and the last two rewrite the records using the tags on your server. Without list, tags columns keep their static options.
geocode
Declare geocode(query, { locale, signal }) to suggest addresses in the editor of location columns and to convert addresses during CSV imports. Return places, best first: [{ lat, lng, label, address? }]. The table debounces the calls, aborts the previous one through signal and keeps at most eight results. Without it, the location editor is a plain address and coordinates form, and imported addresses that are not coordinates are errors. Call your geocoding provider from your server. See Address suggestions.
list response
The list method must return:
{ data: T[]; meta?: { pageCount?: number; totalCount?: number } }The File tree view sends params.scope with kind: "children", "subtree" or "tree-matches". A server that applies it answers meta.scope: "applied", with the optional meta.childCounts and meta.sizes ({ [folderId]: number }), meta.ancestors (rows) for tree-matches, and meta.truncated when it capped the answer.
The Map view sends params.scope with kind: "bbox": { kind: "bbox", field, west, south, east, north }, the location column and the rectangle shown, in degrees (west greater than east crosses the antimeridian). A server that returns only the rows whose place lies inside answers meta.scope: "applied"; otherwise the table filters the loaded rows in the browser, up to table.map.maxRows (2,000). See Search this area.
The facet panel counts the values of a column with aggregate when it can (groupBy: [{ columnId }] and metrics: [{ fn: "count" }], as a chart asks), else from the rows list returns; the folders of the other views load with list and scope: { kind: "subtree", parentId: null }.
aggregate response (optional)
aggregate is used by footer calculations to compute values on the full filtered dataset (not only the current page).
{
results: Record<
string,
{
raw: number | string | null;
label: string;
}
>;
meta?: { totalCount?: number };
}Notes:
calculationsin params is a map:columnId -> CalculationType.If
aggregateis not provided, the table falls back to paginatedlistcalls.You can return custom labels from your API (
label) while keeping machine-readable values inraw.
Chart groups
The Chart view calls the same aggregate with the view's query, an empty calculations map and four more parameters: groupBy: [{ columnId, bucket? }] (at most two levels, the x axis then the series; bucket is "day", "week", "month", "quarter" or "year" for date columns), metrics: [{ columnId?, fn }] (fn is "count", "sum", "avg", "min", "max" or "countDistinct"), timeZone and weekStartsOn. Answer { groups: [{ keys, values }], truncated? }, where keys follow groupBy (null for empty values; YYYY-MM-DD days, the week's first day as YYYY-MM-DD, YYYY-MM months, YYYY-Qn quarters, YYYY years) and values follow metrics. results is optional in the response type. Without groups, or when aggregate fails, the chart groups the rows from list in the browser (capped, with a notice), so existing hosts keep working. See Server-side grouping.
Bulk actions
DataTable will use these by default if you don't provide explicit callbacks:
onBulkEdit→bulkUpdatewhen available, otherwise individualupdateactionsonBulkDelete→bulkDeletewhen available, otherwise individualdeleteactionsonBulkCopy→bulkCopywhen available, otherwise individualduplicateactionsonBulkExport→ internal CSV export of selected rows (client-side)
Inline edit dependency
Inline cell editing depends on the provider update(id, data) action.
No extra API route is used.
When
inlineEditis enabled on the table or columns, each cell commit callsupdate(id, { [field]: value }).If
updateis not configured, inline edit shows an error and does not commit.
Recommended bulk callback contract
To avoid ambiguous branches, return an explicit result object from your bulk callbacks:
type BulkActionResult = {
success: boolean;
closeMenu: boolean;
clearSelection: boolean;
message?: string;
};Example:
onBulkDelete: async (rows) => {
const ids = rows.map((row) => String((row.original as { id: string }).id));
const response = await deleteManyProducts(ids);
return {
success: response.success,
closeMenu: response.success,
clearSelection: response.success,
message: response.success
? `Deleted ${ids.length} products`
: response.error ?? "Delete failed",
};
};See also: