diff --git a/package-lock.json b/package-lock.json index 7c657e2..99dda30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "magento-toolbox", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magento-toolbox", - "version": "0.0.1", + "version": "1.0.0", "dependencies": { "@vscode-elements/elements": "^1.11.0", "@xml-tools/ast": "^5.0.5", @@ -25,6 +25,7 @@ "php-parser": "^3.2.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "slugify": "^1.6.6", "typescript-json-serializer": "^6.0.1", "typescript-memoize": "^1.1.1", "validatorjs": "^3.22.1" @@ -5255,6 +5256,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", diff --git a/package.json b/package.json index 671647f..7442d88 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,18 @@ "type": "string", "default": "bin/magento", "description": "Path to Magento CLI tool. Relative to workspace root or absolute path." + }, + "magento-toolbox.phpFileHeaderComment": { + "type": "string", + "editPresentation": "multilineText", + "default": "", + "markdownDescription": "`%module%` will be replaced with the module name. \n\n **Do not add comment symbols like `/*` or `*/`, they will be added automatically.** \n\n Separate lines with the newline character `\\n`" + }, + "magento-toolbox.xmlFileHeaderComment": { + "type": "string", + "editPresentation": "multilineText", + "default": "", + "markdownDescription": "`%module%` will be replaced with the module name. \n\n **Do not add comment symbols like ``, they will be added automatically.**" } } }, @@ -42,6 +54,10 @@ "command": "magento-toolbox.generateModule", "title": "Magento Toolbox: Generate Module" }, + { + "command": "magento-toolbox.generateBlock", + "title": "Magento Toolbox: Generate Block" + }, { "command": "magento-toolbox.indexWorkspace", "title": "Magento Toolbox: Index Workspace" @@ -61,6 +77,30 @@ { "command": "magento-toolbox.generateObserver", "title": "Magento Toolbox: Generate Observer" + }, + { + "command": "magento-toolbox.generateEventsXml", + "title": "Magento Toolbox: Generate Events XML" + }, + { + "command": "magento-toolbox.generateGraphqlSchemaFile", + "title": "Magento Toolbox: Generate GraphQL Schema File" + }, + { + "command": "magento-toolbox.generateRoutesXmlFile", + "title": "Magento Toolbox: Generate Routes XML" + }, + { + "command": "magento-toolbox.generateAclXmlFile", + "title": "Magento Toolbox: Generate ACL XML" + }, + { + "command": "magento-toolbox.generateDiXmlFile", + "title": "Magento Toolbox: Generate DI XML" + }, + { + "command": "magento-toolbox.generateWebapiXmlFile", + "title": "Magento Toolbox: Generate Webapi XML" } ], "menus": { @@ -97,6 +137,39 @@ "command": "magento-toolbox.copyMagentoPath", "title": "Magento Toolbox: Copy Magento Path", "when": "resourceExtname in magento-toolbox.supportedMagentoPathExtensions && resourcePath =~ /view/" + }, + { + "command": "magento-toolbox.generateObserver", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i", + "arguments": [] + }, + { + "command": "magento-toolbox.generateBlock", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, + { + "command": "magento-toolbox.generateEventsXml", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, + { + "command": "magento-toolbox.generateGraphqlSchemaFile", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, + { + "command": "magento-toolbox.generateRoutesXmlFile", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, + { + "command": "magento-toolbox.generateAclXmlFile", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, + { + "command": "magento-toolbox.generateDiXmlFile", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" + }, + { + "command": "magento-toolbox.generateWebapiXmlFile", + "when": "resourcePath =~ /app\\/code\\/.+\\/.+/i" } ] } @@ -157,6 +230,7 @@ "php-parser": "^3.2.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "slugify": "^1.6.6", "typescript-json-serializer": "^6.0.1", "typescript-memoize": "^1.1.1", "validatorjs": "^3.22.1" diff --git a/src/codelens/ObserverCodelensProvider.ts b/src/codelens/ObserverCodelensProvider.ts index 50261a5..7d3e569 100644 --- a/src/codelens/ObserverCodelensProvider.ts +++ b/src/codelens/ObserverCodelensProvider.ts @@ -61,7 +61,7 @@ export default class ObserverCodelensProvider implements CodeLensProvider { const codelens = new CodeLens(range, { title: 'Create an Observer', command: 'magento-toolbox.generateObserver', - arguments: [event.name], + arguments: [undefined, event.name], }); codelenses.push(codelens); diff --git a/src/command/GenerateAclXmlFileCommand.ts b/src/command/GenerateAclXmlFileCommand.ts new file mode 100644 index 0000000..687ee7a --- /dev/null +++ b/src/command/GenerateAclXmlFileCommand.ts @@ -0,0 +1,22 @@ +import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; +import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; + +export default class GenerateAclXmlFileCommand extends SimpleTemplateGeneratorCommand { + constructor() { + super('magento-toolbox.generateAclXmlFile'); + } + + getWizardTitle(): string { + return 'ACL XML File'; + } + + getTemplatePath(data: TemplateWizardData): string { + const [vendor, module] = data.module.split('_'); + + return `app/code/${vendor}/${module}/etc/acl.xml`; + } + + getTemplateName(data: TemplateWizardData): string { + return 'xml/blank-acl'; + } +} diff --git a/src/command/GenerateBlockCommand.ts b/src/command/GenerateBlockCommand.ts new file mode 100644 index 0000000..8cce4d0 --- /dev/null +++ b/src/command/GenerateBlockCommand.ts @@ -0,0 +1,58 @@ +import { Command } from 'command/Command'; +import BlockClassGenerator from 'generator/block/BlockClassGenerator'; +import BlockWizard, { BlockWizardData } from 'wizard/BlockWizard'; +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 GenerateBlockCommand extends Command { + constructor() { + super('magento-toolbox.generateBlock'); + } + + 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 blockWizard = new BlockWizard(); + + let data: BlockWizardData; + + try { + data = await blockWizard.show(contextModule); + } catch (error) { + if (error instanceof WizzardClosedError) { + return; + } + + throw error; + } + + const manager = new FileGeneratorManager([new BlockClassGenerator(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/GenerateDiXmlFileCommand.ts b/src/command/GenerateDiXmlFileCommand.ts new file mode 100644 index 0000000..e0f6c9a --- /dev/null +++ b/src/command/GenerateDiXmlFileCommand.ts @@ -0,0 +1,39 @@ +import { MagentoScope } from 'types'; +import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; +import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; + +export default class GenerateDiXmlFileCommand extends SimpleTemplateGeneratorCommand { + constructor() { + super('magento-toolbox.generateDiXmlFile'); + } + + getAreas(): MagentoScope[] { + return [ + MagentoScope.Global, + MagentoScope.Adminhtml, + MagentoScope.Frontend, + MagentoScope.Cron, + MagentoScope.WebapiRest, + MagentoScope.WebapiSoap, + MagentoScope.Graphql, + ]; + } + + getWizardTitle(): string { + return 'DI XML File'; + } + + getTemplatePath(data: TemplateWizardData): string { + const [vendor, module] = data.module.split('_'); + + if (data.area && data.area !== MagentoScope.Global) { + return `app/code/${vendor}/${module}/etc/${data.area}/di.xml`; + } + + return `app/code/${vendor}/${module}/etc/di.xml`; + } + + getTemplateName(data: TemplateWizardData): string { + return 'xml/blank-di'; + } +} diff --git a/src/command/GenerateEventsXmlCommand.ts b/src/command/GenerateEventsXmlCommand.ts new file mode 100644 index 0000000..5a18fa1 --- /dev/null +++ b/src/command/GenerateEventsXmlCommand.ts @@ -0,0 +1,44 @@ +import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; +import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; +import { MagentoScope } from 'types'; +import FileHeader from 'common/xml/FileHeader'; + +export default class GenerateEventsXmlCommand extends SimpleTemplateGeneratorCommand { + constructor() { + super('magento-toolbox.generateEventsXml'); + } + + getWizardTitle(): string { + return 'Events XML File'; + } + + getAreas(): MagentoScope[] { + return [ + MagentoScope.Global, + MagentoScope.Adminhtml, + MagentoScope.Frontend, + MagentoScope.Cron, + MagentoScope.WebapiRest, + MagentoScope.WebapiSoap, + MagentoScope.Graphql, + ]; + } + + getFileHeader(data: TemplateWizardData): string | undefined { + return FileHeader.getHeader(data.module); + } + + getTemplatePath(data: TemplateWizardData): string { + const [vendor, module] = data.module.split('_'); + + if (data.area && data.area !== MagentoScope.Global) { + return `app/code/${vendor}/${module}/etc/${data.area}/events.xml`; + } + + return `app/code/${vendor}/${module}/etc/events.xml`; + } + + getTemplateName(data: TemplateWizardData): string { + return 'xml/blank-events'; + } +} diff --git a/src/command/GenerateGraphqlSchemaFile.ts b/src/command/GenerateGraphqlSchemaFile.ts new file mode 100644 index 0000000..7b430fc --- /dev/null +++ b/src/command/GenerateGraphqlSchemaFile.ts @@ -0,0 +1,22 @@ +import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; +import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; + +export default class GenerateGraphqlSchemaFileCommand extends SimpleTemplateGeneratorCommand { + constructor() { + super('magento-toolbox.generateGraphqlSchemaFile'); + } + + getWizardTitle(): string { + return 'GraphQL Schema File'; + } + + getTemplatePath(data: TemplateWizardData): string { + const [vendor, module] = data.module.split('_'); + + return `app/code/${vendor}/${module}/etc/schema.graphqls`; + } + + getTemplateName(data: TemplateWizardData): string { + return 'graphql/blank-schema'; + } +} diff --git a/src/command/GenerateObserverCommand.ts b/src/command/GenerateObserverCommand.ts index bcfd183..9b64674 100644 --- a/src/command/GenerateObserverCommand.ts +++ b/src/command/GenerateObserverCommand.ts @@ -3,22 +3,39 @@ import ObserverWizard, { ObserverWizardData } from 'wizard/ObserverWizard'; import WizzardClosedError from 'webview/error/WizzardClosedError'; import FileGeneratorManager from 'generator/FileGeneratorManager'; import Common from 'util/Common'; -import { window } from 'vscode'; +import { Uri, window } from 'vscode'; import ObserverClassGenerator from 'generator/observer/ObserverClassGenerator'; import ObserverEventsGenerator from 'generator/observer/ObserverEventsGenerator'; +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; export default class GenerateObserverCommand extends Command { constructor() { super('magento-toolbox.generateObserver'); } - public async execute(eventName?: string): Promise { + public async execute(uri?: Uri, eventName?: string): Promise { + eventName = typeof eventName === 'string' ? eventName : undefined; + + 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 observerWizard = new ObserverWizard(); let data: ObserverWizardData; try { - data = await observerWizard.show(eventName); + data = await observerWizard.show(eventName, contextModule); } catch (error) { if (error instanceof WizzardClosedError) { return; diff --git a/src/command/GenerateRoutesXmlFileCommand.ts b/src/command/GenerateRoutesXmlFileCommand.ts new file mode 100644 index 0000000..e42e4fb --- /dev/null +++ b/src/command/GenerateRoutesXmlFileCommand.ts @@ -0,0 +1,94 @@ +import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; +import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; +import { MagentoScope } from 'types'; +import FileHeader from 'common/xml/FileHeader'; +import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; +import { WizardField, WizardValidationRule } from 'webview/types'; + +export default class GenerateRoutesXmlFileCommand extends SimpleTemplateGeneratorCommand { + constructor() { + super('magento-toolbox.generateRoutesXmlFile'); + } + + getWizardTitle(): string { + return 'Routes XML File'; + } + + getAreas(): MagentoScope[] { + return [MagentoScope.Adminhtml, MagentoScope.Frontend]; + } + + getFileHeader(data: TemplateWizardData): string | undefined { + return FileHeader.getHeader(data.module); + } + + getTemplatePath(data: TemplateWizardData): string { + const [vendor, module] = data.module.split('_'); + + return `app/code/${vendor}/${module}/etc/${data.area}/routes.xml`; + } + + getTemplateName(data: TemplateWizardData): string { + return 'xml/blank-routes'; + } + + getWizardFields(): WizardField[] { + return [ + WizardFieldBuilder.select('frontendRouterId', 'Router ID') + .setOptions([ + { + label: 'Default', + value: 'default', + }, + { + label: 'Robots', + value: 'robots', + }, + { + label: 'URL Rewrite', + value: 'urlrewrite', + }, + { + label: 'Standard', + value: 'standard', + }, + { + label: 'CMS', + value: 'cms', + }, + ]) + .setInitialValue('default') + .addDependsOn('area', MagentoScope.Frontend) + .build(), + + WizardFieldBuilder.select('adminRouterId', 'Router ID') + .setOptions([ + { + label: 'Admin', + value: 'admin', + }, + ]) + .addDependsOn('area', MagentoScope.Adminhtml) + .setInitialValue('admin') + .build(), + WizardFieldBuilder.text('routeId', 'Route ID').build(), + WizardFieldBuilder.text('frontName', 'Front Name').build(), + ]; + } + + getWizardValidation(): Record { + return { + adminRouterId: [{ required_if: ['area', MagentoScope.Adminhtml] }], + frontendRouterId: [{ required_if: ['area', MagentoScope.Frontend] }], + routeId: 'required', + frontName: 'required', + }; + } + + getTemplateData(data: TemplateWizardData): Record { + return { + ...data, + routerId: data.frontendRouterId || data.adminRouterId || 'default', + }; + } +} diff --git a/src/command/GenerateWebapiXmlFileCommand.ts b/src/command/GenerateWebapiXmlFileCommand.ts new file mode 100644 index 0000000..5a96b12 --- /dev/null +++ b/src/command/GenerateWebapiXmlFileCommand.ts @@ -0,0 +1,27 @@ +import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; +import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; +import FileHeader from 'common/xml/FileHeader'; + +export default class GenerateWebapiXmlFileCommand extends SimpleTemplateGeneratorCommand { + constructor() { + super('magento-toolbox.generateWebapiXmlFile'); + } + + getWizardTitle(): string { + return 'Webapi XML File'; + } + + getFileHeader(data: TemplateWizardData): string | undefined { + return FileHeader.getHeader(data.module); + } + + getTemplatePath(data: TemplateWizardData): string { + const [vendor, module] = data.module.split('_'); + + return `app/code/${vendor}/${module}/etc/webapi.xml`; + } + + getTemplateName(data: TemplateWizardData): string { + return 'xml/blank-webapi'; + } +} diff --git a/src/command/SimpleTemplateGeneratorCommand.ts b/src/command/SimpleTemplateGeneratorCommand.ts new file mode 100644 index 0000000..69b5bf2 --- /dev/null +++ b/src/command/SimpleTemplateGeneratorCommand.ts @@ -0,0 +1,81 @@ +import IndexManager from 'indexer/IndexManager'; +import { Command } from './Command'; +import { Uri, window } from 'vscode'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import FileGeneratorManager from 'generator/FileGeneratorManager'; +import TemplateGenerator from 'generator/TemplateGenerator'; +import Common from 'util/Common'; +import SimpleTemplateWizard, { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; +import { MagentoScope } from 'types'; +import { WizardField, WizardValidationRule } from 'webview/types'; + +export abstract class SimpleTemplateGeneratorCommand extends Command { + abstract getWizardTitle(): string; + + public getAreas(): MagentoScope[] { + return []; + } + + abstract getTemplatePath(data: TemplateWizardData): string; + + abstract getTemplateName(data: TemplateWizardData): string; + + public getFileHeader(data: TemplateWizardData): string | undefined { + return undefined; + } + + public getWizardFields(): WizardField[] { + return []; + } + + public getWizardValidation(): Record { + return {}; + } + + public getTemplateData(data: TemplateWizardData): Record { + return data; + } + + 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 wizard = new SimpleTemplateWizard(); + const data = await wizard.show( + this.getWizardTitle(), + contextModule, + this.getAreas(), + this.getWizardFields(), + this.getWizardValidation() + ); + + const manager = new FileGeneratorManager([ + new TemplateGenerator(this.getTemplatePath(data), this.getTemplateName(data), { + ...this.getTemplateData(data), + fileHeader: this.getFileHeader(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/common/php/FileHeader.ts b/src/common/php/FileHeader.ts new file mode 100644 index 0000000..f0d30b2 --- /dev/null +++ b/src/common/php/FileHeader.ts @@ -0,0 +1,15 @@ +import { workspace } from 'vscode'; + +export default class FileHeader { + public static getHeader(module: string): string | undefined { + const header = workspace + .getConfiguration('magento-toolbox') + .get('phpFileHeaderComment'); + + if (!header) { + return undefined; + } + + return header.replace('%module%', module); + } +} diff --git a/src/common/xml/FileHeader.ts b/src/common/xml/FileHeader.ts new file mode 100644 index 0000000..16067f2 --- /dev/null +++ b/src/common/xml/FileHeader.ts @@ -0,0 +1,21 @@ +import { workspace } from 'vscode'; + +export default class FileHeader { + public static getHeader(module: string): string | undefined { + const header = workspace + .getConfiguration('magento-toolbox') + .get('xmlFileHeaderComment'); + + if (!header) { + return undefined; + } + + let headerComment = ''; + + return headerComment; + } +} diff --git a/src/extension.ts b/src/extension.ts index e3581ab..9905273 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,6 +17,12 @@ import GenerateXmlCatalogCommand from 'command/GenerateXmlCatalogCommand'; import XmlClasslikeHoverProvider from 'hover/XmlClasslikeHoverProvider'; import ObserverCodelensProvider from 'codelens/ObserverCodelensProvider'; import GenerateObserverCommand from 'command/GenerateObserverCommand'; +import GenerateBlockCommand from 'command/GenerateBlockCommand'; +import GenerateEventsXmlCommand from 'command/GenerateEventsXmlCommand'; +import GenerateGraphqlSchemaFileCommand from 'command/GenerateGraphqlSchemaFile'; +import GenerateRoutesXmlFileCommand from 'command/GenerateRoutesXmlFileCommand'; +import GenerateAclXmlFileCommand from 'command/GenerateAclXmlFileCommand'; +import GenerateDiXmlFileCommand from 'command/GenerateDiXmlFileCommand'; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -30,6 +36,12 @@ export async function activate(context: vscode.ExtensionContext) { CopyMagentoPathCommand, GenerateXmlCatalogCommand, GenerateObserverCommand, + GenerateBlockCommand, + GenerateEventsXmlCommand, + GenerateGraphqlSchemaFileCommand, + GenerateRoutesXmlFileCommand, + GenerateAclXmlFileCommand, + GenerateDiXmlFileCommand, ]; ExtensionState.init(context); diff --git a/src/generator/block/BlockClassGenerator.ts b/src/generator/block/BlockClassGenerator.ts new file mode 100644 index 0000000..3e8838c --- /dev/null +++ b/src/generator/block/BlockClassGenerator.ts @@ -0,0 +1,47 @@ +import FileHeader from 'common/php/FileHeader'; +import GeneratedFile from 'generator/GeneratedFile'; +import ModuleFileGenerator from 'generator/ModuleFileGenerator'; +import { PhpFile, PsrPrinter } from 'node-php-generator'; +import { Uri } from 'vscode'; +import { BlockWizardData } from 'wizard/BlockWizard'; + +/** + * This is file was generated by the Magento Toolbox extension. + * %module% is the name of the module. + */ + +export default class BlockClassGenerator extends ModuleFileGenerator { + private static readonly BLOCK_CLASS_PARENT = 'Magento\\Framework\\View\\Element\\Template'; + + public constructor(protected data: BlockWizardData) { + super(); + } + + public async generate(workspaceUri: Uri): Promise { + const [vendor, module] = this.data.module.split('_'); + const namespaceParts = [vendor, module, this.data.path]; + const moduleDirectory = this.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(BlockClassGenerator.BLOCK_CLASS_PARENT); + + const blockClass = namespace.addClass(this.data.name); + + blockClass.setExtends(BlockClassGenerator.BLOCK_CLASS_PARENT); + + const printer = new PsrPrinter(); + + return new GeneratedFile( + Uri.joinPath(moduleDirectory, this.data.path, `${this.data.name}.php`), + printer.printFile(phpFile) + ); + } +} diff --git a/src/generator/observer/ObserverClassGenerator.ts b/src/generator/observer/ObserverClassGenerator.ts index 7bfddb1..dcdc2e1 100644 --- a/src/generator/observer/ObserverClassGenerator.ts +++ b/src/generator/observer/ObserverClassGenerator.ts @@ -1,3 +1,4 @@ +import FileHeader from 'common/php/FileHeader'; import PhpNamespace from 'common/PhpNamespace'; import GeneratedFile from 'generator/GeneratedFile'; import ModuleFileGenerator from 'generator/ModuleFileGenerator'; @@ -21,6 +22,12 @@ export default class ObserverClassGenerator extends ModuleFileGenerator { const phpFile = new PhpFile(); phpFile.setStrictTypes(true); + const header = FileHeader.getHeader(this.data.module); + + if (header) { + phpFile.addComment(header); + } + const namespace = phpFile.addNamespace(PhpNamespace.fromParts(namespaceParts).toString()); namespace.addUse(ObserverClassGenerator.OBSERVER_INTERFACE); namespace.addUse(ObserverClassGenerator.OBSERVER_CLASS); diff --git a/src/generator/plugin/PluginClassGenerator.ts b/src/generator/plugin/PluginClassGenerator.ts index 49cede9..bf57fe4 100644 --- a/src/generator/plugin/PluginClassGenerator.ts +++ b/src/generator/plugin/PluginClassGenerator.ts @@ -1,3 +1,4 @@ +import FileHeader from 'common/php/FileHeader'; import GeneratedFile from 'generator/GeneratedFile'; import ModuleFileGenerator from 'generator/ModuleFileGenerator'; import { upperFirst } from 'lodash-es'; @@ -29,6 +30,12 @@ export default class PluginClassGenerator extends ModuleFileGenerator { const phpFile = new PhpFile(); phpFile.setStrictTypes(true); + const header = FileHeader.getHeader(this.data.module); + + if (header) { + phpFile.addComment(header); + } + const namespace = phpFile.addNamespace(parts.join('\\')); const phpClass = namespace.addClass(pluginName); const pluginMethod = phpClass.addMethod(pluginMethodName); diff --git a/src/generator/util/FindOrCreateDiXml.ts b/src/generator/util/FindOrCreateDiXml.ts index 4e293e7..c256771 100644 --- a/src/generator/util/FindOrCreateDiXml.ts +++ b/src/generator/util/FindOrCreateDiXml.ts @@ -1,6 +1,7 @@ import { Uri } from 'vscode'; import FileSystem from 'util/FileSystem'; import GenerateFromTemplate from './GenerateFromTemplate'; +import FileHeader from 'common/xml/FileHeader'; export default class FindOrCreateDiXml { public static async execute(workspaceUri: Uri, vendor: string, module: string): Promise { @@ -10,6 +11,10 @@ export default class FindOrCreateDiXml { return await FileSystem.readFile(diFile); } - return await GenerateFromTemplate.generate('xml/blank-di'); + const fileHeader = FileHeader.getHeader(module); + + return await GenerateFromTemplate.generate('xml/blank-di', { + fileHeader, + }); } } diff --git a/src/indexer/module/ModuleIndexData.ts b/src/indexer/module/ModuleIndexData.ts index a74bbf6..b896a6c 100644 --- a/src/indexer/module/ModuleIndexData.ts +++ b/src/indexer/module/ModuleIndexData.ts @@ -17,9 +17,9 @@ export class ModuleIndexData extends AbstractIndexData { return this.getValues().find(module => module.name === name); } - public getModuleByUri(uri: Uri): Module | undefined { + public getModuleByUri(uri: Uri, appOnly = true): Module | undefined { const module = this.getValues().find(module => { - return uri.fsPath.startsWith(module.path); + return uri.fsPath.startsWith(module.path) && (!appOnly || module.location === 'app'); }); return module; diff --git a/src/types.ts b/src/types.ts index 8a47211..7cfc119 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,8 @@ export enum MagentoScope { Global = 'global', Frontend = 'frontend', Adminhtml = 'adminhtml', - Webapi = 'webapi', + WebapiRest = 'webapi_rest', + WebapiSoap = 'webapi_soap', Graphql = 'graphql', Cron = 'cron', } diff --git a/src/webview/WizardFieldBuilder.ts b/src/webview/WizardFieldBuilder.ts index c61cb88..eb1094c 100644 --- a/src/webview/WizardFieldBuilder.ts +++ b/src/webview/WizardFieldBuilder.ts @@ -17,7 +17,8 @@ export class WizardFieldBuilder { private id: string | undefined = undefined, private label: string | undefined = undefined, private description: string[] | undefined = undefined, - private dependsOn: FieldDependency | undefined = undefined + private dependsOn: FieldDependency | undefined = undefined, + private fields: WizardField[] | undefined = undefined ) {} public static new(): WizardFieldBuilder { @@ -40,6 +41,10 @@ export class WizardFieldBuilder { return new WizardFieldBuilder(WizardInput.Checkbox, id, label); } + public static dynamicRow(id?: string, label?: string): WizardFieldBuilder { + return new WizardFieldBuilder(WizardInput.DynamicRow, id, label); + } + public setId(id: string): WizardFieldBuilder { this.id = id; return this; @@ -79,6 +84,11 @@ export class WizardFieldBuilder { return this; } + public addFields(fields: WizardField[]): WizardFieldBuilder { + this.fields = fields; + return this; + } + public addOption(option: WizardSelectOption): WizardFieldBuilder { this.options.push(option); return this; @@ -122,6 +132,15 @@ export class WizardFieldBuilder { initialValue: this.initialValue, type: this.type, }; + case WizardInput.DynamicRow: + return { + id: this.id, + label: this.label, + description: this.description, + dependsOn: this.dependsOn, + fields: this.fields ?? [], + type: this.type, + }; case WizardInput.Checkbox: return { id: this.id, diff --git a/src/webview/WizardFormBuilder.ts b/src/webview/WizardFormBuilder.ts index 8045276..f4dd838 100644 --- a/src/webview/WizardFormBuilder.ts +++ b/src/webview/WizardFormBuilder.ts @@ -1,10 +1,10 @@ import { ErrorMessages, Rules, TypeCheckingRule } from 'validatorjs'; -import { Wizard, WizardField } from './types'; +import { Wizard, WizardTab } from './types'; export class WizardFormBuilder { private title?: string; private description?: string; - private fields: WizardField[] = []; + private tabs: WizardTab[] = []; private validation: Rules = {}; private validationMessages: ErrorMessages = {}; @@ -12,6 +12,10 @@ export class WizardFormBuilder { return new WizardFormBuilder(); } + public addTab(tab: WizardTab): void { + this.tabs.push(tab); + } + public setTitle(title: string): void { this.title = title; } @@ -20,10 +24,6 @@ export class WizardFormBuilder { this.description = description; } - public addField(field: WizardField): void { - this.fields.push(field); - } - public addValidation( field: string, rule: string | Array | Rules @@ -40,14 +40,14 @@ export class WizardFormBuilder { throw new Error('Title is required'); } - if (!this.fields.length) { - throw new Error('Fields are required'); + if (!this.tabs.length) { + throw new Error('Tabs are required'); } return { title: this.title, description: this.description, - fields: this.fields, + tabs: this.tabs, validation: this.validation, validationMessages: this.validationMessages, }; diff --git a/src/webview/WizardTabBuilder.ts b/src/webview/WizardTabBuilder.ts new file mode 100644 index 0000000..2fd9982 --- /dev/null +++ b/src/webview/WizardTabBuilder.ts @@ -0,0 +1,49 @@ +import { WizardField, WizardTab } from './types'; + +export class WizardTabBuilder { + private id?: string; + private title?: string; + private description?: string; + private fields: WizardField[] = []; + + public static new(): WizardTabBuilder { + return new WizardTabBuilder(); + } + + public setId(id: string): void { + this.id = id; + } + + public setTitle(title: string): void { + this.title = title; + } + + public setDescription(description: string): void { + this.description = description; + } + + public addField(field: WizardField): void { + this.fields.push(field); + } + + public build(): WizardTab { + if (!this.id) { + throw new Error('Id is required'); + } + + if (!this.title) { + throw new Error('Title is required'); + } + + if (!this.fields.length) { + throw new Error('Fields are required'); + } + + return { + id: this.id, + title: this.title, + description: this.description, + fields: this.fields, + }; + } +} diff --git a/src/webview/components/Wizard.tsx b/src/webview/components/Wizard.tsx index 7183331..5e59581 100644 --- a/src/webview/components/Wizard.tsx +++ b/src/webview/components/Wizard.tsx @@ -12,7 +12,7 @@ const Wizard: React.FC = ({ data, vscode }) => {

{data.title}

{data.description}

- +
); diff --git a/src/webview/components/Wizard/DynamicRowInput.tsx b/src/webview/components/Wizard/DynamicRowInput.tsx new file mode 100644 index 0000000..6ad7bc4 --- /dev/null +++ b/src/webview/components/Wizard/DynamicRowInput.tsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react'; +import { WizardDynamicRowField, WizardField } from 'webview/types'; +import { FieldRenderer } from './FieldRenderer'; +import { FieldArray, useFormikContext } from 'formik'; + +interface Props { + field: WizardDynamicRowField; +} + +export const DynamicRowInput: React.FC = ({ field }) => { + const { values } = useFormikContext(); + const rows = values[field.id] ?? ([] as Record[]); + + return ( + { + return ( + <> + {/* @ts-ignore */} + + + {field.fields.map(field => ( + + {field.label} + + ))} + Action + + + + {rows.map((row: any, index: number) => ( + + {field.fields.map(childField => ( + + + + ))} + + arrayHelpers.remove(index)}> + Remove + + + + ))} + + + arrayHelpers.push({})}> + Add Row + + + ); + }} + > + ); +}; diff --git a/src/webview/components/Wizard/FieldErrorMessage.tsx b/src/webview/components/Wizard/FieldErrorMessage.tsx new file mode 100644 index 0000000..cdad47a --- /dev/null +++ b/src/webview/components/Wizard/FieldErrorMessage.tsx @@ -0,0 +1,15 @@ +import { Field, FormikProps, getIn } from 'formik'; + +export const FieldErrorMessage: React.FC<{ name: string }> = ({ name }) => { + return ( + }) => { + const error = form.errors[name]; + const touch = getIn(form.touched, name); + + return touch && error ? error : null; + }} + /> + ); +}; diff --git a/src/webview/components/Wizard/FieldRenderer.tsx b/src/webview/components/Wizard/FieldRenderer.tsx index f685063..6375fea 100644 --- a/src/webview/components/Wizard/FieldRenderer.tsx +++ b/src/webview/components/Wizard/FieldRenderer.tsx @@ -2,22 +2,36 @@ import { Option } from '@vscode-elements/elements/dist/includes/vscode-select/ty import { useField, useFormikContext } from 'formik'; import { useEffect, useMemo, useRef } from 'react'; import { WizardField, WizardInput, WizardSelectOption } from 'webview/types'; +import { DynamicRowInput } from './DynamicRowInput'; +import { FieldErrorMessage } from './FieldErrorMessage'; interface Props { field: WizardField; + prefix?: string; + simple?: boolean; } -const getFieldProps = (field: WizardField) => { +const getFieldId = (field: WizardField, prefix?: string) => { + if (prefix) { + return `${prefix}.${field.id}`; + } + + return field.id; +}; + +const getFieldProps = (field: WizardField, prefix?: string) => { switch (field.type) { case WizardInput.Readonly: return { readonly: true }; case WizardInput.Checkbox: - return { name: field.id, checked: false }; + return { name: getFieldId(field, prefix), checked: false }; case WizardInput.Select: - return { name: field.id }; + return { name: getFieldId(field, prefix) }; + case WizardInput.DynamicRow: + return { name: getFieldId(field, prefix) }; default: return { - name: field.id, + name: getFieldId(field, prefix), placeholder: field.placeholder, }; } @@ -33,10 +47,10 @@ const mapOption = (option: WizardSelectOption): Option => { }; }; -export const FieldRenderer: React.FC = ({ field }) => { +export const FieldRenderer: React.FC = ({ field, simple = false, prefix }) => { const { values } = useFormikContext(); const el = useRef(null); - const [fieldProps, meta] = useField(getFieldProps(field)); + const [fieldProps, meta] = useField(getFieldProps(field, prefix)); /** * vscode-elements do not support (yet) onChange prop @@ -86,6 +100,9 @@ export const FieldRenderer: React.FC = ({ field }) => { /> ); } + case WizardInput.DynamicRow: { + return ; + } case WizardInput.Checkbox: { return ; } @@ -99,9 +116,25 @@ export const FieldRenderer: React.FC = ({ field }) => { } if (fieldInner) { + if (simple) { + return ( + <> + {fieldInner} + +

+ +

+
+ + ); + } + return ( - {field.label} + {field.type !== WizardInput.DynamicRow && {field.label}} + {field.type === WizardInput.DynamicRow && ( +
{field.label}
+ )} {fieldInner} {meta.touched && meta.error &&

{meta.error}

} diff --git a/src/webview/components/Wizard/Renderer.tsx b/src/webview/components/Wizard/Renderer.tsx index e663a38..5837012 100644 --- a/src/webview/components/Wizard/Renderer.tsx +++ b/src/webview/components/Wizard/Renderer.tsx @@ -1,4 +1,4 @@ -import { Form, Formik, FormikValues } from 'formik'; +import { Form, Formik, FormikProps, FormikValues } from 'formik'; import { useCallback } from 'react'; import { WebviewApi } from 'vscode-webview'; import Validator from 'validatorjs'; @@ -11,13 +11,23 @@ interface Props { } export const Renderer: React.FC = ({ wizard, vscode }) => { - const initialValues: FormikValues = wizard.fields.reduce((acc: FormikValues, field) => { - if (field.type === WizardInput.Select && field.multiple) { - acc[field.id] = field.initialValue ?? []; + const isSingleTab = wizard.tabs.length === 1; + const initialValues: FormikValues = wizard.tabs.reduce((acc: FormikValues, tab) => { + tab.fields.reduce((_: FormikValues, field) => { + if (field.type === WizardInput.Select && field.multiple) { + acc[field.id] = field.initialValue ?? []; + return acc; + } + + if (field.type === WizardInput.DynamicRow) { + acc[field.id] = field.initialValue ?? []; + return acc; + } + + acc[field.id] = field.initialValue ?? ''; return acc; - } + }, {}); - acc[field.id] = field.initialValue ?? ''; return acc; }, {}); @@ -27,7 +37,6 @@ export const Renderer: React.FC = ({ wizard, vscode }) => { const handleValidation = useCallback((values: FormikValues) => { if (wizard.validation) { - console.log(wizard.validation); const validation = new Validator(values, wizard.validation, wizard.validationMessages); validation.passes(); @@ -39,16 +48,36 @@ export const Renderer: React.FC = ({ wizard, vscode }) => { }, []); return ( -
+
-
- {wizard.fields.map(field => { - return ; - })} -
- Submit -
- + {(props: FormikProps) => { + return ( + <> + + {wizard.tabs.map(tab => { + return ( +
+ {!isSingleTab && ( + {tab.title} + )} + +

{tab.description}

+
+ {tab.fields.map(field => { + return ; + })} +
+
+ ); + })} +
+
+ props.submitForm()} disabled={!props.isValid}> + Submit + + + ); + }}
); diff --git a/src/webview/components/app.css b/src/webview/components/app.css index e60cdb4..de3ee13 100644 --- a/src/webview/components/app.css +++ b/src/webview/components/app.css @@ -1,3 +1,26 @@ .app { padding: 16px; } + +.dynamic-row-cell { + vertical-align: top; + padding-top: 8px; + padding-bottom: 6px; +} + +.dynamic-row-title { + width: 100%; + text-align: center; + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; +} + +.dynamic-row-add-row { + margin-top: 12px; +} + +.tab-panel { + padding: 16px; + overflow: visible; +} diff --git a/src/webview/types.ts b/src/webview/types.ts index 6f15001..95b6fec 100644 --- a/src/webview/types.ts +++ b/src/webview/types.ts @@ -1,4 +1,4 @@ -import { ErrorMessages, Rules } from 'validatorjs'; +import { ErrorMessages, Rules, TypeCheckingRule } from 'validatorjs'; export enum Page { Wizard = 'wizard', @@ -20,18 +20,28 @@ export type WizardMessage = Message; export interface Wizard { title: string; description?: string; - fields: WizardField[]; + tabs: WizardTab[]; validationSchema?: any; validation?: Rules; validationMessages?: ErrorMessages; } +export type WizardValidationRule = string | Array | Rules; + +export interface WizardTab { + id: string; + title: string; + description?: string; + fields: WizardField[]; +} + export type WizardField = | WizardTextField | WizardNumberField | WizardSelectField | WizardReadonlyField - | WizardCheckboxField; + | WizardCheckboxField + | WizardDynamicRowField; export interface WizardTextField extends WizardGenericField { type: WizardInput.Text; @@ -59,7 +69,12 @@ export interface WizardCheckboxField extends WizardGenericField { type: WizardInput.Checkbox; } -export type FieldValue = string | number | boolean; +export interface WizardDynamicRowField extends WizardGenericField { + type: WizardInput.DynamicRow; + fields: WizardField[]; +} + +export type FieldValue = string | number | boolean | Record; export interface FieldDependency { field: string; @@ -80,6 +95,7 @@ export enum WizardInput { Select = 'select', Checkbox = 'checkbox', Readonly = 'readonly', + DynamicRow = 'dynamic-row', } export interface WizardSelectOption { diff --git a/src/wizard/BlockWizard.ts b/src/wizard/BlockWizard.ts new file mode 100644 index 0000000..5908845 --- /dev/null +++ b/src/wizard/BlockWizard.ts @@ -0,0 +1,61 @@ +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 BlockWizardData { + module: string; + name: string; + path: string; +} + +export default class BlockWizard 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 block'); + builder.setDescription('Generates a new Magento2 block class.'); + + const tab = new WizardTabBuilder(); + tab.setId('block'); + tab.setTitle('Block'); + + tab.addField( + WizardFieldBuilder.select('module', 'Module*') + .setOptions(modules) + .setInitialValue(contextModule || modules[0].value) + .build() + ); + + tab.addField( + WizardFieldBuilder.text('name', 'Block Name*').setPlaceholder('Block class name').build() + ); + + tab.addField( + WizardFieldBuilder.text('path', 'Block Directory*') + .setPlaceholder('Block/Path') + .setInitialValue('Block') + .build() + ); + + builder.addTab(tab.build()); + + builder.addValidation('module', 'required'); + builder.addValidation('name', 'required|min:1'); + builder.addValidation('path', 'required|min:1'); + + const data = await this.openWizard(builder.build()); + + return data; + } +} diff --git a/src/wizard/ModuleWizard.ts b/src/wizard/ModuleWizard.ts index 73aa941..aebb7c0 100644 --- a/src/wizard/ModuleWizard.ts +++ b/src/wizard/ModuleWizard.ts @@ -4,6 +4,7 @@ import { License } from 'types'; import { GeneratorWizard } from 'webview/GeneratorWizard'; import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; import { WizardFormBuilder } from 'webview/WizardFormBuilder'; +import { WizardTabBuilder } from 'webview/WizardTabBuilder'; interface ModuleWizardBaseData { vendor: string; @@ -40,21 +41,26 @@ export default class ModuleWizard extends GeneratorWizard { builder.setTitle('Generate a new module'); builder.setDescription('Generates the basic structure of a Magento2 module.'); - builder.addField( + const tab = new WizardTabBuilder(); + tab.setId('module'); + tab.setTitle('Module'); + + tab.addField( WizardFieldBuilder.text('vendor', 'Vendor*').setPlaceholder('Vendor name').build() ); - builder.addField( + tab.addField( WizardFieldBuilder.text('module', 'Module*').setPlaceholder('Module name').build() ); - builder.addField( + tab.addField( WizardFieldBuilder.select('sequence', 'Dependencies') .setOptions(modules) .setMultiple(true) .build() ); - builder.addField( + + tab.addField( WizardFieldBuilder.select('license', 'License') .setOptions([ { label: 'No license', value: 'none' }, @@ -65,28 +71,32 @@ export default class ModuleWizard extends GeneratorWizard { ]) .build() ); - builder.addField( + + tab.addField( WizardFieldBuilder.text('version', 'Version') .setPlaceholder('Version') .setInitialValue('1.0.0') .build() ); - builder.addField(WizardFieldBuilder.checkbox('composer', 'Generate composer.json').build()); - builder.addField( + tab.addField(WizardFieldBuilder.checkbox('composer', 'Generate composer.json').build()); + + tab.addField( WizardFieldBuilder.text('composerName', 'Package name') .setPlaceholder('module/name') .addDependsOn('composer', true) .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.text('composerDescription', 'Package description') .setPlaceholder('Package description') .addDependsOn('composer', true) .build() ); + builder.addTab(tab.build()); + builder.addValidation('vendor', 'required|min:1'); builder.addValidation('module', 'required|min:1'); builder.addValidation('composerName', [{ required_if: ['composer', true] }]); diff --git a/src/wizard/ObserverWizard.ts b/src/wizard/ObserverWizard.ts index bda13c4..284a81d 100644 --- a/src/wizard/ObserverWizard.ts +++ b/src/wizard/ObserverWizard.ts @@ -5,6 +5,7 @@ import { MagentoScope } from 'types'; import { GeneratorWizard } from 'webview/GeneratorWizard'; import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; import { WizardFormBuilder } from 'webview/WizardFormBuilder'; +import { WizardTabBuilder } from 'webview/WizardTabBuilder'; export interface ObserverWizardData { module: string; @@ -16,7 +17,10 @@ export interface ObserverWizardData { } export default class ObserverWizard extends GeneratorWizard { - public async show(initialEventName?: string): Promise { + public async show( + initialEventName?: string, + contextModule?: string + ): Promise { const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); if (!moduleIndexData) { @@ -30,43 +34,50 @@ export default class ObserverWizard extends GeneratorWizard { builder.setTitle('Generate a new observer'); builder.setDescription('Generates a new observer.'); - builder.addField( + const tab = new WizardTabBuilder(); + tab.setId('observer'); + tab.setTitle('Observer'); + + tab.addField( WizardFieldBuilder.select('module', 'Module') .setDescription(['Module where observer will be generated in']) .setOptions(modules) - .setInitialValue(modules[0].value) + .setInitialValue(contextModule || modules[0].value) .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.select('area', 'Area') .setOptions(Object.values(MagentoScope).map(scope => ({ label: scope, value: scope }))) .setInitialValue(MagentoScope.Global) .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.text('eventName', 'Event name') .setPlaceholder('event_name') .setInitialValue(initialEventName) .build() ); - builder.addField( + + tab.addField( WizardFieldBuilder.text('observerName', 'Observer name') .setPlaceholder('observer_name') .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.text('className', 'Observer class name') .setPlaceholder('ObserverName') .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.text('directoryPath', 'Directory path').setInitialValue('Observer').build() ); + builder.addTab(tab.build()); + builder.addValidation('module', 'required'); builder.addValidation('area', 'required'); builder.addValidation('eventName', [ diff --git a/src/wizard/PluginContextWizard.ts b/src/wizard/PluginContextWizard.ts index bb64218..939225b 100644 --- a/src/wizard/PluginContextWizard.ts +++ b/src/wizard/PluginContextWizard.ts @@ -4,6 +4,7 @@ import { MagentoScope } from 'types'; import { GeneratorWizard } from 'webview/GeneratorWizard'; import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; import { WizardFormBuilder } from 'webview/WizardFormBuilder'; +import { WizardTabBuilder } from 'webview/WizardTabBuilder'; export interface PluginContextWizardData { module: string; @@ -32,7 +33,11 @@ export default class PluginContextWizard extends GeneratorWizard { builder.setTitle('Generate a new plugin'); builder.setDescription('Generates a plugin for a method.'); - builder.addField( + const tab = new WizardTabBuilder(); + tab.setId('plugin'); + tab.setTitle('Plugin'); + + tab.addField( WizardFieldBuilder.select('module', 'Module') .setDescription(['Module where plugin will be generated in']) .setOptions(modules) @@ -40,24 +45,24 @@ export default class PluginContextWizard extends GeneratorWizard { .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.select('method', 'Method') .setOptions(allowedMethods.map(method => ({ label: method, value: method }))) .setInitialValue(initialMethod) .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.text('className', 'Plugin class name') .setDescription(['E.g. "MyPlugin"']) .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.text('name', 'Plugin name').setDescription(['E.g. "my_plugin"']).build() ); - builder.addField( + tab.addField( WizardFieldBuilder.select('type', 'Plugin type') .setOptions([ { label: 'Before', value: 'before' }, @@ -68,14 +73,16 @@ export default class PluginContextWizard extends GeneratorWizard { .build() ); - builder.addField( + tab.addField( WizardFieldBuilder.select('scope', 'Scope') .setOptions(Object.values(MagentoScope).map(scope => ({ label: scope, value: scope }))) .setInitialValue(MagentoScope.Global) .build() ); - builder.addField(WizardFieldBuilder.number('sortOrder', 'Sort order').build()); + tab.addField(WizardFieldBuilder.number('sortOrder', 'Sort order').build()); + + builder.addTab(tab.build()); builder.addValidation('module', 'required'); builder.addValidation('method', 'required'); diff --git a/src/wizard/SimpleTemplateWizard.ts b/src/wizard/SimpleTemplateWizard.ts new file mode 100644 index 0000000..3472d37 --- /dev/null +++ b/src/wizard/SimpleTemplateWizard.ts @@ -0,0 +1,76 @@ +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import { MagentoScope } from 'types'; +import { GeneratorWizard } from 'webview/GeneratorWizard'; +import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; +import { WizardFormBuilder } from 'webview/WizardFormBuilder'; +import { WizardTabBuilder } from 'webview/WizardTabBuilder'; +import slugify from 'slugify'; +import { WizardField, WizardValidationRule } from 'webview/types'; + +export type TemplateWizardData = { + module: string; + area?: MagentoScope; +} & Record; + +export default class SimpleTemplateWizard extends GeneratorWizard { + public async show( + title: string, + initialModule?: string, + areas: MagentoScope[] = [], + wizardFields: WizardField[] = [], + wizardValidation: Record = {} + ): Promise { + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + throw new Error('Module index data not found'); + } + + const modules = moduleIndexData.getModuleOptions(module => module.location === 'app'); + + const builder = new WizardFormBuilder(); + + const nameSlug = slugify(title); + + builder.setTitle('Generate a new ' + title); + + const tab = new WizardTabBuilder(); + tab.setId(nameSlug); + tab.setTitle(title); + + tab.addField( + WizardFieldBuilder.select('module', 'Module') + .setDescription(['Module where ' + title + ' will be generated in']) + .setOptions(modules) + .setInitialValue(initialModule || modules[0]?.value) + .build() + ); + builder.addValidation('module', 'required'); + + if (areas.length > 0) { + tab.addField( + WizardFieldBuilder.select('area', 'Area') + .setOptions(areas.map(scope => ({ label: scope, value: scope }))) + .setInitialValue(areas[0]) + .build() + ); + + builder.addValidation('area', 'required'); + } + + wizardFields.forEach(field => { + tab.addField(field); + }); + + Object.entries(wizardValidation).forEach(([field, validation]) => { + builder.addValidation(field, validation); + }); + + builder.addTab(tab.build()); + + const data = await this.openWizard(builder.build()); + + return data; + } +} diff --git a/templates/graphqls/blank-schema.ejs b/templates/graphqls/blank-schema.ejs new file mode 100644 index 0000000..e69de29 diff --git a/templates/xml/blank-acl.ejs b/templates/xml/blank-acl.ejs new file mode 100644 index 0000000..724dbcf --- /dev/null +++ b/templates/xml/blank-acl.ejs @@ -0,0 +1,7 @@ + +<% if (fileHeader) { -%> +<%- fileHeader %> +<% } -%> + + diff --git a/templates/xml/blank-di.ejs b/templates/xml/blank-di.ejs index 6f3211b..0d78280 100644 --- a/templates/xml/blank-di.ejs +++ b/templates/xml/blank-di.ejs @@ -1,4 +1,7 @@ +<% if (fileHeader) { -%> +<%- fileHeader %> +<% } -%> \ No newline at end of file diff --git a/templates/xml/blank-events.ejs b/templates/xml/blank-events.ejs index 4a2bf96..7e7cab1 100644 --- a/templates/xml/blank-events.ejs +++ b/templates/xml/blank-events.ejs @@ -1,4 +1,7 @@ +<% if (fileHeader) { -%> +<%- fileHeader %> +<% } -%> \ No newline at end of file diff --git a/templates/xml/blank-routes.ejs b/templates/xml/blank-routes.ejs new file mode 100644 index 0000000..0f4a9fd --- /dev/null +++ b/templates/xml/blank-routes.ejs @@ -0,0 +1,12 @@ + +<% if (fileHeader) { -%> +<%- fileHeader %> +<% } -%> + + + + + + + diff --git a/templates/xml/blank-webapi.ejs b/templates/xml/blank-webapi.ejs new file mode 100644 index 0000000..147511f --- /dev/null +++ b/templates/xml/blank-webapi.ejs @@ -0,0 +1,7 @@ + +<% if (fileHeader) { -%> +<%- fileHeader %> +<% } -%> + + \ No newline at end of file