diff --git a/.eslintrc.json b/.eslintrc.json index 707762b..2caf91a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,7 +20,6 @@ "no-dupe-keys": "error", "no-duplicate-case": "error", "no-duplicate-imports": "error", - "no-unused-vars": "error", "curly": "error", "eqeqeq": "error", "no-throw-literal": "warn", diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 2edbfda..f2909f1 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -21,7 +21,7 @@ jobs: path: vscode-viva - name: Install the dependencies - run: npm install + run: npm ci working-directory: vscode-viva - name: Checkout cli-microsoft365 @@ -36,7 +36,7 @@ jobs: dir - name: Restore dependencies for cli-microsoft365 - run: npm install + run: npm i working-directory: cli-microsoft365 - name: Build cli-microsoft365 diff --git a/.github/workflows/release-local.yml b/.github/workflows/release-local.yml index b344be9..0c833cc 100644 --- a/.github/workflows/release-local.yml +++ b/.github/workflows/release-local.yml @@ -21,7 +21,7 @@ jobs: path: vscode-viva - name: Install the dependencies - run: npm install + run: npm ci working-directory: vscode-viva - name: Checkout cli-microsoft365 @@ -36,7 +36,7 @@ jobs: dir - name: Restore dependencies for cli-microsoft365 - run: npm install + run: npm i working-directory: cli-microsoft365 - name: Build cli-microsoft365 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20e8410..04c9d98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: path: vscode-viva - name: Install the dependencies - run: npm install + run: npm ci working-directory: vscode-viva - name: Checkout cli-microsoft365 @@ -39,7 +39,7 @@ jobs: dir - name: Restore dependencies for cli-microsoft365 - run: npm install + run: npm i working-directory: cli-microsoft365 - name: Build cli-microsoft365 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1095047..d14f816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log +## [3.3.0] - 2024-06-16 + +- Refactored scaffolding form to smaller components +- Code house keeping +- Added missing ACE template DataVisualization for SPFx 1.19.0 +- Added GitHub Copilot Chat participant for SPFx Toolkit + +## [3.2.0] - 2024-05-12 + +- Added support for SPFx 1.19.0 + +## [3.1.0] - 2024-04-28 + +- Refactor the action naming +- Fixed bug when there's a space in the folder path causing serve + ## [3.0.0] - 2024-03-31 - Added VS Code extension walkthrough diff --git a/README.md b/README.md index 18635a0..a85a16a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@

Capabilities | + Preview features | Architecture | Wiki | Contributing | @@ -33,7 +34,7 @@

