@cccsaurora/howler-ui 2.19.0-cases.1138 → 2.19.0-cases.1192

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 (80) 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.d.ts +1 -0
  5. package/components/app/providers/GridColumnsProvider.test.js +255 -0
  6. package/components/app/providers/RecordSearchProvider.js +10 -18
  7. package/components/elements/addons/buttons/CustomIconButton.test.d.ts +1 -0
  8. package/components/elements/addons/buttons/CustomIconButton.test.js +81 -0
  9. package/components/elements/addons/layout/FlexVertical.test.d.ts +1 -0
  10. package/components/elements/addons/layout/FlexVertical.test.js +32 -0
  11. package/components/elements/addons/layout/vsbox/VSBoxContent.test.d.ts +1 -0
  12. package/components/elements/addons/layout/vsbox/VSBoxContent.test.js +26 -0
  13. package/components/elements/addons/search/phrase/WordLexer.test.d.ts +1 -0
  14. package/components/elements/addons/search/phrase/WordLexer.test.js +55 -0
  15. package/components/elements/hit/HitLabels.test.d.ts +1 -0
  16. package/components/elements/hit/HitLabels.test.js +77 -0
  17. package/components/elements/hit/HitLinks.d.ts +5 -4
  18. package/components/elements/hit/HitLinks.js +28 -10
  19. package/components/elements/hit/HitNotebooks.js +23 -22
  20. package/components/elements/hit/NotebookTooltip.d.ts +3 -0
  21. package/components/elements/hit/NotebookTooltip.js +8 -0
  22. package/components/elements/hit/PivotTooltip.d.ts +7 -0
  23. package/components/elements/hit/PivotTooltip.js +9 -0
  24. package/components/elements/hit/RelatedLinkTooltip.d.ts +6 -0
  25. package/components/elements/hit/RelatedLinkTooltip.js +8 -0
  26. package/components/elements/hit/ResolvePivotUrl.d.ts +4 -0
  27. package/components/elements/hit/ResolvePivotUrl.js +22 -0
  28. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.d.ts +2 -1
  29. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.js +3 -3
  30. package/components/{routes/hits/search → elements/hit}/grid/RecordRow.d.ts +1 -1
  31. package/components/elements/hit/grid/RecordTable.d.ts +13 -0
  32. package/components/elements/hit/grid/RecordTable.js +84 -0
  33. package/components/elements/hit/related/PivotLink.d.ts +3 -0
  34. package/components/elements/hit/related/PivotLink.js +5 -2
  35. package/components/elements/hit/related/RelatedLink.d.ts +1 -0
  36. package/components/elements/hit/related/RelatedLink.js +18 -15
  37. package/components/elements/view/LayoutToggle.d.ts +8 -0
  38. package/components/elements/view/LayoutToggle.js +12 -0
  39. package/components/elements/view/ViewTitle.d.ts +3 -0
  40. package/components/elements/view/ViewTitle.js +6 -3
  41. package/components/hooks/useMyTheme.test.d.ts +1 -0
  42. package/components/hooks/useMyTheme.test.js +35 -0
  43. package/components/routes/advanced/eqlTokenProvider.test.d.ts +1 -0
  44. package/components/routes/advanced/eqlTokenProvider.test.js +34 -0
  45. package/components/routes/advanced/luceneTokenProvider.test.d.ts +1 -0
  46. package/components/routes/advanced/luceneTokenProvider.test.js +34 -0
  47. package/components/routes/hits/search/RecordBrowser.js +4 -3
  48. package/components/routes/hits/search/SearchPane.js +4 -5
  49. package/components/routes/hits/search/ViewLink.js +3 -2
  50. package/components/routes/hits/search/grid/RecordGrid.d.ts +3 -0
  51. package/components/routes/hits/search/grid/RecordGrid.js +64 -0
  52. package/components/routes/hits/search/shared/LayoutSettings.js +25 -0
  53. package/components/routes/hits/search/shared/SearchActionMenu.d.ts +4 -0
  54. package/components/routes/hits/search/shared/SearchActionMenu.js +11 -0
  55. package/components/routes/home/HomeSettings.test.d.ts +1 -0
  56. package/components/routes/home/HomeSettings.test.js +44 -0
  57. package/components/routes/overviews/markdownExtendedTokenProvider.test.d.ts +1 -0
  58. package/components/routes/overviews/markdownExtendedTokenProvider.test.js +50 -0
  59. package/components/routes/views/ViewComposer.js +32 -6
  60. package/locales/en/translation.json +11 -3
  61. package/locales/fr/translation.json +11 -3
  62. package/models/entities/generated/Column.d.ts +7 -0
  63. package/models/entities/generated/Host.d.ts +1 -0
  64. package/models/entities/generated/Settings.d.ts +4 -0
  65. package/package.json +10 -12
  66. package/rest/AxiosClient.test.d.ts +1 -0
  67. package/rest/AxiosClient.test.js +184 -0
  68. package/utils/stringUtils.d.ts +1 -0
  69. package/utils/stringUtils.js +7 -0
  70. package/utils/stringUtils.test.js +21 -1
  71. package/utils/utils.test.js +119 -2
  72. package/components/routes/hits/search/LayoutSettings.js +0 -24
  73. package/components/routes/hits/search/grid/HitGrid.d.ts +0 -3
  74. package/components/routes/hits/search/grid/HitGrid.js +0 -134
  75. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.d.ts +0 -0
  76. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.js +0 -0
  77. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.d.ts +0 -0
  78. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.js +0 -0
  79. /package/components/{routes/hits/search → elements/hit}/grid/RecordRow.js +0 -0
  80. /package/components/routes/hits/search/{LayoutSettings.d.ts → shared/LayoutSettings.d.ts} +0 -0
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /// <reference types="vitest" />
3
+ import { render, screen } from '@testing-library/react';
4
+ import { setupReactRouterMock } from '@cccsaurora/howler-ui/tests/mocks';
5
+ import { vi } from 'vitest';
6
+ setupReactRouterMock();
7
+ import CustomIconButton from './CustomIconButton';
8
+ describe('CustomIconButton', () => {
9
+ afterAll(() => vi.resetModules());
10
+ describe('rendering', () => {
11
+ it('renders a button element', () => {
12
+ render(_jsx(CustomIconButton, { "aria-label": "test", children: "icon" }));
13
+ expect(screen.getByRole('button', { name: 'test' })).toBeInTheDocument();
14
+ });
15
+ it('renders children inside the button', () => {
16
+ render(_jsx(CustomIconButton, { children: "\u2605" }));
17
+ expect(screen.getByText('★')).toBeInTheDocument();
18
+ });
19
+ it('renders with MUI IconButton classes', () => {
20
+ render(_jsx(CustomIconButton, { children: "i" }));
21
+ expect(screen.getByRole('button')).toHaveClass('MuiIconButton-root');
22
+ });
23
+ });
24
+ describe('disabled state', () => {
25
+ it('is disabled when disabled prop is true', () => {
26
+ render(_jsx(CustomIconButton, { disabled: true, children: "icon" }));
27
+ expect(screen.getByRole('button')).toBeDisabled();
28
+ });
29
+ it('is disabled when progress is truthy', () => {
30
+ render(_jsx(CustomIconButton, { progress: true, children: "icon" }));
31
+ expect(screen.getByRole('button')).toBeDisabled();
32
+ });
33
+ it('is NOT disabled when progress is set but clickableWithProgress=true', () => {
34
+ render(_jsx(CustomIconButton, { progress: true, clickableWithProgress: true, children: "icon" }));
35
+ expect(screen.getByRole('button')).not.toBeDisabled();
36
+ });
37
+ });
38
+ describe('progress indicator', () => {
39
+ it('renders a CircularProgress when progress is truthy', () => {
40
+ render(_jsx(CustomIconButton, { progress: true, children: "icon" }));
41
+ // CircularProgress renders an svg with role="progressbar"
42
+ expect(document.querySelector('circle')).toBeInTheDocument();
43
+ });
44
+ it('does not render CircularProgress when progress is falsy', () => {
45
+ render(_jsx(CustomIconButton, { children: "icon" }));
46
+ expect(document.querySelector('[role="progressbar"]')).not.toBeInTheDocument();
47
+ });
48
+ });
49
+ describe('tooltip', () => {
50
+ it('wraps the button in a Tooltip when tooltip prop is provided', () => {
51
+ render(_jsx(CustomIconButton, { tooltip: "Save", children: "icon" }));
52
+ // The button is wrapped in a <span> when a Tooltip is used
53
+ const span = screen.getByRole('button').closest('span');
54
+ expect(span).toBeInTheDocument();
55
+ });
56
+ it('does not add a surrounding span when no tooltip is given', () => {
57
+ render(_jsx(CustomIconButton, { children: "icon" }));
58
+ const button = screen.getByRole('button');
59
+ // Without tooltip the immediate parent should NOT be a span wrapper
60
+ expect(button.parentElement?.tagName).not.toBe('SPAN');
61
+ });
62
+ });
63
+ describe('route link', () => {
64
+ it('wraps the button in a Link when route prop is provided', () => {
65
+ render(_jsx(CustomIconButton, { route: "/hits", children: "icon" }));
66
+ expect(screen.getByRole('link')).toBeInTheDocument();
67
+ });
68
+ it('does not render a link when no route is given', () => {
69
+ render(_jsx(CustomIconButton, { children: "icon" }));
70
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
71
+ });
72
+ });
73
+ describe('href link', () => {
74
+ it('wraps the button in an anchor tag when href is provided', () => {
75
+ render(_jsx(CustomIconButton, { href: "https://example.com", children: "icon" }));
76
+ const anchor = screen.getByRole('link');
77
+ expect(anchor).toBeInTheDocument();
78
+ expect(anchor).toHaveAttribute('href', 'https://example.com');
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,32 @@
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 FlexVertical from './FlexVertical';
6
+ describe('FlexVertical', () => {
7
+ it('renders a div with display:flex and flex-direction:column', () => {
8
+ const { container } = render(_jsx(FlexVertical, { children: _jsx("span", { children: "child" }) }));
9
+ const div = container.firstChild;
10
+ expect(div).toBeInTheDocument();
11
+ expect(div).toHaveStyle({ display: 'flex', flexDirection: 'column' });
12
+ });
13
+ it('renders children inside the flex container', () => {
14
+ render(_jsx(FlexVertical, { children: _jsx("span", { id: "inner", children: "hello" }) }));
15
+ expect(screen.getByTestId('inner')).toBeInTheDocument();
16
+ });
17
+ it('renders multiple children', () => {
18
+ render(_jsxs(FlexVertical, { children: [_jsx("span", { id: "a", children: "A" }), _jsx("span", { id: "b", children: "B" })] }));
19
+ expect(screen.getByTestId('a')).toBeInTheDocument();
20
+ expect(screen.getByTestId('b')).toBeInTheDocument();
21
+ });
22
+ it('applies the default flex value of 1', () => {
23
+ const { container } = render(_jsx(FlexVertical, { children: _jsx("span", { children: "child" }) }));
24
+ const div = container.firstChild;
25
+ expect(div).toHaveStyle({ flex: 1 });
26
+ });
27
+ it('applies a custom flex value when provided', () => {
28
+ const { container } = render(_jsx(FlexVertical, { flex: 2, children: _jsx("span", { children: "child" }) }));
29
+ const div = container.firstChild;
30
+ expect(div).toHaveStyle({ flex: 2 });
31
+ });
32
+ });
@@ -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 {};
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import HitLabels from './HitLabels';
5
+ /**
6
+ * HitLabels.test.tsx
7
+ *
8
+ * Purpose:
9
+ * This test suite validates the core behavior of the HitLabels component.
10
+ * - Correct transformation of hit.howler.labels into UI chips
11
+ * - Proper rendering of all label values
12
+ * - Conditional display of the edit button based on readOnly mode
13
+ * - Safe rendering when no labels are present
14
+ */
15
+ vi.mock('components/hooks/useMyApi', () => ({
16
+ default: () => ({
17
+ dispatchApi: vi.fn()
18
+ })
19
+ }));
20
+ vi.mock('components/app/providers/RecordProvider', () => ({
21
+ RecordContext: {}
22
+ }));
23
+ vi.mock('use-context-selector', () => ({
24
+ useContextSelector: () => vi.fn()
25
+ }));
26
+ vi.mock('react-i18next', () => ({
27
+ useTranslation: () => ({
28
+ t: (key) => key
29
+ }),
30
+ Trans: ({ i18nKey }) => i18nKey
31
+ }));
32
+ vi.mock('utils/constants', () => ({
33
+ LABEL_TYPES: {
34
+ security: { icon: null, color: '#ff0000' },
35
+ system: { icon: null, color: '#00ff00' }
36
+ }
37
+ }));
38
+ const baseHit = {
39
+ howler: {
40
+ id: 'hit-1',
41
+ labels: {
42
+ security: ['critical', 'high'],
43
+ system: ['info']
44
+ }
45
+ }
46
+ };
47
+ describe('HitLabels', () => {
48
+ it('renders all labels from hit.howler.labels', () => {
49
+ render(_jsx(HitLabels, { hit: baseHit, readOnly: true }));
50
+ expect(screen.getByText('critical')).toBeInTheDocument();
51
+ expect(screen.getByText('high')).toBeInTheDocument();
52
+ expect(screen.getByText('info')).toBeInTheDocument();
53
+ });
54
+ it('renders correct number of labels', () => {
55
+ render(_jsx(HitLabels, { hit: baseHit, readOnly: true }));
56
+ const labels = screen.getAllByText(/critical|high|info/);
57
+ expect(labels.length).toBe(3);
58
+ });
59
+ it('shows edit button when not readOnly', () => {
60
+ render(_jsx(HitLabels, { hit: baseHit, readOnly: false }));
61
+ expect(screen.getByRole('button')).toBeInTheDocument();
62
+ });
63
+ it('hides edit button when readOnly', () => {
64
+ render(_jsx(HitLabels, { hit: baseHit, readOnly: true }));
65
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
66
+ });
67
+ it('renders without crashing when labels are empty', () => {
68
+ const emptyHit = {
69
+ howler: {
70
+ id: 'hit-1',
71
+ labels: {}
72
+ }
73
+ };
74
+ const { container } = render(_jsx(HitLabels, { hit: emptyHit, readOnly: true }));
75
+ expect(container).toBeInTheDocument();
76
+ });
77
+ });
@@ -1,10 +1,11 @@
1
+ import type { FC } from 'react';
1
2
  import type { Analytic } from '@cccsaurora/howler-ui/models/entities/generated/Analytic';
2
3
  import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
3
4
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
4
- import type { FC } from 'react';
5
- declare const HitLinks: FC<{
5
+ interface HitLinksProps {
6
6
  hit?: Hit;
7
7
  analytic?: Analytic;
8
- dossiers: Dossier[];
9
- }>;
8
+ dossiers?: Dossier[];
9
+ }
10
+ declare const HitLinks: FC<HitLinksProps>;
10
11
  export default HitLinks;
@@ -1,19 +1,37 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Grid, gridClasses } from '@mui/material';
3
+ import { sortBy, uniqBy } from 'lodash-es';
4
+ import { useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
3
6
  import HitNotebooks from '@cccsaurora/howler-ui/components/elements/hit/HitNotebooks';
7
+ import ResolvePivotUrl from '@cccsaurora/howler-ui/components/elements/hit/ResolvePivotUrl';
4
8
  import PivotLink from '@cccsaurora/howler-ui/components/elements/hit/related/PivotLink';
5
9
  import RelatedLink from '@cccsaurora/howler-ui/components/elements/hit/related/RelatedLink';
6
- import { sortBy, uniqBy } from 'lodash-es';
7
- import { useTranslation } from 'react-i18next';
8
10
  const HitLinks = ({ hit, analytic, dossiers = [] }) => {
9
11
  const { i18n } = useTranslation();
10
- return ((hit?.howler?.links?.length > 0 ||
11
- analytic?.notebooks?.length > 0 ||
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
- uniqBy(hit.howler.links, 'href')
14
- .slice(0, 3)
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
- // eslint-disable-next-line react/no-array-index-key
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 }) }))] })));
12
+ const displayLinks = useMemo(() => uniqBy(hit?.howler?.links ?? [], 'href').slice(0, 3), [hit?.howler?.links]);
13
+ const displayPivots = useMemo(() => {
14
+ const flattened = dossiers.flatMap(dossier => (dossier.pivots ?? []).map(pivot => {
15
+ const pivotUrl = pivot.format === 'link' ? ResolvePivotUrl(pivot, hit) : undefined;
16
+ return {
17
+ pivot,
18
+ dossier,
19
+ resolvedUrl: pivotUrl || `/dossier/${dossier.dossier_id}`
20
+ };
21
+ }));
22
+ return sortBy(flattened, item => item.pivot.label?.[i18n.language]);
23
+ }, [dossiers, i18n.language, hit]);
24
+ const hasNotebooks = (analytic?.notebooks?.length ?? 0) > 0;
25
+ if (displayLinks.length === 0 && displayPivots.length === 0 && !hasNotebooks) {
26
+ return null;
27
+ }
28
+ return (_jsxs(Grid, { container: true, spacing: 1, pr: 2, sx: { [`& .${gridClasses.item}`]: { display: 'flex' } }, children: [displayLinks
29
+ .filter(link => !!link.href)
30
+ .map(link => {
31
+ const safeTitle = link.title ?? link.href;
32
+ return (_jsx(Grid, { item: true, children: _jsx(RelatedLink, { compact: true, title: safeTitle, href: link.href, target: "_blank", rel: "noopener noreferrer" }) }, link.href));
33
+ }), displayPivots.map(({ pivot, dossier, resolvedUrl }) => {
34
+ return (_jsx(Grid, { item: true, children: _jsx(PivotLink, { pivot: pivot, hit: hit, dossier: dossier, resolvedUrl: resolvedUrl, compact: true }) }, `${dossier.dossier_id}-${pivot.value}`));
35
+ }), hasNotebooks && (_jsx(Grid, { item: true, children: _jsx(HitNotebooks, { analytic: analytic, hit: hit, compact: true }) }))] }));
18
36
  };
