Skip to content

Commit

Permalink
History log for schools and teams
Browse files Browse the repository at this point in the history
ashtarcommunications committed Nov 23, 2024
1 parent 952ad1a commit 5db1739
Showing 27 changed files with 545 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/caselist.yml
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ jobs:
- name: Test
run: |
cd client && npm run test-ci
cd client && npm run test:ci
- name: If tests passed change icon
if: success()
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ That will spin up a development server on localhost.

### Testing

Run the test suite with `npm run test` or `npm run test-cover` if you want code coverage metrics.
Run the test suite with `npm run test` or `npm run test:cover` if you want code coverage metrics.

### Production

@@ -96,7 +96,7 @@ This is a standard React front-end bundled with Vite. It uses Vite's built-in su

### Testing

Run the test suite with vitest using `npm run test` or `npm run test-cover` if you want code coverage metrics.
Run the test suite with vitest using `npm run test` or `npm run test:cover` if you want code coverage metrics.

### Production

5 changes: 5 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Upgrade to Node LTS
Add prettier
Add husky
Update eslint config and nsda-js-utils
Set up CI to run server tests with mysql sidecar
22 changes: 0 additions & 22 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -42,9 +42,9 @@
"server": "vite preview",
"lint": "eslint src",
"test": "vitest watch",
"test-silent": "vitest --silent",
"test-cover": "vitest --coverage",
"test-ci": "vitest --run"
"test:silent": "vitest --silent",
"test:cover": "vitest --coverage",
"test:ci": "vitest --run"
},
"eslintConfig": {
"extends": [
2 changes: 2 additions & 0 deletions client/src/helpers/__mocks__/api.js
Original file line number Diff line number Diff line change
@@ -14,11 +14,13 @@ export const loadDownloads = vi.fn().mockResolvedValue([
export const loadSchools = vi.fn().mockResolvedValue([]);
export const addSchool = vi.fn().mockResolvedValue({ school_id: 1, name: 'Test School' });
export const loadSchool = vi.fn().mockResolvedValue({});
export const loadSchoolHistory = vi.fn().mockResolvedValue([{ description: 'Test school history', updated_at: '2023', updated_by: 'Test User' }]);
export const loadTeams = vi.fn().mockResolvedValue([{ team_id: 1, name: 'testteam', display_name: 'Test Team', debater1_first: 'Aaron', debater1_last: 'Hardy', updated_by: 'Test User' }]);
export const addTeam = vi.fn().mockResolvedValue({ message: 'Successfully added team' });
export const updateTeam = vi.fn().mockResolvedValue({ message: 'Successfully updated team' });
export const loadTeam = vi.fn().mockResolvedValue({ team_id: 1, name: 'testteam', display_name: 'Test Team', debater1_first: 'Aaron', debater1_last: 'Hardy', updated_by: 'Test User' });
export const deleteTeam = vi.fn().mockResolvedValue({ message: 'Successfully deleted team' });
export const loadTeamHistory = vi.fn().mockResolvedValue([{ description: 'Test team history', updated_at: '2023', updated_by: 'Test User' }]);
export const loadRounds = vi.fn().mockResolvedValue([
{ round_id: 1, tournament: 'Aff Tournament', side: 'A', round: '1', judge: 'Judge', opponent: 'Opponent', report: 'Report', opensource: '/test.docx', video: 'Video' },
{ round_id: 2, tournament: 'Neg Tournament', side: 'N', round: '2', judge: 'Judge', opponent: 'Opponent', report: 'Report', opensource: '/test.docx', video: 'Video' },
8 changes: 8 additions & 0 deletions client/src/helpers/api.js
Original file line number Diff line number Diff line change
@@ -68,6 +68,10 @@ export const loadSchool = async (caselist, school) => {
return fetchBase(`caselists/${caselist}/schools/${school}`);
};

export const loadSchoolHistory = async (caselist, school) => {
return fetchBase(`caselists/${caselist}/schools/${school}/history`);
};

export const loadTeams = async (caselist, school) => {
return fetchBase(`caselists/${caselist}/schools/${school}/teams`);
};
@@ -88,6 +92,10 @@ export const deleteTeam = async (caselist, school, team) => {
return fetchBase(`caselists/${caselist}/schools/${school}/teams/${team}`, { method: 'DELETE' });
};

export const loadTeamHistory = async (caselist, school, team) => {
return fetchBase(`caselists/${caselist}/schools/${school}/teams/${team}/history`);
};

export const loadRounds = async (caselist, school, team, side) => {
return fetchBase(`caselists/${caselist}/schools/${school}/teams/${team}/rounds?side=${side || ''}`);
};
28 changes: 26 additions & 2 deletions client/src/school/TeamList.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useCallback, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { faTrash, faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons';
import moment from 'moment';
import { toast } from 'react-toastify';
import { affName, negName } from '@speechanddebate/nsda-js-utils';
@@ -17,6 +17,7 @@ import Loader from '../loader/Loader';
import Error from '../layout/Error';
import AddTeam from './AddTeam';
import ConfirmButton from '../helpers/ConfirmButton';
import HistoryTable from '../tables/HistoryTable';

import styles from './TeamList.module.css';

@@ -25,6 +26,7 @@ const TeamList = () => {
const { caselist, school } = useParams();
const { caselistData, schoolData, teams, fetchTeams } = useStore();
const [fetching, setFetching] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const { isMobile } = useDeviceDetect();

useEffect(() => {
@@ -64,6 +66,10 @@ const TeamList = () => {
);
}, [handleDeleteTeam, teams]);

const handleToggleHistory = () => {
setShowHistory(!showHistory);
};

const data = useMemo(() => teams, [teams]);
const columns = useMemo(() => [
{
@@ -158,7 +164,25 @@ const TeamList = () => {
/>
</div>
<hr />
{!caselistData.archived && <AddTeam />}
{
!caselistData.archived && <AddTeam />
}
{
!caselistData.archived &&
<div>
<hr />
<h3 onClick={handleToggleHistory}>
School History
<FontAwesomeIcon
className={styles.showhistory}
data-testid="showhistory"
icon={showHistory ? faCaretDown : faCaretUp}
title="School History"
/>
</h3>
{showHistory && <HistoryTable type="school" />}
</div>
}
</>
);
};
6 changes: 6 additions & 0 deletions client/src/school/TeamList.module.css
Original file line number Diff line number Diff line change
@@ -58,3 +58,9 @@
opacity: 1;
visibility: visible;
}

.showhistory {
font-size: 18px;
margin-left: 10px;
cursor: pointer;
}
21 changes: 20 additions & 1 deletion client/src/school/TeamList.test.jsx
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ describe('TeamList', () => {
render(<TeamList />);
await waitFor(() => assert.strictEqual(store.fetchTeams.mock.calls.length, 1, 'Fetched teams'));

assert.isOk(screen.queryByText(/Test Team/), 'Display name exists');
assert.isOk(screen.queryByText(/Test Team/), 'Display name exists');
assert.isOk(screen.queryByText(/Aff/), 'Aff link exists');
assert.isOk(screen.queryByText(/Neg/), 'Neg link exists');
assert.isOk(screen.queryByText(/updated by/), 'Updated By exists');
@@ -44,6 +44,25 @@ describe('TeamList', () => {
await waitFor(() => assert.strictEqual(store.fetchTeams.mock.calls.length, 2, 'Fetched teams'));
});

it('Optionally renders a school history', async () => {
render(<TeamList />);

const toggle = await screen.findByTestId('showhistory');
assert.isOk(toggle, 'Heading exists');
fireEvent.click(toggle);

await waitFor(() => assert.isOk(screen.queryByText(/Test school history/), 'Shows history table'));
fireEvent.click(toggle);
await waitFor(() => assert.isNotOk(screen.queryByText(/Test school history/), 'Hides history table'));
});

it('should not render a school history on an archived caselist', async () => {
store.caselistData.archived = true;
render(<TeamList />);
await waitFor(() => assert.isNotOk(screen.queryByText(/School History/), 'No school history'));
store.caselistData.archived = false;
});

it('Renders an error message without caselistData', async () => {
const defaultCaselistData = store.caselistData;
store.caselistData = { message: 'No caselistData' };
70 changes: 70 additions & 0 deletions client/src/tables/HistoryTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { useParams } from 'react-router-dom';

import { loadSchoolHistory, loadTeamHistory } from '../helpers/api';
import Table from './Table';
import Loader from '../loader/Loader';

const HistoryTable = ({ type = 'school' }) => {
const { caselist, school, team } = useParams();
const [fetching, setFetching] = useState(false);
const [history, setHistory] = useState([]);

useEffect(() => {
const fetchData = async () => {
try {
setFetching(true);
if (type === 'team') {
setHistory(await loadTeamHistory(caselist, school, team));
} else {
setHistory(await loadSchoolHistory(caselist, school));
}
setFetching(false);
} catch (err) {
console.log(err);
setHistory(err);
setFetching(false);
}
};
fetchData();
}, [caselist, school, team]);

const data = useMemo(() => history, [history]);
const columns = [
{
Header: 'Description',
width: 'auto',
accessor: 'description',
},
{
Header: 'Date (UTC)',
width: 'auto',
accessor: 'updated_at',
},
{
Header: 'User',
width: 'auto',
accessor: 'updated_by',
},
];

if (fetching) { return <Loader />; }

return (
<div>
<Table
columns={columns}
data={data}
noDataText="No history found"
loading={fetching}
/>
</div>
);
};

HistoryTable.propTypes = {
type: PropTypes.string,
};

export default HistoryTable;
33 changes: 33 additions & 0 deletions client/src/tables/HistoryTable.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { assert } from 'chai';
import { vi } from 'vitest';

import { wrappedRender as render, screen, waitFor } from '../setupTests';

import HistoryTable from './HistoryTable';

vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useParams: vi.fn().mockImplementation(() => ({ caselist: 'testcaselist', school: 'testschool', team: 'testteam' })),
};
});

describe('HistoryTable', () => {
it('Renders a history table for a school', async () => {
render(<HistoryTable type="school" />);

await waitFor(() => assert.isOk(screen.queryByText(/Test school history/), 'Description exists'));
await waitFor(() => assert.isOk(screen.queryByText(/2023/), 'Date exists'));
await waitFor(() => assert.isOk(screen.queryByText(/Test User/), 'User exists'));
});

it('Renders a history table for a team', async () => {
render(<HistoryTable type="team" />);

await waitFor(() => assert.isOk(screen.queryByText(/Test team history/), 'Description exists'));
await waitFor(() => assert.isOk(screen.queryByText(/2023/), 'Date exists'));
await waitFor(() => assert.isOk(screen.queryByText(/Test User/), 'User exists'));
});
});
24 changes: 23 additions & 1 deletion client/src/team/TeamRounds.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLink, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faLink, faPlus, faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons';
import { toast } from 'react-toastify';
import moment from 'moment';
import { Link, useParams } from 'react-router-dom';
@@ -18,6 +18,7 @@ import TeamNotes from './TeamNotes';
import AddCite from './AddCite';
import RoundsTable from './RoundsTable';
import CitesTable from './CitesTable';
import HistoryTable from '../tables/HistoryTable';

import styles from './TeamRounds.module.css';

@@ -30,6 +31,7 @@ const TeamRounds = () => {
const [teamData, setTeamData] = useState({});
const [rounds, setRounds] = useState([]);
const [cites, setCites] = useState([]);
const [showHistory, setShowHistory] = useState(false);

const { isMobile } = useDeviceDetect();

@@ -201,6 +203,10 @@ const TeamRounds = () => {
/>);
};

const handleToggleHistory = () => {
setShowHistory(!showHistory);
};

const timestamp = moment(teamData.updated_at, 'YYYY-MM-DD HH:mm:ss').format('l');

if (fetching) { return <Loader />; }
@@ -324,6 +330,22 @@ const TeamRounds = () => {
handleDeleteCiteConfirm={handleDeleteCiteConfirm}
handleToggleCites={handleToggleCites}
/>
{
!caselistData.archived &&
<div>
<hr />
<h3 onClick={handleToggleHistory}>
Team History
<FontAwesomeIcon
className={styles.showhistory}
data-testid="showhistory"
icon={showHistory ? faCaretDown : faCaretUp}
title="Team History"
/>
</h3>
{showHistory && <HistoryTable type="team" />}
</div>
}
</div>
);
};
Loading

0 comments on commit 5db1739

Please sign in to comment.