diff --git a/src/people/WorkSpacePlanner/WorkspacePlannerHeader/index.tsx b/src/people/WorkSpacePlanner/WorkspacePlannerHeader/index.tsx index 35e872f8..e719cf43 100644 --- a/src/people/WorkSpacePlanner/WorkspacePlannerHeader/index.tsx +++ b/src/people/WorkSpacePlanner/WorkspacePlannerHeader/index.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ import React, { useState, useCallback, useEffect } from 'react'; import { EuiText, EuiPopover, EuiCheckboxGroup } from '@elastic/eui'; import MaterialIcon from '@material/react-material-icon'; @@ -43,6 +45,8 @@ interface WorkspacePlannerHeaderProps { }; filterToggle: boolean; setFilterToggle: (a: boolean) => void; + searchText: string; + setSearchText: React.Dispatch>; } interface FeatureOption { @@ -51,7 +55,7 @@ interface FeatureOption { } const ClearButton = styled.button` - color: ${colors.light.primaryColor}; + color: ${colors.light.blue4}; background: none; border: none; font-size: 12px; @@ -64,12 +68,74 @@ const ClearButton = styled.button` } `; +const SearchInputContainer = styled.div` + position: relative; + width: 100%; + max-width: 400px; + margin-bottom: 1rem; + + input { + width: 100%; + padding: 0.5rem 2.5rem 0.5rem 0.5rem; /* Add padding to account for the button */ + border: 1px solid ${colors.light.grayish.G1100}; + border-radius: 4px; + font-size: 1rem; + outline: none; + transition: border-color 0.3s; + + &:focus { + border-color: ${colors.light.blue3}; + } + } + + button { + position: absolute; + top: 50%; + right: 0.5rem; + transform: translateY(-50%); + background: transparent; + border: none; + font-size: 1.25rem; + cursor: pointer; + color: ${colors.light.black400}; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + color: ${colors.light.blue3}; + } + } +`; + +const SearchInput = ({ + value, + onChange, + onClear +}: { + value: string; + onChange: (val: string) => void; + onClear: () => void; +}) => ( + + onChange(e.target.value)} + /> + {value && } + +); + export const WorkspacePlannerHeader = observer( ({ workspace_uuid, workspaceData, filterToggle, - setFilterToggle + setFilterToggle, + searchText, + setSearchText }: WorkspacePlannerHeaderProps) => { const { main, ui } = useStores(); const [isPostBountyModalOpen, setIsPostBountyModalOpen] = useState(false); @@ -78,12 +144,30 @@ export const WorkspacePlannerHeader = observer( const [isPhasePopoverOpen, setIsPhasePopoverOpen] = useState(false); const [isStatusPopoverOpen, setIsStatusPopoverOpen] = useState(false); const bountyCardStore = useBountyCardStore(workspace_uuid); + const [debouncedSearch, setDebouncedSearch] = useState(''); const checkUserPermissions = useCallback(async () => { const hasPermission = await userCanManageBounty(workspace_uuid, ui.meInfo?.pubkey, main); setCanPostBounty(hasPermission); }, [workspace_uuid, ui.meInfo, main]); + useEffect(() => { + const handler = setTimeout(() => setDebouncedSearch(searchText), 300); + return () => clearTimeout(handler); + }, [searchText]); + + const handleClearSearch = () => setSearchText(''); + + useEffect(() => { + bountyCardStore.restoreFilterState(); + const savedSearchText = sessionStorage.getItem('workspaceSearchText'); + if (savedSearchText) setSearchText(savedSearchText); + }, [bountyCardStore, filterToggle, setSearchText]); + + useEffect(() => { + sessionStorage.setItem('workspaceSearchText', searchText); + }, [searchText]); + useEffect(() => { checkUserPermissions(); }, [checkUserPermissions]); @@ -458,6 +542,11 @@ export const WorkspacePlannerHeader = observer( + setSearchText(val)} + onClear={handleClearSearch} + /> diff --git a/src/people/WorkSpacePlanner/index.tsx b/src/people/WorkSpacePlanner/index.tsx index 2c7ce2a9..f2a28779 100644 --- a/src/people/WorkSpacePlanner/index.tsx +++ b/src/people/WorkSpacePlanner/index.tsx @@ -138,6 +138,7 @@ const WorkspacePlanner = observer(() => { const [loading, setLoading] = useState(true); const [workspaceData, setWorkspaceData] = useState(null); const [filterToggle, setFilterToggle] = useState(false); + const [searchText, setSearchText] = useState(''); const bountyCardStore = useBountyCardStore(uuid); useEffect(() => { @@ -189,6 +190,8 @@ const WorkspacePlanner = observer(() => { workspaceData={workspaceData} filterToggle={filterToggle} setFilterToggle={setFilterToggle} + searchText={searchText} + setSearchText={setSearchText} /> @@ -209,13 +212,17 @@ const WorkspacePlanner = observer(() => { ) : bountyCardStore.error ? ( {bountyCardStore.error} ) : ( - groupedBounties[id]?.map((card: BountyCard) => ( - handleCardClick(card.id)} - /> - )) + groupedBounties[id] + ?.filter((card: BountyCard) => + card.title.toLowerCase().includes(searchText.toLowerCase()) + ) + .map((card: BountyCard) => ( + handleCardClick(card.id)} + /> + )) )} diff --git a/src/store/bountyCard.ts b/src/store/bountyCard.ts index 091a3323..ed4020ca 100644 --- a/src/store/bountyCard.ts +++ b/src/store/bountyCard.ts @@ -9,6 +9,7 @@ interface FilterState { selectedPhases: string[]; selectedStatuses: string[]; timestamp: number; + searchText: string; } export class BountyCardStore { @@ -20,6 +21,7 @@ export class BountyCardStore { @observable selectedFeatures: string[] = []; @observable selectedPhases: string[] = []; @observable selectedStatuses: string[] = []; + @observable searchText = ''; constructor(workspaceId: string) { this.currentWorkspaceId = workspaceId; @@ -145,6 +147,7 @@ export class BountyCardStore { selectedFeatures: this.selectedFeatures, selectedPhases: this.selectedPhases, selectedStatuses: this.selectedStatuses, + searchText: this.searchText, timestamp: Date.now() }) ); @@ -205,6 +208,8 @@ export class BountyCardStore { this.selectedPhases = []; sessionStorage.removeItem('bountyFilterState'); this.saveFilterState(); + this.selectedStatuses = []; + this.searchText = ''; } @action @@ -241,6 +246,13 @@ export class BountyCardStore { @computed get filteredBountyCards() { return this.bountyCards.filter((card: BountyCard) => { + const searchMatch = + !this.searchText || + [card.title, card.features?.name, card.phase?.name].some( + (field: string | undefined) => + field?.toLowerCase().includes(this.searchText.toLowerCase().trim()) + ); + const featureMatch = this.selectedFeatures.length === 0 || (this.selectedFeatures.includes('no-feature') && !card.features?.uuid) || @@ -254,7 +266,7 @@ export class BountyCardStore { this.selectedStatuses.length === 0 || (card.status && this.selectedStatuses.includes(card.status)); - return featureMatch && phaseMatch && statusMatch; + return searchMatch && featureMatch && phaseMatch && statusMatch; }); } @@ -262,6 +274,25 @@ export class BountyCardStore { get hasCardsWithoutFeatures() { return this.bountyCards.some((card: BountyCard) => !card.features?.uuid); } + + @action + setSearchText(text: string) { + this.searchText = text.trim(); + this.saveFilterState(); + } + + @action + clearSearch() { + this.searchText = ''; + this.saveFilterState(); + } + + private sanitizeSearchText(text: string): string { + return text + .replace(/\s+/g, ' ') + .replace(/[^\w\s-]/g, '') + .trim(); + } } export const useBountyCardStore = (workspaceId: string) =>