Skip to content

Commit 6fa4efa

Browse files
committed
simplify team policy container
1 parent 2f7072e commit 6fa4efa

File tree

5 files changed

+414
-188
lines changed

5 files changed

+414
-188
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import EditDialog from 'components/dialog/EditDialog';
2+
import { IPolicy } from 'interfaces/usePoliciesApi.interface';
3+
import { ITeam } from 'interfaces/useTeamsApi.interface';
4+
import {
5+
ITeamPolicyFormValues,
6+
TeamPolicyForm,
7+
TeamPolicyFormYupSchema
8+
} from './TeamPolicyForm';
9+
10+
export interface ICreateTeamPolicyDialogProps {
11+
open: boolean;
12+
isLoading: boolean;
13+
teams: ITeam[];
14+
policies: IPolicy[];
15+
initialValues: ITeamPolicyFormValues;
16+
onCancel: () => void;
17+
onSave: (values: ITeamPolicyFormValues) => void;
18+
}
19+
20+
export const CreateTeamPolicyDialog: React.FC<ICreateTeamPolicyDialogProps> = ({
21+
open,
22+
isLoading,
23+
teams,
24+
policies,
25+
initialValues,
26+
onCancel,
27+
onSave
28+
}) => {
29+
return (
30+
<EditDialog<ITeamPolicyFormValues>
31+
open={open}
32+
isLoading={isLoading}
33+
dialogTitle="Add Assignment"
34+
dialogSaveButtonLabel="Add"
35+
component={{
36+
element: <TeamPolicyForm teams={teams} policies={policies} />,
37+
initialValues,
38+
validationSchema: TeamPolicyFormYupSchema
39+
}}
40+
onCancel={onCancel}
41+
onSave={onSave}
42+
/>
43+
);
44+
};
45+
46+
export default CreateTeamPolicyDialog;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import EditDialog from 'components/dialog/EditDialog';
2+
import { IPolicy } from 'interfaces/usePoliciesApi.interface';
3+
import { ITeam } from 'interfaces/useTeamsApi.interface';
4+
import {
5+
ITeamPolicyFormValues,
6+
TeamPolicyForm,
7+
TeamPolicyFormYupSchema
8+
} from './TeamPolicyForm';
9+
10+
export interface IEditTeamPolicyDialogProps {
11+
open: boolean;
12+
isLoading: boolean;
13+
teams: ITeam[];
14+
policies: IPolicy[];
15+
initialValues: ITeamPolicyFormValues;
16+
onCancel: () => void;
17+
onSave: (values: ITeamPolicyFormValues) => void;
18+
}
19+
20+
export const EditTeamPolicyDialog: React.FC<IEditTeamPolicyDialogProps> = ({
21+
open,
22+
isLoading,
23+
teams,
24+
policies,
25+
initialValues,
26+
onCancel,
27+
onSave
28+
}) => {
29+
return (
30+
<EditDialog<ITeamPolicyFormValues>
31+
open={open}
32+
isLoading={isLoading}
33+
dialogTitle="Edit Assignment"
34+
dialogSaveButtonLabel="Save"
35+
component={{
36+
element: <TeamPolicyForm teams={teams} policies={policies} />,
37+
initialValues,
38+
validationSchema: TeamPolicyFormYupSchema
39+
}}
40+
onCancel={onCancel}
41+
onSave={onSave}
42+
/>
43+
);
44+
};
45+
46+
export default EditTeamPolicyDialog;

app/src/features/admin/policies/components/TeamPoliciesContainer.test.tsx

Lines changed: 55 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ import { cleanup, render, waitFor } from 'test-helpers/test-utils';
99
import { Mock } from 'vitest';
1010
import { ITeamPoliciesContainerProps, TeamPoliciesContainer } from './TeamPoliciesContainer';
1111

12-
// Types for DataGrid mock
1312
interface MockDataGridProps {
1413
rows: ITeamPolicyDetails[];
1514
columns: GridColDef[];
1615
localeText?: { noRowsLabel?: string };
1716
}
1817

