diff --git a/CHANGELOG.md b/CHANGELOG.md index d14f816..0fe161e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [3.4.0] - 2024-07-18 + +- Improved welcome experience +- Fixed lags in sample gallery search +- Improved sample gallery filter functionalities +- Improved create project step in walkthrough + ## [3.3.0] - 2024-06-16 - Refactored scaffolding form to smaller components diff --git a/README.md b/README.md index a85a16a..e973784 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@

Capabilities | - Preview features | Architecture | Wiki | Contributing | @@ -264,12 +263,7 @@ 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 +### 1️⃣3️⃣ SPFx Toolkit GitHub Chat Participant ![SPFx Toolkit chat](./assets/images/chat-intro.png) @@ -286,13 +280,7 @@ Currently, we support the following commands: - `/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) +[Check out our docs for more details](https://github.com/pnp/vscode-viva/wiki/8.-SPFx-Toolkit-GitHub-Chat-Participant) ## ⚙️ Architecture diff --git a/assets/walkthrough/create-new-project.md b/assets/walkthrough/create-new-project.md index 135b9c3..756bb6c 100644 --- a/assets/walkthrough/create-new-project.md +++ b/assets/walkthrough/create-new-project.md @@ -8,10 +8,15 @@ Check it out in action 👇. ![Create new project](../images/scaffolding-form.gif) -It's possible to scaffold any kind of SPFx project. +It's possible to scaffold any kind of SPFx project including web parts, extensions, library components, and adaptive card extensions. ![All SPFx project support](../images/scaffolding-support.png) +- **Web parts** are reusable elements that serve as the building blocks for SharePoint pages. They allow you to create customizable controls that can be used across multiple pages. +- **Extensions** enable you to extend the SharePoint user experience by adding scripts to pages, modifying views, adding new actions, and altering list form experiences. +- **Library components** are reusable code elements that can be shared across all the components in the tenant. +- **Adaptive Card Extensions (ACEs)** allow you to build rich, native extensions for Viva Connections Dashboards and SharePoint Pages. + Install additional dependencies with a single click straight from the scaffolding form. Currently we support installing [PnP reusable property pane controls](https://pnp.github.io/sp-dev-fx-property-controls/), [PnP reusable React controls](https://pnp.github.io/sp-dev-fx-controls-react/), and [PnPjs](https://pnp.github.io/pnpjs/). ![Additional dependency step](../images/scaffolding-additional-step.png) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c0f1d12..71422cb 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,18 +1,19 @@ { "name": "viva-connections-toolkit", - "version": "3.3.0", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "viva-connections-toolkit", - "version": "3.3.0", + "version": "3.4.0", "license": "MIT", "dependencies": { "@pnp/cli-microsoft365": "6.11.0", "node-forge": "^1.3.1", "react-markdown": "^9.0.1", - "remark-gfm": "^4.0.0" + "remark-gfm": "^4.0.0", + "use-debounce": "^10.0.1" }, "devDependencies": { "@actions/core": "^1.8.2", @@ -16848,6 +16849,17 @@ "punycode": "^2.1.0" } }, + "node_modules/use-debounce": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.1.tgz", + "integrity": "sha512-0uUXjOfm44e6z4LZ/woZvkM8FwV1wiuoB6xnrrOmeAEjRDDzTLQNRFtYHvqUsJdrz1X37j0rVGIVp144GLHGKg==", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, diff --git a/package.json b/package.json index 48b0ae3..8b64fd5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "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.3.0", + "version": "3.4.0", "publisher": "m365pnp", "preview": false, "homepage": "https://github.com/pnp/vscode-viva", @@ -260,7 +260,7 @@ "viewsWelcome": [ { "view": "pnp-view-empty", - "contents": "Welcome to SharePoint Framework Toolkit\n\n\nTo start create a new SharePoint Framework project. \n[Create a new project](command:spfx-toolkit.createProject)\n\n\n\n\n\nUse a sample to kick off a new project and boost your development.\n[View samples](command:spfx-toolkit.samplesGallery)\n\n\n\n\n\nYou can also open an existing app.\n[Open folder](command:vscode.openFolder)\n\n\n\n\n\nValidate your local workspace and check if you have the required dependencies.\n[Check dependencies](command:spfx-toolkit.checkDependencies)\n\n\n\n\n\nInstall the required dependencies.\n[Install dependencies](command:spfx-toolkit.installDependencies)\n\n\n\nIf you need additional help or resources visit the [extension wiki](https://github.com/pnp/vscode-viva/wiki) for more information or go over the [walkthrough](command:spfx-toolkit.welcome)." + "contents": "Welcome to SharePoint Framework Toolkit\n\n\nTo start create a new SharePoint Framework project. \n[Create a new project](command:spfx-toolkit.createProject)\n\n\n\n\n\nUse a sample to kick off a new project and boost your development.\n[View samples](command:spfx-toolkit.samplesGallery)\n\n\n\n\n\nYou can also open an existing app.\n[Open folder](command:vscode.openFolder)\n\n\n\n\n\nValidate your local workspace and check if you have the required dependencies.\n[Check dependencies](command:spfx-toolkit.checkDependencies)\n\n\n\n\n\nInstall the required dependencies.\n[Install dependencies](command:spfx-toolkit.installDependencies)\n\n\n\nLearn more about SharePoint Framework and this toolkit.\n[Walkthrough](command:spfx-toolkit.welcome)\n\n\n\nFor additional help or resources visit the [extension wiki](https://github.com/pnp/vscode-viva/wiki)." } ], "commands": [ @@ -473,6 +473,7 @@ "@pnp/cli-microsoft365": "6.11.0", "node-forge": "^1.3.1", "react-markdown": "^9.0.1", - "remark-gfm": "^4.0.0" + "remark-gfm": "^4.0.0", + "use-debounce": "^10.0.1" } } diff --git a/src/webview/view/components/controls/MultiSelect.tsx b/src/webview/view/components/controls/MultiSelect.tsx index ad908d1..ea02cac 100644 --- a/src/webview/view/components/controls/MultiSelect.tsx +++ b/src/webview/view/components/controls/MultiSelect.tsx @@ -147,6 +147,7 @@ export const MultiSelect: React.FunctionComponent = ({ onChan option.selected).map((option) => option.key as any)]} options={options} styles={getDropdownStyles()} onChange={onChange} diff --git a/src/webview/view/components/gallery/DetailsView.tsx b/src/webview/view/components/gallery/DetailsView.tsx index a2aab6b..9e11b80 100644 --- a/src/webview/view/components/gallery/DetailsView.tsx +++ b/src/webview/view/components/gallery/DetailsView.tsx @@ -21,7 +21,7 @@ export const DetailsView: React.FunctionComponent = ({ }: Rea useEffect(() => { const { item } = state; setSample(item); - + window.scrollTo(0, 0); const url = item.url.replace('github.com', 'raw.githubusercontent.com').replace('/blob', '').replace('/tree', ''); const fetchData = async () => { diff --git a/src/webview/view/components/gallery/GalleryView.tsx b/src/webview/view/components/gallery/GalleryView.tsx index 735f1e0..73c73bc 100644 --- a/src/webview/view/components/gallery/GalleryView.tsx +++ b/src/webview/view/components/gallery/GalleryView.tsx @@ -1,22 +1,24 @@ import * as React from 'react'; -import { useState } from 'react'; +import { useEffect } from 'react'; import useSamples from '../../hooks/useSamples'; import { List } from './List'; -import { SearchBar } from './SearchBar'; +import { ISelectedFilter, SearchBar } from './SearchBar'; import { NoResults } from './NoResults'; import { GalleryHeader } from './GalleryHeader'; import { GalleryLoader } from './GalleryLoader'; import { IDropdownOption } from '@fluentui/react'; +import useLocalStorage from '../../hooks/useLocalStorage '; export interface IGalleryViewProps { } export const GalleryView: React.FunctionComponent = ({ }: React.PropsWithChildren) => { const [samples, versions, search] = useSamples(); - const [query, setQuery] = useState(''); - const [spfxVersions, setSPFxVersions] = useState(); - const [showOnlyScenarios, setShowOnlyScenarios] = useState(false); - const [componentTypes, setComponentTypes] = useState(); + const [selectedFilters, setSelectedFilters] = useLocalStorage('selectedFilters', []); + const [query, setQuery] = useLocalStorage('query', ''); + const [spfxVersions, setSPFxVersions] = useLocalStorage('spfxVersions', []); + const [showOnlyScenarios, setShowOnlyScenarios] = useLocalStorage('showOnlyScenarios', false); + const [componentTypes, setComponentTypes] = useLocalStorage('componentTypes', []); const onSearchTextboxChange = (event: any) => { const input: string = event.target.value; @@ -24,6 +26,11 @@ export const GalleryView: React.FunctionComponent = ({ }: Rea search(input, componentTypes ?? [], spfxVersions ?? [], showOnlyScenarios); }; + const onClearTextboxChange = () => { + setQuery(''); + search('', componentTypes ?? [], spfxVersions ?? [], showOnlyScenarios); + }; + const onFilterOnlyScenariosChange = () => { setShowOnlyScenarios(!showOnlyScenarios); search(query, componentTypes ?? [], spfxVersions ?? [], !showOnlyScenarios); @@ -33,22 +40,42 @@ export const GalleryView: React.FunctionComponent = ({ }: Rea let spfxVersionsInput: string[] = []; if (option?.selected) { spfxVersionsInput = [...spfxVersions ?? [], option.key as string]; + setSelectedFilters([...selectedFilters, { + key: option.key as string, + text: option.key as string, + kind: 'spfxVersion' + }]); } else { spfxVersionsInput = spfxVersions?.filter(componentType => componentType !== option?.key) ?? []; + const removedFilter = selectedFilters.filter(filter => filter.key !== option?.key); + setSelectedFilters(removedFilter); } - setSPFxVersions(spfxVersionsInput); search(query, componentTypes ?? [], spfxVersionsInput, showOnlyScenarios); }; + const onRemoveFilterBySPFxVersion = (key: string) => { + onFilterBySPFxVersionChange(null, { key: key, text: key, selected: false }); + }; + + const onRemoveFilterByComponentType = (key: string) => { + onFilterByComponentTypeChange(null, { key: key, text: key, selected: false }); + }; + const onFilterByComponentTypeChange = (event: any, option?: IDropdownOption) => { let componentTypesInput: string[] = []; if (option?.selected) { componentTypesInput = [...componentTypes ?? [], option.key as string]; + setSelectedFilters([...selectedFilters, { + key: option.key as string, + text: option.text as string, + kind: 'componentType' + }]); } else { componentTypesInput = componentTypes?.filter(componentType => componentType !== option?.key) ?? []; + const removedFilter = selectedFilters.filter(filter => filter.key !== option?.key); + setSelectedFilters(removedFilter); } - setComponentTypes(componentTypesInput); search(query, componentTypesInput, spfxVersions ?? [], showOnlyScenarios); }; @@ -57,6 +84,7 @@ export const GalleryView: React.FunctionComponent = ({ }: Rea const dropdownOptions: IDropdownOption[] = versions.map(version => ({ key: version, text: version, + selected: selectedFilters.filter(filter => filter.key === version).length > 0 })).filter((value, index, self) => value.key !== null && index === self.findIndex((v) => v.key === value.key) @@ -64,6 +92,23 @@ export const GalleryView: React.FunctionComponent = ({ }: Rea return dropdownOptions; }; + useEffect(() => { + if (samples !== undefined) { + setShowOnlyScenarios(showOnlyScenarios); + search(query, componentTypes ?? [], spfxVersions ?? [], showOnlyScenarios); + } + }, [samples]); + + const clearFilters = () => { + localStorage.clear(); + setSelectedFilters([]); + setSPFxVersions([]); + setComponentTypes([]); + setShowOnlyScenarios(false); + setQuery(''); + search('', [], [], false); + }; + return (

