Skip to content

Commit e5e3699

Browse files
committed
Merge branch 'main' of github.com:devforth/adminforth
2 parents f9523da + 639b264 commit e5e3699

73 files changed

Lines changed: 1317 additions & 712 deletions

File tree

Some content is hidden

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

AGENTS.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11

22
# AGENTS.md
33

4+
## Package manager
5+
6+
All packages and projects in this repo use `pnpm` and not `npm`.
7+
Howeverer internally (e.g. in `codeInjector`) adminforth still supports both `npm` and `pnpm` style install commands, so users of framework itself can use it with either package manager. But in all dev demo/live demo, plugins, adapters, and documentation, we use `pnpm` as the standard.
8+
9+
## Package names rules
10+
11+
12+
All adapters and plugins always have `@adminforth/` prefix in their package name, followed by short lowercase kebab-case plugin/adpater slug.
13+
14+
Every plugin has at least one Docusaurus docs page, which should use the path `/docs/tutorial/Plugins/<plugin-slug>/`.
15+
16+
Same for adapters, but with `/docs/tutorial/Adapters/<adapter-slug>/` path.
17+
18+
Page names in docusarus should be human readabale. We should not use `AuditLog` but instead we should have `Audit Log` via whitespace.
19+
20+
421
## General engineering rules
522

623
Write code as if the system contracts are already defined and trusted.

adminforth/dataConnectors/baseConnector.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ type AdminForthFilterNormalizationResult = {
2222
normalizedFilters?: AdminForthFilterInput;
2323
};
2424

25+
async function publishShowPageUpdate(resource: AdminForthResource, recordId: string, updates: Record<string, any>) {
26+
await global.adminforth.websocket.publish(`/showPage/${resource.resourceId}/${String(recordId)}`, {
27+
resourceId: resource.resourceId,
28+
recordId,
29+
updates,
30+
});
31+
}
32+
2533

2634
export default class AdminForthBaseConnector implements IAdminForthDataSourceConnectorBase {
2735

@@ -578,6 +586,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
578586
afLogger.trace(`🪲✏️ updating record id:${recordId}, values: ${JSON.stringify(recordWithOriginalValues)}`);
579587

580588
await this.updateRecordOriginalValues({ resource, recordId, newValues: recordWithOriginalValues });
589+
await publishShowPageUpdate(resource, recordId, newValues);
581590

582591
return { ok: true };
583592
}

adminforth/dataConnectors/sqlite.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
99

1010
async setupClient(url: string): Promise<void> {
1111
this.client = betterSqlite3(url.replace('sqlite://', ''));
12+
this.client.aggregate('median', {
13+
start: (): number[] => [],
14+
step: (acc: number[], val: any) => {
15+
if (val != null) acc.push(Number(val));
16+
},
17+
result: (acc: number[]): number | null => {
18+
if (acc.length === 0) return null;
19+
const sorted = acc.slice().sort((a, b) => a - b);
20+
const mid = Math.floor(sorted.length / 2);
21+
return sorted.length % 2 === 0
22+
? (sorted[mid - 1] + sorted[mid]) / 2
23+
: sorted[mid];
24+
},
25+
});
1226
}
1327
async getAllTables(): Promise<Array<string>> {
1428
const stmt = this.client.prepare(
@@ -358,7 +372,7 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
358372
case 'avg': selectParts.push(`AVG("${rule.field}") AS "${alias}"`); break;
359373
case 'min': selectParts.push(`MIN("${rule.field}") AS "${alias}"`); break;
360374
case 'max': selectParts.push(`MAX("${rule.field}") AS "${alias}"`); break;
361-
case 'median': throw new Error('Aggregates.median() with GroupBy.Field is not supported in SQLite.');
375+
case 'median': selectParts.push(`median("${rule.field}") AS "${alias}"`); break;
362376
}
363377
}
364378

adminforth/documentation/blog/2024-10-01-ai-blog/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Add modules:
4747

4848
```bash
4949
cd ai-blog
50-
npm i @adminforth/upload @adminforth/rich-editor @adminforth/text-complete @adminforth/chat-gpt slugify http-proxy @adminforth/image-generation-adapter-openai @adminforth/completion-adapter-open-ai-chat-gpt
50+
npm i @adminforth/upload @adminforth/rich-editor @adminforth/text-complete @adminforth/chat-gpt slugify http-proxy @adminforth/image-generation-adapter-openai @adminforth/completion-adapter-openai-responses
5151
```
5252

5353

