Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@
"{{count}} selected_other": "{{count}} selected",
"{{count}} Selected_one": "{{count}} Selected",
"{{count}} Selected_other": "{{count}} Selected",
"{{count}} Selected Staff_one": "{{count}} Selected Staff",
"{{count}} Selected Staff_other": "{{count}} Selected Staff",
"{{count}} years_one": "{{count}} years",
"{{count}} years_other": "{{count}} years",
"{{daysLate}}+ days late": "{{daysLate}}+ days late",
Expand All @@ -276,6 +278,7 @@
"{{name}}'s commitment info updated!": "{{name}}'s commitment info updated!",
"{{name}}'s Gross Requested Salary exceeds their individual Maximum Allowable Salary. If this is correct, please provide reasoning for why {{name}}'s Salary should exceed {{cap}} for division head approval below.": "{{name}}'s Gross Requested Salary exceeds their individual Maximum Allowable Salary. If this is correct, please provide reasoning for why {{name}}'s Salary should exceed {{cap}} for division head approval below.",
"{{preferredName}}'s Pending Additional Salary Request": "{{preferredName}}'s Pending Additional Salary Request",
"{{reassigned}} of the selected staff already have a coach.": "{{reassigned}} of the selected staff already have a coach.",
"{{rows}} row selected": "{{rows}} row selected",
"{{rows}} rows selected": "{{rows}} rows selected",
"{{sectionKind}} Request": "{{sectionKind}} Request",
Expand Down Expand Up @@ -629,6 +632,7 @@
"Assign Coach": "Assign Coach",
"Assignee": "Assignee",
"Assignee: ": "Assignee: ",
"Assigning a new coach will replace the current coach for the following staff.": "Assigning a new coach will replace the current coach for the following staff.",
"at risk": "at risk",
"Attrition": "Attrition",
"Australia": "Australia",
Expand Down Expand Up @@ -796,6 +800,7 @@
"Close pivot settings": "Close pivot settings",
"Closing Balance": "Closing Balance",
"Coach": "Coach",
"Coach assigned successfully.": "Coach assigned successfully.",
"Coaches": "Coaches",
"Coaching": "Coaching",
"Coaching Accounts": "Coaching Accounts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ const handleAssignCoach = jest.fn();

interface TestComponentProps {
coaches?: AssignCoachOption[];
reassignedNames?: string[];
}

