Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix: add optional confirm property for show single title or title and…
… message
  • Loading branch information
kulikp1 committed Jun 11, 2026
commit 85442b2fdab4b7189e74dcdfbe8f2b844de78e83
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,65 @@ If your operation can be expressed more efficiently as a single batched query (e
| `bulkConfirmationMessage` | `string` | Confirmation dialog text shown before the bulk action executes. |
| `bulkSuccessMessage` | `string` | Success message shown after the bulk operation. Defaults to `"N out of M items processed successfully"`. |

## Standalone Bulk Actions

For operations that only apply to multiple selected records, use `options.bulkActions`. The built-in **Delete checked** action is a good reference.

```ts title="./resources/apartments.ts"
{
resourceId: 'aparts',
options: {
bulkActions: [
{
label: 'Send Invitation',
icon: 'flowbite:envelope-solid',
confirm: 'Are you sure you want to send invitation emails?',
allowed: async ({ adminUser }) => adminUser.dbUser.role === 'superadmin',
action: async ({ selectedIds }) => {
await sendBulkInvitations(selectedIds);
return { ok: true, successMessage: `Sent to ${selectedIds.length} users` };
},
},
],
},
}
```

### Confirmation dialog

Pass `confirm` to show a dialog before the action runs.

**String** — shown as the dialog title, no secondary message:

```ts
confirm: 'Are you sure you want to send invitation emails?',
```

**Object** — full control over the dialog. `{count}` in `message` is replaced with the number of selected records; `|` separates singular and plural forms:

```ts
confirm: {
title: 'Are you sure you want to archive the selected items?',
message: 'Archiving {count} item. This process is irreversible. | Archiving {count} items. This process is irreversible.',
yes: 'Archive',
no: 'Cancel',
},
```

Omit `confirm` entirely to skip the dialog and run the action immediately.

### Fields

| Field | Type | Description |
|---|---|---|
| `label` | `string` | Button label shown in the list toolbar. |
| `icon` | `string` | Flowbite icon for the button. |
| `confirm` | `string \| { title?, message?, yes?, no? }` | Confirmation dialog. String → title only. Object → full control. If omitted, no dialog is shown. |
| `successMessage` | `string` | Toast message shown after the action completes. |
| `allowed` | `async ({ adminUser, selectedIds, allowedActions }) => boolean` | Called on page load (no `selectedIds`) to decide visibility, and again on click (with `selectedIds`) to authorize. |
| `action` | `async ({ selectedIds, adminUser, resource, tr }) => { ok, error?, successMessage? }` | Handler called with all selected IDs at once. |
| `showInThreeDotsDropdown` | `boolean` | Show in the three-dots menu of the list header instead of as a top-level button. |

### Access Control

You can control who can use an action through the `allowed` function. This function receives:
Expand Down
5 changes: 4 additions & 1 deletion adminforth/modules/configValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,10 @@ export default class ConfigValidator implements IConfigValidator {
bulkActions.push({
label: `Delete checked`,
icon: 'flowbite:trash-bin-outline',
confirm: 'Are you sure you want to delete selected items?',
confirm: {
title: 'Are you sure you want to delete selected items?',
message: 'Deleting {count} item. This process is irreversible. | Deleting {count} items. This process is irreversible.',
},
allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete },
action: async ({ selectedIds, adminUser, response }) => {
const connector = this.adminforth.connectors[res.dataSource];
Expand Down
21 changes: 19 additions & 2 deletions adminforth/modules/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,15 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
translateRoutines[`bulkAction${i}`] = tr(action.label, `resource.${resource.resourceId}`);
}
if (action.confirm) {
translateRoutines[`bulkActionConfirm${i}`] = tr(action.confirm, `resource.${resource.resourceId}`);
if (typeof action.confirm === 'string') {
translateRoutines[`bulkActionConfirm${i}`] = tr(action.confirm, `resource.${resource.resourceId}`);
} else {
Object.entries(action.confirm).forEach(([key, value]: [string, string]) => {
if (value) {
translateRoutines[`bulkActionConfirm${i}_${key}`] = tr(value, `resource.${resource.resourceId}`);
}
});
}
}
});

Expand Down Expand Up @@ -1217,7 +1225,16 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
(action, i) => ({
...action,
label: action.label ? translated[`bulkAction${i}`] : action.label,
confirm: action.confirm ? translated[`bulkActionConfirm${i}`] : action.confirm,
confirm: !action.confirm ? action.confirm : (
typeof action.confirm === 'string'
? translated[`bulkActionConfirm${i}`]
: Object.fromEntries(
Object.entries(action.confirm).map(([key, value]) => [
key,
value ? translated[`bulkActionConfirm${i}_${key}`] : value,
])
)
),
})
),
actions: allowedCustomActions.map(({ bulkHandler, action: actionFn, ...rest }) => ({
Expand Down
12 changes: 8 additions & 4 deletions adminforth/spa/src/utils/listUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ export async function startBulkAction(actionId: string, resource: AdminForthReso
const { confirm, alert } = useAdminforth();

if (action?.confirm) {
const confirmed = await confirm({
title: action.confirm,
message: t('Deleting {count} item. This process is irreversible. | Deleting {count} items. This process is irreversible.', { count: checkboxes.value.length }),
});
const confirmed = await confirm(
typeof action.confirm === 'string'
? { title: action.confirm }
: {
...action.confirm,
message: action.confirm.message && t(action.confirm.message, { count: checkboxes.value.length }),
}
);
if (!confirmed) {
return;
}
Expand Down
11 changes: 10 additions & 1 deletion adminforth/types/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,17 @@ export interface AdminForthBulkActionCommon {

/**
* Confirmation message which will be displayed to user before action is executed.
* String value is shown as the dialog title without any message under it.
* Use object form to explicitly set a message (e.g. "This process is irreversible.")
* and/or button labels. `{count}` placeholder in message will be replaced with the
* number of selected records, pluralization is supported via `|` separator.
*/
confirm?: string,
confirm?: string | {
title?: string,
message?: string,
yes?: string,
no?: string,
},

/**
* Success message which will be displayed to user after action is executed.
Expand Down