@@ -475,7 +475,7 @@ import UploadPlugin from '@adminforth/upload';
475475
import RichEditorPlugin from '@adminforth/rich-editor';
476476
import ChatGptPlugin from '@adminforth/chat-gpt';
477477
import slugify from 'slugify';
478-
import CompletionAdapterOpenAIChatGPT from "@adminforth/completion-adapter-open-ai-chat-gpt";
478+
import CompletionAdapterOpenAIResponses from "@adminforth/completion-adapter-openai-responses";
479479
import ImageGenerationAdapterOpenAI from '@adminforth/image-generation-adapter-openai';
480480

481481
export default {
@@ -591,7 +591,7 @@ export default {
591591
new RichEditorPlugin({
592592
htmlFieldName: 'content',
593593
completion: {
594-
adapter: new CompletionAdapterOpenAIChatGPT({
594+
adapter: new CompletionAdapterOpenAIResponses({
595595
openAiApiKey: process.env.OPENAI_API_KEY as string,
596596
model: 'gpt-4o',
597597
expert: {

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

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ After the resource file is generated, extend it with display and validation sett
197197
To properly apply these changes, refer to the example below and adjust the configuration according to your settings
198198

199199
```ts title="./resources/apartments.ts"
200-
import { AdminForthDataTypes, AdminForthResourceInput } from 'adminforth';
200+
import { AdminForthResourceInput, AdminForthDataTypes } from 'adminforth';
201201

202202
export default {
203203
dataSource: 'maindb',
@@ -206,32 +206,41 @@ export default {
206206
resourceId: 'apartments'
207207
//diff-add
208208
resourceId: 'aparts', // resourceId is defaulted to table name but you can redefine it like this e.g.
209+
//diff-add
209210
// in case of same table names from different data sources
210211
label: 'Apartments', // label is defaulted to table name but you can change it
211212
//diff-add
212213
recordLabel: (r) => `🏡 ${r.title}`,
213214
columns: [
214215
{
215216
name: 'id',
217+
//diff-add
216218
type: AdminForthDataTypes.STRING,
217219
//diff-add
218220
label: 'Identifier', // if you wish you can redefine label, defaulted to uppercased name
219221
showIn: { // show column in filter and in show page
220-
//diff-add
222+
//diff-remove
223+
all:true,
224+
//diff-add
221225
list: false,
222226
//diff-add
223227
edit: false,
224228
//diff-add
225229
create: false,
226230
},
231+
//diff-add
227232
primaryKey: true,
228233
//diff-add
229234
fillOnCreate: ({ initialRecord, adminUser }) => Math.random().toString(36).substring(7), // called during creation to generate content of field, initialRecord is values user entered, adminUser object of user who creates record
230235
},
231236
{
232-
name: 'title',
237+
name: "title",
238+
//diff-add
233239
required: true,
234-
showIn: { all: true }, // all available options
240+
showIn: {
241+
all:true, // all available options
242+
},
243+
//diff-add
235244
type: AdminForthDataTypes.STRING,
236245
//diff-add
237246
maxLength: 255, // you can set max length for string fields
@@ -240,14 +249,24 @@ export default {
240249
},
241250
{
242251
name: 'created_at',
252+
//diff-add
243253
type: AdminForthDataTypes.DATETIME,
254+
//diff-add
244255
allowMinMaxQuery: true,
245-
showIn: { create: false },
256+
showIn: {
257+
//diff-remove
258+
all:true,
259+
//diff-add
260+
create: false,
261+
},
246262
//diff-add
247263
fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),
248264
},
249265
{
250266
name: 'price',
267+
showIn: {
268+
all:true,
269+
},
251270
//diff-add
252271
inputSuffix: 'USD', // you can add a suffix to an input field that will be displayed when creating or editing records
253272
//diff-add
@@ -257,16 +276,25 @@ export default {
257276
},
258277
{
259278
name: 'square_meter',
279+
//diff-add
260280
label: 'Square',
281+
//diff-add
261282
allowMinMaxQuery: true,
283+
showIn: {
284+
all:true,
285+
},
262286
//diff-add
263287
minValue: 1, // you can set min /max value for number columns so users will not be able to enter more/less
264288
//diff-add
265289
maxValue: 1000,
266290
},
267291
{
268292
name: 'number_of_rooms',
293+
//diff-add
269294
allowMinMaxQuery: true,
295+
showIn: {
296+
all:true,
297+
},
270298
//diff-add
271299
enum: [
272300
//diff-add
@@ -284,11 +312,20 @@ export default {
284312
},
285313
{
286314
name: 'description',
315+
//diff-add
287316
sortable: false,
288-
showIn: { list: false },
317+
showIn: {
318+
//diff-remove
319+
all:true,
320+
//diff-add
321+
list: false,
322+
}
289323
},
290324
{
291325
name: 'country',
326+
showIn: {
327+
all:true,
328+
},
292329
//diff-add
293330
enum: [{
294331
//diff-add
@@ -362,18 +399,27 @@ export default {
362399
name: 'listed',
363400
//diff-add
364401
required: true, // will be required on create/edit
402+
showIn: {
403+
all:true,
404+
}
365405
},
366406
{
367407
name: 'realtor_id',
408+
//diff-add
368409
foreignResource: {
410+
//diff-add
369411
resourceId: 'adminuser',
370412
//diff-add
371413
searchableFields: ["id", "email"], // fields available for search in filter
414+
//diff-add
415+
},
416+
showIn: {
417+
all:true,
372418
}
373419
}
374420
],
375421
options: {
376-
listPageSize: 12,
422+
listPageSize: 10,
377423
//diff-add
378424
allowedActions: {
379425
//diff-add
@@ -439,8 +485,17 @@ export const admin = new AdminForth({
439485
},
440486
{
441487
label: 'Users',
442-
...
443-
}
488+
icon: 'flowbite:user-solid',
489+
resourceId: 'adminuser'
490+
},
491+
{
492+
label: "Apartments",
493+
icon: "flowbite:user-solid",
494+
//diff-remove
495+
resourceId: "apartments",
496+
//diff-add
497+
resourceId: "aparts",
498+
},
444499
],
445500
...
446501
});
@@ -467,7 +522,7 @@ async function seedDatabase() {
467522
//diff-add
468523
title: `Apartment ${i}`,
469524
//diff-add
470-
square_meter: (Math.random() * 100).toFixed(1),
525+
square_meter: Number((Math.random() * 100).toFixed(1)),
471526
//diff-add
472527
price: (Math.random() * 10000).toFixed(2),
473528
//diff-add

adminforth/documentation/docs/tutorial/03-Customization/04-hooks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ When user opens edit page, AdminForth makes a request to the backend to get the
4444

4545
Practically you can use `show.afterDatasourceResponse` to modify or add some data before it is displayed on the edit page.
4646

47-
For example [upload plugin](/docs/tutorial/Plugins/05-0-upload/) uses this hook to generate signed preview URL so user can see existing uploaded file preview in form, and at the same time database stores only original file path which might be not accessible without presigned URL.
47+
For example [upload plugin](/docs/tutorial/Plugins/upload/) uses this hook to generate signed preview URL so user can see existing uploaded file preview in form, and at the same time database stores only original file path which might be not accessible without presigned URL.
4848

4949
## Saving data on edit page
5050

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

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

278278
```
279279

280-
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)
280+
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)
281281

282282

283283
## Custom URL

adminforth/documentation/docs/tutorial/03-Customization/16-websocket.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ On server you can publish data to the topic by calling
2020
admin.websocket.publish('/topic-name', {some: 'data'});
2121
```
2222

23+
If you need to unsubscribe from a whole family of topics, for example when route changes can leave old dynamic subscriptions behind, you can use `unsubscribeByPrefix`:
24+
25+
```javascript
26+
import websocket from '@/websocket';
27+
28+
websocket.unsubscribeByPrefix('/topic-name/');
29+
```
30+
31+
This will unsubscribe from all topics whose name starts with the prefix.
32+
33+
It is useful for dynamic topics like `/topic-name/<resourceId>/<recordId>` where a stale subscription can update the wrong page if component unmount does not happen exactly when you expect.
34+
2335
Let's consider a real-world example.
2436

2537
## Usage example
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Email Adapters
2+
3+
Used to send emails.
4+
5+
[Email adapter base class](https://github.com/devforth/adminforth/blob/917d897c866975a4aee29273377f2c07cb6ddf81/adminforth/types/adapters/EmailAdapter.ts#L17)
6+
7+
## AWS SES Email Adapter
8+
9+
```bash
10+
pnpm i @adminforth/email-adapter-aws-ses
11+
```
12+
13+
Enables email delivery via [Amazon Simple Email Service (SES)](https://aws.amazon.com/ses/), suitable for high-volume, programmatic email sending.
14+
15+
## Mailgun Email Adapter
16+
17+
```bash
18+
pnpm i @adminforth/email-adapter-mailgun
19+
```
20+
21+
Allows sending transactional or marketing emails using [Mailgun](https://www.mailgun.com/), a developer-friendly email service.

0 commit comments

Comments
 (0)