const TestComponent: React.FC<TestComponentProps> = ({
coaches: coachesProp = coaches,
reassignedNames,
}) => (
<ThemeProvider theme={theme}>
<AssignCoachModal
subjectName="Carlos & Michaela Everts"
coaches={coachesProp}
reassignedNames={reassignedNames}
handleClose={handleClose}
handleAssignCoach={handleAssignCoach}
/>
Expand Down Expand Up @@ -55,6 +58,28 @@ describe('AssignCoachModal', () => {
expect(getByRole('button', { name: 'Save' })).toBeDisabled();
});

it('warns about staff whose existing coach will be replaced', () => {
const { getByRole } = render(
<TestComponent reassignedNames={['John & Jane Doe', "James O'Connor"]} />,
);

const alert = getByRole('alert');
expect(alert).toHaveTextContent(
'2 of the selected staff already have a coach.',
);
expect(alert).toHaveTextContent(
'Assigning a new coach will replace the current coach for the following staff.',
);
expect(alert).toHaveTextContent('John & Jane Doe');
expect(alert).toHaveTextContent("James O'Connor");
});

it('does not warn when no selected staff already have a coach', () => {
const { queryByRole } = render(<TestComponent reassignedNames={[]} />);

expect(queryByRole('alert')).not.toBeInTheDocument();
});

it('closes when Cancel is clicked', () => {
const { getByRole } = render(<TestComponent />);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { ReactElement } from 'react';
import {
Alert,
Autocomplete,
Box,
DialogActions,
DialogContent,
TextField,
Typography,
} from '@mui/material';
import { Formik } from 'formik';
import { useTranslation } from 'react-i18next';
Expand All @@ -23,6 +26,11 @@ interface AssignCoachModalProps {
/** Name shown in the modal title, e.g. the staff member the coach is for. */
subjectName: string;
coaches: AssignCoachOption[];
/**
* Names of the staff who already have a coach and will be reassigned. When
* non-empty, a warning listing them is shown so overwrites aren't silent.
*/
reassignedNames?: string[];
handleClose: () => void;
handleAssignCoach: (coachId: string) => Promise<void> | void;
}
Expand All @@ -36,6 +44,7 @@ type AssignCoachFormValues = yup.InferType<typeof assignCoachSchema>;
export const AssignCoachModal: React.FC<AssignCoachModalProps> = ({
subjectName,
coaches,
reassignedNames,
handleClose,
handleAssignCoach,
}) => {
Expand Down Expand Up @@ -66,6 +75,26 @@ export const AssignCoachModal: React.FC<AssignCoachModalProps> = ({
}): ReactElement => (
<form onSubmit={handleSubmit} noValidate>
<DialogContent dividers>
{reassignedNames && reassignedNames.length > 0 && (
<Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="body2" fontWeight="bold">
{t(
'{{reassigned}} of the selected staff already have a coach.',
{ reassigned: reassignedNames.length },
)}
</Typography>
<Typography variant="body2">
{t(
'Assigning a new coach will replace the current coach for the following staff.',
)}
</Typography>
<Box component="ul" sx={{ m: 0, mt: 1, pl: 3 }}>
{reassignedNames.map((name) => (
<li key={name}>{name}</li>
))}
</Box>
</Alert>
)}
<Autocomplete
autoHighlight
disabled={isSubmitting}
Expand Down
35 changes: 29 additions & 6 deletions src/components/HrTools/MpdGoalAdmin/GoalsTable/GoalsTable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ThemeProvider } from '@mui/material/styles';
import { act, render } from '@testing-library/react';
import { act, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import theme from 'src/theme';
import { MpdGoalAdminProvider, useMpdGoalAdmin } from '../MpdGoalAdminContext';
Expand Down Expand Up @@ -57,6 +57,29 @@ describe('GoalsTable', () => {
).toBeInTheDocument();
});

it('assigns a coach to the row from the Assign Coach modal', async () => {
const { getByRole, findByRole } = renderTable();
// 'Carlos & Michaela Everts' (row-2) has no coach on the first page.
userEvent.click(getByRole('button', { name: 'Assign Coach' }));

const dialog = await findByRole('dialog');
expect(dialog).toHaveTextContent(
'Assign Coach for Carlos & Michaela Everts',
);

userEvent.click(getByRole('combobox', { name: 'Coach' }));
userEvent.click(await findByRole('option', { name: 'Tom Harris' }));
userEvent.click(getByRole('button', { name: 'Save' }));

// Formik's submit resolves asynchronously, so wait for the context update
// rather than asserting synchronously after the click.
await waitFor(() =>
expect(ctx.cohorts[0].rows.find((row) => row.id === 'row-2')?.coach).toBe(
'Tom Harris',
),
);
});

it('renders a View/Edit action and a menu button for each row on the page', () => {
const { getAllByText, getAllByRole } = renderTable();
const onPage = Math.min(rows.length, DEFAULT_ROWS_PER_PAGE);
Expand All @@ -69,14 +92,14 @@ describe('GoalsTable', () => {
it('selects a row via its checkbox', async () => {
const { getAllByRole } = renderTable();
// index 0 is the header "select all" checkbox
await userEvent.click(getAllByRole('checkbox')[1]);
userEvent.click(getAllByRole('checkbox')[1]);
expect(ctx.selectedRowIds.has('row-1')).toBe(true);
});

it('selects every row on the page via the header checkbox', async () => {
const { getAllByRole } = renderTable();
// index 0 is the header "select all" checkbox
await userEvent.click(getAllByRole('checkbox')[0]);
userEvent.click(getAllByRole('checkbox')[0]);
// The header checkbox selects only the rows on the current page, not the
// entire filtered set.
rows.slice(0, DEFAULT_ROWS_PER_PAGE).forEach((row) => {
Expand All @@ -97,12 +120,12 @@ describe('GoalsTable', () => {
expect(headerCheckbox).toHaveAttribute('data-indeterminate', 'false');

// Select a single data row.
await userEvent.click(checkboxes[1]);
userEvent.click(checkboxes[1]);
expect(headerCheckbox.checked).toBe(false);
expect(headerCheckbox).toHaveAttribute('data-indeterminate', 'true');

// Select the rest via the header, which now reads "select all".
await userEvent.click(headerCheckbox);
userEvent.click(headerCheckbox);
expect(headerCheckbox.checked).toBe(true);
expect(headerCheckbox).toHaveAttribute('data-indeterminate', 'false');
});
Expand Down Expand Up @@ -144,7 +167,7 @@ describe('GoalsTable', () => {
getByText(`Person ${DEFAULT_ROWS_PER_PAGE - 1}`),
).toBeInTheDocument();
// Advance to the next page via the pagination "next page" button.
await userEvent.click(getByRole('button', { name: /Go to next page/i }));
userEvent.click(getByRole('button', { name: /Go to next page/i }));
expect(getByText(`Person ${DEFAULT_ROWS_PER_PAGE}`)).toBeInTheDocument();

// Changing the filter shrinks the result set. The table must reset to the
Expand Down
33 changes: 21 additions & 12 deletions src/components/HrTools/MpdGoalAdmin/GoalsTable/GoalsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@
import { useTranslation } from 'react-i18next';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
import {
AssignCoachModal,
AssignCoachOption,
} from '../AssignCoachModal/AssignCoachModal';
import { AssignCoachModal } from '../AssignCoachModal/AssignCoachModal';
import { useMpdGoalAdmin } from '../MpdGoalAdminContext';
import { mockCoaches } from '../mockData';
import { StaffGoalRow, isSendable } from '../mpdGoalAdminHelpers';

interface GoalsTableProps {
Expand All @@ -37,20 +35,31 @@
export const GoalsTable: React.FC<GoalsTableProps> = ({ rows }) => {
const { t } = useTranslation();
const locale = useLocale();
const { selectedRowIds, toggleRow, toggleRows, search, selectedCohortId } =
useMpdGoalAdmin();
const {
selectedRowIds,
toggleRow,
toggleRows,
search,
selectedCohortId,
assignCoach,
} = useMpdGoalAdmin();
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE);
// The staff row whose coach is being assigned; null when the modal is closed.
const [coachRow, setCoachRow] = useState<StaffGoalRow | null>(null);

// TODO(MPDX-9699): populate from the assignable-coaches query once the
// backend field exists. Empty for now so the modal renders a UI-only dropdown.
const assignableCoaches: AssignCoachOption[] = [];
// TODO(MPDX-9914): populate from the assignable-coaches query once the
// backend field exists. Same mock list the toolbar's bulk path uses.
const assignableCoaches = mockCoaches;

const handleAssignCoach = async (_coachId: string) => {
// TODO(MPDX-9699): call the assign-coach mutation with { staffId, coachId }
// and refresh the goal-admin rows. Backend contract pending.
// TODO(MPDX-9914): call the assignCoach mutation instead of the mock
// context update once the backend field is wired up.
const handleAssignCoach = (coachId: string) => {
const coach = assignableCoaches.find((option) => option.id === coachId);
if (!coach || !coachRow) {
return;
}
assignCoach([coachRow.id], coach.name);

Check warning on line 62 in src/components/HrTools/MpdGoalAdmin/GoalsTable/GoalsTable.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

GoalsTable:React.FC<GoalsTableProps> increases from 153 to 164 lines of code, threshold = 120 Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
};

// Reset to the first page whenever the filter inputs change, so the user
Expand Down
Loading
Loading