Skip to content

Commit a12e267

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 33a3fa7 + 5fa778c commit a12e267

File tree

8 files changed

+107
-159
lines changed

8 files changed

+107
-159
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
"typescript": "^5.7.3"
1717
},
1818
"dependencies": {
19-
"adminforth": "latest",
19+
"adminforth": "latest"
2020
}
2121
}

adminforth/documentation/docs/tutorial/05-Plugins/05-upload.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
# Upload
42

53
This plugin allows you to upload files to Amazon S3 bucket.
@@ -305,7 +303,7 @@ new UploadPlugin({
305303
});
306304
```
307305
308-
### Max-width for preview image
306+
### Preview Image Size Configuration
309307
310308
You can set the maximum width for the preview image in the `./resources/apartments.ts` file by adding the `maxWidth` property to the `preview` configuration:
311309
@@ -322,8 +320,18 @@ You can set the maximum width for the preview image in the `./resources/apartmen
322320
s3Path: ({originalFilename, originalExtension, contentType}) =>
323321
`aparts/${new Date().getFullYear()}/${uuid()}-${originalFilename}.${originalExtension}`,
324322
preview: {
325-
//diff-add
326-
maxWidth: '200px', // Set the maximum width for the preview image
323+
// Global width settings (applies to all views if specific view settings not provided)
324+
maxWidth: '200px', // Maximum width for preview images
325+
minWidth: '200px', // Minimum width for preview images
326+
327+
// List view specific settings
328+
maxListWidth: '300px', // Maximum width in list view
329+
minListWidth: '100px', // Minimum width in list view
330+
331+
// Show/detail view specific settings
332+
maxShowWidth: '200px', // Maximum width in show view
333+
minShowWidth: '200px', // Minimum width in show view
334+
327335
...
328336
}
329337

adminforth/documentation/docs/tutorial/06-Advanced/01-plugin-development.md

Lines changed: 17 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -21,112 +21,28 @@ Let's create plugin which auto-completes text in strings
2121
```bash
2222
mkdir -p af-plugin-chatgpt
2323
cd af-plugin-chatgpt
24-
npm init -y
25-
touch index.ts
26-
npm i typescript @types/node -D
27-
```
28-
29-
Edit `package.json`:
30-
31-
```json title='./af-plugin-chatgpt/package.json'
32-
{
33-
...
34-
//diff-remove
35-
"main": "index.js",
36-
//diff-add
37-
"main": "dist/index.js",
38-
//diff-add
39-
"types": "dist/index.d.ts",
40-
//diff-add
41-
"type": "module",
42-
"scripts": {
43-
//diff-remove
44-
"test": "echo \"Error: no test specified\" && exit 1",
45-
//diff-add
46-
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/ && npm version patch"
47-
},
48-
}
49-
```
50-
51-
52-
Install AdminForth for types and classes imports:
53-
54-
```bash
55-
npm i adminforth --save
56-
```
57-
58-
Now create plugin boilerplate in `index.ts`:
59-
60-
```ts title='./af-plugin-chatgpt/index.ts'
61-
62-
import { AdminForthPlugin } from "adminforth";
63-
import type { IAdminForth, IHttpServer, AdminForthResourcePages, AdminForthResourceColumn, AdminForthDataTypes, AdminForthResource } from "adminforth";
64-
import type { PluginOptions } from './types.js';
65-
66-
67-
export default class ChatGptPlugin extends AdminForthPlugin {
68-
options: PluginOptions;
69-
70-
constructor(options: PluginOptions) {
71-
super(options, import.meta.url);
72-
this.options = options;
73-
}
74-
75-
async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
76-
super.modifyResourceConfig(adminforth, resourceConfig);
77-
78-
// simply modify resourceConfig or adminforth.config. You can get access to plugin options via this.options;
79-
}
80-
81-
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
82-
// optional method where you can safely check field types after database discovery was performed
83-
}
84-
85-
instanceUniqueRepresentation(pluginOptions: any) : string {
86-
// optional method to return unique string representation of plugin instance.
87-
// Needed if plugin can have multiple instances on one resource
88-
return `single`;
89-
}
90-
91-
setupEndpoints(server: IHttpServer) {
92-
server.endpoint({
93-
method: 'POST',
94-
path: `/plugin/${this.pluginInstanceId}/example`,
95-
handler: async ({ body }) => {
96-
const { name } = body;
97-
return { hey: `Hello ${name}` };
98-
}
99-
});
100-
}
101-
102-
}
24+
npx adminforth create-plugin
10325
```
10426

105-
Create `types.ts` file:
27+
CLI options:
10628

107-
```ts title='./af-plugin-chatgpt/types.ts'
108-
109-
export interface PluginOptions {
110-
111-
}
112-
```
29+
* **`--plugin-name`** - name for your plugin.
11330

31+
This command will:
32+
1. Set up the TypeScript configuration
33+
2. Create initial plugin files
34+
3. Install required dependencies
11435

115-
Create `./af-plugin-chatgpt/tsconfig.json` file:
36+
The CLI will create the following files and directories:
11637

117-
```json title='./af-plugin-chatgpt/tsconfig.json'
118-
{
119-
"compilerOptions": {
120-
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include*/
121-
"module": "node16", /* Specify what module code is generated. */
122-
"outDir": "./dist", /* Specify an output folder for all emitted files. */
123-
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. */
124-
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
125-
"strict": false, /* Enable all strict type-checking options. */
126-
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
127-
},
128-
"exclude": ["node_modules", "dist", "custom"], /* Exclude files from compilation. */
129-
}
38+
```text
39+
af-plugin-chatgpt/
40+
├── custom
41+
│ └── tsconfig.json # TypeScript configuration for custom components
42+
├── index.ts # Main plugin file with boilerplate code
43+
├── package.json # Plugin package configuration
44+
├── tsconfig.json # TypeScript configuration
45+
└── types.ts # TypeScript types for your plugin
13046
13147
```
13248

@@ -204,37 +120,6 @@ export interface PluginOptions {
204120
}
205121
```
206122

207-
Now we have to create custom Vue component which will be used in plugin. To do it create custom folder:
208-
209-
```bash
210-
mkdir -p af-plugin-chatgpt/custom
211-
```
212-
213-
Also create `tsconfig.ts` file so your IDE will be able to resolve adminforth spa imports:
214-
215-
```json title='./af-plugin-chatgpt/custom/tsconfig.json'
216-
{
217-
"compilerOptions": {
218-
"baseUrl": ".", // This should point to your project root
219-
"paths": {
220-
"@/*": [
221-
// "node_modules/adminforth/dist/spa/src/*"
222-
"../../../spa/src/*"
223-
],
224-
"*": [
225-
// "node_modules/adminforth/dist/spa/node_modules/*"
226-
"../../../spa/node_modules/*"
227-
],
228-
"@@/*": [
229-
// "node_modules/adminforth/dist/spa/src/*"
230-
"."
231-
]
232-
}
233-
}
234-
}
235-
```
236-
237-
238123
We will use `vue-suggestion-input` package in our frontend component.
239124
To install package into frontend component, first of all we have to initialize npm package in custom folder:
240125

@@ -535,7 +420,7 @@ Finally, since we want to support multiple installations on one resource (e.g. o
535420
```
536421
537422
538-
Ro compile plugin run:
423+
To compile plugin run:
539424
540425
```bash
541426
npm run build

adminforth/modules/configValidator.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -330,23 +330,38 @@ export default class ConfigValidator implements IConfigValidator {
330330
return showInTransformedToObject as ShowIn;
331331
}
332332

333-
validateFieldGroups(fieldGroups: {
334-
groupName: string;
335-
columns: string[];
336-
}[], resourceColumns: string[]): void {
337-
if (!fieldGroups) return;
333+
validateFieldGroups(fieldGroups: { groupName: string; columns: string[] }[], allColumnsList: string[]): string[] {
334+
if (!fieldGroups) return allColumnsList;
335+
336+
const columnPositions = new Map<string, number>();
337+
let position = 0;
338338

339339
fieldGroups.forEach((group) => {
340340
group.columns.forEach((col) => {
341-
if (!resourceColumns.includes(col)) {
342-
const similar = suggestIfTypo(resourceColumns, col);
341+
if (!allColumnsList.includes(col)) {
342+
const similar = suggestIfTypo(allColumnsList, col);
343343
throw new Error(
344-
`Group '${group.groupName}' has an unknown column '${col}'. ${similar ? `Did you mean '${similar}'?` : ''
344+
`Group '${group.groupName}' has an unknown column '${col}'. ${
345+
similar ? `Did you mean '${similar}'?` : ''
345346
}`
346347
);
347348
}
349+
if (!columnPositions.has(col)) {
350+
columnPositions.set(col, position++);
351+
}
348352
});
349353
});
354+
355+
allColumnsList.forEach((col) => {
356+
if (!columnPositions.has(col)) {
357+
columnPositions.set(col, position++);
358+
}
359+
});
360+
return allColumnsList.sort((a, b) => {
361+
const posA = columnPositions.get(a);
362+
const posB = columnPositions.get(b);
363+
return posA - posB;
364+
});
350365
}
351366

352367
validateAndNormalizeCustomActions(resInput: AdminForthResourceInput, res: Partial<AdminForthResource>, errors: string[]): any[] {
@@ -500,8 +515,8 @@ export default class ConfigValidator implements IConfigValidator {
500515
if (col.masked) {
501516
errors.push(`Resource "${res.resourceId}" column "${col.name}" isArray cannot be used for a masked column`);
502517
}
503-
if (col.foreignResource) {
504-
errors.push(`Resource "${res.resourceId}" column "${col.name}" isArray cannot be used for a foreignResource column`);
518+
if (col.foreignResource && col.foreignResource.polymorphicResources) {
519+
errors.push(`Resource "${res.resourceId}" column "${col.name}" isArray cannot be used for a polymorphic foreignResource column`);
505520
}
506521

507522
if (!col.type || col.type !== AdminForthDataTypes.JSON) {
@@ -703,10 +718,11 @@ export default class ConfigValidator implements IConfigValidator {
703718
options.actions = this.validateAndNormalizeCustomActions(resInput, res, errors);
704719

705720
const allColumnsList = res.columns.map((col) => col.name);
706-
this.validateFieldGroups(options.fieldGroups, allColumnsList);
707-
this.validateFieldGroups(options.showFieldGroups, allColumnsList);
708-
this.validateFieldGroups(options.createFieldGroups, allColumnsList);
709-
this.validateFieldGroups(options.editFieldGroups, allColumnsList);
721+
const sortedColumns = this.validateFieldGroups(options.fieldGroups, allColumnsList);
722+
723+
res.columns = res.columns.sort((a, b) => {
724+
return sortedColumns.indexOf(a.name) - sortedColumns.indexOf(b.name);
725+
});
710726

711727
// if pageInjection is a string, make array with one element. Also check file exists
712728
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];

adminforth/modules/restApi.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -675,13 +675,22 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
675675
const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == col.foreignResource.resourceId);
676676
const targetConnector = this.adminforth.connectors[targetResource.dataSource];
677677
const targetResourcePkField = targetResource.columns.find((col) => col.primaryKey).name;
678-
const pksUnique = [...new Set(data.data.map((item) => item[col.name]))];
678+
const pksUnique = [...new Set(data.data.reduce((pks, item) => {
679+
if (col.isArray?.enabled) {
680+
if (item[col.name]?.length) {
681+
pks = pks.concat(item[col.name]);
682+
}
683+
} else {
684+
pks.push(item[col.name]);
685+
}
686+
return pks;
687+
}, []))];
679688
if (pksUnique.length === 0) {
680689
return;
681690
}
682691
const targetData = await targetConnector.getData({
683692
resource: targetResource,
684-
limit: limit,
693+
limit: pksUnique.length,
685694
offset: 0,
686695
filters: [
687696
{
@@ -755,7 +764,13 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
755764
}
756765

757766
data.data.forEach((item) => {
758-
item[col.name] = targetDataMap[item[col.name]];
767+
if (col.isArray?.enabled) {
768+
if (item[col.name]?.length) {
769+
item[col.name] = item[col.name].map((i) => targetDataMap[i]);
770+
}
771+
} else {
772+
item[col.name] = targetDataMap[item[col.name]];
773+
}
759774

760775
if (!item[col.name]) {
761776
if (col.foreignResource && col.foreignResource.polymorphicResources) {
@@ -1131,6 +1146,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
11311146
return { error };
11321147
}
11331148
return {
1149+
recordId: record.id,
11341150
ok: true
11351151
}
11361152
}

adminforth/spa/src/components/ValueRenderer.vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
<template>
22
<div>
3-
<span @click="(e)=>{e.stopPropagation()}" v-if="column.foreignResource">
4-
<RouterLink v-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
3+
<span
4+
v-if="column.foreignResource"
5+
:class="{'flex flex-wrap': column.isArray?.enabled}"
6+
@click="(e)=>{e.stopPropagation()}"
7+
>
8+
<span
9+
v-if="record[column.name] && column.isArray?.enabled"
10+
v-for="foreignResource in record[column.name]"
11+
class="rounded-md m-0.5 bg-lightAnnouncementBG dark:bg-darkAnnouncementBG text-lightAnnouncementText dark:text-darkAnnouncementText py-0.5 px-2.5 text-sm"
12+
>
13+
<RouterLink
14+
class="font-medium text-lightSidebarText dark:text-darkSidebarText hover:brightness-110 whitespace-nowrap"
15+
:to="{ name: 'resource-show', params: { primaryKey: foreignResource.pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }"
16+
>
17+
{{ foreignResource.label }}
18+
</RouterLink>
19+
</span>
20+
<RouterLink v-else-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
521
:to="{ name: 'resource-show', params: { primaryKey: record[column.name].pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }">
622
{{ record[column.name].label }}
723
</RouterLink>

adminforth/spa/src/views/EditView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ async function saveRecord() {
172172
});
173173
}
174174
saving.value = false;
175-
router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
175+
router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: resp.recordId } });
176176
}
177177
178178
</script>

dev-demo/resources/apartments.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,16 @@ export default {
223223
{
224224
name: "room_sizes",
225225
type: AdminForthDataTypes.JSON,
226+
// isArray: {
227+
// enabled: true,
228+
// itemType: AdminForthDataTypes.FLOAT,
229+
// },
226230
isArray: {
227231
enabled: true,
228-
itemType: AdminForthDataTypes.FLOAT,
232+
itemType: AdminForthDataTypes.STRING,
233+
},
234+
foreignResource: {
235+
resourceId: "users",
229236
},
230237
},
231238
{

0 commit comments

Comments
 (0)