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

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 (45) 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/hit/HitContextMenu.test.d.ts +1 -0
  6. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.d.ts +2 -1
  7. package/components/{routes/hits/search → elements/hit}/grid/ColumnHeader.js +3 -3
  8. package/components/{routes/hits/search → elements/hit}/grid/HitRow.d.ts +1 -1
  9. package/components/elements/hit/grid/HitTable.d.ts +12 -0
  10. package/components/elements/hit/grid/HitTable.js +82 -0
  11. package/components/elements/view/LayoutToggle.d.ts +8 -0
  12. package/components/elements/view/LayoutToggle.js +12 -0
  13. package/components/elements/view/ViewTitle.d.ts +3 -0
  14. package/components/elements/view/ViewTitle.js +6 -3
  15. package/components/routes/hits/search/HitBrowser.js +2 -1
  16. package/components/routes/hits/search/SearchPane.js +6 -8
  17. package/components/routes/hits/search/ViewLink.js +3 -2
  18. package/components/routes/hits/search/grid/HitGrid.js +17 -85
  19. package/components/routes/hits/search/shared/LayoutSettings.js +25 -0
  20. package/components/routes/hits/search/shared/SearchActionMenu.d.ts +4 -0
  21. package/components/routes/hits/search/shared/SearchActionMenu.js +17 -0
  22. package/components/routes/home/ViewCard.js +1 -1
  23. package/components/routes/views/ViewComposer.js +32 -6
  24. package/locales/en/translation.json +6 -3
  25. package/locales/fr/translation.json +6 -3
  26. package/models/entities/generated/Column.d.ts +7 -0
  27. package/models/entities/generated/Host.d.ts +1 -0
  28. package/models/entities/generated/Settings.d.ts +4 -0
  29. package/package.json +2 -1
  30. package/utils/stringUtils.d.ts +1 -0
  31. package/utils/stringUtils.js +7 -0
  32. package/utils/stringUtils.test.js +21 -1
  33. package/components/routes/hits/search/LayoutSettings.js +0 -24
  34. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/GridColumnsProvider.test.d.ts} +0 -0
  35. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.d.ts +0 -0
  36. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.js +0 -0
  37. /package/components/{routes/hits/search → elements/hit}/HitContextMenu.test.js +0 -0
  38. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.d.ts +0 -0
  39. /package/components/{routes/hits/search → elements/hit}/grid/AddColumnModal.js +0 -0
  40. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.d.ts +0 -0
  41. /package/components/{routes/hits/search → elements/hit}/grid/EnhancedCell.js +0 -0
  42. /package/components/{routes/hits/search → elements/hit}/grid/HitRow.js +0 -0
  43. /package/components/routes/hits/search/{BundleParentMenu.d.ts → shared/BundleParentMenu.d.ts} +0 -0
  44. /package/components/routes/hits/search/{BundleParentMenu.js → shared/BundleParentMenu.js} +0 -0
  45. /package/components/routes/hits/search/{LayoutSettings.d.ts → shared/LayoutSettings.d.ts} +0 -0
@@ -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
  };
@@ -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);
@@ -1,51 +1,34 @@
1
1
  import { jsx as _jsx, 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, Info, List, Search, TableChart } from '@mui/icons-material';
5
- import { IconButton, LinearProgress, Paper, Stack, Table, TableBody, TableCell, TableHead, TableRow, ToggleButton, ToggleButtonGroup, Typography, useTheme } from '@mui/material';
6
- import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
2
+ import { Info } from '@mui/icons-material';
3
+ import { LinearProgress, Paper, Stack, TableBody, Typography, useTheme } from '@mui/material';
4
+ import { GridColumnsContext } from '@cccsaurora/howler-ui/components/app/providers/GridColumnsProvider';
7
5
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
8
6
  import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
9
7
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
10
8
  import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
11
9
  import DevelopmentBanner from '@cccsaurora/howler-ui/components/elements/display/features/DevelopmentBanner';
10
+ import AddColumnModal from '@cccsaurora/howler-ui/components/elements/hit/grid/AddColumnModal';
11
+ import HitTable from '@cccsaurora/howler-ui/components/elements/hit/grid/HitTable';
12
+ import HitContextMenu from '@cccsaurora/howler-ui/components/elements/hit/HitContextMenu';
12
13
  import useHitSelection from '@cccsaurora/howler-ui/components/hooks/useHitSelection';
