Skip to content

Commit

Permalink
Merge pull request #1061 from jordan-ae/activity-store
Browse files Browse the repository at this point in the history
Implement TypeScript Interfaces and MobX Store to Manage Activities
  • Loading branch information
humansinstitute authored Feb 12, 2025
2 parents d201f9f + 749dcf7 commit 4c2ff36
Show file tree
Hide file tree
Showing 4 changed files with 485 additions and 1 deletion.
234 changes: 234 additions & 0 deletions src/store/__test__/activityStore.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { activityStore } from '../activityStore';
import { mainStore } from '../main';
import { IActivity, INewActivity } from '../interface';

jest.mock('../main');

describe('ActivityStore', () => {
const mockActivity: IActivity = {
id: '1',
threadId: null,
sequence: 1,
contentType: 'feature_creation',
content: 'Test activity',
workspace: 'test',
featureUUID: 'feature1',
phaseUUID: 'phase1',
feedback: undefined,
actions: [],
questions: [],
timeCreated: new Date().toISOString(),
timeUpdated: new Date().toISOString(),
status: 'active',
author: 'human',
authorRef: 'user1'
};

beforeEach(() => {
activityStore.activities.clear();
activityStore.loading = false;
activityStore.error = null;
});

describe('fetchWorkspaceActivities', () => {
it('should fetch and store activities', async () => {
const mockActivities = [mockActivity];
(mainStore.fetchWorkspaceActivities as jest.Mock).mockResolvedValue(mockActivities);

await activityStore.fetchWorkspaceActivities('test');

expect(activityStore.activities.size).toBe(1);
expect(activityStore.activities.get('1')).toEqual(mockActivity);
expect(activityStore.loading).toBe(false);
});

it('should handle errors', async () => {
(mainStore.fetchWorkspaceActivities as jest.Mock).mockRejectedValue(
new Error('Fetch failed')
);

await activityStore.fetchWorkspaceActivities('test');

expect(activityStore.error).toBe('Fetch failed');
expect(activityStore.loading).toBe(false);
});
});

describe('createActivity', () => {
it('should create and store a new activity', async () => {
const newActivity: INewActivity = {
workspace: 'test',
content: 'New activity',
contentType: 'feature_creation',
featureUUID: 'feature1',
phaseUUID: 'phase1',
author: 'human',
authorRef: 'user1'
};

(mainStore.createActivity as jest.Mock).mockResolvedValue(mockActivity);

const result = await activityStore.createActivity(newActivity);

expect(result).toEqual(mockActivity);
expect(activityStore.activities.get('1')).toEqual(mockActivity);
});

it('should handle creation errors', async () => {
const newActivity: INewActivity = {
workspace: 'test',
content: 'New activity',
contentType: 'feature_creation',
featureUUID: 'feature1',
phaseUUID: 'phase1',
author: 'human',
authorRef: 'user1'
};

(mainStore.createActivity as jest.Mock).mockRejectedValue(new Error('Creation failed'));

const result = await activityStore.createActivity(newActivity);

expect(result).toBeNull();
expect(activityStore.error).toBe('Creation failed');
});
});

describe('createThreadResponse', () => {
it('should create a thread response', async () => {
const threadResponse: INewActivity = {
workspace: 'test',
content: 'Thread response',
contentType: 'general_update',
featureUUID: 'feature1',
phaseUUID: 'phase1',
author: 'human',
authorRef: 'user1'
};

const mockResponse = {
...mockActivity,
id: '2',
threadId: '1'
};

(mainStore.createActivity as jest.Mock).mockResolvedValue(mockResponse);

const result = await activityStore.createThreadResponse('1', threadResponse);

expect(result).toEqual(mockResponse);
expect(activityStore.activities.get('2')).toEqual(mockResponse);
});
});

describe('updateActivity', () => {
it('should update an activity', async () => {
activityStore.activities.set('1', mockActivity);
const updates = { content: 'Updated content' };

(mainStore.updateActivity as jest.Mock).mockResolvedValue(true);

const result = await activityStore.updateActivity('1', updates);

expect(result).toBe(true);
expect(activityStore.activities.get('1')?.content).toBe('Updated content');
});

it('should handle update errors', async () => {
activityStore.activities.set('1', mockActivity);
const updates = { content: 'Updated content' };

(mainStore.updateActivity as jest.Mock).mockRejectedValue(new Error('Update failed'));

const result = await activityStore.updateActivity('1', updates);

expect(result).toBe(false);
expect(activityStore.error).toBe('Update failed');
});
});

describe('deleteActivity', () => {
it('should delete an activity', async () => {
activityStore.activities.set('1', mockActivity);

(mainStore.deleteActivity as jest.Mock).mockResolvedValue(true);

const result = await activityStore.deleteActivity('1');

expect(result).toBe(true);
expect(activityStore.activities.has('1')).toBe(false);
});

it('should handle deletion errors', async () => {
activityStore.activities.set('1', mockActivity);

(mainStore.deleteActivity as jest.Mock).mockRejectedValue(new Error('Deletion failed'));

const result = await activityStore.deleteActivity('1');

expect(result).toBe(false);
expect(activityStore.error).toBe('Deletion failed');
});
});

describe('threadedActivities', () => {
it('should group activities by thread', () => {
const threadActivity: IActivity = {
...mockActivity,
threadId: '1',
id: '2'
};

activityStore.activities.set('1', mockActivity);
activityStore.activities.set('2', threadActivity);

const threaded = activityStore.threadedActivities;
expect(threaded['1'].length).toBe(2);
expect(threaded['1'][0]).toEqual(mockActivity);
expect(threaded['1'][1]).toEqual(threadActivity);
});
});

describe('rootActivities', () => {
it('should return root activities sorted by date', () => {
const olderActivity: IActivity = {
...mockActivity,
id: '2',
timeCreated: new Date('2023-01-01').toISOString()
};

activityStore.activities.set('1', mockActivity);
activityStore.activities.set('2', olderActivity);

const roots = activityStore.rootActivities;
expect(roots.length).toBe(2);
expect(roots[0].id).toBe('1');
expect(roots[1].id).toBe('2');
});
});

describe('getThreadResponses', () => {
it('should return responses for a thread', () => {
const threadResponse: IActivity = {
...mockActivity,
id: '2',
threadId: '1'
};

activityStore.activities.set('1', mockActivity);
activityStore.activities.set('2', threadResponse);

const responses = activityStore.getThreadResponses('1');
expect(responses.length).toBe(1);
expect(responses[0]).toEqual(threadResponse);
});
});

describe('clearError', () => {
it('should clear the error state', () => {
activityStore.error = 'Test error';
activityStore.clearError();
expect(activityStore.error).toBeNull();
});
});
});
133 changes: 133 additions & 0 deletions src/store/activityStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { IActivity, INewActivity } from './interface';
import { mainStore } from './main';

