@cccsaurora/howler-ui 2.19.0-dev.1099 → 2.19.0-dev.1148

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 (66) hide show
  1. package/components/app/App.js +4 -2
  2. package/components/app/providers/GridColumnsProvider.d.ts +52 -0
  3. package/components/app/providers/GridColumnsProvider.js +177 -0
  4. package/components/app/providers/GridColumnsProvider.test.js +255 -0
  5. package/components/elements/addons/buttons/CustomIconButton.test.d.ts +1 -0
  6. package/components/elements/addons/buttons/CustomIconButton.test.js +81 -0
  7. package/components/elements/addons/layout/FlexVertical.test.d.ts +1 -0
  8. package/components/elements/addons/layout/FlexVertical.test.js +32 -0
  9. package/components/elements/addons/layout/vsbox/VSBoxContent.test.d.ts +1 -0
  10. package/components/elements/addons/layout/vsbox/VSBoxContent.test.js +26 -0
  11. package/components/elements/addons/search/phrase/WordLexer.test.d.ts +1 -0
  12. package/components/elements/addons/search/phrase/WordLexer.test.js +55 -0
  13. package/components/elements/hit/HitContextMenu.test.d.ts +1 -0
  14. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.d.ts +2 -1
  15. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.js +3 -3
  16. package/components/{routes/hits/search → elements/hit}/grid/HitRow.d.ts +1 -1
  17. package/components/elements/hit/grid/HitTable.d.ts +12 -0
  18. package/components/elements/hit/grid/HitTable.js +82 -0
  19. package/components/elements/view/LayoutToggle.d.ts +8 -0
  20. package/components/elements/view/LayoutToggle.js +12 -0
  21. package/components/elements/view/ViewTitle.d.ts +3 -0
  22. package/components/elements/view/ViewTitle.js +6 -3
  23. package/components/hooks/useMyTheme.test.d.ts +1 -0
  24. package/components/hooks/useMyTheme.test.js +35 -0
  25. package/components/routes/advanced/eqlTokenProvider.test.d.ts +1 -0
  26. package/components/routes/advanced/eqlTokenProvider.test.js +34 -0
  27. package/components/routes/advanced/luceneTokenProvider.test.d.ts +1 -0
  28. package/components/routes/advanced/luceneTokenProvider.test.js +34 -0
  29. package/components/routes/hits/search/HitBrowser.js +2 -1
  30. package/components/routes/hits/search/SearchPane.js +6 -8
  31. package/components/routes/hits/search/ViewLink.js +3 -2
  32. package/components/routes/hits/search/grid/HitGrid.js +17 -85
  33. package/components/routes/hits/search/shared/LayoutSettings.js +25 -0
  34. package/components/routes/hits/search/shared/SearchActionMenu.d.ts +4 -0
  35. package/components/routes/hits/search/shared/SearchActionMenu.js +17 -0
  36. package/components/routes/home/HomeSettings.test.d.ts +1 -0
  37. package/components/routes/home/HomeSettings.test.js +44 -0
  38. package/components/routes/home/ViewCard.js +1 -1
  39. package/components/routes/overviews/markdownExtendedTokenProvider.test.d.ts +1 -0
  40. package/components/routes/overviews/markdownExtendedTokenProvider.test.js +50 -0
  41. package/components/routes/views/ViewComposer.js +32 -6
  42. package/locales/en/translation.json +6 -3
  43. package/locales/fr/translation.json +6 -3
  44. package/models/entities/generated/Column.d.ts +7 -0
  45. package/models/entities/generated/Host.d.ts +1 -0
  46. package/models/entities/generated/Settings.d.ts +4 -0
  47. package/package.json +2 -1
  48. package/rest/AxiosClient.test.d.ts +1 -0
  49. package/rest/AxiosClient.test.js +184 -0
  50. package/utils/stringUtils.d.ts +1 -0
  51. package/utils/stringUtils.js +7 -0
  52. package/utils/stringUtils.test.js +21 -1
  53. package/utils/utils.test.js +119 -2
  54. package/components/routes/hits/search/LayoutSettings.js +0 -24
  55. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/GridColumnsProvider.test.d.ts} +0 -0
  56. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.d.ts +0 -0
  57. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.js +0 -0
  58. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.test.js +0 -0
  59. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.d.ts +0 -0
  60. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.js +0 -0
  61. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.d.ts +0 -0
  62. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.js +0 -0
  63. /package/components/{routes/hits/search → elements/hit}/grid/HitRow.js +0 -0
  64. /package/components/routes/hits/search/{BundleParentMenu.d.ts → shared/BundleParentMenu.d.ts} +0 -0
  65. /package/components/routes/hits/search/{BundleParentMenu.js → shared/BundleParentMenu.js} +0 -0
  66. /package/components/routes/hits/search/{LayoutSettings.d.ts → shared/LayoutSettings.d.ts} +0 -0
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import { describe, expect, it } from 'vitest';
5
+ import VSBoxContent from './VSBoxContent';
6
+ describe('VSBoxContent', () => {
7
+ it('renders children inside a div with data-vsbox-content attribute', () => {
8
+ render(_jsx(VSBoxContent, { children: _jsx("span", { children: "hello" }) }));
9
+ const el = document.querySelector('[data-vsbox-content]');
10
+ expect(el).toBeInTheDocument();
11
+ expect(el).toHaveAttribute('data-vsbox-content', 'true');
12
+ });
13
+ it('renders children correctly', () => {
14
+ render(_jsx(VSBoxContent, { children: _jsx("span", { id: "child", children: "content" }) }));
15
+ expect(screen.getByTestId('child')).toBeInTheDocument();
16
+ });
17
+ it('passes additional MUI Box props through', () => {
18
+ const { container } = render(_jsx(VSBoxContent, { sx: { display: 'flex' }, children: _jsx("span", { children: "content" }) }));
19
+ expect(container.firstChild).toBeInTheDocument();
20
+ });
21
+ it('renders multiple children', () => {
22
+ render(_jsxs(VSBoxContent, { children: [_jsx("span", { id: "a", children: "A" }), _jsx("span", { id: "b", children: "B" })] }));
23
+ expect(screen.getByTestId('a')).toBeInTheDocument();
24
+ expect(screen.getByTestId('b')).toBeInTheDocument();
25
+ });
26
+ });
@@ -0,0 +1,55 @@
1
+ /// <reference types="vitest" />
2
+ import { describe, expect, it } from 'vitest';
3
+ import WordLexer from './word/WordLexer';
4
+ describe('WordLexer', () => {
5
+ const lexer = new WordLexer();
6
+ describe('parse', () => {
7
+ it('parses a single word', () => {
8
+ const result = lexer.parse('hello');
9
+ expect(result.tokens).toHaveLength(2); // word + eop
10
+ expect(result.tokens[0].type).toBe('word');
11
+ expect(result.tokens[0].value).toBe('hello');
12
+ });
13
+ it('parses two words separated by a space', () => {
14
+ const result = lexer.parse('hello world');
15
+ const nonEop = result.tokens.filter(t => t.type !== 'eop');
16
+ expect(nonEop.some(t => t.type === 'word' && t.value === 'hello')).toBe(true);
17
+ expect(nonEop.some(t => t.type === 'word' && t.value === 'world')).toBe(true);
18
+ });
19
+ it('always ends with an eop token', () => {
20
+ const result = lexer.parse('foo bar');
21
+ const last = result.tokens[result.tokens.length - 1];
22
+ expect(last.type).toBe('eop');
23
+ expect(last.value).toBe('');
24
+ });
25
+ it('returns an eop-only token list for empty input', () => {
26
+ const result = lexer.parse('');
27
+ expect(result.tokens).toHaveLength(1);
28
+ expect(result.tokens[0].type).toBe('eop');
29
+ });
30
+ it('sets cursor correctly', () => {
31
+ const result = lexer.parse('hello', 3);
32
+ expect(result.cursor).toBe(3);
33
+ });
34
+ it('sets current token for cursor in the middle of a word', () => {
35
+ const result = lexer.parse('hello', 3);
36
+ expect(result.current?.type).toBe('word');
37
+ });
38
+ it('provides suggest token for current cursor position', () => {
39
+ const result = lexer.parse('hello world', 7);
40
+ expect(result.suggest).toBeDefined();
41
+ expect(result.suggest.token).toBeDefined();
42
+ });
43
+ it('sets startIndex/endIndex correctly on word tokens', () => {
44
+ const result = lexer.parse('hi');
45
+ const wordToken = result.tokens.find(t => t.type === 'word');
46
+ expect(wordToken.startIndex).toBe(0);
47
+ expect(wordToken.endIndex).toBe(1);
48
+ });
49
+ it('whitespace tokens have correct type', () => {
50
+ const result = lexer.parse('a b');
51
+ const wsToken = result.tokens.find(t => t.type === 'whitespace');
52
+ expect(wsToken).toBeDefined();
53
+ });
54
+ });
55
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,8 @@
1
1
  import { type FC, type SetStateAction } from 'react';
2
2
  declare const ColumnHeader: FC<{
3
- width: string;
3
+ width: number;
4
4
  col: string;
5
+ colSource?: string[];
5
6
  setColumns: (val: SetStateAction<string[]>) => void;
6
7
  onMouseDown: (col: string, event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
7
8
  }>;
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useSortable } from '@dnd-kit/sortable';
3
3
  import { CSS } from '@dnd-kit/utilities';
4
- import { DragIndicator, Remove } from '@mui/icons-material';
4
+ import { DragIndicator, JoinFull, Remove } from '@mui/icons-material';
5
5
  import { Box, IconButton, Stack, TableCell, Tooltip, useTheme } from '@mui/material';
6
6
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
7
7
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
8
8
  import { useContext } from 'react';
9
- const ColumnHeader = ({ col, width, setColumns, onMouseDown }) => {
9
+ const ColumnHeader = ({ col, width, colSource, setColumns, onMouseDown }) => {
10
10
  const theme = useTheme();
11
11
  const { config } = useContext(ApiConfigContext);
12
12
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: col });
@@ -15,7 +15,7 @@ const ColumnHeader = ({ col, width, setColumns, onMouseDown }) => {
15
15
  borderRightColor: 'divider',
16
16
  py: 0.5,
17
17
  position: 'relative'
18
- }, style: { transform: CSS.Translate.toString(transform), transition }, children: _jsxs(Stack, { className: `col-${col.replaceAll('.', '-')}`, direction: "row", spacing: 1, alignItems: "center", sx: [{ minWidth: '220px' }, !!width ? { width, maxWidth: width } : { maxWidth: '300px' }], children: [_jsx(Tooltip, { title: config.indexes.hit[col].description, children: _jsx("span", { children: col }) }), _jsx(FlexOne, {}), _jsx(IconButton, { size: "small", sx: { fontSize: '1rem' }, onClick: () => setColumns(_columns => _columns.filter(_col => _col !== col)), children: _jsx(Remove, { fontSize: "inherit" }) }), _jsx(DragIndicator, { fontSize: "small", sx: { cursor: 'grab' }, ...attributes, ...listeners }), _jsx(Box, { sx: {
18
+ }, style: { transform: CSS.Translate.toString(transform), transition }, children: _jsxs(Stack, { className: `col-${col.replaceAll('.', '-')}`, direction: "row", spacing: 1, alignItems: "center", sx: [{ minWidth: '220px' }, !!width ? { width, maxWidth: width } : { maxWidth: '300px' }], children: [_jsx(Tooltip, { title: config.indexes.hit[col].description, children: _jsx("span", { children: col }) }), colSource?.length > 1 && (_jsx(Tooltip, { title: colSource.join(', '), children: _jsx(JoinFull, { fontSize: "small" }) })), _jsx(FlexOne, {}), _jsx(IconButton, { size: "small", sx: { fontSize: '1rem' }, onClick: () => setColumns(_columns => _columns.filter(_col => _col !== col)), children: _jsx(Remove, { fontSize: "inherit" }) }), _jsx(DragIndicator, { fontSize: "small", sx: { cursor: 'grab' }, ...attributes, ...listeners }), _jsx(Box, { sx: {
19
19
  position: 'absolute',
20
20
  top: theme.spacing(0.75),
21
21
  bottom: theme.spacing(0.75),
@@ -3,7 +3,7 @@ declare const _default: import("react").NamedExoticComponent<{
3
3
  hit: Hit;
4
4
  analyticIds: Record<string, string>;
5
5
  columns: string[];
6
- columnWidths: Record<string, string>;
6
+ columnWidths: Record<string, number>;
7
7
  collapseMainColumn: boolean;
8
8
  onClick: (e: React.MouseEvent<HTMLDivElement>, hit: Hit) => void;
9
9
  }>;
@@ -0,0 +1,12 @@
1
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
3
+ import React, { type PropsWithChildren } from 'react';
4
+ declare const HitTable: ({ query, items, refreshItems, ContextMenu, contextMenuProps, onItemClick }: {
5
+ query: string;
6
+ items?: WithMetadata<Hit>[];
7
+ refreshItems?: (query: string, append?: boolean) => void;
8
+ ContextMenu?: React.FC<PropsWithChildren<object>>;
9
+ contextMenuProps?: object;
10
+ onItemClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>, hit: Hit) => void;
11
+ }) => import("react/jsx-runtime").JSX.Element;
12
+ export default HitTable;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DndContext, KeyboardSensor, PointerSensor, pointerWithin, useSensor, useSensors } from '@dnd-kit/core';
3
+ import { arrayMove, SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
4
+ import { FormatIndentDecrease, FormatIndentIncrease, Search } from '@mui/icons-material';
5
+ import { IconButton, Stack, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material';
6
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
7
+ import { GridColumnsContext } from '@cccsaurora/howler-ui/components/app/providers/GridColumnsProvider';
8
+ import ColumnHeader from '@cccsaurora/howler-ui/components/elements/hit/grid/ColumnHeader';
9
+ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
10
+ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
11
+ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
12
+ import HitRow from './HitRow';
13
+ const HitTable = ({ query, items, refreshItems, ContextMenu, contextMenuProps, onItemClick }) => {
14
+ const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
15
+ const { getMatchingAnalytic } = useMatchers();
16
+ const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
17
+ const [analyticIds, setAnalyticIds] = useState({});
18
+ const { columns, columnWidths, columnSources, setColumnWidth, setColumns, isReady } = useContext(GridColumnsContext);
19
+ const resizingCol = useRef();
20
+ useEffect(() => {
21
+ items?.forEach(hit => {
22
+ if (!analyticIds[hit.howler.analytic]) {
23
+ getMatchingAnalytic(hit).then(_analytic => {
24
+ if (_analytic) {
25
+ setAnalyticIds(_analyticIds => ({ ..._analyticIds, [hit.howler.analytic]: _analytic.analytic_id }));
26
+ }
27
+ });
28
+ }
29
+ });
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [analyticIds, items]);
32
+ const onMouseMove = useCallback((event) => {
33
+ event.stopPropagation();
34
+ event.preventDefault();
35
+ const { col, width } = resizingCol.current;
36
+ const newWidth = width + event.movementX;
37
+ document.querySelectorAll(`.col-${col.replaceAll('.', '-')}`).forEach(el => {
38
+ el.style.maxWidth = newWidth + 'px';
39
+ el.style.width = newWidth + 'px';
40
+ });
41
+ resizingCol.current.width = newWidth;
42
+ }, []);
43
+ const onMouseUp = useCallback(() => {
44
+ const { col, width, element } = resizingCol.current;
45
+ if (isReady) {
46
+ setColumnWidth(col, Math.round(width));
47
+ }
48
+ element.style.width = null;
49
+ element.style.maxWidth = null;
50
+ document.querySelectorAll(`.col-${col.replaceAll('.', '-')}`).forEach(el => {
51
+ el.style.maxWidth = null;
52
+ el.style.width = null;
53
+ });
54
+ window.removeEventListener('mousemove', onMouseMove);
55
+ window.removeEventListener('mouseup', onMouseUp);
56
+ }, [onMouseMove, setColumnWidth, isReady]);
57
+ const onMouseDown = useCallback((col, event) => {
58
+ event.stopPropagation();
59
+ event.preventDefault();
60
+ const element = event.target.parentElement;
61
+ const rect = element.getBoundingClientRect();
62
+ resizingCol.current = { col, width: rect.width, element };
63
+ window.addEventListener('mousemove', onMouseMove);
64
+ window.addEventListener('mouseup', onMouseUp);
65
+ }, [onMouseMove, onMouseUp]);
66
+ const handleDragEnd = useCallback((event) => {
67
+ const { active, over } = event;
68
+ if (over && active.id !== over.id) {
69
+ const oldIndex = (columns ?? []).findIndex(entry => entry === active.id);
70
+ const newIndex = (columns ?? []).findIndex(entry => entry === over.id);
71
+ if (isReady) {
72
+ setColumns(arrayMove(columns, oldIndex, newIndex));
73
+ }
74
+ }
75
+ }, [columns, setColumns, isReady]);
76
+ const tableContent = (_jsxs(_Fragment, { children: [items?.map(hit => (_jsx(HitRow, { hit: hit, analyticIds: analyticIds, columns: columns, columnWidths: columnWidths, collapseMainColumn: collapseMainColumn, onClick: onItemClick ? onItemClick : (_ev, _hit) => null }, hit.howler.id))), refreshItems && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length + 2, children: _jsx(Stack, { alignItems: "center", justifyContent: "center", py: 0.5, px: 1, children: _jsx(IconButton, { onClick: () => refreshItems(query, true), children: _jsx(Search, {}) }) }) }) }))] }));
77
+ return (_jsxs(Table, { sx: { '& td,th': { px: 1, py: 0.25, whiteSpace: 'nowrap' } }, children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: {
78
+ borderRight: 'thin solid',
79
+ borderRightColor: 'divider'
80
+ }, children: _jsx(IconButton, { onClick: () => setCollapseMainColumn(!collapseMainColumn), children: collapseMainColumn ? (_jsx(FormatIndentIncrease, { fontSize: "small" })) : (_jsx(FormatIndentDecrease, { fontSize: "small" })) }) }), _jsx(DndContext, { sensors: sensors, collisionDetection: pointerWithin, onDragEnd: handleDragEnd, children: _jsx(SortableContext, { items: columns, children: columns.map(col => (_jsx(ColumnHeader, { col: col, width: columnWidths[col], colSource: columnSources[col], onMouseDown: onMouseDown, setColumns: isReady ? setColumns : () => null }, col))) }) }), _jsx(TableCell, { sx: { width: '100%' } })] }) }), ContextMenu ? (_jsx(ContextMenu, { ...contextMenuProps, children: tableContent })) : (_jsx(TableBody, { children: tableContent }))] }));
81
+ };
82
+ export default HitTable;
@@ -0,0 +1,8 @@
1
+ export type HowlerViewLayoutType = 'list' | 'grid' | null;
2
+ declare const LayoutToggle: ({ displayType, setDisplayType, size, allowNullValue }: {
3
+ displayType: HowlerViewLayoutType;
4
+ setDisplayType: (type: HowlerViewLayoutType) => void;
5
+ size?: "small" | "medium" | "large";
6
+ allowNullValue?: boolean;
7
+ }) => import("react/jsx-runtime").JSX.Element;
8
+ export default LayoutToggle;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { List, TableChart } from '@mui/icons-material';
3
+ import { ToggleButton, ToggleButtonGroup } from '@mui/material';
4
+ const LayoutToggle = ({ displayType, setDisplayType, size, allowNullValue = false }) => {
5
+ return (_jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => {
6
+ if (!value && !allowNullValue) {
7
+ return;
8
+ }
9
+ setDisplayType(value);
10
+ }, size: size ?? 'small', children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, { fontSize: size ?? 'medium' }) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, { fontSize: size ?? 'medium' }) })] }));
11
+ };
12
+ export default LayoutToggle;
@@ -5,6 +5,9 @@ interface ViewTitleProps {
5
5
  query?: string;
6
6
  sort?: string;
7
7
  span?: string;
8
+ settings?: {
9
+ display?: string;
10
+ };
8
11
  }
