@cccsaurora/howler-ui 2.16.0-dev.376 → 2.16.0-dev.380

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.
Files changed (58) hide show
  1. package/commons/components/app/hooks/useAppConfigs.d.ts +1 -1
  2. package/components/app/App.js +2 -0
  3. package/components/app/hooks/useMatchers.js +0 -4
  4. package/components/app/providers/FavouritesProvider.js +2 -1
  5. package/components/app/providers/FieldProvider.d.ts +2 -2
  6. package/components/app/providers/HitProvider.d.ts +3 -3
  7. package/components/app/providers/HitSearchProvider.d.ts +7 -8
  8. package/components/app/providers/HitSearchProvider.js +64 -39
  9. package/components/app/providers/HitSearchProvider.test.d.ts +1 -0
  10. package/components/app/providers/HitSearchProvider.test.js +505 -0
  11. package/components/app/providers/ParameterProvider.d.ts +13 -5
  12. package/components/app/providers/ParameterProvider.js +240 -84
  13. package/components/app/providers/ParameterProvider.test.d.ts +1 -0
  14. package/components/app/providers/ParameterProvider.test.js +1041 -0
  15. package/components/app/providers/ViewProvider.d.ts +3 -2
  16. package/components/app/providers/ViewProvider.js +21 -14
  17. package/components/app/providers/ViewProvider.test.js +19 -29
  18. package/components/elements/display/ChipPopper.d.ts +21 -0
  19. package/components/elements/display/ChipPopper.js +36 -0
  20. package/components/elements/display/ChipPopper.test.d.ts +1 -0
  21. package/components/elements/display/ChipPopper.test.js +309 -0
  22. package/components/elements/hit/HitActions.js +3 -3
  23. package/components/elements/hit/HitSummary.d.ts +0 -1
  24. package/components/elements/hit/HitSummary.js +11 -21
  25. package/components/elements/hit/aggregate/HitGraph.d.ts +1 -3
  26. package/components/elements/hit/aggregate/HitGraph.js +9 -15
  27. package/components/routes/dossiers/DossierCard.test.js +0 -2
  28. package/components/routes/dossiers/DossierEditor.test.js +27 -33
  29. package/components/routes/hits/search/HitBrowser.js +7 -48
  30. package/components/routes/hits/search/HitContextMenu.test.js +11 -29
  31. package/components/routes/hits/search/InformationPane.js +1 -1
  32. package/components/routes/hits/search/QuerySettings.js +30 -0
  33. package/components/routes/hits/search/QuerySettings.test.d.ts +1 -0
  34. package/components/routes/hits/search/QuerySettings.test.js +553 -0
  35. package/components/routes/hits/search/SearchPane.js +8 -10
  36. package/components/routes/hits/search/ViewLink.d.ts +4 -1
  37. package/components/routes/hits/search/ViewLink.js +37 -19
  38. package/components/routes/hits/search/ViewLink.test.js +349 -303
  39. package/components/routes/hits/search/grid/HitGrid.js +2 -6
  40. package/components/routes/hits/search/shared/HitFilter.d.ts +2 -0
  41. package/components/routes/hits/search/shared/HitFilter.js +31 -23
  42. package/components/routes/hits/search/shared/HitSort.js +16 -8
  43. package/components/routes/hits/search/shared/SearchSpan.js +19 -10
  44. package/components/routes/views/ViewComposer.js +7 -6
  45. package/components/routes/views/Views.js +2 -1
  46. package/locales/en/translation.json +6 -0
  47. package/locales/fr/translation.json +6 -0
  48. package/package.json +2 -2
  49. package/setupTests.js +4 -1
  50. package/tests/mocks.d.ts +18 -0
  51. package/tests/mocks.js +65 -0
  52. package/tests/server-handlers.js +10 -28
  53. package/utils/viewUtils.d.ts +2 -0
  54. package/utils/viewUtils.js +11 -0
  55. package/components/routes/hits/search/shared/QuerySettings.js +0 -22
  56. /package/components/routes/hits/search/{shared/QuerySettings.d.ts → QuerySettings.d.ts} +0 -0
  57. /package/components/routes/hits/search/{CustomSort.d.ts → shared/CustomSort.d.ts} +0 -0
  58. /package/components/routes/hits/search/{CustomSort.js → shared/CustomSort.js} +0 -0
