-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
## π― Aim This PR refactors scaffolding form to smaller components ## π· Result ![image](https://github.com/pnp/vscode-viva/assets/18114579/fcbf9fcc-18b9-44c6-9b96-62eaf206a0f2) ## β What was done - [X] Moved the header section into a separate `FormHeader` component. - [X] Moved the general information section into a separate `GeneralInfoStep` component. - [X] Moved the component details section into a separate `ComponentDetailsStep` component. - [X] Moved the additional steps section into a separate `AdditionalStep` component. - [X] Moved the form action buttons and the progress indicator into a separate `FormActions` component. - [X] Maintained the state management within the main component `ScaffoldSpfxProjectView`. - [X] Added a slight delay to fix terminal receiving `pm i` instead of `npm i` when `npm install` checkbox is selected ## π Related issue Closes: #221
- Loading branch information
1 parent
14a9e6b
commit 7cbdd74
Showing
9 changed files
with
501 additions
and
262 deletions.
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
src/webview/view/components/forms/spfxProject/AdditionalStep.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
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; | ||
// eslint-disable-next-line no-unused-vars | ||
setShouldRunInit: (value: boolean) => void; | ||
shouldInstallReusablePropertyPaneControls: boolean; | ||
// eslint-disable-next-line no-unused-vars | ||
setShouldInstallReusablePropertyPaneControls: (value: boolean) => void; | ||
shouldInstallReusableReactControls: boolean; | ||
// eslint-disable-next-line no-unused-vars | ||
setShouldInstallReusableReactControls: (value: boolean) => void; | ||
shouldInstallPnPJs: boolean; | ||
// eslint-disable-next-line no-unused-vars | ||
setShouldInstallPnPJs: (value: boolean) => void; | ||
} | ||
|
||
export const AdditionalStep: React.FunctionComponent<AdditionalStepProps> = ({ | ||
shouldRunInit, | ||
setShouldRunInit, | ||
shouldInstallReusablePropertyPaneControls, | ||
setShouldInstallReusablePropertyPaneControls, | ||
shouldInstallReusableReactControls, | ||
setShouldInstallReusableReactControls, | ||
shouldInstallPnPJs, | ||
setShouldInstallPnPJs }: React.PropsWithChildren<AdditionalStepProps>) => { | ||
return ( | ||
<div className={'spfx__form__step'}> | ||
<StepHeader step={3} title='Additional steps' /> | ||
<div className={'spfx__form__step__content ml-10'}> | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
Run <code>npm install</code> after the project is created? | ||
</label> | ||
<VSCodeCheckbox checked={shouldRunInit} onChange={() => setShouldRunInit(!shouldRunInit)} /> | ||
</div> | ||
<PackageSelector value={shouldInstallReusablePropertyPaneControls} | ||
setValue={setShouldInstallReusablePropertyPaneControls} | ||
label='Install reusable property pane controls' | ||
link='https://pnp.github.io/sp-dev-fx-property-controls/' /> | ||
|
||
<PackageSelector | ||
value={shouldInstallReusableReactControls} | ||
setValue={setShouldInstallReusableReactControls} | ||
label='Install reusable React controls' | ||
link='https://pnp.github.io/sp-dev-fx-controls-react/' /> | ||
|
||
<PackageSelector | ||
value={shouldInstallPnPJs} | ||
setValue={setShouldInstallPnPJs} | ||
label='Install PnPjs (@pnp/sp, @pnp/graph)' | ||
link='https://pnp.github.io/pnpjs/' /> | ||
</div> | ||
</div> | ||
); | ||
}; |
145 changes: 145 additions & 0 deletions
145
src/webview/view/components/forms/spfxProject/ComponentDetailsStep.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
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, AdaptiveCardTypesNode16, AdaptiveCardTypesNode18 } 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; | ||
// eslint-disable-next-line no-unused-vars | ||
setComponentName: (name: string) => void; | ||
// eslint-disable-next-line no-unused-vars | ||
setIsValidComponentName: (value: boolean | null) => void; | ||
nodeVersion: string; | ||
frameworkType: string; | ||
// eslint-disable-next-line no-unused-vars | ||
setFrameworkType: (type: string) => void; | ||
extensionType: ExtensionType; | ||
// eslint-disable-next-line no-unused-vars | ||
setExtensionType: (type: ExtensionType) => void; | ||
aceType: string; | ||
// eslint-disable-next-line no-unused-vars | ||
setAceType: (type: string) => void; | ||
} | ||
|
||
export const ComponentDetailsStep: React.FunctionComponent<IComponentDetailsStepProps> = ({ | ||
isNewProject, | ||
componentType, | ||
componentName, | ||
isValidComponentName, | ||
setComponentName, | ||
setIsValidComponentName, | ||
nodeVersion, | ||
frameworkType, | ||
setFrameworkType, | ||
extensionType, | ||
setExtensionType, | ||
aceType, | ||
setAceType }: React.PropsWithChildren<IComponentDetailsStepProps>) => { | ||
const componentTypeName = ComponentTypes.find((component) => component.value === componentType)?.name; | ||
|
||
useEffect(() => { | ||
const messageListener = (event: MessageEvent<any>) => { | ||
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 ( | ||
<div className={'spfx__form__step'}> | ||
<StepHeader step={2} title={`${componentTypeName} details`} /> | ||
<div className={'spfx__form__step__content ml-10'}> | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
What should be the name for your {componentTypeName}? * | ||
</label> | ||
<VSCodeTextField className={'w-full'} value={componentName} onChange={(e: any) => validateComponentName(e.target.value)} /> | ||
{ | ||
isValidComponentName === false && | ||
<p className={'text-red-500 text-sm mt-1'}>The component name already exists</p> | ||
} | ||
</div> | ||
{ | ||
componentType === 'extension' && | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
Which extension type would you like to create? | ||
</label> | ||
<VSCodeDropdown className={'w-full'} value={extensionType} onChange={(e: any) => setExtensionType(e.target.value)}> | ||
{ExtensionTypes.map((type) => <VSCodeOption key={type.value} value={type.value}>{type.name}</VSCodeOption>)} | ||
</VSCodeDropdown> | ||
</div> | ||
} | ||
{ | ||
componentType === ComponentType.adaptiveCardExtension && | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
Which adaptive card extension template do you want to use? | ||
</label> | ||
<VSCodeDropdown className={'w-full'} value={aceType} onChange={(e: any) => setAceType(e.target.value)}> | ||
{nodeVersion === '16' ? | ||
AdaptiveCardTypesNode16.map((type) => <VSCodeOption key={type.value} value={type.value}>{type.name}</VSCodeOption>) : | ||
AdaptiveCardTypesNode18.map((type) => <VSCodeOption key={type.value} value={type.value}>{type.name}</VSCodeOption>) | ||
} | ||
</VSCodeDropdown> | ||
</div> | ||
} | ||
{ | ||
componentType === ComponentType.webPart && | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
Which template would you like to use? | ||
</label> | ||
<VSCodeDropdown className={'w-full'} value={frameworkType} onChange={(e: any) => setFrameworkType(e.target.value)}> | ||
{FrameworkTypes.map((framework) => <VSCodeOption key={framework.value} value={framework.value}>{framework.name}</VSCodeOption>)} | ||
</VSCodeDropdown> | ||
</div> | ||
} | ||
{ | ||
componentType === ComponentType.extension && ExtensionTypes.find(e => e.value === extensionType)?.templates.some(t => t) && | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
Which template would you like to use? | ||
</label> | ||
<VSCodeDropdown className={'w-full'} value={frameworkType} onChange={(e: any) => setFrameworkType(e.target.value)}> | ||
{ExtensionTypes.find(e => e.value === extensionType)?.templates.map((framework) => { | ||
const key = FrameworkTypes.find(f => f.name === framework)?.value; | ||
return (<VSCodeOption key={key} value={key}>{framework}</VSCodeOption>); | ||
} | ||
)} | ||
</VSCodeDropdown> | ||
</div> | ||
} | ||
</div> | ||
</div> | ||
); | ||
}; |
50 changes: 50 additions & 0 deletions
50
src/webview/view/components/forms/spfxProject/FormActions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IFormActionsProps> = ({ | ||
isFormValid, | ||
isSubmitting, | ||
isNewProject, | ||
submit }: React.PropsWithChildren<IFormActionsProps>) => { | ||
return ( | ||
<div className={'spfx__action mb-3 pb-3 border-b pl-10'}> | ||
{!isFormValid ? ( | ||
<p className={'py-2'}> | ||
<strong>Please fill up the required fields with valid values</strong> | ||
</p> | ||
) : ( | ||
'' | ||
)} | ||
<VSCodeButton | ||
disabled={!isFormValid ? true : null} | ||
className={isSubmitting ? 'w-full hidden' : 'w-full'} | ||
onClick={submit} | ||
> | ||
<span slot={'start'}> | ||
<AddIcon /> | ||
</span> | ||
{isNewProject ? 'Create a new SPFx project' : 'Add a new SPFx component'} | ||
</VSCodeButton> | ||
<div className={isSubmitting ? '' : 'hidden'}> | ||
<div className={'text-center h-5'}> | ||
<VSCodeProgressRing | ||
style={{ | ||
width: '100%', | ||
height: '100%', | ||
}} | ||
/> | ||
<p className={'mt-4'}>Working on it...</p> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
16 changes: 16 additions & 0 deletions
16
src/webview/view/components/forms/spfxProject/FormHeader.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import * as React from 'react'; | ||
|
||
|
||
export interface IFormHeaderProps { | ||
isNewProject: boolean; | ||
} | ||
|
||
export const FormHeader: React.FunctionComponent<IFormHeaderProps> = ({ isNewProject }: React.PropsWithChildren<IFormHeaderProps>) => { | ||
const title = isNewProject ? 'Create a new SPFx project' : 'Extend an existing SPFx project with a new component'; | ||
|
||
return ( | ||
<div className={'text-center mb-6'}> | ||
<h1 className="text-2xl">{title}</h1> | ||
</div> | ||
); | ||
}; |
121 changes: 121 additions & 0 deletions
121
src/webview/view/components/forms/spfxProject/GeneralInfoStep.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
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; | ||
// eslint-disable-next-line no-unused-vars | ||
setFolderPath: (folderPath: string) => void; | ||
solutionName: string; | ||
// eslint-disable-next-line no-unused-vars | ||
setSolutionName: (value: string) => void; | ||
isValidSolutionName: boolean | null | undefined; | ||
// eslint-disable-next-line no-unused-vars | ||
setIsValidSolutionName: (value: boolean | null) => void; | ||
// eslint-disable-next-line no-unused-vars | ||
setComponentType: (componentType: ComponentType) => void; | ||
componentTypes: { value: string; name: string }[]; | ||
} | ||
|
||
export const GeneralInfoStep: React.FunctionComponent<IGeneralInfoProps> = ({ | ||
isNewProject, | ||
folderPath, | ||
setFolderPath, | ||
solutionName, | ||
setSolutionName, | ||
isValidSolutionName, | ||
setIsValidSolutionName, | ||
setComponentType, | ||
componentTypes }: React.PropsWithChildren<IGeneralInfoProps>) => { | ||
|
||
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<any>) => { | ||
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 ( | ||
<div className={'spfx__form__step'}> | ||
<StepHeader step={1} title='General information' /> | ||
<div className={'spfx__form__step__content ml-10'}> | ||
{ | ||
isNewProject && | ||
<> | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
What should be the parent folder where you want to create the project? * | ||
</label> | ||
<div className={'flex'}> | ||
<div className={'w-4/5'}> | ||
<VSCodeTextField disabled className={'w-full'} value={folderPath} /> | ||
</div> | ||
<div className={'w-1/5'}> | ||
<VSCodeButton className={'w-full'} appearance={'secondary'} onClick={pickFolder}> | ||
<span slot={'start'}><FolderIcon /></span> | ||
Folder | ||
</VSCodeButton> | ||
</div> | ||
</div> | ||
</div> | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
What should be the name of your solution? * | ||
</label> | ||
<VSCodeTextField className={'w-full'} value={solutionName} onChange={(e: any) => validateSolutionName(e.target.value)} /> | ||
{ | ||
isValidSolutionName === false && | ||
<p className={'text-red-500 text-sm mt-1'}>The solution name already exists</p> | ||
} | ||
</div> | ||
</> | ||
} | ||
<div className={'mb-2'}> | ||
<label className={'block mb-1'}> | ||
What component you wish to create? | ||
</label> | ||
<VSCodeDropdown className={'w-full'} onChange={(e: any) => setComponentType(e.target.value)}> | ||
{componentTypes.map((component) => <VSCodeOption key={component.value} value={component.value}>{component.name}</VSCodeOption>)} | ||
</VSCodeDropdown> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.