Skip to content

Commit

Permalink
Sample gallery with extension type, Closes #219 (#260)
Browse files Browse the repository at this point in the history
## 🎯 Aim

Adding an extra filter to Sample gallery so that we can filter on the
extension type. Filter can only be visible when extension type is
selected 'Component Type'

## 📷 Result

![image](https://github.com/pnp/vscode-viva/assets/35696168/c8c2e084-5831-4fc4-861e-ad4c4c0845d7)

## ✅ What was done

- [X] Added Extension filter dropdown
- [X] Added functionality so that extension filter is only visible when
extension is selected in the Component Type dropdown
- [X] Added filter functionality so that samples get filtered based on
selected value in extension dropdown
- [x] Added filter functionality so that samples don't get filtered when
extensions dropdown has selected values but extension is not selected in
component type dropdown

## 🔗 Related issue

Closes: #219
  • Loading branch information
nicodecleyre committed Aug 25, 2024
1 parent e2eec69 commit 11100a4
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 17 deletions.
61 changes: 51 additions & 10 deletions src/webview/view/components/gallery/GalleryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,23 @@ export const GalleryView: React.FunctionComponent<IGalleryViewProps> = ({ }: Rea
const [spfxVersions, setSPFxVersions] = useLocalStorage<string[]>('spfxVersions', []);
const [showOnlyScenarios, setShowOnlyScenarios] = useLocalStorage('showOnlyScenarios', false);
const [componentTypes, setComponentTypes] = useLocalStorage<string[]>('componentTypes', []);
const [extensionTypes, setExtensionTypes] = useLocalStorage<string[]>('extensionTypes', []);
const [isExtensionSelected, setIsExtensionSelected] = useLocalStorage<boolean>('isExtensionSelected', false);

const onSearchTextboxChange = (event: any) => {
const input: string = event.target.value;
setQuery(input);
search(input, componentTypes ?? [], spfxVersions ?? [], showOnlyScenarios);
search(input, componentTypes ?? [], spfxVersions ?? [], extensionTypes ?? [], showOnlyScenarios);
};

const onClearTextboxChange = () => {
setQuery('');
search('', componentTypes ?? [], spfxVersions ?? [], showOnlyScenarios);
search('', componentTypes ?? [], spfxVersions ?? [], extensionTypes ?? [], showOnlyScenarios);
};

const onFilterOnlyScenariosChange = () => {
setShowOnlyScenarios(!showOnlyScenarios);
search(query, componentTypes ?? [], spfxVersions ?? [], !showOnlyScenarios);
search(query, componentTypes ?? [], spfxVersions ?? [], extensionTypes ?? [], !showOnlyScenarios);
};

const onFilterBySPFxVersionChange = (event: any, option?: IDropdownOption) => {
Expand All @@ -51,7 +53,7 @@ export const GalleryView: React.FunctionComponent<IGalleryViewProps> = ({ }: Rea
setSelectedFilters(removedFilter);
}
setSPFxVersions(spfxVersionsInput);
search(query, componentTypes ?? [], spfxVersionsInput, showOnlyScenarios);
search(query, componentTypes ?? [], spfxVersionsInput, extensionTypes ?? [], showOnlyScenarios);
};

const onRemoveFilterBySPFxVersion = (key: string) => {
Expand All @@ -63,6 +65,10 @@ export const GalleryView: React.FunctionComponent<IGalleryViewProps> = ({ }: Rea
};

const onFilterByComponentTypeChange = (event: any, option?: IDropdownOption) => {
if (option?.key === 'extension') {
setIsExtensionSelected(prevState => !prevState);
}

let componentTypesInput: string[] = [];
if (option?.selected) {
componentTypesInput = [...componentTypes ?? [], option.key as string];
Expand All @@ -73,11 +79,38 @@ export const GalleryView: React.FunctionComponent<IGalleryViewProps> = ({ }: Rea
}]);
} else {
componentTypesInput = componentTypes?.filter(componentType => componentType !== option?.key) ?? [];
const removedFilter = selectedFilters.filter(filter => filter.key !== option?.key);
let removedFilter = selectedFilters.filter(filter => filter.key !== option?.key);
if (option?.key === 'extension') {
setExtensionTypes([]);
removedFilter = removedFilter.filter(filter => filter.kind !== 'extensionType');
}
setSelectedFilters(removedFilter);
}
setComponentTypes(componentTypesInput);
search(query, componentTypesInput, spfxVersions ?? [], showOnlyScenarios);
search(query, componentTypesInput, spfxVersions ?? [], extensionTypes ?? [], showOnlyScenarios);
};

const onRemoveFilterByExtensionType = (key: string) => {
onFilterByExtensionTypeChange(null, { key: key, text: key, selected: false });
};

const onFilterByExtensionTypeChange = (event: any, option?: IDropdownOption) => {
let extensionTypesInput: string[] = [];

if (option?.selected) {
extensionTypesInput = [...extensionTypes ?? [], option.key as string];
setSelectedFilters([...selectedFilters, {
key: option.key as string,
text: option.text as string,
kind: 'extensionType'
}]);
} else {
extensionTypesInput = extensionTypes?.filter(componentType => componentType !== option?.key) ?? [];
const removedFilter = selectedFilters.filter(filter => filter.key !== option?.key);
setSelectedFilters(removedFilter);
}
setExtensionTypes(extensionTypesInput);
search(query, componentTypes ?? [], spfxVersions ?? [], extensionTypesInput, showOnlyScenarios);
};

const getSPFxVersions = (): IDropdownOption[] => {
Expand All @@ -95,7 +128,7 @@ export const GalleryView: React.FunctionComponent<IGalleryViewProps> = ({ }: Rea
useEffect(() => {
if (samples !== undefined) {
setShowOnlyScenarios(showOnlyScenarios);
search(query, componentTypes ?? [], spfxVersions ?? [], showOnlyScenarios);
search(query, componentTypes ?? [], spfxVersions ?? [], extensionTypes ?? [], showOnlyScenarios);
}
}, [samples]);

Expand All @@ -104,9 +137,15 @@ export const GalleryView: React.FunctionComponent<IGalleryViewProps> = ({ }: Rea
setSelectedFilters([]);
setSPFxVersions([]);
setComponentTypes([]);
setExtensionTypes([]);
setShowOnlyScenarios(false);
setQuery('');
search('', [], [], false);
search('', [], [], [], false);
setIsExtensionSelected(false);
};

const onClearExtensionTypes = () => {
setExtensionTypes([]);
};

return (
Expand All @@ -125,15 +164,17 @@ export const GalleryView: React.FunctionComponent<IGalleryViewProps> = ({ }: Rea
onFilterBySPFxVersionChange={(event, option) => onFilterBySPFxVersionChange(event, option)}
onFilterByComponentTypeChange={(event, option) => onFilterByComponentTypeChange(event, option)}
onFilterOnlyScenariosChange={() => onFilterOnlyScenariosChange()}
onFilterByExtensionTypeChange={(event, option) => onFilterByExtensionTypeChange(event, option)}
initialQuery={query}
selectedFilters={selectedFilters}
onRemoveFilterBySPFxVersion={onRemoveFilterBySPFxVersion}
onRemoveFilterByComponentType={onRemoveFilterByComponentType}
onRemoveFilterByExtensionType={onRemoveFilterByExtensionType}
clearAllFilters={clearFilters}
onClearTextboxChange={onClearTextboxChange}
showOnlyScenarios={showOnlyScenarios}
spfxVersions={getSPFxVersions()} />

spfxVersions={getSPFxVersions()}
isExtensionSelected={isExtensionSelected} />
{
samples.length === 0 && (
<NoResults />
Expand Down
57 changes: 53 additions & 4 deletions src/webview/view/components/gallery/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,26 @@ export interface ISearchBarProps {
onFilterBySPFxVersionChange: (event: any, option?: IDropdownOption) => void;
onFilterByComponentTypeChange: (event: any, option?: IDropdownOption) => void;
onFilterOnlyScenariosChange: (event: any) => void;
onFilterByExtensionTypeChange: (event: any, option?: IDropdownOption) => void;
initialQuery?: string;
spfxVersions: IDropdownOption[];
selectedFilters: ISelectedFilter[];
onRemoveFilterBySPFxVersion: (key: string) => void;
onRemoveFilterByComponentType: (key: string) => void;
onRemoveFilterByExtensionType: (key: string) => void;
clearAllFilters: () => void;
onClearTextboxChange: () => void;
showOnlyScenarios: boolean;
isExtensionSelected: boolean;
}

export interface ISelectedFilter {
key: string | null;
text: string;
kind: 'spfxVersion' | 'componentType'
kind: 'spfxVersion' | 'componentType' | 'extensionType';
}

export const SearchBar: React.FunctionComponent<ISearchBarProps> = ({ onSearchTextboxChange, onFilterBySPFxVersionChange, onFilterByComponentTypeChange, onFilterOnlyScenariosChange, initialQuery, spfxVersions, selectedFilters, onRemoveFilterByComponentType, onRemoveFilterBySPFxVersion, clearAllFilters, onClearTextboxChange, showOnlyScenarios }: React.PropsWithChildren<ISearchBarProps>) => {
export const SearchBar: React.FunctionComponent<ISearchBarProps> = ({ onSearchTextboxChange, onFilterBySPFxVersionChange, onFilterByComponentTypeChange, onFilterOnlyScenariosChange, onFilterByExtensionTypeChange, initialQuery, spfxVersions, selectedFilters, onRemoveFilterByComponentType, onRemoveFilterBySPFxVersion, onRemoveFilterByExtensionType, clearAllFilters, onClearTextboxChange, showOnlyScenarios, isExtensionSelected }: React.PropsWithChildren<ISearchBarProps>) => {
const [query, setQuery] = useState<string>(initialQuery ?? '');
const [debouncedQuery, setDebounceQuery] = useDebounce(query, 300);

Expand Down Expand Up @@ -65,6 +68,26 @@ export const SearchBar: React.FunctionComponent<ISearchBarProps> = ({ onSearchTe
return options;
};

const getExtensionTypeOptions = (): IDropdownOption[] => {
const extensionTypes: IDropdownOption[] = [
{ key: 'ListViewCommandSet', text: 'List view commandset' },
{ key: 'ApplicationCustomizer', text: 'Application customizer' },
{ key: 'FieldCustomizer', text: 'Field customizer' },
{ key: 'FormCustomizer', text: 'Form customizer' }
];

selectedFilters.forEach(filter => {
if (filter.kind === 'extensionType') {
const matchingOption = extensionTypes.find(extensionType => extensionType.key === filter.key);
if (matchingOption) {
matchingOption.selected = true;
}
}
});

return extensionTypes;
};

const clearQueryAndTextbox = () => {
setQuery('');
onClearTextboxChange();
Expand All @@ -76,10 +99,19 @@ export const SearchBar: React.FunctionComponent<ISearchBarProps> = ({ onSearchTe
};

const componentTypes = getComponentTypeOptions();
const extensionTypes = getExtensionTypeOptions();

const handleComponentTypeChange = (event: any, option?: IDropdownOption) => {
onFilterByComponentTypeChange(event, option);
};

const handleExtensionTypeChange = (event: any, option?: IDropdownOption) => {
onFilterByExtensionTypeChange(event, option);
};

return (
<div>
<div className={'mt-2 columns-1 md:columns-4'}>
<div className={'mt-2 columns-1 md:columns-5'}>
<div>
<VSCodeTextField size="100" placeholder="Search" value={query} onInput={onInputChange}>
<span slot='start' className='mt-0'>
Expand All @@ -91,8 +123,13 @@ export const SearchBar: React.FunctionComponent<ISearchBarProps> = ({ onSearchTe
<MultiSelect options={spfxVersions} label="SPFx version" onChange={onFilterBySPFxVersionChange} />
</div>
<div>
<MultiSelect options={componentTypes} label="Component Type" onChange={onFilterByComponentTypeChange} />
<MultiSelect options={componentTypes} label="Component Type" onChange={handleComponentTypeChange} />
</div>
{isExtensionSelected && (
<div>
<MultiSelect options={extensionTypes} label="Extension type" onChange={handleExtensionTypeChange} />
</div>
)}
<div>
<VSCodeCheckbox checked={showOnlyScenarios} onChange={onFilterOnlyScenariosChange}>show only scenarios</VSCodeCheckbox>
</div>
Expand Down Expand Up @@ -124,6 +161,18 @@ export const SearchBar: React.FunctionComponent<ISearchBarProps> = ({ onSearchTe
</div>
</VSCodeTag>
</label>);
} else if (filter.kind === 'extensionType') {
return (
<label className={'p-1'} >
<VSCodeTag key={index} >
<div className={'flex'}>
{filter.text}
<label className="cursor-pointer" onClick={() => onRemoveFilterByExtensionType(filter.key as string)}>
<CloseIcon />
</label>
</div>
</VSCodeTag>
</label>);
}

return (
Expand Down
11 changes: 8 additions & 3 deletions src/webview/view/hooks/useSamples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Sample } from '../../../models';

const SAMPLES_URL = 'https://raw.githubusercontent.com/pnp/vscode-viva/main/data/sp-dev-fx-samples.json';

export default function useSamples(): [Sample[], string[], ((query: string, componentTypes: string[], spfxVersions: string[], showOnlyScenarios: boolean) => void)] {
export default function useSamples(): [Sample[], string[], ((query: string, componentTypes: string[], spfxVersions: string[], extensionTypes: string[], showOnlyScenarios: boolean) => void)] {
const [allSamples, setAllSamples] = useState<Sample[] | undefined>(undefined);
const [samples, setSamples] = useState<Sample[] | undefined>(undefined);
const state = Messenger.getState() as any || {};
Expand Down Expand Up @@ -67,7 +67,7 @@ export default function useSamples(): [Sample[], string[], ((query: string, comp
});
}, [allSamples]);

const search = (query: string, componentTypes: string[], spfxVersions: string[], showOnlyScenarios: boolean) => {
const search = (query: string, componentTypes: string[], spfxVersions: string[], extensionTypes: string[], showOnlyScenarios: boolean) => {
const currentSamples: Sample[] = state['samples'];
const samplesByTitle: Sample[] = currentSamples!.filter((sample: Sample) => sample.title.toString().toLowerCase().includes(query.toLowerCase()));
const samplesByTag: Sample[] = currentSamples!.filter((sample: Sample) => sample.tags.some(tag => tag.toString().toLowerCase().includes(query.toLowerCase())));
Expand All @@ -90,7 +90,12 @@ export default function useSamples(): [Sample[], string[], ((query: string, comp
filteredSamplesBySPFxVersion = filteredSamplesByComponentType.filter((sample: Sample) => spfxVersions.includes(sample.version));
}

setSamples(filteredSamplesBySPFxVersion);
let filteredSamplesByExtension = filteredSamplesBySPFxVersion;
if (extensionTypes.length > 0 && componentTypes.includes('extension')) {
filteredSamplesByExtension = filteredSamplesBySPFxVersion.filter((sample: Sample) => extensionTypes.includes(sample.extensionType));
}

setSamples(filteredSamplesByExtension);
};

return [samples!, versions, search];
Expand Down

0 comments on commit 11100a4

Please sign in to comment.