YYayaw

Bulk Actions

Confirmation flow and callback contract for copy, delete, edit, and export

Bulk Actions

Confirmation flow

copy and delete use a confirmation dialog.

Current behavior:

  1. User clicks Copy or Delete.
  2. Dialog opens and stores the pending action.
  3. Outside clicks are ignored while confirmation is open.
  4. Confirm executes the pending action exactly once.
  5. Menu closing follows the callback result (closeMenu).

This prevents a no-op scenario where outside-click events would reset state while the portal dialog was open.

Deterministic action behavior

  • edit: executes immediately.
  • export: executes immediately.
  • copy: always requires confirmation.
  • delete: always requires confirmation.

Custom bulk actions

Use customBulkActions on DataTable when selected rows need app-specific operations such as publish, archive, approve, or sync. The prop accepts either a static array or a callback that receives the current selection context.

Custom actions render after the built-in export action and before the built-in delete action. They use the same execution result contract as built-in bulk callbacks.

import { Archive, Send } from "lucide-react";
import { DataTable } from "@/components/ui/yayaw-table";

<DataTable
  tableType="entries"
  customBulkActions={(ctx) => [
    {
      id: "publish-selected",
      label: "Publish",
      icon: Send,
      disabled: ctx.selectedCount === 0,
      onClick: async () => {
        await publishEntries(ctx.selectedOriginalRows);

        return {
          success: true,
          closeMenu: true,
          clearSelection: true,
          message: `Published ${ctx.selectedCount} entries`,
        };
      },
    },
    {
      id: "archive-selected",
      label: "Archive",
      icon: Archive,
      variant: "destructive",
      confirm: {
        title: "Archive selected entries?",
        description: `Archive ${ctx.selectedCount} selected entries.`,
        confirmLabel: "Archive",
      },
      onClick: async () => {
        await archiveEntries(ctx.selectedOriginalRows);

        return {
          success: true,
          closeMenu: true,
          clearSelection: true,
        };
      },
    },
  ]}
/>

The selection context contains:

type BulkActionContext<TData> = {
  selectedRows: Row<TData>[];
  selectedOriginalRows: TData[];
  selectedCount: number;
};

The action definition is:

type BulkAction<TData> = {
  id: string;
  label: string;
  icon: ComponentType<{ className?: string; size?: number }>;
  onClick: (ctx: BulkActionContext<TData>) =>
    | BulkActionResult
    | void
    | Promise<BulkActionResult | void>;
  disabled?: boolean | ((ctx: BulkActionContext<TData>) => boolean);
  variant?: "default" | "destructive";
  confirm?: {
    title?: string | ((ctx: BulkActionContext<TData>) => string);
    description?: string | ((ctx: BulkActionContext<TData>) => string);
    confirmLabel?: string | ((ctx: BulkActionContext<TData>) => string);
    cancelLabel?: string | ((ctx: BulkActionContext<TData>) => string);
  };
};

Bulk callback contract

Recommended return shape:

type BulkActionResult = {
  success: boolean;
  closeMenu: boolean;
  clearSelection: boolean;
  message?: string;
};

Example (onBulkDelete)

onBulkDelete: async (rows) => {
  const ids = rows.map((row) => String((row.original as { id: string }).id));
  const response = await deleteMany(ids);

  return {
    success: response.success,
    closeMenu: response.success,
    clearSelection: response.success,
    message: response.success
      ? `Deleted ${ids.length} rows`
      : response.error ?? "Delete failed",
  };
};

Legacy compatibility

Yayaw Table still normalizes legacy callback returns, but the explicit object contract is strongly recommended for predictable behavior.