{ @@ -71,7 +116,6 @@ export const GalleryView: React.FunctionComponent = ({ }: Rea ) } - { samples !== undefined && (
@@ -82,6 +126,12 @@ export const GalleryView: React.FunctionComponent = ({ }: Rea onFilterByComponentTypeChange={(event, option) => onFilterByComponentTypeChange(event, option)} onFilterOnlyScenariosChange={() => onFilterOnlyScenariosChange()} initialQuery={query} + selectedFilters={selectedFilters} + onRemoveFilterBySPFxVersion={onRemoveFilterBySPFxVersion} + onRemoveFilterByComponentType={onRemoveFilterByComponentType} + clearAllFilters={clearFilters} + onClearTextboxChange={onClearTextboxChange} + showOnlyScenarios={showOnlyScenarios} spfxVersions={getSPFxVersions()} /> { diff --git a/src/webview/view/components/gallery/SearchBar.tsx b/src/webview/view/components/gallery/SearchBar.tsx index 6a7617d..577b6bc 100644 --- a/src/webview/view/components/gallery/SearchBar.tsx +++ b/src/webview/view/components/gallery/SearchBar.tsx @@ -1,9 +1,11 @@ -import { VSCodeCheckbox, VSCodeTextField } from '@vscode/webview-ui-toolkit/react'; +import { VSCodeCheckbox, VSCodeTextField, VSCodeTag } from '@vscode/webview-ui-toolkit/react'; import * as React from 'react'; import { useEffect, useState } from 'react'; import { SearchIcon } from '../icons'; import { IDropdownOption } from '@fluentui/react'; import { MultiSelect } from '../controls'; +import { CloseIcon } from '../icons/CloseIcon'; +import { useDebounce } from 'use-debounce'; export interface ISearchBarProps { @@ -13,47 +15,135 @@ export interface ISearchBarProps { onFilterOnlyScenariosChange: (event: any) => void; initialQuery?: string; spfxVersions: IDropdownOption[]; + selectedFilters: ISelectedFilter[]; + onRemoveFilterBySPFxVersion: (key: string) => void; + onRemoveFilterByComponentType: (key: string) => void; + clearAllFilters: () => void; + onClearTextboxChange: () => void; + showOnlyScenarios: boolean; } -export const SearchBar: React.FunctionComponent = ({ onSearchTextboxChange, onFilterBySPFxVersionChange, onFilterByComponentTypeChange, onFilterOnlyScenariosChange, initialQuery, spfxVersions }: React.PropsWithChildren) => { - const [query, setQuery] = useState(''); +export interface ISelectedFilter { + key: string | null; + text: string; + kind: 'spfxVersion' | 'componentType' +} + +export const SearchBar: React.FunctionComponent = ({ onSearchTextboxChange, onFilterBySPFxVersionChange, onFilterByComponentTypeChange, onFilterOnlyScenariosChange, initialQuery, spfxVersions, selectedFilters, onRemoveFilterByComponentType, onRemoveFilterBySPFxVersion, clearAllFilters, onClearTextboxChange, showOnlyScenarios }: React.PropsWithChildren) => { + const [query, setQuery] = useState(initialQuery ?? ''); + const [debouncedQuery, setDebounceQuery] = useDebounce(query, 300); + + useEffect(() => { + setDebounceQuery(query); + }, [query]); useEffect(() => { - setQuery(initialQuery ?? ''); - }, [initialQuery]); + onSearchTextboxChange({ target: { value: debouncedQuery } }); + }, [debouncedQuery, onSearchTextboxChange]); - const getComponentTypes = (): IDropdownOption[] => { - const componentTypes: IDropdownOption[] = [ + const onInputChange = (event: any) => { + const input: string = event.target.value; + setQuery(input); + }; + + const getComponentTypeOptions = (): IDropdownOption[] => { + const options: IDropdownOption[] = [ { key: 'webpart', text: 'Web Part' }, { key: 'extension', text: 'Extension' }, { key: 'adaptiveCardExtension', text: 'ACE' } ]; - return componentTypes; + selectedFilters.forEach(filter => { + if (filter.kind === 'componentType') { + const matchingOption = options.find(option => option.key === filter.key); + if (matchingOption) { + matchingOption.selected = true; + } + } + }); + + return options; }; - const componentTypes = getComponentTypes(); + const clearQueryAndTextbox = () => { + setQuery(''); + onClearTextboxChange(); + }; + + const resetQueryAndFilters = () => { + clearQueryAndTextbox(); + clearAllFilters(); + }; + + const componentTypes = getComponentTypeOptions(); return (
- +
- +
- +
- show only scenarios + show only scenarios
+ +
+ {query && + + } + {selectedFilters.length > 0 && selectedFilters.map((filter, index) => { + if (filter.kind === 'spfxVersion') { + return ( + ); + } + + return ( + ); + })} + {(selectedFilters.length > 0 || query) && + + } +
); }; \ No newline at end of file diff --git a/src/webview/view/components/icons/CloseIcon.tsx b/src/webview/view/components/icons/CloseIcon.tsx new file mode 100644 index 0000000..2da7449 --- /dev/null +++ b/src/webview/view/components/icons/CloseIcon.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export interface CloseIconProps {} + +export const CloseIcon: React.FunctionComponent = () => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/src/webview/view/hooks/useLocalStorage .ts b/src/webview/view/hooks/useLocalStorage .ts new file mode 100644 index 0000000..5e35e48 --- /dev/null +++ b/src/webview/view/hooks/useLocalStorage .ts @@ -0,0 +1,25 @@ +import { useState } from 'react'; +type UseLocalStorageReturn = [T, (value: T | ((prevValue: T) => T)) => void]; + +function useLocalStorage(key: string, initialValue: T): UseLocalStorageReturn { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + return initialValue; + } + }); + + const setValue = (value: T | ((prevValue: T) => T)) => { + try { + const newValue = value instanceof Function ? value(storedValue) : value; + setStoredValue(newValue); + window.localStorage.setItem(key, JSON.stringify(newValue)); + } catch (error) { + } + }; + + return [storedValue, setValue]; +} +export default useLocalStorage; \ No newline at end of file