Skip to content

Next #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 30, 2025
Merged

Next #218

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
20ce34c
feat: update templates to replace reason placeholders with static tex…
NoOne7135 Apr 30, 2025
816d02d
feat: enhance CRUD injection logging and improve component file namin…
NoOne7135 May 6, 2025
92a7fbf
fix: update import statement for usersResource to include file extension
NoOne7135 May 12, 2025
f34741c
feat: update templates to use TypeScript for props definition and enh…
NoOne7135 May 12, 2025
c08ae0b
Merge branch 'next' of https://github.com/devforth/adminforth into cr…
NoOne7135 May 20, 2025
b3f13f7
fix: prevent users from changing their own role during updates
NoOne7135 May 20, 2025
4a57bc9
fix: rename canModifyUsers to allowedForSuperAdmin for clarity and re…
NoOne7135 May 20, 2025
55a870b
fix: refresh menu badges after closing CTA for improved user experience
NoOne7135 May 21, 2025
cd0a8a9
Merge pull request #213 from devforth/create-cli
ivictbor May 21, 2025
06e7908
feat: implement filtersTools for managing query filters in hooks
NoOne7135 May 22, 2025
1a615ce
fix: streamline filter removal logic in filtersTools module
NoOne7135 May 22, 2025
8eacc8f
feat: update Filters methods to accept variable arguments for improve…
NoOne7135 May 22, 2025
8c768a4
Merge pull request #215 from devforth/enhance-filter-methods
ivictbor May 23, 2025
4de0654
Merge pull request #214 from devforth/simplify-ui-for-working-with-fi…
ivictbor May 23, 2025
a915feb
fix: handle virtual columns in saveRecord function to prevent empty v…
NoOne7135 May 27, 2025
3ca0139
feat: add mode parameter to support create/edit-specific required rules
NoOne7135 May 28, 2025
db95c72
feat: fetch DB tables/columns and generate resource import command
NoOne7135 May 28, 2025
7d1fa97
fix: enhance required field validation to check for empty record values
NoOne7135 May 29, 2025
0bb5ae0
Merge pull request #216 from devforth/fix-ignore-empty-virtual-fields
ivictbor May 29, 2025
fe19ef2
Merge pull request #217 from devforth/add-command-to-import-resource
ivictbor May 29, 2025
0d4acac
fix: exit server when field discovery fails in production
SerVitasik May 29, 2025
d9d4765
fix: add debug error on hashfile does not exist for heavy_debug
ivictbor May 30, 2025
7d771c8
Merge branch 'next' of github.com:devforth/adminforth into next
ivictbor May 30, 2025
b79bee4
feat: add abstract methods for table and column retrieval in base con…
NoOne7135 May 30, 2025
261b45e
fix: remove debug logging of adminForth config in createResource
NoOne7135 May 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions adminforth/commands/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import createApp from "./createApp/main.js";
import generateModels from "./generateModels.js";
import createPlugin from "./createPlugin/main.js";
import createComponent from "./createCustomComponent/main.js";
import createResource from "./createResource/main.js";
import chalk from "chalk";
import path from "path";
import fs from "fs";
Expand Down Expand Up @@ -58,6 +59,9 @@ switch (command) {
case "component":
createComponent(args);
break;
case "resource":
createResource(args);
break;
case "help":
case "--help":
case "-h":
Expand Down
11 changes: 7 additions & 4 deletions adminforth/commands/createApp/templates/adminuser.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import AdminForth, { AdminForthDataTypes } from 'adminforth';
import type { AdminForthResourceInput, AdminForthResource, AdminUser } from 'adminforth';
import { randomUUID } from 'crypto';

async function canModifyUsers({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
async function allowedForSuperAdmin({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
return adminUser.dbUser.role === 'superadmin';
}

Expand All @@ -14,8 +14,8 @@ export default {
recordLabel: (r) => `👤 ${r.email}`,
options: {
allowedActions: {
edit: canModifyUsers,
delete: canModifyUsers,
edit: allowedForSuperAdmin,
delete: allowedForSuperAdmin,
},
},
columns: [
Expand Down Expand Up @@ -93,8 +93,11 @@ export default {
}
},
edit: {
beforeSave: async ({ updates, adminUser, resource }: { updates: any, adminUser: AdminUser, resource: AdminForthResource }) => {
beforeSave: async ({ oldRecord, updates, adminUser, resource }: { oldRecord: any, updates: any, adminUser: AdminUser, resource: AdminForthResource }) => {
console.log('Updating user', updates);
if (oldRecord.id === adminUser.dbUser.id && updates.role) {
return { ok: false, error: 'You cannot change your own role' };
}
if (updates.password) {
updates.password_hash = await AdminForth.Utils.generatePasswordHash(updates.password);
}
Expand Down
209 changes: 135 additions & 74 deletions adminforth/commands/createCustomComponent/configUpdater.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ async function findResourceFilePath(resourceId) {
throw new Error(`Failed to read resources directory ${resourcesDir}: ${error.message}`);
}

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

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

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

if (fieldTypeProperty) {
injectionLine = fieldTypeProperty.loc?.start.line ?? null;
fieldTypeProperty.value = newComponentValue;
console.log(chalk.dim(`Updated '${fieldType}' component path in column '${columnName}'.`));
} else {
fieldTypeProperty = b.objectProperty(b.identifier(fieldType), newComponentValue);
componentsObject.properties.push(fieldTypeProperty);
fieldTypeProperty = componentsObject.properties.find(p =>
n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === fieldType
);
injectionLine = fieldTypeProperty.loc?.start.line ?? null;
console.log(chalk.dim(`Added '${fieldType}' component path to column '${columnName}'.`));
}

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

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

} catch (error) {
console.error(chalk.red(`❌ Error processing resource file: ${filePath}`));
Expand All @@ -211,78 +221,106 @@ export async function injectLoginComponent(indexFilePath, componentPath) {
console.log(chalk.dim(`Reading file: ${indexFilePath}`));
const content = await fs.readFile(indexFilePath, 'utf-8');
const ast = recast.parse(content, {
parser: typescriptParser,
parser: typescriptParser,
});

let updated = false;

let injectionLine = null;

recast.visit(ast, {
visitNewExpression(path) {
if (
n.Identifier.check(path.node.callee) &&
path.node.callee.name === 'AdminForth' &&
path.node.arguments.length > 0 &&
n.ObjectExpression.check(path.node.arguments[0])
) {
const configObject = path.node.arguments[0];

let customizationProp = configObject.properties.find(
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'customization'
);

if (!customizationProp) {
const customizationObj = b.objectExpression([]);
customizationProp = b.objectProperty(b.identifier('customization'), customizationObj);
configObject.properties.push(customizationProp);
console.log(chalk.dim(`Added missing 'customization' property.`));
}

const customizationValue = customizationProp.value;
if (!n.ObjectExpression.check(customizationValue)) return false;

let loginPageInjections = customizationValue.properties.find(
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'loginPageInjections'
);

if (!loginPageInjections) {
const injectionsObj = b.objectExpression([]);
loginPageInjections = b.objectProperty(b.identifier('loginPageInjections'), injectionsObj);
customizationValue.properties.push(loginPageInjections);
console.log(chalk.dim(`Added missing 'loginPageInjections'.`));
}

const injectionsValue = loginPageInjections.value;
if (!n.ObjectExpression.check(injectionsValue)) return false;

let underInputsProp = injectionsValue.properties.find(
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'underInputs'
);

if (underInputsProp) {
underInputsProp.value = b.stringLiteral(componentPath);
console.log(chalk.dim(`Updated 'underInputs' to ${componentPath}`));
} else {
injectionsValue.properties.push(
b.objectProperty(b.identifier('underInputs'), b.stringLiteral(componentPath))
);
console.log(chalk.dim(`Added 'underInputs': ${componentPath}`));
}

updated = true;
this.abort();
visitNewExpression(path) {
if (
n.Identifier.check(path.node.callee) &&
path.node.callee.name === 'AdminForth' &&
path.node.arguments.length > 0 &&
n.ObjectExpression.check(path.node.arguments[0])
) {
const configObject = path.node.arguments[0];

const getOrCreateProp = (obj, name) => {
let prop = obj.properties.find(
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === name
);
if (!prop) {
const newObj = b.objectExpression([]);
prop = b.objectProperty(b.identifier(name), newObj);
obj.properties.push(prop);
console.log(chalk.dim(`Added missing '${name}' property.`));
}
return false;
return prop.value;
};

const customization = getOrCreateProp(configObject, 'customization');
if (!n.ObjectExpression.check(customization)) return false;

const loginPageInjections = getOrCreateProp(customization, 'loginPageInjections');
if (!n.ObjectExpression.check(loginPageInjections)) return false;

let underInputsProp = loginPageInjections.properties.find(
p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'underInputs'
);

if (underInputsProp) {
const currentVal = underInputsProp.value;
injectionLine = underInputsProp.loc?.start.line ?? null;
if (n.StringLiteral.check(currentVal)) {
if (currentVal.value !== componentPath) {
underInputsProp.value = b.arrayExpression([
b.stringLiteral(currentVal.value),
b.stringLiteral(componentPath),
]);
console.log(chalk.dim(`Converted 'underInputs' to array with existing + new path.`));
} else {
console.log(chalk.dim(`Component path already present as string. Skipping.`));
}
} else if (n.ArrayExpression.check(currentVal)) {
const exists = currentVal.elements.some(
el => n.StringLiteral.check(el) && el.value === componentPath
);
if (!exists) {
currentVal.elements.push(b.stringLiteral(componentPath));
console.log(chalk.dim(`Appended new component path to existing 'underInputs' array.`));
} else {
console.log(chalk.dim(`Component path already present in array. Skipping.`));
}
} else {
console.warn(chalk.yellow(`⚠️ 'underInputs' is not a string or array. Skipping.`));
return false;
}
} else {
const newProperty = b.objectProperty(
b.identifier('underInputs'),
b.stringLiteral(componentPath)
);

if (newProperty.loc) {
console.log(chalk.dim(`Adding 'underInputs' at line: ${newProperty.loc.start.line}`));
}

loginPageInjections.properties.push(newProperty);
console.log(chalk.dim(`Added 'underInputs': ${componentPath}`));
}

updated = true;
this.abort();
}
return false;
}
});

if (!updated) {
throw new Error(`Could not find AdminForth configuration in file: ${indexFilePath}`);
throw new Error(`Could not find AdminForth configuration in file: ${indexFilePath}`);
}

const outputCode = recast.print(ast).code;
await fs.writeFile(indexFilePath, outputCode, 'utf-8');
console.log(chalk.green(`✅ Successfully updated login injection in: ${indexFilePath}`));
}
console.log(
chalk.green(
`✅ Successfully updated CRUD injection in resource file: ${indexFilePath}` +
(injectionLine !== null ? `:${injectionLine}` : '')
)
);
}


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

let updated = false;

let injectionLine = null;
console.log(JSON.stringify(injectionType));
recast.visit(ast, {
visitNewExpression(path) {
Expand All @@ -315,7 +353,7 @@ export async function injectGlobalComponent(indexFilePath, injectionType, compon
configObject.properties.push(customizationProp);
console.log(chalk.dim(`Added missing 'customization' property.`));
}

const customizationValue = customizationProp.value;
if (!n.ObjectExpression.check(customizationValue)) return false;

Expand All @@ -338,7 +376,7 @@ export async function injectGlobalComponent(indexFilePath, injectionType, compon
);
if (injectionProp) {
const currentValue = injectionProp.value;

injectionLine = injectionProp.loc?.start.line ?? null;
if (n.ArrayExpression.check(currentValue)) {
currentValue.elements.push(b.stringLiteral(componentPath));
console.log(chalk.dim(`Added '${componentPath}' to existing array in '${injectionType}'`));
Expand Down Expand Up @@ -374,14 +412,20 @@ export async function injectGlobalComponent(indexFilePath, injectionType, compon

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

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

let content;
let injectionLine = null;
try {
content = await fs.readFile(filePath, 'utf-8');
} catch (error) {
Expand Down Expand Up @@ -439,7 +483,7 @@ export async function updateCrudInjectionConfig(resourceId, crudType, injectionP
);
pageInjections.properties.push(crudProp);
}

injectionLine = crudProp.loc?.start.line ?? null;
const crudValue = crudProp.value;
if (!n.ObjectExpression.check(crudValue)) return false;

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

if (injectionProp) {
injectionProp.value = newInjectionObject;
console.log(chalk.dim(`Updated '${injectionPosition}' injection for '${crudType}'.`));
if (n.ArrayExpression.check(injectionProp.value)) {
injectionProp.value.elements.push(newInjectionObject);
console.log(chalk.dim(`Appended new injection to array at '${injectionPosition}' for '${crudType}'.`));
}
else if (n.ObjectExpression.check(injectionProp.value)) {
injectionProp.value = b.arrayExpression([injectionProp.value, newInjectionObject]);
console.log(chalk.dim(`Converted to array and added new injection at '${injectionPosition}' for '${crudType}'.`));
}
else {
injectionProp.value = b.arrayExpression([newInjectionObject]);
console.log(chalk.yellow(`⚠️ Replaced invalid injection at '${injectionPosition}' with array.`));
}
} else {
crudValue.properties.push(b.objectProperty(b.identifier(injectionPosition), newInjectionObject));
console.log(chalk.dim(`Added '${injectionPosition}' injection for '${crudType}'.`));
crudValue.properties.push(
b.objectProperty(b.identifier(injectionPosition), b.arrayExpression([newInjectionObject]))
);
console.log(chalk.dim(`Added new array of injections at '${injectionPosition}' for '${crudType}'.`));
}

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

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

} catch (error) {
console.error(chalk.red(`❌ Error processing resource file: ${filePath}`));
Expand Down
14 changes: 12 additions & 2 deletions adminforth/commands/createCustomComponent/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,26 @@ async function handleCrudPageInjectionCreation(config, resources) {
});
if (injectionPosition === '__BACK__') return handleCrudPageInjectionCreation(config, resources);

const additionalName = await input({
message: 'Enter additional name (optional, e.g., "CustomExport"):',
validate: (value) => {
if (!value) return true;
return /^[A-Za-z0-9_-]+$/.test(value) || 'Only alphanumeric characters, hyphens or underscores are allowed.';
},
});

const isThin = await select({
message: 'Will this component be thin enough to fit on the same page with list (so list will still shrink)?',
choices: [
{ name: 'Yes', value: true },
{ name: 'No', value: false },
],
});

const formattedAdditionalName = additionalName
? additionalName[0].toUpperCase() + additionalName.slice(1)
: '';
const safeResourceLabel = sanitizeLabel(selectedResource.label)
const componentFileName = `${safeResourceLabel}${crudType.charAt(0).toUpperCase() + crudType.slice(1)}${injectionPosition.charAt(0).toUpperCase() + injectionPosition.slice(1)}.vue`;
const componentFileName = `${safeResourceLabel}${crudType.charAt(0).toUpperCase() + crudType.slice(1)}${injectionPosition.charAt(0).toUpperCase() + injectionPosition.slice(1) + formattedAdditionalName}.vue`;
const componentPathForConfig = `@@/${componentFileName}`;

try {
Expand Down
Loading