9
12
  export declare const ViewTitle: FC<ViewTitleProps>;
10
13
  export {};
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDownward, ArrowUpward, Language, Lock, Person } from '@mui/icons-material';
2
+ import { ArrowDownward, ArrowUpward, Language, List, Lock, Person, TableChart } from '@mui/icons-material';
3
3
  import { Chip, Stack, Tooltip, Typography } from '@mui/material';
4
4
  import { useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { convertLuceneToDate } from '@cccsaurora/howler-ui/utils/utils';
7
- export const ViewTitle = ({ title, type, query, sort, span }) => {
7
+ export const ViewTitle = ({ title, type, query, sort, span, settings }) => {
8
8
  const { t } = useTranslation();
9
9
  const spanLabel = useMemo(() => {
10
10
  if (!span) {
@@ -21,5 +21,8 @@ export const ViewTitle = ({ title, type, query, sort, span }) => {
21
21
  readonly: _jsx(Lock, { fontSize: "small" }),
22
22
  global: _jsx(Language, { fontSize: "small" }),
23
23
  personal: _jsx(Person, { fontSize: "small" })
24
- }[type] }), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, { size: "small", label: spanLabel })] }))] }));
24
+ }[type] }), settings?.display && (_jsx(Tooltip, { title: t(`page.settings.local.hits.display_type.${settings.display}`), children: {
25
+ list: _jsx(List, { fontSize: "small" }),
26
+ grid: _jsx(TableChart, { fontSize: "small" })
27
+ }[settings.display] })), _jsx(Typography, { variant: "body1", children: t(title) })] }), _jsx(Typography, { variant: "caption", children: _jsx("code", { children: query }) }), (sort || span) && (_jsxs(Stack, { direction: "row", sx: { mt: 1 }, spacing: 1, children: [sort?.split(',').map(_sort => (_jsx(Chip, { size: "small", label: _sort.split(' ')[0], icon: _sort.endsWith('desc') ? _jsx(ArrowDownward, {}) : _jsx(ArrowUpward, {}) }, _sort.split(' ')[0]))), spanLabel && _jsx(Chip, { size: "small", label: spanLabel })] }))] }));
25
28
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ /// <reference types="vitest" />
2
+ import { renderHook } from '@testing-library/react';
3
+ import { describe, expect, it } from 'vitest';
4
+ import useMyTheme from './useMyTheme';
5
+ describe('useMyTheme', () => {
6
+ it('returns a theme configuration object', () => {
7
+ const { result } = renderHook(() => useMyTheme());
8
+ expect(result.current).toBeDefined();
9
+ expect(typeof result.current).toBe('object');
10
+ });
11
+ it('contains a dark palette entry', () => {
12
+ const { result } = renderHook(() => useMyTheme());
13
+ expect(result.current.palette?.dark).toBeDefined();
14
+ });
15
+ it('contains a light palette entry', () => {
16
+ const { result } = renderHook(() => useMyTheme());
17
+ expect(result.current.palette?.light).toBeDefined();
18
+ });
19
+ it('returns the same reference on re-render (stable return value)', () => {
20
+ const { result, rerender } = renderHook(() => useMyTheme());
21
+ const first = result.current;
22
+ rerender();
23
+ expect(result.current).toBe(first);
24
+ });
25
+ it('dark palette includes primary and secondary colours', () => {
26
+ const { result } = renderHook(() => useMyTheme());
27
+ expect(result.current.palette.dark.primary).toHaveProperty('main');
28
+ expect(result.current.palette.dark.secondary).toHaveProperty('main');
29
+ });
30
+ it('light palette includes primary and secondary colours', () => {
31
+ const { result } = renderHook(() => useMyTheme());
32
+ expect(result.current.palette.light.primary).toHaveProperty('main');
33
+ expect(result.current.palette.light.secondary).toHaveProperty('main');
34
+ });
35
+ });
@@ -0,0 +1,34 @@
1
+ /// <reference types="vitest" />
2
+ import { describe, expect, it } from 'vitest';
3
+ import TOKEN_PROVIDER from './eqlTokenProvider';
4
+ describe('EQL token provider', () => {
5
+ it('exports a non-null object', () => {
6
+ expect(TOKEN_PROVIDER).toBeDefined();
7
+ expect(TOKEN_PROVIDER).not.toBeNull();
8
+ expect(typeof TOKEN_PROVIDER).toBe('object');
9
+ });
10
+ it('has a "root" tokenizer entry', () => {
11
+ expect(TOKEN_PROVIDER.tokenizer).toBeDefined();
12
+ expect(TOKEN_PROVIDER.tokenizer.root).toBeDefined();
13
+ expect(Array.isArray(TOKEN_PROVIDER.tokenizer.root)).toBe(true);
14
+ });
15
+ it('uses "invalid" as the defaultToken', () => {
16
+ expect(TOKEN_PROVIDER.defaultToken).toBe('invalid');
17
+ });
18
+ it('defines the expected EQL keywords', () => {
19
+ const kws = TOKEN_PROVIDER.keywords;
20
+ expect(kws).toContain('where');
21
+ expect(kws).toContain('not');
22
+ expect(kws).toContain('in');
23
+ expect(kws).toContain('head');
24
+ expect(kws).toContain('tail');
25
+ });
26
+ it('defines boolean operators', () => {
27
+ const booleans = TOKEN_PROVIDER.booleans;
28
+ expect(booleans).toContain('and');
29
+ expect(booleans).toContain('or');
30
+ });
31
+ it('has includeLF set to true', () => {
32
+ expect(TOKEN_PROVIDER.includeLF).toBe(true);
33
+ });
34
+ });
@@ -0,0 +1,34 @@
1
+ /// <reference types="vitest" />
2
+ import { describe, expect, it } from 'vitest';
3
+ import TOKEN_PROVIDER from './luceneTokenProvider';
4
+ describe('Lucene token provider', () => {
5
+ it('exports a non-null object', () => {
6
+ expect(TOKEN_PROVIDER).toBeDefined();
7
+ expect(TOKEN_PROVIDER).not.toBeNull();
8
+ expect(typeof TOKEN_PROVIDER).toBe('object');
9
+ });
10
+ it('has a "root" tokenizer entry', () => {
11
+ expect(TOKEN_PROVIDER.tokenizer).toBeDefined();
12
+ expect(TOKEN_PROVIDER.tokenizer.root).toBeDefined();
13
+ expect(Array.isArray(TOKEN_PROVIDER.tokenizer.root)).toBe(true);
14
+ });
15
+ it('defines the expected operators', () => {
16
+ const ops = TOKEN_PROVIDER.operators;
17
+ expect(ops).toContain('-');
18
+ expect(ops).toContain('&&');
19
+ expect(ops).toContain('||');
20
+ expect(ops).toContain(':');
21
+ });
22
+ it('defines the expected keywords (AND, OR, NOT)', () => {
23
+ const kws = TOKEN_PROVIDER.keywords;
24
+ expect(kws).toContain('AND');
25
+ expect(kws).toContain('OR');
26
+ expect(kws).toContain('NOT');
27
+ });
28
+ it('uses "default" as the defaultToken', () => {
29
+ expect(TOKEN_PROVIDER.defaultToken).toBe('default');
30
+ });
31
+ it('includes a string tokenizer state for quoted values', () => {
32
+ expect(TOKEN_PROVIDER.tokenizer).toHaveProperty('string');
33
+ });
34
+ });
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronLeft, Close, ManageSearch } from '@mui/icons-material';
3
3
  import { Box, Card, Checkbox, Collapse, Drawer, Fab, IconButton, Stack, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
4
+ import GridColumnsProvider from '@cccsaurora/howler-ui/components/app/providers/GridColumnsProvider';
4
5
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
5
6
  import HitSearchProvider, { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
6
7
  import ParameterProvider, { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
@@ -128,6 +129,6 @@ const HitBrowser = () => {
128
129
  }, children: _jsx(ChevronLeft, { sx: { transition: 'rotate 250ms', rotate: show ? '180deg' : '0deg' } }) }))] }));