13
- import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
14
14
  import { uniq } from 'lodash-es';
15
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
+ import { useCallback, useContext, useMemo } from 'react';
16
16
  import { useTranslation } from 'react-i18next';
17
17
  import { useContextSelector } from 'use-context-selector';
18
- import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
19
- import HitContextMenu from '../HitContextMenu';
20
18
  import HitQuery from '../HitQuery';
21
19
  import QuerySettings from '../QuerySettings';
22
- import AddColumnModal from './AddColumnModal';
23
- import ColumnHeader from './ColumnHeader';
24
- import HitRow from './HitRow';
20
+ import SearchActionMenu from '../shared/SearchActionMenu';
25
21
  const HitGrid = () => {
26
22
  const { t } = useTranslation();
27
23
  const theme = useTheme();
28
- const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
29
24
  const { onClick } = useHitSelection();
30
- const { getMatchingAnalytic } = useMatchers();
31
25
  const search = useContextSelector(HitSearchContext, ctx => ctx.search);
32
- const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
33
- const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
34
26
  const response = useContextSelector(HitSearchContext, ctx => ctx.response);
35
27
  const searching = useContextSelector(HitSearchContext, ctx => ctx.searching);
36
28
  const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
37
29
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
38
30
  const selected = useContextSelector(ParameterContext, ctx => ctx.selected);
39
- const [collapseMainColumn, setCollapseMainColumn] = useMyLocalStorageItem(StorageKey.GRID_COLLAPSE_COLUMN, false);
40
- const [columns, setColumns] = useMyLocalStorageItem(StorageKey.GRID_COLUMNS, [
41
- 'howler.outline.threat',
42
- 'howler.outline.target',
43
- 'howler.outline.indicators',
44
- 'howler.outline.summary'
45
- ]);
46
- const [columnWidths, setColumnWidths] = useMyLocalStorageItem(StorageKey.GRID_COLUMN_WIDTHS, {});
47
- const [analyticIds, setAnalyticIds] = useState({});
48
- const resizingCol = useRef();
31
+ const { columns, setColumns, isReady } = useContext(GridColumnsContext);
49
32
  const showSelectBar = useMemo(() => {
50
33
  if (selectedHits.length > 1) {
51
34
  return true;
@@ -55,60 +38,12 @@ const HitGrid = () => {
55
38
  }
56
39
  return false;
57
40
  }, [selected, selectedHits]);
58
- useEffect(() => {
59
- response?.items.forEach(hit => {
60
- if (!analyticIds[hit.howler.analytic]) {
61
- getMatchingAnalytic(hit).then(_analytic => setAnalyticIds(_analyticIds => ({ ..._analyticIds, [hit.howler.analytic]: _analytic.analytic_id })));
62
- }
63
- });
64
- // eslint-disable-next-line react-hooks/exhaustive-deps
65
- }, [analyticIds, response]);
66
- const onMouseMove = useCallback((event) => {
67
- event.stopPropagation();
68
- event.preventDefault();
69
- const [col, element] = resizingCol.current;
70
- const rect = element.getBoundingClientRect();
71
- document.querySelectorAll(`.col-${col.replaceAll('.', '-')}`).forEach(el => {
72
- el.style.maxWidth = rect.width + event.movementX + 'px';
73
- el.style.width = rect.width + event.movementX + 'px';
74
- });
75
- }, []);
76
- const onMouseUp = useCallback(() => {
77
- const [col, element] = resizingCol.current;
78
- setColumnWidths({
79
- ...columnWidths,
80
- [col]: element.style.width
81
- });
82
- element.style.width = null;
83
- element.style.maxWidth = null;
84
- document.querySelectorAll(`.col-${col.replaceAll('.', '-')}`).forEach(el => {
85
- el.style.maxWidth = null;
86
- el.style.width = null;
87
- });
88
- window.removeEventListener('mousemove', onMouseMove);
89
- window.removeEventListener('mouseup', onMouseUp);
90
- }, [columnWidths, onMouseMove, setColumnWidths]);
91
- const onMouseDown = useCallback((col, event) => {
92
- event.stopPropagation();
93
- event.preventDefault();
94
- resizingCol.current = [col, event.target.parentElement];
95
- window.addEventListener('mousemove', onMouseMove);
96
- window.addEventListener('mouseup', onMouseUp);
97
- }, [onMouseMove, onMouseUp]);
98
41
  const onScroll = useCallback((event) => {
99
42
  const target = event.target;
100
43
  if (target.scrollHeight - target.scrollTop === target.clientHeight) {
101
44
  search(query, true);
102
45
  }
103
46
  }, [query, search]);
104
- const handleDragEnd = useCallback((event) => {
105
- const { active, over } = event;
106
- if (over && active.id !== over.id) {
107
- const oldIndex = (columns ?? []).findIndex(entry => entry === active.id);
108
- const newIndex = (columns ?? []).findIndex(entry => entry === over.id);
109
- setColumns(arrayMove(columns, oldIndex, newIndex));
110
- }
111
- }, [columns, setColumns]);
112
47
  const getSelectedId = useCallback((event) => {
113
48
  const target = event.target;
114
49
  const selectedElement = target.closest('[id]');
@@ -117,16 +52,13 @@ const HitGrid = () => {
117
52
  }
118
53
  return selectedElement.id;
119
54
  }, []);
120
- return (_jsxs(Stack, { spacing: 1, p: 2, width: "100%", sx: { overflow: 'hidden', height: `calc(100vh - ${theme.spacing(showSelectBar ? 13 : 8)})` }, children: [_jsx(DevelopmentBanner, {}), _jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5, textAlign: 'left' }, variant: "body2", children: t('hit.search.prompt') }), response && (_jsx(SearchTotal, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", offset: response.offset, pageLength: response.rows, total: response.total }))] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { position: "relative", flex: 1, children: [_jsx(HitQuery, { searching: searching, triggerSearch: search, compact: true }), searching && (_jsx(LinearProgress, { sx: {
121
- position: 'absolute',
122
- left: 0,
123
- right: 0,
124
- bottom: 0,
125
- borderBottomLeftRadius: theme.shape.borderRadius,
126
- borderBottomRightRadius: theme.shape.borderRadius
127
- } }))] }), _jsxs(ToggleButtonGroup, { exclusive: true, value: displayType, onChange: (__, value) => setDisplayType(value), size: "small", children: [_jsx(ToggleButton, { value: "list", children: _jsx(List, {}) }), _jsx(ToggleButton, { value: "grid", children: _jsx(TableChart, {}) })] })] }), _jsxs(Stack, { direction: "row", spacing: 1, width: "100%", alignItems: "center", children: [_jsx(QuerySettings, { boxSx: { flex: 1 } }), _jsx(AddColumnModal, { columns: columns, addColumn: key => setColumns(uniq([...columns, key])) })] }), _jsxs(Stack, { component: Paper, spacing: 1, width: "100%", height: "100%", sx: { overflow: 'auto', flex: 1 }, onScroll: onScroll, children: [_jsxs(Table, { sx: { '& td,th': { px: 1, py: 0.25, whiteSpace: 'nowrap' } }, children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: {
128
- borderRight: 'thin solid',
129
- borderRightColor: 'divider'
130
- }, 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], onMouseDown: onMouseDown, setColumns: setColumns }, col))) }) }), _jsx(TableCell, { sx: { width: '100%' } })] }) }), _jsxs(HitContextMenu, { Component: TableBody, getSelectedId: getSelectedId, children: [response?.items.map(hit => (_jsx(HitRow, { hit: hit, analyticIds: analyticIds, columns: columns, columnWidths: columnWidths, collapseMainColumn: collapseMainColumn, onClick: onClick }, hit.howler.id))), _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: () => search(query, true), children: _jsx(Search, {}) }) }) }) })] })] }), (response?.total ?? 0) < 1 && (_jsx(Stack, { direction: "row", spacing: 1, alignItems: "center", p: 1, justifyContent: "center", flex: 1, children: _jsxs(Typography, { variant: "h3", color: "text.secondary", display: "flex", flexDirection: "row", alignItems: "center", children: [_jsx(Info, { fontSize: "inherit", sx: { color: 'text.secondary', mr: 1 } }), _jsx("span", { children: t('app.list.empty') })] }) }))] })] }));
55
+ return (_jsxs(Stack, { spacing: 1, p: 2, width: "100%", sx: { overflow: 'hidden', height: `calc(100vh - ${theme.spacing(showSelectBar ? 13 : 8)})` }, children: [_jsx(DevelopmentBanner, {}), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", children: [_jsx(Typography, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5, textAlign: 'left' }, variant: "body2", children: t('hit.search.prompt') }), _jsx(SearchActionMenu, { query: query })] }), _jsx(Stack, { direction: "row", spacing: 1, children: _jsxs(Stack, { position: "relative", flex: 1, children: [_jsx(HitQuery, { searching: searching, triggerSearch: search, compact: true }), searching && (_jsx(LinearProgress, { sx: {
56
+ position: 'absolute',
57
+ left: 0,
58
+ right: 0,
59
+ bottom: 0,
60
+ borderBottomLeftRadius: theme.shape.borderRadius,
61
+ borderBottomRightRadius: theme.shape.borderRadius
62
+ } }))] }) }), _jsxs(Stack, { direction: "row", spacing: 1, width: "100%", alignItems: "center", children: [_jsx(QuerySettings, { boxSx: { flex: 1 } }), _jsx(AddColumnModal, { columns: columns, addColumn: key => isReady && setColumns(uniq([...columns, key])) })] }), response && (_jsx(SearchTotal, { sx: { color: 'text.secondary', fontSize: '0.9em', fontStyle: 'italic', mb: 0.5 }, variant: "body2", offset: response.offset, pageLength: response.rows, total: response.total })), _jsxs(Stack, { component: Paper, spacing: 1, width: "100%", height: "100%", sx: { overflow: 'auto', flex: 1 }, onScroll: onScroll, children: [_jsx(HitTable, { query: query, items: response?.items, refreshItems: search, ContextMenu: HitContextMenu, contextMenuProps: { Component: TableBody, getSelectedId: getSelectedId }, onItemClick: onClick }), (response?.total ?? 0) < 1 && (_jsx(Stack, { direction: "row", spacing: 1, alignItems: "center", p: 1, justifyContent: "center", flex: 1, children: _jsxs(Typography, { variant: "h3", color: "text.secondary", display: "flex", flexDirection: "row", alignItems: "center", children: [_jsx(Info, { fontSize: "inherit", sx: { color: 'text.secondary', mr: 1 } }), _jsx("span", { children: t('app.list.empty') })] }) }))] })] }));
131
63
  };
