Skip to content

Commit cbb6e64

Browse files
authored
Merge pull request #218 from devforth/next
Next
2 parents 9cfc2a8 + 261b45e commit cbb6e64

File tree

37 files changed

+805
-235
lines changed

37 files changed

+805
-235
lines changed

adminforth/commands/cli.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import createApp from "./createApp/main.js";
88
import generateModels from "./generateModels.js";
99
import createPlugin from "./createPlugin/main.js";
1010
import createComponent from "./createCustomComponent/main.js";
11+
import createResource from "./createResource/main.js";
1112
import chalk from "chalk";
1213
import path from "path";
1314
import fs from "fs";
@@ -58,6 +59,9 @@ switch (command) {
5859
case "component":
5960
createComponent(args);
6061
break;
62+
case "resource":
63+
createResource(args);
64+
break;
6165
case "help":
6266
case "--help":
6367
case "-h":

adminforth/commands/createApp/templates/adminuser.ts.hbs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import AdminForth, { AdminForthDataTypes } from 'adminforth';
22
import type { AdminForthResourceInput, AdminForthResource, AdminUser } from 'adminforth';
33
import { randomUUID } from 'crypto';
44

5-
async function canModifyUsers({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
5+
async function allowedForSuperAdmin({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
66
return adminUser.dbUser.role === 'superadmin';
77
}
88

@@ -14,8 +14,8 @@ export default {
1414
recordLabel: (r) => `👤 ${r.email}`,
1515
options: {
1616
allowedActions: {
17-
edit: canModifyUsers,
18-
delete: canModifyUsers,
17+
edit: allowedForSuperAdmin,
18+
delete: allowedForSuperAdmin,
1919
},
2020
},
2121
columns: [
@@ -93,8 +93,11 @@ export default {
9393
}
9494
},
9595
edit: {
96-
beforeSave: async ({ updates, adminUser, resource }: { updates: any, adminUser: AdminUser, resource: AdminForthResource }) => {
96+
beforeSave: async ({ oldRecord, updates, adminUser, resource }: { oldRecord: any, updates: any, adminUser: AdminUser, resource: AdminForthResource }) => {
9797
console.log('Updating user', updates);
98+
if (oldRecord.id === adminUser.dbUser.id && updates.role) {
99+
return { ok: false, error: 'You cannot change your own role' };
100+
}
98101
if (updates.password) {
99102
updates.password_hash = await AdminForth.Utils.generatePasswordHash(updates.password);
100103
}

adminforth/commands/createCustomComponent/configUpdater.js

Lines changed: 135 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ async function findResourceFilePath(resourceId) {
2626
throw new Error(`Failed to read resources directory ${resourcesDir}: ${error.message}`);
2727
}
2828

29-
console.log(chalk.dim(`Found .ts files to scan: ${tsFiles.join(', ') || 'None'}`));
3029

3130
for (const file of tsFiles) {
3231
const filePath = path.resolve(resourcesDir, file);
@@ -94,6 +93,7 @@ export async function updateResourceConfig(resourceId, columnName, fieldType, co
9493
console.log(chalk.dim(`Attempting to update resource config: ${filePath}`));
9594

9695
let content;
96+
let injectionLine = null;
9797
try {
9898
content = await fs.readFile(filePath, 'utf-8');
9999
} catch (error) {
@@ -176,11 +176,16 @@ export async function updateResourceConfig(resourceId, columnName, fieldType, co
176176
const newComponentValue = b.stringLiteral(componentPathForConfig);
177177

178178
if (fieldTypeProperty) {
179+
injectionLine = fieldTypeProperty.loc?.start.line ?? null;
179180
fieldTypeProperty.value = newComponentValue;
180181
console.log(chalk.dim(`Updated '${fieldType}' component path in column '${columnName}'.`));
181182
} else {
182183
fieldTypeProperty = b.objectProperty(b.identifier(fieldType), newComponentValue);
183184
componentsObject.properties.push(fieldTypeProperty);
185+
fieldTypeProperty = componentsObject.properties.find(p =>
186+
n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === fieldType
187+
);
188+
injectionLine = fieldTypeProperty.loc?.start.line ?? null;
184189
console.log(chalk.dim(`Added '${fieldType}' component path to column '${columnName}'.`));
185190
}
186191

@@ -197,7 +202,12 @@ export async function updateResourceConfig(resourceId, columnName, fieldType, co
197202
const outputCode = recast.print(ast).code;
198203

199204
await fs.writeFile(filePath, outputCode, 'utf-8');
200-
console.log(chalk.dim(`Successfully updated resource configuration file (preserving formatting): ${filePath}`));
205+
console.log(
206+
chalk.green(
207+
`✅ Successfully updated CRUD injection in resource file: ${filePath}` +
208+
(injectionLine !== null ? `:${injectionLine}` : '')
209+
)
210+
);
201211

202212
} catch (error) {
203213
console.error(chalk.red(`❌ Error processing resource file: ${filePath}`));
@@ -211,78 +221,106 @@ export async function injectLoginComponent(indexFilePath, componentPath) {
211221
console.log(chalk.dim(`Reading file: ${indexFilePath}`));
212222
const content = await fs.readFile(indexFilePath, 'utf-8');
213223
const ast = recast.parse(content, {
214-
parser: typescriptParser,
224+
parser: typescriptParser,
215225
});
216-
226+
217227
let updated = false;
218-
228+
let injectionLine = null;
229+
219230
recast.visit(ast, {
220-
visitNewExpression(path) {
221-
if (
222-
n.Identifier.check(path.node.callee) &&
223-
path.node.callee.name === 'AdminForth' &&
224-
path.node.arguments.length > 0 &&
225-
n.ObjectExpression.check(path.node.arguments[0])
226-
) {
227-
const configObject = path.node.arguments[0];
228-
229-
let customizationProp = configObject.properties.find(
230-
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'customization'
231-
);
232-
233-
if (!customizationProp) {
234-
const customizationObj = b.objectExpression([]);
235-
customizationProp = b.objectProperty(b.identifier('customization'), customizationObj);
236-
configObject.properties.push(customizationProp);
237-
console.log(chalk.dim(`Added missing 'customization' property.`));
238-
}
239-
240-
const customizationValue = customizationProp.value;
241-
if (!n.ObjectExpression.check(customizationValue)) return false;
242-
243-
let loginPageInjections = customizationValue.properties.find(
244-
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'loginPageInjections'
245-
);
246-
247-
if (!loginPageInjections) {
248-
const injectionsObj = b.objectExpression([]);
249-
loginPageInjections = b.objectProperty(b.identifier('loginPageInjections'), injectionsObj);
250-
customizationValue.properties.push(loginPageInjections);
251-
console.log(chalk.dim(`Added missing 'loginPageInjections'.`));
252-
}
253-
254-
const injectionsValue = loginPageInjections.value;
255-
if (!n.ObjectExpression.check(injectionsValue)) return false;
256-
257-
let underInputsProp = injectionsValue.properties.find(
258-
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'underInputs'
259-
);
260-
261-
if (underInputsProp) {
262-
underInputsProp.value = b.stringLiteral(componentPath);
263-
console.log(chalk.dim(`Updated 'underInputs' to ${componentPath}`));
264-
} else {
265-
injectionsValue.properties.push(
266-
b.objectProperty(b.identifier('underInputs'), b.stringLiteral(componentPath))
267-
);
268-
console.log(chalk.dim(`Added 'underInputs': ${componentPath}`));
269-
}
270-
271-
updated = true;
272-
this.abort();
231+
visitNewExpression(path) {
232+
if (
233+
n.Identifier.check(path.node.callee) &&
234+
path.node.callee.name === 'AdminForth' &&
235+
path.node.arguments.length > 0 &&
236+
n.ObjectExpression.check(path.node.arguments[0])
237+
) {
238+
const configObject = path.node.arguments[0];
239+
240+
const getOrCreateProp = (obj, name) => {
241+
let prop = obj.properties.find(
242+
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === name
243+
);
244+
if (!prop) {
245+
const newObj = b.objectExpression([]);
246+
prop = b.objectProperty(b.identifier(name), newObj);
247+
obj.properties.push(prop);
248+
console.log(chalk.dim(`Added missing '${name}' property.`));
273249
}
274-
return false;
250+
return prop.value;
251+
};
252+
253+
const customization = getOrCreateProp(configObject, 'customization');
254+
if (!n.ObjectExpression.check(customization)) return false;
255+
256+
const loginPageInjections = getOrCreateProp(customization, 'loginPageInjections');
257+
if (!n.ObjectExpression.check(loginPageInjections)) return false;
258+
259+
let underInputsProp = loginPageInjections.properties.find(
260+
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'underInputs'
261+
);
262+
263+
if (underInputsProp) {
264+
const currentVal = underInputsProp.value;
265+
injectionLine = underInputsProp.loc?.start.line ?? null;
266+
if (n.StringLiteral.check(currentVal)) {
267+
if (currentVal.value !== componentPath) {
268+
underInputsProp.value = b.arrayExpression([
269+
b.stringLiteral(currentVal.value),
270+
b.stringLiteral(componentPath),
271+
]);
272+
console.log(chalk.dim(`Converted 'underInputs' to array with existing + new path.`));
273+
} else {
274+
console.log(chalk.dim(`Component path already present as string. Skipping.`));
275+
}
276+
} else if (n.ArrayExpression.check(currentVal)) {
277+
const exists = currentVal.elements.some(
278+
el => n.StringLiteral.check(el) && el.value === componentPath
279+
);
280+
if (!exists) {
281+
currentVal.elements.push(b.stringLiteral(componentPath));
282+
console.log(chalk.dim(`Appended new component path to existing 'underInputs' array.`));
283+
} else {
284+
console.log(chalk.dim(`Component path already present in array. Skipping.`));
285+
}
286+
} else {
287+
console.warn(chalk.yellow(`⚠️ 'underInputs' is not a string or array. Skipping.`));
288+
return false;
289+
}
290+
} else {
291+
const newProperty = b.objectProperty(
292+
b.identifier('underInputs'),
293+
b.stringLiteral(componentPath)
294+
);
295+
296+
if (newProperty.loc) {
297+
console.log(chalk.dim(`Adding 'underInputs' at line: ${newProperty.loc.start.line}`));
298+
}
299+
300+
loginPageInjections.properties.push(newProperty);
301+
console.log(chalk.dim(`Added 'underInputs': ${componentPath}`));
302+
}
303+
304+
updated = true;
305+
this.abort();
275306
}
307+
return false;
308+
}
276309
});
277-
310+
278311
if (!updated) {
279-
throw new Error(`Could not find AdminForth configuration in file: ${indexFilePath}`);
312+
throw new Error(`Could not find AdminForth configuration in file: ${indexFilePath}`);
280313
}
281-
314+
282315
const outputCode = recast.print(ast).code;
283316
await fs.writeFile(indexFilePath, outputCode, 'utf-8');
284-
console.log(chalk.green(`✅ Successfully updated login injection in: ${indexFilePath}`));
285-
}
317+
console.log(
318+
chalk.green(
319+
`✅ Successfully updated CRUD injection in resource file: ${indexFilePath}` +
320+
(injectionLine !== null ? `:${injectionLine}` : '')
321+
)
322+
);
323+
}
286324

287325

288326
export async function injectGlobalComponent(indexFilePath, injectionType, componentPath) {
@@ -293,7 +331,7 @@ export async function injectGlobalComponent(indexFilePath, injectionType, compon
293331
});
294332

295333
let updated = false;
296-
334+
let injectionLine = null;
297335
console.log(JSON.stringify(injectionType));
298336
recast.visit(ast, {
299337
visitNewExpression(path) {
@@ -315,7 +353,7 @@ export async function injectGlobalComponent(indexFilePath, injectionType, compon
315353
configObject.properties.push(customizationProp);
316354
console.log(chalk.dim(`Added missing 'customization' property.`));
317355
}
318-
356+
319357
const customizationValue = customizationProp.value;
320358
if (!n.ObjectExpression.check(customizationValue)) return false;
321359

@@ -338,7 +376,7 @@ export async function injectGlobalComponent(indexFilePath, injectionType, compon
338376
);
339377
if (injectionProp) {
340378
const currentValue = injectionProp.value;
341-
379+
injectionLine = injectionProp.loc?.start.line ?? null;
342380
if (n.ArrayExpression.check(currentValue)) {
343381
currentValue.elements.push(b.stringLiteral(componentPath));
344382
console.log(chalk.dim(`Added '${componentPath}' to existing array in '${injectionType}'`));
@@ -374,14 +412,20 @@ export async function injectGlobalComponent(indexFilePath, injectionType, compon
374412

375413
const outputCode = recast.print(ast).code;
376414
await fs.writeFile(indexFilePath, outputCode, 'utf-8');
377-
console.log(chalk.green(`✅ Successfully updated global injection '${injectionType}' in: ${indexFilePath}`));
415+
console.log(
416+
chalk.green(
417+
`✅ Successfully updated CRUD injection in resource file: ${indexFilePath}` +
418+
(injectionLine !== null ? `:${injectionLine}` : '')
419+
)
420+
);
378421
}
379422

380423
export async function updateCrudInjectionConfig(resourceId, crudType, injectionPosition, componentPathForConfig, isThin) {
381424
const filePath = await findResourceFilePath(resourceId);
382425
console.log(chalk.dim(`Attempting to update resource CRUD injection: ${filePath}`));
383426

384427
let content;
428+
let injectionLine = null;
385429
try {
386430
content = await fs.readFile(filePath, 'utf-8');
387431
} catch (error) {
@@ -439,7 +483,7 @@ export async function updateCrudInjectionConfig(resourceId, crudType, injectionP
439483
);
440484
pageInjections.properties.push(crudProp);
441485
}
442-
486+
injectionLine = crudProp.loc?.start.line ?? null;
443487
const crudValue = crudProp.value;
444488
if (!n.ObjectExpression.check(crudValue)) return false;
445489

@@ -458,11 +502,23 @@ export async function updateCrudInjectionConfig(resourceId, crudType, injectionP
458502
]);
459503

460504
if (injectionProp) {
461-
injectionProp.value = newInjectionObject;
462-
console.log(chalk.dim(`Updated '${injectionPosition}' injection for '${crudType}'.`));
505+
if (n.ArrayExpression.check(injectionProp.value)) {
506+
injectionProp.value.elements.push(newInjectionObject);
507+
console.log(chalk.dim(`Appended new injection to array at '${injectionPosition}' for '${crudType}'.`));
508+
}
509+
else if (n.ObjectExpression.check(injectionProp.value)) {
510+
injectionProp.value = b.arrayExpression([injectionProp.value, newInjectionObject]);
511+
console.log(chalk.dim(`Converted to array and added new injection at '${injectionPosition}' for '${crudType}'.`));
512+
}
513+
else {
514+
injectionProp.value = b.arrayExpression([newInjectionObject]);
515+
console.log(chalk.yellow(`⚠️ Replaced invalid injection at '${injectionPosition}' with array.`));
516+
}
463517
} else {
464-
crudValue.properties.push(b.objectProperty(b.identifier(injectionPosition), newInjectionObject));
465-
console.log(chalk.dim(`Added '${injectionPosition}' injection for '${crudType}'.`));
518+
crudValue.properties.push(
519+
b.objectProperty(b.identifier(injectionPosition), b.arrayExpression([newInjectionObject]))
520+
);
521+
console.log(chalk.dim(`Added new array of injections at '${injectionPosition}' for '${crudType}'.`));
466522
}
467523

468524
updateApplied = true;
@@ -477,7 +533,12 @@ export async function updateCrudInjectionConfig(resourceId, crudType, injectionP
477533

478534
const outputCode = recast.print(ast).code;
479535
await fs.writeFile(filePath, outputCode, 'utf-8');
480-
console.log(chalk.dim(`✅ Successfully updated CRUD injection in resource file: ${filePath}`));
536+
console.log(
537+
chalk.green(
538+
`✅ Successfully updated CRUD injection in resource file: ${filePath}` +
539+
(injectionLine !== null ? `:${injectionLine}` : '')
540+
)
541+
);
481542

482543
} catch (error) {
483544
console.error(chalk.red(`❌ Error processing resource file: ${filePath}`));

adminforth/commands/createCustomComponent/main.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,16 +170,26 @@ async function handleCrudPageInjectionCreation(config, resources) {
170170
});
171171
if (injectionPosition === '__BACK__') return handleCrudPageInjectionCreation(config, resources);
172172

173+
const additionalName = await input({
174+
message: 'Enter additional name (optional, e.g., "CustomExport"):',
175+
validate: (value) => {
176+
if (!value) return true;
177+
return /^[A-Za-z0-9_-]+$/.test(value) || 'Only alphanumeric characters, hyphens or underscores are allowed.';
178+
},
179+
});
180+
173181
const isThin = await select({
174182
message: 'Will this component be thin enough to fit on the same page with list (so list will still shrink)?',
175183
choices: [
176184
{ name: 'Yes', value: true },
177185
{ name: 'No', value: false },
178186
],
179187
});
180-
188+
const formattedAdditionalName = additionalName
189+
? additionalName[0].toUpperCase() + additionalName.slice(1)
190+
: '';
181191
const safeResourceLabel = sanitizeLabel(selectedResource.label)
182-
const componentFileName = `${safeResourceLabel}${crudType.charAt(0).toUpperCase() + crudType.slice(1)}${injectionPosition.charAt(0).toUpperCase() + injectionPosition.slice(1)}.vue`;
192+
const componentFileName = `${safeResourceLabel}${crudType.charAt(0).toUpperCase() + crudType.slice(1)}${injectionPosition.charAt(0).toUpperCase() + injectionPosition.slice(1) + formattedAdditionalName}.vue`;
183193
const componentPathForConfig = `@@/${componentFileName}`;
184194

185195
try {

0 commit comments

Comments
 (0)