Skip to content

Commit

Permalink
feat: implemented bounty card store
Browse files Browse the repository at this point in the history
  • Loading branch information
Ekep-Obasi committed Dec 25, 2024
1 parent 871c293 commit 7252a30
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 1 deletion.
38 changes: 37 additions & 1 deletion src/people/WorkSpacePlanner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { observer } from 'mobx-react-lite';
import { useParams } from 'react-router-dom';
import { EuiLoadingSpinner } from '@elastic/eui';
import styled from 'styled-components';
import { useBountyCardStore } from 'store/bountyCard';
import { BountyCard } from 'store/interface';
import { useStores } from '../../store';
import { colors } from '../../config';
import { WorkspacePlannerHeader } from './WorkspacePlannerHeader';
Expand All @@ -24,22 +26,38 @@ const ContentArea = styled.div`
padding: 20px;
`;

const BountyCardList = styled.ul`
list-style: none;
padding: 0;
margin: 20px 0;
li {
background: ${colors.light.grayish.G800};
margin: 10px 0;
padding: 15px;
border-radius: 5px;
text-align: left;
}
`;

const WorkspacePlanner = () => {
const { uuid } = useParams<{ uuid: string }>();
const { main } = useStores();
const [loading, setLoading] = useState(true);
const [workspaceData, setWorkspaceData] = useState<any>(null);
const bountyCardStore = useBountyCardStore(uuid);

useEffect(() => {
const fetchWorkspaceData = async () => {
if (!uuid) return;
const data = await main.getUserWorkspaceByUuid(uuid);
setWorkspaceData(data);
bountyCardStore.loadWorkspaceBounties();
setLoading(false);
};

fetchWorkspaceData();
}, [main, uuid]);
}, [main, uuid, bountyCardStore]);

