Skip to content

Commit 0337b34

Browse files
authored
Merge pull request #537 from devforth/next
Next
2 parents 64901ef + ed11c92 commit 0337b34

File tree

102 files changed

+11571
-5255
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+11571
-5255
lines changed

adminforth/commands/callTsProxy.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,26 @@ export function callTsProxy(tsCode, silent=false) {
2525
const child = spawn("tsx", [path.join(currentFileFolder, "proxy.ts")], {
2626
env: process.env,
2727
});
28-
let stdout = "";
2928
let stderr = "";
29+
let stdoutLogs = [];
3030

3131
child.stdout.on("data", (data) => {
32-
stdout += data;
32+
stdoutLogs.push(data.toString());
3333
});
3434

3535
child.stderr.on("data", (data) => {
3636
stderr += data;
3737
});
3838

3939
child.on("close", (code) => {
40+
const tsProxyResult = stdoutLogs.find(log => log.includes('>>>>>>>'));
41+
const preparedStdout = tsProxyResult.slice(tsProxyResult.indexOf('>>>>>>>') + 7, tsProxyResult.lastIndexOf('<<<<<<<'));
42+
const preparedStdoutLogs = stdoutLogs.filter(log => !log.includes('>>>>>>>'));
4043
if (code === 0) {
4144
try {
42-
const preparedStdout = stdout.slice(stdout.indexOf("{"), stdout.lastIndexOf("}") + 1);
45+
for (const log of preparedStdoutLogs) {
46+
console.log(log);
47+
}
4348
const parsed = JSON.parse(preparedStdout);
4449
if (!silent) {
4550
parsed.capturedLogs.forEach((log) => {
@@ -52,10 +57,10 @@ export function callTsProxy(tsCode, silent=false) {
5257
}
5358
resolve(parsed.result);
5459
} catch (e) {
55-
reject(new Error("Invalid JSON from tsproxy: " + stdout));
60+
reject(new Error("Invalid JSON from tsproxy: " + preparedStdout));
5661
}
5762
} else {
58-
console.error(`tsproxy exited with non-0, this should never happen, stdout: ${stdout}, stderr: ${stderr}`);
63+
console.error(`tsproxy exited with non-0, this should never happen, stdout: ${preparedStdout}, stderr: ${stderr}`);
5964
reject(new Error(stderr));
6065
}
6166
});

adminforth/commands/createApp/templates/package.json.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"devDependencies": {
4141
"typescript": "5.4.5",
4242
"tsx": "4.11.2",
43-
"@types/express": "latest",
43+
"@types/express": "^4.17.21",
4444
"@types/node": "latest",
4545
"@prisma/client": "latest",
4646
"prisma": "^7.0.0"

adminforth/commands/proxy.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,27 @@ import path from 'path';
4141

4242
// Restore original console.log
4343
console.log = origLog;
44-
console.log(JSON.stringify({
45-
result,
46-
capturedLogs,
47-
error: null
48-
}));
44+
console.log(
45+
">>>>>>>"+
46+
JSON.stringify({
47+
result,
48+
capturedLogs,
49+
error: null
50+
})
51+
+"<<<<<<<"
52+
);
4953
} catch (error: any) {
5054
// Restore original console.log
5155
console.log = origLog;
52-
console.log(JSON.stringify({
53-
error: error.message,
54-
stack: error.stack,
55-
capturedLogs
56-
}));
56+
console.log(
57+
">>>>>>>"+
58+
JSON.stringify({
59+
error: error.message,
60+
stack: error.stack,
61+
capturedLogs
62+
})
63+
+"<<<<<<<"
64+
);
5765
} finally {
5866
await unlink(tmpFile).catch(() => {});
5967
}

adminforth/documentation/docs/tutorial/001-gettingStarted.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ cd myadmin # or any other name you provided
4040
CLI options:
4141

4242
* **`--app-name`** - name for your project. Used in `package.json`, `index.ts` branding, etc. Default value: **`adminforth-app`**.
43-
* **`--db`** - database connection string. Currently PostgreSQL, MongoDB, SQLite, MySQL, Clickhouse are supported. Default value: **`sqlite://.db.sqlite`**
43+
* **`--db`** - database connection string. Currently PostgreSQL, MongoDB, SQLite, MySQL, Clickhouse and Qdrant (read only) are supported. Default value: **`sqlite://.db.sqlite`**
4444

4545
> ☝️ Database Connection String format:
4646
>
@@ -51,6 +51,7 @@ CLI options:
5151
> - MongoDB — `mongodb://localhost:27017/dbname`
5252
> - Clickhouse — `clickhouse://localhost:8123/dbname`
5353
> - MySQL — `mysql://user:password@localhost:3306/dbname`
54+
> - Qdrant - `qdrant://localhost:6333`
5455
5556
### Understand the generated Project Structure
5657

adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md

Lines changed: 62 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,66 @@ Here's how to add a custom action:
4747
- `name`: Display name of the action
4848
- `icon`: Icon to show (using Flowbite icon set)
4949
- `allowed`: Function to control access to the action
50-
- `action`: Handler function that executes when action is triggered
50+
- `action`: Handler function that executes when action is triggered for a **single** record
51+
- `bulkHandler`: Handler function that executes when the action is triggered for **multiple** records at once (see [Bulk button with bulkHandler](#bulk-button-with-bulkhandler))
5152
- `showIn`: Controls where the action appears
52-
- `list`: whether to show in list view
53-
- `listThreeDotsMenu`: whether to show in three dots menu in the list view
54-
- `showButton`: whether to show as a button on show view
55-
- `showThreeDotsMenu`: when to show in the three-dots menu of show view
53+
- `list`: whether to show as an icon button per row in the list view
54+
- `listThreeDotsMenu`: whether to show in the three-dots menu per row in the list view
55+
- `showButton`: whether to show as a button on the show view
56+
- `showThreeDotsMenu`: whether to show in the three-dots menu of the show view
57+
- `bulkButton`: whether to show as a bulk action button when rows are selected
58+
59+
### Bulk button with `action`
60+
61+
When `showIn.bulkButton` is `true` and only `action` (not `bulkHandler`) is defined, AdminForth automatically calls your `action` function **once per selected record** using `Promise.all`. This is convenient for simple cases but means N separate handler invocations run in parallel:
62+
63+
```ts title="./resources/apartments.ts"
64+
{
65+
name: 'Auto submit',
66+
action: async ({ recordId }) => {
67+
// Called once per selected record when used as a bulk button
68+
await doSomething(recordId);
69+
return { ok: true, successMessage: 'Done' };
70+
},
71+
showIn: {
72+
bulkButton: true, // triggers Promise.all over selected records
73+
showButton: true,
74+
}
75+
}
76+
```
77+
78+
If your operation can be expressed more efficiently as a single batched query (e.g., a single `UPDATE … WHERE id IN (…)`), define `bulkHandler` instead. AdminForth will call it **once** with all selected record IDs:
79+
80+
```ts title="./resources/apartments.ts"
81+
{
82+
name: 'Auto submit',
83+
// bulkHandler receives all recordIds in one call – use it for batched operations
84+
bulkHandler: async ({ recordIds, adminforth, resource }) => {
85+
await doSomethingBatch(recordIds);
86+
return { ok: true, successMessage: `Processed ${recordIds.length} records` };
87+
},
88+
// You can still keep `action` for the single-record show/edit buttons
89+
action: async ({ recordId }) => {
90+
await doSomething(recordId);
91+
return { ok: true, successMessage: 'Done' };
92+
},
93+
showIn: {
94+
bulkButton: true,
95+
showButton: true,
96+
}
97+
}
98+
```
99+
100+
> ☝️ When both `action` and `bulkHandler` are defined, AdminForth uses `bulkHandler` for bulk operations and `action` for single-record operations. When only `action` is defined and `bulkButton` is enabled, AdminForth falls back to `Promise.all` over individual `action` calls.
101+
102+
### Bulk-specific options
103+
104+
| Option | Type | Description |
105+
|---|---|---|
106+
| `showIn.bulkButton` | `boolean` | Show as a bulk action button in the list toolbar. |
107+
| `bulkHandler` | `async ({ recordIds, adminUser, adminforth, resource, response, tr }) => { ok, error?, message? }` | Called with all selected IDs at once. Falls back to calling `action` per record in parallel if omitted. |
108+
| `bulkConfirmationMessage` | `string` | Confirmation dialog text shown before the bulk action executes. |
109+
| `bulkSuccessMessage` | `string` | Success message shown after the bulk operation. Defaults to `"N out of M items processed successfully"`. |
56110

57111
### Access Control
58112

@@ -84,46 +138,11 @@ The `allowed` function receives:
84138
Return:
85139
- `true` to allow access
86140
- `false` to deny access
87-
- A string with an error message to explain why access was denied
141+
- A string with an error message to explain why access was denied — e.g. `return 'Only superadmins can perform this action'`
88142

89143
Here is how it looks:
90144
![alt text](<Single record actions.png>)
91145

92-
93-
You might want to allow only certain users to perform your custom bulk action.
94-
95-
To implement this limitation use `allowed`:
96-
97-
If you want to prohibit the use of bulk action for user, you can do it this way:
98-
99-
```ts title="./resources/apartments.ts"
100-
import { admin } from '../index';
101-
102-
....
103-
104-
bulkActions: [
105-
{
106-
label: 'Mark as listed',
107-
icon: 'flowbite:eye-solid',
108-
allowed: async ({ resource, adminUser, selectedIds }) => {
109-
if (adminUser.dbUser.role !== 'superadmin') {
110-
return false;
111-
}
112-
return true;
113-
},
114-
confirm: 'Are you sure you want to mark all selected apartments as listed?',
115-
action: async ({ resource, selectedIds, adminUser, tr }) => {
116-
const stmt = admin.resource('aparts').dataConnector.client.prepare(
117-
`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(() => '?').join(',')})`
118-
);
119-
await stmt.run(...selectedIds);
120-
121-
return { ok: true, message: tr(`Marked ${selectedIds.length} apartments as listed`) };
122-
},
123-
}
124-
],
125-
```
126-
127146
### Action URL
128147

129148
Instead of defining an `action` handler, you can specify a `url` that the user will be redirected to when clicking the action button:
@@ -146,7 +165,7 @@ The URL can be:
146165
- A relative path within your admin panel (starting with '/')
147166
- An absolute URL (starting with 'http://' or 'https://')
148167

149-
To open the URL in a new tab, add `?target=_blank` to the URL:
168+
To open the URL in a new tab, append `target=_blank` as a query parameter. If the URL already has query parameters, use `&target=_blank`; otherwise use `?target=_blank`:
150169

151170
```ts
152171
{
@@ -162,118 +181,12 @@ To open the URL in a new tab, add `?target=_blank` to the URL:
162181

163182
> ☝️ Note: You cannot specify both `action` and `url` for the same action - only one should be used.
164183
165-
166-
167-
## Custom bulk actions
168-
169-
You might need to give admin users a feature to perform same action on multiple records at once.
170-
171-
For example you might want allow setting `listed` field to `false` for multiple apartment records at once.
172-
173-
AdminForth by default provides a checkbox in first column of the list view for this purposes.
174-
175-
By default AdminForth provides only one bulk action `delete` which allows to delete multiple records at once
176-
(if deletion for records available by [resource.options.allowedActions](/docs/api/Back/interfaces/ResourceOptions/#allowedactions))
177-
178-
To add custom bulk action quickly:
179-
180-
```ts title="./resources/apartments.ts"
181-
//diff-add
182-
import { AdminUser } from 'adminforth';
183-
//diff-add
184-
import { admin } from '../index';
185-
186-
{
187-
...
188-
resourceId: 'aparts',
189-
...
190-
options: {
191-
//diff-add
192-
bulkActions: [
193-
//diff-add
194-
{
195-
//diff-add
196-
label: 'Mark as listed',
197-
//diff-add
198-
icon: 'flowbite:eye-solid',
199-
//diff-add
200-
// if optional `confirm` is provided, user will be asked to confirm action
201-
//diff-add
202-
confirm: 'Are you sure you want to mark all selected apartments as listed?',
203-
//diff-add
204-
action: async function ({selectedIds, adminUser }: {selectedIds: any[], adminUser: AdminUser }) {
205-
//diff-add
206-
const stmt = admin.resource('aparts').dataConnector.client.prepare(`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(() => '?').join(',')})`);
207-
//diff-add
208-
await stmt.run(...selectedIds);
209-
//diff-add
210-
return { ok: true, successMessage: `Marked ${selectedIds.length} apartments as listed` };
211-
//diff-add
212-
},
213-
//diff-add
214-
}
215-
//diff-add
216-
],
217-
}
218-
}
219-
```
220-
221-
Action code is called on the server side only and allowed to only authorized users.
222-
223-
> ☝️ AdminForth provides no way to update the data, it is your responsibility to manage the data by selectedIds. You can use any ORM system
224-
> or write raw queries to update the data.
225-
226-
> ☝️ You can use `adminUser` object to check whether user is allowed to perform bulk action
227-
228-
229-
> Action response can return optional `successMessage` property which will be shown to user after action is performed. If this property is not provided, no messages will be shown to user.
230-
231-
Here is how it looks:
232-
![alt text](<Custom bulk actions.png>)
233-
234-
235-
## Limiting access to bulk actions
236-
237-
You might want to allow only certain users to perform your custom bulk action.
238-
239-
To implement this limitation use `allowed`:
240-
241-
If you want to prohibit the use of bulk action for user, you can do it this way:
242-
243-
```ts title="./resources/apartments.ts"
244-
bulkActions: [
245-
{
246-
label: 'Mark as listed',
247-
icon: 'flowbite:eye-solid',
248-
//diff-add
249-
allowed: async ({ resource, adminUser, selectedIds }) => {
250-
//diff-add
251-
if (adminUser.dbUser.role !== 'superadmin') {
252-
//diff-add
253-
return false;
254-
//diff-add
255-
}
256-
//diff-add
257-
return true;
258-
//diff-add
259-
},
260-
// if optional `confirm` is provided, user will be asked to confirm action
261-
confirm: 'Are you sure you want to mark all selected apartments as listed?',
262-
action: async function ({selectedIds, adminUser }: {selectedIds: any[], adminUser: AdminUser }, allow) {
263-
const stmt = admin.resource('aparts').dataConnector.client.prepare(`UPDATE apartments SET listed = 1 WHERE id IN (${selectedIds.map(() => '?').join(',')}`);
264-
await stmt.run(...selectedIds);
265-
return { ok: true, error: false, successMessage: `Marked ${selectedIds.length} apartments as listed` };
266-
},
267-
}
268-
],
269-
```
270-
271184
## Custom Component
272185

273186
If you want to style an action's button/icon without changing its behavior, attach a custom UI wrapper via `customComponent`.
274187
The file points to your SFC in the custom folder (alias `@@/`), and `meta` lets you pass lightweight styling options (e.g., border color, radius).
275188

276-
Below we wrap a Mark as listed action (see the original example in [Custom bulk actions](#custom-bulk-actions)).
189+
Below we wrap a "Mark as listed" action.
277190

278191
```ts title="./resources/apartments.ts"
279192
{

adminforth/documentation/docs/tutorial/03-Customization/10-menuConfiguration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ auth: {
275275

276276
```
277277

278-
This syntax can be use to get unique avatar for each user of hardcode avatar, but it makes more sense to use it with [upload plugin](https://adminforth.dev/docs/tutorial/Plugins/upload/#using-plugin-for-uploading-avatar)
278+
This syntax can be use to get unique avatar for each user of hardcode avatar, but it makes more sense to use it with [upload plugin](https://adminforth.dev/docs/tutorial/Plugins/05-0-upload/#using-plugin-for-uploading-avatar)
279279

280280

281281
## Custom URL

0 commit comments

Comments
 (0)