![Sample Gallery](./assets/images/start.png) - + ## ⭐ Capabilities The extension provides the following capabilities: @@ -263,6 +264,36 @@ By default, the SharePoint Framework Toolkit will use the Node.js version that i You can use the settings to change which Node.js version manager you want to use. You may choose between `nvm` and `nvs`. If you wish to avoid using a Node.js version manager, you can set the value to `none` +## 🧪 Preview features + +> [!WARNING] +> Features described in this section are considered as an early beta feature. They may change or be removed in a future major or minor release. + +### 1️⃣ SPFx Toolkit GitHub Chat Participant + +![SPFx Toolkit chat](./assets/images/chat-intro.png) + +Now you may use SPFx Toolkit as a chat participant in GitHub Copilot chat extension. Simply, mention @spfx in the chat to ask dedicated questions regarding SharePoint Framework development. + +![SPFx Toolkit chat in action](./assets/images/chat-in-action.gif) + +@spfx is your dedicated AI Copilot that will help you with anything that is needed to develop your SharePoint Framework project. It has predefined commands that are tailored toward a specific activity for which you require guidance. + +![SPFx Toolkit chat commands](./assets/images/chat-commands.png) + +Currently, we support the following commands: +- `/setup` - that is dedicated to providing information on how to setup your local workspace for SharePoint Framework development +- `/new` - that may be used to get guidance on how to create a new solution or find and reuse an existing sample from the PnP SPFx sample gallery +- `/code` - that is fine-tuned to provide help in coding your SharePoint Framework project and provides additional boosters like validating the correctness of your SPFx project, scaffolding a CI/CD workflow, or renaming your project, and many more. + +> [!IMPORTANT] +> In order for this feature to work you need to meet the following requirements: +> - Use the [Visual Studio Code Insiders](https://code.visualstudio.com/insiders/) release +> - Use the pre-release version of the [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) extension +> - Use latest version of [SPFx Toolkit](https://marketplace.visualstudio.com/items?itemName=m365pnp.viva-connections-toolkit) + +[Check out our docs for more details](https://github.com/pnp/vscode-viva/wiki/8.-Preview-features) + ## ⚙️ Architecture SharePoint Framework Toolkit for Visual Studio Code is an abstraction layer on top of the [SPFx](https://aka.ms/spfx) Yeoman generator and [CLI for Microsoft 365](https://pnp.github.io/cli-microsoft365/). diff --git a/assets/images/chat-commands.png b/assets/images/chat-commands.png new file mode 100644 index 0000000..d93097a Binary files /dev/null and b/assets/images/chat-commands.png differ diff --git a/assets/images/chat-in-action.gif b/assets/images/chat-in-action.gif new file mode 100644 index 0000000..7596dc3 Binary files /dev/null and b/assets/images/chat-in-action.gif differ diff --git a/assets/images/chat-intro.png b/assets/images/chat-intro.png new file mode 100644 index 0000000..513fcdc Binary files /dev/null and b/assets/images/chat-intro.png differ diff --git a/package-lock.json b/npm-shrinkwrap.json similarity index 99% rename from package-lock.json rename to npm-shrinkwrap.json index 876732c..c0f1d12 100644 --- a/package-lock.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "viva-connections-toolkit", - "version": "3.2.0", + "version": "3.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "viva-connections-toolkit", - "version": "3.2.0", + "version": "3.3.0", "license": "MIT", "dependencies": { "@pnp/cli-microsoft365": "6.11.0", @@ -24,9 +24,10 @@ "@types/react": "^18.0.8", "@types/react-dom": "^18.0.3", "@types/react-router-dom": "^5.3.3", - "@types/vscode": "^1.65.0", + "@types/vscode": "^1.90.0", "@typescript-eslint/eslint-plugin": "^5.9.1", "@typescript-eslint/parser": "^5.9.1", + "@vscode/prompt-tsx": "^0.2.3-alpha", "@vscode/test-electron": "^2.0.3", "@vscode/webview-ui-toolkit": "^1.0.0", "autoprefixer": "^10.4.5", @@ -57,7 +58,7 @@ "webpack-dev-server": "^4.8.1" }, "engines": { - "vscode": "^1.65.0" + "vscode": "^1.90.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3828,19 +3829,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "extraneous": true }, - "node_modules/@pnp/cli-microsoft365/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "extraneous": true, - "hasInstallScript": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/@pnp/cli-microsoft365/node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7195,9 +7183,10 @@ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" }, "node_modules/@types/vscode": { - "version": "1.84.1", - "dev": true, - "license": "MIT" + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.90.0.tgz", + "integrity": "sha512-oT+ZJL7qHS9Z8bs0+WKf/kQ27qWYR3trsXpq46YDjFqBsMLG4ygGGjPaJ2tyrH0wJzjOEmDyg9PDJBBhWg9pkQ==", + "dev": true }, "node_modules/@types/vscode-webview": { "version": "1.57.0", @@ -7401,6 +7390,12 @@ "version": "1.2.0", "license": "ISC" }, + "node_modules/@vscode/prompt-tsx": { + "version": "0.2.3-alpha", + "resolved": "https://registry.npmjs.org/@vscode/prompt-tsx/-/prompt-tsx-0.2.3-alpha.tgz", + "integrity": "sha512-6u/WX4kjl/spf/jGL+sgI+ELlIFO42GqbiwDWcB7epB55YTQzl543Iwj0+dzu70ofr/kqV48IbGP0EjTuCUzGw==", + "dev": true + }, "node_modules/@vscode/test-electron": { "version": "2.3.6", "dev": true, @@ -13140,7 +13135,7 @@ } }, "node_modules/npm/node_modules/ip": { - "version": "2.0.0", + "version": "2.0.1", "dev": true, "inBundle": true, "license": "MIT" @@ -14277,7 +14272,7 @@ "inBundle": true, "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip": "^2.0.1", "smart-buffer": "^4.2.0" }, "engines": { diff --git a/package.json b/package.json index 860fd58..48b0ae3 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,20 @@ "name": "viva-connections-toolkit", "displayName": "SharePoint Framework Toolkit", "description": "SharePoint Framework Toolkit aims to boost your productivity in developing and managing SharePoint Framework solutions helping at every stage of your development flow, from setting up your development workspace to deploying a solution straight to your tenant without the need to leave VS Code and now even create a CI/CD pipeline to introduce automate deployment of your app. This toolkit is provided by the community.", - "version": "3.2.0", + "version": "3.3.0", "publisher": "m365pnp", "preview": false, "homepage": "https://github.com/pnp/vscode-viva", "icon": "assets/logo-large.png", "engines": { - "vscode": "^1.65.0" + "vscode": "^1.90.0" }, "categories": [ "Snippets", "Extension Packs", - "Other" + "Other", + "AI", + "Chat" ], "repository": { "type": "git", @@ -45,6 +47,29 @@ "license": "MIT", "main": "./dist/extension.js", "contributes": { + "chatParticipants": [ + { + "id": "spfx-toolkit.pnp", + "fullName": "SPFx Toolkit", + "name": "spfx", + "description": "How can I help you with SPFx development?", + "isSticky": true, + "commands": [ + { + "name": "setup", + "description": "Prepare your local workspace for SharePoint Framework development" + }, + { + "name": "new", + "description": "Create a new SharePoint Framework project." + }, + { + "name": "code", + "description": "Let's write some SPFx code" + } + ] + } + ], "walkthroughs": [ { "id": "spfx-toolkit-intro", @@ -64,8 +89,8 @@ "title": "Check and get required dependencies", "description": "Validate if your local workspace is ready for SharePoint Framework development. \n[Check dependencies](command:spfx-toolkit.checkDependencies)", "media": { - "image": "assets/images/validate-dependency.png", - "altText": "Validate dependencies" + "image": "assets/images/validate-dependency.png", + "altText": "Validate dependencies" } }, { @@ -113,7 +138,7 @@ "title": "Continue your learning and get additional tooling", "description": "SharePoint Framework Toolkit is your single point to get to the official Microsoft documentation and learning regarding SharePoint Framework and additional tooling that will help you create apps for Microsoft 365.", "media": { - "image": "assets/images/help-and-feedback.png", + "image": "assets/images/help-and-feedback.png", "altText": "help and feedback section" } }, @@ -122,8 +147,8 @@ "title": "Connect with the Microsoft 365 and Power Platform community", "description": "Stay up to date and connected. \nVisit the [Microsoft 365 and Power Platform community](https://pnp.github.io/) to get more learning materials, guidance, tooling, and engage in community calls. \nJoin the [community Discord server](https://aka.ms/community/discord) to stay connected and help out others. \n Sharing is caring!", "media": { - "image": "assets/images/parker-pnp.png", - "altText": "PnP Parker" + "image": "assets/images/parker-pnp.png", + "altText": "PnP Parker" } } ] @@ -411,9 +436,10 @@ "@types/react": "^18.0.8", "@types/react-dom": "^18.0.3", "@types/react-router-dom": "^5.3.3", - "@types/vscode": "^1.65.0", + "@types/vscode": "^1.90.0", "@typescript-eslint/eslint-plugin": "^5.9.1", "@typescript-eslint/parser": "^5.9.1", + "@vscode/prompt-tsx": "^0.2.3-alpha", "@vscode/test-electron": "^2.0.3", "@vscode/webview-ui-toolkit": "^1.0.0", "autoprefixer": "^10.4.5", @@ -449,4 +475,4 @@ "react-markdown": "^9.0.1", "remark-gfm": "^4.0.0" } -} \ No newline at end of file +} diff --git a/src/chat/PromptHandlers.ts b/src/chat/PromptHandlers.ts new file mode 100644 index 0000000..f28f70f --- /dev/null +++ b/src/chat/PromptHandlers.ts @@ -0,0 +1,113 @@ +import * as vscode from 'vscode'; +import { Commands, promptCodeContext, promptContext, promptNewContext, promptSetupContext } from '../constants'; + +const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: 'copilot', family: 'gpt-4' }; + +export class PromptHandlers { + + public static async handle(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise { + stream.progress(PromptHandlers.getRandomProgressMessage()); + const chatCommand = (request.command && ['setup', 'new', 'code'].indexOf(request.command) > -1) ? request.command : ''; + + const messages = [vscode.LanguageModelChatMessage.Assistant(promptContext)]; + messages.push(vscode.LanguageModelChatMessage.Assistant(PromptHandlers.getChatCommandPrompt(chatCommand))); + messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); + const [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR); + try { + const chatResponse = await model.sendRequest(messages, {}, token); + for await (const fragment of chatResponse.text) { + stream.markdown(fragment); + } + PromptHandlers.getChatCommandButtons(chatCommand).forEach(button => stream.button(button)); + return { metadata: { command: chatCommand } }; + } catch (err) { + if (err instanceof vscode.LanguageModelError) { + if (err.message.includes('off_topic')) { + stream.markdown('...I am sorry, I am not able to help with that. Please try again with a different question.'); + } + } else { + stream.markdown('...It seems that something is not working as expected. Please try again later.'); + } + + return { metadata: { command: '' } }; + } + } + + private static getChatCommandButtons(chatCommand: string) { + switch (chatCommand) { + case 'setup': + return [{ + command: Commands.checkDependencies, + title: vscode.l10n.t('Check if my local workspace is ready'), + }, + { + command: Commands.installDependencies, + title: vscode.l10n.t('Install required dependencies'), + }]; + case 'new': + return [{ + command: Commands.createProject, + title: vscode.l10n.t('Create a new project'), + }, + { + command: Commands.samplesGallery, + title: vscode.l10n.t('View samples'), + }]; + case 'code': + // TODO: make visible only in context of a project + return [{ + command: Commands.upgradeProject, + title: vscode.l10n.t('Get upgrade guidance to latest SPFx version'), + }, + { + command: Commands.validateProject, + title: vscode.l10n.t('Validate your project'), + }, + { + command: Commands.renameProject, + title: vscode.l10n.t('Rename your project'), + }, + { + command: Commands.pipeline, + title: vscode.l10n.t('Create a CI/CD workflow'), + }]; + default: + return []; + } + } + + private static getChatCommandPrompt(chatCommand: string): string { + switch (chatCommand) { + case 'setup': + return promptSetupContext; + case 'new': + return promptNewContext; + case 'code': + return promptCodeContext; + default: + return ''; + } + } + + private static getRandomProgressMessage(): string { + const messages = [ + 'Checking...', + 'Let me think about it...', + 'Reading the docs...', + 'Cracking the code...', + 'Unleashing the algorithms...', + 'Beaming up the data...', + 'Feeding the hamsters...', + 'Charging the flux capacitor...', + 'Warming up the servers...', + 'Consulting with the rubber duck...', + 'Asking the magic 8-ball...', + 'Counting backwards from infinity...', + 'Commencing time travel...', + 'Converting coffee to code...', + 'Adjusting the reality matrix...', + 'Waking up the AI...' + ]; + return messages[Math.floor(Math.random() * messages.length)]; + } +} \ No newline at end of file diff --git a/src/constants/AdaptiveCardTypes.ts b/src/constants/AdaptiveCardTypes.ts index ba94fe4..b2992c3 100644 --- a/src/constants/AdaptiveCardTypes.ts +++ b/src/constants/AdaptiveCardTypes.ts @@ -1,19 +1,4 @@ -export const AdaptiveCardTypesNode16 = [ - { - name: 'Basic Card Template', - value: 'Basic' - }, - { - name: 'Image Card Template', - value: 'Image' - }, - { - name: 'Primary Text Template', - value: 'PrimaryText' - } -]; - -export const AdaptiveCardTypesNode18 = [ +export const AdaptiveCardTypes = [ { name: 'Generic Card Template', value: 'Generic' @@ -21,5 +6,9 @@ export const AdaptiveCardTypesNode18 = [ { name: 'Search Query Modifier', value: 'Search' + }, + { + name: 'Data Visualization Card Template', + value: 'DataVisualization' } ]; \ No newline at end of file diff --git a/src/constants/ComponentTypes.ts b/src/constants/ComponentTypes.ts index 1256f07..87ba79f 100644 --- a/src/constants/ComponentTypes.ts +++ b/src/constants/ComponentTypes.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ // eslint-disable-next-line no-shadow export enum ComponentType { adaptiveCardExtension = 'adaptiveCardExtension', diff --git a/src/constants/ExtensionTypes.ts b/src/constants/ExtensionTypes.ts index c9360fb..429bc54 100644 --- a/src/constants/ExtensionTypes.ts +++ b/src/constants/ExtensionTypes.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ // eslint-disable-next-line no-shadow export enum ExtensionType { application = 'ApplicationCustomizer', diff --git a/src/constants/FrameworkTypes.ts b/src/constants/FrameworkTypes.ts index 8a17f55..b1fa20e 100644 --- a/src/constants/FrameworkTypes.ts +++ b/src/constants/FrameworkTypes.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ // eslint-disable-next-line no-shadow export enum FrameworkType { none = 'none', diff --git a/src/constants/General.ts b/src/constants/General.ts index 654eb19..96036ab 100644 --- a/src/constants/General.ts +++ b/src/constants/General.ts @@ -1 +1,2 @@ -export const EXTENSION_NAME = 'spfx-toolkit'; \ No newline at end of file +export const EXTENSION_NAME = 'spfx-toolkit'; +export const CHAT_PARTICIPANT_NAME = 'spfx-toolkit.pnp'; \ No newline at end of file diff --git a/src/constants/NodeVersionManagers.ts b/src/constants/NodeVersionManagers.ts index 65b0c49..c4209f7 100644 --- a/src/constants/NodeVersionManagers.ts +++ b/src/constants/NodeVersionManagers.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ // eslint-disable-next-line no-shadow export enum NodeVersionManagers { nvm = 'nvm', diff --git a/src/constants/ProjectFileContent.ts b/src/constants/ProjectFileContent.ts index 0f3461a..ba5a945 100644 --- a/src/constants/ProjectFileContent.ts +++ b/src/constants/ProjectFileContent.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ // eslint-disable-next-line no-shadow export enum ProjectFileContent { init = 'init-project', diff --git a/src/constants/Prompts.ts b/src/constants/Prompts.ts new file mode 100644 index 0000000..a5a8d0e --- /dev/null +++ b/src/constants/Prompts.ts @@ -0,0 +1,39 @@ +export const personality = 'You are a kind and helpful assistant named SPFx Toolkit. Your main passion is SharePoint Framework (SPFx) development.'; + +export const aim = 'You will provide support in coding and managing SharePoint Framework (SPFx) solutions.'; + +export const msLearnLink = 'learn.microsoft.com'; +export const msSampleGalleryLink = 'https://adoption.microsoft.com/en-us/sample-solution-gallery/'; +export const msLinks = `${msLearnLink}, ${msSampleGalleryLink}`; + +export const spfxOverviewLink = 'https://learn.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview'; +export const spfxSetupLink = 'https://learn.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment'; +export const spfxWebPartLink = 'https://learn.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/overview-client-side-web-parts'; +export const spfxExtensionLink = 'https://learn.microsoft.com/en-us/sharepoint/dev/spfx/extensions/overview-extensions'; +export const spfxLibraryLink = 'https://learn.microsoft.com/en-us/sharepoint/dev/spfx/library-component-overview'; +export const spfxApiLink = 'https://learn.microsoft.com/en-us/javascript/api/overview/sharepoint?view=sp-typescript-latest'; +export const spfxGuideLink = 'https://learn.microsoft.com/en-us/sharepoint/dev/spfx/enterprise-guidance'; +export const spfxVivaLink = 'https://learn.microsoft.com/en-us/sharepoint/dev/spfx/viva/overview-viva-connections'; +export const spfxLinks = `${spfxOverviewLink}, ${spfxSetupLink}, ${spfxWebPartLink}, ${spfxExtensionLink}, ${spfxLibraryLink}, ${spfxApiLink}, ${spfxGuideLink}, ${spfxVivaLink}`; + +export const pnpCommunityHomePageLink = 'https://pnp.github.io/'; +export const pnpSpfxSamplesLink = 'https://pnp.github.io/sp-dev-fx-webparts/'; +export const pnpCliM365Link = 'https://pnp.github.io/cli-microsoft365/'; +export const pnpPSLink = 'https://pnp.github.io/powershell/'; +export const pnpReactControlsLink = 'https://pnp.github.io/sp-dev-fx-controls-react/'; +export const pnpPropertyPaneControlsLink = 'https://pnp.github.io/sp-dev-fx-property-controls/'; +export const pnpLinks = `${pnpCommunityHomePageLink}, ${pnpSpfxSamplesLink}, ${pnpCliM365Link}, ${pnpPSLink}, ${pnpReactControlsLink}, ${pnpPropertyPaneControlsLink}`; + +export const spfxSnippetsLink = 'https://marketplace.visualstudio.com/items?itemName=eliostruyf.spfx-snippets'; + +export const references = `You will be using the following links as references to documentation: ${msLinks}, ${spfxLinks}, ${pnpLinks}`; + +export const community = `You will promote the Microsoft 365 & Power Platform community: ${pnpCommunityHomePageLink}.`; + +export const promptContext = `${personality}${aim}${community}${references}`; + +export const promptSetupContext = 'You will provide support in setting up your development environment for SharePoint Framework (SPFx) development by suggesting the correct version of Node.js and required dependencies.'; + +export const promptNewContext = `You will provide support in creating a new SharePoint Framework project by suggesting the scaffolding form in SPFx Toolkit VS Code extension or by suggesting one of the samples from the ${pnpSpfxSamplesLink}.`; + +export const promptCodeContext = `You will provide support in writting code for SharePoint Framework (SPFx) solutions by suggesting the correct coding practices or spfx snippets that may be used from ${spfxSnippetsLink}. You will always provide coding sample for a given prompt.`; \ No newline at end of file diff --git a/src/constants/WebViewTypes.ts b/src/constants/WebViewTypes.ts index f657bfd..c64d83c 100644 --- a/src/constants/WebViewTypes.ts +++ b/src/constants/WebViewTypes.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ // eslint-disable-next-line no-shadow export enum WebViewType { samplesGallery = 'samplesGallery', diff --git a/src/constants/WorkflowTypes.ts b/src/constants/WorkflowTypes.ts index 9dc1e2c..c07e6cd 100644 --- a/src/constants/WorkflowTypes.ts +++ b/src/constants/WorkflowTypes.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ // eslint-disable-next-line no-shadow export enum WorkflowType { gitHub = 'GitHub', diff --git a/src/constants/index.ts b/src/constants/index.ts index cb7760c..4f4a48e 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -7,6 +7,7 @@ export * from './FrameworkTypes'; export * from './General'; export * from './NodeVersionManagers'; export * from './ProjectFileContent'; +export * from './Prompts'; export * from './WebviewCommand'; export * from './WebViewTypes'; export * from './WorkflowTypes'; diff --git a/src/extension.ts b/src/extension.ts index 8b26567..d91d49c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ import { PnPWebview } from './webview/PnPWebview'; import { CommandPanel } from './panels/CommandPanel'; -import { workspace, window, ThemeIcon, commands, ExtensionContext } from 'vscode'; +import * as vscode from 'vscode'; +import { workspace, window, ThemeIcon, commands } from 'vscode'; import { PROJECT_FILE, Scaffolder } from './services/Scaffolder'; import { Extension } from './services/Extension'; import { Dependencies } from './services/Dependencies'; @@ -8,10 +9,15 @@ import { unlinkSync, readFileSync } from 'fs'; import { TerminalCommandExecuter } from './services/TerminalCommandExecuter'; import { AuthProvider } from './providers/AuthProvider'; import { CliActions } from './services/CliActions'; -import { ProjectFileContent } from './constants'; +import { PromptHandlers } from './chat/PromptHandlers'; +import { CHAT_PARTICIPANT_NAME, ProjectFileContent } from './constants'; -export async function activate(context: ExtensionContext) { +export async function activate(context: vscode.ExtensionContext) { + + const chatParticipant = vscode.chat.createChatParticipant(CHAT_PARTICIPANT_NAME, PromptHandlers.handle); + chatParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'assets', 'images', 'parker-pnp.png'); + Extension.getInstance(context); TerminalCommandExecuter.register(); diff --git a/src/providers/AuthProvider.ts b/src/providers/AuthProvider.ts index 1999a0c..ad66c80 100644 --- a/src/providers/AuthProvider.ts +++ b/src/providers/AuthProvider.ts @@ -19,7 +19,6 @@ export class M365AuthenticationSession implements AuthenticationSession { // Required for the session, but not for M365 CLI public readonly accessToken: string = ''; - // eslint-disable-next-line no-unused-vars constructor(public readonly account: AuthenticationSessionAccountInformation) { } } @@ -31,7 +30,7 @@ export class AuthProvider implements AuthenticationProvider, Disposable { private initializedDisposable: Disposable | undefined; /** - * Register the authentication provider + * Registers the authentication provider and associated commands. */ public static register() { const ext = Extension.getInstance(); @@ -56,60 +55,59 @@ export class AuthProvider implements AuthenticationProvider, Disposable { } /** - * Returns the auth instance - * @returns + * Returns the singleton instance of the AuthProvider class. + * @returns The singleton instance of the AuthProvider class. */ public static getInstance(): AuthProvider { return AuthProvider.instance; } /** - * Verify if the user is logged in + * Verifies the authentication status. + * Calls the `login` method of the `AuthProvider` class with `false` as the argument. */ public static verify() { AuthProvider.login(false); } /** - * Login to M365 - * @param createIfNone - * @returns + * Logs in the user. + * @param createIfNone - A boolean indicating whether to create a new session if none exists. */ public static async login(createIfNone: boolean = true) { await authentication.getSession(AuthProvider.id, [], { createIfNone }); } /** - * Logout from M365 + * Logs out the user by removing the session. */ public static async logout() { AuthProvider.instance.removeSession(''); } /** - * Event emitter for session changes + * Event that fires when the authentication sessions change. */ public get onDidChangeSessions(): Event { return this.onDidChangeEventEmit.event; } /** - * Get the current session - * @param scopes - * @returns + * Retrieves the authentication sessions for the specified scopes. + * If no scopes are provided, retrieves all authentication sessions. + * @param scopes - The scopes for which to retrieve authentication sessions. + * @returns A promise that resolves to an array of authentication sessions. */ - // eslint-disable-next-line no-unused-vars public async getSessions(scopes?: readonly string[]): Promise { const account = await this.getAccount(); return account ? [account] : []; } /** - * Create a new session - * @param _scopes - * @returns + * Creates a session for authentication. + * @param _scopes - The scopes for the session. + * @returns A promise that resolves to an AuthenticationSession. */ - // eslint-disable-next-line no-unused-vars public async createSession(_scopes: string[]): Promise { return new Promise((resolve) => { window.withProgress({ @@ -157,11 +155,10 @@ export class AuthProvider implements AuthenticationProvider, Disposable { } /** - * Remove a session - * @param _sessionId - * @returns + * Removes a session with the specified session ID. + * @param _sessionId - The ID of the session to remove. + * @returns A Promise that resolves when the session is successfully removed. */ - // eslint-disable-next-line no-unused-vars public async removeSession(_sessionId: string): Promise { const output = await executeCommand('logout', { output: 'text' }); @@ -183,8 +180,12 @@ export class AuthProvider implements AuthenticationProvider, Disposable { } /** - * Get the account that is currently signed in - * @returns + * Retrieves the M365 authentication session for the current account. + * If the account is not available, it tries to fetch the account information using the 'status' command. + * If successful, it returns a new M365AuthenticationSession object with the account details. + * If unsuccessful, it logs an error message and returns undefined. + * If the account is already available, it returns a new M365AuthenticationSession object with the account details. + * @returns A Promise that resolves to an M365AuthenticationSession object or undefined. */ public async getAccount(): Promise { if (!EnvironmentInformation.account) { diff --git a/src/services/AdaptiveCardCheck.ts b/src/services/AdaptiveCardCheck.ts index db7a374..877d053 100644 --- a/src/services/AdaptiveCardCheck.ts +++ b/src/services/AdaptiveCardCheck.ts @@ -5,8 +5,11 @@ import { parseYoRc } from '../utils/parseYoRc'; export class AdaptiveCardCheck { + /** - * Check if yo-rc has ACE component + * Validates the ACE (Adaptive Card Extension) component. + * If the required extension is not installed, prompts the user to install it. + * @returns A promise that resolves when the validation is complete. */ public static async validateACEComponent() { try { diff --git a/src/services/CertificateActions.ts b/src/services/CertificateActions.ts index 60106f8..6f347eb 100644 --- a/src/services/CertificateActions.ts +++ b/src/services/CertificateActions.ts @@ -7,6 +7,11 @@ import { Uri, workspace } from 'vscode'; export class CertificateActions { + /** + * Generates a certificate and saves it as a PFX file in the workspace. + * @param certPassword - The password to protect the generated certificate. + * @returns A base64-encoded string representation of the generated PFX file. + */ public static async generateCertificate(certPassword: string): Promise { try { const keys = pki.rsa.generateKeyPair(2048); diff --git a/src/services/CliActions.ts b/src/services/CliActions.ts index 8b7608a..2258b8c 100644 --- a/src/services/CliActions.ts +++ b/src/services/CliActions.ts @@ -46,6 +46,10 @@ export class CliActions { ); } + /** + * Retrieves the URLs of the app catalogs in the environment. + * @returns A promise that resolves to an array of app catalog URLs, or undefined if no app catalogs are found. + */ public static async appCatalogUrlsGet(): Promise { const appCatalogUrls: string[] = []; const tenantAppCatalog = (await CliExecuter.execute('spo tenant appcatalogurl get', 'json')).stdout || undefined; @@ -64,6 +68,12 @@ export class CliActions { return EnvironmentInformation.appCatalogUrls; } + /** + * Retrieves the tenant-wide extensions from the specified tenant app catalog URL. + * @param tenantAppCatalogUrl The URL of the tenant app catalog. + * @returns A promise that resolves to an array of objects containing the URL and title of each tenant-wide extension, + * or undefined if no extensions are found. + */ public static async getTenantWideExtensions(tenantAppCatalogUrl: string): Promise<{Url: string, Title: string}[] | undefined> { const origin = new URL(tenantAppCatalogUrl).origin; const commandOptions: any = { @@ -86,6 +96,11 @@ export class CliActions { return tenantWideExtensionList; } + /** + * Retrieves the health information of the tenant services. + * @returns A promise that resolves to an array of objects containing the title and URL of the health information. + * Returns undefined if there is no health information available. + */ public static async getTenantHealthInfo(): Promise<{Title: string, Url: string}[] | undefined> { const healthInfo = (await CliExecuter.execute('tenant serviceannouncement health list', 'json')).stdout || undefined; @@ -103,6 +118,11 @@ export class CliActions { return healthInfoList; } + /** + * Generates a workflow form based on the provided input. + * @param input - The input for generating the workflow form. + * @returns A Promise that resolves when the workflow form generation is complete. + */ public static async generateWorkflowForm(input: GenerateWorkflowCommandInput) { // Change the current working directory to the root of the Project const wsFolder = await Folders.getWorkspaceFolder(); @@ -132,7 +152,6 @@ export class CliActions { location: ProgressLocation.Notification, title: 'Creating app registration...', cancellable: true - // eslint-disable-next-line no-unused-vars }, async (progress: Progress<{ message?: string; increment?: number }>) => { try { const commandOptions: any = {}; @@ -171,7 +190,6 @@ export class CliActions { location: ProgressLocation.Notification, title: `Generating ${input.workflowType === WorkflowType.gitHub ? 'GitHub Workflow' : 'Azure DevOps Pipeline'}...`, cancellable: true - // eslint-disable-next-line no-unused-vars }, async (progress: Progress<{ message?: string; increment?: number }>) => { try { const commandOptions: any = {}; @@ -224,6 +242,10 @@ export class CliActions { }); } + /** + * Upgrades the project by generating the upgrade steps and displaying them in a Markdown preview. + * @private + */ private static async upgrade() { // Change the current working directory to the root of the Project const wsFolder = await Folders.getWorkspaceFolder(); @@ -241,7 +263,6 @@ export class CliActions { location: ProgressLocation.Notification, title: 'Generating the upgrade steps...', cancellable: true - // eslint-disable-next-line no-unused-vars }, async (progress: Progress<{ message?: string; increment?: number }>) => { try { const result = await CliExecuter.execute('spfx project upgrade', 'md'); @@ -267,6 +288,10 @@ export class CliActions { }); } + /** + * Renames the current project. + * @returns A promise that resolves when the project is renamed. + */ private static async renameProject() { // Change the current working directory to the root of the Project const wsFolder = await Folders.getWorkspaceFolder(); @@ -309,7 +334,6 @@ export class CliActions { location: ProgressLocation.Notification, title: 'Renaming the current project...', cancellable: true - // eslint-disable-next-line no-unused-vars }, async (progress: Progress<{ message?: string; increment?: number }>) => { try { let result: CommandOutput; @@ -329,6 +353,14 @@ export class CliActions { }); } + /** + * Grants API permissions for the current project. + * This method changes the current working directory to the root of the project, + * and then executes the command to grant API permissions. + * If the project is a Teams Toolkit project, the source directory is set to 'src'. + * Displays progress notifications during the execution. + * @returns A promise that resolves when the API permissions are granted. + */ private static async grantAPIPermissions() { // Change the current working directory to the root of the Project const wsFolder = await Folders.getWorkspaceFolder(); @@ -346,7 +378,6 @@ export class CliActions { location: ProgressLocation.Notification, title: 'Granting API permissions for the current project...', cancellable: true - // eslint-disable-next-line no-unused-vars }, async (progress: Progress<{ message?: string; increment?: number }>) => { try { await CliExecuter.execute('spfx project permissions grant', 'json'); @@ -363,6 +394,11 @@ export class CliActions { }); } + /** + * Displays the generate workflow form in a PnPWebview. + * Retrieves the necessary data and opens the webview with the data. + * @returns A promise that resolves when the form is displayed. + */ private static async showGenerateWorkflowForm() { const content = await parseYoRc(); const data = { @@ -373,6 +409,13 @@ export class CliActions { PnPWebview.open(WebViewType.workflowForm, data); } + /** + * Validates the current project. + * This method changes the current working directory to the root of the project and performs + * validation on the project. If the project is a Teams Toolkit project, it changes the working + * directory to the 'src' folder before performing validation. + * @returns A promise that resolves when the validation is complete. + */ private static async validateProject() { // Change the current working directory to the root of the Project const wsFolder = await Folders.getWorkspaceFolder(); @@ -390,7 +433,6 @@ export class CliActions { location: ProgressLocation.Notification, title: 'Validating the current project...', cancellable: true - // eslint-disable-next-line no-unused-vars }, async (progress: Progress<{ message?: string; increment?: number }>) => { try { const result = await CliExecuter.execute('spfx project doctor', 'md'); @@ -416,6 +458,10 @@ export class CliActions { }); } + /** + * Deploys a project. + * @param file The file to deploy. If not provided, the method will search for .sppkg files in the workspace and prompt the user to select one. + */ private static async deploy(file: Uri | undefined) { const authInstance = AuthProvider.getInstance(); const account = await authInstance.getAccount(); @@ -481,7 +527,6 @@ export class CliActions { location: ProgressLocation.Notification, title: `Deploying the ${basename(file.fsPath)} project. Check [output window](command:${Commands.showOutputChannel}) to follow the progress.`, cancellable: false - // eslint-disable-next-line no-unused-vars }, async (progress: Progress<{ message?: string; increment?: number }>) => { try { const addResult = await CliExecuter.execute('spo app add', 'json', { filePath: file?.fsPath, appCatalogUrl: appCatalogUrl, appCatalogScope: appCatalogScope, overwrite: true }); @@ -521,6 +566,10 @@ export class CliActions { }); } + /** + * Serves the project by executing the specified configuration using Gulp. + * Prompts the user to select a configuration from the serve.json file. + */ public static async serveProject() { const wsFolder = Folders.getWorkspaceFolder(); if (!wsFolder) { diff --git a/src/services/CliCommandExecuter.ts b/src/services/CliCommandExecuter.ts index 1273ac1..5a550b7 100644 --- a/src/services/CliCommandExecuter.ts +++ b/src/services/CliCommandExecuter.ts @@ -5,22 +5,24 @@ import { CommandOutput, executeCommand } from '@pnp/cli-microsoft365'; export class CliExecuter { /** - * Execute the command - * @param command - * @param args - * @returns + * Executes a CLI command and returns the command output. + * @param command - The CLI command to execute. + * @param output - The desired output format of the command. Defaults to 'text'. + * @param args - Additional arguments to pass to the command. + * @returns A promise that resolves to the command output. */ public static async execute(command: string, output: string | undefined = 'text', args?: any): Promise { return await CliExecuter.tryExecuteCommand(command, output, args); } /** - * Try to execute the command and log the output to the output channel. - * @param command - * @returns + * Executes a CLI command asynchronously and returns the command output. + * @param command The CLI command to execute. + * @param output The type of output to expect from the command (default: 'text'). + * @param args Additional arguments to pass to the command. + * @returns A Promise that resolves to the command output. */ private static async tryExecuteCommand(command: string, output: string | undefined = 'text', args?: any): Promise { - // eslint-disable-next-line no-unused-vars return await new Promise((resolve: (res: CommandOutput) => void, reject: (e: Error) => void): void => { Logger.getInstance(); let cmdOutput: string = ''; diff --git a/src/services/CommandExecuter.ts b/src/services/CommandExecuter.ts index 2d2f7c4..901e6ec 100644 --- a/src/services/CommandExecuter.ts +++ b/src/services/CommandExecuter.ts @@ -7,11 +7,11 @@ import { Logger } from './Logger'; export class Executer { /** - * Execute the command - * @param workingDirectory - * @param command - * @param args - * @returns + * Executes a command in the specified working directory with optional arguments. + * @param workingDirectory The working directory in which to execute the command. + * @param command The command to execute. + * @param args Optional arguments to pass to the command. + * @returns A promise that resolves to the exit code of the command. */ public static async executeCommand(workingDirectory: string, command: string, args: string[] = []): Promise { const result: CommandResult = await Executer.tryExecuteCommand(workingDirectory, command, ...args); @@ -19,14 +19,13 @@ export class Executer { } /** - * Try to execute the command and log the output to the output channel. - * @param workingDirectory - * @param command - * @param args - * @returns + * Executes a command asynchronously and returns a promise that resolves to a CommandResult. + * @param workingDirectory - The working directory for the command execution. + * @param command - The command to execute. + * @param args - The arguments to pass to the command. + * @returns A promise that resolves to a CommandResult. */ private static async tryExecuteCommand(workingDirectory: string | undefined, command: string, ...args: string[]): Promise { - // eslint-disable-next-line no-unused-vars return await new Promise((resolve: (res: CommandResult) => void, reject: (e: Error) => void): void => { Logger.getInstance(); let cmdOutput: string = ''; diff --git a/src/services/DebuggerCheck.ts b/src/services/DebuggerCheck.ts index 4fce999..a29ed3e 100644 --- a/src/services/DebuggerCheck.ts +++ b/src/services/DebuggerCheck.ts @@ -11,8 +11,9 @@ export class DebuggerCheck { private static servePlaceholderUrl = 'https://contoso.sharepoint.com'; /** - * Check if the URL is used in the launch.json file - * @param url + * Validates the provided URL by checking the launch.json and serve.json files. + * If any errors occur during validation, they will be logged using the Logger. + * @param url - The URL to validate. */ public static async validateUrl(url: string) { try { @@ -29,9 +30,8 @@ export class DebuggerCheck { } /** - * Validate the launch.json file - * @param url - * @returns + * Validates the launch configuration and updates the placeholder URL with the provided URL. + * @param url The URL to update the placeholder URL with. */ private static async validateLaunch(url: string) { let launchFiles = await workspace.findFiles('.vscode/launch.json', '**/node_modules/**'); @@ -74,9 +74,10 @@ export class DebuggerCheck { } /** - * Validate the serve.json file - * @param url - * @returns + * Validates the serve configuration by checking if the initialPage and serveConfigurations + * use placeholder URLs and prompts the user to update them with the provided URL. + * If the user chooses to update, the URLs are modified and the serve configuration file is saved. + * @param url - The URL to be used as a replacement for the placeholder URLs. */ private static async validateServe(url: string) { let serveFiles = await workspace.findFiles('config/serve.json', '**/node_modules/**'); diff --git a/src/services/Dependencies.ts b/src/services/Dependencies.ts index 7fa5770..8d36ee1 100644 --- a/src/services/Dependencies.ts +++ b/src/services/Dependencies.ts @@ -25,7 +25,9 @@ export class Dependencies { } /** - * Validate if all the required dependencies are installed + * Validates the dependencies required for SPFx development. + * Checks the Node.js version and npm dependencies. + * Displays notifications for missing or incompatible dependencies. */ public static async validate() { await window.withProgress({ @@ -90,7 +92,7 @@ export class Dependencies { } /** - * Install all the dependencies + * Installs the dependencies by running the npm install command in a terminal. */ public static install() { const terminal = window.createTerminal({ @@ -105,7 +107,8 @@ export class Dependencies { } /** - * Check node version + * Checks if the installed version of Node.js is valid. + * @returns Returns true if the installed version of Node.js is valid, otherwise false. */ private static isValidNodeJs() { try { @@ -145,7 +148,11 @@ export class Dependencies { } /** - * split dependency into name and version + * Splits a dependency string into an array of strings. + * If the dependency starts with '@', it splits the string into two parts: the scope and the package name. + * If the dependency does not start with '@', it splits the string into two parts: the package name and the version. + * @param dependency - The dependency string to split. + * @returns An array of strings containing the split parts of the dependency. */ private static splitDependency(dependency: string): string[] { if (dependency.startsWith('@')) { diff --git a/src/services/Extension.ts b/src/services/Extension.ts index 810cd2a..b87bc74 100644 --- a/src/services/Extension.ts +++ b/src/services/Extension.ts @@ -4,12 +4,13 @@ import { ExtensionContext, ExtensionMode, SecretStorage } from 'vscode'; export class Extension { private static instance: Extension; - // eslint-disable-next-line no-unused-vars private constructor(private ctx: ExtensionContext) {} /** - * Creates the singleton instance for the extension. - * @param ctx + * Gets the instance of the Extension class. + * If an instance doesn't exist, it creates a new one using the provided ExtensionContext. + * @param ctx - The ExtensionContext object. + * @returns The instance of the Extension class. */ public static getInstance(ctx?: ExtensionContext): Extension { if (!Extension.instance && ctx) { @@ -20,49 +21,56 @@ export class Extension { } /** - * Get the name of the extension + * Gets the name of the extension. + * @returns The name of the extension. */ public get name(): string { return this.ctx.extension.packageJSON.name; } /** - * Get the display name of the extension + * Gets the display name of the extension. + * @returns The display name as a string. */ public get displayName(): string { return this.ctx.extension.packageJSON.displayName; } /** - * Returns the extension's version + * Gets the version of the extension. + * @returns The version string. */ public get version(): string { return this.ctx.extension.packageJSON.version; } /** - * Check if the extension is in production/development mode + * Determines whether the extension is running in production mode. + * @returns True if the extension is running in production mode, false otherwise. */ public get isProductionMode(): boolean { return this.ctx.extensionMode === ExtensionMode.Production; } /** - * Get the extension's subscriptions + * Returns the subscriptions of the Extension. + * @returns An array of disposable objects representing the subscriptions. */ public get subscriptions(): { dispose(): any; }[] { return this.ctx.subscriptions; } /** - * Get the extension's secrets + * Gets the secret storage. + * @returns The secret storage. */ public get secrets(): SecretStorage { return this.ctx.secrets; } /** - * Get the extension's path + * Gets the path of the extension. + * @returns The path of the extension. */ public get extensionPath(): string { return this.ctx.extensionPath; diff --git a/src/services/Folders.ts b/src/services/Folders.ts index d469a84..6f61ddc 100644 --- a/src/services/Folders.ts +++ b/src/services/Folders.ts @@ -4,8 +4,9 @@ import { workspace, window, WorkspaceFolder } from 'vscode'; export class Folders { /** - * Retrieve the workspace folder - */ + * Retrieves the workspace folder. + * @returns A promise that resolves to the workspace folder, or undefined if there are no workspace folders or the user cancels the selection. + */ public static async getWorkspaceFolder(): Promise { let folder: WorkspaceFolder | undefined; diff --git a/src/services/Scaffolder.ts b/src/services/Scaffolder.ts index 26fa759..d6faa24 100644 --- a/src/services/Scaffolder.ts +++ b/src/services/Scaffolder.ts @@ -34,14 +34,27 @@ export class Scaffolder { ); } + /** + * Creates a project using the provided input. + * @param input - The input for the project creation. + */ public static async createProject(input: SpfxScaffoldCommandInput) { Scaffolder.scaffold(input, true); } + /** + * Adds a component to the project. + * @param input - The input for the SpfxAddComponentCommand. + */ public static async addComponentToProject(input: SpfxAddComponentCommandInput) { Scaffolder.scaffold(input, false); } + /** + * Uses the provided sample to create a new project. + * @param sample - The sample to use for creating the project. + * @returns A Promise that resolves when the project creation is complete. + */ public static async useSample(sample: Sample) { Logger.info(`Start using sample ${sample.name}`); @@ -92,6 +105,9 @@ export class Scaffolder { }); } + /** + * Displays a dialog to pick a folder and sends the selected folder path to the webview. + */ public static async pickFolder() { const folder = await window.showOpenDialog({ canSelectFolders: true, @@ -105,6 +121,11 @@ export class Scaffolder { } } + /** + * Validates the solution name by checking if a folder with the same name already exists. + * @param folderPath - The path of the folder where the solution will be created. + * @param solutionNameInput - The input solution name to be validated. + */ public static validateSolutionName(folderPath: string, solutionNameInput: string) { if (existsSync(join(folderPath, solutionNameInput))) { PnPWebview.postMessage(WebviewCommand.toWebview.validateSolutionName, false); @@ -114,6 +135,11 @@ export class Scaffolder { PnPWebview.postMessage(WebviewCommand.toWebview.validateSolutionName, true); } + /** + * Validates the component name for a given component type. + * @param componentType - The type of the component. + * @param componentNameInput - The input component name to validate. + */ public static async validateComponentName(componentType: ComponentType, componentNameInput: string) { if (await Scaffolder.componentFolderExists(componentType, componentNameInput)) { PnPWebview.postMessage(WebviewCommand.toWebview.validateComponentName, false); @@ -123,6 +149,12 @@ export class Scaffolder { PnPWebview.postMessage(WebviewCommand.toWebview.validateComponentName, true); } + /** + * Scaffold method for creating a new project. + * @param input - The input for the scaffold command. + * @param isNewProject - A boolean indicating whether it's a new project or not. + * @returns A Promise that resolves when the scaffold process is complete. + */ private static async scaffold(input: SpfxScaffoldCommandInput | SpfxAddComponentCommandInput, isNewProject: boolean) { Logger.info('Start creating a new project'); @@ -208,6 +240,10 @@ export class Scaffolder { }); } + /** + * Retrieves the folder path where the project will be created. + * @returns A Promise that resolves to the selected folder path, or undefined if no folder is selected. + */ private static async getFolderPath(): Promise { const wsFolder = await Folders.getWorkspaceFolder(); const folderOptions: QuickPickItem[] = [{ @@ -248,27 +284,31 @@ export class Scaffolder { return folderPath; } + /** + * Displays the create project form in a webview. + * @returns A promise that resolves when the form is displayed. + */ private static async showCreateProjectForm() { PnPWebview.open(WebViewType.scaffoldForm, { - isNewProject: true, - nodeVersion: Scaffolder.getNodeVersion() + isNewProject: true }); } + /** + * Displays the add project form in a PnPWebview. + * @returns A promise that resolves when the form is displayed. + */ private static async showAddProjectForm() { PnPWebview.open(WebViewType.scaffoldForm, { - isNewProject: false, - nodeVersion: Scaffolder.getNodeVersion() + isNewProject: false }); } - 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; - } - + /** + * Creates a project file with the given content and opens the folder in Visual Studio Code. + * @param folderPath - The path of the folder where the project file will be created. + * @param content - The content of the project file. + */ private static async createProjectFileAndOpen(folderPath: string, content: any) { if (content) { writeFileSync(join(folderPath, PROJECT_FILE), content, { encoding: 'utf8' }); @@ -281,6 +321,11 @@ export class Scaffolder { } } + /** + * Retrieves the solution name from the user. + * @param folderPath - The path of the folder where the solution will be created. + * @returns A promise that resolves to the solution name entered by the user, or undefined if no solution name is provided. + */ private static async getSolutionName(folderPath: string): Promise { return await window.showInputBox({ title: 'What is your solution name?', @@ -301,6 +346,12 @@ export class Scaffolder { }); } + /** + * Checks if a component folder exists in the workspace. + * @param type - The type of the component. + * @param value - The value of the component. + * @returns A boolean indicating whether the component folder exists. + */ private static async componentFolderExists(type: ComponentType, value: string) { let componentFolder = ''; switch (type) { diff --git a/src/services/TerminalCommandExecuter.ts b/src/services/TerminalCommandExecuter.ts index 9687945..287642b 100644 --- a/src/services/TerminalCommandExecuter.ts +++ b/src/services/TerminalCommandExecuter.ts @@ -22,10 +22,20 @@ export class TerminalCommandExecuter { TerminalCommandExecuter.initShellPath(); } + /** + * Gets the shell path. + * @returns The shell path. + */ public static get shell() { return TerminalCommandExecuter.shellPath; } + /** + * Initializes the shell path for executing terminal commands. + * If the shell path is an object with a `path` property, it sets the `shellPath` to that value. + * If the shell path is a string, it sets the `shellPath` to that value. + * If the shell path is undefined, it sets the `shellPath` to undefined. + */ private static initShellPath() { const shell: string | { path: string } | undefined = TerminalCommandExecuter.getShellPath(); @@ -37,8 +47,8 @@ export class TerminalCommandExecuter { } /** - * Retrieve the automation profile for the current platform - * @returns + * Retrieves the shell path or shell setting based on the current platform. + * @returns The shell path or shell setting for the current platform, or undefined if not found. */ private static getShellPath(): string | ShellSetting | undefined { const platform = getPlatform(); @@ -59,12 +69,25 @@ export class TerminalCommandExecuter { return terminalSettings.get(`integrated.shell.${platform}`); } + /** + * Registers the commands for execution. + * @param subscriptions - The array of subscriptions to add the registered command to. + */ private static registerCommands(subscriptions: Subscription[]) { subscriptions.push( commands.registerCommand(Commands.executeTerminalCommand, TerminalCommandExecuter.runCommand) ); } + /** + * Creates a new terminal with the specified name and icon. + * If a terminal with the same name already exists, it returns that terminal instead. + * If the user's settings specify to use a specific node version manager (nvm or nvs), + * it checks for the presence of .nvmrc files and sets the appropriate node version manager command. + * @param name - The name of the terminal. + * @param icon - The path to the icon for the terminal. + * @returns A promise that resolves to the created terminal or the existing terminal with the same name. + */ private static async createTerminal(name?: string, icon?: string): Promise { let terminal = window.terminals.find(t => t.name === name); @@ -94,10 +117,22 @@ export class TerminalCommandExecuter { return terminal; } + /** + * Retrieves the extension settings value for the specified setting. + * If the setting is not found, the default value is returned. + * @param setting - The name of the setting to retrieve. + * @param defaultValue - The default value to return if the setting is not found. + * @returns The value of the setting, or the default value if the setting is not found. + */ private static getExtensionSettings(setting: string, defaultValue: T): T { return workspace.getConfiguration(EXTENSION_NAME).get(setting, defaultValue); } + /** + * Runs a command in the specified terminal. + * @param command - The command to run. + * @param terminal - The terminal in which to run the command. + */ private static async runInTerminal(command: string, terminal?: Terminal | undefined) { if (terminal) { terminal.show(true); @@ -105,7 +140,11 @@ export class TerminalCommandExecuter { } } - // eslint-disable-next-line no-unused-vars + /** + * Runs a command in the terminal. + * @param command - The command to run. + * @param args - The arguments for the command. + */ public static async runCommand(command: string, args: string[]) { const terminal = await TerminalCommandExecuter.createTerminal('Gulp task', 'tasks-list-configure'); diff --git a/src/webview/PnPWebview.ts b/src/webview/PnPWebview.ts index 4ef5c8b..c7cc12c 100644 --- a/src/webview/PnPWebview.ts +++ b/src/webview/PnPWebview.ts @@ -60,10 +60,6 @@ export class PnPWebview { messageData.isNewProject = data.isNewProject; } - if (data && data.nodeVersion) { - messageData.nodeVersion = data.nodeVersion; - } - PnPWebview.postMessage(WebviewCommand.toWebview.viewType, messageData); } } @@ -107,10 +103,6 @@ export class PnPWebview { 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; diff --git a/src/webview/view/components/App.tsx b/src/webview/view/components/App.tsx index ffafe68..daf0da9 100644 --- a/src/webview/view/components/App.tsx +++ b/src/webview/view/components/App.tsx @@ -15,7 +15,6 @@ export interface IAppProps { data: any | null } -// eslint-disable-next-line no-unused-vars export const App: React.FunctionComponent = ({ url, data }: React.PropsWithChildren) => { const navigate = useNavigate(); diff --git a/src/webview/view/components/controls/MultiSelect.tsx b/src/webview/view/components/controls/MultiSelect.tsx index 129d2d9..ad908d1 100644 --- a/src/webview/view/components/controls/MultiSelect.tsx +++ b/src/webview/view/components/controls/MultiSelect.tsx @@ -4,7 +4,6 @@ import { ChevronDownIcon } from '../icons/ChevronDownIcon'; export interface IMultiSelectProps { - // eslint-disable-next-line no-unused-vars onChange: (event: any, option?: IDropdownOption) => void; options: IDropdownOption[]; label?: string; diff --git a/src/webview/view/components/forms/spfxProject/AdditionalStep.tsx b/src/webview/view/components/forms/spfxProject/AdditionalStep.tsx new file mode 100644 index 0000000..9c001cc --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/AdditionalStep.tsx @@ -0,0 +1,56 @@ +import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; +import * as React from 'react'; +import { StepHeader } from './StepHeader'; +import { PackageSelector } from './PackageSelector'; + + +interface AdditionalStepProps { + shouldRunInit: boolean; + setShouldRunInit: (value: boolean) => void; + shouldInstallReusablePropertyPaneControls: boolean; + setShouldInstallReusablePropertyPaneControls: (value: boolean) => void; + shouldInstallReusableReactControls: boolean; + setShouldInstallReusableReactControls: (value: boolean) => void; + shouldInstallPnPJs: boolean; + setShouldInstallPnPJs: (value: boolean) => void; +} + +export const AdditionalStep: React.FunctionComponent = ({ + shouldRunInit, + setShouldRunInit, + shouldInstallReusablePropertyPaneControls, + setShouldInstallReusablePropertyPaneControls, + shouldInstallReusableReactControls, + setShouldInstallReusableReactControls, + shouldInstallPnPJs, + setShouldInstallPnPJs }: React.PropsWithChildren) => { + return ( +
+ +
+
+ + setShouldRunInit(!shouldRunInit)} /> +
+ + + + + +
+
+ ); +}; \ No newline at end of file diff --git a/src/webview/view/components/forms/spfxProject/ComponentDetailsStep.tsx b/src/webview/view/components/forms/spfxProject/ComponentDetailsStep.tsx new file mode 100644 index 0000000..da851bd --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/ComponentDetailsStep.tsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { useEffect, useCallback } from 'react'; +import { VSCodeDropdown, VSCodeOption, VSCodeTextField } from '@vscode/webview-ui-toolkit/react'; +import { ComponentType, ComponentTypes, WebviewCommand, ExtensionType, ExtensionTypes, FrameworkTypes, AdaptiveCardTypes } from '../../../../../constants'; +import { StepHeader } from './StepHeader'; +import { Messenger } from '@estruyf/vscode/dist/client'; + + +interface IComponentDetailsStepProps { + isNewProject: boolean; + componentType: ComponentType; + componentName: string; + isValidComponentName: boolean | null | undefined; + setComponentName: (name: string) => void; + setIsValidComponentName: (value: boolean | null) => void; + frameworkType: string; + setFrameworkType: (type: string) => void; + extensionType: ExtensionType; + setExtensionType: (type: ExtensionType) => void; + aceType: string; + setAceType: (type: string) => void; +} + +export const ComponentDetailsStep: React.FunctionComponent = ({ + isNewProject, + componentType, + componentName, + isValidComponentName, + setComponentName, + setIsValidComponentName, + frameworkType, + setFrameworkType, + extensionType, + setExtensionType, + aceType, + setAceType }: React.PropsWithChildren) => { + const componentTypeName = ComponentTypes.find((component) => component.value === componentType)?.name; + + useEffect(() => { + const messageListener = (event: MessageEvent) => { + const { command, payload } = event.data; + if (command === WebviewCommand.toWebview.validateComponentName) { + setIsValidComponentName(payload); + } + }; + + Messenger.listen(messageListener); + + return () => { + Messenger.unlisten(messageListener); + }; + }, [setIsValidComponentName]); + + + const validateComponentName = useCallback((componentNameInput: string) => { + setComponentName(componentNameInput); + if (!componentNameInput) { + setIsValidComponentName(null); + return; + } + + if (isNewProject) { + setIsValidComponentName(true); + return; + } + + Messenger.send(WebviewCommand.toVSCode.validateComponentName, { componentType, componentNameInput }); + }, [setComponentName, setIsValidComponentName, isNewProject, componentType]); + + return ( +
+ +
+
+ + 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)}> + { + AdaptiveCardTypes.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}); + } + )} + +
+ } +
+
+ ); +}; \ No newline at end of file diff --git a/src/webview/view/components/forms/spfxProject/FormActions.tsx b/src/webview/view/components/forms/spfxProject/FormActions.tsx new file mode 100644 index 0000000..dacb0c6 --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/FormActions.tsx @@ -0,0 +1,50 @@ +import { VSCodeButton, VSCodeProgressRing } from '@vscode/webview-ui-toolkit/react'; +import * as React from 'react'; +import { AddIcon } from '../../icons'; + + +export interface IFormActionsProps { + isFormValid: boolean; + isSubmitting: boolean; + isNewProject: boolean; + submit: () => void; +} + +export const FormActions: React.FunctionComponent = ({ + isFormValid, + isSubmitting, + isNewProject, + submit }: React.PropsWithChildren) => { + return ( +
+ {!isFormValid ? ( +

+ Please 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/forms/spfxProject/FormHeader.tsx b/src/webview/view/components/forms/spfxProject/FormHeader.tsx new file mode 100644 index 0000000..630a277 --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/FormHeader.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + + +export interface IFormHeaderProps { + isNewProject: boolean; +} + +export const FormHeader: React.FunctionComponent = ({ isNewProject }: React.PropsWithChildren) => { + const title = isNewProject ? 'Create a new SPFx project' : 'Extend an existing SPFx project with a new component'; + + return ( +
+

{title}

+
+ ); +}; \ No newline at end of file diff --git a/src/webview/view/components/forms/spfxProject/GeneralInfoStep.tsx b/src/webview/view/components/forms/spfxProject/GeneralInfoStep.tsx new file mode 100644 index 0000000..bb7881f --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/GeneralInfoStep.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import { useEffect, useCallback } from 'react'; +import { VSCodeButton, VSCodeDropdown, VSCodeOption, VSCodeTextField } from '@vscode/webview-ui-toolkit/react'; +import { ComponentType, WebviewCommand } from '../../../../../constants'; +import { FolderIcon } from '../../icons'; +import { StepHeader } from './StepHeader'; +import { Messenger } from '@estruyf/vscode/dist/client'; + + +export interface IGeneralInfoProps { + isNewProject: boolean; + folderPath: string; + setFolderPath: (folderPath: string) => void; + solutionName: string; + setSolutionName: (value: string) => void; + isValidSolutionName: boolean | null | undefined; + setIsValidSolutionName: (value: boolean | null) => void; + setComponentType: (componentType: ComponentType) => void; + componentTypes: { value: string; name: string }[]; +} + +export const GeneralInfoStep: React.FunctionComponent = ({ + isNewProject, + folderPath, + setFolderPath, + solutionName, + setSolutionName, + isValidSolutionName, + setIsValidSolutionName, + setComponentType, + componentTypes }: React.PropsWithChildren) => { + + const pickFolder = useCallback(() => { + Messenger.send(WebviewCommand.toVSCode.pickFolder, {}); + }, []); + + const validateSolutionName = useCallback((solutionNameInput: string) => { + setSolutionName(solutionNameInput); + if (!solutionNameInput) { + setIsValidSolutionName(null); + return; + } + + Messenger.send(WebviewCommand.toVSCode.validateSolutionName, { folderPath, solutionNameInput }); + }, [folderPath, setSolutionName, setIsValidSolutionName]); + + useEffect(() => { + 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); + } + }; + + Messenger.listen(messageListener); + + return () => { + Messenger.unlisten(messageListener); + }; + }, [setFolderPath, setSolutionName]); + + return ( +
+ +
+ { + isNewProject && + <> +
+ +
+
+ +
+
+ + + Folder + +
+
+
+
+ + validateSolutionName(e.target.value)} /> + { + isValidSolutionName === false && +

The solution name already exists

+ } +
+ + } +
+ + setComponentType(e.target.value)}> + {componentTypes.map((component) => {component.name})} + +
+
+
+ ); +}; diff --git a/src/webview/view/components/forms/spfxProject/PackageSelector.tsx b/src/webview/view/components/forms/spfxProject/PackageSelector.tsx new file mode 100644 index 0000000..8e007fa --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/PackageSelector.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { VSCodeButton, VSCodeLink } from '@vscode/webview-ui-toolkit/react'; + + +interface PackageSelectorProps { + value: boolean; + setValue: (value: boolean) => void, + label: string, + link?: string +} + +export const PackageSelector: React.FunctionComponent = ({ + value, + setValue, + label, + link }: React.PropsWithChildren) => { + + return ( +
+ setValue(!value)} appearance={value ? '' : 'secondary'} className={'float-left'}> + Yes + + setValue(!value)} appearance={!value ? '' : 'secondary'} className={'float-left'}> + No + + +
+ ); +}; \ 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 index 423b479..f721286 100644 --- a/src/webview/view/components/forms/spfxProject/ScaffoldSpfxProjectView.tsx +++ b/src/webview/view/components/forms/spfxProject/ScaffoldSpfxProjectView.tsx @@ -1,19 +1,16 @@ -import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeLink, 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, WebviewCommand } from '../../../../../constants'; +import { AdaptiveCardTypes, ComponentType, ComponentTypes, ExtensionType, FrameworkType, 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'; import { SpfxAddComponentCommandInput, SpfxScaffoldCommandInput } from '../../../../../models'; +import { FormHeader, GeneralInfoStep, ComponentDetailsStep, AdditionalStep, FormActions } from '../spfxProject'; 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(); @@ -22,7 +19,7 @@ export const ScaffoldSpfxProjectView: React.FunctionComponent(); const [frameworkType, setFrameworkType] = useState(FrameworkType.none); const [extensionType, setExtensionType] = useState(ExtensionType.application); - const [aceType, setAceType] = useState(AdaptiveCardTypesNode18[0].value); + const [aceType, setAceType] = useState(AdaptiveCardTypes[0].value); const [isFormValid, setIsFormValid] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [shouldRunInit, setShouldRunInit] = useState(true); @@ -30,28 +27,12 @@ export const ScaffoldSpfxProjectView: React.FunctionComponent(false); const [shouldInstallPnPJs, setShouldInstallPnPJs] = 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(() => { @@ -75,57 +56,6 @@ export const ScaffoldSpfxProjectView: React.FunctionComponent>) => { - 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) { @@ -155,208 +85,53 @@ export const ScaffoldSpfxProjectView: React.FunctionComponent -
-

- { - 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}); - } - )} - -
- } -
-
+ + { - isNewProject && -
-
- -
-

Additional steps

-
-
-
-
- - setShouldRunInit(!shouldRunInit)} /> -
-
- setShouldInstallReusablePropertyPaneControls(!shouldInstallReusablePropertyPaneControls)} appearance={shouldInstallReusablePropertyPaneControls ? '' : 'secondary'} className={'float-left'}> - Yes - - setShouldInstallReusablePropertyPaneControls(!shouldInstallReusablePropertyPaneControls)} appearance={!shouldInstallReusablePropertyPaneControls ? '' : 'secondary'} className={'float-left'}> - No - - -
-
- setShouldInstallReusableReactControls(!shouldInstallReusableReactControls)} appearance={shouldInstallReusableReactControls ? '' : 'secondary'} className={'float-left'}> - Yes - - setShouldInstallReusableReactControls(!shouldInstallReusableReactControls)} appearance={!shouldInstallReusableReactControls ? '' : 'secondary'} className={'float-left'}> - No - - -
-
- setShouldInstallPnPJs(!shouldInstallPnPJs)} appearance={shouldInstallPnPJs ? '' : 'secondary'} className={'float-left'}> - Yes - - setShouldInstallPnPJs(!shouldInstallPnPJs)} appearance={!shouldInstallPnPJs ? '' : 'secondary'} className={'float-left'}> - No - - -
-
-
+ isNewProject && + }
-
- {!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/forms/spfxProject/StepHeader.tsx b/src/webview/view/components/forms/spfxProject/StepHeader.tsx new file mode 100644 index 0000000..0acc4ee --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/StepHeader.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + + +export interface IStepHeaderProps { + step: number; + title: string; +} + +export const StepHeader: React.FunctionComponent = ({ step, title }: React.PropsWithChildren) => { + return ( +
+ +
+

{title}

+
+
+ ); +}; \ No newline at end of file diff --git a/src/webview/view/components/forms/spfxProject/index.ts b/src/webview/view/components/forms/spfxProject/index.ts new file mode 100644 index 0000000..aa78913 --- /dev/null +++ b/src/webview/view/components/forms/spfxProject/index.ts @@ -0,0 +1,7 @@ +export * from './AdditionalStep'; +export * from './ComponentDetailsStep'; +export * from './FormActions'; +export * from './FormHeader'; +export * from './GeneralInfoStep'; +export * from './PackageSelector'; +export * from './StepHeader'; \ No newline at end of file diff --git a/src/webview/view/components/gallery/SearchBar.tsx b/src/webview/view/components/gallery/SearchBar.tsx index 7bde3af..6a7617d 100644 --- a/src/webview/view/components/gallery/SearchBar.tsx +++ b/src/webview/view/components/gallery/SearchBar.tsx @@ -7,13 +7,9 @@ import { MultiSelect } from '../controls'; export interface ISearchBarProps { - // eslint-disable-next-line no-unused-vars onSearchTextboxChange: (event: any) => void; - // eslint-disable-next-line no-unused-vars onFilterBySPFxVersionChange: (event: any, option?: IDropdownOption) => void; - // eslint-disable-next-line no-unused-vars onFilterByComponentTypeChange: (event: any, option?: IDropdownOption) => void; - // eslint-disable-next-line no-unused-vars onFilterOnlyScenariosChange: (event: any) => void; initialQuery?: string; spfxVersions: IDropdownOption[]; diff --git a/src/webview/view/hooks/useSamples.tsx b/src/webview/view/hooks/useSamples.tsx index 9725e27..2aee059 100644 --- a/src/webview/view/hooks/useSamples.tsx +++ b/src/webview/view/hooks/useSamples.tsx @@ -6,7 +6,6 @@ import { Sample } from '../../../models'; const SAMPLES_URL = 'https://raw.githubusercontent.com/pnp/vscode-viva/main/data/sp-dev-fx-samples.json'; -// eslint-disable-next-line no-unused-vars export default function useSamples(): [Sample[], string[], ((query: string, componentTypes: string[], spfxVersions: string[], showOnlyScenarios: boolean) => void)] { const [allSamples, setAllSamples] = useState(undefined); const [samples, setSamples] = useState(undefined); diff --git a/src/webview/view/index.tsx b/src/webview/view/index.tsx index 35a0b64..571b151 100644 --- a/src/webview/view/index.tsx +++ b/src/webview/view/index.tsx @@ -21,7 +21,6 @@ if (elm) { 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) { @@ -36,10 +35,6 @@ if (elm) { data.isNewProject = isNewProject; } - if (nodeVersion) { - data.nodeVersion = nodeVersion; - } - const routeEntry = Object.keys(routeEntries).findIndex(key => key === type); root.render(