@cccsaurora/howler-ui 3.1.0-dev.1708 → 3.1.0-dev.1712
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/api/index.d.ts +3 -3
- package/api/index.js +7 -7
- package/api/index.test.js +6 -6
- package/api/search/user.d.ts +1 -1
- package/api/v2/index.d.ts +2 -1
- package/api/v2/index.js +2 -1
- package/api/v2/index.test.js +3 -1
- package/api/v2/search/index.d.ts +1 -1
- package/api/v2/search/index.js +2 -2
- package/api/v2/search/index.test.js +2 -2
- package/api/v2/task/index.d.ts +20 -0
- package/api/v2/task/index.js +12 -0
- package/api/v2/task/index.test.d.ts +1 -0
- package/api/v2/task/index.test.js +26 -0
- package/components/app/providers/FieldProvider.js +22 -8
- package/components/app/providers/FieldProvider.test.js +20 -2
- package/components/app/providers/RecordSearchProvider.test.js +22 -17
- package/components/hooks/useMyApi.js +3 -0
- package/components/hooks/useMyUserFunctions.d.ts +5 -5
- package/components/routes/hits/search/shared/HitFilter.js +19 -2
- package/components/routes/hits/search/shared/HitFilter.test.js +87 -20
- package/components/routes/home/AddNewCard.js +15 -2
- package/components/routes/home/CasePanel.d.ts +16 -0
- package/components/routes/home/CasePanel.js +105 -0
- package/components/routes/home/CasePanel.test.d.ts +1 -0
- package/components/routes/home/CasePanel.test.js +86 -0
- package/components/routes/home/TasksPanel.d.ts +13 -0
- package/components/routes/home/TasksPanel.js +129 -0
- package/components/routes/home/TasksPanel.test.d.ts +1 -0
- package/components/routes/home/TasksPanel.test.js +173 -0
- package/components/routes/home/ViewCard.d.ts +2 -1
- package/components/routes/home/ViewCard.js +60 -48
- package/components/routes/home/ViewCard.test.js +29 -3
- package/components/routes/home/ViewRefresh.d.ts +14 -9
- package/components/routes/home/ViewRefresh.js +63 -20
- package/components/routes/home/ViewRefresh.test.js +69 -22
- package/components/routes/home/index.js +39 -10
- package/components/routes/home/index.test.js +9 -1
- package/components/routes/views/ViewComposer.test.js +1 -0
- package/locales/en/translation.json +16 -0
- package/locales/fr/translation.json +16 -0
- package/models/entities/HowlerUser.d.ts +1 -1
- package/package.json +3 -1
- package/rest/AxiosClient.d.ts +1 -1
- package/rest/AxiosClient.js +3 -2
- package/rest/AxiosClient.test.js +6 -0
- package/rest/FetchClient.d.ts +1 -1
- package/rest/FetchClient.js +3 -2
- package/rest/FetchClient.test.js +6 -0
- package/rest/index.d.ts +1 -1
|
@@ -7,6 +7,10 @@ import { I18nextProvider } from 'react-i18next';
|
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
8
|
import ViewRefresh, {} from './ViewRefresh';
|
|
9
9
|
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
10
|
+
const createRefs = (refreshRate = 30, viewCardIds = ['one', 'two']) => ({
|
|
11
|
+
refreshRateRef: { current: refreshRate },
|
|
12
|
+
viewCardIdsRef: { current: viewCardIds }
|
|
13
|
+
});
|
|
10
14
|
describe('ViewRefresh', () => {
|
|
11
15
|
beforeEach(() => {
|
|
12
16
|
vi.useFakeTimers();
|
|
@@ -15,21 +19,36 @@ describe('ViewRefresh', () => {
|
|
|
15
19
|
vi.useRealTimers();
|
|
16
20
|
});
|
|
17
21
|
it('should render the refresh button', () => {
|
|
18
|
-
|
|
19
|
-
render(_jsx(ViewRefresh, { refreshRate: 30, viewCardCount: 2, onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
22
|
+
render(_jsx(ViewRefresh, { ...createRefs(), onRefresh: vi.fn() }), { wrapper: Wrapper });
|
|
20
23
|
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
21
24
|
});
|
|
22
25
|
it('should show a progress indicator initially', () => {
|
|
23
|
-
|
|
24
|
-
render(_jsx(ViewRefresh, { refreshRate: 30, viewCardCount: 2, onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
26
|
+
render(_jsx(ViewRefresh, { ...createRefs(), onRefresh: vi.fn() }), { wrapper: Wrapper });
|
|
25
27
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
26
28
|
});
|
|
29
|
+
it('reschedules the countdown using an updated refresh rate', () => {
|
|
30
|
+
const ref = React.createRef();
|
|
31
|
+
const refs = createRefs(30);
|
|
32
|
+
render(_jsx(ViewRefresh, { ref: ref, ...refs, onRefresh: vi.fn() }), { wrapper: Wrapper });
|
|
33
|
+
const progress = screen.getByRole('progressbar');
|
|
34
|
+
expect(progress).toHaveAttribute('aria-valuenow', '0');
|
|
35
|
+
act(() => {
|
|
36
|
+
refs.refreshRateRef.current = 60;
|
|
37
|
+
ref.current?.updateRefreshRate();
|
|
38
|
+
});
|
|
39
|
+
act(() => {
|
|
40
|
+
vi.advanceTimersByTime(300);
|
|
41
|
+
});
|
|
42
|
+
expect(progress).toHaveAttribute('aria-valuenow', '0');
|
|
43
|
+
act(() => {
|
|
44
|
+
vi.advanceTimersByTime(300);
|
|
45
|
+
});
|
|
46
|
+
expect(progress).toHaveAttribute('aria-valuenow', '1');
|
|
47
|
+
});
|
|
27
48
|
it('should trigger refresh when progress reaches 100%', async () => {
|
|
28
49
|
const onRefresh = vi.fn();
|
|
29
|
-
render(_jsx(ViewRefresh, {
|
|
50
|
+
render(_jsx(ViewRefresh, { ...createRefs(), onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
30
51
|
// Progress increments by 1 every refreshRate*10ms = 300ms.
|
|
31
|
-
// Each increment triggers a re-render and a new setTimeout.
|
|
32
|
-
// We advance one tick at a time inside act to let React process each state update.
|
|
33
52
|
for (let i = 0; i < 101; i++) {
|
|
34
53
|
await act(async () => {
|
|
35
54
|
vi.advanceTimersByTime(300);
|
|
@@ -40,40 +59,68 @@ describe('ViewRefresh', () => {
|
|
|
40
59
|
it('should clear refreshing state when all cards report back via ref', async () => {
|
|
41
60
|
const onRefresh = vi.fn();
|
|
42
61
|
const ref = React.createRef();
|
|
43
|
-
render(_jsx(ViewRefresh, { ref: ref,
|
|
44
|
-
// Advance to 100% to trigger refresh
|
|
62
|
+
render(_jsx(ViewRefresh, { ref: ref, ...createRefs(), onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
45
63
|
for (let i = 0; i < 101; i++) {
|
|
46
64
|
await act(async () => {
|
|
47
65
|
vi.advanceTimersByTime(300);
|
|
48
66
|
});
|
|
49
67
|
}
|
|
50
68
|
expect(onRefresh).toHaveBeenCalled();
|
|
51
|
-
// Simulate both cards completing
|
|
52
69
|
act(() => {
|
|
53
|
-
ref.current?.handleRefreshComplete();
|
|
54
|
-
ref.current?.handleRefreshComplete();
|
|
70
|
+
ref.current?.handleRefreshComplete('one', onRefresh.mock.calls[0][0]);
|
|
71
|
+
ref.current?.handleRefreshComplete('two', onRefresh.mock.calls[0][0]);
|
|
55
72
|
});
|
|
56
|
-
// After all cards complete, the progress should reset (button re-enabled)
|
|
57
73
|
expect(screen.getByRole('button')).not.toBeDisabled();
|
|
58
74
|
});
|
|
59
75
|
it('should trigger refresh via manual click', () => {
|
|
60
76
|
const onRefresh = vi.fn();
|
|
61
|
-
render(_jsx(ViewRefresh, {
|
|
62
|
-
// Click the refresh button directly using fireEvent (avoids userEvent timer issues)
|
|
63
|
-
const button = screen.getByRole('button');
|
|
77
|
+
render(_jsx(ViewRefresh, { ...createRefs(), onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
64
78
|
act(() => {
|
|
65
|
-
button.click();
|
|
79
|
+
screen.getByRole('button').click();
|
|
66
80
|
});
|
|
67
81
|
expect(onRefresh).toHaveBeenCalled();
|
|
68
82
|
});
|
|
69
|
-
it('should not call onRefresh when
|
|
83
|
+
it('should not call onRefresh when there are no refreshable panels and the button is clicked', () => {
|
|
70
84
|
const onRefresh = vi.fn();
|
|
71
|
-
render(_jsx(ViewRefresh, {
|
|
72
|
-
const button = screen.getByRole('button');
|
|
85
|
+
render(_jsx(ViewRefresh, { ...createRefs(30, []), onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
73
86
|
act(() => {
|
|
74
|
-
button.click();
|
|
87
|
+
screen.getByRole('button').click();
|
|
75
88
|
});
|
|
76
|
-
// onRefresh should not be called because viewCardCount is 0
|
|
77
89
|
expect(onRefresh).not.toHaveBeenCalled();
|
|
78
90
|
});
|
|
91
|
+
it('keeps active refresh tracking in sync with dashboard panel edits', () => {
|
|
92
|
+
const onRefresh = vi.fn();
|
|
93
|
+
const ref = React.createRef();
|
|
94
|
+
const refs = createRefs();
|
|
95
|
+
render(_jsx(ViewRefresh, { ref: ref, ...refs, onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
96
|
+
act(() => {
|
|
97
|
+
screen.getByRole('button').click();
|
|
98
|
+
});
|
|
99
|
+
const refreshTick = onRefresh.mock.calls[0][0];
|
|
100
|
+
act(() => {
|
|
101
|
+
refs.viewCardIdsRef.current = ['one', 'two', 'three'];
|
|
102
|
+
ref.current?.updateViewCardIds(refs.viewCardIdsRef.current);
|
|
103
|
+
ref.current?.handleRefreshComplete('one', refreshTick);
|
|
104
|
+
ref.current?.handleRefreshComplete('two', refreshTick);
|
|
105
|
+
});
|
|
106
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
107
|
+
act(() => {
|
|
108
|
+
ref.current?.handleRefreshComplete('three', refreshTick);
|
|
109
|
+
});
|
|
110
|
+
expect(screen.getByRole('button')).not.toBeDisabled();
|
|
111
|
+
});
|
|
112
|
+
it('ends an active refresh when dashboard edits remove all pending panels', () => {
|
|
113
|
+
const onRefresh = vi.fn();
|
|
114
|
+
const ref = React.createRef();
|
|
115
|
+
const refs = createRefs();
|
|
116
|
+
render(_jsx(ViewRefresh, { ref: ref, ...refs, onRefresh: onRefresh }), { wrapper: Wrapper });
|
|
117
|
+
act(() => {
|
|
118
|
+
screen.getByRole('button').click();
|
|
119
|
+
});
|
|
120
|
+
act(() => {
|
|
121
|
+
refs.viewCardIdsRef.current = [];
|
|
122
|
+
ref.current?.updateViewCardIds(refs.viewCardIdsRef.current);
|
|
123
|
+
});
|
|
124
|
+
expect(screen.getByRole('button')).not.toBeDisabled();
|
|
125
|
+
});
|
|
79
126
|
});
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';
|
|
3
3
|
import { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
|
4
4
|
import { Cancel, Check, Close, OpenInNew } from '@mui/icons-material';
|
|
5
|
-
import { Alert, AlertTitle, CircularProgress, Grid, IconButton, Stack, Typography } from '@mui/material';
|
|
5
|
+
import { Alert, AlertTitle, Box, CircularProgress, Grid, IconButton, Stack, Typography } from '@mui/material';
|
|
6
6
|
import { PageCenter, useAppUser } from '@tui/core';
|
|
7
7
|
import api from '@cccsaurora/howler-ui/api';
|
|
8
8
|
import { AppBrand } from '@cccsaurora/howler-ui/branding/AppBrand';
|
|
@@ -20,8 +20,10 @@ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
|
20
20
|
import ErrorBoundary from '../ErrorBoundary';
|
|
21
21
|
import AddNewCard from './AddNewCard';
|
|
22
22
|
import AnalyticCard, {} from './AnalyticCard';
|
|
23
|
+
import CasePanel, {} from './CasePanel';
|
|
23
24
|
import EntryWrapper from './EntryWrapper';
|
|
24
25
|
import HomeSettings from './HomeSettings';
|
|
26
|
+
import TasksPanel, {} from './TasksPanel';
|
|
25
27
|
import ViewCard, {} from './ViewCard';
|
|
26
28
|
import ViewRefresh, {} from './ViewRefresh';
|
|
27
29
|
const LUCENE_DATE_FMT = 'YYYY-MM-DD[T]HH:mm:ss';
|
|
@@ -40,6 +42,8 @@ const Home = () => {
|
|
|
40
42
|
const [refreshRate, setRefreshRate] = useState(user.refresh_rate ?? 15);
|
|
41
43
|
const [refreshTick, setRefreshTick] = useState();
|
|
42
44
|
const viewRefreshRef = useRef(null);
|
|
45
|
+
const refreshRateRef = useRef(refreshRate);
|
|
46
|
+
const refreshableCardIdsRef = useRef([]);
|
|
43
47
|
const debounceTimerRef = useRef(null);
|
|
44
48
|
const updateQuery = useMemo(() => `(howler.log.user:${user.username} OR howler.assignment:${user.username}) AND howler.log.timestamp:{${lastViewed} TO now} AND -howler.status:resolved`, [lastViewed, user.username]);
|
|
45
49
|
const getIdFromEntry = useCallback((entry) => {
|
|
@@ -50,6 +54,9 @@ const Home = () => {
|
|
|
50
54
|
else if (entry.type === 'view') {
|
|
51
55
|
return settings.viewId;
|
|
52
56
|
}
|
|
57
|
+
else if (entry.type === 'case' || entry.type === 'tasks') {
|
|
58
|
+
return entry.entry_id;
|
|
59
|
+
}
|
|
53
60
|
else {
|
|
54
61
|
return 'unknown';
|
|
55
62
|
}
|
|
@@ -57,8 +64,8 @@ const Home = () => {
|
|
|
57
64
|
const setLocalDashboard = useCallback((_dashboard) => {
|
|
58
65
|
setStateDashboard(_dashboard ?? []);
|
|
59
66
|
}, []);
|
|
60
|
-
const handleRefreshComplete = useCallback(() => {
|
|
61
|
-
viewRefreshRef.current?.handleRefreshComplete();
|
|
67
|
+
const handleRefreshComplete = useCallback((panelId, completedRefreshTick) => {
|
|
68
|
+
viewRefreshRef.current?.handleRefreshComplete(panelId, completedRefreshTick);
|
|
62
69
|
}, []);
|
|
63
70
|
const handleRefreshRateChange = useCallback((newRate) => {
|
|
64
71
|
setRefreshRate(newRate);
|
|
@@ -74,8 +81,8 @@ const Home = () => {
|
|
|
74
81
|
void setRefreshRateBackend(newRate);
|
|
75
82
|
}, 500);
|
|
76
83
|
}, [setRefreshRateBackend, setUser, user]);
|
|
77
|
-
const handleRefresh = useCallback(() => {
|
|
78
|
-
setRefreshTick(
|
|
84
|
+
const handleRefresh = useCallback((nextRefreshTick) => {
|
|
85
|
+
setRefreshTick(nextRefreshTick);
|
|
79
86
|
}, []);
|
|
80
87
|
const saveChanges = useCallback(async () => {
|
|
81
88
|
setLoading(true);
|
|
@@ -119,15 +126,29 @@ const Home = () => {
|
|
|
119
126
|
clearTimeout(debounceTimerRef.current);
|
|
120
127
|
};
|
|
121
128
|
}, []);
|
|
122
|
-
const
|
|
129
|
+
const refreshableCardIds = useMemo(() => (dashboard ?? []).filter(e => ['view', 'case', 'tasks'].includes(e.type)).map(e => e.entry_id), [dashboard]);
|
|
123
130
|
useEffect(() => {
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
refreshableCardIdsRef.current = refreshableCardIds;
|
|
132
|
+
viewRefreshRef.current?.updateViewCardIds(refreshableCardIds);
|
|
133
|
+
}, [refreshableCardIds]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
refreshRateRef.current = refreshRate;
|
|
136
|
+
viewRefreshRef.current?.updateRefreshRate();
|
|
137
|
+
}, [refreshRate]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
addToAppBar('left', 'view_spacer', _jsx(Box, { px: 0.25 }));
|
|
140
|
+
addToAppBar('left', 'view_refresh', _jsx(ViewRefresh, { ref: viewRefreshRef, refreshRateRef: refreshRateRef, viewCardIdsRef: refreshableCardIdsRef, onRefresh: handleRefresh }));
|
|
126
141
|
return () => {
|
|
142
|
+
removeFromAppBar('view_spacer');
|
|
127
143
|
removeFromAppBar('view_refresh');
|
|
144
|
+
};
|
|
145
|
+
}, [addToAppBar, handleRefresh, removeFromAppBar]);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
addToAppBar('left', 'home_settings', _jsx(HomeSettings, { isEditing: isEditing, refreshRate: refreshRate, onRefreshRateChange: handleRefreshRateChange, onEdit: () => setIsEditing(true) }));
|
|
148
|
+
return () => {
|
|
128
149
|
removeFromAppBar('home_settings');
|
|
129
150
|
};
|
|
130
|
-
}, [addToAppBar,
|
|
151
|
+
}, [addToAppBar, handleRefreshRateChange, isEditing, refreshRate, removeFromAppBar]);
|
|
131
152
|
return (_jsx(PageCenter, { maxWidth: "100%", textAlign: "left", height: "100%", children: _jsx(ErrorBoundary, { children: _jsxs(Stack, { direction: "column", spacing: 1, sx: { height: '100%' }, children: [_jsxs(Stack, { direction: "row", justifyContent: "end", spacing: 1, children: [isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", color: "error", startIcon: _jsx(Cancel, {}), onClick: discardChanges, children: t('cancel') })), isEditing && (_jsx(CustomButton, { variant: "outlined", size: "small", disabled: isEqual(dashboard, user.dashboard), color: 'success', startIcon: loading ? _jsx(CircularProgress, { size: 20 }) : _jsx(Check, {}), onClick: saveChanges, children: t('save') }))] }), updatedHitTotal > 0 && (_jsxs(Alert, { severity: "info", variant: "outlined", action: _jsxs(Stack, { spacing: 1, direction: "row", children: [_jsx(IconButton, { color: "info", component: Link, to: `/hits?query=${encodeURIComponent(updateQuery)}`, onClick: () => setLastViewed(dayjs().utc().format(LUCENE_DATE_FMT)), children: _jsx(OpenInNew, {}) }), _jsx(IconButton, { color: "info", onClick: () => {
|
|
132
153
|
setLastViewed(dayjs().utc().format(LUCENE_DATE_FMT));
|
|
133
154
|
setUpdatedHitTotal(0);
|
|
@@ -144,12 +165,20 @@ const Home = () => {
|
|
|
144
165
|
], children: [(dashboard ?? []).map(entry => {
|
|
145
166
|
if (entry.type === 'view') {
|
|
146
167
|
const settings = JSON.parse(entry.config);
|
|
147
|
-
return (_jsx(EntryWrapper, { editing: isEditing, id: settings.viewId, onDelete: () => setLocalDashboard((dashboard ?? []).filter(_entry => _entry.entry_id !== getIdFromEntry(entry))), children: _jsx(ViewCard, { refreshTick: refreshTick, onRefreshComplete: handleRefreshComplete, ...settings }, entry.config) }, entry.entry_id));
|
|
168
|
+
return (_jsx(EntryWrapper, { editing: isEditing, id: settings.viewId, onDelete: () => setLocalDashboard((dashboard ?? []).filter(_entry => _entry.entry_id !== getIdFromEntry(entry))), children: _jsx(ViewCard, { panelId: entry.entry_id, refreshTick: refreshTick, onRefreshComplete: handleRefreshComplete, ...settings }, entry.config) }, entry.entry_id));
|
|
148
169
|
}
|
|
149
170
|
else if (entry.type === 'analytic') {
|
|
150
171
|
const settings = JSON.parse(entry.config);
|
|
151
172
|
return (_jsx(EntryWrapper, { editing: isEditing, id: getIdFromEntry(entry), onDelete: () => setLocalDashboard((dashboard ?? []).filter(_entry => _entry.entry_id !== getIdFromEntry(entry))), children: _jsx(AnalyticCard, { ...settings }, entry.config) }, entry.entry_id));
|
|
152
173
|
}
|
|
174
|
+
else if (entry.type === 'case') {
|
|
175
|
+
const settings = JSON.parse(entry.config);
|
|
176
|
+
return (_jsx(EntryWrapper, { editing: isEditing, id: getIdFromEntry(entry), onDelete: () => setLocalDashboard((dashboard ?? []).filter(_entry => _entry.entry_id !== getIdFromEntry(entry))), children: _jsx(CasePanel, { panelId: entry.entry_id, refreshTick: refreshTick, onRefreshComplete: handleRefreshComplete, ...settings }) }, entry.entry_id));
|
|
177
|
+
}
|
|
178
|
+
else if (entry.type === 'tasks') {
|
|
179
|
+
const settings = JSON.parse(entry.config);
|
|
180
|
+
return (_jsx(EntryWrapper, { editing: isEditing, id: getIdFromEntry(entry), onDelete: () => setLocalDashboard((dashboard ?? []).filter(_entry => _entry.entry_id !== getIdFromEntry(entry))), children: _jsx(TasksPanel, { panelId: entry.entry_id, refreshTick: refreshTick, onRefreshComplete: handleRefreshComplete, ...settings }) }, entry.entry_id));
|
|
181
|
+
}
|
|
153
182
|
else {
|
|
154
183
|
return null;
|
|
155
184
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/// <reference types="vitest" />
|
|
3
2
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
4
|
const mockDispatchApi = vi.hoisted(() => vi.fn());
|
|
@@ -47,6 +46,7 @@ vi.mock('@mui/material', () => ({
|
|
|
47
46
|
Grid: ({ children }) => _jsx("div", { children: children }),
|
|
48
47
|
IconButton: ({ children, onClick, component, to }) => (_jsx("button", { "data-component": String(!!component), "data-to": to, onClick: onClick, children: children })),
|
|
49
48
|
Stack: ({ children }) => _jsx("div", { children: children }),
|
|
49
|
+
Box: ({ children }) => _jsx("div", { children: children }),
|
|
50
50
|
Typography: ({ children }) => _jsx("div", { children: children })
|
|
51
51
|
}));
|
|
52
52
|
vi.mock('@tui/core', () => ({
|
|
@@ -170,6 +170,8 @@ describe('Home', () => {
|
|
|
170
170
|
expect(screen.getByText('analytic:a1:created')).toBeInTheDocument();
|
|
171
171
|
expect(mockAddToAppBar).toHaveBeenCalledWith('left', 'view_refresh', expect.anything());
|
|
172
172
|
expect(mockAddToAppBar).toHaveBeenCalledWith('left', 'home_settings', expect.anything());
|
|
173
|
+
const viewRefreshNode = mockAddToAppBar.mock.calls.find(([, key]) => key === 'view_refresh')[2];
|
|
174
|
+
expect(viewRefreshNode.props.viewCardIdsRef.current).toEqual(['view-entry']);
|
|
173
175
|
const homeSettings = mockAddToAppBar.mock.calls.find(([, key]) => key === 'home_settings')[2];
|
|
174
176
|
await act(async () => {
|
|
175
177
|
homeSettings.props.onEdit();
|
|
@@ -180,7 +182,13 @@ describe('Home', () => {
|
|
|
180
182
|
await new Promise(resolve => setTimeout(resolve, 550));
|
|
181
183
|
});
|
|
182
184
|
expect(mockSetRefreshRateBackend).toHaveBeenCalledWith(30);
|
|
185
|
+
expect(viewRefreshNode.props.refreshRateRef.current).toBe(30);
|
|
186
|
+
expect(mockRemoveFromAppBar).not.toHaveBeenCalledWith('view_refresh');
|
|
187
|
+
expect(mockAddToAppBar.mock.calls.filter(([, key]) => key === 'view_refresh')).toHaveLength(1);
|
|
183
188
|
fireEvent.click(screen.getByText('add-card'));
|
|
189
|
+
await waitFor(() => expect(viewRefreshNode.props.viewCardIdsRef.current).toEqual(['view-entry', 'new-entry']));
|
|
190
|
+
expect(mockRemoveFromAppBar).not.toHaveBeenCalledWith('view_refresh');
|
|
191
|
+
expect(mockAddToAppBar.mock.calls.filter(([, key]) => key === 'view_refresh')).toHaveLength(1);
|
|
184
192
|
const saveButton = screen.getByText('save');
|
|
185
193
|
expect(saveButton).not.toBeDisabled();
|
|
186
194
|
fireEvent.click(saveButton);
|
|
@@ -300,6 +300,7 @@ describe('ViewComposer', () => {
|
|
|
300
300
|
expect(parameterContextValue.setSpan).toHaveBeenCalledWith('7d');
|
|
301
301
|
expect(screen.getByText('event:event-1')).toBeInTheDocument();
|
|
302
302
|
fireEvent.change(screen.getByLabelText('route.views.name'), { target: { value: 'Updated View' } });
|
|
303
|
+
fireEvent.click(screen.getByText('record-query-clean'));
|
|
303
304
|
fireEvent.click(screen.getByText('checked'));
|
|
304
305
|
fireEvent.click(screen.getByText('save'));
|
|
305
306
|
await waitFor(() => expect(mockEditView).toHaveBeenCalledWith('view-1', {
|
|
@@ -825,6 +825,10 @@
|
|
|
825
825
|
"route.home.add.type": "Panel Type",
|
|
826
826
|
"route.home.add.type.analytic": "Analytic Panel",
|
|
827
827
|
"route.home.add.type.analytic.description": "Present statistics on a given analytic = the resulting assessments, number of hits created, or escalation of created hits",
|
|
828
|
+
"route.home.add.type.case": "Case Panel",
|
|
829
|
+
"route.home.add.type.case.description": "Present cases with filters for status, assignee, and creation date.",
|
|
830
|
+
"route.home.add.type.tasks": "Tasks Panel",
|
|
831
|
+
"route.home.add.type.tasks.description": "Present your tasks across all cases, filtered by completion.",
|
|
828
832
|
"route.home.add.type.view": "View Panel",
|
|
829
833
|
"route.home.add.type.view.description": "Present a saved view, limited to a certain number of hits.",
|
|
830
834
|
"route.home.add.view": "View to present",
|
|
@@ -842,7 +846,19 @@
|
|
|
842
846
|
"route.home.add.visualization.unavailable": "No visualization options available.",
|
|
843
847
|
"route.home.alert.updated.description": "There have been updates to alerts you are involved in. Since you last viewed, {{count}} alert(s) have updates.",
|
|
844
848
|
"route.home.alert.updated.title": "Alert Updates",
|
|
849
|
+
"route.home.case.error": "Unable to load cases.",
|
|
850
|
+
"route.home.case.open": "Open cases",
|
|
851
|
+
"route.home.case.title": "Cases",
|
|
845
852
|
"route.home.description": "Welcome to Howler. You can add items to your dashboard using the buttons above.",
|
|
853
|
+
"route.home.tasks.error": "Unable to load tasks.",
|
|
854
|
+
"route.home.tasks.filter": "Task status",
|
|
855
|
+
"route.home.tasks.filter.all": "All tasks",
|
|
856
|
+
"route.home.tasks.filter.complete": "Completed",
|
|
857
|
+
"route.home.tasks.filter.incomplete": "Not completed",
|
|
858
|
+
"route.home.tasks.next": "Next",
|
|
859
|
+
"route.home.tasks.open": "Open cases",
|
|
860
|
+
"route.home.tasks.previous": "Previous",
|
|
861
|
+
"route.home.tasks.title": "My Tasks",
|
|
846
862
|
"route.home.title": "Howler",
|
|
847
863
|
"route.integrations": "Integrations",
|
|
848
864
|
"route.login.button.oauth": "Sign in with",
|
|
@@ -825,6 +825,10 @@
|
|
|
825
825
|
"route.home.add.type": "Type de panneau",
|
|
826
826
|
"route.home.add.type.analytic": "Panneau analytique",
|
|
827
827
|
"route.home.add.type.analytic.description": "Présenter les statistiques d'une analyse donnée = les évaluations résultantes, le nombre de résultats créés ou l'escalade des résultats créés",
|
|
828
|
+
"route.home.add.type.case": "Panneau des cas",
|
|
829
|
+
"route.home.add.type.case.description": "Présenter les cas avec des filtres pour le statut, l'assigné et la date de création.",
|
|
830
|
+
"route.home.add.type.tasks": "Panneau des tâches",
|
|
831
|
+
"route.home.add.type.tasks.description": "Présenter vos tâches dans tous les cas, filtrées selon leur achèvement.",
|
|
828
832
|
"route.home.add.type.view": "Panneau d'affichage",
|
|
829
833
|
"route.home.add.type.view.description": "Présenter une vue sauvegardée, limitée à un certain nombre de résultats",
|
|
830
834
|
"route.home.add.view": "Vue à présenter",
|
|
@@ -842,7 +846,19 @@
|
|
|
842
846
|
"route.home.add.visualization.unavailable": "Aucune option de visualisation disponible.",
|
|
843
847
|
"route.home.alert.updated.description": "Des mises à jour ont été apportées aux alertes auxquelles vous participez. Depuis votre dernière consultation, {{count}} alertes ont été mises à jour",
|
|
844
848
|
"route.home.alert.updated.title": "Mises à jour d'alert",
|
|
849
|
+
"route.home.case.error": "Impossible de charger les cas.",
|
|
850
|
+
"route.home.case.open": "Ouvrir les cas",
|
|
851
|
+
"route.home.case.title": "Cas",
|
|
845
852
|
"route.home.description": "Bienvenue sur Howler. Vous pouvez ajouter des éléments à votre tableau de bord en utilisant les boutons ci-dessus.",
|
|
853
|
+
"route.home.tasks.error": "Impossible de charger les tâches.",
|
|
854
|
+
"route.home.tasks.filter": "État des tâches",
|
|
855
|
+
"route.home.tasks.filter.all": "Toutes les tâches",
|
|
856
|
+
"route.home.tasks.filter.complete": "Terminées",
|
|
857
|
+
"route.home.tasks.filter.incomplete": "Non terminées",
|
|
858
|
+
"route.home.tasks.next": "Suivant",
|
|
859
|
+
"route.home.tasks.open": "Ouvrir les cas",
|
|
860
|
+
"route.home.tasks.previous": "Précédent",
|
|
861
|
+
"route.home.tasks.title": "Mes tâches",
|
|
846
862
|
"route.home.title": "Howler",
|
|
847
863
|
"route.integrations": "Intégrations",
|
|
848
864
|
"route.login.button.oauth": "Se connecter avec",
|
|
@@ -14,6 +14,6 @@ export interface HowlerUser extends AppUser {
|
|
|
14
14
|
is_active?: boolean;
|
|
15
15
|
favourite_views?: string[];
|
|
16
16
|
favourite_analytics?: string[];
|
|
17
|
-
dashboard?: { entry_id: string; type: 'view' | 'analytic'; config: string }[];
|
|
17
|
+
dashboard?: { entry_id: string; type: 'view' | 'analytic' | 'case' | 'tasks'; config: string }[];
|
|
18
18
|
refresh_rate?: number;
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"url": "https://github.com/CybercentreCanada/howler"
|
|
98
98
|
},
|
|
99
99
|
"type": "module",
|
|
100
|
-
"version": "3.1.0-dev.
|
|
100
|
+
"version": "3.1.0-dev.1712",
|
|
101
101
|
"exports": {
|
|
102
102
|
"./i18n": "./i18n.js",
|
|
103
103
|
"./index.css": "./index.css",
|
|
@@ -243,6 +243,8 @@
|
|
|
243
243
|
"./api/v2/case": "./api/v2/case/index.js",
|
|
244
244
|
"./api/v2/search/*": "./api/v2/search/*.js",
|
|
245
245
|
"./api/v2/search": "./api/v2/search/index.js",
|
|
246
|
+
"./api/v2/task/*": "./api/v2/task/*.js",
|
|
247
|
+
"./api/v2/task": "./api/v2/task/index.js",
|
|
246
248
|
"./api/v2/case/rules/*": "./api/v2/case/rules/*.js",
|
|
247
249
|
"./api/v2/case/rules": "./api/v2/case/rules/index.js",
|
|
248
250
|
"./api/v2/case/rules/backfill/*": "./api/v2/case/rules/backfill/*.js",
|
package/rest/AxiosClient.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export default class AxiosClient implements RestClient {
|
|
|
4
4
|
private cache;
|
|
5
5
|
private client;
|
|
6
6
|
constructor();
|
|
7
|
-
fetch<R>(url: string, method?: 'get' | 'post' | 'put' | 'delete' | 'patch', body?: any, params?: URLSearchParams, headers?: HeadersInit): Promise<[HowlerResponse<R>, number, {
|
|
7
|
+
fetch<R>(url: string, method?: 'get' | 'post' | 'put' | 'delete' | 'patch', body?: any, params?: URLSearchParams, headers?: HeadersInit, signal?: AbortSignal): Promise<[HowlerResponse<R>, number, {
|
|
8
8
|
[index: string]: any;
|
|
9
9
|
}]>;
|
|
10
10
|
}
|
package/rest/AxiosClient.js
CHANGED
|
@@ -37,14 +37,15 @@ export default class AxiosClient {
|
|
|
37
37
|
retryDelay: exponentialDelay
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
-
async fetch(url, method = 'get', body, params, headers) {
|
|
40
|
+
async fetch(url, method = 'get', body, params, headers, signal) {
|
|
41
41
|
const config = {
|
|
42
42
|
url,
|
|
43
43
|
params,
|
|
44
44
|
method,
|
|
45
45
|
withCredentials: true,
|
|
46
46
|
data: JSON.stringify(body),
|
|
47
|
-
headers: headers
|
|
47
|
+
headers: headers,
|
|
48
|
+
signal
|
|
48
49
|
};
|
|
49
50
|
try {
|
|
50
51
|
const response = await this.client(config);
|
package/rest/AxiosClient.test.js
CHANGED
|
@@ -103,6 +103,12 @@ describe('AxiosClient', () => {
|
|
|
103
103
|
const config = mockAxiosInstance.mock.calls[0][0];
|
|
104
104
|
expect(config.headers).toEqual(headers);
|
|
105
105
|
});
|
|
106
|
+
it('forwards an abort signal', async () => {
|
|
107
|
+
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
await client.fetch('/api/v1/hit', 'get', undefined, undefined, undefined, controller.signal);
|
|
110
|
+
expect(mockAxiosInstance.mock.calls[0][0].signal).toBe(controller.signal);
|
|
111
|
+
});
|
|
106
112
|
it('sets withCredentials to true', async () => {
|
|
107
113
|
mockAxiosInstance.mockResolvedValueOnce({ data: {}, status: 200, headers: {} });
|
|
108
114
|
await client.fetch('/api/v1/hit');
|
package/rest/FetchClient.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { HowlerResponse } from '@cccsaurora/howler-ui/api';
|
|
2
2
|
import type RestClient from '@cccsaurora/howler-ui/rest';
|
|
3
3
|
export default class FetchClient implements RestClient {
|
|
4
|
-
fetch<R>(url: string, method?: 'get' | 'post' | 'put' | 'delete', body?: any, params?: URLSearchParams, headers?: HeadersInit): Promise<[HowlerResponse<R>, number, {
|
|
4
|
+
fetch<R>(url: string, method?: 'get' | 'post' | 'put' | 'delete' | 'patch', body?: any, params?: URLSearchParams, headers?: HeadersInit, signal?: AbortSignal): Promise<[HowlerResponse<R>, number, {
|
|
5
5
|
[index: string]: any;
|
|
6
6
|
}] | undefined>;
|
|
7
7
|
}
|
package/rest/FetchClient.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export default class FetchClient {
|
|
2
|
-
async fetch(url, method = 'get', body, params, headers) {
|
|
2
|
+
async fetch(url, method = 'get', body, params, headers, signal) {
|
|
3
3
|
const _url = `${url}${params ? `?${params.toString()}` : ''}`;
|
|
4
4
|
const request = {
|
|
5
5
|
method,
|
|
6
6
|
credentials: 'same-origin',
|
|
7
|
-
headers
|
|
7
|
+
headers,
|
|
8
|
+
signal
|
|
8
9
|
};
|
|
9
10
|
if (method !== 'get' && body) {
|
|
10
11
|
request.body = JSON.stringify(body);
|
package/rest/FetchClient.test.js
CHANGED
|
@@ -62,6 +62,12 @@ describe('FetchClient', () => {
|
|
|
62
62
|
const callArgs = spy.mock.calls[0][1];
|
|
63
63
|
expect(callArgs.headers.Authorization).toBe('******');
|
|
64
64
|
});
|
|
65
|
+
it('forwards an abort signal to fetch', async () => {
|
|
66
|
+
const spy = mockFetch(200, {});
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
await client.fetch('/api/v1/hit', 'get', undefined, undefined, undefined, controller.signal);
|
|
69
|
+
expect(spy.mock.calls[0][1]?.signal).toBe(controller.signal);
|
|
70
|
+
});
|
|
65
71
|
it('returns undefined for a 204 No Content response', async () => {
|
|
66
72
|
mockFetch(204, null);
|
|
67
73
|
const result = await client.fetch('/api/v1/hit', 'delete');
|
package/rest/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export type RestResponse<R> = [HowlerResponse<R>, number, {
|
|
|
3
3
|
[index: string]: any;
|
|
4
4
|
}];
|
|
5
5
|
export default interface RestClient {
|
|
6
|
-
fetch<R>(url: string, method: 'get' | 'post' | 'put' | 'delete', body?: any, params?: URLSearchParams, headers?: HeadersInit): Promise<RestResponse<R> | undefined>;
|
|
6
|
+
fetch<R>(url: string, method: 'get' | 'post' | 'put' | 'delete' | 'patch', body?: any, params?: URLSearchParams, headers?: HeadersInit, signal?: AbortSignal): Promise<RestResponse<R> | undefined>;
|
|
7
7
|
}
|