@cccsaurora/howler-ui 2.18.0-dev.762 → 2.18.0-dev.766
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/Cases.js +58 -11
- package/components/routes/cases/constants.d.ts +1 -0
- package/components/routes/cases/constants.js +1 -0
- package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
- package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
- package/components/routes/cases/search/CaseDateFilter.js +26 -0
- package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
- package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
- package/components/routes/cases/search/CaseStatusFilter.js +13 -0
- package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
- package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
- package/locales/en/translation.json +10 -1
- package/locales/fr/translation.json +10 -1
- package/package.json +2 -1
- package/utils/constants.d.ts +1 -0
- package/utils/constants.js +6 -0
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Topic } from '@mui/icons-material';
|
|
3
|
-
import { Typography } from '@mui/material';
|
|
3
|
+
import { Stack, Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
5
|
import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
|
|
6
6
|
import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
|
|
7
7
|
import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
|
|
8
8
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
9
9
|
import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
|
|
10
|
-
import
|
|
10
|
+
import dayjs from 'dayjs';
|
|
11
|
+
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
11
12
|
import { useTranslation } from 'react-i18next';
|
|
12
13
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
13
|
-
import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
14
|
+
import { DATE_RANGE_LUCENE, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
|
|
15
|
+
import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
|
|
14
16
|
import CaseCard from '../../elements/case/CaseCard';
|
|
17
|
+
import CaseAssigneeFilter from './search/CaseAssigneeFilter';
|
|
18
|
+
import CaseDateFilter, {} from './search/CaseDateFilter';
|
|
19
|
+
import CaseStatusFilter from './search/CaseStatusFilter';
|
|
20
|
+
const buildPhraseQuery = (phrase) => {
|
|
21
|
+
const sanitized = sanitizeLuceneQuery(phrase);
|
|
22
|
+
if (!phrase) {
|
|
23
|
+
return '(title:* OR summary:* OR overview:* OR participants:* OR tasks.summary:* OR tasks.assignment:*)';
|
|
24
|
+
}
|
|
25
|
+
return (`(title:*${sanitized}* OR summary:*${sanitized}* OR overview:*${sanitized}* OR participants:*${sanitized}* ` +
|
|
26
|
+
`OR tasks.summary:*${sanitized}* OR tasks.assignment:*${sanitized}*)`);
|
|
27
|
+
};
|
|
15
28
|
const CasesBase = () => {
|
|
16
29
|
const { t } = useTranslation();
|
|
17
30
|
const navigate = useNavigate();
|
|
@@ -24,6 +37,31 @@ const CasesBase = () => {
|
|
|
24
37
|
const [response, setResponse] = useState(null);
|
|
25
38
|
const [hasError, setHasError] = useState(false);
|
|
26
39
|
const [loading, setLoading] = useState(false);
|
|
40
|
+
const [statusFilter, setStatusFilter] = useState([]);
|
|
41
|
+
const [assigneeFilter, setAssigneeFilter] = useState([]);
|
|
42
|
+
const [dateRange, setDateRange] = useState('date.range.all');
|
|
43
|
+
const [customStart, setCustomStart] = useState(dayjs().subtract(2, 'days'));
|
|
44
|
+
const [customEnd, setCustomEnd] = useState(dayjs());
|
|
45
|
+
const filtersReady = useRef(false);
|
|
46
|
+
const buildFilters = useCallback(() => {
|
|
47
|
+
const filters = [];
|
|
48
|
+
if (statusFilter.length > 0) {
|
|
49
|
+
filters.push(`status:(${statusFilter.map(status => `"${status}"`).join(' OR ')})`);
|
|
50
|
+
}
|
|
51
|
+
if (assigneeFilter.length > 0) {
|
|
52
|
+
filters.push(assigneeFilter
|
|
53
|
+
.map(assignee => `(participants:"${sanitizeLuceneQuery(assignee)}" OR tasks.assignment:"${sanitizeLuceneQuery(assignee)}")`)
|
|
54
|
+
.join(' OR '));
|
|
55
|
+
}
|
|
56
|
+
const lucene = DATE_RANGE_LUCENE[dateRange];
|
|
57
|
+
if (lucene) {
|
|
58
|
+
filters.push(`created:[${lucene} TO now]`);
|
|
59
|
+
}
|
|
60
|
+
else if (dateRange === 'date.range.custom') {
|
|
61
|
+
filters.push(`created:[${customStart.toISOString()} TO ${customEnd.toISOString()}]`);
|
|
62
|
+
}
|
|
63
|
+
return filters;
|
|
64
|
+
}, [statusFilter, assigneeFilter, dateRange, customStart, customEnd]);
|
|
27
65
|
const onSearch = useCallback(async () => {
|
|
28
66
|
try {
|
|
29
67
|
setLoading(true);
|
|
@@ -35,11 +73,10 @@ const CasesBase = () => {
|
|
|
35
73
|
searchParams.delete('phrase');
|
|
36
74
|
}
|
|
37
75
|
setSearchParams(searchParams, { replace: true });
|
|
38
|
-
|
|
39
|
-
const query = phrase ? `*:*${phrase}*` : '*:*';
|
|
40
|
-
// Ensure the overview should be visible and/or matches the type we are filtering for
|
|
76
|
+
const filters = buildFilters();
|
|
41
77
|
setResponse(await dispatchApi(api.search.case.post({
|
|
42
|
-
query,
|
|
78
|
+
query: buildPhraseQuery(phrase),
|
|
79
|
+
filters,
|
|
43
80
|
rows: pageCount,
|
|
44
81
|
offset
|
|
45
82
|
})));
|
|
@@ -50,9 +87,8 @@ const CasesBase = () => {
|
|
|
50
87
|
finally {
|
|
51
88
|
setLoading(false);
|
|
52
89
|
}
|
|
53
|
-
}, [phrase, setSearchParams, searchParams, dispatchApi, pageCount, offset]);
|
|
90
|
+
}, [buildFilters, phrase, setSearchParams, searchParams, dispatchApi, pageCount, offset]);
|
|
54
91
|
// Load the items into list when response changes.
|
|
55
|
-
// This hook should only trigger when the 'response' changes.
|
|
56
92
|
useEffect(() => {
|
|
57
93
|
if (response) {
|
|
58
94
|
load(response.items.map((item) => ({
|
|
@@ -92,8 +128,19 @@ const CasesBase = () => {
|
|
|
92
128
|
}
|
|
93
129
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
94
130
|
}, [offset]);
|
|
131
|
+
// Re-search when filter chips change, but skip the initial render.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!filtersReady.current) {
|
|
134
|
+
filtersReady.current = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!loading) {
|
|
138
|
+
onSearch();
|
|
139
|
+
}
|
|
140
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
+
}, [statusFilter, assigneeFilter, dateRange, customStart, customEnd]);
|
|
95
142
|
const renderer = useCallback((item, className) => _jsx(CaseCard, { case: item, className: className }), []);
|
|
96
|
-
return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: loading, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.cases.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/cases/${item.id}`), onCreate: () => navigate('/cases/create'), createPrompt: "route.cases.create", searchPrompt: "route.cases.manager.search", createIcon: _jsx(Topic, { sx: { mr: 1 } }) }));
|
|
143
|
+
return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: loading, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.cases.search.prompt') }), searchFilters: _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, sx: { mt: 0.5, flexWrap: 'wrap' }, children: [_jsx(CaseStatusFilter, { statusFilter: statusFilter, onChange: setStatusFilter }), _jsx(CaseAssigneeFilter, { assigneeFilter: assigneeFilter, onChange: setAssigneeFilter }), _jsx(CaseDateFilter, { dateRange: dateRange, onChange: setDateRange, customStart: customStart, customEnd: customEnd, onCustomStartChange: setCustomStart, onCustomEndChange: setCustomEnd })] }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/cases/${item.id}`), onCreate: () => navigate('/cases/create'), createPrompt: "route.cases.create", searchPrompt: "route.cases.manager.search", createIcon: _jsx(Topic, { sx: { mr: 1 } }) }));
|
|
97
144
|
};
|
|
98
145
|
const Cases = () => {
|
|
99
146
|
return (_jsx(TuiListProvider, { children: _jsx(CasesBase, {}) }));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Person } from '@mui/icons-material';
|
|
3
|
+
import { AvatarGroup, Checkbox, Divider, FormControlLabel, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
5
|
+
import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
|
|
6
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
7
|
+
import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
|
|
8
|
+
import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
|
|
9
|
+
import { useCallback, useContext, useEffect } from 'react';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
const CaseAssigneeFilter = ({ assigneeFilter, onChange }) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const { user: currentUser } = useAppUser();
|
|
14
|
+
const { users, searchUsers } = useContext(UserListContext);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
searchUsers('uname:*');
|
|
17
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
18
|
+
}, []);
|
|
19
|
+
const toggleMyself = useCallback((_, checked) => {
|
|
20
|
+
if (!currentUser) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
onChange(checked
|
|
24
|
+
? [...assigneeFilter.filter(a => a !== currentUser.username), currentUser.username]
|
|
25
|
+
: assigneeFilter.filter(a => a !== currentUser.username));
|
|
26
|
+
}, [currentUser, assigneeFilter, onChange]);
|
|
27
|
+
return (_jsx(ChipPopper, { icon: assigneeFilter.length > 0 ? (_jsx(AvatarGroup, { sx: { '& .MuiAvatar-root': { height: 18, width: 18, fontSize: '0.6rem' } }, children: assigneeFilter.map(u => (_jsx(HowlerAvatar, { userId: u, sx: { height: 18, width: 18 } }, u))) })) : (_jsx(Person, { fontSize: "small" })), label: _jsx(Typography, { variant: "body2", children: assigneeFilter.length === 0
|
|
28
|
+
? t('route.cases.filter.assignee')
|
|
29
|
+
: assigneeFilter.length === 1
|
|
30
|
+
? (users[assigneeFilter[0]]?.name ?? assigneeFilter[0])
|
|
31
|
+
: `${assigneeFilter.length} ${t('route.cases.filter.assignees')}` }), minWidth: "260px", slotProps: { chip: { size: 'small', color: assigneeFilter.length > 0 ? 'primary' : 'default' } }, children: _jsxs(Stack, { direction: "row", divider: _jsx(Divider, { orientation: "vertical", flexItem: true }), spacing: 1, children: [_jsx(UserList, { userIds: assigneeFilter, onChange: onChange, i18nLabel: "route.cases.filter.assignee", multiple: true }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: currentUser ? assigneeFilter.includes(currentUser.username) : false, onChange: toggleMyself }), label: t('route.cases.filter.myself') })] }) }));
|
|
32
|
+
};
|
|
33
|
+
export default CaseAssigneeFilter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } 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 { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
|
|
6
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
7
|
+
import { I18nextProvider } from 'react-i18next';
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import CaseAssigneeFilter from './CaseAssigneeFilter';
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Hoisted mocks
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
vi.mock('commons/components/app/hooks/useAppUser', () => ({
|
|
15
|
+
useAppUser: () => ({ user: { username: 'alice', name: 'Alice Smith' } })
|
|
16
|
+
}));
|
|
17
|
+
// Stub HowlerAvatar to avoid avatar API calls
|
|
18
|
+
vi.mock('components/elements/display/HowlerAvatar', () => ({
|
|
19
|
+
default: ({ userId }) => _jsx("div", { id: `avatar-${userId}`, children: userId })
|
|
20
|
+
}));
|
|
21
|
+
// Stub UserList to a simple multi-select so we can test the onChange wire-up
|
|
22
|
+
// without pulling in the full component and its popover.
|
|
23
|
+
vi.mock('components/elements/UserList', () => ({
|
|
24
|
+
default: ({ userIds, onChange, multiple }) => (_jsxs("div", { id: "user-list", children: [_jsx("button", { id: "user-list-add", onClick: () => onChange([...userIds, 'bob']), children: "Add bob" }), multiple &&
|
|
25
|
+
userIds.map(id => (_jsxs("button", { id: `remove-${id}`, onClick: () => onChange(userIds.filter(u => u !== id)), children: ["Remove ", id] }, id)))] }))
|
|
26
|
+
}));
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const mockSearchUsers = vi.fn();
|
|
31
|
+
const USERS = {
|
|
32
|
+
alice: { username: 'alice', name: 'Alice Smith' },
|
|
33
|
+
bob: { username: 'bob', name: 'Bob Jones' }
|
|
34
|
+
};
|
|
35
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(UserListContext.Provider, { value: { users: USERS, searchUsers: mockSearchUsers, fetchUsers: vi.fn() }, children: children }) }));
|
|
36
|
+
const renderFilter = (assigneeFilter, onChange = vi.fn()) => render(_jsx(CaseAssigneeFilter, { assigneeFilter: assigneeFilter, onChange: onChange }), { wrapper: Wrapper });
|
|
37
|
+
const openPopper = async (user, labelText) => {
|
|
38
|
+
const chip = screen.getByText(labelText).closest('.MuiChip-root');
|
|
39
|
+
await user.click(chip);
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByTestId('user-list')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Tests
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
describe('CaseAssigneeFilter', () => {
|
|
48
|
+
let user;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
user = userEvent.setup();
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
describe('searchUsers on mount', () => {
|
|
54
|
+
it('calls searchUsers with "uname:*" on mount', () => {
|
|
55
|
+
renderFilter([]);
|
|
56
|
+
expect(mockSearchUsers).toHaveBeenCalledWith('uname:*');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('label', () => {
|
|
60
|
+
it('shows the default "Assignee" label when no assignees are selected', () => {
|
|
61
|
+
renderFilter([]);
|
|
62
|
+
expect(screen.getByText(i18n.t('route.cases.filter.assignee'))).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
it('shows the user display name when one assignee is selected', () => {
|
|
65
|
+
renderFilter(['alice']);
|
|
66
|
+
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
it('shows count + "Assignees" when multiple assignees are selected', () => {
|
|
69
|
+
renderFilter(['alice', 'bob']);
|
|
70
|
+
expect(screen.getByText(`2 ${i18n.t('route.cases.filter.assignees')}`)).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
it('falls back to username when user is not in the user map', () => {
|
|
73
|
+
renderFilter(['unknown-user']);
|
|
74
|
+
const chipLabel = document.querySelector('.MuiChip-label');
|
|
75
|
+
expect(chipLabel?.textContent).toContain('unknown-user');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('chip color', () => {
|
|
79
|
+
it('uses default color when no assignees are selected', () => {
|
|
80
|
+
renderFilter([]);
|
|
81
|
+
const chip = screen.getByText(i18n.t('route.cases.filter.assignee')).closest('.MuiChip-root');
|
|
82
|
+
expect(chip).not.toHaveClass('MuiChip-colorPrimary');
|
|
83
|
+
});
|
|
84
|
+
it('uses primary color when assignees are selected', () => {
|
|
85
|
+
renderFilter(['alice']);
|
|
86
|
+
const chip = screen.getByText('Alice Smith').closest('.MuiChip-root');
|
|
87
|
+
expect(chip).toHaveClass('MuiChip-colorPrimary');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('"Myself" checkbox', () => {
|
|
91
|
+
it('is unchecked when the current user is not in the filter', async () => {
|
|
92
|
+
renderFilter([]);
|
|
93
|
+
await openPopper(user, i18n.t('route.cases.filter.assignee'));
|
|
94
|
+
const checkbox = screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') });
|
|
95
|
+
expect(checkbox).not.toBeChecked();
|
|
96
|
+
});
|
|
97
|
+
it('is checked when the current user is already in the filter', async () => {
|
|
98
|
+
renderFilter(['alice']);
|
|
99
|
+
await openPopper(user, 'Alice Smith');
|
|
100
|
+
const checkbox = screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') });
|
|
101
|
+
expect(checkbox).toBeChecked();
|
|
102
|
+
});
|
|
103
|
+
it('adds the current user to the filter when the checkbox is checked', async () => {
|
|
104
|
+
const onChange = vi.fn();
|
|
105
|
+
renderFilter([], onChange);
|
|
106
|
+
await openPopper(user, i18n.t('route.cases.filter.assignee'));
|
|
107
|
+
await user.click(screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') }));
|
|
108
|
+
expect(onChange).toHaveBeenCalledWith(['alice']);
|
|
109
|
+
});
|
|
110
|
+
it('removes the current user from the filter when the checkbox is unchecked', async () => {
|
|
111
|
+
const onChange = vi.fn();
|
|
112
|
+
renderFilter(['alice', 'bob'], onChange);
|
|
113
|
+
await openPopper(user, `2 ${i18n.t('route.cases.filter.assignees')}`);
|
|
114
|
+
await user.click(screen.getByRole('checkbox', { name: i18n.t('route.cases.filter.myself') }));
|
|
115
|
+
expect(onChange).toHaveBeenCalledWith(['bob']);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('UserList onChange passthrough', () => {
|
|
119
|
+
it('passes onChange directly to UserList', async () => {
|
|
120
|
+
const onChange = vi.fn();
|
|
121
|
+
renderFilter(['alice'], onChange);
|
|
122
|
+
await openPopper(user, 'Alice Smith');
|
|
123
|
+
await user.click(screen.getByTestId('user-list-add'));
|
|
124
|
+
expect(onChange).toHaveBeenCalledWith(['alice', 'bob']);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Dayjs } from 'dayjs';
|
|
2
|
+
import type { FC } from 'react';
|
|
3
|
+
import { DATE_RANGES } from '@cccsaurora/howler-ui/utils/constants';
|
|
4
|
+
export type DateRangeOption = (typeof DATE_RANGES)[number];
|
|
5
|
+
declare const CaseDateFilter: FC<{
|
|
6
|
+
dateRange: DateRangeOption;
|
|
7
|
+
onChange: (v: DateRangeOption) => void;
|
|
8
|
+
customStart: Dayjs;
|
|
9
|
+
customEnd: Dayjs;
|
|
10
|
+
onCustomStartChange: (v: Dayjs) => void;
|
|
11
|
+
onCustomEndChange: (v: Dayjs) => void;
|
|
12
|
+
}>;
|
|
13
|
+
export default CaseDateFilter;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AvTimer } from '@mui/icons-material';
|
|
3
|
+
import { Autocomplete, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
+
import { LocalizationProvider } from '@mui/x-date-pickers';
|
|
5
|
+
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
6
|
+
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
|
7
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { DATE_RANGES } from '@cccsaurora/howler-ui/utils/constants';
|
|
10
|
+
const CaseDateFilter = ({ dateRange, onChange, customStart, customEnd, onCustomStartChange, onCustomEndChange }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
return (_jsx(ChipPopper, { icon: _jsx(AvTimer, { fontSize: "small" }), label: _jsx(Typography, { variant: "body2", children: dateRange === 'date.range.all'
|
|
13
|
+
? t('route.cases.filter.date')
|
|
14
|
+
: dateRange === 'date.range.custom'
|
|
15
|
+
? `${customStart.format('YYYY-MM-DD')} ${t('to')} ${customEnd.format('YYYY-MM-DD')}`
|
|
16
|
+
: t(dateRange) }), minWidth: "225px", slotProps: { chip: { size: 'small', color: dateRange !== 'date.range.all' ? 'primary' : 'default' } }, children: _jsxs(Stack, { spacing: 1, children: [_jsx(Autocomplete, { size: "small", value: dateRange, options: [...DATE_RANGES], getOptionLabel: o => t(o), disableClearable: true, onChange: (_, nv) => onChange(nv), renderInput: params => _jsx(TextField, { ...params, label: t('route.cases.filter.date') }) }), dateRange === 'date.range.custom' && (_jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: _jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, flexWrap: "wrap", children: [_jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.start'), value: customStart, maxDate: customEnd, onChange: nv => {
|
|
17
|
+
if (nv) {
|
|
18
|
+
onCustomStartChange(nv);
|
|
19
|
+
}
|
|
20
|
+
}, ampm: false, disableFuture: true }), _jsx(DateTimePicker, { sx: { minWidth: '175px', flexGrow: 1 }, slotProps: { textField: { size: 'small' } }, label: t('date.select.end'), value: customEnd, minDate: customStart, onChange: nv => {
|
|
21
|
+
if (nv) {
|
|
22
|
+
onCustomEndChange(nv);
|
|
23
|
+
}
|
|
24
|
+
}, ampm: false, disableFuture: true })] }) }))] }) }));
|
|
25
|
+
};
|
|
26
|
+
export default CaseDateFilter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
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 dayjs from 'dayjs';
|
|
6
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
7
|
+
import { I18nextProvider } from 'react-i18next';
|
|
8
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import CaseDateFilter from './CaseDateFilter';
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Stub the date pickers so jsdom doesn't choke on them
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
vi.mock('@mui/x-date-pickers/DateTimePicker', () => ({
|
|
15
|
+
DateTimePicker: ({ label, onChange }) => (_jsx("button", { id: `picker-${label}`, onClick: () => onChange(dayjs('2025-06-01')), children: label }))
|
|
16
|
+
}));
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Wrapper / render helper
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const FIXED_START = dayjs('2025-01-01');
|
|
21
|
+
const FIXED_END = dayjs('2025-03-01');
|
|
22
|
+
const Wrapper = ({ children }) => _jsx(I18nextProvider, { i18n: i18n, children: children });
|
|
23
|
+
const renderFilter = ({ dateRange = 'date.range.all', onChange = vi.fn(), onCustomStartChange = vi.fn(), onCustomEndChange = vi.fn() } = {}) => render(_jsx(CaseDateFilter, { dateRange: dateRange, onChange: onChange, customStart: FIXED_START, customEnd: FIXED_END, onCustomStartChange: onCustomStartChange, onCustomEndChange: onCustomEndChange }), { wrapper: Wrapper });
|
|
24
|
+
const openPopper = async (user) => {
|
|
25
|
+
// The chip label varies; find the chip by the AvTimer icon's sibling
|
|
26
|
+
const chip = document.querySelector('.MuiChip-root');
|
|
27
|
+
await user.click(chip);
|
|
28
|
+
await waitFor(() => {
|
|
29
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Tests
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
describe('CaseDateFilter', () => {
|
|
36
|
+
let user;
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
user = userEvent.setup();
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
describe('label', () => {
|
|
42
|
+
it('shows "Date Range" label when date.range.all is selected', () => {
|
|
43
|
+
renderFilter({ dateRange: 'date.range.all' });
|
|
44
|
+
expect(screen.getByText(i18n.t('route.cases.filter.date'))).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
it('shows the translated range label for a non-all preset', () => {
|
|
47
|
+
renderFilter({ dateRange: 'date.range.1.week' });
|
|
48
|
+
expect(screen.getByText(i18n.t('date.range.1.week'))).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
it('shows formatted start/end dates for the custom range', () => {
|
|
51
|
+
renderFilter({ dateRange: 'date.range.custom' });
|
|
52
|
+
const expected = `${FIXED_START.format('YYYY-MM-DD')} ${i18n.t('to')} ${FIXED_END.format('YYYY-MM-DD')}`;
|
|
53
|
+
expect(screen.getByText(expected)).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('chip color', () => {
|
|
57
|
+
it('uses default color for date.range.all', () => {
|
|
58
|
+
renderFilter({ dateRange: 'date.range.all' });
|
|
59
|
+
const chip = document.querySelector('.MuiChip-root');
|
|
60
|
+
expect(chip).not.toHaveClass('MuiChip-colorPrimary');
|
|
61
|
+
});
|
|
62
|
+
it('uses primary color for any non-all range', () => {
|
|
63
|
+
renderFilter({ dateRange: 'date.range.1.day' });
|
|
64
|
+
const chip = document.querySelector('.MuiChip-root');
|
|
65
|
+
expect(chip).toHaveClass('MuiChip-colorPrimary');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('popper content', () => {
|
|
69
|
+
it('does not show the autocomplete before the chip is clicked', () => {
|
|
70
|
+
renderFilter();
|
|
71
|
+
expect(screen.queryByRole('combobox')).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
it('shows the range autocomplete after the chip is clicked', async () => {
|
|
74
|
+
renderFilter();
|
|
75
|
+
await openPopper(user);
|
|
76
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
it('does not show date pickers when a preset (non-custom) range is active', async () => {
|
|
79
|
+
renderFilter({ dateRange: 'date.range.1.day' });
|
|
80
|
+
await openPopper(user);
|
|
81
|
+
expect(screen.queryByTestId(`picker-${i18n.t('date.select.start')}`)).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
it('shows start and end date pickers when custom range is active', async () => {
|
|
84
|
+
renderFilter({ dateRange: 'date.range.custom' });
|
|
85
|
+
await openPopper(user);
|
|
86
|
+
expect(screen.getByTestId(`picker-${i18n.t('date.select.start')}`)).toBeInTheDocument();
|
|
87
|
+
expect(screen.getByTestId(`picker-${i18n.t('date.select.end')}`)).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('interactions', () => {
|
|
91
|
+
it('calls onChange when a range option is selected from the autocomplete', async () => {
|
|
92
|
+
const onChange = vi.fn();
|
|
93
|
+
renderFilter({ dateRange: 'date.range.all', onChange });
|
|
94
|
+
await openPopper(user);
|
|
95
|
+
await user.click(screen.getByRole('combobox'));
|
|
96
|
+
const option = await screen.findByRole('option', { name: i18n.t('date.range.1.week') });
|
|
97
|
+
await user.click(option);
|
|
98
|
+
expect(onChange).toHaveBeenCalledWith('date.range.1.week');
|
|
99
|
+
});
|
|
100
|
+
it('calls onCustomStartChange when the start date picker fires', async () => {
|
|
101
|
+
const onCustomStartChange = vi.fn();
|
|
102
|
+
renderFilter({ dateRange: 'date.range.custom', onCustomStartChange });
|
|
103
|
+
await openPopper(user);
|
|
104
|
+
await user.click(screen.getByTestId(`picker-${i18n.t('date.select.start')}`));
|
|
105
|
+
expect(onCustomStartChange).toHaveBeenCalledWith(dayjs('2025-06-01'));
|
|
106
|
+
});
|
|
107
|
+
it('calls onCustomEndChange when the end date picker fires', async () => {
|
|
108
|
+
const onCustomEndChange = vi.fn();
|
|
109
|
+
renderFilter({ dateRange: 'date.range.custom', onCustomEndChange });
|
|
110
|
+
await openPopper(user);
|
|
111
|
+
await user.click(screen.getByTestId(`picker-${i18n.t('date.select.end')}`));
|
|
112
|
+
expect(onCustomEndChange).toHaveBeenCalledWith(dayjs('2025-06-01'));
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FilterList } from '@mui/icons-material';
|
|
3
|
+
import { ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
|
|
4
|
+
import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { CASE_STATUSES } from '../constants';
|
|
7
|
+
const CaseStatusFilter = ({ statusFilter, onChange }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
return (_jsx(ChipPopper, { icon: _jsx(FilterList, { fontSize: "small" }), label: _jsx(Typography, { variant: "body2", children: statusFilter.length === 0
|
|
10
|
+
? t('route.cases.filter.status')
|
|
11
|
+
: statusFilter.map(s => t(`page.cases.status.${s}`)).join(', ') }), minWidth: "200px", slotProps: { chip: { size: 'small', color: statusFilter.length > 0 ? 'primary' : 'default' } }, children: _jsx(ToggleButtonGroup, { value: statusFilter, onChange: (_, nv) => onChange(nv), size: "small", orientation: "vertical", sx: { width: '100%' }, children: CASE_STATUSES.map(s => (_jsx(ToggleButton, { value: s, sx: { justifyContent: 'flex-start' }, children: t(`page.cases.status.${s}`) }, s))) }) }));
|
|
12
|
+
};
|
|
13
|
+
export default CaseStatusFilter;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
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 i18n from '@cccsaurora/howler-ui/i18n';
|
|
6
|
+
import { I18nextProvider } from 'react-i18next';
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import CaseStatusFilter from './CaseStatusFilter';
|
|
9
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
const Wrapper = ({ children }) => _jsx(I18nextProvider, { i18n: i18n, children: children });
|
|
11
|
+
const renderFilter = (statusFilter, onChange = vi.fn()) => render(_jsx(CaseStatusFilter, { statusFilter: statusFilter, onChange: onChange }), { wrapper: Wrapper });
|
|
12
|
+
const openPopper = async (user) => {
|
|
13
|
+
const chip = screen.getByText(i18n.t('route.cases.filter.status')).closest('.MuiChip-root');
|
|
14
|
+
await user.click(chip);
|
|
15
|
+
await waitFor(() => {
|
|
16
|
+
expect(screen.getByRole('group')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
describe('CaseStatusFilter', () => {
|
|
20
|
+
let user;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
user = userEvent.setup();
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
describe('label', () => {
|
|
26
|
+
it('shows the default "Status" label when no statuses are selected', () => {
|
|
27
|
+
renderFilter([]);
|
|
28
|
+
expect(screen.getByText(i18n.t('route.cases.filter.status'))).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
it('shows a comma-separated list of status labels when statuses are selected', () => {
|
|
31
|
+
renderFilter(['open', 'on-hold']);
|
|
32
|
+
const expected = [i18n.t('page.cases.status.open'), i18n.t('page.cases.status.on-hold')].join(', ');
|
|
33
|
+
expect(screen.getByText(expected)).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
it('shows a single status label when one status is selected', () => {
|
|
36
|
+
renderFilter(['resolved']);
|
|
37
|
+
expect(screen.getByText(i18n.t('page.cases.status.resolved'))).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('chip color', () => {
|
|
41
|
+
it('uses default color when no statuses are selected', () => {
|
|
42
|
+
renderFilter([]);
|
|
43
|
+
const chip = screen.getByText(i18n.t('route.cases.filter.status')).closest('.MuiChip-root');
|
|
44
|
+
expect(chip).not.toHaveClass('MuiChip-colorPrimary');
|
|
45
|
+
});
|
|
46
|
+
it('uses primary color when statuses are selected', () => {
|
|
47
|
+
renderFilter(['open']);
|
|
48
|
+
const chip = screen.getByText(i18n.t('page.cases.status.open')).closest('.MuiChip-root');
|
|
49
|
+
expect(chip).toHaveClass('MuiChip-colorPrimary');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('popper content', () => {
|
|
53
|
+
it('does not show toggle buttons before the chip is clicked', () => {
|
|
54
|
+
renderFilter([]);
|
|
55
|
+
expect(screen.queryByRole('group')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
it('shows all four status toggle buttons when the chip is clicked', async () => {
|
|
58
|
+
renderFilter([]);
|
|
59
|
+
await openPopper(user);
|
|
60
|
+
expect(screen.getByText(i18n.t('page.cases.status.open'))).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByText(i18n.t('page.cases.status.in-progress'))).toBeInTheDocument();
|
|
62
|
+
expect(screen.getByText(i18n.t('page.cases.status.on-hold'))).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText(i18n.t('page.cases.status.resolved'))).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('interactions', () => {
|
|
67
|
+
it('calls onChange when a toggle button is clicked', async () => {
|
|
68
|
+
const onChange = vi.fn();
|
|
69
|
+
renderFilter([], onChange);
|
|
70
|
+
await openPopper(user);
|
|
71
|
+
await user.click(screen.getByText(i18n.t('page.cases.status.open')));
|
|
72
|
+
expect(onChange).toHaveBeenCalledWith(['open']);
|
|
73
|
+
});
|
|
74
|
+
it('calls onChange with empty array when the only selected status is deselected', async () => {
|
|
75
|
+
const onChange = vi.fn();
|
|
76
|
+
renderFilter(['open'], onChange);
|
|
77
|
+
const chip = screen.getByText(i18n.t('page.cases.status.open')).closest('.MuiChip-root');
|
|
78
|
+
await user.click(chip);
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(screen.getByRole('group')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
await user.click(screen.getAllByText(i18n.t('page.cases.status.open'))[1]);
|
|
83
|
+
expect(onChange).toHaveBeenCalledWith([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -408,6 +408,10 @@
|
|
|
408
408
|
"page.cases.sidebar.item.remove": "Remove item",
|
|
409
409
|
"page.cases.sidebar.item.rename": "Rename item",
|
|
410
410
|
"page.cases.sources": "Sources",
|
|
411
|
+
"page.cases.status.in-progress": "In Progress",
|
|
412
|
+
"page.cases.status.on-hold": "On Hold",
|
|
413
|
+
"page.cases.status.open": "Open",
|
|
414
|
+
"page.cases.status.resolved": "Resolved",
|
|
411
415
|
"page.cases.timeline": "Timeline",
|
|
412
416
|
"page.cases.timeline.empty": "No events match the selected filters.",
|
|
413
417
|
"page.cases.timeline.filter.escalation": "Escalation",
|
|
@@ -620,8 +624,13 @@
|
|
|
620
624
|
"route.analytics.view": "View Analytic",
|
|
621
625
|
"route.cases": "Cases",
|
|
622
626
|
"route.cases.create": "Create Case",
|
|
627
|
+
"route.cases.filter.assignee": "Assignee",
|
|
628
|
+
"route.cases.filter.assignees": "Assignees",
|
|
629
|
+
"route.cases.filter.date": "Date Range",
|
|
630
|
+
"route.cases.filter.myself": "Myself",
|
|
631
|
+
"route.cases.filter.status": "Status",
|
|
623
632
|
"route.cases.manager.search": "Search Cases",
|
|
624
|
-
"route.cases.search.prompt": "Search
|
|
633
|
+
"route.cases.search.prompt": "Search cases by title, summary, description, participants or task details",
|
|
625
634
|
"route.cases.view": "View Case",
|
|
626
635
|
"route.clear": "Clear query",
|
|
627
636
|
"route.dossiers": "Dossiers",
|
|
@@ -408,6 +408,10 @@
|
|
|
408
408
|
"page.cases.sidebar.item.remove": "Supprimer l'élément",
|
|
409
409
|
"page.cases.sidebar.item.rename": "Renommer l'élément",
|
|
410
410
|
"page.cases.sources": "Sources",
|
|
411
|
+
"page.cases.status.in-progress": "En cours",
|
|
412
|
+
"page.cases.status.on-hold": "En attente",
|
|
413
|
+
"page.cases.status.open": "Ouvert",
|
|
414
|
+
"page.cases.status.resolved": "Résolu",
|
|
411
415
|
"page.cases.timeline": "Chronologie",
|
|
412
416
|
"page.cases.timeline.empty": "Aucun événement ne correspond aux filtres sélectionnés.",
|
|
413
417
|
"page.cases.timeline.filter.escalation": "Escalade",
|
|
@@ -620,8 +624,13 @@
|
|
|
620
624
|
"route.analytics.view": "Voir l'analyse",
|
|
621
625
|
"route.cases": "Cas",
|
|
622
626
|
"route.cases.create": "Créer un cas",
|
|
627
|
+
"route.cases.filter.assignee": "Assigné",
|
|
628
|
+
"route.cases.filter.assignees": "Assignés",
|
|
629
|
+
"route.cases.filter.date": "Plage de dates",
|
|
630
|
+
"route.cases.filter.myself": "Moi-même",
|
|
631
|
+
"route.cases.filter.status": "Statut",
|
|
623
632
|
"route.cases.manager.search": "Rechercher des cas",
|
|
624
|
-
"route.cases.search.prompt": "Rechercher des cas par titre,
|
|
633
|
+
"route.cases.search.prompt": "Rechercher des cas par titre, résumé, description, participants ou détails des tâches",
|
|
625
634
|
"route.cases.view": "Voir le cas",
|
|
626
635
|
"route.clear": "Effacer la requête",
|
|
627
636
|
"route.dossiers": "Dossiers",
|
package/package.json
CHANGED
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"internal-slot": "1.0.7"
|
|
102
102
|
},
|
|
103
103
|
"type": "module",
|
|
104
|
-
"version": "2.18.0-dev.
|
|
104
|
+
"version": "2.18.0-dev.766",
|
|
105
105
|
"exports": {
|
|
106
106
|
"./i18n": "./i18n.js",
|
|
107
107
|
"./index.css": "./index.css",
|
|
@@ -172,6 +172,7 @@
|
|
|
172
172
|
"./components/routes/action/view/markdown/*.md": "./components/routes/action/view/markdown/*.md.js",
|
|
173
173
|
"./components/routes/cases/modals/*": "./components/routes/cases/modals/*.js",
|
|
174
174
|
"./components/routes/cases/detail/*": "./components/routes/cases/detail/*.js",
|
|
175
|
+
"./components/routes/cases/search/*": "./components/routes/cases/search/*.js",
|
|
175
176
|
"./components/routes/cases/hooks/*": "./components/routes/cases/hooks/*.js",
|
|
176
177
|
"./components/routes/cases/detail/assets/*": "./components/routes/cases/detail/assets/*.js",
|
|
177
178
|
"./components/routes/cases/detail/sidebar/*": "./components/routes/cases/detail/sidebar/*.js",
|
package/utils/constants.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export declare const RULE_INTERVALS: {
|
|
|
74
74
|
crontab: string;
|
|
75
75
|
}[];
|
|
76
76
|
export declare const DATE_RANGES: string[];
|
|
77
|
+
export declare const DATE_RANGE_LUCENE: Partial<Record<(typeof DATE_RANGES)[number], string>>;
|
|
77
78
|
interface LabelData {
|
|
78
79
|
icon?: ReactElement;
|
|
79
80
|
color?: string;
|
package/utils/constants.js
CHANGED
|
@@ -104,6 +104,12 @@ export const DATE_RANGES = [
|
|
|
104
104
|
'date.range.all',
|
|
105
105
|
'date.range.custom'
|
|
106
106
|
];
|
|
107
|
+
export const DATE_RANGE_LUCENE = {
|
|
108
|
+
'date.range.1.day': 'now-1d/d',
|
|
109
|
+
'date.range.3.day': 'now-3d/d',
|
|
110
|
+
'date.range.1.week': 'now-7d/d',
|
|
111
|
+
'date.range.1.month': 'now-1M/M'
|
|
112
|
+
};
|
|
107
113
|
export const LABEL_TYPES = {
|
|
108
114
|
insight: { icon: _jsx(PsychologyAlt, { fontSize: "small" }), color: '#FFFFFF' }, //brain icon
|
|
109
115
|
mitigation: { icon: _jsx(LocalPolice, { fontSize: "small" }), color: blue[600] }, //police badge
|