Skip to content

Commit aeb89ae

Browse files
committed
feat: viewmodel class generator
1 parent 6e4efdd commit aeb89ae

File tree

7 files changed

+182
-3
lines changed

7 files changed

+182
-3
lines changed

.cursor/rules/commands.mdc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ globs: src/command/*.ts
44
alwaysApply: false
55
---
66
- Commands are located in `src/command` directory
7-
- Commands must extend the [Command.ts](mdc:src/command/Command.ts) abstract class or any other class that inherits it
7+
- Commands must extend the [Command.ts](mdc:magento-toolbox/src/command/Command.ts) abstract class or any other class that inherits it
88
- Command name must be defined in format: `magentoToolbox.commandName`
9-
- New commands must be added to [index.ts](mdc:src/command/index.ts) file so that they are loaded. Commands also need to be added to [package.json](mdc:package.json) under contributes -> commands
10-
- Commands that only generate files based on a pre-defined template can extend the [SimpleTemplateGeneratorCommand.ts](mdc:src/command/SimpleTemplateGeneratorCommand.ts) class
9+
- New commands must be added to [index.ts](mdc:magento-toolbox/src/command/index.ts) file so that they are loaded. Commands also need to be added to [package.json](mdc:magento-toolbox/package.json) under contributes -> commands
10+
- Commands that only generate files based on a pre-defined template can extend the [SimpleTemplateGeneratorCommand.ts](mdc:magento-toolbox/src/command/SimpleTemplateGeneratorCommand.ts) class

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to the "magento-toolbox" extension will be documented in thi
44

55
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
66

7+
## [Unreleased]
8+
- Added: Generator command for a ViewModel class
9+
710
## [1.3.1] - 2025-03-23
811
- Fixed: Generated plugin class arguments contain an incorrect namespace
912

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
"title": "Generate Block",
6666
"category": "Magento Toolbox"
6767
},
68+
{
69+
"command": "magento-toolbox.generateViewModel",
70+
"title": "Generate ViewModel",
71+
"category": "Magento Toolbox"
72+
},
6873
{
6974
"command": "magento-toolbox.indexWorkspace",
7075
"title": "Index Workspace",
@@ -244,6 +249,10 @@
244249
"command": "magento-toolbox.generateBlock",
245250
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
246251
},
252+
{
253+
"command": "magento-toolbox.generateViewModel",
254+
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
255+
},
247256
{
248257
"command": "magento-toolbox.generateEventsXml",
249258
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Command } from 'command/Command';
2+
import ViewModelClassGenerator from 'generator/viewModel/ViewModelClassGenerator';
3+
import ViewModelWizard, { ViewModelWizardData } from 'wizard/ViewModelWizard';
4+
import FileGeneratorManager from 'generator/FileGeneratorManager';
5+
import { Uri, window } from 'vscode';
6+
import Common from 'util/Common';
7+
import WizzardClosedError from 'webview/error/WizzardClosedError';
8+
import IndexManager from 'indexer/IndexManager';
9+
import ModuleIndexer from 'indexer/module/ModuleIndexer';
10+
11+
export default class GenerateViewModelCommand extends Command {
12+
constructor() {
13+
super('magento-toolbox.generateViewModel');
14+
}
15+
16+
public async execute(uri?: Uri): Promise<void> {
17+
const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY);
18+
let contextModule: string | undefined;
19+
20+
const contextUri = uri || window.activeTextEditor?.document.uri;
21+
22+
if (moduleIndex && contextUri) {
23+
const module = moduleIndex.getModuleByUri(contextUri);
24+
25+
if (module) {
26+
contextModule = module.name;
27+
}
28+
}
29+
30+
const viewModelWizard = new ViewModelWizard();
31+
32+
let data: ViewModelWizardData;
33+
34+
try {
35+
data = await viewModelWizard.show(contextModule);
36+
} catch (error) {
37+
if (error instanceof WizzardClosedError) {
38+
return;
39+
}
40+
41+
throw error;
42+
}
43+
44+
const manager = new FileGeneratorManager([new ViewModelClassGenerator(data)]);
45+
46+
const workspaceFolder = Common.getActiveWorkspaceFolder();
47+
48+
if (!workspaceFolder) {
49+
window.showErrorMessage('No active workspace folder');
50+
return;
51+
}
52+
53+
await manager.generate(workspaceFolder.uri);
54+
await manager.writeFiles();
55+
await manager.refreshIndex(workspaceFolder);
56+
manager.openFirstFile();
57+
}
58+
}

src/command/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { default as CopyMagentoPathCommand } from './CopyMagentoPathCommand';
55
export { default as GenerateXmlCatalogCommand } from './GenerateXmlCatalogCommand';
66
export { default as GenerateObserverCommand } from './GenerateObserverCommand';
77
export { default as GenerateBlockCommand } from './GenerateBlockCommand';
8+
export { default as GenerateViewModelCommand } from './GenerateViewModelCommand';
89
export { default as GenerateEventsXmlCommand } from './GenerateEventsXmlCommand';
910
export { default as GenerateGraphqlSchemaFileCommand } from './GenerateGraphqlSchemaFile';
1011
export { default as GenerateRoutesXmlFileCommand } from './GenerateRoutesXmlFileCommand';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import FileHeader from 'common/php/FileHeader';
2+
import FileGenerator from 'generator/FileGenerator';
3+
import GeneratedFile from 'generator/GeneratedFile';
4+
import { PhpFile, PsrPrinter } from 'node-php-generator';
5+
import Magento from 'util/Magento';
6+
import { Uri } from 'vscode';
7+
import { ViewModelWizardData } from 'wizard/ViewModelWizard';
8+
9+
export default class ViewModelClassGenerator extends FileGenerator {
10+
private static readonly ARGUMENT_INTERFACE =
11+
'Magento\\Framework\\View\\Element\\Block\\ArgumentInterface';
12+
13+
public constructor(protected data: ViewModelWizardData) {
14+
super();
15+
}
16+
17+
public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
18+
const [vendor, module] = this.data.module.split('_');
19+
const pathParts = this.data.directory.split('/');
20+
const namespaceParts = [vendor, module, ...pathParts];
21+
const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri);
22+
23+
const header = FileHeader.getHeader(this.data.module);
24+
25+
const phpFile = new PhpFile();
26+
if (header) {
27+
phpFile.addComment(header);
28+
}
29+
phpFile.setStrictTypes(true);
30+
31+
const namespace = phpFile.addNamespace(namespaceParts.join('\\'));
32+
namespace.addUse(ViewModelClassGenerator.ARGUMENT_INTERFACE);
33+
34+
const viewModelClass = namespace.addClass(this.data.className);
35+
36+
viewModelClass.setImplements([ViewModelClassGenerator.ARGUMENT_INTERFACE]);
37+
38+
const printer = new PsrPrinter();
39+
40+
return new GeneratedFile(
41+
Uri.joinPath(moduleDirectory, this.data.directory, `${this.data.className}.php`),
42+
printer.printFile(phpFile)
43+
);
44+
}
45+
}

src/wizard/ViewModelWizard.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import IndexManager from 'indexer/IndexManager';
2+
import ModuleIndexer from 'indexer/module/ModuleIndexer';
3+
import { GeneratorWizard } from 'webview/GeneratorWizard';
4+
import { WizardFieldBuilder } from 'webview/WizardFieldBuilder';
5+
import { WizardFormBuilder } from 'webview/WizardFormBuilder';
6+
import { WizardTabBuilder } from 'webview/WizardTabBuilder';
7+
8+
export interface ViewModelWizardData {
9+
module: string;
10+
className: string;
11+
directory: string;
12+
}
13+
14+
export default class ViewModelWizard extends GeneratorWizard {
15+
public async show(contextModule?: string): Promise<ViewModelWizardData> {
16+
const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY);
17+
18+
if (!moduleIndexData) {
19+
throw new Error('Module index data not found');
20+
}
21+
22+
const modules = moduleIndexData.getModuleOptions(m => m.location === 'app');
23+
24+
const builder = new WizardFormBuilder();
25+
26+
builder.setTitle('Generate a new ViewModel');
27+
builder.setDescription('Generates a new Magento2 ViewModel class.');
28+
29+
const tab = new WizardTabBuilder();
30+
tab.setId('viewModel');
31+
tab.setTitle('ViewModel');
32+
33+
tab.addField(
34+
WizardFieldBuilder.select('module', 'Module*')
35+
.setOptions(modules)
36+
.setInitialValue(contextModule || modules[0].value)
37+
.build()
38+
);
39+
40+
tab.addField(
41+
WizardFieldBuilder.text('className', 'Class Name*')
42+
.setPlaceholder('ViewModel class name')
43+
.build()
44+
);
45+
46+
tab.addField(
47+
WizardFieldBuilder.text('directory', 'Directory*')
48+
.setPlaceholder('ViewModel/Path')
49+
.setInitialValue('ViewModel')
50+
.build()
51+
);
52+
53+
builder.addTab(tab.build());
54+
55+
builder.addValidation('module', 'required');
56+
builder.addValidation('className', 'required|min:1');
57+
builder.addValidation('directory', 'required|min:1');
58+
59+
const data = await this.openWizard<ViewModelWizardData>(builder.build());
60+
61+
return data;
62+
}
63+
}

0 commit comments

Comments
 (0)