@@ -12,10 +12,11 @@ export interface ViewContextType {
12
12
  addView: (v: View) => Promise<View>;
13
13
  editView: (id: string, newView: Partial<Omit<View, 'view_id' | 'owner'>>) => Promise<View>;
14
14
  removeView: (id: string) => Promise<void>;
15
- getCurrentView: (config?: {
15
+ getCurrentViews: (config?: {
16
16
  viewId?: string;
17
17
  lazy?: boolean;
18
- }) => Promise<View>;
18
+ ignoreParams?: boolean;
19
+ }) => Promise<View[]>;
19
20
  }
20
21
  export declare const ViewContext: import("use-context-selector").Context<ViewContextType>;
21
22
  declare const ViewProvider: FC<PropsWithChildren>;
@@ -5,7 +5,7 @@ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
5
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
6
6
  import { has, omit } from 'lodash-es';
7
7
  import { useCallback, useEffect, useState } from 'react';
8
- import { useLocation, useParams } from 'react-router-dom';
8
+ import { useSearchParams } from 'react-router-dom';
9
9
  import { createContext, useContextSelector } from 'use-context-selector';
10
10
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
11
11
  export const ViewContext = createContext(null);
@@ -13,8 +13,7 @@ const ViewProvider = ({ children }) => {
13
13
  const { dispatchApi } = useMyApi();
14
14
  const appUser = useAppUser();
15
15
  const [defaultView, setDefaultView] = useMyLocalStorageItem(StorageKey.DEFAULT_VIEW);
16
- const location = useLocation();
17
- const routeParams = useParams();
16
+ const [searchParams] = useSearchParams();
18
17
  const [views, setViews] = useState({});
19
18
  const fetchViews = useCallback(async (ids) => {
20
19
  if (!ids) {
@@ -60,18 +59,26 @@ const ViewProvider = ({ children }) => {
60
59
  }
61
60
  })();
62
61
  }, [defaultView, fetchViews, setDefaultView, views]);