132
64
  export default HitGrid;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowDropDown, InfoOutlined, Settings, ViewComfy, ViewCompact, ViewModule } from '@mui/icons-material';
3
+ import { Checkbox, Divider, FormLabel, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material';
4
+ import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
5
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
6
+ import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
7
+ import LayoutToggle from '@cccsaurora/howler-ui/components/elements/view/LayoutToggle';
8
+ import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { useContextSelector } from 'use-context-selector';
11
+ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
12
+ const LayoutSettings = () => {
13
+ const { t } = useTranslation();
14
+ const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
15
+ const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
16
+ const [hitLayout, setHitLayout] = useMyLocalStorageItem(StorageKey.HIT_LAYOUT, false);
17
+ const [templateFieldCount, setTemplateFieldCount] = useMyLocalStorageItem(StorageKey.TEMPLATE_FIELD_COUNT, null);
18
+ return (_jsx(ChipPopper, { icon: _jsx(Tooltip, { title: t('search.layout.settings'), children: _jsx(Settings, {}) }), deleteIcon: _jsx(ArrowDropDown, {}), toggleOnDelete: true, disablePortal: false, slotProps: { chip: { size: 'medium', 'aria-label': t('search.layout.settings') } }, placement: "bottom-end", children: _jsxs(Stack, { spacing: 1, alignItems: "start", children: [_jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "display_type", children: t('page.settings.local.hits.display_type') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.display_type.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsx(LayoutToggle, { displayType: displayType, setDisplayType: setDisplayType }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "layout", children: t('page.settings.local.hits.layout') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.layout.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(ToggleButtonGroup, { exclusive: true, size: "small", value: hitLayout, onChange: (_, value) => setHitLayout(value), "aria-labelledby": "layout", children: [_jsx(ToggleButton, { value: HitLayout.DENSE, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewCompact, {}), _jsx("span", { children: t('page.settings.local.hits.layout.dense') })] }) }), _jsx(ToggleButton, { value: HitLayout.NORMAL, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewModule, {}), _jsx("span", { children: t('page.settings.local.hits.layout.normal') })] }) }), _jsx(ToggleButton, { value: HitLayout.COMFY, children: _jsxs(Stack, { direction: "row", spacing: 0.5, children: [_jsx(ViewComfy, {}), _jsx("span", { children: t('page.settings.local.hits.layout.comfy') })] }) })] }), _jsx(Divider, { flexItem: true }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", alignSelf: "stretch", children: [_jsx(FormLabel, { id: "field_count", children: t('page.settings.local.hits.field_count') }), _jsx("div", { style: { flex: 1 } }), _jsx(Tooltip, { title: t('page.settings.local.hits.field_count.description'), children: _jsx(InfoOutlined, { fontSize: "inherit" }) })] }), _jsxs(Stack, { direction: "row", spacing: 0.5, alignSelf: "stretch", children: [_jsx(Checkbox, { checked: templateFieldCount !== null, onChange: (_, checked) => setTemplateFieldCount(checked ? 3 : null), size: "small" }), _jsx(TextField, { type: "number", size: "small", disabled: templateFieldCount === null, value: templateFieldCount ?? 3, fullWidth: true, onChange: e => {
19
+ const val = parseInt(e.target.value);
20
+ if (!isNaN(val)) {
21
+ setTemplateFieldCount(Math.min(15, Math.max(0, val)));
22
+ }
23
+ }, inputProps: { min: 0, max: 15, 'aria-labelledby': 'field_count' } })] })] }) }));
24
+ };
25
+ export default LayoutSettings;
@@ -0,0 +1,4 @@
1
+ declare const SearchActionMenu: ({ query }: {
2
+ query: string;
3
+ }) => import("react/jsx-runtime").JSX.Element;
4
+ export default SearchActionMenu;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Close, SavedSearch, Terminal } from '@mui/icons-material';
3
+ import { IconButton, Stack, Tooltip } from '@mui/material';
4
+ import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Link, useNavigate, useParams } from 'react-router-dom';
7
+ import { useContextSelector } from 'use-context-selector';
8
+ import BundleParentMenu from './BundleParentMenu';
9
+ import LayoutSettings from './LayoutSettings';
10
+ const SearchActionMenu = ({ query }) => {
11
+ const { t } = useTranslation();
12
+ const navigate = useNavigate();
13
+ const routeParams = useParams();
14
+ const bundleHit = useContextSelector(HitContext, ctx => location.pathname.startsWith('/bundles') ? ctx.hits[routeParams.id] : null);
15
+ return (_jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [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, {})] }));
16
+ };
17
+ export default SearchActionMenu;
@@ -6,9 +6,9 @@ import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppLi
6
6
  import { useHitContextSelector } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