19
37
  export default HitLinks;
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Avatar, Backdrop, Box, Button, Chip, CircularProgress, ClickAwayListener, Fade, MenuItem, Paper, Popper, Stack, TextField } from '@mui/material';
2
+ import { Avatar, Backdrop, Box, Button, Chip, CircularProgress, ClickAwayListener, Fade, MenuItem, Paper, Popper, Stack, TextField, Tooltip } from '@mui/material';
3
3
  import api from '@cccsaurora/howler-ui/api';
4
4
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
5
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
6
+ import NotebookTooltip from '@cccsaurora/howler-ui/components/elements/hit/NotebookTooltip';
6
7
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
7
8
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
8
9
  import { memo, useCallback, useContext, useEffect, useState } from 'react';
@@ -124,26 +125,26 @@ const HitNotebooks = ({ analytic, selectedNotebook, hit, compact }) => {
124
125
  MenuProps: {
125
126
  style: { zIndex: 35001 }
126
127
  }
127
- }, defaultValue: selectedNotebook ? analytic?.notebooks.find(n => n.name === selectedNotebook).value : '', children: [_jsx(MenuItem, { disabled: true, value: "", children: _jsx("em", { children: t('hit.notebook.select') }) }), analytic?.notebooks.sort(safeStringPropertyCompare('detection')).map(e => (_jsx(MenuItem, { value: e.value, children: _jsxs(Stack, { direction: 'row', sx: { width: '100%' }, children: [e.name, e.detection && (_jsxs(_Fragment, { children: [_jsx(Box, { flex: 1 }), _jsx(Chip, { label: e.detection, size: "small" })] }))] }) }, e.value)))] }), _jsx(Button, { variant: "outlined", disabled: loadedNotebook.name === '' || envs.length === 0, color: "success", onClick: () => checkJupyhub(), children: t('hit.notebook.goTo') })] })] }) })) }), _jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: handleToggle, sx: [
128
- theme => ({
129
- cursor: 'pointer',
130
- backgroundColor: 'transparent',
131
- transition: theme.transitions.create(['border-color']),
132
- '&:hover': { borderColor: 'primary.main', '& a': { textDecoration: 'underline' } },
133
- '& > div': {
134
- height: '100%'
135
- },
136
- '& a': { textDecoration: 'none', color: 'text.primary' }
137
- }),
138
- !compact && { border: 'thin solid', borderColor: 'transparent' }
139
- ], children: _jsxs(Stack, { direction: "row", p: 1, spacing: 1, alignItems: "center", children: [_jsx(Avatar, { variant: "rounded", src: '/images/jupyter_notebook_file_icon.png', sx: [
140
- theme => ({
141
- width: theme.spacing(3),
142
- height: theme.spacing(3),
143
- '& img': {
144
- objectFit: 'contain'
145
- }
146
- })
147
- ], children: 'jupyter_notebook_file_icon.png' }), t('hit.notebook.tooltip')] }) })] }) })));
128
+ }, defaultValue: selectedNotebook ? analytic?.notebooks.find(n => n.name === selectedNotebook).value : '', children: [_jsx(MenuItem, { disabled: true, value: "", children: _jsx("em", { children: t('hit.notebook.select') }) }), analytic?.notebooks.sort(safeStringPropertyCompare('detection')).map(e => (_jsx(MenuItem, { value: e.value, children: _jsxs(Stack, { direction: 'row', sx: { width: '100%' }, children: [e.name, e.detection && (_jsxs(_Fragment, { children: [_jsx(Box, { flex: 1 }), _jsx(Chip, { label: e.detection, size: "small" })] }))] }) }, e.value)))] }), _jsx(Button, { variant: "outlined", disabled: loadedNotebook.name === '' || envs.length === 0, color: "success", onClick: () => checkJupyhub(), children: t('hit.notebook.goTo') })] })] }) })) }), _jsx(Tooltip, { title: _jsx(NotebookTooltip, {}), children: _jsx("span", { children: _jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: handleToggle, sx: [
129
+ theme => ({
130
+ cursor: 'pointer',
131
+ backgroundColor: 'transparent',
132
+ transition: theme.transitions.create(['border-color']),
133
+ '&:hover': { borderColor: 'primary.main', '& a': { textDecoration: 'underline' } },
134
+ '& > div': {
135
+ height: '100%'
136
+ },
137
+ '& a': { textDecoration: 'none', color: 'text.primary' }
138
+ }),
139
+ !compact && { border: 'thin solid', borderColor: 'transparent' }
140
+ ], children: _jsxs(Stack, { direction: "row", p: 1, spacing: 1, alignItems: "center", children: [_jsx(Avatar, { variant: "rounded", src: '/images/jupyter_notebook_file_icon.png', sx: [
141
+ theme => ({
142
+ width: theme.spacing(3),
143
+ height: theme.spacing(3),
144
+ '& img': {
145
+ objectFit: 'contain'
146
+ }
147
+ })
148
+ ], children: 'jupyter_notebook_file_icon.png' }), t('hit.notebook.tooltip')] }) }) }) })] }) })));
148
149
  };
