@cccsaurora/howler-ui 2.19.0-cases.1097 → 2.19.0-cases.1110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/routes/cases/detail/CaseDetails.js +13 -4
- package/components/routes/cases/detail/CaseDetails.test.d.ts +1 -0
- package/components/routes/cases/detail/CaseDetails.test.js +146 -0
- package/locales/en/translation.json +1 -0
- package/locales/fr/translation.json +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Check, FormatListBulleted, HourglassBottom, Pause, People, WarningRounded } from '@mui/icons-material';
|
|
3
|
-
import { Autocomplete, AvatarGroup, Card,
|
|
2
|
+
import { Check, FormatListBulleted, HourglassBottom, Pause, People, TrendingUp, WarningRounded } from '@mui/icons-material';
|
|
3
|
+
import { Autocomplete, AvatarGroup, Card, Divider, LinearProgress, Skeleton, Stack, Table, TableBody, TableCell, TableRow, TextField, Typography } from '@mui/material';
|
|
4
4
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
5
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
6
|
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
@@ -10,6 +10,7 @@ import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
|
|
|
10
10
|
import dayjs from 'dayjs';
|
|
11
11
|
import { useContext, useState } from 'react';
|
|
12
12
|
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import { ESCALATION_COLOR_MAP } from '../constants';
|
|
13
14
|
import useCase from '../hooks/useCase';
|
|
14
15
|
import ResolveModal from '../modals/ResolveModal';
|
|
15
16
|
import SourceAggregate from './aggregates/SourceAggregate';
|
|
@@ -61,10 +62,18 @@ const CaseDetails = ({ case: providedCase }) => {
|
|
|
61
62
|
'in-progress': _jsx(HourglassBottom, { color: "warning" }),
|
|
62
63
|
'on-hold': _jsx(Pause, { color: "disabled" }),
|
|
63
64
|
resolved: _jsx(Check, { color: "success" })
|
|
64
|
-
}[_case.status] ?? _jsx(WarningRounded, { fontSize: "small" }), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.status') })] }), _jsx(Autocomplete, { size: "small", disabled: loading, value: _case.status, options: config.lookups['howler.status'], renderInput: params => _jsx(TextField, { ...params, size: "small" }), onChange: (_ev, status) =>
|
|
65
|
+
}[_case.status] ?? _jsx(WarningRounded, { fontSize: "small" }), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.status') })] }), _jsx(Autocomplete, { size: "small", disabled: loading, disableClearable: true, value: _case.status, options: config.lookups['howler.status'], renderInput: params => _jsx(TextField, { ...params, size: "small" }), onChange: (_ev, status) => {
|
|
66
|
+
if (status) {
|
|
67
|
+
handleStatus(status);
|
|
68
|
+
}
|
|
69
|
+
} }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(TrendingUp, { color: ESCALATION_COLOR_MAP[_case.escalation] }), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.escalation') })] }), _jsx(Autocomplete, { size: "small", disabled: loading, disableClearable: true, value: _case.escalation ?? null, options: config.lookups['case.escalation'], renderInput: params => _jsx(TextField, { ...params, size: "small" }), onChange: (_ev, escalation) => {
|
|
70
|
+
if (escalation) {
|
|
71
|
+
wrappedUpdate({ escalation });
|
|
72
|
+
}
|
|
73
|
+
} })] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(People, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.participants') })] }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(UserList, { buttonSx: { alignSelf: 'start' }, multiple: true, i18nLabel: "page.cases.detail.assignment", userIds: _case.participants ?? [], onChange: participants => wrappedUpdate({ participants }), disabled: loading }), _jsx("div", { style: { flex: 1 } })] }), caseViewers.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsxs(Stack, { direction: "row", alignItems: "center", children: [_jsx(SocketBadge, { size: "medium" }), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.viewers') })] }), _jsx(AvatarGroup, { max: 4, sx: { alignSelf: 'start' }, componentsProps: {
|
|
65
74
|
additionalAvatar: {
|
|
66
75
|
sx: { height: 32, width: 32, fontSize: '12px' }
|
|
67
76
|
}
|
|
68
|
-
}, children: caseViewers.map(viewer => (_jsx(HowlerAvatar, { userId: viewer, sx: { height: 32, width: 32 } }, viewer))) })] }))] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(FormatListBulleted, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.properties') })] }), _jsx(Table, { sx: { '& td': { p: 1 } }, children: _jsxs(TableBody, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.
|
|
77
|
+
}, children: caseViewers.map(viewer => (_jsx(HowlerAvatar, { userId: viewer, sx: { height: 32, width: 32 } }, viewer))) })] }))] }), _jsx(Divider, {}), _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(FormatListBulleted, {}), _jsx(Typography, { variant: "body1", children: t('page.cases.detail.properties') })] }), _jsx(Table, { sx: { '& td': { p: 1 } }, children: _jsxs(TableBody, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.created') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.created).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.updated') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: dayjs(_case.updated).toString() }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: t('page.cases.sources') }) }), _jsx(TableCell, { children: _jsx(Typography, { variant: "caption", children: _jsx(SourceAggregate, { case: _case }) }) })] })] }) })] })] })] }));
|
|
69
78
|
};
|
|
70
79
|
export default CaseDetails;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent, {} from '@testing-library/user-event';
|
|
5
|
+
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
6
|
+
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
7
|
+
import {} from 'react';
|
|
8
|
+
import { createMockCase } from '@cccsaurora/howler-ui/tests/utils';
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Hoisted stubs
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const mockUpdate = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
15
|
+
const mockShowModal = vi.hoisted(() => vi.fn());
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Module mocks (registered before the dynamic import below)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
vi.mock('../hooks/useCase', () => ({
|
|
20
|
+
default: ({ case: c }) => ({
|
|
21
|
+
case: c,
|
|
22
|
+
update: mockUpdate,
|
|
23
|
+
loading: false,
|
|
24
|
+
missing: false
|
|
25
|
+
})
|
|
26
|
+
}));
|
|
27
|
+
vi.mock('components/app/providers/ModalProvider', async () => {
|
|
28
|
+
const { createContext } = await import('react');
|
|
29
|
+
return {
|
|
30
|
+
ModalContext: createContext({
|
|
31
|
+
showModal: mockShowModal,
|
|
32
|
+
close: vi.fn(),
|
|
33
|
+
setContent: vi.fn(),
|
|
34
|
+
withConfirmDeleteModal: vi.fn()
|
|
35
|
+
})
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
vi.mock('components/elements/UserList', () => ({
|
|
39
|
+
default: () => _jsx("div", { id: "user-list" })
|
|
40
|
+
}));
|
|
41
|
+
vi.mock('components/elements/display/HowlerAvatar', () => ({
|
|
42
|
+
default: ({ userId }) => _jsx("div", { children: userId })
|
|
43
|
+
}));
|
|
44
|
+
vi.mock('components/elements/display/icons/SocketBadge', () => ({
|
|
45
|
+
default: () => _jsx("span", {})
|
|
46
|
+
}));
|
|
47
|
+
vi.mock('./aggregates/SourceAggregate', () => ({
|
|
48
|
+
default: () => _jsx("span", {})
|
|
49
|
+
}));
|
|
50
|
+
vi.mock('../modals/ResolveModal', () => ({
|
|
51
|
+
default: () => null
|
|
52
|
+
}));
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Shared provider config
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
const mockConfig = {
|
|
57
|
+
lookups: {
|
|
58
|
+
'howler.status': ['open', 'in-progress', 'on-hold', 'resolved'],
|
|
59
|
+
'case.escalation': ['normal', 'focus', 'crisis'],
|
|
60
|
+
'howler.escalation': ['miss', 'hit', 'alert', 'evidence']
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const mockViewers = {};
|
|
64
|
+
const Wrapper = ({ children }) => (_jsx(ApiConfigContext.Provider, { value: { config: mockConfig, setConfig: vi.fn() }, children: _jsx(SocketContext.Provider, { value: {
|
|
65
|
+
emit: vi.fn(),
|
|
66
|
+
open: true,
|
|
67
|
+
fetchViewers: vi.fn(),
|
|
68
|
+
addListener: vi.fn(),
|
|
69
|
+
removeListener: vi.fn(),
|
|
70
|
+
status: 1,
|
|
71
|
+
reconnect: vi.fn(),
|
|
72
|
+
viewers: { ...mockViewers }
|
|
73
|
+
}, children: children }) }));
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Import after mocks
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
const { default: CaseDetails } = await import('./CaseDetails');
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Tests
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
describe('CaseDetails', () => {
|
|
82
|
+
let user;
|
|
83
|
+
let testCase;
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
user = userEvent.setup();
|
|
86
|
+
vi.clearAllMocks();
|
|
87
|
+
mockUpdate.mockResolvedValue(undefined);
|
|
88
|
+
testCase = createMockCase({ case_id: 'test-case-id', status: 'open', escalation: 'normal' });
|
|
89
|
+
Object.keys(mockViewers).forEach(k => delete mockViewers[k]);
|
|
90
|
+
});
|
|
91
|
+
it('renders a skeleton and no controls when the case is null', () => {
|
|
92
|
+
render(_jsx(CaseDetails, { case: null }), { wrapper: Wrapper });
|
|
93
|
+
expect(document.querySelector('.MuiSkeleton-root')).toBeTruthy();
|
|
94
|
+
expect(screen.queryByRole('combobox')).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it('renders the status label and current status value', () => {
|
|
97
|
+
render(_jsx(CaseDetails, { case: testCase }), { wrapper: Wrapper });
|
|
98
|
+
expect(screen.getByText('page.cases.detail.status')).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByDisplayValue('open')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
it('renders the escalation label and current escalation value', () => {
|
|
102
|
+
render(_jsx(CaseDetails, { case: testCase }), { wrapper: Wrapper });
|
|
103
|
+
expect(screen.getByText('page.cases.detail.escalation')).toBeInTheDocument();
|
|
104
|
+
expect(screen.getByDisplayValue('normal')).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
describe('status changes', () => {
|
|
107
|
+
it('calls update with the new status when a non-resolved option is selected', async () => {
|
|
108
|
+
render(_jsx(CaseDetails, { case: testCase }), { wrapper: Wrapper });
|
|
109
|
+
await user.click(screen.getByDisplayValue('open'));
|
|
110
|
+
await user.click(await screen.findByRole('option', { name: 'in-progress' }));
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(mockUpdate).toHaveBeenCalledWith({ status: 'in-progress' });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it('opens the resolve modal instead of calling update when "resolved" is selected', async () => {
|
|
116
|
+
render(_jsx(CaseDetails, { case: testCase }), { wrapper: Wrapper });
|
|
117
|
+
await user.click(screen.getByDisplayValue('open'));
|
|
118
|
+
await user.click(await screen.findByRole('option', { name: 'resolved' }));
|
|
119
|
+
expect(mockShowModal).toHaveBeenCalledOnce();
|
|
120
|
+
expect(mockUpdate).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('escalation changes', () => {
|
|
124
|
+
it('calls update with the new escalation when an option is selected', async () => {
|
|
125
|
+
render(_jsx(CaseDetails, { case: testCase }), { wrapper: Wrapper });
|
|
126
|
+
await user.click(screen.getByDisplayValue('normal'));
|
|
127
|
+
await user.click(await screen.findByRole('option', { name: 'focus' }));
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(mockUpdate).toHaveBeenCalledWith({ escalation: 'focus' });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('viewers section', () => {
|
|
134
|
+
it('hides the viewers section when no viewers are active', () => {
|
|
135
|
+
render(_jsx(CaseDetails, { case: testCase }), { wrapper: Wrapper });
|
|
136
|
+
expect(screen.queryByText('page.cases.detail.viewers')).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
it('shows the viewers section and renders viewer avatars when active viewers are present', () => {
|
|
139
|
+
mockViewers['test-case-id'] = ['user-1', 'user-2'];
|
|
140
|
+
render(_jsx(CaseDetails, { case: testCase }), { wrapper: Wrapper });
|
|
141
|
+
expect(screen.getByText('page.cases.detail.viewers')).toBeInTheDocument();
|
|
142
|
+
expect(screen.getByText('user-1')).toBeInTheDocument();
|
|
143
|
+
expect(screen.getByText('user-2')).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -420,6 +420,7 @@
|
|
|
420
420
|
"page.cases.detail.participants": "Participants",
|
|
421
421
|
"page.cases.detail.properties": "Properties",
|
|
422
422
|
"page.cases.detail.status": "Status",
|
|
423
|
+
"page.cases.detail.escalation": "Escalation",
|
|
423
424
|
"page.cases.detail.viewers": "Active Viewers",
|
|
424
425
|
"page.cases.escalation": "Escalation",
|
|
425
426
|
"page.cases.folder.drop.root": "Place here to move to root",
|
|
@@ -420,6 +420,7 @@
|
|
|
420
420
|
"page.cases.detail.participants": "Participants",
|
|
421
421
|
"page.cases.detail.properties": "Propriétés",
|
|
422
422
|
"page.cases.detail.status": "Statut",
|
|
423
|
+
"page.cases.detail.escalation": "Escalade",
|
|
423
424
|
"page.cases.detail.viewers": "Spectateurs actifs",
|
|
424
425
|
"page.cases.escalation": "Escalade",
|
|
425
426
|
"page.cases.folder.drop.root": "Déposer ici pour déplacer à la racine",
|