7
7
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
8
8
  import HitBanner from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
9
+ import HitContextMenu from '@cccsaurora/howler-ui/components/elements/hit/HitContextMenu';
9
10
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
10
11
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
11
- import HitContextMenu from '@cccsaurora/howler-ui/components/routes/hits/search/HitContextMenu';
12
12
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  import { Link, useNavigate } from 'react-router-dom';
@@ -1,25 +1,33 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useState } from 'react';
2
+ import { useCallback, useContext, useEffect, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { HelpOutline, Save } from '@mui/icons-material';
5
- import { Alert, Checkbox, CircularProgress, LinearProgress, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
4
+ import { HelpOutline, Save, Settings } from '@mui/icons-material';
5
+ import { Alert, Checkbox, CircularProgress, LinearProgress, Paper, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from '@mui/material';
6
6
  import api from '@cccsaurora/howler-ui/api';
7
7
  import AppListEmpty from '@cccsaurora/howler-ui/commons/components/display/AppListEmpty';
8
8
  import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
9
+ import { GridColumnsContext } from '@cccsaurora/howler-ui/components/app/providers/GridColumnsProvider';
9
10
  import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
11
+ import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
10
12
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
11
13
  import { ViewContext } from '@cccsaurora/howler-ui/components/app/providers/ViewProvider';
12
14
  import CustomButton from '@cccsaurora/howler-ui/components/elements/addons/buttons/CustomButton';
15
+ import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
13
16
  import FlexPort from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexPort';
14
17
  import VSBox from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBox';
15
18
  import VSBoxContent from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxContent';
16
19
  import VSBoxHeader from '@cccsaurora/howler-ui/components/elements/addons/layout/vsbox/VSBoxHeader';
17
20
  import SearchTotal from '@cccsaurora/howler-ui/components/elements/addons/search/SearchTotal';
21
+ import ChipPopper from '@cccsaurora/howler-ui/components/elements/display/ChipPopper';
22
+ import AddColumnModal from '@cccsaurora/howler-ui/components/elements/hit/grid/AddColumnModal';
23
+ import HitTable from '@cccsaurora/howler-ui/components/elements/hit/grid/HitTable';
18
24
  import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
19
25
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
26
+ import LayoutToggle, {} from '@cccsaurora/howler-ui/components/elements/view/LayoutToggle';
20
27
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
21
28
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
22
29
  import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
30
+ import { uniq } from 'lodash-es';
23
31
  import { useNavigate, useParams } from 'react-router-dom';
24
32
  import { useContextSelector } from 'use-context-selector';
25
33
  import { DEFAULT_QUERY, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
@@ -44,6 +52,7 @@ const ViewComposer = () => {
44
52
  const [title, setTitle] = useState('');
45
53
  const [type, setType] = useState('global');
46
54
  const [advanceOnTriage, setAdvanceOnTriage] = useState(false);
55
+ const { columns, setColumns, columnWidths, isReady } = useContext(GridColumnsContext);
47
56
  const query = useContextSelector(ParameterContext, ctx => ctx.query);
48
57
  const setQuery = useContextSelector(ParameterContext, ctx => ctx.setQuery);
49
58
  const sort = useContextSelector(ParameterContext, ctx => ctx.sort);
@@ -56,8 +65,14 @@ const ViewComposer = () => {
56
65
  const [searching, setSearching] = useState(false);
57
66
  const [error, setError] = useState(null);
58
67
  const [response, setResponse] = useState();
68
+ const displayType = useContextSelector(HitSearchContext, ctx => ctx.displayType);
69
+ const setDisplayType = useContextSelector(HitSearchContext, ctx => ctx.setDisplayType);
59
70
  const onSave = useCallback(async () => {
60
71
  setLoading(true);
72
+ const _columnData = columns.map(column => ({
73
+ field: column,
74
+ width: columnWidths[column] ?? null
75
+ }));
61
76
  try {
62
77
  if (!routeParams.id) {
63
78
  const newView = await addView({
@@ -67,7 +82,9 @@ const ViewComposer = () => {
67
82
  sort: sort || null,
68
83
  span: span || null,
69
84
  settings: {
70
- advance_on_triage: advanceOnTriage
85
+ advance_on_triage: advanceOnTriage,
86
+ display: displayType,
87
+ columns: displayType === 'grid' ? _columnData : null
71
88
  }
72
89
  });
73
90
  navigate(buildViewUrl(newView));
@@ -79,7 +96,11 @@ const ViewComposer = () => {
79
96
  query,
80
97
  sort,
81
98
  span,
82
- settings: { advance_on_triage: advanceOnTriage }
99
+ settings: {
100
+ advance_on_triage: advanceOnTriage,
101
+ display: displayType,
102
+ columns: displayType === 'grid' ? _columnData : null
103
+ }
83
104
  });
84
105
  }
85
106
  showSuccessMessage(t(routeParams.id ? 'route.views.update.success' : 'route.views.create.success'));
@@ -101,7 +122,10 @@ const ViewComposer = () => {
101
122
  sort,
102
123
  span,
103
124
  advanceOnTriage,
125
+ displayType,
126
+ columns,
104
127
  navigate,
128
+ columnWidths,
105
129
  editView,
106
130
  showErrorMessage
107
131
  ]);
@@ -154,6 +178,8 @@ const ViewComposer = () => {
154
178
  setTitle(viewToEdit.title);
155
179
  setAdvanceOnTriage(viewToEdit.settings?.advance_on_triage ?? false);
156
180
  setQuery(viewToEdit.query);
181
+ setDisplayType((viewToEdit.settings?.display ?? null));
182
+ setType(viewToEdit.type);
157
183
  if (viewToEdit.sort) {
158
184
  setSort(viewToEdit.sort);
159
185
  }
@@ -172,6 +198,6 @@ const ViewComposer = () => {
172
198
  fontSize: '0.9em',
173
199
  fontStyle: 'italic',
174
200
  mb: 0.5
175
- }), variant: "body2", children: t('hit.search.prompt') }), _jsx(HitQuery, { triggerSearch: search, searching: searching, onChange: (_query, isDirty) => setIsSearchDirty(isDirty) }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(HitSort, {}), _jsx(SearchSpan, { omitCustom: true }), _jsx("div", { style: { flex: 1 } }), _jsxs(Stack, { spacing: 1, direction: "row", alignItems: "center", sx: { flex: '0 !important', minWidth: '300px' }, children: [_jsx(Typography, { component: "span", children: t('view.settings.advance_on_triage') }), _jsx(Tooltip, { title: t('view.settings.advance_on_triage.description'), children: _jsx(HelpOutline, { sx: { fontSize: '16px' } }) }), _jsx(Checkbox, { size: "small", checked: advanceOnTriage, onChange: (_event, checked) => setAdvanceOnTriage(checked) })] })] }), response?.total ? (_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) })) : null, _jsx(LinearProgress, { sx: [!searching && { opacity: 0 }] })] }) }), _jsx(VSBoxContent, { children: _jsxs(Stack, { spacing: 1, children: [!response?.total && _jsx(AppListEmpty, {}), response?.items.map(hit => (_jsx(HitCard, { id: hit.howler.id, layout: HitLayout.DENSE }, hit.howler.id)))] }) })] }) }) }) }));
201
+ }), variant: "body2", children: t('hit.search.prompt') }), _jsx(Stack, { direction: "row", width: "100%", spacing: 2, alignItems: "flex-start", paddingBottom: 1, children: _jsxs(Stack, { direction: "column", width: "100%", spacing: 1, children: [_jsx(HitQuery, { triggerSearch: search, searching: searching, onChange: (_query, isDirty) => setIsSearchDirty(isDirty) }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(HitSort, {}), _jsx(SearchSpan, { omitCustom: true }), _jsx("div", { style: { flex: 1 } }), _jsx(ChipPopper, { label: _jsx(Typography, { variant: "body2", children: t('view.settings') }), deleteIcon: _jsx(Settings, {}), toggleOnDelete: true, slotProps: { chip: { size: 'small' } }, placement: "bottom-end", children: _jsxs(Stack, { direction: "column", spacing: 1, children: [_jsxs(Stack, { spacing: 1, direction: "row", alignItems: "center", sx: { flex: '0 !important', minWidth: '300px' }, children: [_jsx(Typography, { component: "span", children: t('view.settings.advance_on_triage') }), _jsx(Tooltip, { title: t('view.settings.advance_on_triage.description'), children: _jsx(HelpOutline, { sx: { fontSize: '16px' } }) }), _jsx(FlexOne, {}), _jsx(Checkbox, { size: "small", checked: advanceOnTriage, onChange: (_event, checked) => setAdvanceOnTriage(checked) })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", justifyContent: "space-between", children: [_jsx(Typography, { component: "span", children: t('view.settings.layout') }), _jsx(LayoutToggle, { displayType: displayType, setDisplayType: setDisplayType, size: "small", allowNullValue: true })] })] }) })] })] }) }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "flex-end", justifyContent: "space-between", children: [response && (_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) })), _jsx(FlexOne, {}), displayType === 'grid' && (_jsx(AddColumnModal, { columns: columns, addColumn: key => isReady && setColumns(uniq([...columns, key])) }))] }), _jsx(LinearProgress, { sx: [!searching && { opacity: 0 }] })] }) }), _jsx(VSBoxContent, { children: displayType === 'grid' ? (_jsx(Stack, { component: Paper, spacing: 1, width: "100%", height: "100%", sx: { overflow: 'auto', flex: 1 }, children: _jsx(HitTable, { query: query, items: response?.items }) })) : (_jsxs(Stack, { spacing: 1, children: [!response?.total && _jsx(AppListEmpty, {}), response?.items.map(hit => (_jsx(HitCard, { id: hit.howler.id, layout: HitLayout.DENSE }, hit.howler.id)))] })) })] }) }) }) }));
176
202
  };
177
203
  export default ViewComposer;