149
150
  export default memo(HitNotebooks);
@@ -0,0 +1,3 @@
1
+ import type { FC } from 'react';
2
+ declare const NotebookTooltip: FC;
3
+ export default NotebookTooltip;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Typography } from '@mui/material';
3
+ import { useTranslation } from 'react-i18next';
4
+ const NotebookTooltip = () => {
5
+ const { t } = useTranslation();
6
+ return (_jsxs(Box, { sx: { maxWidth: 300 }, children: [_jsx(Typography, { variant: "subtitle2", children: t('notebook.title') }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('notebook.opens.panel') })] }));
7
+ };
8
+ export default NotebookTooltip;
@@ -0,0 +1,7 @@
1
+ import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
2
+ import type { FC } from 'react';
3
+ declare const PivotTooltip: FC<{
4
+ dossier: Dossier;
5
+ resolvedUrl: string;
6
+ }>;
7
+ export default PivotTooltip;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Link, Typography } from '@mui/material';
3
+ import { useTranslation } from 'react-i18next';
4
+ const PivotTooltip = ({ dossier, resolvedUrl }) => {
5
+ const dossierUrl = `/dossiers/${dossier.dossier_id}/edit?tab=leads&query=${encodeURIComponent(dossier.query)}`;
6
+ const { t } = useTranslation();
7
+ return (_jsxs(Box, { sx: { maxWidth: 300 }, children: [_jsx(Typography, { variant: "subtitle2", children: dossier.title }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: dossier.owner }), _jsx(Typography, { variant: "body2", sx: { mt: 2 }, children: dossier.leads?.[0]?.content?.slice(0, 120) ?? t('pivot.description.none') }), _jsxs(Box, { sx: { mt: 2, wordBreak: 'break-all' }, children: [_jsx(Typography, { variant: "caption", display: "block", children: t('pivot.url') }), _jsx(Link, { href: resolvedUrl, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: resolvedUrl })] }), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Link, { href: dossierUrl, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: t('pivot.dossier.open') }) })] }));
8
+ };
9
+ export default PivotTooltip;
@@ -0,0 +1,6 @@
1
+ import type { FC } from 'react';
2
+ declare const RelatedLinkTooltip: FC<{
3
+ title: string;
4
+ href: string;
5
+ }>;
6
+ export default RelatedLinkTooltip;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Link, Typography } from '@mui/material';
3
+ import { useTranslation } from 'react-i18next';
4
+ const RelatedLinkTooltip = ({ title, href }) => {
5
+ const { t } = useTranslation();
6
+ return (_jsxs(Box, { sx: { maxWidth: 300 }, children: [_jsx(Typography, { variant: "subtitle2", children: title }), _jsx(Box, { sx: { mt: 1, wordBreak: 'break-all' }, children: _jsx(Link, { href: href, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: href }) }), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Link, { href: href, target: "_blank", rel: "noopener noreferrer", underline: "hover", children: t('hit.header.link') }) })] }));
7
+ };
8
+ export default RelatedLinkTooltip;
@@ -0,0 +1,4 @@
1
+ import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
2
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
+ declare const resolvePivotUrl: (pivot: NonNullable<Dossier["pivots"]>[number], currentHit?: Hit) => string;
4
+ export default resolvePivotUrl;
@@ -0,0 +1,22 @@
1
+ import Handlebars from 'handlebars';
2
+ import { flattenDeep } from '@cccsaurora/howler-ui/utils/utils';
3
+ const resolvePivotUrl = (pivot, currentHit) => {
4
+ const flatHit = flattenDeep(currentHit ?? {});
5
+ const templateObject = Object.fromEntries((pivot.mappings ?? []).map(mapping => {
6
+ const value = mapping.field === 'custom'
7
+ ? mapping.custom_value
8
+ : Array.isArray(flatHit[mapping.field])
9
+ ? flatHit[mapping.field][0]
10
+ : flatHit[mapping.field];
11
+ return [mapping.key, value];
12
+ }));
13
+ try {
14
+ return Handlebars.compile(pivot.value)(templateObject);
15
+ }
16
+ catch (e) {
17
+ // eslint-disable-next-line no-console
18
+ console.error(`Failed to compile pivot template for value "${pivot.value}":`, e);
19
+ return pivot.value;
20
+ }
21
+ };
22
+ export default resolvePivotUrl;
@@ -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),
@@ -4,7 +4,7 @@ declare const _default: import("react").NamedExoticComponent<{
4
4
  record: Hit | Event;
5
5
  analyticIds: Record<string, string>;
6
6
  columns: string[];
7
- columnWidths: Record<string, string>;
7
+ columnWidths: Record<string, number>;
8
8
  collapseMainColumn: boolean;
9
9
  onClick: (e: React.MouseEvent<HTMLDivElement>, record: Hit | Event) => void;
10
10
  }>;
@@ -0,0 +1,13 @@
1
+ import type { Event } from '@cccsaurora/howler-ui/models/entities/generated/Event';
2
+ import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
+ import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
4
+ import React, { type PropsWithChildren } from 'react';
5
+ declare const RecordTable: ({ query, items, refreshItems, ContextMenu, contextMenuProps, onItemClick }: {
6
+ query: string;
7
+ items?: WithMetadata<Hit | Event>[];
8
+ refreshItems?: (query: string, append?: boolean) => void;
9
+ ContextMenu?: React.FC<PropsWithChildren<object>>;
10
+ contextMenuProps?: object;
11
+ onItemClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>, record: Hit | Event) => void;
12
+ }) => import("react/jsx-runtime").JSX.Element;
13
+ export default RecordTable;