Skip to content

Commit

Permalink
Merge pull request stakwork#929 from jordan-ae/db-filters
Browse files Browse the repository at this point in the history
Add an Assignee Filter to Workspace Planner
  • Loading branch information
humansinstitute authored Jan 15, 2025
2 parents 98133b2 + f42574e commit e72c4d1
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 13 deletions.
8 changes: 7 additions & 1 deletion src/people/WorkSpacePlanner/BountyCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const CardTitle = styled.h3`
&:hover {
color: ${colors.light.primaryColor};
}
display: flex;
flex-direction: column;
gap: 5px;
`;

const AssignerPic = styled.div`
Expand Down Expand Up @@ -117,7 +120,8 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
assignee_img,
workspace,
status,
onclick
onclick,
assignee_name
}: BountyCardProps) => (
<CardContainer onClick={() => onclick(id)}>
<CardHeader>
Expand All @@ -130,6 +134,7 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
}}
>
{title}
<span style={{ fontSize: '16px', marginTop: '10px' }}>{assignee_name}</span>
</CardTitle>
{assignee_img && (
<AssignerPic>
Expand All @@ -140,6 +145,7 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({

<RowT>
<span title={features?.name ?? 'No Feature'}>
{/* <span>Jordan</span> */}
{truncate(features?.name ?? 'No Feature', 10)}
</span>
<span title={phase?.name ?? 'No Phase'}>{truncate(phase?.name ?? 'No Phase', 20)}</span>
Expand Down
92 changes: 92 additions & 0 deletions src/people/WorkSpacePlanner/WorkspacePlannerHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export const WorkspacePlannerHeader = observer(
const [canPostBounty, setCanPostBounty] = useState(false);
const [isFeaturePopoverOpen, setIsFeaturePopoverOpen] = useState<boolean>(false);
const [isPhasePopoverOpen, setIsPhasePopoverOpen] = useState<boolean>(false);
const [isAssigneePopoverOpen, setIsAssigneePopoverOpen] = useState<boolean>(false);
const [isStatusPopoverOpen, setIsStatusPopoverOpen] = useState<boolean>(false);
const bountyCardStore = useBountyCardStore(workspace_uuid);
const [debouncedSearch, setDebouncedSearch] = useState('');
Expand Down Expand Up @@ -199,9 +200,14 @@ export const WorkspacePlannerHeader = observer(
setIsStatusPopoverOpen((isPopoverOpen: boolean) => !isPopoverOpen);
};

const onAssigneeButtonClick = (): void => {
setIsAssigneePopoverOpen((isPopoverOpen: boolean) => !isPopoverOpen);
};

const closeFeaturePopover = () => setIsFeaturePopoverOpen(false);
const closePhasePopover = () => setIsPhasePopoverOpen(false);
const closeStatusPopover = () => setIsStatusPopoverOpen(false);
const closeAssigneePopover = () => setIsAssigneePopoverOpen(false);

const getFeatureOptions = (): FeatureOption[] => {
const options: FeatureOption[] = [];
Expand Down Expand Up @@ -542,6 +548,92 @@ export const WorkspacePlannerHeader = observer(
</div>
</EuiPopover>
</NewStatusContainer>

<NewStatusContainer>
<EuiPopover
button={
<StatusContainer
onClick={onAssigneeButtonClick}
color={color}
// style={{
// opacity: isPhaseFilterDisabled ? 0.5 : 1,
// cursor: isPhaseFilterDisabled ? 'not-allowed' : 'pointer'
// }}
>
<InnerContainer>
<EuiText className="statusText">Assignee</EuiText>
<Formatter>
{bountyCardStore.selectedAssignees.length > 0 && (
<FilterCount color={color}>
<EuiText className="filterCountText">
{bountyCardStore.selectedAssignees.length}
</EuiText>
</FilterCount>
)}
</Formatter>
<MaterialIcon
icon={isAssigneePopoverOpen ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
/>
</InnerContainer>
</StatusContainer>
}
isOpen={isAssigneePopoverOpen}
closePopover={closeAssigneePopover}
panelStyle={{
border: 'none',
boxShadow: `0px 1px 20px ${color.black90}`,
background: `${color.pureWhite}`,
borderRadius: '0px 0px 6px 6px',
maxWidth: '140px',
minHeight: '160px',
marginTop: '0px',
marginLeft: '20px'
}}
panelClassName="yourClassNameHere"
panelPaddingSize="none"
anchorPosition="downLeft"
>
<div style={{ display: 'flex', flex: 'row' }}>
<EuiPopOverCheckbox className="CheckboxOuter" color={color}>
<EuiCheckboxGroup
options={bountyCardStore.availableAssignees.map((assignee: any) => ({
label: assignee.name,
id: assignee
}))}
idToSelectedMap={bountyCardStore.availableAssignees.reduce(
(acc: { [key: string]: boolean }, assignee: any) => {
acc[assignee] = bountyCardStore.selectedAssignees.includes(assignee);
return acc;
},
{}
)}
onChange={(id: string) => {
bountyCardStore.toggleAssignee(id);
setFilterToggle(!filterToggle);
}}
/>
{bountyCardStore.selectedAssignees.length > 0 && (
<div
style={{
padding: '8px 16px',
borderTop: `1px solid ${color.grayish.G800}`
}}
>
<ClearButton
onClick={(e: React.MouseEvent): void => {
e.stopPropagation();
bountyCardStore.clearAssigneeFilters();
setFilterToggle(!filterToggle);
}}
>
Clear All
</ClearButton>
</div>
)}
</EuiPopOverCheckbox>
</div>
</EuiPopover>
</NewStatusContainer>
<SearchInput
value={searchText}
onChange={(val: string) => setSearchText(val)}
Expand Down
2 changes: 1 addition & 1 deletion src/people/widgetViews/WorkspaceMission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ const WorkspaceMission = () => {
<Box fontSize={20} textAlign="center">
Are you sure you want to <br />
<Box component="span" fontWeight="500">
Delete this Repo?
Delete this Repo? h
</Box>
</Box>
)
Expand Down
70 changes: 69 additions & 1 deletion src/store/bountyCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ interface FilterState {
selectedFeatures: string[];
selectedPhases: string[];
selectedStatuses: string[];
selectedAssignees: string[];
timestamp: number;
searchText: string;
}

interface Assignee {
id: string;
name: string;
count: number;
}

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

constructor(workspaceId: string) {
Expand Down Expand Up @@ -147,6 +155,7 @@ export class BountyCardStore {
selectedFeatures: this.selectedFeatures,
selectedPhases: this.selectedPhases,
selectedStatuses: this.selectedStatuses,
selectedAssignees: this.selectedAssignees,
searchText: this.searchText,
timestamp: Date.now()
})
Expand All @@ -162,6 +171,7 @@ export class BountyCardStore {
this.selectedFeatures = state.selectedFeatures;
this.selectedPhases = state.selectedPhases;
this.selectedStatuses = state.selectedStatuses;
this.selectedAssignees = state.selectedAssignees || [];
});
}
}
Expand All @@ -175,6 +185,16 @@ export class BountyCardStore {
this.saveFilterState();
}

@action
toggleAssignee(assigneeId: string) {
if (this.selectedAssignees.includes(assigneeId)) {
this.selectedAssignees = this.selectedAssignees.filter((id: string) => id !== assigneeId);
} else {
this.selectedAssignees.push(assigneeId);
}
this.saveFilterState();
}

@action
togglePhase(phaseId: string) {
if (this.selectedPhases.includes(phaseId)) {
Expand All @@ -199,6 +219,7 @@ export class BountyCardStore {
clearAllFilters() {
this.selectedFeatures = [];
this.selectedPhases = [];
this.selectedAssignees = [];
sessionStorage.removeItem('bountyFilterState');
this.saveFilterState();
}
Expand Down Expand Up @@ -243,6 +264,42 @@ export class BountyCardStore {
return Array.from(uniquePhases.values());
}

@computed
get availableAssignees(): any[] {
const assigneeCounts = new Map<string, any>();

// Add "Unassigned" option with count of unassigned bounties
const unassignedCount = this.bountyCards.filter((card: BountyCard) => !card.assignee).length;
assigneeCounts.set('unassigned', {
id: 'unassigned',
name: 'Unassigned',
count: unassignedCount
});

// Count cards per assignee
this.bountyCards.forEach((card: BountyCard) => {
if (card.assignee_name) {
const existing = assigneeCounts.get(card.assignee);
if (existing) {
existing.count++;
} else {
assigneeCounts.set(card.assignee, {
id: card.assignee,
name: card.assignee_name,
count: 1
});
}
}
});

// Convert to array and sort by name (keeping Unassigned at top)
return Array.from(assigneeCounts.values()).sort((a: Assignee, b: Assignee) => {
if (a.id === 'unassigned') return -1;
if (b.id === 'unassigned') return 1;
return a.name.localeCompare(b.name);
});
}

@computed
get filteredBountyCards() {
return this.bountyCards.filter((card: BountyCard) => {
Expand All @@ -266,7 +323,12 @@ export class BountyCardStore {
this.selectedStatuses.length === 0 ||
(card.status && this.selectedStatuses.includes(card.status));

return searchMatch && featureMatch && phaseMatch && statusMatch;
const assigneeMatch =
this.selectedAssignees.length === 0 ||
(this.selectedAssignees.includes('unassigned') && !card.assignee_img) ||
(card.assignee && this.selectedAssignees.includes(card.assignee));

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

Expand All @@ -287,6 +349,12 @@ export class BountyCardStore {
this.saveFilterState();
}

@action
clearAssigneeFilters() {
this.selectedAssignees = [];
this.saveFilterState();
}

private sanitizeSearchText(text: string): string {
return text
.replace(/\s+/g, ' ')
Expand Down
12 changes: 2 additions & 10 deletions src/store/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,15 +510,6 @@ export interface CodeGraph {
updated?: string;
}

export interface BountyCard {
id: string;
title: string;
features: Feature;
phase: Phase;
workspace: Workspace;
assignee_img?: string;
}

export interface BountyCard {
id: string;
title: string;
Expand All @@ -530,7 +521,8 @@ export interface BountyCard {
paid?: boolean;
completed?: boolean;
payment_pending?: boolean;
assignee?: string;
assignee?: any;
assignee_name?: string;
pow?: number;
}

Expand Down

0 comments on commit e72c4d1

Please sign in to comment.