Skip to content

Commit

Permalink
Refactors scaffolding form to smaller components. Closes #221 (#242)
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
Saurabh7019 authored Jun 10, 2024
1 parent 14a9e6b commit 7cbdd74
Show file tree
Hide file tree
Showing 9 changed files with 501 additions and 262 deletions.
60 changes: 60 additions & 0 deletions src/webview/view/components/forms/spfxProject/AdditionalStep.tsx
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 src/webview/view/components/forms/spfxProject/ComponentDetailsStep.tsx
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 src/webview/view/components/forms/spfxProject/FormActions.tsx
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 src/webview/view/components/forms/spfxProject/FormHeader.tsx
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 src/webview/view/components/forms/spfxProject/GeneralInfoStep.tsx
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>
);
};
Loading

0 comments on commit 7cbdd74

Please sign in to comment.