adminforth 1.21.0-next.11 → 1.21.0-next.12

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.
@@ -205,3 +205,282 @@ export async function updateResourceConfig(resourceId, columnName, fieldType, co
205
205
  throw new Error(`Failed to update resource file ${path.basename(filePath)}: ${error.message}`);
206
206
  }
207
207
  }
208
+
209
+
210
+ export async function injectLoginComponent(indexFilePath, componentPath) {
211
+ console.log(chalk.dim(`Reading file: ${indexFilePath}`));
212
+ const content = await fs.readFile(indexFilePath, 'utf-8');
213
+ const ast = recast.parse(content, {
214
+ parser: typescriptParser,
215
+ });
216
+
217
+ let updated = false;
218
+
219
+ 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();
273
+ }
274
+ return false;
275
+ }
276
+ });
277
+
278
+ if (!updated) {
279
+ throw new Error(`Could not find AdminForth configuration in file: ${indexFilePath}`);
280
+ }
281
+
282
+ const outputCode = recast.print(ast).code;
283
+ await fs.writeFile(indexFilePath, outputCode, 'utf-8');
284
+ console.log(chalk.green(`✅ Successfully updated login injection in: ${indexFilePath}`));
285
+ }
286
+
287
+
288
+ export async function injectGlobalComponent(indexFilePath, injectionType, componentPath) {
289
+ console.log(chalk.dim(`Reading file: ${indexFilePath}`));
290
+ const content = await fs.readFile(indexFilePath, 'utf-8');
291
+ const ast = recast.parse(content, {
292
+ parser: typescriptParser,
293
+ });
294
+
295
+ let updated = false;
296
+
297
+ console.log(JSON.stringify(injectionType));
298
+ recast.visit(ast, {
299
+ visitNewExpression(path) {
300
+ if (
301
+ n.Identifier.check(path.node.callee) &&
302
+ path.node.callee.name === 'AdminForth' &&
303
+ path.node.arguments.length > 0 &&
304
+ n.ObjectExpression.check(path.node.arguments[0])
305
+ ) {
306
+ const configObject = path.node.arguments[0];
307
+
308
+ let customizationProp = configObject.properties.find(
309
+ p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'customization'
310
+ );
311
+
312
+ if (!customizationProp) {
313
+ const customizationObj = b.objectExpression([]);
314
+ customizationProp = b.objectProperty(b.identifier('customization'), customizationObj);
315
+ configObject.properties.push(customizationProp);
316
+ console.log(chalk.dim(`Added missing 'customization' property.`));
317
+ }
318
+
319
+ const customizationValue = customizationProp.value;
320
+ if (!n.ObjectExpression.check(customizationValue)) return false;
321
+
322
+ let globalInjections = customizationValue.properties.find(
323
+ p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'globalInjections'
324
+ );
325
+
326
+ if (!globalInjections) {
327
+ const injectionsObj = b.objectExpression([]);
328
+ globalInjections = b.objectProperty(b.identifier('globalInjections'), injectionsObj);
329
+ customizationValue.properties.push(globalInjections);
330
+ console.log(chalk.dim(`Added missing 'globalInjections'.`));
331
+ }
332
+
333
+ const injectionsValue = globalInjections.value;
334
+ if (!n.ObjectExpression.check(injectionsValue)) return false;
335
+ console.log(JSON.stringify(injectionType));
336
+ let injectionProp = injectionsValue.properties.find(
337
+ p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === injectionType
338
+ );
339
+ if (injectionProp) {
340
+ const currentValue = injectionProp.value;
341
+
342
+ if (n.ArrayExpression.check(currentValue)) {
343
+ currentValue.elements.push(b.stringLiteral(componentPath));
344
+ console.log(chalk.dim(`Added '${componentPath}' to existing array in '${injectionType}'`));
345
+ } else if (n.StringLiteral.check(currentValue)) {
346
+ injectionProp.value = b.arrayExpression([
347
+ b.stringLiteral(currentValue.value),
348
+ b.stringLiteral(componentPath)
349
+ ]);
350
+ console.log(chalk.dim(`Converted '${injectionType}' from string to array and added '${componentPath}'`));
351
+ } else {
352
+ throw new Error(`Unsupported value type for '${injectionType}'. Must be string or array.`);
353
+ }
354
+ } else {
355
+ injectionsValue.properties.push(
356
+ b.objectProperty(
357
+ b.identifier(injectionType),
358
+ b.arrayExpression([b.stringLiteral(componentPath)])
359
+ )
360
+ );
361
+ console.log(chalk.dim(`Added new array for '${injectionType}' with '${componentPath}'`));
362
+ }
363
+
364
+ updated = true;
365
+ this.abort();
366
+ }
367
+ return false;
368
+ }
369
+ });
370
+
371
+ if (!updated) {
372
+ throw new Error(`Could not find AdminForth configuration in file: ${indexFilePath}`);
373
+ }
374
+
375
+ const outputCode = recast.print(ast).code;
376
+ await fs.writeFile(indexFilePath, outputCode, 'utf-8');
377
+ console.log(chalk.green(`✅ Successfully updated global injection '${injectionType}' in: ${indexFilePath}`));
378
+ }
379
+
380
+ export async function updateCrudInjectionConfig(resourceId, crudType, injectionPosition, componentPathForConfig, isThin) {
381
+ const filePath = await findResourceFilePath(resourceId);
382
+ console.log(chalk.dim(`Attempting to update resource CRUD injection: ${filePath}`));
383
+
384
+ let content;
385
+ try {
386
+ content = await fs.readFile(filePath, 'utf-8');
387
+ } catch (error) {
388
+ console.error(chalk.red(`❌ Error reading resource file: ${filePath}`));
389
+ throw new Error(`Could not read resource file ${filePath}.`);
390
+ }
391
+
392
+ try {
393
+ const ast = recast.parse(content, {
394
+ parser: typescriptParser
395
+ });
396
+
397
+ let updateApplied = false;
398
+
399
+ recast.visit(ast, {
400
+ visitExportDefaultDeclaration(path) {
401
+ const declaration = path.node.declaration;
402
+ let objectExpressionNode = null;
403
+
404
+ if (n.TSAsExpression.check(declaration) && n.ObjectExpression.check(declaration.expression)) {
405
+ objectExpressionNode = declaration.expression;
406
+ } else if (n.ObjectExpression.check(declaration)) {
407
+ objectExpressionNode = declaration;
408
+ }
409
+
410
+ if (!objectExpressionNode) {
411
+ console.warn(chalk.yellow(`Warning: Default export in ${filePath} is not an ObjectExpression. Skipping update.`));
412
+ return false;
413
+ }
414
+
415
+ const getOrCreateObjectProp = (obj, propName) => {
416
+ let prop = obj.properties.find(p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === propName);
417
+ if (!prop) {
418
+ const newObject = b.objectExpression([]);
419
+ prop = b.objectProperty(b.identifier(propName), newObject);
420
+ obj.properties.push(prop);
421
+ }
422
+ return prop.value;
423
+ };
424
+
425
+ const options = getOrCreateObjectProp(objectExpressionNode, 'options');
426
+ if (!n.ObjectExpression.check(options)) return false;
427
+
428
+ const pageInjections = getOrCreateObjectProp(options, 'pageInjections');
429
+ if (!n.ObjectExpression.check(pageInjections)) return false;
430
+
431
+ let crudProp = pageInjections.properties.find(p =>
432
+ n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === crudType
433
+ );
434
+
435
+ if (!crudProp) {
436
+ crudProp = b.objectProperty(
437
+ b.identifier(crudType),
438
+ b.objectExpression([])
439
+ );
440
+ pageInjections.properties.push(crudProp);
441
+ }
442
+
443
+ const crudValue = crudProp.value;
444
+ if (!n.ObjectExpression.check(crudValue)) return false;
445
+
446
+ let injectionProp = crudValue.properties.find(p =>
447
+ n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === injectionPosition
448
+ );
449
+
450
+ const newInjectionObject = b.objectExpression([
451
+ b.objectProperty(b.identifier('file'), b.stringLiteral(componentPathForConfig)),
452
+ b.objectProperty(
453
+ b.identifier('meta'),
454
+ b.objectExpression([
455
+ b.objectProperty(b.identifier('thinEnoughToShrinkTable'), b.booleanLiteral(!!isThin)),
456
+ ])
457
+ ),
458
+ ]);
459
+
460
+ if (injectionProp) {
461
+ injectionProp.value = newInjectionObject;
462
+ console.log(chalk.dim(`Updated '${injectionPosition}' injection for '${crudType}'.`));
463
+ } else {
464
+ crudValue.properties.push(b.objectProperty(b.identifier(injectionPosition), newInjectionObject));
465
+ console.log(chalk.dim(`Added '${injectionPosition}' injection for '${crudType}'.`));
466
+ }
467
+
468
+ updateApplied = true;
469
+ this.abort();
470
+ return false;
471
+ }
472
+ });
473
+
474
+ if (!updateApplied) {
475
+ throw new Error(`Could not inject CRUD component in resource ${resourceId}.`);
476
+ }
477
+
478
+ const outputCode = recast.print(ast).code;
479
+ await fs.writeFile(filePath, outputCode, 'utf-8');
480
+ console.log(chalk.dim(`✅ Successfully updated CRUD injection in resource file: ${filePath}`));
481
+
482
+ } catch (error) {
483
+ console.error(chalk.red(`❌ Error processing resource file: ${filePath}`));
484
+ throw new Error(`Failed to inject CRUD component in ${path.basename(filePath)}: ${error.message}`);
485
+ }
486
+ }
@@ -1,4 +1,5 @@
1
1
  import fs from 'fs/promises';
