diff --git a/src/constants/ExtensionTypes.ts b/src/constants/ExtensionTypes.ts index 72585f7..c9360fb 100644 --- a/src/constants/ExtensionTypes.ts +++ b/src/constants/ExtensionTypes.ts @@ -17,7 +17,7 @@ export const ExtensionTypes = [ { name: 'Field Customizer', value: ExtensionType.field, - templates: ['React', 'Minimal', 'No framework'] + templates: ['No framework', 'React', 'Minimal'] }, { name: 'ListView Command Set', @@ -27,7 +27,7 @@ export const ExtensionTypes = [ { name: 'Form Customizer', value: ExtensionType.formCustomizer, - templates: ['React', 'No framework'] + templates: ['No framework', 'React'] }, { name: 'Search Query Modifier', diff --git a/src/constants/SpfxAddComponentCommandInput.ts b/src/constants/SpfxAddComponentCommandInput.ts new file mode 100644 index 0000000..434297a --- /dev/null +++ b/src/constants/SpfxAddComponentCommandInput.ts @@ -0,0 +1,10 @@ +import { ComponentType } from './ComponentTypes'; +import { ExtensionType } from './ExtensionTypes'; + +export interface SpfxAddComponentCommandInput { + componentType: ComponentType; + componentName: string; + frameworkType: string; + extensionType: ExtensionType; + aceType: string; +} \ No newline at end of file diff --git a/src/constants/SpfxScaffoldCommandInput.ts b/src/constants/SpfxScaffoldCommandInput.ts new file mode 100644 index 0000000..3d51f02 --- /dev/null +++ b/src/constants/SpfxScaffoldCommandInput.ts @@ -0,0 +1,6 @@ +import { SpfxAddComponentCommandInput } from './SpfxAddComponentCommandInput'; + +export interface SpfxScaffoldCommandInput extends SpfxAddComponentCommandInput { + folderPath: string; + solutionName: string; +} \ No newline at end of file diff --git a/src/constants/WebViewTypes.ts b/src/constants/WebViewTypes.ts index 6738691..f657bfd 100644 --- a/src/constants/WebViewTypes.ts +++ b/src/constants/WebViewTypes.ts @@ -2,7 +2,8 @@ // eslint-disable-next-line no-shadow export enum WebViewType { samplesGallery = 'samplesGallery', - workflowForm = 'workflowForm' + workflowForm = 'workflowForm', + scaffoldForm = 'scaffoldForm' } export const WebViewTypes = [ @@ -15,5 +16,10 @@ export const WebViewTypes = [ Title: 'Sample Gallery', homePageUrl: '/sp-dev-fx-samples', value: WebViewType.samplesGallery + }, + { + Title: 'Scaffold Form', + homePageUrl: '/scaffold-form', + value: WebViewType.scaffoldForm } ]; \ No newline at end of file diff --git a/src/constants/WebviewCommand.ts b/src/constants/WebviewCommand.ts index 5787318..78d8cc1 100644 --- a/src/constants/WebviewCommand.ts +++ b/src/constants/WebviewCommand.ts @@ -2,11 +2,19 @@ export const WebviewCommand = { toWebview: { viewType: 'view-type', WorkflowCreated: 'workflow-created', + folderPath: 'folder-path', + validateSolutionName: 'validate-solution-name', + validateComponentName: 'validate-component-name', }, toVSCode: { useSample: 'use-sample', redirectTo: 'redirect-to', logError: 'log-error', createWorkFlow: 'create-workflow', + createSpfxProject: 'create-spfx-project', + pickFolder: 'pick-folder', + validateSolutionName: 'validate-solution-name', + validateComponentName: 'validate-component-name', + addSpfxComponent: 'add-spfx-component', } }; \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index 33a885b..36a5c9c 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -7,5 +7,7 @@ export * from './FrameworkTypes'; export * from './General'; export * from './GenerateWorkflowCommandInput'; export * from './ProjectFileContent'; +export * from './SpfxAddComponentCommandInput'; +export * from './SpfxScaffoldCommandInput'; export * from './WebviewCommand'; export * from './WebViewTypes'; diff --git a/src/services/Scaffolder.ts b/src/services/Scaffolder.ts index eb30361..1bdc53c 100644 --- a/src/services/Scaffolder.ts +++ b/src/services/Scaffolder.ts @@ -1,70 +1,47 @@ import { parseWinPath } from './../utils/parseWinPath'; -import { Executer } from './CommandExecuter'; import { Folders } from './Folders'; import { Notifications } from './Notifications'; import { Logger } from './Logger'; import { commands, ProgressLocation, QuickPickItem, Uri, window } from 'vscode'; -import { AdaptiveCardTypesNode16, AdaptiveCardTypesNode18, Commands, ComponentType, ComponentTypes, FrameworkTypes, ProjectFileContent } from '../constants'; +import { Commands, ComponentType, ProjectFileContent, SpfxAddComponentCommandInput, SpfxScaffoldCommandInput, WebviewCommand, WebViewType } from '../constants'; import { Sample, Subscription } from '../models'; import { join } from 'path'; import { existsSync, mkdirSync, writeFileSync } from 'fs'; import * as glob from 'fast-glob'; -import { ExtensionTypes } from '../constants/ExtensionTypes'; import { Extension } from './Extension'; import download from 'github-directory-downloader/esm'; import { CliExecuter } from './CliCommandExecuter'; import { getPlatform } from '../utils'; import { TerminalCommandExecuter } from './TerminalCommandExecuter'; import { execSync } from 'child_process'; +import { PnPWebview } from '../webview/PnPWebview'; +import { Executer } from './CommandExecuter'; import { TeamsToolkitIntegration } from './TeamsToolkitIntegration'; export const PROJECT_FILE = 'project.pnp'; -interface NameValue { - name: string; - value: string; -} - export class Scaffolder { public static registerCommands() { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.createProject, Scaffolder.createProject) + commands.registerCommand(Commands.createProject, Scaffolder.showCreateProjectForm) ); subscriptions.push( - commands.registerCommand(Commands.addToProject, Scaffolder.addProject) + commands.registerCommand(Commands.addToProject, Scaffolder.showAddProjectForm) ); } - /** - * Create a new project - * @returns - */ - public static async createProject() { - Logger.info('Start creating a new project'); - - const folderPath = await Scaffolder.getFolderPath(); - if (!folderPath) { - Notifications.warning('You must select the parent folder to create the project in'); - return; - } - - const solutionName = await Scaffolder.getSolutionName(folderPath); - if (!solutionName) { - Logger.warning('Cancelled solution name input'); - return; - } + public static async createProject(input: SpfxScaffoldCommandInput) { + Scaffolder.scaffold(input, true); + } - Scaffolder.addProject(solutionName, folderPath); + public static async addComponentToProject(input: SpfxAddComponentCommandInput) { + Scaffolder.scaffold(input, false); } - /** - * Start from a sample - * @param sample - */ public static async useSample(sample: Sample) { Logger.info(`Start using sample ${sample.name}`); @@ -115,159 +92,60 @@ export class Scaffolder { }); } - /** - * Create project file and open it in VS Code - * @param folderPath - * @param content - */ - private static async createProjectFileAndOpen(folderPath: string, content: any) { - writeFileSync(join(folderPath, PROJECT_FILE), content, { encoding: 'utf8' }); - - if (getPlatform() === 'windows') { - await commands.executeCommand('vscode.openFolder', Uri.file(parseWinPath(folderPath))); - } else { - await commands.executeCommand('vscode.openFolder', Uri.parse(folderPath)); - } - } - - /** - * Get the name of the solution to create - * @returns - */ - private static async getSolutionName(folderPath: string): Promise { - return await window.showInputBox({ - title: 'What is your solution name?', - placeHolder: 'Enter your solution name', - ignoreFocusOut: true, - validateInput: (value) => { - if (!value) { - return 'Solution name is required'; - } - - const solutionPath = join(folderPath, value); - if (existsSync(solutionPath)) { - return `Folder with '${value}' already exists`; - } - - return undefined; - } + public static async pickFolder() { + const folder = await window.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + openLabel: 'Select', + title: 'Select the parent folder where you want to create the project', }); + if (folder?.length) { + PnPWebview.postMessage(WebviewCommand.toWebview.folderPath, folder[0].fsPath); + } } - /** - * Select the path to create the project in - * @returns - */ - private static async getFolderPath(): Promise { - const wsFolder = await Folders.getWorkspaceFolder(); - const folderOptions: QuickPickItem[] = [{ - label: '$(folder) Browse...', - alwaysShow: true, - description: 'Browse for the parent folder to create the project in' - }]; - - if (wsFolder) { - folderOptions.push({ - label: `\$(folder-active) ${wsFolder.name}`, - description: wsFolder.uri.fsPath - }); + public static validateSolutionName(folderPath: string, solutionNameInput: string) { + if (existsSync(join(folderPath, solutionNameInput))) { + PnPWebview.postMessage(WebviewCommand.toWebview.validateSolutionName, false); + return; } - const folderPath = await window.showQuickPick(folderOptions, { - canPickMany: false, - ignoreFocusOut: true, - title: 'Select the parent folder to create the project in' - }).then(async (selectedFolder) => { - if (selectedFolder?.label === '$(folder) Browse...') { - const folder = await window.showOpenDialog({ - canSelectFolders: true, - canSelectFiles: false, - canSelectMany: false, - openLabel: 'Select', - title: 'Select the parent folder where you want to create the project', - }); - if (folder?.length) { - return folder[0].fsPath; - } - return undefined; - } - - return selectedFolder?.description; - }); - - return folderPath; + PnPWebview.postMessage(WebviewCommand.toWebview.validateSolutionName, true); } - /** - * Add a new component to the project - */ - private static async addProject(solutionName?: string | undefined, folderPath?: string | undefined) { - const componentTypeChoice = await window.showQuickPick(ComponentTypes.map(ct => ct.name), { - title: 'Which type of client-side component to create?', - ignoreFocusOut: true, - canPickMany: false - }); - - if (!componentTypeChoice) { - Logger.warning('Cancelled client-side component input'); + public static async validateComponentName(componentType: ComponentType, componentNameInput: string) { + if (await Scaffolder.componentFolderExists(componentType, componentNameInput)) { + PnPWebview.postMessage(WebviewCommand.toWebview.validateComponentName, false); return; } - const componentType = ComponentTypes.find(ct => ct.name === componentTypeChoice); + PnPWebview.postMessage(WebviewCommand.toWebview.validateComponentName, true); + } - if (!componentType) { - Logger.error(`Unknown component type: ${componentTypeChoice}`); - return; - } + private static async scaffold(input: SpfxScaffoldCommandInput | SpfxAddComponentCommandInput, isNewProject: boolean) { + Logger.info('Start creating a new project'); let yoCommand = ''; - const yoCommandSolutionName = solutionName ? ` --solution-name "${solutionName}"` : ''; + const yoCommandSolutionName = isNewProject ? ` --solution-name "${(input as SpfxScaffoldCommandInput).solutionName}"` : ''; // Ask questions per component type - if (componentType.value === ComponentType.adaptiveCardExtension) { - const componentAnswers = await Scaffolder.aceComponent(); - if (!componentAnswers) { - return; - } - - const { aceTemplateType, componentName } = componentAnswers; - - yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${componentType.value} --aceTemplateType ${aceTemplateType?.value} --component-name "${componentName}" --skip-install`; - } else if (componentType.value === ComponentType.extension) { - const componentAnswers = await Scaffolder.extensionComponent(); - if (!componentAnswers) { - return; - } - - const { componentName, extensionType, framework } = componentAnswers; + if (input.componentType === ComponentType.adaptiveCardExtension) { + yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${input.componentType} --aceTemplateType ${input.aceType} --component-name "${input.componentName}" --skip-install`; + } else if (input.componentType === ComponentType.extension) { + yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${input.componentType} --extension-type ${input.extensionType} --component-name "${input.componentName}" --skip-install`; - yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${componentType.value} --extension-type ${extensionType} --component-name "${componentName}" --skip-install`; - - if (framework) { - yoCommand += ` --framework ${framework}`; + if (input.frameworkType) { + yoCommand += ` --framework ${input.frameworkType}`; } else { // To prevent the 'templates/react' scandir issue yoCommand += ' --template ""'; } - } else if (componentType.value === ComponentType.webPart) { - const componentAnswers = await Scaffolder.webpartComponent(); - if (!componentAnswers) { - return; - } - - const { componentName, framework } = componentAnswers; - - yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${componentType.value} --component-name "${componentName}" --framework ${framework} --skip-install`; - } else if (componentType.value === ComponentType.library) { - const componentAnswers = await Scaffolder.libraryComponent(); - if (!componentAnswers) { - return; - } - - const { componentName } = componentAnswers; - - yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${componentType.value} --component-name "${componentName}" --skip-install`; + } else if (input.componentType === ComponentType.webPart) { + yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${input.componentType} --component-name "${input.componentName}" --framework ${input.frameworkType} --skip-install`; + } else if (input.componentType === ComponentType.library) { + yoCommand = `yo @microsoft/sharepoint ${yoCommandSolutionName} --component-type ${input.componentType} --component-name "${input.componentName}" --skip-install`; } if (!yoCommand) { @@ -282,6 +160,7 @@ export class Scaffolder { cancellable: false }, async () => { try { + let folderPath = isNewProject ? (input as SpfxScaffoldCommandInput).folderPath : ''; if (!folderPath) { const wsFolder = await Folders.getWorkspaceFolder(); let path = wsFolder?.uri.fsPath; @@ -299,9 +178,12 @@ export class Scaffolder { return; } - if (solutionName) { - const newFolderPath = join(folderPath, solutionName!); + if (isNewProject) { + const newSolutionInput = input as SpfxScaffoldCommandInput; + const newFolderPath = join(newSolutionInput.folderPath, newSolutionInput.solutionName!); Scaffolder.createProjectFileAndOpen(newFolderPath, 'init'); + } else { + PnPWebview.close(); } Notifications.info('Component successfully created.'); @@ -312,201 +194,97 @@ export class Scaffolder { }); } - /** - * Questions to create a new ACE component - * @returns - */ - private static async aceComponent(): Promise<{ aceTemplateType: NameValue, componentName: string } | undefined> { - const output = execSync('node --version', { shell: TerminalCommandExecuter.shell }); - const match = /v(?\d+)\.(?\d+)\.(?\d+)/gm.exec(output.toString()); - const nodeVersion = null === match ? '18' : match.groups?.major_version!; - const adaptiveCardTypes = nodeVersion === '16' ? AdaptiveCardTypesNode16 : AdaptiveCardTypesNode18; - - const aceTemplateTypeChoice = await window.showQuickPick(adaptiveCardTypes.map(ace => ace.name), { - title: 'Which adaptive card extension template do you want to use?', - ignoreFocusOut: true, - canPickMany: false - }); + private static async getFolderPath(): Promise { + const wsFolder = await Folders.getWorkspaceFolder(); + const folderOptions: QuickPickItem[] = [{ + label: '$(folder) Browse...', + alwaysShow: true, + description: 'Browse for the parent folder to create the project in' + }]; - if (!aceTemplateTypeChoice) { - Logger.warning('Cancelled ACE template input'); - return; + if (wsFolder) { + folderOptions.push({ + label: `\$(folder-active) ${wsFolder.name}`, + description: wsFolder.uri.fsPath + }); } - const aceTemplateType = adaptiveCardTypes.find(ace => ace.name === aceTemplateTypeChoice); - - const componentName = await window.showInputBox({ - title: 'What is your Adaptive Card Extension name?', - value: 'HelloWorld', + const folderPath = await window.showQuickPick(folderOptions, { + canPickMany: false, ignoreFocusOut: true, - validateInput: async (value) => { - if (!value) { - return 'Component name is required'; - } - - if (await Scaffolder.componentFolderExists(ComponentType.adaptiveCardExtension, value)) { - return 'Component name already exists'; + title: 'Select the parent folder to create the project in' + }).then(async (selectedFolder) => { + if (selectedFolder?.label === '$(folder) Browse...') { + const folder = await window.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + openLabel: 'Select', + title: 'Select the parent folder where you want to create the project', + }); + if (folder?.length) { + return folder[0].fsPath; } - return undefined; } - }); - if (!componentName) { - Logger.warning('Cancelled component name input'); - return; - } + return selectedFolder?.description; + }); - return { - aceTemplateType: aceTemplateType as NameValue, - componentName - }; + return folderPath; } - /** - * Questions to create a new library component - * @returns - */ - private static async libraryComponent(): Promise<{ componentName: string } | undefined> { - const componentName = await window.showInputBox({ - title: 'What is your library name?', - value: 'HelloWorld', - ignoreFocusOut: true, - validateInput: async (value) => { - if (!value) { - return 'Component name is required'; - } - - if (await Scaffolder.componentFolderExists(ComponentType.library, value)) { - return 'Component name already exists'; - } - - return undefined; - } + private static async showCreateProjectForm() { + PnPWebview.open(WebViewType.scaffoldForm, { + isNewProject: true, + nodeVersion: Scaffolder.getNodeVersion() }); - - if (!componentName) { - Logger.warning('Cancelled component name input'); - return; - } - - return { - componentName - }; } - /** - * Questions to create a new web part component - * @returns - */ - private static async webpartComponent(): Promise<{ componentName: string, framework: string } | undefined> { - const componentName = await window.showInputBox({ - title: 'What is your web part name?', - value: 'HelloWorld', - ignoreFocusOut: true, - validateInput: async (value) => { - if (!value) { - return 'Component name is required'; - } - - if (await Scaffolder.componentFolderExists(ComponentType.webPart, value)) { - return 'Component name already exists'; - } - - return undefined; - } + private static async showAddProjectForm() { + PnPWebview.open(WebViewType.scaffoldForm, { + isNewProject: false, + nodeVersion: Scaffolder.getNodeVersion() }); + } - if (!componentName) { - Logger.warning('Cancelled component name input'); - return; - } + private static getNodeVersion(): string { + const output = execSync('node --version', { shell: TerminalCommandExecuter.shell }); + const match = /v(?\d+)\.(?\d+)\.(?\d+)/gm.exec(output.toString()); + const nodeVersion = null === match ? '18' : match.groups?.major_version!; + return nodeVersion; + } - const frameworkChoice = await window.showQuickPick(FrameworkTypes.map(type => type.name), { - title: 'Which template would you like to use?', - ignoreFocusOut: true, - canPickMany: false - }); + private static async createProjectFileAndOpen(folderPath: string, content: any) { + writeFileSync(join(folderPath, PROJECT_FILE), content, { encoding: 'utf8' }); - if (!frameworkChoice) { - Logger.warning('Cancelled template input'); - return; + if (getPlatform() === 'windows') { + await commands.executeCommand('vscode.openFolder', Uri.file(parseWinPath(folderPath))); + } else { + await commands.executeCommand('vscode.openFolder', Uri.parse(folderPath)); } - - const framework = FrameworkTypes.find(type => type.name === frameworkChoice); - - return { - componentName, - framework: framework?.value as string - }; } - /** - * Questions to create a new extension component - * @returns - */ - private static async extensionComponent(): Promise<{ componentName: string, extensionType: string, framework: string | undefined } | undefined> { - const componentName = await window.showInputBox({ - title: 'What is your extension name?', - value: 'HelloWorld', + private static async getSolutionName(folderPath: string): Promise { + return await window.showInputBox({ + title: 'What is your solution name?', + placeHolder: 'Enter your solution name', ignoreFocusOut: true, - validateInput: async (value) => { + validateInput: (value) => { if (!value) { - return 'Component name is required'; + return 'Solution name is required'; } - if (await Scaffolder.componentFolderExists(ComponentType.extension, value)) { - return 'Component name already exists'; + const solutionPath = join(folderPath, value); + if (existsSync(solutionPath)) { + return `Folder with '${value}' already exists`; } return undefined; } }); - - if (!componentName) { - Logger.warning('Cancelled component name input'); - return; - } - - const extensionChoice = await window.showQuickPick(ExtensionTypes.map(type => type.name), { - title: 'Which extension type would you like to create?', - ignoreFocusOut: true, - canPickMany: false - }); - - if (!extensionChoice) { - Logger.warning('Cancelled extension type input'); - return; - } - - const extension = ExtensionTypes.find(type => type.name === extensionChoice); - - let framework: string | undefined = undefined; - if (extension && extension.templates.length > 0) { - const frameworkChoice = await window.showQuickPick(extension.templates, { - title: 'Which template would you like to use?', - ignoreFocusOut: true, - canPickMany: false - }); - - if (!frameworkChoice) { - Logger.warning('Cancelled template input'); - return; - } - - framework = frameworkChoice; - } - - return { - componentName, - extensionType: extension?.value as string, - framework - }; } - /** - * Check if a component folder exists - */ private static async componentFolderExists(type: ComponentType, value: string) { let componentFolder = ''; switch (type) { diff --git a/src/webview/PnPWebview.ts b/src/webview/PnPWebview.ts index 5d77135..f8dc0e5 100644 --- a/src/webview/PnPWebview.ts +++ b/src/webview/PnPWebview.ts @@ -56,6 +56,14 @@ export class PnPWebview { messageData.appCatalogUrls = data.appCatalogUrls; } + if (data && data.isNewProject !== undefined) { + messageData.isNewProject = data.isNewProject; + } + + if (data && data.nodeVersion) { + messageData.nodeVersion = data.nodeVersion; + } + PnPWebview.postMessage(WebviewCommand.toWebview.viewType, messageData); } } @@ -95,6 +103,14 @@ export class PnPWebview { webViewData.appCatalogUrls = data.appCatalogUrls; } + if (data && data.isNewProject !== undefined) { + webViewData.isNewProject = data.isNewProject; + } + + if (data && data.nodeVersion) { + webViewData.nodeVersion = data.nodeVersion; + } + PnPWebview.webview.webview.html = PnPWebview.getWebviewContent(PnPWebview.webview.webview, webViewData); PnPWebview.webview.title = webViewType?.Title as string; @@ -121,6 +137,21 @@ export class PnPWebview { case WebviewCommand.toVSCode.createWorkFlow: CliActions.generateWorkflowForm(payload); break; + case WebviewCommand.toVSCode.pickFolder: + Scaffolder.pickFolder(); + break; + case WebviewCommand.toVSCode.validateSolutionName: + Scaffolder.validateSolutionName(payload.folderPath, payload.solutionNameInput); + break; + case WebviewCommand.toVSCode.createSpfxProject: + Scaffolder.createProject(payload); + break; + case WebviewCommand.toVSCode.validateComponentName: + Scaffolder.validateComponentName(payload.componentType, payload.componentNameInput); + break; + case WebviewCommand.toVSCode.addSpfxComponent: + Scaffolder.addComponentToProject(payload); + break; } }); } diff --git a/src/webview/view/components/App.tsx b/src/webview/view/components/App.tsx index 2e0b3a3..ffafe68 100644 --- a/src/webview/view/components/App.tsx +++ b/src/webview/view/components/App.tsx @@ -7,6 +7,7 @@ import { EventData } from '@estruyf/vscode/dist/models/EventData'; import { WebviewCommand } from '../../../constants'; import { routeEntries } from '..'; import { ScaffoldWorkflowView } from './forms/workflow/ScaffoldWorkflowView'; +import { ScaffoldSpfxProjectView } from './forms/spfxProject/ScaffoldSpfxProjectView'; export interface IAppProps { @@ -43,6 +44,7 @@ export const App: React.FunctionComponent = ({ url, data }: React.Pro } /> } /> } /> + } /> ); }; \ No newline at end of file diff --git a/src/webview/view/components/forms/spfxProject/ScaffoldSpfxProjectView.tsx b/src/webview/view/components/forms/spfxProject/ScaffoldSpfxProjectView.tsx new file mode 100644 index 0000000..b28213e --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/ScaffoldSpfxProjectView.tsx @@ -0,0 +1,301 @@ +import { VSCodeButton, VSCodeDropdown, VSCodeOption, VSCodeProgressRing, VSCodeTextField } from '@vscode/webview-ui-toolkit/react'; +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { AdaptiveCardTypesNode16, AdaptiveCardTypesNode18, ComponentType, ComponentTypes, ExtensionType, ExtensionTypes, FrameworkType, FrameworkTypes, SpfxAddComponentCommandInput, SpfxScaffoldCommandInput, WebviewCommand } from '../../../../../constants'; +import { useLocation } from 'react-router-dom'; +import { AddIcon, FolderIcon } from '../../icons'; +import { Messenger } from '@estruyf/vscode/dist/client'; +import { EventData } from '@estruyf/vscode/dist/models/EventData'; + + +export interface IScaffoldSpfxProjectViewProps { } + +export const ScaffoldSpfxProjectView: React.FunctionComponent = ({ }: React.PropsWithChildren) => { + const [isNewProject, setIsNewProject] = useState(true); + const [nodeVersion, setNodeVersion] = useState('18'); + const [folderPath, setFolderPath] = useState(''); + const [solutionName, setSolutionName] = useState(''); + const [isValidSolutionName, setIsValidSolutionName] = useState(); + const [componentType, setComponentType] = useState(ComponentType.webPart); + const [componentName, setComponentName] = useState(''); + const [isValidComponentName, setIsValidComponentName] = useState(); + const [frameworkType, setFrameworkType] = useState(FrameworkType.none); + const [extensionType, setExtensionType] = useState(ExtensionType.application); + const [aceType, setAceType] = useState(AdaptiveCardTypesNode18[0].value); + const [isFormValid, setIsFormValid] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const location: any = useLocation(); + const componentTypeName = ComponentTypes.find((component) => component.value === componentType)?.name; + + useEffect(() => { + Messenger.listen(messageListener); + + return () => { + Messenger.unlisten(messageListener); + }; + }, []); + + useEffect(() => { + if (location.state.isNewProject !== undefined) { + const isNewProjectBool = typeof (location.state.isNewProject) === 'string' ? (location.state.isNewProject === 'false' ? false : true) : location.state.isNewProject; + setIsNewProject(isNewProjectBool); + } + + if (location.state.nodeVersion) { + setNodeVersion(location.state.nodeVersion); + if (location.state.nodeVersion === '16') { + setAceType(AdaptiveCardTypesNode16[0].value); + } + } + }, [location]); + + useEffect(() => { + if (isNewProject) { + if (!folderPath || !solutionName || !componentName) { + setIsFormValid(false); + return; + } + + if (!isValidSolutionName) { + setIsFormValid(false); + return; + } + } else { + if (!isValidComponentName) { + setIsFormValid(false); + return; + } + } + + setIsFormValid(true); + }, [folderPath, solutionName, isValidSolutionName, componentName, isValidComponentName]); + + const messageListener = (event: MessageEvent>) => { + const { command, payload } = event.data; + + if (command === WebviewCommand.toWebview.folderPath) { + setFolderPath(payload); + if (solutionName) { + Messenger.send(WebviewCommand.toVSCode.validateSolutionName, { + folderPath: payload, + solutionName: solutionName + }); + } + } + + if (command === WebviewCommand.toWebview.validateSolutionName) { + setIsValidSolutionName(payload); + } + + if (command === WebviewCommand.toWebview.validateComponentName) { + setIsValidComponentName(payload); + } + }; + + const pickFolder = () => { + Messenger.send(WebviewCommand.toVSCode.pickFolder, {}); + }; + + const validateSolutionName = (solutionNameInput: string) => { + setSolutionName(solutionNameInput); + if (!solutionNameInput) { + setIsValidSolutionName(null); + return; + } + + Messenger.send(WebviewCommand.toVSCode.validateSolutionName, { folderPath, solutionNameInput }); + }; + + const validateComponentName = (componentNameInput: string) => { + setComponentName(componentNameInput); + if (!componentNameInput) { + setIsValidComponentName(null); + return; + } + + if (isNewProject) { + setIsValidComponentName(true); + return; + } + + Messenger.send(WebviewCommand.toVSCode.validateComponentName, { componentType, componentNameInput }); + }; + + const submit = () => { + setIsSubmitting(true); + if (!isNewProject) { + Messenger.send(WebviewCommand.toVSCode.addSpfxComponent, { + componentType, + componentName, + frameworkType, + extensionType, + aceType + } as SpfxAddComponentCommandInput); + } else { + Messenger.send(WebviewCommand.toVSCode.createSpfxProject, { + folderPath, + solutionName, + componentType, + componentName, + frameworkType, + extensionType, + aceType + } as SpfxScaffoldCommandInput); + } + }; + + return ( +
+
+

+ { + isNewProject + ? 'Create a new SPFx project' + : 'Extend an existing SPFx project with a new component' + } +

+
+
+
+
+ +
+

General information

+
+
+
+ { + isNewProject && + <> +
+ +
+
+ +
+
+ + + Folder + +
+
+
+
+ + validateSolutionName(e.target.value)} /> + { + isValidSolutionName === false && +

The solution name already exists

+ } +
+ + } +
+ + setComponentType(e.target.value)}> + {ComponentTypes.map((component) => {component.name})} + +
+
+
+
+
+ +
+

{componentTypeName} details

+
+
+
+
+ + validateComponentName(e.target.value)} /> + { + isValidComponentName === false && +

The component name already exists

+ } +
+ { + componentType === 'extension' && +
+ + setExtensionType(e.target.value)}> + {ExtensionTypes.map((type) => {type.name})} + +
+ } + { + componentType === ComponentType.adaptiveCardExtension && +
+ + setAceType(e.target.value)}> + {nodeVersion === '16' ? + AdaptiveCardTypesNode16.map((type) => {type.name}) : + AdaptiveCardTypesNode18.map((type) => {type.name}) + } + +
+ } + { + componentType === ComponentType.webPart && +
+ + setFrameworkType(e.target.value)}> + {FrameworkTypes.map((framework) => {framework.name})} + +
+ } + { + componentType === ComponentType.extension && ExtensionTypes.find(e => e.value === extensionType)?.templates.some(t => t) && +
+ + setFrameworkType(e.target.value)}> + {ExtensionTypes.find(e => e.value === extensionType)?.templates.map((framework) => { + const key = FrameworkTypes.find(f => f.name === framework)?.value; + return ({framework}); + } + )} + +
+ } +
+
+
+
+ {!isFormValid ? +

Please provide fill up the required fields with valid values

: + ''} + + + {isNewProject ? 'Create a new SPFx project' : 'Add a new SPFx component'} + +
+
+ + +

