Skip to content

Commit

Permalink
Merge pull request #823 from MuhammadUmer44/feature/bounty-card-statu…
Browse files Browse the repository at this point in the history
…s-display

🚀✨ Add Status Calculation and Display to BountyCard 🔥📝💡
  • Loading branch information
humansinstitute authored Dec 29, 2024
2 parents 0a81066 + 4e80319 commit 1b4c015
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 8 deletions.
24 changes: 22 additions & 2 deletions src/people/WorkSpacePlanner/BountyCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { BountyCard } from '../../../store/interface';
import { BountyCard, BountyCardStatus } from '../../../store/interface';
import { colors } from '../../../config';

const truncate = (str: string, n: number) => (str.length > n ? `${str.substr(0, n - 1)}...` : str);
Expand Down Expand Up @@ -88,6 +88,22 @@ const RowB = styled.div`
}
`;

const StatusText = styled.span<{ status?: BountyCardStatus }>`
color: ${({ status }: { status?: BountyCardStatus }): string => {
switch (status) {
case 'Paid':
return colors.light.statusPaid;
case 'Complete':
return colors.light.statusCompleted;
case 'Assigned':
return colors.light.statusAssigned;
default:
return colors.light.pureBlack;
}
}};
font-weight: 500;
`;

interface BountyCardProps extends BountyCard {
onclick: (bountyId: string) => void;
}
Expand All @@ -99,6 +115,7 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
phase,
assignee_img,
workspace,
status,
onclick
}: BountyCardProps) => (
<CardContainer onClick={() => onclick(id)}>
Expand All @@ -124,7 +141,9 @@ const BountyCardComponent: React.FC<BountyCardProps> = ({
<span title={workspace?.name ?? 'No Workspace'}>
{truncate(workspace?.name ?? 'No Workspace', 20)}
</span>
<span className="last-span">Paid?</span>
<StatusText className="last-span" status={status}>
{status}
</StatusText>
</RowB>
</CardContainer>
);
Expand All @@ -142,6 +161,7 @@ BountyCardComponent.propTypes = {
workspace: PropTypes.shape({
name: PropTypes.string
}) as PropTypes.Validator<BountyCard['workspace']>,
status: PropTypes.oneOf(['Todo', 'Assigned', 'Complete', 'Paid'] as BountyCardStatus[]),
onclick: PropTypes.func.isRequired
};

Expand Down
130 changes: 128 additions & 2 deletions src/store/__test__/bountyCard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('BountyCardStore', () => {

store = await waitFor(() => new BountyCardStore(mockWorkspaceId));

expect(store.bountyCards).toEqual(mockBounties);
expect(store.bountyCards).toEqual([{ ...mockBounties[0], status: 'Todo' }]);
expect(store.loading).toBe(false);
expect(store.error).toBeNull();
});
Expand Down Expand Up @@ -81,7 +81,7 @@ describe('BountyCardStore', () => {
await waitFor(() => store.switchWorkspace(newWorkspaceId));
expect(store.currentWorkspaceId).toBe(newWorkspaceId);
expect(store.pagination.currentPage).toBe(1);
expect(store.bountyCards).toEqual(mockBounties);
expect(store.bountyCards).toEqual([{ ...mockBounties[0], status: 'Todo' }]);
});

it('should not reload if workspace id is the same', async () => {
Expand Down Expand Up @@ -113,4 +113,130 @@ describe('BountyCardStore', () => {
await waitFor(() => store.loadNextPage());
});
});

describe('calculateBountyStatus', () => {
let store: BountyCardStore;

beforeEach(async () => {
store = await waitFor(() => new BountyCardStore(mockWorkspaceId));
});

it('should return "Paid" when bounty is paid', async () => {
const mockBounty = {
id: '1',
title: 'Test Bounty',
paid: true,
completed: true,
payment_pending: false,
assignee_img: 'test.jpg'
};

fetchStub.resolves({
ok: true,
json: async () => [mockBounty]
} as Response);

await store.loadWorkspaceBounties();
expect(store.bountyCards[0].status).toBe('Paid');
});

it('should return "Complete" when bounty is completed but not paid', async () => {
const mockBounty = {
id: '1',
title: 'Test Bounty',
paid: false,
completed: true,
payment_pending: false,
assignee_img: 'test.jpg'
};

fetchStub.resolves({
ok: true,
json: async () => [mockBounty]
} as Response);

await store.loadWorkspaceBounties();
expect(store.bountyCards[0].status).toBe('Complete');
});

it('should return "Complete" when payment is pending', async () => {
const mockBounty = {
id: '1',
title: 'Test Bounty',
paid: false,
completed: false,
payment_pending: true,
assignee_img: 'test.jpg'
};

fetchStub.resolves({
ok: true,
json: async () => [mockBounty]
} as Response);

await store.loadWorkspaceBounties();
expect(store.bountyCards[0].status).toBe('Complete');
});

it('should return "Assigned" when bounty has assignee but not completed or paid', async () => {
const mockBounty = {
id: '1',
title: 'Test Bounty',
paid: false,
completed: false,
payment_pending: false,
assignee_img: 'test.jpg'
};

fetchStub.resolves({
ok: true,
json: async () => [mockBounty]
} as Response);

await store.loadWorkspaceBounties();
expect(store.bountyCards[0].status).toBe('Assigned');
});

it('should return "Todo" when bounty has no assignee and is not completed or paid', async () => {
const mockBounty = {
id: '1',
title: 'Test Bounty',
paid: false,
completed: false,
payment_pending: false,
assignee_img: undefined
};

fetchStub.resolves({
ok: true,
json: async () => [mockBounty]
} as Response);

await store.loadWorkspaceBounties();
expect(store.bountyCards[0].status).toBe('Todo');
});

describe('computed status lists', () => {
it('should correctly filter bounties by status', async () => {
const mockBounties = [
{ id: '1', title: 'Bounty 1', paid: true, completed: true, assignee_img: 'test.jpg' },
{ id: '2', title: 'Bounty 2', completed: true, assignee_img: 'test.jpg' },
{ id: '3', title: 'Bounty 3', assignee_img: 'test.jpg' },
{ id: '4', title: 'Bounty 4' }
];

fetchStub.resolves({
ok: true,
json: async () => mockBounties
} as Response);

await store.loadWorkspaceBounties();

expect(store.paidItems.length).toBe(1);
expect(store.completedItems.length).toBe(1);
expect(store.assignedItems.length).toBe(1);
expect(store.todoItems.length).toBe(1);
});
});
});
});
46 changes: 42 additions & 4 deletions src/store/bountyCard.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { makeAutoObservable, runInAction, computed } from 'mobx';
import { TribesURL } from 'config';
import { useMemo } from 'react';
import { BountyCard } from './interface';
import { BountyCard, BountyCardStatus } from './interface';
import { uiStore } from './ui';

