Skip to content

Commit e982c89

Browse files
committed
isolate hook types for create/edit/delete. Check translations for broken params, add optional record param to s3 callback
1 parent f42e699 commit e982c89

File tree

21 files changed

+203
-87
lines changed

21 files changed

+203
-87
lines changed

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

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,30 @@ AWS_ACCESS_KEY_ID=your_access_key_id
6060
AWS_SECRET_ACCESS_KEY=your_secret_access_key
6161
```
6262
63+
Now add a column for storing the path to the file in the database, add this statement to the `./schema.prisma`:
64+
65+
```ts title="./schema.prisma"
66+
model apartments {
67+
id String @id
68+
created_at DateTime?
69+
title String
70+
square_meter Float?
71+
price Decimal
72+
number_of_rooms Int?
73+
description String?
74+
country String?
75+
listed Boolean
76+
realtor_id String?
77+
//diff-add
78+
apartment_image String?
79+
}
80+
```
81+
82+
Migrate prisma schema:
83+
84+
```bash
85+
npx prisma migrate dev --name add-apartment-image
86+
```
6387
6488
Add column to `aparts` resource configuration:
6589
@@ -116,26 +140,6 @@ export const admin = new AdminForth({
116140
});
117141
```
118142
119-
Add a column for storing the path to the file in the database, add this statement to the `./schema.prisma`:
120-
121-
```ts title="./schema.prisma"
122-
model apartments {
123-
id String @id
124-
created_at DateTime?
125-
title String
126-
square_meter Float?
127-
price Decimal
128-
number_of_rooms Int?
129-
description String?
130-
country String?
131-
listed Boolean
132-
realtor_id String?
133-
//diff-add
134-
apartment_image String?
135-
}
136-
```
137-
138-
![alt text](Upload.png)
139143
140144
Here you can see how the plugin works:
141145
@@ -145,7 +149,23 @@ Here you can see how the plugin works:
145149
This setup will upload files to S3 bucket with private ACL and save path to file (relative to bucket root) in `apartment_image` column.
146150
147151
Once you will go to show or list view of `aparts` resource you will see preview of uploaded file by using presigned temporary URLs
148-
which are generated by plugin.
152+
which are generated by plugin:
153+
154+
155+
![alt text](Upload.png)
156+
157+
158+
> ☝ When upload feature is used on record which already exists in database (from 'edit' view), s3path callback will
159+
> receive additional parameter `record` with all values of record. Generally we don't recommend denormalizing any state
160+
> of record into s3Path (and instead only store links from record to file path like in example above).
161+
> But if you are 100% sure this kind of sate will be static, you might link to it:
162+
>
163+
> ```ts
164+
> s3Path: ({originalExtension, record}) => `game_images/${record.game_code}.${originalExtension}`
165+
> ```
166+
>
167+
> ! Please note that when upload is done from create view, record will be `undefined`.
168+
149169
150170
If you want to draw such images in main non-admin app e.g. Nuxt, you should generate presigned URLs by yourself. Here is NodeJS an example of how to do it:
151171
@@ -167,7 +187,7 @@ export async function getPresignedUrl(s3Path: string): Promise<string> {
167187
}
168188
```
169189
170-
Alternatively you might want to make all objects public, let's consider how to do it.
190+
Alternatively, if you don't want to generate presigned URLs, you might want to make all objects public. Then you will be able concatenate backet base domain and path stored in db, and use it as source of image. Let's consider how to do it.
171191
172192
### S3 upload with public access
173193

adminforth/documentation/docs/tutorial/05-Plugins/10-i18n.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,12 @@ For English it will use 2 pluralization forms (1 and other), for Slavic language
295295
![alt text](image-4.png)
296296

297297

298+
## Limiting access to translating
299+
300+
If you want to limit access to translations resource only to developers or translators, you can use [limiting access](/docs/tutorial/Customization/limitingAccess/) feature.
301+
302+
Please note that access to "Translate selected" bulk action which uses LLM AI translation adapter is determined by allowedActions.edit permission of resource.
303+
298304
## Translations in custom APIs
299305

300306
Sometimes you need to return a translated error or success message from your API. You can use special `tr` function for this.

adminforth/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import {
1515
type AdminForthResourceColumn,
1616
IOperationalResource,
1717
IHttpServer,
18-
BeforeSaveFunction,
19-
AfterSaveFunction,
2018
AdminForthResource,
2119
IAdminForthDataSourceConnectorBase,
2220
IWebSocketBroker,
@@ -340,7 +338,7 @@ class AdminForth implements IAdminForth {
340338
const primaryKey = record[resource.columns.find((col) => col.primaryKey).name];
341339

342340
// execute hook if needed
343-
for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
341+
for (const hook of listify(resource.hooks?.create?.afterSave)) {
344342
process.env.HEAVY_DEBUG && console.log('🪲 Hook afterSave', hook);
345343
const resp = await hook({
346344
recordId: primaryKey,
@@ -372,11 +370,12 @@ class AdminForth implements IAdminForth {
372370
): Promise<{ error?: string }> {
373371

374372
// execute hook if needed
375-
for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
373+
for (const hook of listify(resource.hooks?.edit?.beforeSave)) {
376374
const resp = await hook({
377375
recordId,
378376
resource,
379377
record,
378+
updates: record,
380379
oldRecord,
381380
adminUser,
382381
adminforth: this,
@@ -408,10 +407,11 @@ class AdminForth implements IAdminForth {
408407
}
409408

410409
// execute hook if needed
411-
for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
410+
for (const hook of listify(resource.hooks?.edit?.afterSave)) {
412411
const resp = await hook({
413412
resource,
414-
record,
413+
record,
414+
updates: record,
415415
adminUser,
416416
oldRecord,
417417
recordId,
@@ -434,7 +434,7 @@ class AdminForth implements IAdminForth {
434434
{ resource: AdminForthResource, recordId: any, adminUser: AdminUser, record: any, extra?: HttpExtra }
435435
): Promise<{ error?: string }> {
436436
// execute hook if needed
437-
for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
437+
for (const hook of listify(resource.hooks?.delete?.beforeSave)) {
438438
const resp = await hook({
439439
resource,
440440
record,
@@ -456,7 +456,7 @@ class AdminForth implements IAdminForth {
456456
await connector.deleteRecord({ resource, recordId});
457457

458458
// execute hook if needed
459-
for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
459+
for (const hook of listify(resource.hooks?.delete?.afterSave)) {
460460
const resp = await hook({
461461
resource,
462462
record,

adminforth/modules/configValidator.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
AdminForthConfig,
33
AdminForthResource,
44
IAdminForth, IConfigValidator,
5-
type AfterSaveFunction,
65
AdminForthBulkAction,
76
AdminForthInputConfig,
87
AdminForthConfigCustomization,
@@ -20,7 +19,6 @@ import {
2019
AllowedActionsEnum,
2120
AdminForthComponentDeclaration ,
2221
AdminForthResourcePages,
23-
AdminForthResourceInputCommon,
2422
AdminForthResourceColumnInputCommon,
2523
} from "../types/Common.js";
2624
import AdminForth from "adminforth";
@@ -237,7 +235,7 @@ export default class ConfigValidator implements IConfigValidator {
237235
const record = await connector.getRecordByPrimaryKey(res as AdminForthResource, recordId);
238236

239237
await Promise.all(
240-
(res.hooks.delete.beforeSave as AfterSaveFunction[]).map(
238+
(res.hooks.delete.beforeSave).map(
241239
async (hook) => {
242240
const resp = await hook({
243241
recordId: recordId,
@@ -260,7 +258,7 @@ export default class ConfigValidator implements IConfigValidator {
260258
await connector.deleteRecord({ resource: res as AdminForthResource, recordId });
261259
// call afterDelete hook
262260
await Promise.all(
263-
(res.hooks.delete.afterSave as AfterSaveFunction[]).map(
261+
(res.hooks.delete.afterSave).map(
264262
async (hook) => {
265263
await hook({
266264
resource: res as AdminForthResource,

adminforth/modules/restApi.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export async function interpretResource(
3737
// 'delete' needed for ActionCheckSource.deleteRequest and ActionCheckSource.displayButtons and ActionCheckSource.bulkActionRequest
3838
// 'list' needed for ActionCheckSource.listRequest
3939
// 'create' needed for ActionCheckSource.createRequest and ActionCheckSource.displayButtons
40+
// for bulk actions we need to check all actions because bulk action can use any of them e.g sync allowed with edit
4041
const neededActions = {
4142
[ActionCheckSource.ShowRequest]: ['show'],
4243
[ActionCheckSource.EditRequest]: ['edit'],
@@ -45,7 +46,7 @@ export async function interpretResource(
4546
[ActionCheckSource.ListRequest]: ['list'],
4647
[ActionCheckSource.CreateRequest]: ['create'],
4748
[ActionCheckSource.DisplayButtons]: ['show', 'edit', 'delete', 'create', 'filter'],
48-
[ActionCheckSource.BulkActionRequest]: ['delete'],
49+
[ActionCheckSource.BulkActionRequest]: ['show', 'edit', 'delete', 'create', 'filter'],
4950
}[source];
5051

5152
await Promise.all(
@@ -966,7 +967,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
966967
if (action.allowed) {
967968
const execAllowed = await action.allowed({ adminUser, resource, selectedIds: recordIds, allowedActions });
968969
if (!execAllowed) {
969-
return { error: await tr(`Action {actionId} not allowed`, 'errors', { actionId }) };
970+
return { error: await tr(`Action "{actionId}" not allowed`, 'errors', { actionId: action.label }) };
970971
}
971972
}
972973
const response = await action.action({selectedIds: recordIds, adminUser, resource, tr});

adminforth/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

adminforth/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "adminforth",
3-
"version": "1.5.8-next.22",
3+
"version": "1.5.8-next.25",
44
"description": "OpenSource Vue3 powered forth-generation admin panel",
55
"main": "dist/index.js",
66
"module": "dist/index.js",

adminforth/plugins/audit-log/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ export default class AuditLogPlugin extends AdminForthPlugin {
149149
};
150150

151151
['edit', 'delete'].forEach((hook) => {
152-
resource.hooks[hook].afterSave.push(async ({resource, record, adminUser, oldRecord, extra}) => {
153-
return await this.createLogRecord(resource, hook as AllowedActionsEnum, record, adminUser, oldRecord, extra)
152+
resource.hooks[hook].afterSave.push(async ({resource, updates, adminUser, oldRecord, extra}) => {
153+
return await this.createLogRecord(resource, hook as AllowedActionsEnum, updates, adminUser, oldRecord, extra)
154154
})
155155
});
156156

adminforth/plugins/i18n/index.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ class CachingAdapterMemory implements ICachingAdapter {
4949
}
5050
}
5151

52+
function ensureTemplateHasAllParams(template, newTemplate) {
53+
// string ensureTemplateHasAllParams("a {b} c {d}", "я {b} і {d} в") // true
54+
// string ensureTemplateHasAllParams("a {b} c {d}", "я і {d} в") // false
55+
// string ensureTemplateHasAllParams("a {b} c {d}", "я {bb} і {d} в") // false
56+
const existingParams = template.match(/{[^}]+}/g);
57+
const newParams = newTemplate.match(/{[^}]+}/g);
58+
const existingParamsSet = new Set(existingParams);
59+
const newParamsSet = new Set(newParams);
60+
return existingParamsSet.size === newParamsSet.size && [...existingParamsSet].every(p => newParamsSet.has(p));
61+
}
62+
5263
class AiTranslateError extends Error {
5364
constructor(message: string) {
5465
super(message);
@@ -216,19 +227,31 @@ export default class I18N extends AdminForthPlugin {
216227
// disable create allowedActions for translations
217228
resourceConfig.options.allowedActions.create = false;
218229

230+
// add hook to validate user did not screw up with template params
231+
resourceConfig.hooks.edit.beforeSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
232+
for (const lang of this.options.supportedLanguages) {
233+
if (lang === 'en') {
234+
continue;
235+
}
236+
if (updates[this.trFieldNames[lang]]) { // if user set '', it will have '' in updates, then it is fine, we shoudl nto check it
237+
if (!ensureTemplateHasAllParams(oldRecord[this.enFieldName], updates[this.trFieldNames[lang]])) {
238+
return { ok: false, error: `Template params mismatch for ${updates[this.enFieldName]}. Template param names should be the same as in original string. E. g. 'Hello {name}', should be 'Hola {name}' and not 'Hola {nombre}'!` };
239+
}
240+
}
241+
}
242+
return { ok: true };
243+
});
219244

220245
// add hook on edit of any translation
221-
resourceConfig.hooks.edit.afterSave.push(async ({ record, oldRecord }: { record: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
246+
resourceConfig.hooks.edit.afterSave.push(async ({ updates, oldRecord }: { updates: any, oldRecord?: any }): Promise<{ ok: boolean, error?: string }> => {
247+
console.log('🪲edit.afterSave', JSON.stringify(updates, null, 2),'-----', JSON.stringify(oldRecord, null, 2));
222248
if (oldRecord) {
223249
// find lang which changed
224250
let langsChanged: LanguageCode[] = [];
225251
for (const lang of this.options.supportedLanguages) {
226252
if (lang === 'en') {
227253
continue;
228254
}
229-
if (record[this.trFieldNames[lang]] !== oldRecord[this.trFieldNames[lang]]) {
230-
langsChanged.push(lang);
231-
}
232255
}
233256

234257
// clear frontend cache for all langsChanged
@@ -241,6 +264,7 @@ export default class I18N extends AdminForthPlugin {
241264
}
242265
// clear frontend cache for all lan
243266

267+
244268
return { ok: true };
245269
});
246270

@@ -319,11 +343,16 @@ export default class I18N extends AdminForthPlugin {
319343
if (this.options.completeAdapter) {
320344
resourceConfig.options.bulkActions.push(
321345
{
346+
id: 'translate_all',
322347
label: 'Translate selected',
323348
icon: 'flowbite:language-outline',
324349
// if optional `confirm` is provided, user will be asked to confirm action
325350
confirm: 'Are you sure you want to translate selected items?',
326351
state: 'selected',
352+
allowed: ({ resource, adminUser, selectedIds, allowedActions }) => {
353+
console.log('allowedActions', JSON.stringify(allowedActions));
354+
return allowedActions.edit;
355+
},
327356
action: async ({ selectedIds, tr }) => {
328357
let translatedCount = 0;
329358
try {
@@ -460,6 +489,11 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
460489
strId: translation[this.primaryKeyFieldName],
461490
};
462491
}
492+
// make sure LLM did not screw up with template params
493+
if (translation[this.enFieldName].includes('{') && !ensureTemplateHasAllParams(translation[this.enFieldName], translatedStr)) {
494+
console.warn(`LLM Screwed up with template params mismatch for "${translation[this.enFieldName]}"on language ${lang}, it returned "${translatedStr}"`);
495+
continue;
496+
}
463497
updateStrings[
464498
translation[this.primaryKeyFieldName]
465499
].updates[this.trFieldNames[lang]] = translatedStr;
@@ -507,7 +541,7 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
507541
category: string,
508542
strId: string,
509543
translatedStr: string
510-
}> = {};
544+
}> = {};
511545

512546

513547
const langsInvolved = new Set(Object.keys(needToTranslateByLang));
@@ -535,7 +569,7 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
535569
Object.entries(updateStrings).map(
536570
async ([_, { updates, strId }]: [string, { updates: any, category: string, strId: string }]) => {
537571
// because this will translate all languages, we can set completedLangs to all languages
538-
const futureCompletedFieldValue = this.fullCompleatedFieldValue;
572+
const futureCompletedFieldValue = this.computeCompletedFieldValue(updates);
539573

540574
await this.adminforth.resource(this.resourceConfig.resourceId).update(strId, {
541575
...updates,

adminforth/plugins/rich-editor/ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [1.0.18] - 2024-12-26
2+
3+
### Improved
4+
5+
- COmpatibility with latest AdminForth, remove dependency of hook types
6+
17
## [1.0.17] - 2024-12-06
28

39
### Added

0 commit comments

Comments
 (0)