63
- const getCurrentView = useCallback(async ({ viewId, lazy = false } = {}) => {
64
- if (!viewId) {
65
- viewId = location.pathname.startsWith('/views') ? routeParams.id : defaultView;
62
+ const getCurrentViews = useCallback(async ({ viewId, lazy = false, ignoreParams = false } = {}) => {
63
+ const currentViews = ignoreParams ? [] : searchParams.getAll('view');
64
+ if (viewId && !currentViews.includes(viewId)) {
65
+ currentViews.push(viewId);
66
66
  }
67
- if (!viewId) {
68
- return null;
69
- }
70
- if (!has(views, viewId) && !lazy) {
71
- return (await fetchViews([viewId]))[0];
67
+ if (currentViews.length < 1) {
68
+ return [];
72
69
  }
73
- return views[viewId];
74
- }, [defaultView, fetchViews, location.pathname, routeParams.id, views]);
70
+ const results = [];
71
+ const missing = [];
72
+ currentViews.forEach(_view => {
73
+ if (has(views, _view)) {
74
+ results.push(views[_view]);
75
+ }
76
+ else if (!lazy) {
77
+ missing.push(_view);
78
+ }
79
+ });
80
+ return [...results, ...(await fetchViews(missing))];
81
+ }, [fetchViews, searchParams, views]);
75
82
  const editView = useCallback(async (id, partialView) => {
76
83
  const result = await dispatchApi(api.view.put(id, partialView));
77
84
  setViews(_views => ({
@@ -118,7 +125,7 @@ const ViewProvider = ({ children }) => {
118
125
  removeView,
119
126
  defaultView,
120
127
  setDefaultView,
121
- getCurrentView
128
+ getCurrentViews
122
129
  }, children: children }));
123
130
  };
124
131
  export const useViewContextSelector = (selector) => {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { act, renderHook, waitFor } from '@testing-library/react';
3
3
  import { hget, hpost, hput } from '@cccsaurora/howler-ui/api';
4
- import MockLocalStorage from '@cccsaurora/howler-ui/tests/MockLocalStorage';
4
+ import { setupLocalStorageMock, setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
5
5
  import { MOCK_RESPONSES } from '@cccsaurora/howler-ui/tests/server-handlers';
6
6
  import { useContextSelector } from 'use-context-selector';
7
7
  import { DEFAULT_QUERY, MY_LOCAL_STORAGE_PREFIX, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
@@ -9,23 +9,17 @@ import ViewProvider, { ViewContext } from './ViewProvider';
9
9
  let mockUser = {
10
10
  favourite_views: ['favourited_view_id']
11
11
  };
12
+ setupReactRouterMock();
13
+ const mockLocalStorage = setupLocalStorageMock();
14
+ import { useSearchParams } from 'react-router-dom';
15
+ vi.mocked(useSearchParams).mockReturnValue([new URLSearchParams('?view=searched_view_id')]);
12
16
  vi.mock('api', { spy: true });
13
- vi.mock('react-router-dom', () => ({
14
- useLocation: vi.fn(() => ({ pathname: '/views/searched_view_id' })),
15
- useParams: vi.fn(() => ({ id: 'searched_view_id' }))
16
- }));
17
17
  vi.mock('commons/components/app/hooks', () => ({
18
18
  useAppUser: () => ({
19
19
  user: mockUser,
20
20
  setUser: _user => (mockUser = _user)
21
21
  })
22
22
  }));
23
- const mockLocalStorage = new MockLocalStorage();
24
- // Replace localStorage in global scope
25
- Object.defineProperty(window, 'localStorage', {
26
- value: mockLocalStorage,
27
- writable: true
28
- });
29
23
  const Wrapper = ({ children }) => {
30
24
  return _jsx(ViewProvider, { children: children });
31
25
  };
@@ -35,7 +29,7 @@ beforeEach(() => {
35
29
  describe('ViewContext', () => {
36
30
  it('should fetch the defaultView on initialization', async () => {
37
31
  mockLocalStorage.setItem(`${MY_LOCAL_STORAGE_PREFIX}.${StorageKey.DEFAULT_VIEW}`, JSON.stringify('searched_view_id'));
38
- let hook = await act(async () => renderHook(() => useContextSelector(ViewContext, ctx => ctx.views), { wrapper: Wrapper }));
32
+ const hook = renderHook(() => useContextSelector(ViewContext, ctx => ctx.views), { wrapper: Wrapper });
39
33
  await waitFor(() => expect(hook.result.current.searched_view_id).not.toBeFalsy());
40
34
  expect(hook.result.current.searched_view_id).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items[0]);
41
35
  });
@@ -73,16 +67,16 @@ describe('ViewContext', () => {
73
67
  }));
74
68
  hook.rerender();
75
69
  expect(hook.result.current.views[result.view_id]).toEqual(result);
76
- await act(async () => hook.result.current.removeView(result.view_id));
70
+ await act(async () => {
71
+ await hook.result.current.removeView(result.view_id);
72
+ });
77
73
  hook.rerender();
78
74
  expect(hook.result.current.views[result.view_id]).toBeFalsy();
79
75
  });
80
76
  describe('fetchViews', () => {
81
77
  let hook;
82
- beforeEach(async () => {
83
- hook = await act(async () => {
84
- return renderHook(() => useContextSelector(ViewContext, ctx => ctx.fetchViews), { wrapper: Wrapper });
85
- });
78
+ beforeEach(() => {
79
+ hook = renderHook(() => useContextSelector(ViewContext, ctx => ctx.fetchViews), { wrapper: Wrapper });
86
80
  vi.mocked(hpost).mockClear();
87
81
  vi.mocked(hget).mockClear();
88
82
  });
@@ -127,32 +121,28 @@ describe('ViewContext', () => {
127
121
  expect(hpost).toHaveBeenCalledOnce();
128
122
  });
129
123
  });
130
- describe('getCurrentView', () => {
124
+ describe('getCurrentViews', () => {
131
125
  let hook;
132
126
  beforeEach(async () => {
133
- hook = await act(async () => {
134
- return renderHook(() => useContextSelector(ViewContext, ctx => ctx.getCurrentView), { wrapper: Wrapper });
135
- });
127
+ hook = renderHook(() => useContextSelector(ViewContext, ctx => ctx.getCurrentViews), { wrapper: Wrapper });
136
128
  });
137
129
  it('should allow the user to fetch their current view based on the location', async () => {
138
130
  // lazy load should return nothing
139
- await expect(hook.result.current({ lazy: true })).resolves.toBeFalsy();
131
+ await expect(hook.result.current({ lazy: true })).resolves.toEqual([]);
140
132
  const result = await act(async () => hook.result.current());
141
- expect(result).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items[0]);
133
+ expect(result).toEqual([MOCK_RESPONSES['/api/v1/search/view'].items[0]]);
142
134
  });
143
135
  it('should allow the user to fetch their current view based on the view ID', async () => {
144
136
  // lazy load should return nothing
145
- await expect(hook.result.current({ lazy: true })).resolves.toBeFalsy();
137
+ await expect(hook.result.current({ lazy: true })).resolves.toEqual([]);
146
138
  const result = await act(async () => hook.result.current({ viewId: 'searched_view_id' }));
147
- expect(result).toEqual(MOCK_RESPONSES['/api/v1/search/view'].items[0]);
139
+ expect(result).toEqual([MOCK_RESPONSES['/api/v1/search/view'].items[0]]);
148
140
  });
149
141
  });
150
142
  describe('editView', () => {
151
143
  let hook;
152
- beforeAll(async () => {
153
- hook = await act(async () => {
154
- return renderHook(() => useContextSelector(ViewContext, ctx => ctx.editView), { wrapper: Wrapper });
155
- });
144
+ beforeAll(() => {
145
+ hook = renderHook(() => useContextSelector(ViewContext, ctx => ctx.editView), { wrapper: Wrapper });
156
146
  });
157
147
  beforeEach(() => {
158
148
  vi.mocked(hput).mockClear();
@@ -0,0 +1,21 @@
1
+ import type { ChipProps, PaperProps, SxProps } from '@mui/material';
2
+ import type { ReactElement, ReactNode } from 'react';
3
+ interface ChipPopperProps {
4
+ icon?: ReactElement;
5
+ deleteIcon?: ReactElement;
6
+ label?: ReactNode;
7
+ children: ReactNode;
8
+ slotProps?: {
9
+ chip?: Partial<ChipProps>;
10
+ paper?: PaperProps;
11
+ };
12
+ paperSx?: SxProps;
13
+ minWidth?: string | number;
14
+ placement?: 'bottom-start' | 'bottom-end' | 'bottom';
15
+ onToggle?: (show: boolean) => void;
16
+ onDelete?: (event?: any) => void;
17
+ toggleOnDelete?: boolean;
18
+ closeOnClick?: boolean;
19
+ }
20
+ declare const _default: import("react").NamedExoticComponent<ChipPopperProps>;
21
+ export default _default;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Chip, ClickAwayListener, Collapse, Paper, Popper } from '@mui/material';
3
+ import { memo, useRef, useState } from 'react';
4
+ const ChipPopper = ({ icon, deleteIcon, label, children, minWidth, placement = 'bottom-start', onToggle, onDelete, toggleOnDelete = false, closeOnClick = false, slotProps = {} }) => {
5
+ const [show, setShow] = useState(false);
6
+ const anchorEl = useRef(null);
7
+ const handleToggle = (newShow) => {
8
+ setShow(newShow);
9
+ onToggle?.(newShow);
10
+ };
11
+ return (_jsxs(_Fragment, { children: [_jsx(Chip, { icon: icon, deleteIcon: deleteIcon, label: label, onClick: e => {
12
+ handleToggle(!show);
13
+ e.stopPropagation();
14
+ }, onDelete: onDelete ?? (toggleOnDelete ? () => handleToggle(!show) : null), ref: anchorEl, sx: [
15
+ theme => ({
16
+ position: 'relative',
17
+ zIndex: 1,
18
+ transition: theme.transitions.create(['border-bottom-left-radius', 'border-bottom-right-radius'])
19
+ }),
20
+ show && { borderBottomLeftRadius: '0', borderBottomRightRadius: '0' },
21
+ ...(Array.isArray(slotProps.chip?.sx) ? slotProps.chip.sx : [slotProps.chip?.sx])
22
+ ], ...(slotProps.chip ?? {}) }), _jsx(Popper, { placement: placement, anchorEl: anchorEl.current, disablePortal: true, open: true, sx: {
23
+ minWidth: Math.max(typeof minWidth === 'number' ? minWidth : parseInt(minWidth?.replace('px', '')) || 0, anchorEl.current?.clientWidth || 0),
24
+ zIndex: 1
25
+ }, children: _jsx(Collapse, { in: show, unmountOnExit: true, onClick: () => {
26
+ if (closeOnClick) {
27
+ handleToggle(false);
28
+ }
29
+ }, children: _jsx(ClickAwayListener, { onClickAway: () => {
30
+ handleToggle(false);
31
+ }, children: _jsx(Paper, { sx: [
32
+ { borderTopLeftRadius: 0, borderTopRightRadius: 0, px: 1, py: 2 },
33
+ ...(Array.isArray(slotProps.paper?.sx) ? slotProps.paper.sx : [slotProps.paper?.sx])
34
+ ], ...(slotProps.paper ?? {}), children: children }) }) }) })] }));
35
+ };
36
+ export default memo(ChipPopper);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,309 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { Info } from '@mui/icons-material';
4
+ import { render, screen, waitFor } from '@testing-library/react';
5
+ import userEvent, {} from '@testing-library/user-event';
6
+ import { vi } from 'vitest';
7
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
8
+ import ChipPopper from './ChipPopper';
9
+ describe('ChipPopper', () => {
10
+ let user;
11
+ beforeEach(() => {
12
+ user = userEvent.setup();
13
+ vi.clearAllMocks();
14
+ });
15
+ describe('Rendering', () => {
16
+ it('should render chip with icon and label', () => {
17
+ render(_jsx(ChipPopper, { icon: _jsx(Info, { id: "chip-icon" }), label: "Test Label", children: _jsx("div", { children: "Content" }) }));
18
+ expect(screen.getByTestId('chip-icon')).toBeInTheDocument();
19
+ expect(screen.getByText('Test Label')).toBeInTheDocument();
20
+ });
21
+ it('should render chip with complex label', () => {
22
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: _jsxs("div", { children: [_jsx("span", { children: "Complex" }), " ", _jsx("strong", { children: "Label" })] }), children: _jsx("div", { children: "Content" }) }));
23
+ expect(screen.getByText('Complex')).toBeInTheDocument();
24
+ expect(screen.getByText('Label')).toBeInTheDocument();
25
+ });
26
+ it('should not show popper content initially', () => {
27
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { id: "popper-content", children: "Content" }) }));
28
+ // Content should not be visible initially
29
+ expect(screen.queryByTestId('popper-content')).toBeNull();
30
+ });
31
+ });
32
+ describe('Toggle Behavior', () => {
33
+ it('should show popper content when chip is clicked', async () => {
34
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { id: "popper-content", children: "Content" }) }));
35
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
36
+ await user.click(chip);
37
+ await waitFor(() => {
38
+ expect(screen.getByTestId('popper-content')).toBeVisible();
39
+ });
40
+ });
41
+ it('should hide popper content when chip is clicked again', async () => {
42
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { id: "popper-content", children: "Content" }) }));
43
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
44
+ // Open
45
+ await user.click(chip);
46
+ await waitFor(() => {
47
+ expect(screen.getByTestId('popper-content')).toBeVisible();
48
+ });
49
+ // Close
50
+ await user.click(chip);
51
+ await waitFor(() => {
52
+ expect(screen.queryByTestId('popper-content')).toBeNull();
53
+ });
54
+ });
55
+ it('should call onToggle callback when toggled', async () => {
56
+ const onToggle = vi.fn();
57
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", onToggle: onToggle, children: _jsx("div", { children: "Content" }) }));
58
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
59
+ await user.click(chip);
60
+ expect(onToggle).toHaveBeenCalledWith(true);
61
+ await user.click(chip);
62
+ expect(onToggle).toHaveBeenCalledWith(false);
63
+ expect(onToggle).toHaveBeenCalledTimes(2);
64
+ });
65
+ it('should stop event propagation when chip is clicked', async () => {
66
+ const parentClickHandler = vi.fn();
67
+ render(_jsx("div", { onClick: parentClickHandler, children: _jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { children: "Content" }) }) }));
68
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
69
+ await user.click(chip);
70
+ expect(parentClickHandler).not.toHaveBeenCalled();
71
+ });
72
+ });
73
+ describe('Click Away Behavior', () => {
74
+ it('should close popper when clicking away', async () => {
75
+ render(_jsxs("div", { children: [_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { id: "popper-content", children: "Content" }) }), _jsx("button", { id: "outside-button", children: "Outside" })] }));
76
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
77
+ await user.click(chip);
78
+ await waitFor(() => {
79
+ expect(screen.getByTestId('popper-content')).toBeVisible();
80
+ });
81
+ // Click outside
82
+ const outsideButton = screen.getByTestId('outside-button');
83
+ await user.click(outsideButton);
84
+ await waitFor(() => {
85
+ expect(screen.queryByTestId('popper-content')).toBeNull();
86
+ });
87
+ });
88
+ it('should call onToggle when clicking away', async () => {
89
+ const onToggle = vi.fn();
90
+ render(_jsxs("div", { children: [_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", onToggle: onToggle, children: _jsx("div", { children: "Content" }) }), _jsx("button", { id: "outside-button", children: "Outside" })] }));
91
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
92
+ await user.click(chip);
93
+ expect(onToggle).toHaveBeenCalledWith(true);
94
+ onToggle.mockClear();
95
+ // Click outside
96
+ const outsideButton = screen.getByTestId('outside-button');
97
+ await user.click(outsideButton);
98
+ expect(onToggle).toHaveBeenCalledWith(false);
99
+ });
100
+ });
101
+ describe('Props Customization', () => {
102
+ it('should apply custom chip props', () => {
103
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", slotProps: {
104
+ chip: {
105
+ color: 'primary',
106
+ variant: 'outlined',
107
+ id: 'custom-chip'
108
+ }
109
+ }, children: _jsx("div", { children: "Content" }) }));
110
+ const chip = screen.getByTestId('custom-chip');
111
+ expect(chip).toHaveClass('MuiChip-colorPrimary');
112
+ expect(chip).toHaveClass('MuiChip-outlined');
113
+ });
114
+ it('should apply custom chip sx prop', () => {
115
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", slotProps: {
116
+ chip: {
117
+ sx: { backgroundColor: 'red' }
118
+ }
119
+ }, children: _jsx("div", { children: "Content" }) }));
120
+ const chip = container.querySelector('.MuiChip-root');
121
+ expect(window.getComputedStyle(chip).getPropertyValue('background-color')).toBe('rgb(255, 0, 0)');
122
+ });
123
+ it('should apply custom paper props', async () => {
124
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", slotProps: {
125
+ paper: {
126
+ elevation: 8,
127
+ id: 'custom-paper'
128
+ }
129
+ }, children: _jsx("div", { children: "Content" }) }));
130
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
131
+ await user.click(chip);
132
+ await waitFor(() => {
133
+ const paper = screen.getByTestId('custom-paper');
134
+ expect(paper).toBeInTheDocument();
135
+ expect(paper).toHaveClass('MuiPaper-elevation8');
136
+ });
137
+ });
138
+ it('should apply custom paper sx prop', async () => {
139
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", slotProps: {
140
+ paper: {
141
+ sx: { backgroundColor: 'blue' }
142
+ }
143
+ }, children: _jsx("div", { children: "Content" }) }));
144
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
145
+ await user.click(chip);
146
+ await waitFor(() => {
147
+ const paper = container.querySelector('.MuiPaper-root');
148
+ expect(window.getComputedStyle(paper).getPropertyValue('background-color')).toBe('rgb(0, 0, 255)');
149
+ });
150
+ });
151
+ it('should use custom placement', async () => {
152
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", placement: "bottom-end", children: _jsx("div", { children: "Content" }) }));
153
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
154
+ await user.click(chip);
155
+ await waitFor(() => {
156
+ const popper = container.querySelector('[data-popper-placement]');
157
+ expect(popper).toHaveAttribute('data-popper-placement', 'bottom-end');
158
+ });
159
+ });
160
+ it('should use custom minWidth when provided', async () => {
161
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", minWidth: "500px", children: _jsx("div", { children: "Content" }) }));
162
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
163
+ await user.click(chip);
164
+ await waitFor(() => {
165
+ const popper = container.querySelector('.MuiPopper-root');
166
+ expect(popper).toHaveStyle({ minWidth: '500px' });
167
+ });
168
+ });
169
+ it('should default to chip width when minWidth not provided', async () => {
170
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { children: "Content" }) }));
171
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
172
+ await user.click(chip);
173
+ await waitFor(() => {
174
+ const popper = container.querySelector('.MuiPopper-root');
175
+ expect(popper).toBeInTheDocument();
176
+ });
177
+ });
178
+ });
179
+ describe('Styling', () => {
180
+ it('should apply border radius transition styles to chip', () => {
181
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { children: "Content" }) }));
182
+ const chip = container.querySelector('.MuiChip-root');
183
+ expect(chip).toHaveStyle({ position: 'relative', zIndex: '1' });
184
+ });
185
+ it('should apply rounded corner styles when popper is closed', () => {
186
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { children: "Content" }) }));
187
+ const chip = container.querySelector('.MuiChip-root');
188
+ // Should have all corners rounded when closed
189
+ expect(chip).not.toHaveStyle({
190
+ borderBottomLeftRadius: '0',
191
+ borderBottomRightRadius: '0'
192
+ });
193
+ });
194
+ it('should remove bottom border radius when popper is open', async () => {
195
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { children: "Content" }) }));
196
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
197
+ await user.click(chip);
198
+ await waitFor(() => {
199
+ expect(chip).toHaveStyle({
200
+ borderBottomLeftRadius: '0',
201
+ borderBottomRightRadius: '0'
202
+ });
203
+ });
204
+ });
205
+ it('should apply top border radius 0 to paper', async () => {
206
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { children: "Content" }) }));
207
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
208
+ await user.click(chip);
209
+ await waitFor(() => {
210
+ const paper = container.querySelector('.MuiPaper-root');
211
+ expect(paper).toHaveStyle({
212
+ borderTopLeftRadius: '0',
213
+ borderTopRightRadius: '0'
214
+ });
215
+ });
216
+ });
217
+ });
218
+ describe('Children Rendering', () => {
219
+ it('should render children content inside popper', async () => {
220
+ render(_jsxs(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: [_jsx("div", { id: "child-1", children: "Child 1" }), _jsx("div", { id: "child-2", children: "Child 2" })] }));
221
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
222
+ await user.click(chip);
223
+ await waitFor(() => {
224
+ expect(screen.getByTestId('child-1')).toBeVisible();
225
+ expect(screen.getByTestId('child-2')).toBeVisible();
226
+ });
227
+ });
228
+ it('should render complex children components', async () => {
229
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsxs("div", { children: [_jsx("input", { id: "test-input", type: "text" }), _jsx("button", { id: "test-button", children: "Click me" })] }) }));
230
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
231
+ await user.click(chip);
232
+ await waitFor(() => {
233
+ expect(screen.getByTestId('test-input')).toBeVisible();
234
+ expect(screen.getByTestId('test-button')).toBeVisible();
235
+ });
236
+ });
237
+ it('should allow interaction with children', async () => {
238
+ const buttonClick = vi.fn();
239
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("button", { id: "child-button", onClick: buttonClick, children: "Click me" }) }));
240
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
241
+ await user.click(chip);
242
+ await waitFor(() => {
243
+ expect(screen.getByTestId('child-button')).toBeVisible();
244
+ });
245
+ const button = screen.getByTestId('child-button');
246
+ await user.click(button);
247
+ expect(buttonClick).toHaveBeenCalledTimes(1);
248
+ });
249
+ });
250
+ describe('Edge Cases', () => {
251
+ it('should handle rapid toggling', async () => {
252
+ const onToggle = vi.fn();
253
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", onToggle: onToggle, children: _jsx("div", { children: "Content" }) }));
254
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
255
+ // Rapidly toggle multiple times
256
+ await user.click(chip);
257
+ await user.click(chip);
258
+ await user.click(chip);
259
+ await user.click(chip);
260
+ expect(onToggle).toHaveBeenCalledTimes(4);
261
+ expect(onToggle).toHaveBeenNthCalledWith(1, true);
262
+ expect(onToggle).toHaveBeenNthCalledWith(2, false);
263
+ expect(onToggle).toHaveBeenNthCalledWith(3, true);
264
+ expect(onToggle).toHaveBeenNthCalledWith(4, false);
265
+ });
266
+ it('should work without onToggle callback', async () => {
267
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: _jsx("div", { id: "content", children: "Content" }) }));
268
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
269
+ // Should not throw error
270
+ await user.click(chip);
271
+ await waitFor(() => {
272
+ expect(screen.getByTestId('content')).toBeVisible();
273
+ });
274
+ });
275
+ it('should handle empty children', async () => {
276
+ render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", children: null }));
277
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
278
+ await user.click(chip);
279
+ // Should not throw error
280
+ await waitFor(() => {
281
+ const paper = document.querySelector('.MuiPaper-root');
282
+ expect(paper).toBeInTheDocument();
283
+ });
284
+ });
285
+ it('should handle array sx props in chip slotProps', () => {
286
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", slotProps: {
287
+ chip: {
288
+ sx: [{ backgroundColor: 'red' }, { color: 'white !important' }]
289
+ }
290
+ }, children: _jsx("div", { children: "Content" }) }));
291
+ const chip = container.querySelector('.MuiChip-root');
292
+ expect(window.getComputedStyle(chip).getPropertyValue('color')).toBe('rgb(255, 255, 255)');
293
+ expect(window.getComputedStyle(chip).getPropertyValue('background-color')).toBe('rgb(255, 0, 0)');
294
+ });
295
+ it('should handle array sx props in paper slotProps', async () => {
296
+ const { container } = render(_jsx(ChipPopper, { icon: _jsx(Info, {}), label: "Test", slotProps: {
297
+ paper: {
298
+ sx: [{ backgroundColor: 'blue !important' }, { padding: '20px' }]
299
+ }
300
+ }, children: _jsx("div", { children: "Content" }) }));
301
+ const chip = screen.getByText('Test').closest('.MuiChip-root');
302
+ await user.click(chip);
303
+ await waitFor(() => {
304
+ const paper = container.querySelector('.MuiPaper-root');
305
+ expect(paper).toHaveStyle({ backgroundColor: 'blue !important', padding: '20px' });
306
+ });
307
+ });
308
+ });
309
+ });
@@ -27,7 +27,7 @@ const HitActions = ({ hit, orientation = 'horizontal' }) => {
27
27
  const { values, set } = useMyLocalStorageProvider();
28
28
  const pluginStore = usePluginStore();
29
29
  const { getMatchingAnalytic } = useMatchers();
30
- const getCurrentView = useContextSelector(ViewContext, ctx => ctx.getCurrentView);
30
+ const getCurrentViews = useContextSelector(ViewContext, ctx => ctx.getCurrentViews);
31
31
  const selected = useContextSelector(ParameterContext, ctx => ctx?.selected);
32
32
  const setSelected = useContextSelector(ParameterContext, ctx => ctx?.setSelected);
33
33
  const clearSelectedHits = useContextSelector(HitContext, ctx => ctx.clearSelectedHits);
@@ -67,7 +67,7 @@ const HitActions = ({ hit, orientation = 'horizontal' }) => {
67
67
  actionFunction: async () => {
68
68
  if (!loading) {
69
69
  await assess(assessment, analytic?.triage_settings?.skip_rationale);
70
- if ((await getCurrentView())?.settings?.advance_on_triage && nextHit) {
70
+ if ((await getCurrentViews())[0]?.settings?.advance_on_triage && nextHit) {
71
71
  clearSelectedHits(nextHit.howler.id);
72
72
  setSelected?.(nextHit.howler.id);
73
73
  }
@@ -86,7 +86,7 @@ const HitActions = ({ hit, orientation = 'horizontal' }) => {
86
86
  canVote,
87
87
  clearSelectedHits,
88
88
  config.lookups,
89
- getCurrentView,
89
+ getCurrentViews,
90
90
  loading,
91
91
  nextHit,
92
92
  setSelected,
@@ -2,7 +2,6 @@ import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
2
2
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
3
  import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
4
4
  declare const _default: import("react").NamedExoticComponent<{
5
- query: string;
6
5
  response?: HowlerSearchResponse<WithMetadata<Hit>>;
7
6
  execute?: boolean;
8
7
  onStart?: () => void;