Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
8eb1304
feat: create useFindSession Hook
david-roper Oct 24, 2025
a841790
feat: make useSession hook return one session instead of array
david-roper Oct 27, 2025
39fc54a
feat: add hook to return list of user ids
david-roper Oct 27, 2025
0b74c71
feat: new api reqs for finding sessions
david-roper Oct 27, 2025
58b2a55
feat: add query from nest
david-roper Nov 6, 2025
bfd6425
feat: add userinfo method
david-roper Nov 6, 2025
0911db7
refactor: remove unused useFindSession hook
david-roper Nov 10, 2025
814e882
feat: add userInfo call to useEffect to get userId from the session
david-roper Nov 10, 2025
13f063d
chore: rename user id column
david-roper Nov 10, 2025
ce76213
feat: collect username with user api call
david-roper Nov 12, 2025
1241f72
refactor: move session and user api methods to separate hook files
david-roper Nov 12, 2025
4c2b614
feat: create mocks for findUser and findSession hooks
david-roper Nov 12, 2025
8f46e50
test: fix use tests with wait for methods
david-roper Nov 13, 2025
f9828f9
refactor: make Username a standalone column in long export formats
david-roper Nov 14, 2025
b8e362a
test: change tests to include username
david-roper Nov 14, 2025
ee04c04
chore: small changes to test
david-roper Nov 14, 2025
279da7c
fix: fix description in session controller
david-roper Nov 17, 2025
f46ddd2
fix: add ability to find sessions list
david-roper Nov 17, 2025
6cc7066
fix: error msg in usefindsession
david-roper Nov 17, 2025
2e72fde
fix: throw and error to catch instead of returning null when error oc…
david-roper Nov 17, 2025
8d545ad
feat: use schema parsing to confirm contents instead of casting it
david-roper Nov 17, 2025
307effc
feat: adjust username variable to start as N/A, adjust tests
david-roper Nov 17, 2025
527543c
fix: fix type exports
david-roper Nov 17, 2025
8ed3922
test: change positions of subjectId and username column to make linte…
david-roper Nov 17, 2025
f88971d
refactor: remove unused api call
david-roper Nov 17, 2025
f53062a
chore: linter fixes
david-roper Nov 18, 2025
6d1b710
test: add resolved promise is session and userinfo mocked methods
david-roper Nov 18, 2025
fa97ac9
fix: remove extra append statement in excel download method
david-roper Nov 18, 2025
51e121a
feat: fix not finding user id issue by making subject inclusion optional
david-roper Nov 18, 2025
53016ce
chore: revert session schema and parse change
david-roper Nov 18, 2025
576794e
fix: make username column in wideRow method and its tests more consi…
david-roper Nov 18, 2025
ebed1a3
feat: remove redundant null return type from useFindSession
david-roper Nov 18, 2025
3dc1a85
feat: add error notification for useEffect
david-roper Nov 18, 2025
ac7316f
chore: and encodeUriComponent to ids
david-roper Nov 19, 2025
ea000e0
feat: fetch sessions then users in parrallel and update test mock values
david-roper Nov 19, 2025
d27535a
feat: add cancelled var to avoid race conditions in fetch records
david-roper Nov 19, 2025
763be84
feat: return null on errors userInfo issues
david-roper Nov 19, 2025
089598e
feat: add new findAllSessionsIncludeUsernames api call and todo comments
david-roper Nov 19, 2025
05fbd7c
feat: rename function to useFindSessionQuery
david-roper Nov 20, 2025
f36631a
feat: update types of findAllIncludeUsernames
david-roper Nov 20, 2025
1f6f093
feat: update query and type imports
david-roper Nov 20, 2025
e0d45a9
feat: changed how we find sessions to useFindSessionQuery instead, te…
david-roper Nov 20, 2025
ceb6dcb
feat: cleanup unused sessionInfo method, resolve prettier issues
david-roper Nov 21, 2025
f615161
test: update test mocks
david-roper Nov 21, 2025
ea093eb
feat: user find method instead of filter to get 1 unique userSession
david-roper Nov 21, 2025
ee70035
fix: adding ! to currentSession subject mentions at they should alway…
david-roper Nov 21, 2025
d607dc6
feat: create useFindSession Hook
david-roper Oct 24, 2025
7f6d418
feat: make useSession hook return one session instead of array
david-roper Oct 27, 2025
f0e9d18
feat: add hook to return list of user ids
david-roper Oct 27, 2025
99397f2
feat: new api reqs for finding sessions
david-roper Oct 27, 2025
eafc5e4
refactor: remove unused useFindSession hook
david-roper Nov 10, 2025
cd05322
feat: collect username with user api call
david-roper Nov 12, 2025
b7201be
refactor: move session and user api methods to separate hook files
david-roper Nov 12, 2025
0bf25b3
feat: create mocks for findUser and findSession hooks
david-roper Nov 12, 2025
c9bf879
test: fix use tests with wait for methods
david-roper Nov 13, 2025
91521d4
refactor: make Username a standalone column in long export formats
david-roper Nov 14, 2025
47e5d66
chore: small changes to test
david-roper Nov 14, 2025
c3703ab
fix: fix description in session controller
david-roper Nov 17, 2025
d6d4d0b
fix: add ability to find sessions list
david-roper Nov 17, 2025
3cf2358
fix: error msg in usefindsession
david-roper Nov 17, 2025
90b6abc
fix: throw and error to catch instead of returning null when error oc…
david-roper Nov 17, 2025
72c0a2a
feat: use schema parsing to confirm contents instead of casting it
david-roper Nov 17, 2025
52828a9
fix: fix type exports
david-roper Nov 17, 2025
6c72e35
test: change positions of subjectId and username column to make linte…
david-roper Nov 17, 2025
bccf67a
refactor: remove unused api call
david-roper Nov 17, 2025
182463a
feat: fix not finding user id issue by making subject inclusion optional
david-roper Nov 18, 2025
895197b
chore: revert session schema and parse change
david-roper Nov 18, 2025
7c02666
feat: remove redundant null return type from useFindSession
david-roper Nov 18, 2025
25903b0
chore: and encodeUriComponent to ids
david-roper Nov 19, 2025
05ba9de
feat: add cancelled var to avoid race conditions in fetch records
david-roper Nov 19, 2025
bcd84a5
feat: add new findAllSessionsIncludeUsernames api call and todo comments
david-roper Nov 19, 2025
8b4bed1
feat: rename function to useFindSessionQuery
david-roper Nov 20, 2025
c35549b
feat: update types of findAllIncludeUsernames
david-roper Nov 20, 2025
8b26015
feat: update query and type imports
david-roper Nov 20, 2025
623c2e9
Remove duplicate import for SessionWithUser type
david-roper Nov 25, 2025
688887f
fix: use zod error message for useFindSessionQuery
david-roper Nov 26, 2025
0e2f80e
fix: remove unused parts of tests, remove unused userInfo method
david-roper Nov 26, 2025
a98c43f
chore: add linter format changes
david-roper Nov 26, 2025
db53c16
chore: update parse to parseAsync in useFindSessionQuery
david-roper Nov 26, 2025
4ab6ac0
fix: removed cancelled var
david-roper Nov 27, 2025
7455698
Update apps/web/src/hooks/useFindSessionQuery.ts
david-roper Nov 27, 2025
51e1579
fix: remove extra brackets
david-roper Nov 27, 2025
2e4a766
chore: rename userId in datahub export column to username
david-roper Nov 27, 2025
01d5e0c
chore: remove redundant fetchRecords and console error code
david-roper Dec 1, 2025
f687926
fix: make session service return error upon empty array
david-roper Dec 1, 2025
cea17b2
fix: keep console.error
david-roper Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export class InstrumentRecordsService {
subjectId: removeSubjectIdScope(record.subject.id),
subjectSex: record.subject.sex,
timestamp: record.date.toISOString(),
userId: record.session.user?.username ?? 'N/A',
username: record.session.user?.username ?? 'N/A',
value: measureValue
});
}
Expand All @@ -210,7 +210,7 @@ export class InstrumentRecordsService {
subjectId: removeSubjectIdScope(record.subject.id),
subjectSex: record.subject.sex,
timestamp: record.date.toISOString(),
userId: record.session.user?.username ?? 'N/A',
username: record.session.user?.username ?? 'N/A',
value: arrayEntry.measureValue
});
});
Expand Down
13 changes: 12 additions & 1 deletion apps/api/src/sessions/sessions.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CurrentUser } from '@douglasneuroinformatics/libnest';
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { ApiOperation } from '@nestjs/swagger';
import type { SessionWithUser } from '@opendatacapture/schemas/session';
import type { Session } from '@prisma/client';

