From 26b5f1c1701d7db381c7bdc859c5a980d9d74ed0 Mon Sep 17 00:00:00 2001 From: MuhammadUmer44 Date: Sun, 29 Dec 2024 10:17:49 +0500 Subject: [PATCH 1/3] feat: add status calculation and display to bounty cards --- .../WorkSpacePlanner/BountyCard/index.tsx | 24 +++++++++- src/store/bountyCard.ts | 46 +++++++++++++++++-- src/store/interface.ts | 15 ++++++ 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/people/WorkSpacePlanner/BountyCard/index.tsx b/src/people/WorkSpacePlanner/BountyCard/index.tsx index 403f60f7..145cf7fe 100644 --- a/src/people/WorkSpacePlanner/BountyCard/index.tsx +++ b/src/people/WorkSpacePlanner/BountyCard/index.tsx @@ -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); @@ -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; } @@ -99,6 +115,7 @@ const BountyCardComponent: React.FC = ({ phase, assignee_img, workspace, + status, onclick }: BountyCardProps) => ( onclick(id)}> @@ -124,7 +141,9 @@ const BountyCardComponent: React.FC = ({ {truncate(workspace?.name ?? 'No Workspace', 20)} - Paid? + + {status || 'Todo'} + ); @@ -142,6 +161,7 @@ BountyCardComponent.propTypes = { workspace: PropTypes.shape({ name: PropTypes.string }) as PropTypes.Validator, + status: PropTypes.oneOf(['Todo', 'Assigned', 'Complete', 'Paid'] as BountyCardStatus[]), onclick: PropTypes.func.isRequired }; diff --git a/src/store/bountyCard.ts b/src/store/bountyCard.ts index 50d38525..646b89f3 100644 --- a/src/store/bountyCard.ts +++ b/src/store/bountyCard.ts @@ -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 { @@ -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 => { if (!this.currentWorkspaceId || !uiStore.meInfo?.tribe_jwt) { runInAction(() => { @@ -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; }); @@ -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) => diff --git a/src/store/interface.ts b/src/store/interface.ts index b5460c34..6f037772 100644 --- a/src/store/interface.ts +++ b/src/store/interface.ts @@ -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; @@ -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; +} From 775f760fe264479b229f111061a38b8bd66e541a Mon Sep 17 00:00:00 2001 From: MuhammadUmer44 Date: Sun, 29 Dec 2024 10:50:00 +0500 Subject: [PATCH 2/3] fix unit test --- src/store/__test__/bountyCard.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/__test__/bountyCard.spec.ts b/src/store/__test__/bountyCard.spec.ts index 72cf0464..94287203 100644 --- a/src/store/__test__/bountyCard.spec.ts +++ b/src/store/__test__/bountyCard.spec.ts @@ -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(); }); @@ -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 () => { From 4e80319d42ee7e749aadd1d43fd2c89a8fbafda8 Mon Sep 17 00:00:00 2001 From: MuhammadUmer44 Date: Sun, 29 Dec 2024 11:14:14 +0500 Subject: [PATCH 3/3] test(bountyCard): add status calculation tests --- .../WorkSpacePlanner/BountyCard/index.tsx | 2 +- src/store/__test__/bountyCard.spec.ts | 126 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/people/WorkSpacePlanner/BountyCard/index.tsx b/src/people/WorkSpacePlanner/BountyCard/index.tsx index 145cf7fe..4a99d351 100644 --- a/src/people/WorkSpacePlanner/BountyCard/index.tsx +++ b/src/people/WorkSpacePlanner/BountyCard/index.tsx @@ -142,7 +142,7 @@ const BountyCardComponent: React.FC = ({ {truncate(workspace?.name ?? 'No Workspace', 20)} - {status || 'Todo'} + {status} diff --git a/src/store/__test__/bountyCard.spec.ts b/src/store/__test__/bountyCard.spec.ts index 94287203..948eba22 100644 --- a/src/store/__test__/bountyCard.spec.ts +++ b/src/store/__test__/bountyCard.spec.ts @@ -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); + }); + }); + }); });