if (loading) {
return (
Expand All @@ -54,6 +72,24 @@ const WorkspacePlanner = () => {
<WorkspacePlannerHeader workspace_uuid={uuid} workspaceData={workspaceData} />
<ContentArea>
<h1>Welcome to the new Workspace Planner</h1>
<h2>Bounty Cards</h2>
{bountyCardStore.loading ? (
<EuiLoadingSpinner size="m" />
) : bountyCardStore.error ? (
<p style={{ color: 'red' }}>{bountyCardStore.error}</p>
) : (
<BountyCardList>
{bountyCardStore.bountyCards.map((card: BountyCard) => (
<li key={card.id}>
<strong>{card.title}</strong>
</li>
))}
</BountyCardList>
)}
{bountyCardStore.pagination.currentPage * bountyCardStore.pagination.pageSize <
bountyCardStore.pagination.total && (
<button onClick={() => bountyCardStore.loadNextPage()}>Load More</button>
)}
</ContentArea>
</PlannerContainer>
);
Expand Down
115 changes: 115 additions & 0 deletions src/store/__test__/bountyCard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import sinon from 'sinon';
import { setupStore } from '__test__/__mockData__/setupStore';
import { waitFor } from '@testing-library/react';
import { BountyCardStore } from '../bountyCard';

describe('BountyCardStore', () => {
let store: BountyCardStore;
let fetchStub: sinon.SinonStub;
const mockWorkspaceId = 'test-workspace-123';

beforeAll(() => {
setupStore();
});

beforeEach(() => {
fetchStub = sinon.stub(global, 'fetch');
});

afterEach(() => {
sinon.restore();
jest.clearAllMocks();
});

describe('constructor', () => {
it('should initialize with correct default values', async () => {
store = await waitFor(() => (store = new BountyCardStore(mockWorkspaceId)));

expect(store.bountyCards).toEqual([]);
expect(store.currentWorkspaceId).toBe(mockWorkspaceId);
expect(store.loading).toBeFalsy();
expect(store.pagination).toEqual({
currentPage: 1,
pageSize: 10,
total: 0
});
});
});

describe('loadWorkspaceBounties', () => {
it('should handle successful bounty cards fetch', async () => {
const mockBounties = [{ id: 1, title: 'Test Bounty' }];
fetchStub.resolves({
ok: true,
json: async () => mockBounties
} as Response);

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

expect(store.bountyCards).toEqual(mockBounties);
expect(store.loading).toBe(false);
expect(store.error).toBeNull();
});

it('should handle failed bounty cards fetch', async () => {
const errorMessage = 'Failed to load bounties';
fetchStub.resolves({
ok: false,
statusText: errorMessage
} as Response);

store = await waitFor(() => (store = new BountyCardStore(mockWorkspaceId)));
expect(store.bountyCards).toEqual([]);
expect(store.loading).toBe(false);
expect(store.error).toBe(`Failed to load bounties: ${errorMessage}`);
});
});

describe('switchWorkspace', () => {
it('should switch workspace and reload bounties', async () => {
const newWorkspaceId = 'new-workspace-456';
const mockBounties = [{ id: 2, title: 'New Bounty' }];

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

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

await waitFor(() => store.switchWorkspace(newWorkspaceId));
expect(store.currentWorkspaceId).toBe(newWorkspaceId);
expect(store.pagination.currentPage).toBe(1);
expect(store.bountyCards).toEqual(mockBounties);
});

it('should not reload if workspace id is the same', async () => {
store = await waitFor(() => (store = new BountyCardStore(mockWorkspaceId)));
const initialFetchCount = fetchStub.callCount;

await waitFor(() => store.switchWorkspace(mockWorkspaceId));

expect(fetchStub.callCount).toBe(initialFetchCount);
});
});

describe('loadNextPage', () => {
it('should not load next page if already loading', async () => {
store = await waitFor(() => (store = new BountyCardStore(mockWorkspaceId)));

store.loading = true;

await waitFor(() => store.loadNextPage());
});

it('should not load next page if all items are loaded', async () => {
store = await waitFor(() => (store = new BountyCardStore(mockWorkspaceId)));

store.pagination.total = 10;
store.pagination.currentPage = 1;
store.pagination.pageSize = 10;

await waitFor(() => store.loadNextPage());
});
});
});
113 changes: 113 additions & 0 deletions src/store/bountyCard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { TribesURL } from 'config';
import { useMemo } from 'react';
import { BountyCard } from './interface';
import { uiStore } from './ui';

export class BountyCardStore {
bountyCards: BountyCard[] = [];
currentWorkspaceId: string;
loading = false;
error: string | null = null;
pagination = {
currentPage: 1,
pageSize: 10,
total: 0
};

constructor(workspaceId: string) {
this.currentWorkspaceId = workspaceId;
makeAutoObservable(this);
this.loadWorkspaceBounties();
}

private constructQueryParams(): string {
const { currentPage, pageSize } = this.pagination;
return new URLSearchParams({
page: currentPage.toString(),
limit: pageSize.toString()
}).toString();
}

loadWorkspaceBounties = async (): Promise<void> => {
if (!this.currentWorkspaceId || !uiStore.meInfo?.tribe_jwt) {
runInAction(() => {
this.error = 'Missing workspace ID or authentication';
});
return;
}

try {
runInAction(() => {
this.loading = true;
this.error = null;
});

const queryParams = this.constructQueryParams();
const response = await fetch(
`${TribesURL}/gobounties/bounty-cards?workspace_uuid=${this.currentWorkspaceId}&${queryParams}`,
{
method: 'GET',
headers: {
'x-jwt': uiStore.meInfo.tribe_jwt,
'Content-Type': 'application/json'
}
}
);

if (!response.ok) {
throw new Error(`Failed to load bounties: ${response.statusText}`);
}

const data = (await response.json()) as BountyCard[] | null;

runInAction(() => {
if (this.pagination.currentPage === 1) {
console.log({ workspace: this.currentWorkspaceId, data });
this.bountyCards = data || [];
} else {
this.bountyCards = [...this.bountyCards, ...(data || [])];
}
this.pagination.total = data?.length || 0;
});
} catch (error) {
runInAction(() => {
this.error = error instanceof Error ? error.message : 'An unknown error occurred';
});
} finally {
runInAction(() => {
this.loading = false;
});
}
};

switchWorkspace = async (newWorkspaceId: string): Promise<void> => {
if (this.currentWorkspaceId === newWorkspaceId) return;

runInAction(() => {
this.currentWorkspaceId = newWorkspaceId;
this.pagination.currentPage = 1;
this.bountyCards = [];
});

await this.loadWorkspaceBounties();
};

loadNextPage = async (): Promise<void> => {
if (
this.loading ||
this.pagination.currentPage * this.pagination.pageSize >= this.pagination.total
) {
return;
}

runInAction(() => {
this.pagination.currentPage += 1;
});

await this.loadWorkspaceBounties();
};
}

export const useBountyCardStore = (workspaceId: string) =>
useMemo(() => new BountyCardStore(workspaceId), [workspaceId]);
9 changes: 9 additions & 0 deletions src/store/interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Phase } from 'people/widgetViews/workspace/interface';
import { Extras } from '../components/form/inputs/widgets/interfaces';

export interface Tribe {
Expand Down Expand Up @@ -503,3 +504,11 @@ export interface CodeGraph {
created?: string;
updated?: string;
}

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

0 comments on commit 7252a30

Please sign in to comment.