129
130
  };
130
131
  const HitBrowserProvider = () => {
131
- return (_jsx(ParameterProvider, { children: _jsx(HitSearchProvider, { children: _jsx(HitBrowser, {}) }) }));
132
+ return (_jsx(ParameterProvider, { children: _jsx(HitSearchProvider, { children: _jsx(GridColumnsProvider, { children: _jsx(HitBrowser, {}) }) }) }));
132
133
  };
133
134
  export default HitBrowserProvider;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Close, ErrorOutline, SavedSearch, Terminal } from '@mui/icons-material';
3
- import { Box, IconButton, LinearProgress, Stack, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
2
+ import { ErrorOutline } from '@mui/icons-material';
3
+ import { Box, LinearProgress, Stack, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material';
4
4
  import { grey } from '@mui/material/colors';
5
5
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
6
6
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
@@ -17,21 +17,20 @@ import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search
17
17
  import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
18
18
  import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
19
19
  import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
20
+ import HitContextMenu from '@cccsaurora/howler-ui/components/elements/hit/HitContextMenu';
20
21
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
21
22
  import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelection';
22
23
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
23
24
  import React, { memo, useCallback, useEffect, useMemo } from 'react';
24
25
  import { isMobile } from 'react-device-detect';
25
26
  import { useTranslation } from 'react-i18next';
26
- import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
27
+ import { useLocation, useParams } from 'react-router-dom';
27
28
  import { useContextSelector } from 'use-context-selector';
28
29
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
29
- import BundleParentMenu from './BundleParentMenu';
30
30
  import { BundleScroller } from './BundleScroller';
31
- import HitContextMenu from './HitContextMenu';
32
31
  import HitQuery from './HitQuery';
33
- import LayoutSettings from './LayoutSettings';
34
32
  import QuerySettings from './QuerySettings';
33
+ import SearchActionMenu from './shared/SearchActionMenu';
35
34
  const Item = memo(({ hit, onClick }) => {
36
35
  const theme = useTheme();
37
36
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
@@ -80,7 +79,6 @@ const Item = memo(({ hit, onClick }) => {
80
79
  const SearchPane = () => {
81
80
  const { t } = useTranslation();
82
81
  const location = useLocation();
83
- const navigate = useNavigate();
84
82
  const routeParams = useParams();
85
83
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
86
84
  const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
@@ -117,7 +115,7 @@ const SearchPane = () => {
117
115
  ], onClick: () => {
118
116
  clearSelectedHits(bundleHit.howler.id);
119
117
  setSelected(bundleHit.howler.id);
120
- }, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), bundleHit?.howler.bundles.length > 0 && _jsx(BundleParentMenu, { bundle: bundleHit }), bundleHit && (_jsx(Tooltip, { title: t('hit.bundle.close'), children: _jsx(IconButton, { size: "small", onClick: () => navigate('/search'), children: _jsx(Close, {}) }) })), _jsx(Tooltip, { title: t('route.views.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/views/create?query=${query}`, children: _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('route.actions.save'), children: _jsx(IconButton, { component: Link, disabled: !query, to: `/action/execute?query=${query}`, children: _jsx(Terminal, {}) }) }), _jsx(LayoutSettings, {})] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 989 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
118
+ }, children: _jsx(HitBanner, { hit: bundleHit, layout: HitLayout.DENSE, useListener: true }) }) }) }) })), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", children: t('hit.search.prompt') }), error && (_jsx(Tooltip, { title: `${t('route.advanced.error')}: ${error}`, children: _jsx(ErrorOutline, { fontSize: "small", color: "error" }) })), _jsx(FlexOne, {}), _jsx(SearchActionMenu, { query: query })] })] }), _jsxs(VSBoxHeader, { ml: -3, mr: -3, px: 2, pb: 1, sx: { zIndex: 989 }, children: [_jsxs(Stack, { sx: { pt: 1 }, children: [_jsxs(Stack, { sx: { position: 'relative', flex: 1 }, children: [_jsx(HitQuery, { searching: searching, triggerSearch: triggerSearch }), searching && (_jsx(LinearProgress, { sx: theme => ({
121
119
  position: 'absolute',
