@cccsaurora/howler-ui 2.18.0-dev.841 → 2.19.0-cases.1040
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/action/index.d.ts +5 -4
- package/api/action/index.js +8 -8
- package/api/analytic/index.d.ts +3 -2
- package/api/analytic/index.js +4 -4
- package/api/analytic/owner.d.ts +2 -1
- package/api/analytic/owner.js +2 -2
- package/api/analytic/rules.d.ts +2 -1
- package/api/analytic/rules.js +2 -2
- package/api/dossier/index.d.ts +4 -3
- package/api/dossier/index.js +6 -6
- package/api/hit/index.d.ts +3 -2
- package/api/hit/index.js +4 -4
- package/api/hit/labels.d.ts +3 -2
- package/api/hit/labels.js +4 -4
- package/api/hit/overwrite.d.ts +2 -1
- package/api/hit/overwrite.js +2 -2
- package/api/hit/transition.d.ts +2 -1
- package/api/hit/transition.js +2 -2
- package/api/index.d.ts +5 -4
- package/api/index.js +8 -8
- package/api/overview/index.d.ts +4 -3
- package/api/overview/index.js +6 -6
- package/api/search/overview.d.ts +2 -2
- package/api/template/index.d.ts +4 -3
- package/api/template/index.js +6 -6
- package/api/user/index.d.ts +2 -1
- package/api/user/index.js +2 -2
- package/api/view/index.d.ts +4 -3
- package/api/view/index.js +6 -6
- package/components/app/App.d.ts +1 -0
- package/components/app/App.js +24 -6
- package/components/app/providers/FavouritesProvider.js +0 -4
- package/components/app/providers/ModalProvider.d.ts +1 -0
- package/components/app/providers/ModalProvider.js +5 -1
- package/components/app/providers/RecordProvider.d.ts +1 -1
- package/components/app/providers/RecordProvider.js +1 -1
- package/components/app/providers/SearchResponseProvider.d.ts +21 -0
- package/components/app/providers/SearchResponseProvider.js +81 -0
- package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
- package/components/app/providers/SearchResponseProvider.test.js +406 -0
- package/components/app/providers/ViewProvider.test.js +4 -1
- package/components/elements/addons/buttons/CustomButton.test.d.ts +1 -0
- package/components/elements/addons/buttons/CustomButton.test.js +106 -0
- package/components/elements/addons/layout/FlexOne.test.d.ts +1 -0
- package/components/elements/addons/layout/FlexOne.test.js +30 -0
- package/components/elements/addons/lists/TuiListBase.js +4 -0
- package/components/elements/addons/lists/TuiListElement.js +6 -3
- package/components/elements/addons/lists/index.d.ts +1 -0
- package/components/elements/addons/search/SearchPagination.d.ts +2 -1
- package/components/elements/addons/search/SearchPagination.js +4 -4
- package/components/elements/addons/search/SearchPagination.test.d.ts +1 -0
- package/components/elements/addons/search/SearchPagination.test.js +80 -0
- package/components/elements/addons/search/SearchTotal.test.d.ts +1 -0
- package/components/elements/addons/search/SearchTotal.test.js +47 -0
- package/components/elements/display/DocumentationButton.test.d.ts +1 -0
- package/components/elements/display/DocumentationButton.test.js +84 -0
- package/components/elements/display/DynamicTabs.test.d.ts +1 -0
- package/components/elements/display/DynamicTabs.test.js +93 -0
- package/components/elements/display/HowlerAvatar.js +2 -1
- package/components/elements/display/HowlerCard.test.d.ts +1 -0
- package/components/elements/display/HowlerCard.test.js +51 -0
- package/components/elements/display/Image.test.d.ts +1 -0
- package/components/elements/display/Image.test.js +79 -0
- package/components/elements/display/ItemManager.d.ts +2 -2
- package/components/elements/display/ItemManager.js +1 -1
- package/components/elements/display/Markdown.js +9 -2
- package/components/elements/display/QueryResultText.test.d.ts +1 -0
- package/components/elements/display/QueryResultText.test.js +48 -0
- package/components/elements/display/TextDivider.test.d.ts +1 -0
- package/components/elements/display/TextDivider.test.js +43 -0
- package/components/elements/display/TypingIndicator.test.d.ts +1 -0
- package/components/elements/display/TypingIndicator.test.js +33 -0
- package/components/elements/display/modals/ConfirmDeleteModal.d.ts +4 -0
- package/components/elements/display/modals/ConfirmDeleteModal.js +4 -2
- package/components/elements/hit/HitBanner.js +6 -3
- package/components/elements/hit/HitLinks.js +1 -1
- package/components/elements/hit/HitSummary.js +3 -2
- package/components/elements/hit/elements/AnalyticLink.js +1 -1
- package/components/elements/hit/elements/Assigned.d.ts +2 -0
- package/components/elements/hit/elements/Assigned.js +15 -13
- package/components/elements/record/RecordContextMenu.js +15 -3
- package/components/elements/record/RecordContextMenu.test.js +96 -0
- package/components/hooks/useMyApi.d.ts +1 -1
- package/components/hooks/useMyPreferences.js +2 -1
- package/components/routes/403.d.ts +3 -0
- package/components/routes/403.js +10 -0
- package/components/routes/403.test.d.ts +1 -0
- package/components/routes/403.test.js +27 -0
- package/components/routes/404.test.d.ts +1 -0
- package/components/routes/404.test.js +26 -0
- package/components/routes/ErrorBoundary.test.d.ts +1 -0
- package/components/routes/ErrorBoundary.test.js +43 -0
- package/components/routes/ErrorOccured.test.d.ts +1 -0
- package/components/routes/ErrorOccured.test.js +35 -0
- package/components/routes/Logout.test.d.ts +1 -0
- package/components/routes/Logout.test.js +59 -0
- package/components/routes/action/edit/ActionEditor.js +1 -1
- package/components/routes/action/useMyActionFunctions.js +30 -6
- package/components/routes/action/view/ActionDetails.js +15 -2
- package/components/routes/action/view/ActionSearch.js +32 -20
- package/components/routes/action/view/markdown/integrations.en.md.js +1 -1
- package/components/routes/action/view/markdown/integrations.fr.md.js +1 -1
- package/components/routes/analytics/AnalyticDetails.js +10 -6
- package/components/routes/analytics/AnalyticSearch.js +6 -6
- package/components/routes/cases/Cases.js +5 -6
- package/components/routes/cases/modals/ResolveModal.js +14 -13
- package/components/routes/cases/modals/ResolveModal.test.js +85 -25
- package/components/routes/dossiers/Dossiers.js +25 -19
- package/components/routes/help/ActionIntroductionDocumentation.js +1 -1
- package/components/routes/help/markdown/en/client.md.js +1 -1
- package/components/routes/help/markdown/en/retention.md.js +1 -1
- package/components/routes/help/markdown/fr/client.md.js +1 -1
- package/components/routes/help/markdown/fr/retention.md.js +1 -1
- package/components/routes/hits/search/shared/CustomSpan.js +7 -7
- package/components/routes/hits/search/shared/CustomSpan.test.d.ts +1 -0
- package/components/routes/hits/search/shared/CustomSpan.test.js +170 -0
- package/components/routes/home/AddNewCard.js +1 -1
- package/components/routes/home/AddNewCard.test.d.ts +1 -0
- package/components/routes/home/AddNewCard.test.js +86 -0
- package/components/routes/home/ViewCard.js +1 -1
- package/components/routes/home/ViewRefresh.test.d.ts +1 -0
- package/components/routes/home/ViewRefresh.test.js +79 -0
- package/components/routes/overviews/OverviewCard.test.d.ts +1 -0
- package/components/routes/overviews/OverviewCard.test.js +108 -0
- package/components/routes/overviews/OverviewViewer.js +16 -8
- package/components/routes/overviews/Overviews.js +25 -19
- package/components/routes/overviews/template/en.md.js +1 -1
- package/components/routes/overviews/template/fr.md.js +1 -1
- package/components/routes/settings/LocalSection.js +2 -1
- package/components/routes/settings/SettingsSection.test.d.ts +1 -0
- package/components/routes/settings/SettingsSection.test.js +19 -0
- package/components/routes/templates/TemplateCard.d.ts +3 -1
- package/components/routes/templates/TemplateCard.js +8 -4
- package/components/routes/templates/TemplateCard.test.d.ts +1 -0
- package/components/routes/templates/TemplateCard.test.js +115 -0
- package/components/routes/templates/TemplateViewer.js +62 -16
- package/components/routes/templates/Templates.js +35 -13
- package/components/routes/views/Views.js +21 -14
- package/locales/en/translation.json +25 -4
- package/locales/fr/translation.json +25 -4
- package/models/entities/generated/ApiType.d.ts +1 -1
- package/models/entities/generated/Hit.d.ts +1 -0
- package/models/entities/generated/Howler.d.ts +1 -0
- package/models/entities/generated/UserUser.d.ts +2 -0
- package/package.json +143 -148
- package/rest/FetchClient.test.d.ts +1 -0
- package/rest/FetchClient.test.js +81 -0
- package/setupTests.js +5 -0
- package/utils/Throttler.test.d.ts +1 -0
- package/utils/Throttler.test.js +135 -0
- package/utils/actionUtils.test.d.ts +1 -0
- package/utils/actionUtils.test.js +140 -0
- package/utils/constants.d.ts +1 -0
- package/utils/constants.js +1 -0
- package/utils/hitFunctions.test.d.ts +1 -0
- package/utils/hitFunctions.test.js +52 -0
- package/utils/localStorage.test.d.ts +1 -0
- package/utils/localStorage.test.js +96 -0
- package/utils/menuUtils.js +1 -1
- package/utils/menuUtils.test.d.ts +1 -0
- package/utils/menuUtils.test.js +198 -0
- package/utils/sessionStorage.test.d.ts +1 -0
- package/utils/sessionStorage.test.js +81 -0
- package/utils/stringUtils.test.d.ts +1 -0
- package/utils/stringUtils.test.js +159 -0
- package/utils/utils.test.d.ts +1 -0
- package/utils/utils.test.js +292 -0
- package/utils/viewUtils.test.d.ts +1 -0
- package/utils/viewUtils.test.js +45 -0
- package/utils/xsrf.test.d.ts +1 -0
- package/utils/xsrf.test.js +41 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import TextDivider from './TextDivider';
|
|
7
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
8
|
+
describe('TextDivider', () => {
|
|
9
|
+
describe('rendering', () => {
|
|
10
|
+
it('renders without crashing', () => {
|
|
11
|
+
const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
|
|
12
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
it('renders with inline-block display style', () => {
|
|
15
|
+
const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
|
|
16
|
+
expect(container.firstChild).toHaveStyle({ display: 'inline-block' });
|
|
17
|
+
});
|
|
18
|
+
it('renders with text-align center', () => {
|
|
19
|
+
const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
|
|
20
|
+
expect(container.firstChild).toHaveStyle({ textAlign: 'center' });
|
|
21
|
+
});
|
|
22
|
+
it('renders with width 100%', () => {
|
|
23
|
+
const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
|
|
24
|
+
expect(container.firstChild).toHaveStyle({ width: '100%' });
|
|
25
|
+
});
|
|
26
|
+
it('renders with position relative', () => {
|
|
27
|
+
const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
|
|
28
|
+
expect(container.firstChild).toHaveStyle({ position: 'relative' });
|
|
29
|
+
});
|
|
30
|
+
it('renders a nested div element', () => {
|
|
31
|
+
const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
|
|
32
|
+
const outerDiv = container.firstChild;
|
|
33
|
+
expect(outerDiv.querySelector('div')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
it('renders the translated divider text', () => {
|
|
36
|
+
const { container } = render(_jsx(TextDivider, {}), { wrapper: Wrapper });
|
|
37
|
+
// The divider uses i18n key 'divider', text should be present
|
|
38
|
+
const innerDiv = container.querySelector('div > div');
|
|
39
|
+
expect(innerDiv).toBeInTheDocument();
|
|
40
|
+
expect(innerDiv.textContent).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import TypingIndicator from './TypingIndicator';
|
|
5
|
+
describe('TypingIndicator', () => {
|
|
6
|
+
describe('rendering', () => {
|
|
7
|
+
it('renders exactly three circle icons', () => {
|
|
8
|
+
const { container } = render(_jsx(TypingIndicator, {}));
|
|
9
|
+
// Each Circle icon renders as an <svg> element
|
|
10
|
+
const svgs = container.querySelectorAll('svg');
|
|
11
|
+
expect(svgs).toHaveLength(3);
|
|
12
|
+
});
|
|
13
|
+
it('renders a root Stack element', () => {
|
|
14
|
+
const { container } = render(_jsx(TypingIndicator, {}));
|
|
15
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
it('renders with flex row layout', () => {
|
|
18
|
+
const { container } = render(_jsx(TypingIndicator, {}));
|
|
19
|
+
// MUI Stack with direction="row" uses flexbox
|
|
20
|
+
const root = container.firstChild;
|
|
21
|
+
expect(root).toHaveStyle({ display: 'flex', flexDirection: 'row' });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('accessibility', () => {
|
|
25
|
+
it('renders the circles as SVG elements', () => {
|
|
26
|
+
const { container } = render(_jsx(TypingIndicator, {}));
|
|
27
|
+
const svgs = container.querySelectorAll('svg');
|
|
28
|
+
svgs.forEach(svg => {
|
|
29
|
+
expect(svg.tagName.toLowerCase()).toBe('svg');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -3,13 +3,15 @@ import { Button, Stack, Typography } from '@mui/material';
|
|
|
3
3
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
4
4
|
import { useCallback, useContext } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
const ConfirmDeleteModal = ({ onConfirm }) => {
|
|
6
|
+
const ConfirmDeleteModal = ({ onConfirm, title, description, preferDelete, preferCancel }) => {
|
|
7
7
|
const { t } = useTranslation();
|
|
8
8
|
const { close } = useContext(ModalContext);
|
|
9
9
|
const handleConfirm = useCallback(() => {
|
|
10
10
|
onConfirm();
|
|
11
11
|
close();
|
|
12
12
|
}, [close, onConfirm]);
|
|
13
|
-
|
|
13
|
+
const modalTitle = title ?? t('modal.confirm.delete.title');
|
|
14
|
+
const modalDesc = description ?? t('modal.confirm.delete.description');
|
|
15
|
+
return (_jsxs(Stack, { spacing: 2, p: 2, alignItems: "start", sx: { minWidth: '500px' }, children: [_jsx(Typography, { variant: "h4", children: modalTitle }), _jsx(Typography, { children: modalDesc }), _jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "end", children: [_jsx(Button, { variant: preferCancel ? 'contained' : 'outlined', onClick: close, children: t('button.cancel') }), _jsx(Button, { variant: preferDelete ? 'contained' : 'outlined', onClick: handleConfirm, children: t('button.delete') })] })] }));
|
|
14
16
|
};
|
|
15
17
|
export default ConfirmDeleteModal;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { OpenInNew } from '@mui/icons-material';
|
|
3
|
+
import { Box, Chip, Divider, Grid, Stack, Tooltip, Typography, avatarClasses, chipClasses, iconButtonClasses, useTheme } from '@mui/material';
|
|
3
4
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
4
5
|
import { uniq } from 'lodash-es';
|
|
5
6
|
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
@@ -75,7 +76,7 @@ const HitBanner = ({ hit, lazy = false, layout = HitLayout.NORMAL, showAssigned
|
|
|
75
76
|
], children: [t(i18nKey), ":"] }), (Array.isArray(value) ? value : [value]).map(val => (_jsx(PluginTypography, { component: "span", context: "banner", variant: textVariant, noWrap: compressed, textOverflow: compressed ? 'ellipsis' : 'wrap', ...typographyProps, value: val, field: field, obj: hit }, val)))] }));
|
|
76
77
|
return compressed ? (_jsx(Tooltip, { title: Array.isArray(value) ? (_jsx("div", { children: value.map(_indicator => (_jsx("p", { style: { margin: 0, padding: 0 }, children: _indicator }, _indicator))) })) : (value), children: _children })) : (_children);
|
|
77
78
|
}, [compressed, hit, t, textVariant]);
|
|
78
|
-
return (_jsxs(Box, { display: "grid", gridTemplateColumns: "minmax(0, auto) minmax(0, 1fr) minmax(0, auto)", alignItems: "stretch", sx: { width: '100%', ml: 0, overflow: 'hidden' }, children: [leftBox, _jsxs(Stack, { sx: {
|
|
79
|
+
return (_jsxs(Box, { display: "grid", gridTemplateColumns: "minmax(0, auto) minmax(0, 1fr) minmax(0, auto)", alignItems: "stretch", sx: { width: '100%', ml: 0, overflow: 'hidden', textDecoration: 'none', color: 'text.primary' }, component: "a", href: `/hits/${hit?.howler.id}`, onClick: e => e.preventDefault(), children: [leftBox, _jsxs(Stack, { sx: {
|
|
79
80
|
height: '100%',
|
|
80
81
|
padding: theme.spacing(1),
|
|
81
82
|
gridColumn: { xs: 'span 3', sm: 'span 1', md: 'span 1' }
|
|
@@ -96,6 +97,8 @@ const HitBanner = ({ hit, lazy = false, layout = HitLayout.NORMAL, showAssigned
|
|
|
96
97
|
width: theme.spacing(3)
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
|
-
], children: [_jsx(HitTimestamp, { hit: hit, layout: layout }),
|
|
100
|
+
], children: [_jsx(HitTimestamp, { hit: hit, layout: layout }), _jsx(Assigned, { hit: hit, layout: layout, showAssigned: showAssigned }), hit.howler.links?.[0]?.href && (_jsx(Chip, { icon: _jsx(OpenInNew, {}), label: hit.howler.links[0].title || t('hit.header.link'), size: layout !== HitLayout.COMFY ? 'small' : 'medium', component: "a", href: hit.howler.links[0].href, target: "_blank", rel: "noopener noreferrer", sx: { [`.${chipClasses.label}`]: { cursor: 'pointer !important' } }, onClick: e => {
|
|
101
|
+
e.stopPropagation();
|
|
102
|
+
} })), _jsxs(Stack, { direction: "row", spacing: layout !== HitLayout.COMFY ? 0.5 : 1, children: [_jsx(EscalationChip, { hit: hit, layout: layout }), ['in-progress', 'on-hold'].includes(hit.howler.status) && (_jsx(Chip, { sx: { width: 'fit-content', display: 'inline-flex' }, label: hit.howler.status, color: "primary" }))] }), hit.howler.related && _jsx(RelatedRecords, { hit: hit }), howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.status`, { hit, layout }))] })] }));
|
|
100
103
|
};
|
|
101
104
|
export default HitBanner;
|
|
@@ -12,7 +12,7 @@ const HitLinks = ({ hit, analytic, dossiers = [] }) => {
|
|
|
12
12
|
dossiers.filter(_dossier => _dossier.pivots?.length > 0).length > 0) && (_jsxs(Grid, { container: true, spacing: 1, pr: 2, sx: { [`& .${gridClasses.item}`]: { display: 'flex' } }, children: [hit?.howler?.links?.length > 0 &&
|
|
13
13
|
uniqBy(hit.howler.links, 'href')
|
|
14
14
|
.slice(0, 3)
|
|
15
|
-
.map(l => (_jsx(Grid, { item: true, children: _jsx(RelatedLink, { compact: true, ...l }) }, l.href))), sortBy(dossiers.flatMap(_dossier => _dossier.pivots ?? []), `label.${i18n.language}`).map((_pivot, index) => (
|
|
15
|
+
.map(l => (_jsx(Grid, { item: true, children: _jsx(RelatedLink, { compact: true, ...l, target: "_blank", rel: "noopener noreferrer" }) }, l.href))), sortBy(dossiers.flatMap(_dossier => _dossier.pivots ?? []), `label.${i18n.language}`).map((_pivot, index) => (
|
|
16
16
|
// eslint-disable-next-line react/no-array-index-key
|
|
17
17
|
_jsx(Grid, { item: true, children: _jsx(PivotLink, { pivot: _pivot, hit: hit, compact: true }) }, _pivot.value + index))), analytic?.notebooks?.length > 0 && (_jsx(Grid, { item: true, children: _jsx(HitNotebooks, { analytic: analytic, hit: hit, compact: true }) }))] })));
|
|
18
18
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Analytics, InfoOutlined } from '@mui/icons-material';
|
|
3
3
|
import { Alert, AlertTitle, Autocomplete, Box, Button, Chip, CircularProgress, Divider, Fade, Grid, LinearProgress, Stack, TextField, Tooltip, Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
@@ -24,6 +24,7 @@ const HitSummary = ({ response, onStart, onComplete }) => {
|
|
|
24
24
|
const { hitFields } = useContext(FieldContext);
|
|
25
25
|
const { showErrorMessage } = useMySnackbar();
|
|
26
26
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
27
|
+
const [showHitSummaryGraph] = useMyLocalStorageItem(StorageKey.SHOW_HIT_SUMMARY_GRAPH, true);
|
|
27
28
|
const { getMatchingTemplate } = useMatchers();
|
|
28
29
|
const searching = useContextSelector(RecordSearchContext, ctx => ctx.searching);
|
|
29
30
|
const error = useContextSelector(RecordSearchContext, ctx => ctx.error);
|
|
@@ -131,7 +132,7 @@ const HitSummary = ({ response, onStart, onComplete }) => {
|
|
|
131
132
|
performAggregation();
|
|
132
133
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
134
|
}, [query, views, searching, error]);
|
|
134
|
-
return (_jsxs(Stack, { sx: { mx: 2, height: '100%' }, spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('hit.summary.aggregate.title') }), _jsx(Divider, { flexItem: true }), _jsx(HitGraph, {}), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { sx: { overflow: 'auto', marginTop: '0 !important' }, pt: 1, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(Autocomplete, { fullWidth: true, multiple: true, sx: { minWidth: '175px' }, size: "small", value: customKeys, options: hitFields.map(_field => _field.key), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.summary.adhoc') }), onChange: (_, value) => setCustomKeys(value) }), _jsx(Button, { variant: "outlined", startIcon: loading ? _jsx(CircularProgress, { size: 20, sx: { ml: 1 } }) : _jsx(Analytics, { sx: { ml: 1 } }), disabled: loading, onClick: () => performAggregation(), children: t('button.aggregate') })] }), isEmpty(aggregateResults) && (_jsxs(Alert, { severity: "info", variant: "outlined", children: [_jsx(AlertTitle, { children: t('hit.summary.aggregate.nokeys.title') }), t('hit.summary.aggregate.nokeys.description')] })), loading && _jsx(LinearProgress, { sx: { minHeight: '4px' } }), Object.keys(aggregateResults)
|
|
135
|
+
return (_jsxs(Stack, { sx: { mx: 2, height: '100%' }, spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('hit.summary.aggregate.title') }), _jsx(Divider, { flexItem: true }), showHitSummaryGraph && (_jsxs(_Fragment, { children: [_jsx(HitGraph, {}), _jsx(Divider, { flexItem: true })] })), _jsxs(Stack, { sx: { overflow: 'auto', marginTop: '0 !important' }, pt: 1, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 2, mb: 2, alignItems: "stretch", children: [_jsx(Autocomplete, { fullWidth: true, multiple: true, sx: { minWidth: '175px' }, size: "small", value: customKeys, options: hitFields.map(_field => _field.key), renderInput: _params => _jsx(TextField, { ..._params, label: t('hit.summary.adhoc') }), onChange: (_, value) => setCustomKeys(value) }), _jsx(Button, { variant: "outlined", startIcon: loading ? _jsx(CircularProgress, { size: 20, sx: { ml: 1 } }) : _jsx(Analytics, { sx: { ml: 1 } }), disabled: loading, onClick: () => performAggregation(), children: t('button.aggregate') })] }), isEmpty(aggregateResults) && (_jsxs(Alert, { severity: "info", variant: "outlined", children: [_jsx(AlertTitle, { children: t('hit.summary.aggregate.nokeys.title') }), t('hit.summary.aggregate.nokeys.description')] })), loading && _jsx(LinearProgress, { sx: { minHeight: '4px' } }), Object.keys(aggregateResults)
|
|
135
136
|
.filter(key => !isEmpty(aggregateResults[key]))
|
|
136
137
|
.flatMap(key => [
|
|
137
138
|
_jsx(Fade, { in: true, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [_jsx(Typography, { variant: "body1", children: key }, key + '-title'), keyCounts[key]?.count < 0 ? (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", t('hit.summary.adhoc.custom'), ")"] })) : (_jsxs(Typography, { variant: "caption", color: "text.secondary", children: ["(", keyCounts[key]?.count ?? '?', " ", t('references'), ")"] })), _jsx(Tooltip, { title: _jsxs(Stack, { children: [_jsx(Typography, { variant: "caption", children: t('hit.summary.aggregate.sources') }), keyCounts[key]?.sources.map(source => (_jsx(Typography, { variant: "caption", children: source }, source))) ?? '?'] }), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }) }, key + '-refs'),
|
|
@@ -13,7 +13,7 @@ const AnalyticLink = ({ hit, lazy = false, compressed, alignSelf = 'start' }) =>
|
|
|
13
13
|
getMatchingAnalytic(hit).then(analytic => setAnalyticId(analytic?.analytic_id));
|
|
14
14
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
15
15
|
}, [hit?.howler.analytic]);
|
|
16
|
-
return (_jsxs(Typography, { variant: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf, '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, onAuxClick: e => {
|
|
16
|
+
return (_jsxs(Typography, { variant: compressed ? 'body1' : 'h6', fontWeight: compressed && 'bold', sx: { alignSelf, '& a': { color: 'text.primary' } }, children: [analyticId ? (_jsx(Link, { to: `/analytics/${analyticId}`, target: "_blank", rel: "noopener noreferrer", onAuxClick: e => {
|
|
17
17
|
e.stopPropagation();
|
|
18
18
|
}, onClick: e => {
|
|
19
19
|
e.stopPropagation();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { avatarClasses, AvatarGroup, Chip, Stack } from '@mui/material';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { avatarClasses, AvatarGroup, Chip, Divider, Stack, Typography } from '@mui/material';
|
|
3
3
|
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
4
4
|
import { SocketContext } from '@cccsaurora/howler-ui/components/app/providers/SocketProvider';
|
|
5
5
|
import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
|
|
@@ -7,21 +7,23 @@ import { uniq } from 'lodash-es';
|
|
|
7
7
|
import { useContext } from 'react';
|
|
8
8
|
import { useTranslation } from 'react-i18next';
|
|
9
9
|
import { HitLayout } from '../HitLayout';
|
|
10
|
-
const
|
|
10
|
+
const AvatarChip = ({ userId, noUser, placeholder, layout, hideLabel }) => {
|
|
11
|
+
const userAvatar = (_jsx(HowlerAvatar, { userId: userId, sx: { height: layout !== HitLayout.COMFY ? 24 : 32, width: layout !== HitLayout.COMFY ? 24 : 32 } }));
|
|
12
|
+
return hideLabel ? (userAvatar) : (_jsx(Chip, { variant: "outlined", sx: {
|
|
13
|
+
width: 'fit-content',
|
|
14
|
+
'& .MuiChip-icon': {
|
|
15
|
+
marginLeft: 0
|
|
16
|
+
}
|
|
17
|
+
}, icon: userAvatar, label: userId && userId !== noUser ? userId : placeholder, size: layout !== HitLayout.COMFY ? 'small' : 'medium' }));
|
|
18
|
+
};
|
|
19
|
+
const Assigned = ({ hit, layout, hideLabel = false, showAssessor = false, showAssigned = false }) => {
|
|
11
20
|
const { t } = useTranslation();
|
|
12
21
|
const { user } = useAppUser();
|
|
13
22
|
const { viewers } = useContext(SocketContext);
|
|
14
23
|
const hitViewers = uniq(viewers[hit?.howler?.id] ?? []).filter(viewer => viewer !== user.username);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'& .MuiChip-icon': {
|
|
19
|
-
marginLeft: 0
|
|
20
|
-
}
|
|
21
|
-
}, icon: userAvatar, label: !hideLabel &&
|
|
22
|
-
(hit?.howler.assignment !== 'unassigned'
|
|
23
|
-
? hit?.howler.assignment
|
|
24
|
-
: t('app.drawer.hit.assignment.unassigned.name')), size: layout !== HitLayout.COMFY ? 'small' : 'medium' })), _jsx(AvatarGroup, { max: 3, sx: { [`.${avatarClasses.root}`]: { border: 0, marginLeft: 0.5 } }, componentsProps: {
|
|
24
|
+
const assessorVisible = showAssessor || hit.howler.assessment != null;
|
|
25
|
+
const assigneeVisible = !hit.howler.assessor && (showAssigned || hit.howler.assignment !== 'unassigned');
|
|
26
|
+
return (_jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsxs(Stack, { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", alignItems: "center", columnGap: 0.5, rowGap: 0.25, children: [assigneeVisible && (_jsxs(_Fragment, { children: [!hideLabel && _jsxs(Typography, { variant: "caption", children: [t('app.drawer.hit.assignment.assignee'), ":"] }), _jsx(AvatarChip, { userId: hit?.howler.assignment, noUser: "unassigned", placeholder: t('app.drawer.hit.assignment.unassigned.name'), layout: layout, hideLabel: hideLabel })] })), assessorVisible && (_jsxs(_Fragment, { children: [!hideLabel && _jsxs(Typography, { variant: "caption", children: [t('app.drawer.hit.assessment.assessor'), ":"] }), _jsx(AvatarChip, { userId: hit.howler.assessor, noUser: "unknown", placeholder: t('app.drawer.hit.assessment.unknown.name'), layout: layout, hideLabel: hideLabel })] }))] }), hitViewers.length > 0 && hideLabel && _jsx(Divider, { orientation: "vertical", flexItem: true, variant: "middle" }), _jsx(AvatarGroup, { max: 3, sx: { [`.${avatarClasses.root}`]: { border: 0, marginLeft: 0.5 } }, componentsProps: {
|
|
25
27
|
additionalAvatar: {
|
|
26
28
|
sx: {
|
|
27
29
|
height: layout !== HitLayout.COMFY ? 24 : 32,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { AddCircleOutline, Assignment, CreateNewFolder, Edit, HowToVote, NoteAdd, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
|
|
3
3
|
import api from '@cccsaurora/howler-ui/api';
|
|
4
|
+
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks/useAppUser';
|
|
4
5
|
import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
|
|
5
6
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
6
7
|
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
@@ -43,6 +44,7 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
|
|
|
43
44
|
const { t } = useTranslation();
|
|
44
45
|
const { dispatchApi } = useMyApi();
|
|
45
46
|
const { executeAction } = useMyActionFunctions();
|
|
47
|
+
const appUser = useAppUser();
|
|
46
48
|
const { config } = useContext(ApiConfigContext);
|
|
47
49
|
const { showModal } = useContext(ModalContext);
|
|
48
50
|
const pluginStore = usePluginStore();
|
|
@@ -62,6 +64,14 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
|
|
|
62
64
|
: [], [record, selectedRecords]);
|
|
63
65
|
const hits = useMemo(() => records.filter(isHit), [records]);
|
|
64
66
|
const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
|
|
67
|
+
/**
|
|
68
|
+
* Checks if the current user has permission to run actions.
|
|
69
|
+
* Users must have one of the automation or actionrunner roles, or be an admin.
|
|
70
|
+
*/
|
|
71
|
+
const canRunActions = useCallback(() => {
|
|
72
|
+
const roles = ['admin', 'automation_advanced', 'automation_basic', 'actionrunner_advanced', 'actionrunner_basic'];
|
|
73
|
+
return roles.some((role) => appUser.user?.roles?.includes(role));
|
|
74
|
+
}, [appUser.user?.roles]);
|
|
65
75
|
/**
|
|
66
76
|
* Called by ContextMenu after the menu is positioned and opened.
|
|
67
77
|
* Identifies the clicked record and fetches available actions.
|
|
@@ -69,8 +79,10 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
|
|
|
69
79
|
const onOpen = useCallback(async (event) => {
|
|
70
80
|
const _id = getSelectedId(event);
|
|
71
81
|
setId(_id);
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
// TODO: Bumping the number of rows is a temporary fix - we'll need to improve this.
|
|
83
|
+
const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*', rows: 100, sort: 'name asc' }), {
|
|
84
|
+
throwError: false
|
|
85
|
+
}))?.items;
|
|
74
86
|
if (_actions) {
|
|
75
87
|
setActions(_actions);
|
|
76
88
|
}
|
|
@@ -163,7 +175,7 @@ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
|
|
|
163
175
|
id: 'actions',
|
|
164
176
|
icon: _jsx(SettingsSuggest, {}),
|
|
165
177
|
label: t('route.actions.change'),
|
|
166
|
-
disabled: actions.length < 1,
|
|
178
|
+
disabled: actions.length < 1 || !canRunActions(),
|
|
167
179
|
items: actions.map(action => ({
|
|
168
180
|
key: action.action_id,
|
|
169
181
|
label: action.name,
|
|
@@ -8,6 +8,16 @@ import { vi } from 'vitest';
|
|
|
8
8
|
// Mock API
|
|
9
9
|
vi.mock('api', { spy: true });
|
|
10
10
|
setupContextSelectorMock();
|
|
11
|
+
// Mock useAppUser hook
|
|
12
|
+
const mockUseAppUser = vi.hoisted(() => vi.fn(() => ({
|
|
13
|
+
user: {
|
|
14
|
+
username: 'test-user',
|
|
15
|
+
roles: ['automation_basic', 'actionrunner_basic']
|
|
16
|
+
}
|
|
17
|
+
})));
|
|
18
|
+
vi.mock('commons/components/app/hooks/useAppUser', () => ({
|
|
19
|
+
useAppUser: mockUseAppUser
|
|
20
|
+
}));
|
|
11
21
|
// Mock react-router-dom
|
|
12
22
|
const mockNavigate = vi.fn();
|
|
13
23
|
vi.mock('react-router-dom', async () => {
|
|
@@ -948,4 +958,90 @@ describe('HitContextMenu', () => {
|
|
|
948
958
|
});
|
|
949
959
|
});
|
|
950
960
|
});
|
|
961
|
+
describe('Role-Based Action Permissions', () => {
|
|
962
|
+
afterEach(() => {
|
|
963
|
+
// Reset to default user with required roles
|
|
964
|
+
mockUseAppUser.mockReturnValue({
|
|
965
|
+
user: {
|
|
966
|
+
username: 'test-user',
|
|
967
|
+
roles: ['automation_basic', 'actionrunner_basic']
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
it('should disable actions menu when user lacks required roles', async () => {
|
|
972
|
+
mockUseAppUser.mockReturnValue({
|
|
973
|
+
user: {
|
|
974
|
+
username: 'test-user',
|
|
975
|
+
roles: ['user', 'viewer']
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
979
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
980
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
981
|
+
act(() => {
|
|
982
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
983
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
984
|
+
});
|
|
985
|
+
await waitFor(() => {
|
|
986
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
987
|
+
expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
it('should enable actions menu when user has automation_basic role', async () => {
|
|
991
|
+
mockUseAppUser.mockReturnValue({
|
|
992
|
+
user: {
|
|
993
|
+
username: 'test-user',
|
|
994
|
+
roles: ['automation_basic']
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
998
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
999
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
1000
|
+
act(() => {
|
|
1001
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
1002
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
1003
|
+
});
|
|
1004
|
+
await waitFor(() => {
|
|
1005
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
1006
|
+
expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
it('should enable actions menu when user has actionrunner_advanced role', async () => {
|
|
1010
|
+
mockUseAppUser.mockReturnValue({
|
|
1011
|
+
user: {
|
|
1012
|
+
username: 'test-user',
|
|
1013
|
+
roles: ['actionrunner_advanced']
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
const mockActions = [createMockAction({ action_id: 'action-1', name: 'Custom Action 1' })];
|
|
1017
|
+
mockDispatchApi.mockResolvedValue({ items: mockActions });
|
|
1018
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
1019
|
+
act(() => {
|
|
1020
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
1021
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
1022
|
+
});
|
|
1023
|
+
await waitFor(() => {
|
|
1024
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
1025
|
+
expect(actionsMenuItem).not.toHaveAttribute('aria-disabled', 'true');
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
it('should still disable actions menu when user has roles but no actions available', async () => {
|
|
1029
|
+
mockUseAppUser.mockReturnValue({
|
|
1030
|
+
user: {
|
|
1031
|
+
username: 'test-user',
|
|
1032
|
+
roles: ['automation_advanced']
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
mockDispatchApi.mockResolvedValue({ items: [] });
|
|
1036
|
+
rerender(_jsx(Wrapper, { children: _jsx(RecordContextMenu, { getSelectedId: mockGetSelectedId, children: _jsx("div", { children: "Test Content" }) }) }));
|
|
1037
|
+
act(() => {
|
|
1038
|
+
const contextMenuWrapper = screen.getByText('Test Content').parentElement;
|
|
1039
|
+
fireEvent.contextMenu(contextMenuWrapper);
|
|
1040
|
+
});
|
|
1041
|
+
await waitFor(() => {
|
|
1042
|
+
const actionsMenuItem = screen.getByTestId('actions-menu-item');
|
|
1043
|
+
expect(actionsMenuItem).toHaveAttribute('aria-disabled', 'true');
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
951
1047
|
});
|
|
@@ -285,7 +285,8 @@ const useMyPreferences = () => {
|
|
|
285
285
|
rightBeforeSearch: (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", pr: 1, children: [rightItems.map(item => (_jsx(Fragment, { children: item.component }, item.id))), _jsx(Classification, {})] }))
|
|
286
286
|
},
|
|
287
287
|
leftnav: {
|
|
288
|
-
elements: MENU_ITEMS
|
|
288
|
+
elements: MENU_ITEMS,
|
|
289
|
+
width: 280
|
|
289
290
|
}
|
|
290
291
|
}), [USER_MENU_ITEMS, ADMIN_MENU_ITEMS, MENU_ITEMS, leftItems, rightItems]);
|
|
291
292
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PersonOff } from '@mui/icons-material';
|
|
3
|
+
import { Box, Typography } from '@mui/material';
|
|
4
|
+
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
const PermissionDeniedPage = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
return (_jsxs(PageCenter, { width: "75%", children: [_jsx(Box, { pt: 6, textAlign: "center", fontSize: 200, children: _jsx(PersonOff, { color: "secondary", fontSize: "inherit" }) }), _jsx(Box, { pb: 2, children: _jsx(Typography, { variant: "h2", children: t('page.403.title') }) }), _jsx(Box, { children: _jsx(Typography, { variant: "h5", children: t('page.403.description') }) })] }));
|
|
9
|
+
};
|
|
10
|
+
export default PermissionDeniedPage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import PermissionDeniedPage from './403';
|
|
8
|
+
vi.mock('commons/components/pages/PageCenter', () => ({
|
|
9
|
+
default: ({ children }) => _jsx("div", { id: "page-center", children: children })
|
|
10
|
+
}));
|
|
11
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
12
|
+
describe('PermissionDeniedPage (403)', () => {
|
|
13
|
+
it('should render the 403 title and description', () => {
|
|
14
|
+
render(_jsx(PermissionDeniedPage, {}), { wrapper: Wrapper });
|
|
15
|
+
expect(screen.getByText('403: Access Forbidden')).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText('You do not have permission to access this page.')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
it('should render inside PageCenter', () => {
|
|
19
|
+
render(_jsx(PermissionDeniedPage, {}), { wrapper: Wrapper });
|
|
20
|
+
expect(screen.getByTestId('page-center')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
it('should render the PersonOff icon', () => {
|
|
23
|
+
render(_jsx(PermissionDeniedPage, {}), { wrapper: Wrapper });
|
|
24
|
+
// MUI SVG icons have data-testid attribute
|
|
25
|
+
expect(document.querySelector('[data-testid="PersonOffIcon"]')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import NotFoundPage from './404';
|
|
8
|
+
vi.mock('commons/components/pages/PageCenter', () => ({
|
|
9
|
+
default: ({ children }) => _jsx("div", { id: "page-center", children: children })
|
|
10
|
+
}));
|
|
11
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
12
|
+
describe('NotFoundPage (404)', () => {
|
|
13
|
+
it('should render the 404 title and description', () => {
|
|
14
|
+
render(_jsx(NotFoundPage, {}), { wrapper: Wrapper });
|
|
15
|
+
expect(screen.getByText('404: Not found')).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText('The page you are looking for cannot be found...')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
it('should render inside PageCenter', () => {
|
|
19
|
+
render(_jsx(NotFoundPage, {}), { wrapper: Wrapper });
|
|
20
|
+
expect(screen.getByTestId('page-center')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
it('should render the LinkOff icon', () => {
|
|
23
|
+
render(_jsx(NotFoundPage, {}), { wrapper: Wrapper });
|
|
24
|
+
expect(document.querySelector('[data-testid="LinkOffIcon"]')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import ErrorBoundary from './ErrorBoundary';
|
|
8
|
+
vi.mock('commons/components/pages/PageCenter', () => ({
|
|
9
|
+
default: ({ children }) => _jsx("div", { id: "page-center", children: children })
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('react-pluggable', () => ({
|
|
12
|
+
usePluginStore: () => ({
|
|
13
|
+
executeFunction: vi.fn(() => null)
|
|
14
|
+
})
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('plugins/store', () => ({
|
|
17
|
+
default: { plugins: [] }
|
|
18
|
+
}));
|
|
19
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
20
|
+
const ThrowingComponent = ({ error }) => {
|
|
21
|
+
throw error;
|
|
22
|
+
};
|
|
23
|
+
describe('ErrorBoundary', () => {
|
|
24
|
+
it('should render children when no error occurs', () => {
|
|
25
|
+
render(_jsx(ErrorBoundary, { children: _jsx("div", { children: "Child content" }) }), { wrapper: Wrapper });
|
|
26
|
+
expect(screen.getByText('Child content')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it('should render error UI when a child throws', () => {
|
|
29
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
30
|
+
const testError = new Error('Test error message');
|
|
31
|
+
testError.stack = 'Error: Test error message\n at TestComponent';
|
|
32
|
+
render(_jsx(ErrorBoundary, { children: _jsx(ThrowingComponent, { error: testError }) }), { wrapper: Wrapper });
|
|
33
|
+
expect(screen.getByText('Test error message')).toBeInTheDocument();
|
|
34
|
+
consoleSpy.mockRestore();
|
|
35
|
+
});
|
|
36
|
+
it('should display the error page title when an error occurs', () => {
|
|
37
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
38
|
+
const testError = new Error('Something went wrong');
|
|
39
|
+
render(_jsx(ErrorBoundary, { children: _jsx(ThrowingComponent, { error: testError }) }), { wrapper: Wrapper });
|
|
40
|
+
expect(screen.getByText('Application Stopped Working')).toBeInTheDocument();
|
|
41
|
+
consoleSpy.mockRestore();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import ErrorOccured from './ErrorOccured';
|
|
8
|
+
vi.mock('commons/components/pages/PageCenter', () => ({
|
|
9
|
+
default: ({ children }) => _jsx("div", { id: "page-center", children: children })
|
|
10
|
+
}));
|
|
11
|
+
const mockExecuteFunction = vi.fn(() => null);
|
|
12
|
+
vi.mock('react-pluggable', () => ({
|
|
13
|
+
usePluginStore: () => ({
|
|
14
|
+
executeFunction: mockExecuteFunction
|
|
15
|
+
})
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('plugins/store', () => ({
|
|
18
|
+
default: { plugins: ['TestPlugin'] }
|
|
19
|
+
}));
|
|
20
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
21
|
+
describe('ErrorOccured', () => {
|
|
22
|
+
it('should render the error title and description', () => {
|
|
23
|
+
render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
|
|
24
|
+
expect(screen.getByText('Application Stopped Working')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText('The application stopped working suddenly. If the problem persists please reach out on teams.')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
it('should render inside PageCenter', () => {
|
|
28
|
+
render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
|
|
29
|
+
expect(screen.getByTestId('page-center')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
it('should execute plugin support functions', () => {
|
|
32
|
+
render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
|
|
33
|
+
expect(mockExecuteFunction).toHaveBeenCalledWith('TestPlugin.support');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|