2
+ import fsSync from 'fs';
2
3
  import path from 'path';
3
4
  import chalk from 'chalk';
4
5
  import Handlebars from 'handlebars';
@@ -16,22 +17,30 @@ async function renderHBSTemplate(templatePath, data) {
16
17
  }
17
18
 
18
19
  async function generateVueContent(fieldType, { resource, column }) {
19
- const componentName = `${resource.label}${column.label}${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)}`;
20
- const columnName = column.name;
20
+ const hasColumn = !!column;
21
+ const componentName = hasColumn
22
+ ? `${resource.label}${column.label}${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)}`
23
+ : `${resource.label}${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)}`;
24
+
21
25
  const resourceId = resource.resourceId;
22
26
 
23
27
  const __filename = fileURLToPath(import.meta.url);
24
28
  const __dirname = path.dirname(__filename);
25
- const templatePath = path.join(__dirname, 'templates', 'customFields', `${fieldType}.vue.hbs`);
29
+
30
+ const templatePath = hasColumn
31
+ ? path.join(__dirname, 'templates', 'customFields', `${fieldType}.vue.hbs`)
32
+ : path.join(__dirname, 'templates', 'customCrud', `${fieldType}.vue.hbs`);
26
33
 
27
34
  console.log(chalk.dim(`Using template: ${templatePath}`));
28
35
 
29
36
  const context = {
30
37
  componentName,
31
- columnName,
32
38
  resourceId,
33
39
  resource,
34
- column
40
+ ...(hasColumn && {
41
+ column,
42
+ columnName: column.name,
43
+ }),
35
44
  };
36
45
 
37
46
  try {
@@ -43,6 +52,7 @@ async function generateVueContent(fieldType, { resource, column }) {
43
52
  }
44
53
  }
45
54
 
55
+
46
56
  export async function generateComponentFile(componentFileName, fieldType, context, config) {
47
57
 
48
58
  const customDirRelative = 'custom';
@@ -50,7 +60,10 @@ export async function generateComponentFile(componentFileName, fieldType, contex
50
60
  const projectRoot = process.cwd();
51
61
  const customDirPath = path.resolve(projectRoot, customDirRelative);
52
62
  const absoluteComponentPath = path.resolve(customDirPath, componentFileName);
53
-
63
+ if (fsSync.existsSync(absoluteComponentPath)) {
64
+ console.log(chalk.yellow(`⚠️ Component file already exists: ${absoluteComponentPath}`));
65
+ return {"alreadyExists": true, "path": absoluteComponentPath}
66
+ }
54
67
  try {
55
68
  await fs.mkdir(customDirPath, { recursive: true });
56
69
  console.log(chalk.dim(`Ensured custom directory exists: ${customDirPath}`));
@@ -60,7 +73,7 @@ export async function generateComponentFile(componentFileName, fieldType, contex
60
73
  await fs.writeFile(absoluteComponentPath, fileContent, 'utf-8');
61
74
  console.log(chalk.green(`✅ Generated component file: ${absoluteComponentPath}`));
62
75
 
63
- return absoluteComponentPath;
76
+ return {"alreadyExists": false, "path": absoluteComponentPath}
64
77
 
65
78
  } catch (error) {
66
79
  console.error(chalk.red(`❌ Error creating component file at ${absoluteComponentPath}:`));
@@ -70,3 +83,66 @@ export async function generateComponentFile(componentFileName, fieldType, contex
70
83
  throw error;
71
84
  }
72
85
  }
86
+
87
+ export async function generateCrudInjectionComponent(componentFileName, crudType, context, config) {
88
+ const customDirRelative = 'custom';
89
+ const projectRoot = process.cwd();
90
+ const customDirPath = path.resolve(projectRoot, customDirRelative);
91
+ const absoluteComponentPath = path.resolve(customDirPath, componentFileName);
92
+
93
+ if (fsSync.existsSync(absoluteComponentPath)) {
94
+ console.log(chalk.yellow(`⚠️ Component file already exists: ${absoluteComponentPath}`));
95
+ return { alreadyExists: true, path: absoluteComponentPath };
96
+ }
97
+
98
+ try {
99
+ await fs.mkdir(customDirPath, { recursive: true });
100
+ console.log(chalk.dim(`Ensured custom directory exists: ${customDirPath}`));
101
+
102
+ const fileContent = await generateVueContent(crudType, context);
103
+
104
+ await fs.writeFile(absoluteComponentPath, fileContent, 'utf-8');
105
+ console.log(chalk.green(`✅ Generated component file: ${absoluteComponentPath}`));
106
+
107
+ return { alreadyExists: false, path: absoluteComponentPath };
108
+ } catch (error) {
109
+ console.error(chalk.red(`❌ Error creating component file at ${absoluteComponentPath}:`));
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ export async function generateLoginOrGlobalComponentFile(componentFileName, injectionType, context) {
115
+ const customDirRelative = 'custom';
116
+ const projectRoot = process.cwd();
117
+ const customDirPath = path.resolve(projectRoot, customDirRelative);
118
+ const absoluteComponentPath = path.resolve(customDirPath, componentFileName);
119
+
120
+ if (fsSync.existsSync(absoluteComponentPath)) {
121
+ console.log(chalk.yellow(`⚠️ Component file already exists: ${absoluteComponentPath}`));
122
+ return { alreadyExists: true, path: absoluteComponentPath };
123
+ }
124
+
125
+ try {
126
+ await fs.mkdir(customDirPath, { recursive: true });
127
+ console.log(chalk.dim(`Ensured custom directory exists: ${customDirPath}`));
128
+
129
+ const __filename = fileURLToPath(import.meta.url);
130
+ const __dirname = path.dirname(__filename);
131
+ let templatePath;
132
+ if (injectionType === 'afterLogin') {
133
+ templatePath = path.join(__dirname, 'templates', 'login', `${injectionType}.vue.hbs`);
134
+ } else {
135
+ templatePath = path.join(__dirname, 'templates', 'global', `${injectionType}.vue.hbs`);
136
+ }
137
+
138
+ const fileContent = await renderHBSTemplate(templatePath, context);
139
+
140
+ await fs.writeFile(absoluteComponentPath, fileContent, 'utf-8');
141
+ console.log(chalk.green(`✅ Generated login injection component: ${absoluteComponentPath}`));
142
+
143
+ return { alreadyExists: false, path: absoluteComponentPath };
144
+ } catch (error) {
145
+ console.error(chalk.red(`❌ Error creating login component at ${absoluteComponentPath}`));
146
+ throw error;
147
+ }
148
+ }
@@ -1,10 +1,17 @@
1
- import { select, confirm, Separator } from '@inquirer/prompts';
2
- import chalk from 'chalk';
3
- import path from 'path'; // Import path
1
+ import { select, Separator, search, input } from '@inquirer/prompts';
2
+ import chalk from 'chalk';// Import path
3
+ import path from 'path';
4
4
  import { loadAdminForthConfig } from './configLoader.js'; // Helper to load config
5
- import { generateComponentFile } from './fileGenerator.js'; // Helper to create the .vue file
6
- import { updateResourceConfig } from './configUpdater.js'; // Helper to modify resource .ts file
7
- // import { openFileInIde } from './ideHelper.js'; // Helper to open file
5
+ import { generateComponentFile, generateLoginOrGlobalComponentFile, generateCrudInjectionComponent } from './fileGenerator.js'; // Helper to create the .vue file
6
+ import { updateResourceConfig, injectLoginComponent, injectGlobalComponent, updateCrudInjectionConfig } from './configUpdater.js'; // Helper to modify resource .ts file
7
+
8
+ function sanitizeLabel(input){
9
+ return input
10
+ .replace(/[^a-zA-Z0-9\s]/g, '')
11
+ .split(' ')
12
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
13
+ .join('');
14
+ }
8
15
 
9
16
  export default async function createComponent(args) {
10
17
  console.log('This command will help you to generate boilerplate for component.\n');
@@ -64,13 +71,23 @@ async function handleFieldComponentCreation(config, resources) {
64
71
  const selectedResource = resources.find(r => r.resourceId === resourceId);
65
72
  console.log(chalk.grey(`Selected ❯ 🔤 Custom fields ❯ ${fieldType} ❯ ${selectedResource.label}`));
66
73
 
67
- const columnName = await select({
68
- message: 'Select column for which you want to create component:',
69
- choices: [
70
- ...selectedResource.columns.map(c => ({ name: `${c.label} ${chalk.grey(`${c.name}`)}`, value: c.name })),
71
- new Separator(),
72
- { name: '🔙 BACK', value: '__BACK__' },
73
- ]
74
+ const columnName = await search({
75
+ message: 'Select column for which you want to create component:',
76
+ source: async (input) => {
77
+ const searchTerm = input ? input.toLowerCase() : '';
78
+
79
+ const filteredColumns = selectedResource.columns.filter(c => {
80
+ const label = c.label || '';
81
+ const name = c.name || '';
82
+ return label.toLowerCase().includes(searchTerm) || name.toLowerCase().includes(searchTerm);
83
+ });
84
+
85
+ return [
86
+ ...filteredColumns.map(c => ({ name: `${c.label} ${chalk.grey(`${c.name}`)}`, value: c.name })),
87
+ new Separator(),
88
+ { name: '🔙 BACK', value: '__BACK__' },
89
+ ];
90
+ },
74
91
  });
75
92
  if (columnName === '__BACK__') return handleFieldComponentCreation(config, resources); // Pass config back
76
93
 
@@ -79,60 +96,226 @@ async function handleFieldComponentCreation(config, resources) {
79
96
  console.log(chalk.dim(`One-line alternative: |adminforth component fields.${fieldType}.${resourceId}.${columnName}|`));
80
97
 
81
98
 
82
- const existingComponentPath = null;
83
-
84
- if (existingComponentPath) {
85
- const action = await select({
86
- message: 'You already have a component for this field, open it in editor?',
87
- choices: [
88
- { name: '✏️ Open in IDE', value: 'open' },
89
- { name: '🔙 BACK', value: '__BACK__' },
90
- { name: '🚪 Exit', value: '__EXIT__' },
91
- ]
92
- });
93
- if (action === 'open') {
94
- // await openFileInIde(existingComponentPath); // Needs absolute path
95
- console.log(`Opening ${existingComponentPath}... (Implementation needed)`);
96
- } else if (action === '__BACK__') {
97
- return handleFieldComponentCreation(config, resources); // Pass config back
98
- } else {
99
- process.exit(0);
99
+ const safeResourceLabel = sanitizeLabel(selectedResource.label)
100
+ const safeColumnLabel = sanitizeLabel(selectedColumn.label)
101
+ const componentFileName = `${safeResourceLabel}${safeColumnLabel}${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)}.vue`; // e.g., UserEmailShow.vue
102
+ const componentPathForConfig = `@@/${componentFileName}`; // Path relative to custom dir for config
103
+
104
+
105
+ try {
106
+ const { alreadyExists, path: absoluteComponentPath } = await generateComponentFile(
107
+ componentFileName,
108
+ fieldType,
109
+ { resource: selectedResource, column: selectedColumn },
110
+ config
111
+ );
112
+ if (!alreadyExists) {
113
+ console.log(chalk.dim(`Component generation successful: ${absoluteComponentPath}`));
114
+
115
+ await updateResourceConfig(selectedResource.resourceId, columnName, fieldType, componentPathForConfig);
116
+ console.log(
117
+ chalk.bold.greenBright('You can now open the component in your IDE:'),
118
+ chalk.underline.cyanBright(absoluteComponentPath)
119
+ );
120
+ }
121
+ process.exit(0);
122
+ }catch (error) {
123
+ console.error(error);
124
+ console.error(chalk.red('\n❌ Component creation failed. Please check the errors above.'));
125
+ process.exit(1);
126
+ }
127
+ }
128
+
129
+ async function handleCrudPageInjectionCreation(config, resources) {
130
+ console.log(chalk.grey('Selected ❯ 📄 CRUD Page Injection'));
131
+
132
+ const crudType = await select({
133
+ message: 'What view do you want to inject a custom component into?',
134
+ choices: [
135
+ { name: '🔸 list', value: 'list' },
136
+ { name: '📃 show', value: 'show' },
137
+ { name: '✏️ edit', value: 'edit' },
138
+ { name: '➕ create', value: 'create' },
139
+ new Separator(),
140
+ { name: '🔙 BACK', value: '__BACK__' },
141
+ ],
142
+ });
143
+ if (crudType === '__BACK__') return createComponent([]);
144
+
145
+ console.log(chalk.grey(`Selected ❯ 📄 CRUD Page Injection ❯ ${crudType}`));
146
+
147
+ const resourceId = await select({
148
+ message: 'Select resource for which you want to inject the component:',
149
+ choices: [
150
+ ...resources.map(r => ({ name: `${r.label} ${chalk.grey(`${r.resourceId}`)}`, value: r.resourceId })),
151
+ new Separator(),
152
+ { name: '🔙 BACK', value: '__BACK__' },
153
+ ],
154
+ });
155
+ if (resourceId === '__BACK__') return handleCrudPageInjectionCreation(config, resources);
156
+
157
+ const selectedResource = resources.find(r => r.resourceId === resourceId);
158
+ console.log(chalk.grey(`Selected ❯ 📄 CRUD Page Injection ❯ ${crudType} ❯ ${selectedResource.label}`));
159
+
160
+ const injectionPosition = await select({
161
+ message: 'Where exactly do you want to inject the component?',
162
+ choices: [
163
+ { name: '⬆️ Before Breadcrumbs', value: 'beforeBreadcrumbs' },
164
+ { name: '⬇️ After Breadcrumbs', value: 'afterBreadcrumbs' },
165
+ { name: '📄 After Page', value: 'bottom' },
166
+ { name: '⋯ threeDotsDropdownItems', value: 'threeDotsDropdownItems' },
167
+ new Separator(),
168
+ { name: '🔙 BACK', value: '__BACK__' },
169
+ ],
170
+ });
171
+ if (injectionPosition === '__BACK__') return handleCrudPageInjectionCreation(config, resources);
172
+
173
+ const isThin = await select({
174
+ message: 'Will this component be thin enough to fit on the same page with list (so list will still shrink)?',
175
+ choices: [
176
+ { name: 'Yes', value: true },
177
+ { name: 'No', value: false },
178
+ ],
179
+ });
180
+
181
+ const safeResourceLabel = sanitizeLabel(selectedResource.label)
182
+ const componentFileName = `${safeResourceLabel}${crudType.charAt(0).toUpperCase() + crudType.slice(1)}${injectionPosition.charAt(0).toUpperCase() + injectionPosition.slice(1)}.vue`;
183
+ const componentPathForConfig = `@@/${componentFileName}`;
184
+
185
+ try {
186
+ const { alreadyExists, path: absoluteComponentPath } = await generateCrudInjectionComponent(
187
+ componentFileName,
188
+ injectionPosition,
189
+ { resource: selectedResource },
190
+ config
191
+ );
192
+
193
+ if (!alreadyExists) {
194
+ console.log(chalk.dim(`Component generation successful: ${absoluteComponentPath}`));
195
+
196
+ await updateCrudInjectionConfig(
197
+ selectedResource.resourceId,
198
+ crudType,
199
+ injectionPosition,
200
+ componentPathForConfig,
201
+ isThin
202
+ );
203
+ console.log(
204
+ chalk.bold.greenBright('You can now open the component in your IDE:'),
205
+ chalk.underline.cyanBright(absoluteComponentPath)
206
+ );
100
207
  }
101
- } else {
102
- const safeResourceLabel = selectedResource.label.replace(/[^a-zA-Z0-9]/g, '');
103
- const safeColumnLabel = selectedColumn.label.replace(/[^a-zA-Z0-9]/g, '');
104
- const componentFileName = `${safeResourceLabel}${safeColumnLabel}${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)}.vue`; // e.g., UserEmailShow.vue
105
- const componentPathForConfig = `@@/${componentFileName}`; // Path relative to custom dir for config
106
-
107
- let absoluteComponentPath;
108
- try {
109
- absoluteComponentPath = await generateComponentFile(
110
- componentFileName,
111
- fieldType,
112
- { resource: selectedResource, column: selectedColumn },
113
- config
114
- );
115
- console.log(chalk.dim(`Component generation successful: ${absoluteComponentPath}`));
116
-
117
- await updateResourceConfig(selectedResource.resourceId, columnName, fieldType, componentPathForConfig);
118
- console.log(chalk.green(`\n✅ Successfully created component ${componentPathForConfig} and updated configuration.`));
119
-
120
- const openNow = await confirm({
121
- message: 'Open the new component file in your IDE?',
122
- default: true
123
- });
124
- if (openNow) { // await openFileInIde(absoluteComponentPath); // Use the absolute path here
125
- console.log(`Opening ${absoluteComponentPath}... (Implementation needed)`);
126
- }
127
-
128
- } catch (error) {
129
- console.error(chalk.red('\n❌ Component creation failed. Please check the errors above.'));
130
- process.exit(1);
208
+ process.exit(0);
209
+ } catch (error) {
210
+ console.error(error);
211
+ console.error(chalk.red('\n❌ Component creation failed. Please check the errors above.'));
212
+ process.exit(1);
213
+ }
214
+ }
215
+
216
+
217
+ async function handleLoginPageInjectionCreation(config) {
218
+ console.log('Selected ❯ 🔐 Login page injections');
219
+ const injectionType = await select({
220
+ message: 'Select injection type:',
221
+ choices: [
222
+ { name: 'After Login and password inputs', value: 'afterLogin' },
223
+ { name: '🔙 BACK', value: '__BACK__' },
224
+ ],
225
+ });
226
+ if (injectionType === '__BACK__') return createComponent([]);
227
+
228
+ console.log(chalk.grey(`Selected 🔐 Login page injections ${injectionType}`));
229
+
230
+ const reason = await input({
231
+ message: 'What will you need component for? (enter name)',
232
+ });
233
+
234
+ console.log(chalk.grey(`Selected ❯ 🔐 Login page injections ❯ ${injectionType} ❯ ${reason}`));
235
+
236
+
237
+ try {
238
+ const safeName = sanitizeLabel(reason)
239
+ const componentFileName = `CustomLogin${safeName}.vue`;
240
+
241
+ const context = { reason };
242
+
243
+ const { alreadyExists, path: absoluteComponentPath } = await generateLoginOrGlobalComponentFile(
244
+ componentFileName,
245
+ injectionType,
246
+ context
247
+ );
248
+ if (!alreadyExists) {
249
+ console.log(chalk.dim(`Component generation successful: ${absoluteComponentPath}`));
250
+ const configFilePath = path.resolve(process.cwd(), 'index.ts');
251
+ console.log(chalk.dim(`Injecting component: ${configFilePath}, ${componentFileName}`));
252
+ await injectLoginComponent(configFilePath, `@@/${componentFileName}`);
253
+
254
+ console.log(
255
+ chalk.bold.greenBright('You can now open the component in your IDE:'),
256
+ chalk.underline.cyanBright(absoluteComponentPath)
257
+ );
131
258
  }
259
+ process.exit(0);
260
+ }catch (error) {
261
+ console.error(error);
262
+ console.error(chalk.red('\n❌ Component creation failed. Please check the errors above.'));
263
+ process.exit(1);
132
264
  }
265
+
133
266
  }
134
267
 
135
- // --- TODO: Implement similar handlers for other component types (pass config) ---
136
- async function handleCrudPageInjectionCreation(config, resources) { console.log('CRUD Page Injection creation not implemented yet.'); }
137
- async function handleLoginPageInjectionCreation(config) { console.log('Login Page Injection creation not implemented yet.'); }
138
- async function handleGlobalInjectionCreation(config) { console.log('Global Injection creation not implemented yet.'); }
268
+ async function handleGlobalInjectionCreation(config) {
269
+ console.log('Selected 🌍 Global page injections');
270
+
271
+ const injectionType = await select({
272
+ message: 'Select global injection type:',
273
+ choices: [
274
+ { name: 'User Menu', value: 'userMenu' },
275
+ { name: 'Header', value: 'header' },
276
+ { name: 'Sidebar', value: 'sidebar' },
277
+ { name: 'Every Page Bottom', value: 'everyPageBottom' },
278
+ { name: '🔙 BACK', value: '__BACK__' },
279
+ ],
280
+ });
281
+
282
+ if (injectionType === '__BACK__') return createComponent([]);
283
+
284
+ console.log(chalk.grey(`Selected ❯ 🌍 Global page injections ❯ ${injectionType}`));
285
+
286
+ const reason = await input({
287
+ message: 'What will you need the component for? (enter name)',
288
+ });
289
+
290
+ console.log(chalk.grey(`Selected ❯ 🌍 Global page injections ❯ ${injectionType} ❯ ${reason}`));
291
+
292
+ try {
293
+ const safeName = sanitizeLabel(reason)
294
+ const componentFileName = `CustomGlobal${safeName}.vue`;
295
+
296
+ const context = { reason };
297
+
298
+ const { alreadyExists, path: absoluteComponentPath } = await generateLoginOrGlobalComponentFile(
299
+ componentFileName,
300
+ injectionType,
301
+ context
302
+ );
303
+ if (!alreadyExists) {
304
+ console.log(chalk.dim(`Component generation successful: ${absoluteComponentPath}`));
305
+
306
+ const configFilePath = path.resolve(process.cwd(), 'index.ts');
307
+
308
+ await injectGlobalComponent(configFilePath, injectionType, `@@/${componentFileName}`);
309
+
310
+ console.log(
311
+ chalk.bold.greenBright('You can now open the component in your IDE:'),
312
+ chalk.underline.cyanBright(absoluteComponentPath)
313
+ );
314
+ }
315
+ process.exit(0);
316
+ } catch (error) {
317
+ console.error(error);
318
+ console.error(chalk.red('\n❌ Component creation failed. Please check the errors above.'));
319
+ process.exit(1);
320
+ }
321
+ }
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
3
+ <button
4
+ type="button"
5
+ @click="handleClick"
6
+ class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800"
7
+ >
8
+ Example
9
+ </button>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { onMounted } from 'vue';
15
+ import { useI18n } from 'vue-i18n';
16
+ import adminforth from '@/adminforth';
17
+
18
+ const { t } = useI18n();
19
+
20
+ onMounted(() => {
21
+ // Logic on mount if needed
22
+ });
23
+
24
+ function handleClick() {
25
+ adminforth.alert({
26
+ message: t('Confirmed'),
27
+ variant: 'success',
28
+ });
29
+ }
30
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
3
+ <button
4
+ type="button"
5
+ @click="handleClick"
6
+ class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800"
7
+ >
8
+ Example
9
+ </button>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { onMounted } from 'vue';
15
+ import { useI18n } from 'vue-i18n';
16
+ import adminforth from '@/adminforth';
17
+
18
+ const { t } = useI18n();
19
+
20
+ onMounted(() => {
21
+ // Logic on mount if needed
22
+ });
23
+
24
+ function handleClick() {
25
+ adminforth.alert({
26
+ message: t('Confirmed'),
27
+ variant: 'success',
28
+ });
29
+ }
30
+ </script>
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <div class="overflow-auto p-4">
3
+ <table class="min-w-full text-sm text-left text-gray-500 dark:text-gray-400">
4
+ <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
5
+ <tr>
6
+ <th scope="col" class="px-6 py-3" v-for="header in headers" :key="header">
7
+ {{ header }}
8
+ </th>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ <tr
13
+ v-for="row in rows"
14
+ :key="row.id"
15
+ class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
16
+ >
17
+ <td class="px-6 py-4" v-for="cell in row.cells" :key="cell">
18
+ {{ cell }}
19
+ </td>
20
+ </tr>
21
+ </tbody>
22
+ </table>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { onMounted, ref } from 'vue';
28
+
29
+ const headers = [
30
+ 'ID',
31
+ 'Name',
32
+ 'Email',
33
+ 'Phone',
34
+ 'Address',
35
+ 'City',
36
+ 'Country',
37
+ 'Company',
38
+ 'Position',
39
+ 'Status',
40
+ ];
41
+
42
+ const rows = Array.from({ length: 50 }, (_, i) => ({
43
+ id: i + 1,
44
+ cells: [
45
+ i + 1,
46
+ `Name ${i + 1}`,
47
+ `user${i + 1}@example.com`,
48
+ `+1-555-010${i.toString().padStart(2, '0')}`,
49
+ `Address ${i + 1}`,
50
+ `City ${i + 1}`,
51
+ `Country ${i + 1}`,
52
+ `Company ${i + 1}`,
53
+ `Position ${i + 1}`,
54
+ i % 2 === 0 ? 'Active' : 'Inactive',
55
+ ],
56
+ }));
57
+
58
+ onMounted(() => {
59
+ console.log('Table mounted. Rows:', rows.length);
60
+ });
61
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="flex flex-col p-4 space-y-2">
3
+ <button
4
+ v-for="item in menuItems"
5
+ :key="item.label"
6
+ @click="item.action"
7
+ class="w-full text-left text-sm px-4 py-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"
8
+ >
9
+ {{ t(item.label) }}
10
+ </button>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { useI18n } from 'vue-i18n';
16
+ import adminforth from '@/adminforth';
17
+ import { onMounted } from 'vue';
18
+
19
+ const { t } = useI18n();
20
+
21
+ const menuItems = [
22
+ {
23
+ label: 'Show Success Alert',
24
+ action: () => {
25
+ adminforth.alert({
26
+ message: t('Success Alert'),
27
+ variant: 'success',
28
+ });
29
+ },
30
+ },
31
+ {
32
+ label: 'Show Warning Alert',
33
+ action: () => {
34
+ adminforth.alert({
35
+ message: t('Warning Alert'),
36
+ variant: 'warning',
37
+ });
38
+ },
39
+ },
40
+ {
41
+ label: 'Show Error Alert',
42
+ action: () => {
43
+ adminforth.alert({
44
+ message: t('Error Alert'),
45
+ variant: 'danger',
46
+ });
47
+ },
48
+ },
49
+ ];
50
+
51
+ onMounted(() => {
52
+ // Logic on mount if needed
53
+ });
54
+
55
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="flex items-center justify-center text-gray-900 dark:text-gray-400 p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
3
+ {{reason}}
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ defineProps({
9
+ reason: String
10
+ });
11
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="text-center text-gray-500 text-sm mt-4">
3
+ {{reason}}
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ defineProps({
9
+ reason: String
10
+ });
11
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="display-flex justify-center text-lightSidebarIcons text-center dark:text-darkSidebarIcons">
3
+ {{reason}}
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ defineProps({
9
+ reason: String
10
+ });
11
+ </script>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="text-center text-gray-500 text-sm mt-4">
3
+ {{reason}}
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ defineProps({
9
+ reason: String
10
+ });
11
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div class="text-center text-gray-500 text-sm mt-4">
3
+ {{reason}}
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ defineProps({
9
+ reason: String
10
+ });
11
+ </script>
12
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.21.0-next.11",
3
+ "version": "1.21.0-next.12",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",