122
120
  left: 0,
123
121
  right: 0,
@@ -1,6 +1,6 @@
1
1
  import { createElement as _createElement } from "react";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { ArrowDropDown, Delete, Edit, Language, Lock, OpenInNew, Person, Refresh, SavedSearch, SelectAll } from '@mui/icons-material';
3
+ import { ArrowDropDown, Delete, Edit, Language, Lock, OpenInNew, Person, Refresh, SavedSearch, SelectAll, Warning } from '@mui/icons-material';
4
4
  import { Autocomplete, Chip, CircularProgress, IconButton, Stack, TextField, Tooltip, Typography } from '@mui/material';
5
5
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
6
6
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
@@ -21,6 +21,7 @@ const ViewLink = ({ id, viewId }) => {
21
21
  const removeView = useContextSelector(ParameterContext, ctx => ctx.removeView);
22
22
  const setParamView = useContextSelector(ParameterContext, ctx => ctx.setView);
23
23
  const search = useContextSelector(HitSearchContext, ctx => ctx.search);
24
+ const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
24
25
  const [loading, setLoading] = useState(true);
25
26
  const [view, setView] = useState(null);
26
27
  useEffect(() => {
@@ -60,6 +61,6 @@ const ViewLink = ({ id, viewId }) => {
60
61
  readonly: _jsx(Lock, { fontSize: "small", "aria-label": t(`route.views.manager.${view.type}`) }),
61
62
  global: _jsx(Language, { fontSize: "small", "aria-label": t(`route.views.manager.${view.type}`) }),
62
63
  personal: _jsx(Person, { fontSize: "small", "aria-label": t(`route.views.manager.${view.type}`) })
63
- }[view.type] }), label: _jsx(Tooltip, { title: view.query, children: _jsx(Typography, { role: "link", sx: { color: 'text.primary' }, variant: "body2", component: Link, to: `/views/${view.view_id}/edit`, "aria-label": `${t(view.title)} - ${view.query ?? t('unknown')}`, children: t(view.title) }) }), onDelete: () => removeView(viewId), children: _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Tooltip, { title: view ? t('route.views.edit') : t('route.views.create'), children: _jsx(IconButton, { "aria-label": view ? t('route.views.edit') : t('route.views.create'), size: "small", component: Link, disabled: (!view && !query) || span?.endsWith('custom'), to: viewUrl, role: "link", children: view ? _jsx(Edit, { fontSize: "small" }) : _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('view.refresh'), children: _jsx(IconButton, { size: "small", onClick: () => search(query), "aria-label": t('view.refresh'), children: _jsx(Refresh, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('view.open'), children: _jsx(IconButton, { size: "small", component: Link, to: `/search?query=${view.query}`, "aria-label": t('view.open'), role: "link", children: _jsx(OpenInNew, { fontSize: "small" }) }) })] }) }));
64
+ }[view.type] }), label: _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Tooltip, { title: view.query, children: _jsx(Typography, { role: "link", sx: { color: 'text.primary' }, variant: "body2", component: Link, to: `/views/${view.view_id}/edit`, "aria-label": `${t(view.title)} - ${view.query ?? t('unknown')}`, children: t(view.title) }) }), view.settings?.display === 'grid' && displayType !== 'grid' && (_jsx(Tooltip, { title: t('view.display.grid.inactive_warning'), children: _jsx(Warning, { fontSize: "small", color: "warning" }) }))] }), onDelete: () => removeView(viewId), children: _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", children: [_jsx(Tooltip, { title: view ? t('route.views.edit') : t('route.views.create'), children: _jsx(IconButton, { "aria-label": view ? t('route.views.edit') : t('route.views.create'), size: "small", component: Link, disabled: (!view && !query) || span?.endsWith('custom'), to: viewUrl, role: "link", children: view ? _jsx(Edit, { fontSize: "small" }) : _jsx(SavedSearch, {}) }) }), _jsx(Tooltip, { title: t('view.refresh'), children: _jsx(IconButton, { size: "small", onClick: () => search(query), "aria-label": t('view.refresh'), children: _jsx(Refresh, { fontSize: "small" }) }) }), _jsx(Tooltip, { title: t('view.open'), children: _jsx(IconButton, { size: "small", component: Link, to: `/search?query=${view.query}`, "aria-label": t('view.open'), role: "link", children: _jsx(OpenInNew, { fontSize: "small" }) }) })] }) }));
64
65
  };
65
66
  export default memo(ViewLink);