Docs
Reference

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.

editable-actions.ts
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;
editable-actions.ts
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.

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:

  • calculations in params is a map: columnId -> CalculationType.

  • If aggregate is not provided, the table falls back to paginated list calls.

  • You can return custom labels from your API (label) while keeping machine-readable values in raw.

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 → bulkUpdate when available, otherwise individual update actions

  • onBulkDelete → bulkDelete when available, otherwise individual delete actions

  • onBulkCopy → bulkCopy when available, otherwise individual duplicate actions

  • onBulkExport → 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 inlineEdit is enabled on the table or columns, each cell commit calls update(id, { [field]: value }).

  • If update is not configured, inline edit shows an error and does not commit.

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: