adminforth 1.0.83 → 1.0.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +34 -16
- package/dist/modules/codeInjector.js +19 -10
- package/dist/plugins/ForeignInlineListPlugin/index.js +3 -2
- package/dist/servers/express.js +0 -3
- package/dist/spa/spa/package-lock.json +13 -0
- package/dist/spa/spa/package.json +1 -0
- package/dist/spa/spa/src/App.vue +44 -19
- package/dist/spa/spa/src/components/AcceptModal.vue +2 -9
- package/dist/spa/spa/src/components/Toast.vue +65 -0
- package/dist/spa/spa/src/components/ValueRenderer.vue +8 -8
- package/dist/spa/spa/src/composables/useStores.ts +51 -0
- package/dist/spa/spa/src/stores/core.ts +5 -1
- package/dist/spa/spa/src/stores/modal.ts +13 -2
- package/dist/spa/spa/src/stores/toast.ts +15 -0
- package/dist/spa/spa/src/views/CreateView.vue +25 -2
- package/dist/spa/spa/src/views/EditView.vue +28 -3
- package/dist/spa/spa/src/views/ListView.vue +28 -5
- package/dist/spa/spa/src/views/ShowView.vue +33 -7
- package/dist/types/AdminForthConfig.js +54 -0
- package/dist/types/FrontendAPI.js +7 -0
- package/documentation/docs/Getting Started.md +374 -0
- package/documentation/docs/Glossary.md +37 -0
- package/documentation/docs/image.png +0 -0
- package/documentation/docusaurus.config.ts +4 -4
- package/documentation/static/CNAME +1 -0
- package/index.ts +50 -35
- package/modules/codeInjector.ts +15 -6
- package/package.json +1 -1
- package/plugins/ForeignInlineListPlugin/index.ts +3 -3
- package/servers/express.ts +4 -9
- package/spa/package-lock.json +13 -0
- package/spa/package.json +1 -0
- package/spa/src/App.vue +44 -19
- package/spa/src/components/AcceptModal.vue +2 -9
- package/spa/src/components/Toast.vue +65 -0
- package/spa/src/components/ValueRenderer.vue +8 -8
- package/spa/src/composables/useStores.ts +51 -0
- package/spa/src/stores/core.ts +5 -1
- package/spa/src/stores/modal.ts +13 -2
- package/spa/src/stores/toast.ts +15 -0
- package/spa/src/views/CreateView.vue +25 -2
- package/spa/src/views/EditView.vue +28 -3
- package/spa/src/views/ListView.vue +28 -5
- package/spa/src/views/ShowView.vue +33 -7
- package/types/AdminForthConfig.ts +469 -42
- package/types/FrontendAPI.ts +73 -0
- package/documentation/static/img/docusaurus-social-card.jpg +0 -0
- package/documentation/static/img/tail.png:Zone.Identifier +0 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
|
|
2
|
+
# Installation
|
|
3
|
+
|
|
4
|
+
## Prerequisites
|
|
5
|
+
|
|
6
|
+
We recommend using Node v18 and higher
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
mkdir myadmin
|
|
12
|
+
cd myadmin
|
|
13
|
+
npm install adminforth
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
AdminForth does not provide own HTTP server, but can add own listeners over exisitng [Express](https://expressjs.com/) server (Fastify support is planned in future). This allows to create custom APIs for backoffice in a way you know.
|
|
17
|
+
|
|
18
|
+
Let's install express:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install express@4.19.2
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For demo purposes we will use SQLite data source. You can use postgress, Mongo or Clickhouse as well.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install better-sqlite3@10.0.0
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can use adminforth in pure Node, but we recommend using TypeScript for better development experience.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install typescript@5.4.5 --save-dev
|
|
35
|
+
npm install tsx@4.11.2 --save-dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
# Philosophy
|
|
39
|
+
|
|
40
|
+
AdminForth connects to existing databases and provide a backoffice for managing data including CRUD operations, filtering, sorting, and more.
|
|
41
|
+
|
|
42
|
+
Database should be already created by using any database management tool, ORM or migrator. AdminForth does not provide a way to create tables or columns in the database.
|
|
43
|
+
|
|
44
|
+
Once you have a database, you pass a connection string to AdminForth and define resources(tables) and columns you would like to see in backoffice. For most DB AdminForth can guess column types and constraints (e.g. max-lenght) by connecting to DB. However you can redefine them in AdminForth configuration. Type and constraints definition are take precedence over DB metadata.
|
|
45
|
+
|
|
46
|
+
Also in AdminForth you can define in "Vue" way how each field will be rendered, and create own pages e.g. Dashboards.
|
|
47
|
+
|
|
48
|
+
In the demo we will create a simple database with 2 tables: `apartments` and `users`. We will just use plain SQL to create tables and insert some fake data.
|
|
49
|
+
|
|
50
|
+
Users table will be used to store a credentials for login into backoffice itself.
|
|
51
|
+
|
|
52
|
+
## Possible configuration options
|
|
53
|
+
|
|
54
|
+
We will use schema with different column types for apartments to show many of AdminForth features.
|
|
55
|
+
|
|
56
|
+
Check [AdminForthConfig](/docs/api/type-aliases/AdminForthConfig.md) for all possible AdminForth Configs.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Setting up a demo
|
|
60
|
+
|
|
61
|
+
Open `package.json`, set `type` to `module` and add `start` script:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
...
|
|
66
|
+
"type": "module",
|
|
67
|
+
"scripts": {
|
|
68
|
+
...
|
|
69
|
+
"start": "ADMINFORTH_SECRET=CHANGE_ME_IN_PRODUCTION NODE_ENV=development tsx watch index.ts"
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
Create `index.ts` file with following content:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
|
|
79
|
+
import betterSqlite3 from 'better-sqlite3';
|
|
80
|
+
import express from 'express';
|
|
81
|
+
import AdminForth from 'adminforth';
|
|
82
|
+
|
|
83
|
+
const dbFile = 'test.sqlite';
|
|
84
|
+
const db = betterSqlite3(dbFile)
|
|
85
|
+
|
|
86
|
+
const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='apartments';`).get();
|
|
87
|
+
if (!tableExists) {
|
|
88
|
+
await db.prepare(`
|
|
89
|
+
CREATE TABLE apartments (
|
|
90
|
+
id VARCHAR(20) PRIMARY KEY NOT NULL,
|
|
91
|
+
title VARCHAR(255) NOT NULL,
|
|
92
|
+
square_meter REAL,
|
|
93
|
+
price DECIMAL(10, 2) NOT NULL,
|
|
94
|
+
number_of_rooms INT,
|
|
95
|
+
description TEXT,
|
|
96
|
+
property_type VARCHAR(255) DEFAULT 'apartment',
|
|
97
|
+
listed BOOLEAN DEFAULT FALSE,
|
|
98
|
+
created_at TIMESTAMP,
|
|
99
|
+
user_id VARCHAR(255)
|
|
100
|
+
);`).run();
|
|
101
|
+
|
|
102
|
+
await db.prepare(`
|
|
103
|
+
CREATE TABLE users (
|
|
104
|
+
id VARCHAR(255) PRIMARY KEY NOT NULL,
|
|
105
|
+
email VARCHAR(255) NOT NULL,
|
|
106
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
107
|
+
created_at VARCHAR(255) NOT NULL,
|
|
108
|
+
role VARCHAR(255) NOT NULL
|
|
109
|
+
);`).run();
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < 50; i++) {
|
|
112
|
+
await db.prepare(`
|
|
113
|
+
INSERT INTO apartments (
|
|
114
|
+
id, title, square_meter, price, number_of_rooms, description, created_at, listed, property_type
|
|
115
|
+
) VALUES ('${i}', 'Apartment ${i}', ${Math.random() * 100}, ${Math.random() * 10000}, ${Math
|
|
116
|
+
.floor(Math.random() * 5) }, 'Next gen appartments', ${Date.now() / 1000 - i * 60 * 60 * 24}, ${i % 2 == 0}, ${i % 2 == 0 ? "'house'" : "'apartment'"});
|
|
117
|
+
`).run();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ADMIN_BASE_URL = '';
|
|
122
|
+
|
|
123
|
+
const admin = new AdminForth({
|
|
124
|
+
baseUrl : ADMIN_BASE_URL,
|
|
125
|
+
rootUser: {
|
|
126
|
+
username: 'adminforth', // use these as credentials to login
|
|
127
|
+
password: 'adminforth',
|
|
128
|
+
},
|
|
129
|
+
auth: {
|
|
130
|
+
resourceId: 'users', // resource for getting user
|
|
131
|
+
usernameField: 'email',
|
|
132
|
+
passwordHashField: 'password_hash',
|
|
133
|
+
},
|
|
134
|
+
customization: {
|
|
135
|
+
brandName: 'My Admin',
|
|
136
|
+
datesFormat: 'D MMM YY HH:mm:ss',
|
|
137
|
+
emptyFieldPlaceholder: '-',
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
dataSources: [
|
|
141
|
+
{
|
|
142
|
+
id: 'maindb',
|
|
143
|
+
url: `sqlite://${dbFile}`
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
resources: [
|
|
147
|
+
{
|
|
148
|
+
dataSource: 'maindb',
|
|
149
|
+
table: 'apartments',
|
|
150
|
+
resourceId: 'apparts', // resourceId is defaulted to table name but you can change it e.g.
|
|
151
|
+
// in case of same table names from different data sources
|
|
152
|
+
label: 'Apartments', // label is defaulted to table name but you can change it
|
|
153
|
+
recordLabel: (r) => `🏡 ${r.title}`,
|
|
154
|
+
columns: [
|
|
155
|
+
{
|
|
156
|
+
name: 'id',
|
|
157
|
+
label: 'Identifier', // if you wish you can redefine label
|
|
158
|
+
showIn: ['filter', 'show'], // show in filter and in show page
|
|
159
|
+
primaryKey: true,
|
|
160
|
+
fillOnCreate: ({initialRecord, adminUser}) => Math.random().toString(36).substring(7), // initialRecord is values user entered, adminUser object of user who creates record
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'title',
|
|
164
|
+
required: true,
|
|
165
|
+
showIn: ['list', 'create', 'edit', 'filter', 'show'], // the default is full set
|
|
166
|
+
maxLength: 255, // you can set max length for string fields
|
|
167
|
+
minLength: 3, // you can set min length for string fields
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'created_at',
|
|
171
|
+
type: AdminForth.Types.DATETIME ,
|
|
172
|
+
allowMinMaxQuery: true,
|
|
173
|
+
showIn: ['list', 'filter', 'show', 'edit'],
|
|
174
|
+
fillOnCreate: ({initialRecord, adminUser}) => (new Date()).toISOString(),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'price',
|
|
178
|
+
min: 10,
|
|
179
|
+
max: 10000.12,
|
|
180
|
+
allowMinMaxQuery: true, // use better experience for filtering e.g. date range, set it only if you have index on this column or if there will be low number of rows
|
|
181
|
+
editingNote: 'Price is in USD', // you can appear note on editing or creating page
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'square_meter',
|
|
185
|
+
label: 'Square',
|
|
186
|
+
allowMinMaxQuery: true,
|
|
187
|
+
minValue: 1, // you can set min /max value for number fields
|
|
188
|
+
maxValue: 1000,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'number_of_rooms',
|
|
192
|
+
allowMinMaxQuery: true,
|
|
193
|
+
enum: [
|
|
194
|
+
{ value: 1, label: '1 room' },
|
|
195
|
+
{ value: 2, label: '2 rooms' },
|
|
196
|
+
{ value: 3, label: '3 rooms' },
|
|
197
|
+
{ value: 4, label: '4 rooms' },
|
|
198
|
+
{ value: 5, label: '5 rooms' },
|
|
199
|
+
],
|
|
200
|
+
allowCustomValue: true,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'description',
|
|
204
|
+
sortable: false,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'property_type',
|
|
208
|
+
enum: [{
|
|
209
|
+
value: 'house',
|
|
210
|
+
label: 'House'
|
|
211
|
+
}, {
|
|
212
|
+
value: 'apartment',
|
|
213
|
+
label: 'Apartment'
|
|
214
|
+
}, {
|
|
215
|
+
value: null,
|
|
216
|
+
label: 'Not defined'
|
|
217
|
+
}],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'listed',
|
|
221
|
+
required: true, // will be required on create/edit
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'user_id',
|
|
225
|
+
foreignResource: {
|
|
226
|
+
resourceId: 'users',
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
options: {
|
|
231
|
+
listPageSize: 12,
|
|
232
|
+
allowedActions:{
|
|
233
|
+
edit: false,
|
|
234
|
+
delete: true,
|
|
235
|
+
show: true,
|
|
236
|
+
filter: true,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
dataSource: 'maindb',
|
|
242
|
+
table: 'users',
|
|
243
|
+
resourceId: 'users',
|
|
244
|
+
label: 'Users',
|
|
245
|
+
recordLabel: (r) => `👤 ${r.email}`,
|
|
246
|
+
columns: [
|
|
247
|
+
{
|
|
248
|
+
name: 'id',
|
|
249
|
+
primaryKey: true,
|
|
250
|
+
fillOnCreate: ({initialRecord, adminUser}) => Math.random().toString(36).substring(7),
|
|
251
|
+
showIn: ['list', 'filter', 'show'],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'email',
|
|
255
|
+
required: true,
|
|
256
|
+
isUnique: true,
|
|
257
|
+
validation: [
|
|
258
|
+
{
|
|
259
|
+
regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
260
|
+
message: 'Email is not valid, must be in format example@test.com'
|
|
261
|
+
},
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'created_at',
|
|
266
|
+
type: AdminForth.Types.DATETIME,
|
|
267
|
+
showIn: ['list', 'filter', 'show'],
|
|
268
|
+
fillOnCreate: ({initialRecord, adminUser}) => (new Date()).toISOString(),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'role',
|
|
272
|
+
enum: [
|
|
273
|
+
{ value: 'superadmin', label: 'Super Admin' },
|
|
274
|
+
{ value: 'user', label: 'User' },
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'password',
|
|
279
|
+
virtual: true, // field will not be persisted into db
|
|
280
|
+
required: { create: true }, // make required only on create page
|
|
281
|
+
editingNote: { edit: 'Leave empty to keep password unchanged' },
|
|
282
|
+
minLength: 8,
|
|
283
|
+
type: AdminForth.Types.STRING,
|
|
284
|
+
showIn: ['create', 'edit'], // to show field only on create and edit pages
|
|
285
|
+
masked: true, // to show stars in input field
|
|
286
|
+
}
|
|
287
|
+
],
|
|
288
|
+
hooks: {
|
|
289
|
+
create: {
|
|
290
|
+
beforeSave: async ({ record, adminUser, resource }) => {
|
|
291
|
+
record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);
|
|
292
|
+
return { ok:true, error: false };
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
edit: {
|
|
296
|
+
beforeSave: async ({ record, adminUser, resource}) => {
|
|
297
|
+
if (record.password) {
|
|
298
|
+
record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);
|
|
299
|
+
}
|
|
300
|
+
return { ok: true, error: false }
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
menu: [
|
|
307
|
+
{
|
|
308
|
+
label: 'Core',
|
|
309
|
+
icon: 'flowbite:brain-solid', // any icon from iconify supported in format <setname>:<icon>, e.g. from here https://icon-sets.iconify.design/flowbite/
|
|
310
|
+
open: true,
|
|
311
|
+
children: [
|
|
312
|
+
{
|
|
313
|
+
homepage: true,
|
|
314
|
+
label: 'Appartments',
|
|
315
|
+
icon: 'flowbite:home-solid',
|
|
316
|
+
resourceId: 'apparts',
|
|
317
|
+
},
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
type: 'gap'
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
type: 'divider'
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
type: 'heading',
|
|
328
|
+
label: 'SYSTEM',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
label: 'Users',
|
|
332
|
+
icon: 'flowbite:user-solid',
|
|
333
|
+
resourceId: 'users',
|
|
334
|
+
}
|
|
335
|
+
],
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
const app = express()
|
|
340
|
+
app.use(express.json());
|
|
341
|
+
const port = 3500;
|
|
342
|
+
|
|
343
|
+
(async () => {
|
|
344
|
+
// needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime
|
|
345
|
+
await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});
|
|
346
|
+
console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');
|
|
347
|
+
})();
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
// serve after you added all api
|
|
351
|
+
admin.express.serve(app, express)
|
|
352
|
+
admin.discoverDatabases();
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
app.listen(port, () => {
|
|
356
|
+
console.log(`Example app listening at http://localhost:${port}`)
|
|
357
|
+
console.log(`\n⚡ AdminForth is available at http://localhost:${port}${ADMIN_BASE_URL}\n`)
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
Now you can run your app:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
npm start
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Open http://localhost:3500 in your browser and login with credentials `adminforth` / `adminforth`.
|
|
369
|
+
|
|
370
|
+

|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
After Login you should see:
|
|
374
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
# dataSource
|
|
3
|
+
|
|
4
|
+
A DataSource is a connection to one database. Datasources has id for references from resources and URL which follows the standard URI format. For example `mysql://user:password@localhost:3306/database`.
|
|
5
|
+
It used to:
|
|
6
|
+
|
|
7
|
+
* Discover the columns in the database
|
|
8
|
+
* Make queries to get the list and show records
|
|
9
|
+
* Make queries to modify data
|
|
10
|
+
|
|
11
|
+
There might be several datasources in the system for vairous databases e.g. One 2 Mongo DBs and 1 Postgres DB.
|
|
12
|
+
|
|
13
|
+
# resource
|
|
14
|
+
|
|
15
|
+
A Resource is a representation of a table or collection in AdminForth. One resource is one table in the database.
|
|
16
|
+
It has a `name` which should match name in database, a datasource id, and a list of columns.
|
|
17
|
+
Also it has various customization options.
|
|
18
|
+
|
|
19
|
+
# column
|
|
20
|
+
|
|
21
|
+
A Column is a representation of a column in a table. It has a `name` which should be equal to name in database and various configuration options.
|
|
22
|
+
|
|
23
|
+
# record
|
|
24
|
+
|
|
25
|
+
A record is a row in a relational database table. Or Document in document database table.
|
|
26
|
+
|
|
27
|
+
# adminUser
|
|
28
|
+
|
|
29
|
+
Object which represents a user who logged in to the AdminForth
|
|
30
|
+
|
|
31
|
+
# hook
|
|
32
|
+
|
|
33
|
+
Hook is a optional async function which allows to inject in backend logic before exuting the datasource query or after it
|
|
34
|
+
|
|
35
|
+
# component
|
|
36
|
+
|
|
37
|
+
Component is a Vue component which is used to add or modify UI elements in AdminForth.
|
|
Binary file
|
|
@@ -8,7 +8,7 @@ const config: Config = {
|
|
|
8
8
|
favicon: 'img/favicon.png',
|
|
9
9
|
|
|
10
10
|
// Set the production url of your site here
|
|
11
|
-
url: 'https://adminforth.
|
|
11
|
+
url: 'https://adminforth.dev',
|
|
12
12
|
// Set the /<baseUrl>/ pathname under which your site is served
|
|
13
13
|
// For GitHub pages deployment, it is often '/<projectName>/'
|
|
14
14
|
baseUrl: '/',
|
|
@@ -58,7 +58,7 @@ const config: Config = {
|
|
|
58
58
|
[
|
|
59
59
|
"docusaurus-plugin-typedoc",
|
|
60
60
|
{
|
|
61
|
-
entryPoints: ["../types/AdminForthConfig.ts"],
|
|
61
|
+
entryPoints: ["../types/AdminForthConfig.ts", "../types/FrontendAPI.ts"],
|
|
62
62
|
plugin: ["./typedoc-plugin.mjs"],
|
|
63
63
|
readme: "none",
|
|
64
64
|
indexFormat: "table",
|
|
@@ -76,7 +76,7 @@ const config: Config = {
|
|
|
76
76
|
|
|
77
77
|
themeConfig: {
|
|
78
78
|
// Replace with your project's social card
|
|
79
|
-
image: 'img/docusaurus-social-card.jpg',
|
|
79
|
+
// image: 'img/docusaurus-social-card.jpg',
|
|
80
80
|
navbar: {
|
|
81
81
|
title: 'AdminForth',
|
|
82
82
|
logo: {
|
|
@@ -162,7 +162,7 @@ const config: Config = {
|
|
|
162
162
|
copyright: `Copyright © ${new Date().getFullYear()} Devforth sp. z o.o.`,
|
|
163
163
|
},
|
|
164
164
|
prism: {
|
|
165
|
-
theme: prismThemes.
|
|
165
|
+
theme: prismThemes.okaidia,
|
|
166
166
|
darkTheme: prismThemes.dracula,
|
|
167
167
|
},
|
|
168
168
|
} satisfies Preset.ThemeConfig,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adminforth.dev
|
package/index.ts
CHANGED
|
@@ -9,18 +9,15 @@ import ExpressServer from './servers/express.js';
|
|
|
9
9
|
import {v1 as uuid} from 'uuid';
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import { ADMINFORTH_VERSION } from './modules/utils.js';
|
|
12
|
-
import { AdminForthConfig, AdminForthClass, AdminForthFilterOperators, AdminForthDataTypes } from './types/AdminForthConfig.js';
|
|
12
|
+
import { AdminForthConfig, AdminForthClass, AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages } from './types/AdminForthConfig.js';
|
|
13
13
|
import { getFunctionList } from './modules/utils.js';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
|
|
16
|
-
const AVAILABLE_SHOW_IN = ['list', 'edit', 'create', 'filter', 'show'];
|
|
17
|
-
const DEFAULT_ALLOWED_ACTIONS = {create: true, edit: true, show: true, delete: true};
|
|
18
16
|
|
|
17
|
+
//get array from enum AdminForthResourcePages
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ALLOWED_ACTIONS = {create: true, edit: true, show: true, delete: true};
|
|
19
20
|
|
|
20
|
-
type ValidationObject = {
|
|
21
|
-
regex: string,
|
|
22
|
-
message: string,
|
|
23
|
-
}
|
|
24
21
|
|
|
25
22
|
class AdminForth implements AdminForthClass {
|
|
26
23
|
static Types = AdminForthDataTypes;
|
|
@@ -41,7 +38,7 @@ class AdminForth implements AdminForthClass {
|
|
|
41
38
|
codeInjector: CodeInjector;
|
|
42
39
|
connectors: any;
|
|
43
40
|
connectorClasses: any;
|
|
44
|
-
runningHotReload
|
|
41
|
+
runningHotReload: boolean;
|
|
45
42
|
|
|
46
43
|
statuses: {
|
|
47
44
|
dbDiscover?: 'running' | 'done',
|
|
@@ -151,12 +148,12 @@ class AdminForth implements AdminForthClass {
|
|
|
151
148
|
if (!res.table) {
|
|
152
149
|
errors.push(`Resource "${res.dataSource}" is missing table`);
|
|
153
150
|
}
|
|
154
|
-
// if
|
|
155
|
-
if (res.
|
|
156
|
-
errors.push(`Resource "${res.dataSource}"
|
|
151
|
+
// if recordLabel is not callable, throw error
|
|
152
|
+
if (res.recordLabel && typeof res.recordLabel !== 'function') {
|
|
153
|
+
errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
|
|
157
154
|
}
|
|
158
|
-
if (!res.
|
|
159
|
-
res.
|
|
155
|
+
if (!res.recordLabel) {
|
|
156
|
+
res.recordLabel = (item) => {
|
|
160
157
|
const pkVal = item[res.columns.find((col) => col.primaryKey).name];
|
|
161
158
|
return `${res.label} ${pkVal}`;
|
|
162
159
|
}
|
|
@@ -203,13 +200,11 @@ class AdminForth implements AdminForthClass {
|
|
|
203
200
|
}
|
|
204
201
|
}
|
|
205
202
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const wrongShowIn = col.showIn && col.showIn.find((c) => !AVAILABLE_SHOW_IN.includes(c));
|
|
203
|
+
const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
|
|
209
204
|
if (wrongShowIn) {
|
|
210
|
-
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${
|
|
205
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
|
|
211
206
|
}
|
|
212
|
-
col.showIn = col.showIn
|
|
207
|
+
col.showIn = col.showIn || Object.values(AdminForthResourcePages);
|
|
213
208
|
})
|
|
214
209
|
|
|
215
210
|
if (!res.options) {
|
|
@@ -243,21 +238,41 @@ class AdminForth implements AdminForthClass {
|
|
|
243
238
|
return Object.assign(action, {id: uuid()});
|
|
244
239
|
});
|
|
245
240
|
bulkActions = newBulkActions;
|
|
246
|
-
}
|
|
247
241
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
242
|
+
// if pageInjection is a string, make array with one element. Also check file exists
|
|
243
|
+
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom'];
|
|
244
|
+
if(res.options.pageInjections) {
|
|
245
|
+
Object.entries(res.options.pageInjections).map(([key, value]) => {
|
|
246
|
+
Object.entries(value).map(([injection, target]) => {
|
|
247
|
+
if (possibleInjections.includes(injection)) {
|
|
248
|
+
if (typeof target === 'string') {
|
|
249
|
+
res.options.pageInjections[key][injection] = [target];
|
|
250
|
+
}
|
|
251
|
+
res.options.pageInjections[key][injection].forEach((target) => {
|
|
252
|
+
errors.push(...this.checkCustomFileExists(target));
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
})
|
|
258
260
|
}
|
|
259
|
-
})
|
|
260
261
|
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//add default allowedActions to resources
|
|
265
|
+
if(res.options.allowedActions){
|
|
266
|
+
//check if allowedActions is an object
|
|
267
|
+
if(typeof res.options.allowedActions !== 'object'){
|
|
268
|
+
errors.push(`Resource "${res.resourceId}" allowedActions must be an object`);
|
|
269
|
+
}
|
|
270
|
+
const userAllowedActions = res.options.allowedActions
|
|
271
|
+
res.options.allowedActions = Object.assign({}, DEFAULT_ALLOWED_ACTIONS, userAllowedActions);
|
|
272
|
+
} else {
|
|
273
|
+
res.options.allowedActions = DEFAULT_ALLOWED_ACTIONS;
|
|
274
|
+
}
|
|
275
|
+
})
|
|
261
276
|
|
|
262
277
|
|
|
263
278
|
|
|
@@ -335,8 +350,8 @@ class AdminForth implements AdminForthClass {
|
|
|
335
350
|
// check is all custom components files exists
|
|
336
351
|
for (const resource of this.config.resources) {
|
|
337
352
|
for (const column of resource.columns) {
|
|
338
|
-
if (column.
|
|
339
|
-
for (const [key, value] of Object.entries(column.
|
|
353
|
+
if (column.components) {
|
|
354
|
+
for (const [key, value] of Object.entries(column.components)) {
|
|
340
355
|
if (this.codeInjector.allComponentNames[value]) {
|
|
341
356
|
// not obvious, but if we are in this if, it means that this is plugin component
|
|
342
357
|
// and there is no sense to check if it exists in users folder
|
|
@@ -675,7 +690,7 @@ class AdminForth implements AdminForthClass {
|
|
|
675
690
|
});
|
|
676
691
|
const targetDataMap = targetData.data.reduce((acc, item) => {
|
|
677
692
|
acc[item[targetResourcePkField]] = {
|
|
678
|
-
label: targetResource.
|
|
693
|
+
label: targetResource.recordLabel(item),
|
|
679
694
|
pk: item[targetResourcePkField],
|
|
680
695
|
}
|
|
681
696
|
return acc;
|
|
@@ -707,7 +722,7 @@ class AdminForth implements AdminForthClass {
|
|
|
707
722
|
});
|
|
708
723
|
|
|
709
724
|
data.data.forEach((item) => {
|
|
710
|
-
item._label = resource.
|
|
725
|
+
item._label = resource.recordLabel(item);
|
|
711
726
|
});
|
|
712
727
|
|
|
713
728
|
return {
|
|
@@ -761,7 +776,7 @@ class AdminForth implements AdminForthClass {
|
|
|
761
776
|
});
|
|
762
777
|
const items = dbDataItems.data.map((item) => {
|
|
763
778
|
const pk = item[targetResource.columns.find((col) => col.primaryKey).name];
|
|
764
|
-
const labler = targetResource.
|
|
779
|
+
const labler = targetResource.recordLabel;
|
|
765
780
|
return {
|
|
766
781
|
value: pk,
|
|
767
782
|
label: labler(item),
|
package/modules/codeInjector.ts
CHANGED
|
@@ -10,7 +10,7 @@ import AdminForth from '../index.js';
|
|
|
10
10
|
import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
|
|
11
11
|
import { getComponentNameFromPath } from './utils.js';
|
|
12
12
|
import { styles } from '../styles.js'
|
|
13
|
-
|
|
13
|
+
import { CodeInjectorType } from '../types/AdminForthConfig.js';
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
@@ -30,7 +30,7 @@ function hashify(obj) {
|
|
|
30
30
|
('sha256').update(JSON.stringify(obj)).digest('hex');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
class CodeInjector {
|
|
33
|
+
class CodeInjector implements CodeInjectorType {
|
|
34
34
|
|
|
35
35
|
allWatchers = [];
|
|
36
36
|
adminforth: AdminForth;
|
|
@@ -260,19 +260,28 @@ class CodeInjector {
|
|
|
260
260
|
}).join('\n');
|
|
261
261
|
|
|
262
262
|
// for each custom component generate import statement
|
|
263
|
-
const customComponentsDir = this.adminforth.config.customization?.customComponentsDir;
|
|
264
|
-
|
|
265
263
|
const customResourceComponents = [];
|
|
266
264
|
this.adminforth.config.resources.forEach((resource) => {
|
|
267
265
|
resource.columns.forEach((field) => {
|
|
268
|
-
if (field.
|
|
269
|
-
Object.values(field.
|
|
266
|
+
if (field.components) {
|
|
267
|
+
Object.values(field.components).forEach((filePath) => {
|
|
270
268
|
if (!customResourceComponents.includes(filePath)) {
|
|
271
269
|
customResourceComponents.push(filePath);
|
|
272
270
|
}
|
|
273
271
|
});
|
|
274
272
|
}
|
|
275
273
|
});
|
|
274
|
+
(Object.values(resource.options?.pageInjections || {})).forEach((injection) => {
|
|
275
|
+
Object.values(injection).forEach((filePathes: string[]) => {
|
|
276
|
+
filePathes.forEach((filePath) => {
|
|
277
|
+
if (!customResourceComponents.includes(filePath)) {
|
|
278
|
+
customResourceComponents.push(filePath);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
|
|
276
285
|
});
|
|
277
286
|
|
|
278
287
|
customResourceComponents.forEach((filePath) => {
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdminForthResource } from "../../types/AdminForthConfig.js";
|
|
1
|
+
import { AdminForthResource, AdminForthResourcePages } from "../../types/AdminForthConfig.js";
|
|
2
2
|
import AdminForthPlugin from "../base.js";
|
|
3
3
|
import AdminForth from "../../index.js";
|
|
4
4
|
|
|
@@ -30,8 +30,8 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
|
|
|
30
30
|
name: `foreignInlineList_${this.foreignResource.resourceId}`,
|
|
31
31
|
label: 'Foreign Inline List',
|
|
32
32
|
virtual: true,
|
|
33
|
-
showIn: [
|
|
34
|
-
|
|
33
|
+
showIn: [AdminForthResourcePages.show],
|
|
34
|
+
components: {
|
|
35
35
|
showRow: this.componentPath('InlineList.vue'),
|
|
36
36
|
},
|
|
37
37
|
});
|