19-
// Simple DataGrid mock - just renders rows as divs
2018
vi.mock('@mui/x-data-grid', () => ({
2119
DataGrid: ({ rows, columns, localeText }: MockDataGridProps) => (
2220
<div data-testid="mock-data-grid">
@@ -27,7 +25,6 @@ vi.mock('@mui/x-data-grid', () => ({
2725
<div key={row.team_policy_id} data-testid={`row-${row.team_policy_id}`}>
2826
<span>{row.team_name}</span>
2927
<span>{row.policy_name}</span>
30-
{/* Render actions column */}
3128
{columns.find((c) => c.field === 'actions')?.renderCell?.({ row } as never)}
3229
</div>
3330
))
@@ -70,11 +67,19 @@ const mockPolicies: IPolicy[] = [
7067

7168
const mockCreateTeamPolicy = vi.fn();
7269
const mockDeleteTeamPolicy = vi.fn();
70+
const mockGetTeams = vi.fn();
71+
const mockGetPolicies = vi.fn();
7372

7473
const mockUseApi = {
7574
teamPolicies: {
7675
createTeamPolicy: mockCreateTeamPolicy,
7776
deleteTeamPolicy: mockDeleteTeamPolicy
77+
},
78+
teams: {
79+
getTeams: mockGetTeams
80+
},
81+
policies: {
82+
getPolicies: mockGetPolicies
7883
}
7984
};
8085

@@ -100,6 +105,8 @@ const renderComponent = (props: Partial<ITeamPoliciesContainerProps> = {}) => {
100105

101106
describe('TeamPoliciesContainer', () => {
102107
beforeEach(() => {
108+
mockGetTeams.mockResolvedValue({ teams: mockTeams });
109+
mockGetPolicies.mockResolvedValue({ policies: mockPolicies });
103110
mockBiohubApi.mockImplementation(() => mockUseApi);
104111
});
105112

@@ -108,154 +115,80 @@ describe('TeamPoliciesContainer', () => {
108115
vi.clearAllMocks();
109116
});
110117

111-
describe('Header', () => {
112-
it('displays rowCount in header', async () => {
113-
// Step 1: Render with default props (rowCount: 2)
114-
const { getByText } = renderComponent();
118+
it('displays rowCount in header', async () => {
119+
const { getByText } = renderComponent();
115120

116-
// Step 2: Verify dynamic rowCount appears in header
117-
await waitFor(() => {
118-
expect(getByText('(2)')).toBeVisible();
119-
});
121+
await waitFor(() => {
122+
expect(getByText('(2)')).toBeVisible();
120123
});
124+
});
121125

122-
it('shows team-specific header when team is selected', async () => {
123-
// Step 1: Render with selectedTeam prop
124-
const { getByText } = renderComponent({ selectedTeam: mockTeams[0] });
126+
it('opens Add Assignment dialog when Add is clicked', async () => {
127+
const { getByRole, getByText } = renderComponent();
125128

126-
// Step 2: Verify header shows team-specific text
127-
await waitFor(() => {
128-
expect(getByText('Policies for "Alpha Team"')).toBeVisible();
129-
});
129+
await waitFor(() => {
130+
expect(getByRole('button', { name: /add/i })).toBeEnabled();
130131
});
131132

132-
it('shows policy-specific header when policy is selected', async () => {
133-
// Step 1: Render with selectedPolicy prop
134-
const { getByText } = renderComponent({ selectedPolicy: mockPolicies[0] });
133+
fireEvent.click(getByRole('button', { name: /add/i }));
135134

136-
// Step 2: Verify header shows policy-specific text
137-
await waitFor(() => {
138-
expect(getByText('Teams with "Data Access Policy"')).toBeVisible();
139-
});
140-
});
141-
142-
it('shows combined header when both team and policy are selected', async () => {
143-
// Step 1: Render with both selectedTeam and selectedPolicy props
144-
const { getByText } = renderComponent({
145-
selectedTeam: mockTeams[0],
146-
selectedPolicy: mockPolicies[0]
147-
});
148-
149-
// Step 2: Verify header shows combined assignment text
150-
await waitFor(() => {
151-
expect(getByText('Assignment: Alpha Team + Data Access Policy')).toBeVisible();
152-
});
135+
await waitFor(() => {
136+
expect(getByText('Add Assignment')).toBeVisible();
153137
});
154138
});
155139

156-
describe('Assign Button', () => {
157-
it('does not show Assign button when no selection', async () => {
158-
// Step 1: Render with no selection (default props)
159-
const { queryByRole } = renderComponent();
140+
it('calls createTeamPolicy API when Add dialog is saved', async () => {
141+
mockCreateTeamPolicy.mockResolvedValueOnce({});
142+
const mockRefresh = vi.fn();
160143

161-
// Step 2: Verify Assign button is NOT visible
162-
await waitFor(() => {
163-
expect(queryByRole('button', { name: /assign/i })).toBeNull();
164-
});
144+
const { getByRole, getByTestId } = renderComponent({
145+
selectedTeam: mockTeams[2],
146+
selectedPolicy: mockPolicies[2],
147+
refresh: mockRefresh
165148
});
166149

167-
it('does not show Assign button when only team is selected', async () => {
168-
// Step 1: Render with only selectedTeam (no policy)
169-
const { queryByRole } = renderComponent({ selectedTeam: mockTeams[0] });
170-
171-
// Step 2: Verify Assign button is NOT visible (needs both)
172-
await waitFor(() => {
173-
expect(queryByRole('button', { name: /assign/i })).toBeNull();
174-
});
150+
await waitFor(() => {
151+
expect(getByRole('button', { name: /add/i })).toBeEnabled();
175152
});
176153

177-
it('does not show Assign button when only policy is selected', async () => {
178-
// Step 1: Render with only selectedPolicy (no team)
179-
const { queryByRole } = renderComponent({ selectedPolicy: mockPolicies[0] });
154+
fireEvent.click(getByRole('button', { name: /add/i }));
155+
fireEvent.click(getByTestId('edit-dialog-save-button'));
180156

181-
// Step 2: Verify Assign button is NOT visible (needs both)
182-
await waitFor(() => {
183-
expect(queryByRole('button', { name: /assign/i })).toBeNull();
157+
await waitFor(() => {
158+
expect(mockCreateTeamPolicy).toHaveBeenCalledWith({
159+
team_id: 'team-3',
160+
policy_id: 'policy-3'
184161
});
185162
});
186163

187-
it('shows Assign button when both selected and assignment does not exist', async () => {
188-
// Step 1: Render with team + policy that are NOT already assigned
189-
const { getByRole } = renderComponent({
190-
selectedTeam: mockTeams[2], // Gamma Team - not in mockTeamPolicies
191-
selectedPolicy: mockPolicies[2] // Admin Policy - not in mockTeamPolicies
192-
});
193-
194-
// Step 2: Verify Assign button IS visible (can create new assignment)
195-
await waitFor(() => {
196-
expect(getByRole('button', { name: /assign/i })).toBeVisible();
197-
});
164+
await waitFor(() => {
165+
expect(mockRefresh).toHaveBeenCalled();
198166
});
167+
});
199168

200-
it('does not show Assign button when assignment already exists', async () => {
201-
// Step 1: Render with team + policy that ARE already assigned
202-
const { queryByRole } = renderComponent({
203-
selectedTeam: mockTeams[0], // Alpha Team
204-
selectedPolicy: mockPolicies[0] // Data Access Policy - already assigned
205-
});
169+
it('opens Edit Assignment dialog from row action', async () => {
170+
const { getByTestId, getByText } = renderComponent();
206171

207-
// Step 2: Verify Assign button is NOT visible (duplicate prevention)
208-
await waitFor(() => {
209-
expect(queryByRole('button', { name: /assign/i })).toBeNull();
210-
});
172+
await waitFor(() => {
173+
expect(getByTestId('row-tp-1')).toBeVisible();
211174
});
212175

213-
it('calls createTeamPolicy API when Assign is clicked', async () => {
214-
// Step 1: Setup - make createTeamPolicy return {} (simulates successful API response)
215-
mockCreateTeamPolicy.mockResolvedValueOnce({});
216-
217-
// Step 2: Create mock refresh function to verify it's called after submit
218-
const mockRefresh = vi.fn();
219-
220-
// Step 3: Render component with selected team + policy (enables Assign button)
221-
const { getByRole } = renderComponent({
222-
selectedTeam: mockTeams[2],
223-
selectedPolicy: mockPolicies[2],
224-
refresh: mockRefresh
225-
});
176+
const firstRow = getByTestId('row-tp-1');
177+
const rowActionButton = firstRow.querySelector('[data-testid="custom-menu-icon-Actions"]');
178+
expect(rowActionButton).toBeTruthy();
179+
fireEvent.click(rowActionButton as Element);
180+
fireEvent.click(getByTestId('custom-menu-icon-item-Editassignment'));
226181

227-
// Step 4: Wait for Assign button to appear
228-
await waitFor(() => {
229-
expect(getByRole('button', { name: /assign/i })).toBeVisible();
230-
});
231-
232-
// Step 5: Click Assign button
233-
fireEvent.click(getByRole('button', { name: /assign/i }));
234-
235-
// Step 6: Verify API was called with correct params
236-
await waitFor(() => {
237-
expect(mockCreateTeamPolicy).toHaveBeenCalledWith({
238-
team_id: 'team-3',
239-
policy_id: 'policy-3'
240-
});
241-
});
242-
243-
// Step 7: Verify refresh was called after success
244-
await waitFor(() => {
245-
expect(mockRefresh).toHaveBeenCalled();
246-
});
182+
await waitFor(() => {
183+
expect(getByText('Edit Assignment')).toBeVisible();
247184
});
248185
});
249186

250-
describe('Empty State', () => {
251-
it('shows empty state message', async () => {
252-
// Step 1: Render with empty teamPolicies array
253-
const { getByText } = renderComponent({ teamPolicies: [], rowCount: 0 });
187+
it('shows empty state message', async () => {
188+
const { getByText } = renderComponent({ teamPolicies: [], rowCount: 0 });
254189

255-
// Step 2: Verify empty state message appears (from DataGrid localeText)
256-
await waitFor(() => {
257-
expect(getByText('No Team-Policy Assignments')).toBeVisible();
258-
});
190+
await waitFor(() => {
191+
expect(getByText('No Team-Policy Assignments')).toBeVisible();
259192
});
260193
});
261194
});

0 commit comments

Comments
 (0)