Working on it...

+
+
+
+
+ ); +}; diff --git a/src/webview/view/components/icons/AddIcon.tsx b/src/webview/view/components/icons/AddIcon.tsx new file mode 100644 index 0000000..e0580a1 --- /dev/null +++ b/src/webview/view/components/icons/AddIcon.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export interface IAddIconProps {} + +export const AddIcon: React.FunctionComponent = () => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/src/webview/view/components/icons/FolderIcon.tsx b/src/webview/view/components/icons/FolderIcon.tsx new file mode 100644 index 0000000..9b9113a --- /dev/null +++ b/src/webview/view/components/icons/FolderIcon.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export interface IFolderIconProps {} + +export const FolderIcon: React.FunctionComponent = () => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/src/webview/view/components/icons/index.ts b/src/webview/view/components/icons/index.ts index 70c93aa..ff43494 100644 --- a/src/webview/view/components/icons/index.ts +++ b/src/webview/view/components/icons/index.ts @@ -9,4 +9,6 @@ export * from './ClearIcon'; export * from './ListIcon'; export * from './CardIcon'; export * from './RocketIcon'; -export * from './CopyIcon'; \ No newline at end of file +export * from './CopyIcon'; +export * from './AddIcon'; +export * from './FolderIcon'; \ No newline at end of file diff --git a/src/webview/view/index.tsx b/src/webview/view/index.tsx index acd9d07..35a0b64 100644 --- a/src/webview/view/index.tsx +++ b/src/webview/view/index.tsx @@ -9,6 +9,7 @@ import { WebViewType, WebViewTypes } from '../../constants'; export const routeEntries: { [routeKey: string]: string } = { [WebViewType.samplesGallery]: WebViewTypes.find(type => type.value === WebViewType.samplesGallery)?.homePageUrl as string, [WebViewType.workflowForm]: WebViewTypes.find(type => type.value === WebViewType.workflowForm)?.homePageUrl as string, + [WebViewType.scaffoldForm]: WebViewTypes.find(type => type.value === WebViewType.scaffoldForm)?.homePageUrl as string, }; const elm = document.querySelector('#root'); @@ -19,6 +20,8 @@ if (elm) { const spfxPackageName = elm.getAttribute('data-spfxPackageName'); const appCatalogUrls = elm.getAttribute('data-appCatalogUrls'); const type = elm.getAttribute('data-type'); + const isNewProject = elm.getAttribute('data-isNewProject'); + const nodeVersion = elm.getAttribute('data-nodeVersion'); const data: any = {}; if (spfxPackageName) { @@ -29,6 +32,14 @@ if (elm) { data.appCatalogUrls = appCatalogUrls; } + if (isNewProject !== undefined) { + data.isNewProject = isNewProject; + } + + if (nodeVersion) { + data.nodeVersion = nodeVersion; + } + const routeEntry = Object.keys(routeEntries).findIndex(key => key === type); root.render(