From aeb89ae1d3221e7b16646df620b43a135bac3547 Mon Sep 17 00:00:00 2001 From: raideer Date: Sun, 23 Mar 2025 22:36:03 +0200 Subject: [PATCH 1/3] feat: viewmodel class generator --- .cursor/rules/commands.mdc | 6 +- CHANGELOG.md | 3 + package.json | 9 +++ src/command/GenerateViewModelCommand.ts | 58 +++++++++++++++++ src/command/index.ts | 1 + .../viewModel/ViewModelClassGenerator.ts | 45 +++++++++++++ src/wizard/ViewModelWizard.ts | 63 +++++++++++++++++++ 7 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 src/command/GenerateViewModelCommand.ts create mode 100644 src/generator/viewModel/ViewModelClassGenerator.ts create mode 100644 src/wizard/ViewModelWizard.ts diff --git a/.cursor/rules/commands.mdc b/.cursor/rules/commands.mdc index a27f087..def3077 100644 --- a/.cursor/rules/commands.mdc +++ b/.cursor/rules/commands.mdc @@ -4,7 +4,7 @@ globs: src/command/*.ts alwaysApply: false --- - Commands are located in `src/command` directory -- Commands must extend the [Command.ts](mdc:src/command/Command.ts) abstract class or any other class that inherits it +- Commands must extend the [Command.ts](mdc:magento-toolbox/src/command/Command.ts) abstract class or any other class that inherits it - Command name must be defined in format: `magentoToolbox.commandName` -- 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 -- Commands that only generate files based on a pre-defined template can extend the [SimpleTemplateGeneratorCommand.ts](mdc:src/command/SimpleTemplateGeneratorCommand.ts) class +- 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 +- Commands that only generate files based on a pre-defined template can extend the [SimpleTemplateGeneratorCommand.ts](mdc:magento-toolbox/src/command/SimpleTemplateGeneratorCommand.ts) class diff --git a/CHANGELOG.md b/CHANGELOG.md index e99e226..552f61c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to the "magento-toolbox" extension will be documented in thi Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [Unreleased] +- Added: Generator command for a ViewModel class + ## [1.3.1] - 2025-03-23 - Fixed: Generated plugin class arguments contain an incorrect namespace diff --git a/package.json b/package.json index 0f1fcf5..f3e63fc 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,11 @@ "title": "Generate Block", "category": "Magento Toolbox" }, + { + "command": "magento-toolbox.generateViewModel", + "title": "Generate ViewModel", + "category": "Magento Toolbox" + }, { "command": "magento-toolbox.indexWorkspace", "title": "Index Workspace", @@ -244,6 +249,10 @@ "command": "magento-toolbox.generateBlock", "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" }, + { + "command": "magento-toolbox.generateViewModel", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, { "command": "magento-toolbox.generateEventsXml", "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" diff --git a/src/command/GenerateViewModelCommand.ts b/src/command/GenerateViewModelCommand.ts new file mode 100644 index 0000000..9be95fc --- /dev/null +++ b/src/command/GenerateViewModelCommand.ts @@ -0,0 +1,58 @@ +import { Command } from 'command/Command'; +import ViewModelClassGenerator from 'generator/viewModel/ViewModelClassGenerator'; +import ViewModelWizard, { ViewModelWizardData } from 'wizard/ViewModelWizard'; +import FileGeneratorManager from 'generator/FileGeneratorManager'; +import { Uri, window } from 'vscode'; +import Common from 'util/Common'; +import WizzardClosedError from 'webview/error/WizzardClosedError'; +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; + +export default class GenerateViewModelCommand extends Command { + constructor() { + super('magento-toolbox.generateViewModel'); + } + + public async execute(uri?: Uri): Promise { + const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY); + let contextModule: string | undefined; + + const contextUri = uri || window.activeTextEditor?.document.uri; + + if (moduleIndex && contextUri) { + const module = moduleIndex.getModuleByUri(contextUri); + + if (module) { + contextModule = module.name; + } + } + + const viewModelWizard = new ViewModelWizard(); + + let data: ViewModelWizardData; + + try { + data = await viewModelWizard.show(contextModule); + } catch (error) { + if (error instanceof WizzardClosedError) { + return; + } + + throw error; + } + + const manager = new FileGeneratorManager([new ViewModelClassGenerator(data)]); + + const workspaceFolder = Common.getActiveWorkspaceFolder(); + + if (!workspaceFolder) { + window.showErrorMessage('No active workspace folder'); + return; + } + + await manager.generate(workspaceFolder.uri); + await manager.writeFiles(); + await manager.refreshIndex(workspaceFolder); + manager.openFirstFile(); + } +} diff --git a/src/command/index.ts b/src/command/index.ts index 16dc4fa..97c6332 100644 --- a/src/command/index.ts +++ b/src/command/index.ts @@ -5,6 +5,7 @@ export { default as CopyMagentoPathCommand } from './CopyMagentoPathCommand'; export { default as GenerateXmlCatalogCommand } from './GenerateXmlCatalogCommand'; export { default as GenerateObserverCommand } from './GenerateObserverCommand'; export { default as GenerateBlockCommand } from './GenerateBlockCommand'; +export { default as GenerateViewModelCommand } from './GenerateViewModelCommand'; export { default as GenerateEventsXmlCommand } from './GenerateEventsXmlCommand'; export { default as GenerateGraphqlSchemaFileCommand } from './GenerateGraphqlSchemaFile'; export { default as GenerateRoutesXmlFileCommand } from './GenerateRoutesXmlFileCommand'; diff --git a/src/generator/viewModel/ViewModelClassGenerator.ts b/src/generator/viewModel/ViewModelClassGenerator.ts new file mode 100644 index 0000000..354ad86 --- /dev/null +++ b/src/generator/viewModel/ViewModelClassGenerator.ts @@ -0,0 +1,45 @@ +import FileHeader from 'common/php/FileHeader'; +import FileGenerator from 'generator/FileGenerator'; +import GeneratedFile from 'generator/GeneratedFile'; +import { PhpFile, PsrPrinter } from 'node-php-generator'; +import Magento from 'util/Magento'; +import { Uri } from 'vscode'; +import { ViewModelWizardData } from 'wizard/ViewModelWizard'; + +export default class ViewModelClassGenerator extends FileGenerator { + private static readonly ARGUMENT_INTERFACE = + 'Magento\\Framework\\View\\Element\\Block\\ArgumentInterface'; + + public constructor(protected data: ViewModelWizardData) { + super(); + } + + public async generate(workspaceUri: Uri): Promise { + const [vendor, module] = this.data.module.split('_'); + const pathParts = this.data.directory.split('/'); + const namespaceParts = [vendor, module, ...pathParts]; + const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri); + + const header = FileHeader.getHeader(this.data.module); + + const phpFile = new PhpFile(); + if (header) { + phpFile.addComment(header); + } + phpFile.setStrictTypes(true); + + const namespace = phpFile.addNamespace(namespaceParts.join('\\')); + namespace.addUse(ViewModelClassGenerator.ARGUMENT_INTERFACE); + + const viewModelClass = namespace.addClass(this.data.className); + + viewModelClass.setImplements([ViewModelClassGenerator.ARGUMENT_INTERFACE]); + + const printer = new PsrPrinter(); + + return new GeneratedFile( + Uri.joinPath(moduleDirectory, this.data.directory, `${this.data.className}.php`), + printer.printFile(phpFile) + ); + } +} diff --git a/src/wizard/ViewModelWizard.ts b/src/wizard/ViewModelWizard.ts new file mode 100644 index 0000000..864f789 --- /dev/null +++ b/src/wizard/ViewModelWizard.ts @@ -0,0 +1,63 @@ +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import { GeneratorWizard } from 'webview/GeneratorWizard'; +import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; +import { WizardFormBuilder } from 'webview/WizardFormBuilder'; +import { WizardTabBuilder } from 'webview/WizardTabBuilder'; + +export interface ViewModelWizardData { + module: string; + className: string; + directory: string; +} + +export default class ViewModelWizard extends GeneratorWizard { + public async show(contextModule?: string): Promise { + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + throw new Error('Module index data not found'); + } + + const modules = moduleIndexData.getModuleOptions(m => m.location === 'app'); + + const builder = new WizardFormBuilder(); + + builder.setTitle('Generate a new ViewModel'); + builder.setDescription('Generates a new Magento2 ViewModel class.'); + + const tab = new WizardTabBuilder(); + tab.setId('viewModel'); + tab.setTitle('ViewModel'); + + tab.addField( + WizardFieldBuilder.select('module', 'Module*') + .setOptions(modules) + .setInitialValue(contextModule || modules[0].value) + .build() + ); + + tab.addField( + WizardFieldBuilder.text('className', 'Class Name*') + .setPlaceholder('ViewModel class name') + .build() + ); + + tab.addField( + WizardFieldBuilder.text('directory', 'Directory*') + .setPlaceholder('ViewModel/Path') + .setInitialValue('ViewModel') + .build() + ); + + builder.addTab(tab.build()); + + builder.addValidation('module', 'required'); + builder.addValidation('className', 'required|min:1'); + builder.addValidation('directory', 'required|min:1'); + + const data = await this.openWizard(builder.build()); + + return data; + } +} From b1a1beb2bf9583fcfb330c050b6fc06347cc2bdc Mon Sep 17 00:00:00 2001 From: raideer Date: Sun, 23 Mar 2025 22:39:12 +0200 Subject: [PATCH 2/3] chore: added test for viewmodel generator --- .../viewModel/ViewModelClassGenerator.test.ts | 98 +++++++++++++++++++ .../generator/viewModel/TestViewModel.php | 15 +++ .../viewModel/TestViewModelCustomPath.php | 15 +++ 3 files changed, 128 insertions(+) create mode 100644 src/test/generator/viewModel/ViewModelClassGenerator.test.ts create mode 100644 test-resources/reference/generator/viewModel/TestViewModel.php create mode 100644 test-resources/reference/generator/viewModel/TestViewModelCustomPath.php diff --git a/src/test/generator/viewModel/ViewModelClassGenerator.test.ts b/src/test/generator/viewModel/ViewModelClassGenerator.test.ts new file mode 100644 index 0000000..5ead13d --- /dev/null +++ b/src/test/generator/viewModel/ViewModelClassGenerator.test.ts @@ -0,0 +1,98 @@ +import { ViewModelWizardData } from 'wizard/ViewModelWizard'; +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import ViewModelClassGenerator from 'generator/viewModel/ViewModelClassGenerator'; +import { describe, it, before, afterEach } from 'mocha'; +import { setup } from 'test/setup'; +import { getReferenceFile, getTestWorkspaceUri } from 'test/util'; +import FileHeader from 'common/php/FileHeader'; +import sinon from 'sinon'; + +describe('ViewModelClassGenerator Tests', () => { + const viewModelWizardData: ViewModelWizardData = { + module: 'Foo_Bar', + className: 'TestViewModel', + directory: 'ViewModel', + }; + + before(async () => { + await setup(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should generate viewModel class file', async () => { + // Mock the FileHeader.getHeader method to return a consistent header + sinon.stub(FileHeader, 'getHeader').returns('Foo_Bar'); + + // Create the generator with test data + const generator = new ViewModelClassGenerator(viewModelWizardData); + + // Use a test workspace URI + const workspaceUri = getTestWorkspaceUri(); + + // Generate the file + const generatedFile = await generator.generate(workspaceUri); + + // Get the reference file content + const referenceContent = getReferenceFile('generator/viewModel/TestViewModel.php'); + + // Compare the generated content with reference + assert.strictEqual(generatedFile.content, referenceContent); + }); + + it('should generate file in correct location', async () => { + // Create the generator with test data + const generator = new ViewModelClassGenerator(viewModelWizardData); + + // Use a test workspace URI + const workspaceUri = getTestWorkspaceUri(); + + // Generate the file + const generatedFile = await generator.generate(workspaceUri); + + // Expected path + const expectedPath = Uri.joinPath( + workspaceUri, + 'app/code/Foo/Bar/ViewModel/TestViewModel.php' + ).fsPath; + + assert.strictEqual(generatedFile.uri.fsPath, expectedPath); + }); + + it('should generate viewModel class in custom directory', async () => { + // Create test data with custom directory + const customDirectoryData: ViewModelWizardData = { + ...viewModelWizardData, + directory: 'ViewModel/Custom/Path', + }; + + // Mock the FileHeader.getHeader method to return a consistent header + sinon.stub(FileHeader, 'getHeader').returns('Foo_Bar'); + + // Create the generator with custom directory data + const generator = new ViewModelClassGenerator(customDirectoryData); + + // Use a test workspace URI + const workspaceUri = getTestWorkspaceUri(); + + // Generate the file + const generatedFile = await generator.generate(workspaceUri); + + // Get the reference file content for custom directory + const referenceContent = getReferenceFile('generator/viewModel/TestViewModelCustomPath.php'); + + // Compare the generated content with reference + assert.strictEqual(generatedFile.content, referenceContent); + + // Verify file location + const expectedPath = Uri.joinPath( + workspaceUri, + 'app/code/Foo/Bar/ViewModel/Custom/Path/TestViewModel.php' + ).fsPath; + + assert.strictEqual(generatedFile.uri.fsPath, expectedPath); + }); +}); diff --git a/test-resources/reference/generator/viewModel/TestViewModel.php b/test-resources/reference/generator/viewModel/TestViewModel.php new file mode 100644 index 0000000..bef083c --- /dev/null +++ b/test-resources/reference/generator/viewModel/TestViewModel.php @@ -0,0 +1,15 @@ + Date: Sun, 23 Mar 2025 22:42:12 +0200 Subject: [PATCH 3/3] fix: mdc paths --- .cursor/rules/commands.mdc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.cursor/rules/commands.mdc b/.cursor/rules/commands.mdc index def3077..a27f087 100644 --- a/.cursor/rules/commands.mdc +++ b/.cursor/rules/commands.mdc @@ -4,7 +4,7 @@ globs: src/command/*.ts alwaysApply: false --- - Commands are located in `src/command` directory -- Commands must extend the [Command.ts](mdc:magento-toolbox/src/command/Command.ts) abstract class or any other class that inherits it +- Commands must extend the [Command.ts](mdc:src/command/Command.ts) abstract class or any other class that inherits it - Command name must be defined in format: `magentoToolbox.commandName` -- 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 -- Commands that only generate files based on a pre-defined template can extend the [SimpleTemplateGeneratorCommand.ts](mdc:magento-toolbox/src/command/SimpleTemplateGeneratorCommand.ts) class +- 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 +- Commands that only generate files based on a pre-defined template can extend the [SimpleTemplateGeneratorCommand.ts](mdc:src/command/SimpleTemplateGeneratorCommand.ts) class