import type { AppAbility } from '@/auth/auth.types';
Expand All @@ -20,6 +21,16 @@ export class SessionsController {
return this.sessionsService.create(data);
}

@ApiOperation({ description: 'Find all sessions and usernames attached to them' })
@Get()
@RouteAccess({ action: 'read', subject: 'Session' })
findAllIncludeUsernames(
@CurrentUser('ability') ability: AppAbility,
@Query('groupId') groupId?: string
): Promise<SessionWithUser[]> {
return this.sessionsService.findAllIncludeUsernames(groupId, { ability });
}

@ApiOperation({ description: 'Find Session by ID' })
@Get(':id')
@RouteAccess({ action: 'read', subject: 'Session' })
Expand Down
24 changes: 23 additions & 1 deletion apps/api/src/sessions/sessions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export class SessionsService {
let group: Group | null = null;
if (groupId && !subject.groupIds.includes(groupId)) {
group = await this.groupsService.findById(groupId);
await this.subjectsService.addGroupForSubject(subject.id, group.id);
if (group) {
await this.subjectsService.addGroupForSubject(subject.id, group.id);
}
}

const { id } = await this.sessionModel.create({
Expand Down Expand Up @@ -94,6 +96,26 @@ export class SessionsService {
});
}

async findAllIncludeUsernames(groupId?: string, { ability }: EntityOperationOptions = {}) {
const sessionsWithUsers = await this.sessionModel.findMany({
include: {
subject: true,
user: {
select: {
username: true
}
}
},
where: {
AND: [accessibleQuery(ability, 'read', 'Session'), { groupId }]
}
});
if (sessionsWithUsers.length < 1) {
throw new NotFoundException(`Failed to find users`);
}
return sessionsWithUsers;
}

async findById(id: string, { ability }: EntityOperationOptions = {}) {
const session = await this.sessionModel.findFirst({
where: { AND: [accessibleQuery(ability, 'read', 'Session')], id }
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const Sidebar = () => {
>
<h5 className="text-sm font-medium">{t('common.sessionInProgress')}</h5>
<hr className="my-1.5 h-[1px] border-none bg-slate-700" />
{isSubjectWithPersonalInfo(currentSession.subject) ? (
{isSubjectWithPersonalInfo(currentSession.subject!) ? (
<div data-testid="current-session-info">
<p>{`${t('core.fullName')}: ${currentSession.subject.firstName} ${currentSession.subject.lastName}`}</p>
<p>
Expand All @@ -100,7 +100,7 @@ export const Sidebar = () => {
</div>
) : (
<div data-testid="current-session-info">
<p>ID: {removeSubjectIdScope(currentSession.subject.id)}</p>
<p>ID: {removeSubjectIdScope(currentSession.subject!.id)}</p>
</div>
)}
</motion.div>
Expand Down
91 changes: 65 additions & 26 deletions apps/web/src/hooks/__tests__/useInstrumentVisualization.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toBasicISOString } from '@douglasneuroinformatics/libjs';
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { useInstrumentVisualization } from '../useInstrumentVisualization';
Expand Down Expand Up @@ -32,7 +32,19 @@ const mockInstrumentRecords = {
{
computedMeasures: {},
data: { someValue: 'abc' },
date: FIXED_TEST_DATE
date: FIXED_TEST_DATE,
sessionId: '123'
}
]
};

const mockSessionWithUsername = {
data: [
{
id: '123',
user: {
username: 'testusername'
}
}
]
};
Expand Down Expand Up @@ -63,78 +75,97 @@ vi.mock('@/hooks/useInstrumentRecords', () => ({
useInstrumentRecords: () => mockInstrumentRecords
}));

vi.mock('@/hooks/useFindSessionQuery', () => ({
useFindSessionQuery: () => mockSessionWithUsername
}));

describe('useInstrumentVisualization', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('CSV', () => {
it('Should download', () => {
it('Should download', async () => {
const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } }));
const { records } = result.current;
await waitFor(() => {
expect(result.current.records.length).toBeGreaterThan(0);
});
act(() => result.current.dl('CSV'));
expect(records).toBeDefined();
expect(mockDownloadFn).toHaveBeenCalledTimes(1);
const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? [];
expect(filename).toContain('.csv');
const csvContents = getContentFn();
expect(csvContents).toMatch(
`GroupID,subjectId,Date,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},abc`
`GroupID,subjectId,Date,Username,someValue\r\ntestGroupId,testId,${toBasicISOString(FIXED_TEST_DATE)},testusername,abc`
);
});
});
describe('TSV', () => {
it('Should download', () => {
it('Should download', async () => {
const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } }));
const { dl, records } = result.current;
act(() => dl('TSV'));
const { records } = result.current;
await waitFor(() => {
expect(result.current.records.length).toBeGreaterThan(0);
});
act(() => result.current.dl('TSV'));
expect(records).toBeDefined();
expect(mockDownloadFn).toHaveBeenCalledTimes(1);
const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? [];
expect(filename).toContain('.tsv');
const tsvContents = getContentFn();
expect(tsvContents).toMatch(
`GroupID\tsubjectId\tDate\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\tabc`
`GroupID\tsubjectId\tDate\tUsername\tsomeValue\r\ntestGroupId\ttestId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestusername\tabc`
);
});
});
describe('CSV Long', () => {
it('Should download', () => {
it('Should download', async () => {
const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } }));
const { dl, records } = result.current;
act(() => dl('CSV Long'));
const { records } = result.current;
await waitFor(() => {
expect(result.current.records.length).toBeGreaterThan(0);
});
act(() => result.current.dl('CSV Long'));
expect(records).toBeDefined();
expect(mockDownloadFn).toHaveBeenCalledTimes(1);

const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? [];
expect(filename).toContain('.csv');
const csvLongContents = getContentFn();
expect(csvLongContents).toMatch(
`GroupID,Date,SubjectID,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,abc,someValue`
`GroupID,Date,SubjectID,Username,Value,Variable\r\ntestGroupId,${toBasicISOString(FIXED_TEST_DATE)},testId,testusername,abc,someValue`
);
});
});
describe('TSV Long', () => {
it('Should download', () => {
it('Should download', async () => {
const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } }));
const { dl, records } = result.current;
act(() => dl('TSV Long'));
const { records } = result.current;
await waitFor(() => {
expect(result.current.records.length).toBeGreaterThan(0);
});
act(() => result.current.dl('TSV Long'));
expect(records).toBeDefined();
expect(mockDownloadFn).toHaveBeenCalledTimes(1);

const [filename, getContentFn] = mockDownloadFn.mock.calls[0] ?? [];
expect(filename).toMatch('.tsv');
const tsvLongContents = getContentFn();
expect(tsvLongContents).toMatch(
`GroupID\tDate\tSubjectID\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\tabc\tsomeValue`
`GroupID\tDate\tSubjectID\tUsername\tValue\tVariable\r\ntestGroupId\t${toBasicISOString(FIXED_TEST_DATE)}\ttestId\ttestusername\tabc\tsomeValue`
);
});
});
describe('Excel', () => {
it('Should download', () => {
it('Should download', async () => {
const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } }));
const { dl, records } = result.current;
act(() => dl('Excel'));
const { records } = result.current;
await waitFor(() => {
expect(result.current.records.length).toBeGreaterThan(0);
});
act(() => result.current.dl('Excel'));
expect(records).toBeDefined();
expect(mockExcelDownloadFn).toHaveBeenCalledTimes(1);
const [filename, getContentFn] = mockExcelDownloadFn.mock.calls[0] ?? [];
Expand All @@ -143,20 +174,24 @@ describe('useInstrumentVisualization', () => {

expect(excelContents).toEqual([
{
Date: '2025-04-30',
GroupID: 'testGroupId',
subjectId: 'testId',
// eslint-disable-next-line perfectionist/sort-objects
Date: '2025-04-30',
someValue: 'abc'
someValue: 'abc',
Username: 'testusername'
}
]);
});
});
describe('Excel Long', () => {
it('Should download', () => {
it('Should download', async () => {
const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } }));
const { dl, records } = result.current;
act(() => dl('Excel Long'));
const { records } = result.current;
await waitFor(() => {
expect(result.current.records.length).toBeGreaterThan(0);
});
act(() => result.current.dl('Excel Long'));
expect(records).toBeDefined();
expect(mockExcelDownloadFn).toHaveBeenCalledTimes(1);

Expand All @@ -169,6 +204,7 @@ describe('useInstrumentVisualization', () => {
Date: '2025-04-30',
GroupID: 'testGroupId',
SubjectID: 'testId',
Username: 'testusername',
Value: 'abc',
Variable: 'someValue'
}
Expand All @@ -178,8 +214,11 @@ describe('useInstrumentVisualization', () => {
describe('JSON', () => {
it('Should download', async () => {
const { result } = renderHook(() => useInstrumentVisualization({ params: { subjectId: 'testId' } }));
const { dl, records } = result.current;
act(() => dl('JSON'));
const { records } = result.current;
await waitFor(() => {
expect(result.current.records.length).toBeGreaterThan(0);
});
act(() => result.current.dl('JSON'));
expect(records).toBeDefined();
expect(mockDownloadFn).toHaveBeenCalledTimes(1);

Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/hooks/useFindSessionQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { $SessionWithUser } from '@opendatacapture/schemas/session';
import type { SessionWithUserQueryParams } from '@opendatacapture/schemas/session';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

type UseSessionOptions = {
enabled?: boolean;
params: SessionWithUserQueryParams;
};

export const useFindSessionQuery = (
{ enabled, params }: UseSessionOptions = {
enabled: true,
params: {}
}
) => {
return useQuery({
enabled,
queryFn: async () => {
const response = await axios.get('/v1/sessions', {
params
});
return $SessionWithUser.array().parseAsync(response.data);
},
queryKey: ['sessions', ...Object.values(params)]
});
};
Loading