export class ActivityStore {
activities: Map<string, IActivity> = new Map();
loading = false;
error: string | null = null;

constructor() {
makeAutoObservable(this);
}

get threadedActivities(): { [key: string]: IActivity[] } {
const grouped: { [key: string]: IActivity[] } = {};
Array.from(this.activities.values()).forEach((activity: IActivity) => {
const threadId = activity.threadId || activity.id;
if (!grouped[threadId]) {
grouped[threadId] = [];
}
grouped[threadId].push(activity);
});
Object.keys(grouped).forEach((threadId: string) => {
grouped[threadId].sort((a: IActivity, b: IActivity) => a.sequence - b.sequence);
});
return grouped;
}

get rootActivities(): IActivity[] {
return Array.from(this.activities.values())
.filter((activity: IActivity) => !activity.threadId)
.sort(
(a: IActivity, b: IActivity) =>
new Date(b.timeCreated).getTime() - new Date(a.timeCreated).getTime()
);
}

getThreadResponses(threadId: string): IActivity[] {
return Array.from(this.activities.values())
.filter((activity: IActivity) => activity.threadId === threadId)
.sort((a: IActivity, b: IActivity) => a.sequence - b.sequence);
}

async fetchWorkspaceActivities(workspace: string): Promise<void> {
this.loading = true;
this.error = null;
try {
const activities = await mainStore.fetchWorkspaceActivities(workspace);
runInAction(() => {
this.activities.clear();
activities.forEach((activity: IActivity) => {
this.activities.set(activity.id, activity);
});
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.error = error instanceof Error ? error.message : 'Unknown error';
this.loading = false;
});
}
}

async createActivity(newActivity: INewActivity): Promise<IActivity | null> {
try {
const activity = await mainStore.createActivity(newActivity);
if (activity) {
runInAction(() => {
this.activities.set(activity.id, activity);
});
}
return activity;
} catch (error) {
runInAction(() => {
this.error = error instanceof Error ? error.message : 'Unknown error';
});
return null;
}
}

async createThreadResponse(
threadId: string,
newActivity: Omit<INewActivity, 'threadId'>
): Promise<IActivity | null> {
return this.createActivity({ ...newActivity, threadId });
}

async updateActivity(
id: string,
updates: Partial<Omit<IActivity, 'id' | 'threadId' | 'sequence'>>
): Promise<boolean> {
try {
const success = await mainStore.updateActivity(id, updates);
if (success) {
const activity = this.activities.get(id);
if (activity) {
runInAction(() => {
this.activities.set(id, { ...activity, ...updates });
});
}
}
return success;
} catch (error) {
runInAction(() => {
this.error = error instanceof Error ? error.message : 'Unknown error';
});
return false;
}
}

async deleteActivity(id: string): Promise<boolean> {
try {
const success = await mainStore.deleteActivity(id);
if (success) {
runInAction(() => {
this.activities.delete(id);
});
}
return success;
} catch (error) {
runInAction(() => {
this.error = error instanceof Error ? error.message : 'Unknown error';
});
return false;
}
}

clearError(): void {
this.error = null;
}
}

export const activityStore = new ActivityStore();
Loading

0 comments on commit 4c2ff36

Please sign in to comment.