export class BountyCardStore {
Expand Down Expand Up @@ -29,6 +29,19 @@ export class BountyCardStore {
}).toString();
}

private calculateBountyStatus(bounty: BountyCard): BountyCardStatus {
if (bounty.paid) {
return 'Paid';
}
if (bounty.completed || bounty.payment_pending) {
return 'Complete';
}
if (bounty.assignee_img) {
return 'Assigned';
}
return 'Todo';
}

loadWorkspaceBounties = async (): Promise<void> => {
if (!this.currentWorkspaceId || !uiStore.meInfo?.tribe_jwt) {
runInAction(() => {
Expand Down Expand Up @@ -63,9 +76,18 @@ export class BountyCardStore {

runInAction(() => {
if (this.pagination.currentPage === 1) {
this.bountyCards = data || [];
this.bountyCards = (data || []).map((bounty: BountyCard) => ({
...bounty,
status: this.calculateBountyStatus(bounty)
}));
} else {
this.bountyCards = [...this.bountyCards, ...(data || [])];
this.bountyCards = [
...this.bountyCards,
...(data || []).map((bounty: BountyCard) => ({
...bounty,
status: this.calculateBountyStatus(bounty)
}))
];
}
this.pagination.total = data?.length || 0;
});
Expand Down Expand Up @@ -106,6 +128,22 @@ export class BountyCardStore {

await this.loadWorkspaceBounties();
};

@computed get todoItems() {
return this.bountyCards.filter((card: BountyCard) => card.status === 'Todo');
}

@computed get assignedItems() {
return this.bountyCards.filter((card: BountyCard) => card.status === 'Assigned');
}

@computed get completedItems() {
return this.bountyCards.filter((card: BountyCard) => card.status === 'Complete');
}

@computed get paidItems() {
return this.bountyCards.filter((card: BountyCard) => card.status === 'Paid');
}
}

export const useBountyCardStore = (workspaceId: string) =>
Expand Down
15 changes: 15 additions & 0 deletions src/store/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ export type ChatRole = 'user' | 'assistant';
export type ChatStatus = 'sending' | 'sent' | 'error';
export type ContextTagType = 'productBrief' | 'featureBrief' | 'schematic';
export type ChatSource = 'user' | 'agent';
export type BountyCardStatus = 'Todo' | 'Assigned' | 'Complete' | 'Paid';

export interface ContextTag {
type: ContextTagType;
Expand Down Expand Up @@ -513,3 +514,17 @@ export interface BountyCard {
workspace: Workspace;
assignee_img?: string;
}

export interface BountyCard {
id: string;
title: string;
features: Feature;
phase: Phase;
workspace: Workspace;
assignee_img?: string;
status?: BountyCardStatus;
paid?: boolean;
completed?: boolean;
payment_pending?: boolean;
assignee?: string;
}

0 comments on commit 1b4c015

Please sign in to comment.