Skip to content

Commit

Permalink
Merge pull request stakwork#911 from jordan-ae/search-filters
Browse files Browse the repository at this point in the history
Update search based filtering on workspace board
  • Loading branch information
humansinstitute authored Jan 11, 2025
2 parents a2b1fc9 + 645e322 commit 92eb5db
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 10 deletions.
93 changes: 91 additions & 2 deletions src/people/WorkSpacePlanner/WorkspacePlannerHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -43,6 +45,8 @@ interface WorkspacePlannerHeaderProps {
};
filterToggle: boolean;
setFilterToggle: (a: boolean) => void;
searchText: string;
setSearchText: React.Dispatch<React.SetStateAction<string>>;
}

interface FeatureOption {
Expand All @@ -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;
Expand All @@ -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;
}) => (
<SearchInputContainer>
<input
type="text"
placeholder="Search tickets..."
value={value}
onChange={(e: any) => onChange(e.target.value)}
/>
{value && <button onClick={onClear}>×</button>}
</SearchInputContainer>
);

export const WorkspacePlannerHeader = observer(
({
workspace_uuid,
workspaceData,
filterToggle,
setFilterToggle
setFilterToggle,
searchText,
setSearchText
}: WorkspacePlannerHeaderProps) => {
const { main, ui } = useStores();
const [isPostBountyModalOpen, setIsPostBountyModalOpen] = useState(false);
Expand All @@ -78,12 +144,30 @@ export const WorkspacePlannerHeader = observer(
const [isPhasePopoverOpen, setIsPhasePopoverOpen] = useState<boolean>(false);
const [isStatusPopoverOpen, setIsStatusPopoverOpen] = useState<boolean>(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]);
Expand Down Expand Up @@ -458,6 +542,11 @@ export const WorkspacePlannerHeader = observer(
</div>
</EuiPopover>
</NewStatusContainer>
<SearchInput
value={searchText}
onChange={(val: string) => setSearchText(val)}
onClear={handleClearSearch}
/>
</FiltersRight>
</Filters>
</FillContainer>
Expand Down
21 changes: 14 additions & 7 deletions src/people/WorkSpacePlanner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const WorkspacePlanner = observer(() => {
const [loading, setLoading] = useState(true);
const [workspaceData, setWorkspaceData] = useState<any>(null);
const [filterToggle, setFilterToggle] = useState(false);
const [searchText, setSearchText] = useState('');
const bountyCardStore = useBountyCardStore(uuid);

useEffect(() => {
Expand Down Expand Up @@ -189,6 +190,8 @@ const WorkspacePlanner = observer(() => {
workspaceData={workspaceData}
filterToggle={filterToggle}
setFilterToggle={setFilterToggle}
searchText={searchText}
setSearchText={setSearchText}
/>
<ContentArea>
<ColumnsContainer>
Expand All @@ -209,13 +212,17 @@ const WorkspacePlanner = observer(() => {
) : bountyCardStore.error ? (
<ErrorMessage>{bountyCardStore.error}</ErrorMessage>
) : (
groupedBounties[id]?.map((card: BountyCard) => (
<BountyCardComp
key={card.id}
{...card}
onclick={() => handleCardClick(card.id)}
/>
))
groupedBounties[id]
?.filter((card: BountyCard) =>
card.title.toLowerCase().includes(searchText.toLowerCase())
)
.map((card: BountyCard) => (
<BountyCardComp
key={card.id}
{...card}
onclick={() => handleCardClick(card.id)}
/>
))
)}
</ColumnContent>
</Column>
Expand Down
33 changes: 32 additions & 1 deletion src/store/bountyCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface FilterState {
selectedPhases: string[];
selectedStatuses: string[];
timestamp: number;
searchText: string;
}

export class BountyCardStore {
Expand All @@ -20,6 +21,7 @@ export class BountyCardStore {
@observable selectedFeatures: string[] = [];
@observable selectedPhases: string[] = [];
@observable selectedStatuses: string[] = [];
@observable searchText = '';

constructor(workspaceId: string) {
this.currentWorkspaceId = workspaceId;
Expand Down Expand Up @@ -145,6 +147,7 @@ export class BountyCardStore {
selectedFeatures: this.selectedFeatures,
selectedPhases: this.selectedPhases,
selectedStatuses: this.selectedStatuses,
searchText: this.searchText,
timestamp: Date.now()
})
);
Expand Down Expand Up @@ -205,6 +208,8 @@ export class BountyCardStore {
this.selectedPhases = [];
sessionStorage.removeItem('bountyFilterState');
this.saveFilterState();
this.selectedStatuses = [];
this.searchText = '';
}

@action
Expand Down Expand Up @@ -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) ||
Expand All @@ -254,14 +266,33 @@ export class BountyCardStore {
this.selectedStatuses.length === 0 ||
(card.status && this.selectedStatuses.includes(card.status));

return featureMatch && phaseMatch && statusMatch;
return searchMatch && featureMatch && phaseMatch && statusMatch;
});
}

@computed
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) =>
Expand Down

0 comments on commit 92eb5db

Please sign in to comment.