# Bulk action bar

> A bar over a selection of rows that runs an action on all of them and reports failures.

- Block: [bulk](https://bettersupabase.com/blocks/bulk.md)
- Kind: component
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/bulk-action-bar

## Install the block

```bash
npx better-supabase add bulk
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert alert-dialog button select
```

From `apps/examples/nextjs/src/features/work/components/bulk-action-bar.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/work/components/bulk-action-bar.tsx):

```tsx
/**
 * The bar over the grid's selected rows. It changes their state or moves
 * them to the trash through the bulk block, which runs each row on its own;
 * the rows that failed stay selected and are listed with their reason.
 */
export function BulkActionBar({
  selected,
  onSettled,
}: {
  selected: readonly Pick<WorkItemRow, "id" | "identifier">[];
  onSettled: (failedIds: readonly string[]) => void;
}) {
  const t = useExtracted("work");
  const errorMessage = useErrorMessage();
  const stateLabel = useStateLabel();
  const [to, setTo] = useState<WorkItemState | null>(null);
  const [confirming, setConfirming] = useState(false);
  const [failures, setFailures] = useState<readonly BulkFailure[]>([]);
  const bulk = useAction(runWorkBulkAction);

  const identifiers = new Map(selected.map((row) => [row.id, row.identifier]));

  const run = async (
    input: { action: "state"; to: WorkItemState } | { action: "delete" },
  ): Promise<void> => {
    const ids = selected.map((row) => row.id);
    const result = await bulk.run({ ...input, ids });

    if (!result.ok) {
      toast.error(errorMessage(result.error));

      return;
    }

    const { succeeded, failed } = result.data;

    setFailures(
      failed.map((row) => ({
        id: row.id,
        identifier: identifiers.get(row.id) ?? row.id,
        message: row.message === "" ? t("No reason given.") : row.message,
      })),
    );

    if (failed.length === 0) {
      toast.success(
        t("{count, plural, one {# item updated} other {# items updated}}", {
          count: succeeded,
        }),
      );
    } else {
      toast.warning(
        t("{failed, number} of {total, number} items failed", {
          failed: failed.length,
          total: result.data.total,
        }),
      );
    }

    onSettled(failed.map((row) => row.id));
  };

  const stateItems = WORK_ITEM_STATES.map((state) => ({
    value: state,
    label: stateLabel(state),
  }));

  if (selected.length === 0 && failures.length === 0) {
    return null;
  }

  return (
    <div className="flex flex-col gap-3" data-recipe="bulk-action-bar">
      {selected.length === 0 ? null : (
        <div
          role="toolbar"
          aria-label={t("Selected items")}
          className="flex flex-wrap items-center gap-2 rounded-lg border bg-muted/50 p-2"
        >
          <span className="px-2 text-sm font-medium tabular-nums">
            {t("{count, plural, one {# selected} other {# selected}}", {
              count: selected.length,
            })}
          </span>
          <Select
            value={to}
            items={stateItems}
            disabled={bulk.pending}
            onValueChange={(value) => {
              const state = WORK_ITEM_STATES.find((each) => each === value);

              setTo(state ?? null);
            }}
          >
            <SelectTrigger
              size="sm"
              className="w-40"
              aria-label={t("New state")}
            >
              <SelectValue placeholder={t("Set state")} />
            </SelectTrigger>
            <SelectContent>
              {stateItems.map((item) => (
                <SelectItem key={item.value} value={item.value}>
                  {item.label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Button
            size="sm"
            disabled={to === null || bulk.pending}
            onClick={() => {
              if (to !== null) {
                void run({ action: "state", to });
              }
            }}
          >
            {t("Apply")}
          </Button>
          <AlertDialog
            open={confirming}
            onOpenChange={(next) => {
              if (!bulk.pending) {
                setConfirming(next);
              }
            }}
          >
            <AlertDialogTrigger
              render={
                <Button
                  type="button"
                  size="sm"
                  variant="destructive"
                  disabled={bulk.pending}
                />
              }
            >
              <Trash2Icon />
              {t("Delete")}
            </AlertDialogTrigger>
            <AlertDialogContent>
              <AlertDialogHeader>
                <AlertDialogTitle>
                  {t(
                    "{count, plural, one {Delete # item?} other {Delete # items?}}",
                    { count: selected.length },
                  )}
                </AlertDialogTitle>
                <AlertDialogDescription>
                  {t("They move to the trash, where they can be restored.")}
                </AlertDialogDescription>
              </AlertDialogHeader>
              <AlertDialogFooter>
                <AlertDialogCancel disabled={bulk.pending}>
                  {t("Cancel")}
                </AlertDialogCancel>
                <AlertDialogAction
                  type="button"
                  variant="destructive"
                  disabled={bulk.pending}
                  onClick={() => {
                    void run({ action: "delete" }).then(() => {
                      setConfirming(false);
                    });
                  }}
                >
                  {t("Delete")}
                </AlertDialogAction>
              </AlertDialogFooter>
            </AlertDialogContent>
          </AlertDialog>
          <Button
            size="sm"
            variant="ghost"
            disabled={bulk.pending}
            onClick={() => {
              onSettled([]);
            }}
          >
            <XIcon />
            {t("Clear")}
          </Button>
        </div>
      )}
      {failures.length === 0 ? null : (
        <Alert variant="destructive">
          <TriangleAlertIcon />
          <AlertTitle>
            {t(
              "{count, plural, one {# item wasn't changed} other {# items weren't changed}}",
              { count: failures.length },
            )}
          </AlertTitle>
          <AlertDescription>
            <ul className="flex flex-col gap-1">
              {failures.map((failure) => (
                <li key={failure.id}>
                  <span className="font-medium tabular-nums">
                    {failure.identifier}
                  </span>
                  {": "}
                  {failure.message}
                </li>
              ))}
            </ul>
          </AlertDescription>
          <AlertAction>
            <Button
              size="sm"
              variant="outline"
              onClick={() => {
                setFailures([]);
              }}
            >
              {t("Dismiss")}
            </Button>
          </AlertAction>
        </Alert>
      )}
    </div>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Bulk action bar" in a React web app that uses shadcn/ui components. A bar over a selection of rows that runs an action on all of them and reports failures.

It uses the better-supabase `bulk` block (Bulk actions). It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add bulk` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert alert-dialog button select`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the component. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/work/components/bulk-action-bar.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it reads the same block data, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/bulk-action-bar.md
- Block docs: https://bettersupabase.com/docs/blocks/bulk
```
