@addev-be/ui 0.3.3 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/components/data/DataGrid/DataGridRowTemplate.tsx +69 -0
- package/src/components/data/DataGrid/VirtualScroller.tsx +7 -6
- package/src/components/data/DataGrid/hooks/useDataGrid.tsx +14 -0
- package/src/components/data/DataGrid/index.tsx +2 -82
- package/src/components/data/DataGrid/types.ts +9 -0
- package/src/components/forms/styles.ts +3 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/Dropdown/index.tsx +3 -2
- package/src/components/layout/Dropdown/styles.ts +3 -2
- package/src/components/search/HighlightedText.tsx +30 -0
- package/src/components/search/QuickSearchBar.tsx +80 -0
- package/src/components/search/QuickSearchResults.tsx +86 -0
- package/src/components/search/index.ts +5 -0
- package/src/components/search/styles.ts +79 -0
- package/src/components/search/types.ts +26 -0
- package/src/helpers/index.ts +2 -0
- package/src/index.ts +1 -0
- package/src/providers/ThemeProvider/index.ts +1 -1
- package/src/services/WebSocketService.ts +1 -0
- package/src/services/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addev-be/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"watch": "tsc -b --watch",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"vite-plugin-svgr": "^4.2.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@uidotdev/usehooks": "^2.4.1",
|
|
44
45
|
"fp-ts": "^2.16.9",
|
|
45
46
|
"io-ts": "^2.2.21",
|
|
46
47
|
"lodash": "^4.17.21",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as styles from './styles';
|
|
2
|
+
|
|
3
|
+
import { DataGridCell } from './DataGridCell';
|
|
4
|
+
import { DataGridRowTemplateProps } from './types';
|
|
5
|
+
import { useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
export const DataGridRowTemplate = <R,>({
|
|
8
|
+
row,
|
|
9
|
+
rowIndex,
|
|
10
|
+
context,
|
|
11
|
+
}: DataGridRowTemplateProps<R>) => {
|
|
12
|
+
const { visibleColumns, rowKeyGetter, toggleSelection, ...props } =
|
|
13
|
+
useContext(context);
|
|
14
|
+
|
|
15
|
+
if (!row) {
|
|
16
|
+
return (
|
|
17
|
+
<styles.DataGridRow key={`loading-row-${rowIndex}`}>
|
|
18
|
+
{!!props.selectable && (
|
|
19
|
+
<styles.LoadingCell className="animate-pulse">
|
|
20
|
+
<div />
|
|
21
|
+
</styles.LoadingCell>
|
|
22
|
+
)}
|
|
23
|
+
{visibleColumns.map((_, index) => (
|
|
24
|
+
<styles.LoadingCell
|
|
25
|
+
className="animate-pulse"
|
|
26
|
+
key={`loading-${rowIndex}-${index}`}
|
|
27
|
+
>
|
|
28
|
+
<div />
|
|
29
|
+
</styles.LoadingCell>
|
|
30
|
+
))}
|
|
31
|
+
</styles.DataGridRow>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const key = rowKeyGetter(row);
|
|
35
|
+
const selected = props.selectedKeys.includes(key);
|
|
36
|
+
const { className, style } = props.rowClassNameGetter?.(row) ?? {
|
|
37
|
+
className: '',
|
|
38
|
+
style: undefined,
|
|
39
|
+
};
|
|
40
|
+
return (
|
|
41
|
+
<styles.DataGridRow key={key}>
|
|
42
|
+
{!!props.selectable && (
|
|
43
|
+
<styles.SelectionCell
|
|
44
|
+
key="__select_checkbox__"
|
|
45
|
+
onClick={() => toggleSelection(key)}
|
|
46
|
+
>
|
|
47
|
+
<input
|
|
48
|
+
type="checkbox"
|
|
49
|
+
value={key as string}
|
|
50
|
+
checked={selected}
|
|
51
|
+
readOnly
|
|
52
|
+
/>
|
|
53
|
+
</styles.SelectionCell>
|
|
54
|
+
)}
|
|
55
|
+
{visibleColumns.map(([key, col], index) => (
|
|
56
|
+
<DataGridCell
|
|
57
|
+
key={`loading-${rowIndex}-${index}`}
|
|
58
|
+
{...(index === 0 ? { className, style } : {})}
|
|
59
|
+
row={row}
|
|
60
|
+
rowIndex={rowIndex}
|
|
61
|
+
column={col}
|
|
62
|
+
columnIndex={index}
|
|
63
|
+
context={context}
|
|
64
|
+
columnKey={key}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</styles.DataGridRow>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import * as styles from './styles';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { DataGridContext } from './types';
|
|
3
|
+
import { DataGridContext, DataGridRowTemplateProps } from './types';
|
|
4
|
+
import { FC, useContext } from 'react';
|
|
6
5
|
|
|
7
6
|
type VirtualScrollerProps<R> = {
|
|
8
7
|
showAllRows?: boolean;
|
|
9
|
-
rowTemplate:
|
|
8
|
+
rowTemplate: FC<DataGridRowTemplateProps<R>>;
|
|
10
9
|
hasFooter?: boolean;
|
|
11
10
|
context: DataGridContext<R>;
|
|
12
11
|
onRangeChange?: (startIndex: number, length: number) => void;
|
|
@@ -22,7 +21,7 @@ export const VirtualScroller = <R,>(props: VirtualScrollerProps<R>) => {
|
|
|
22
21
|
gridTemplateColumns,
|
|
23
22
|
} = useContext(props.context);
|
|
24
23
|
const {
|
|
25
|
-
rowTemplate,
|
|
24
|
+
rowTemplate: RowTemplate,
|
|
26
25
|
// hasFooter, onRangeChange
|
|
27
26
|
} = props;
|
|
28
27
|
|
|
@@ -39,7 +38,9 @@ export const VirtualScroller = <R,>(props: VirtualScrollerProps<R>) => {
|
|
|
39
38
|
$topPadding={topPadding}
|
|
40
39
|
$rowHeight={rowHeight}
|
|
41
40
|
>
|
|
42
|
-
{visibleRows.map(
|
|
41
|
+
{visibleRows.map((row, index) => (
|
|
42
|
+
<RowTemplate row={row} rowIndex={index} context={props.context} />
|
|
43
|
+
))}
|
|
43
44
|
</styles.VirtualScrollerRowsContainer>
|
|
44
45
|
</styles.VirtualScrollerContainer>
|
|
45
46
|
);
|
|
@@ -92,6 +92,17 @@ export const useDataGrid = <R,>(
|
|
|
92
92
|
onSelectionChange?.(selectedKeys);
|
|
93
93
|
}, [onSelectionChange, selectedKeys]);
|
|
94
94
|
|
|
95
|
+
const toggleSelection = useCallback(
|
|
96
|
+
(key: string) => {
|
|
97
|
+
if (selectedKeys.includes(key)) {
|
|
98
|
+
setSelectedKeys(selectedKeys.filter((p) => p !== key));
|
|
99
|
+
} else {
|
|
100
|
+
setSelectedKeys([...selectedKeys, key]);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[selectedKeys, setSelectedKeys]
|
|
104
|
+
);
|
|
105
|
+
|
|
95
106
|
/** ROWS FILTERING **/
|
|
96
107
|
|
|
97
108
|
const [filters, setFilters] = useState<DataGridFilters>({});
|
|
@@ -240,6 +251,7 @@ export const useDataGrid = <R,>(
|
|
|
240
251
|
footers,
|
|
241
252
|
setFooters,
|
|
242
253
|
footerFunctions,
|
|
254
|
+
toggleSelection,
|
|
243
255
|
...override,
|
|
244
256
|
}),
|
|
245
257
|
[
|
|
@@ -265,6 +277,7 @@ export const useDataGrid = <R,>(
|
|
|
265
277
|
gridTemplateColumns,
|
|
266
278
|
footers,
|
|
267
279
|
footerFunctions,
|
|
280
|
+
toggleSelection,
|
|
268
281
|
override,
|
|
269
282
|
]
|
|
270
283
|
);
|
|
@@ -298,6 +311,7 @@ export const useDataGrid = <R,>(
|
|
|
298
311
|
gridTemplateColumns: '',
|
|
299
312
|
footers: {},
|
|
300
313
|
setFooters: () => {},
|
|
314
|
+
toggleSelection: () => {},
|
|
301
315
|
}),
|
|
302
316
|
[]
|
|
303
317
|
);
|
|
@@ -2,11 +2,10 @@ import * as styles from './styles';
|
|
|
2
2
|
|
|
3
3
|
import { DataGridContextProps, DataGridProps } from './types';
|
|
4
4
|
|
|
5
|
-
import { DataGridCell } from './DataGridCell';
|
|
6
5
|
import { DataGridFooter } from './DataGridFooter';
|
|
7
6
|
import { DataGridHeader } from './DataGridHeader';
|
|
7
|
+
import { DataGridRowTemplate } from './DataGridRowTemplate';
|
|
8
8
|
import { VirtualScroller } from './VirtualScroller';
|
|
9
|
-
import { useCallback } from 'react';
|
|
10
9
|
import { useDataGrid } from './hooks';
|
|
11
10
|
|
|
12
11
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
@@ -25,95 +24,16 @@ export const DataGrid = <R,>({
|
|
|
25
24
|
} = props;
|
|
26
25
|
const [contextProps, DataGridContext] = useDataGrid(props, contextOverride);
|
|
27
26
|
const {
|
|
28
|
-
selectedKeys,
|
|
29
|
-
setSelectedKeys,
|
|
30
27
|
columns,
|
|
31
|
-
visibleColumns,
|
|
32
28
|
rowHeight = 32,
|
|
33
29
|
headerRowHeight = 40,
|
|
34
30
|
scrollableRef,
|
|
35
31
|
onScroll,
|
|
36
|
-
rowKeyGetter,
|
|
37
32
|
} = contextProps;
|
|
38
33
|
|
|
39
34
|
const hasFooter = Object.values(columns).some((col) => col.footer);
|
|
40
35
|
|
|
41
|
-
const
|
|
42
|
-
(key: string) => {
|
|
43
|
-
if (selectedKeys.includes(key)) {
|
|
44
|
-
setSelectedKeys(selectedKeys.filter((p) => p !== key));
|
|
45
|
-
} else {
|
|
46
|
-
setSelectedKeys([...selectedKeys, key]);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
[selectedKeys, setSelectedKeys]
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const rowTemplate = useCallback(
|
|
53
|
-
(row: R, rowIndex: number) => {
|
|
54
|
-
if (!row) {
|
|
55
|
-
return (
|
|
56
|
-
<styles.DataGridRow key={`loading-row-${rowIndex}`}>
|
|
57
|
-
{!!props.selectable && (
|
|
58
|
-
<styles.LoadingCell className="animate-pulse">
|
|
59
|
-
<div />
|
|
60
|
-
</styles.LoadingCell>
|
|
61
|
-
)}
|
|
62
|
-
{visibleColumns.map((_, index) => (
|
|
63
|
-
<styles.LoadingCell
|
|
64
|
-
className="animate-pulse"
|
|
65
|
-
key={`loading-${rowIndex}-${index}`}
|
|
66
|
-
>
|
|
67
|
-
<div />
|
|
68
|
-
</styles.LoadingCell>
|
|
69
|
-
))}
|
|
70
|
-
</styles.DataGridRow>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
const key = rowKeyGetter(row);
|
|
74
|
-
const { className, style } = props.rowClassNameGetter?.(row) ?? {
|
|
75
|
-
className: '',
|
|
76
|
-
style: undefined,
|
|
77
|
-
};
|
|
78
|
-
return (
|
|
79
|
-
<styles.DataGridRow key={key}>
|
|
80
|
-
{!!props.selectable && (
|
|
81
|
-
<styles.SelectionCell
|
|
82
|
-
key="__select_checkbox__"
|
|
83
|
-
onClick={() => toggleSelection(key)}
|
|
84
|
-
>
|
|
85
|
-
<input
|
|
86
|
-
type="checkbox"
|
|
87
|
-
value={key as string}
|
|
88
|
-
checked={selectedKeys.includes(key)}
|
|
89
|
-
readOnly
|
|
90
|
-
/>
|
|
91
|
-
</styles.SelectionCell>
|
|
92
|
-
)}
|
|
93
|
-
{visibleColumns.map(([key, col], index) => (
|
|
94
|
-
<DataGridCell
|
|
95
|
-
key={`loading-${rowIndex}-${index}`}
|
|
96
|
-
{...(index === 0 ? { className, style } : {})}
|
|
97
|
-
row={row}
|
|
98
|
-
rowIndex={rowIndex}
|
|
99
|
-
column={col}
|
|
100
|
-
columnIndex={index}
|
|
101
|
-
context={DataGridContext}
|
|
102
|
-
columnKey={key}
|
|
103
|
-
/>
|
|
104
|
-
))}
|
|
105
|
-
</styles.DataGridRow>
|
|
106
|
-
);
|
|
107
|
-
},
|
|
108
|
-
[
|
|
109
|
-
DataGridContext,
|
|
110
|
-
props,
|
|
111
|
-
rowKeyGetter,
|
|
112
|
-
selectedKeys,
|
|
113
|
-
toggleSelection,
|
|
114
|
-
visibleColumns,
|
|
115
|
-
]
|
|
116
|
-
);
|
|
36
|
+
const rowTemplate = DataGridRowTemplate;
|
|
117
37
|
|
|
118
38
|
return (
|
|
119
39
|
<DataGridContext.Provider value={contextProps}>
|
|
@@ -131,6 +131,7 @@ export type DataGridContextProps<R> = DataGridProps<R> & {
|
|
|
131
131
|
length: number;
|
|
132
132
|
rowKeyGetter: (row: R) => string;
|
|
133
133
|
gridTemplateColumns: string;
|
|
134
|
+
toggleSelection: (key: string) => void;
|
|
134
135
|
};
|
|
135
136
|
|
|
136
137
|
export type DataGridContext<R> = Context<DataGridContextProps<R>>;
|
|
@@ -265,3 +266,11 @@ export type DataGridFilterCheckbox = {
|
|
|
265
266
|
values: DataGridFilterValue[];
|
|
266
267
|
level: number;
|
|
267
268
|
};
|
|
269
|
+
|
|
270
|
+
export type DataGridRowTemplateProps<R> = {
|
|
271
|
+
row: R | null;
|
|
272
|
+
rowIndex: number;
|
|
273
|
+
selected?: boolean;
|
|
274
|
+
toggleSelection?: () => void;
|
|
275
|
+
context: DataGridContext<R>;
|
|
276
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -57,6 +57,7 @@ const getDropdownStyle = (dropdown: DropdownProps): CSSProperties => {
|
|
|
57
57
|
export const Dropdown: FC<DropdownProps> = ({
|
|
58
58
|
children,
|
|
59
59
|
onClose,
|
|
60
|
+
$backdropAlpha,
|
|
60
61
|
...props
|
|
61
62
|
}) => {
|
|
62
63
|
const { createPortal } = usePortals();
|
|
@@ -66,13 +67,13 @@ export const Dropdown: FC<DropdownProps> = ({
|
|
|
66
67
|
const modalPortal = useMemo(
|
|
67
68
|
() =>
|
|
68
69
|
createPortal(
|
|
69
|
-
<styles.DropdownBackdrop onClick={onClose}>
|
|
70
|
+
<styles.DropdownBackdrop onClick={onClose} $alpha={$backdropAlpha}>
|
|
70
71
|
<styles.DropdownContainer {...props} style={style}>
|
|
71
72
|
{children}
|
|
72
73
|
</styles.DropdownContainer>
|
|
73
74
|
</styles.DropdownBackdrop>
|
|
74
75
|
),
|
|
75
|
-
[children, createPortal, onClose, props, style]
|
|
76
|
+
[$backdropAlpha, children, createPortal, onClose, props, style]
|
|
76
77
|
);
|
|
77
78
|
|
|
78
79
|
return <>{modalPortal}</>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styled, { css } from 'styled-components';
|
|
2
2
|
|
|
3
|
-
export const DropdownBackdrop = styled.div.attrs({
|
|
3
|
+
export const DropdownBackdrop = styled.div.attrs<{ $alpha?: number }>({
|
|
4
4
|
className: 'DropdownBackdrop',
|
|
5
5
|
})`
|
|
6
6
|
position: absolute;
|
|
@@ -9,7 +9,7 @@ export const DropdownBackdrop = styled.div.attrs({
|
|
|
9
9
|
left: 0;
|
|
10
10
|
right: 0;
|
|
11
11
|
bottom: 0;
|
|
12
|
-
background
|
|
12
|
+
background: rgba(0, 0, 0, ${({ $alpha = 0.5 }) => $alpha});
|
|
13
13
|
display: flex;
|
|
14
14
|
justify-content: center;
|
|
15
15
|
align-items: center;
|
|
@@ -19,6 +19,7 @@ export type DropdownContainerProps = {
|
|
|
19
19
|
$width: number;
|
|
20
20
|
$height: number | number[];
|
|
21
21
|
$zIndex?: number;
|
|
22
|
+
$backdropAlpha?: number;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
export const DropdownContainer = styled.div.attrs({
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
export const HighlightedText: FC<
|
|
4
|
+
{
|
|
5
|
+
text: string;
|
|
6
|
+
highlight?: string;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
} & HTMLAttributes<HTMLSpanElement>
|
|
9
|
+
> = ({ text, highlight, ...props }) => {
|
|
10
|
+
const parts = text?.split(new RegExp(`(${highlight})`, 'gi')) ?? [];
|
|
11
|
+
if (!highlight) return <span {...props}>{text}</span>;
|
|
12
|
+
return (
|
|
13
|
+
<span {...props}>
|
|
14
|
+
{parts
|
|
15
|
+
.filter((part) => !!part)
|
|
16
|
+
.map((part, index) => (
|
|
17
|
+
<span
|
|
18
|
+
key={index}
|
|
19
|
+
style={
|
|
20
|
+
part.toLowerCase() === highlight.toLowerCase()
|
|
21
|
+
? { backgroundColor: 'gold' }
|
|
22
|
+
: {}
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
{part}
|
|
26
|
+
</span>
|
|
27
|
+
))}
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as styles from './styles';
|
|
2
|
+
|
|
3
|
+
import { SearchResults, SearchTypeDefinitions } from './types';
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { Dropdown } from '../layout';
|
|
7
|
+
import { Input } from '../forms';
|
|
8
|
+
import { QuickSearchResults } from './QuickSearchResults';
|
|
9
|
+
import { useDebounce } from '@uidotdev/usehooks';
|
|
10
|
+
import { useGlobalSearchRequestHandler } from '../../services';
|
|
11
|
+
|
|
12
|
+
type QuickSearchBarProps<R> = {
|
|
13
|
+
definitions: SearchTypeDefinitions<R>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const QuickSearchBar = <R,>({ definitions }: QuickSearchBarProps<R>) => {
|
|
17
|
+
const [term, setTerm] = useState('');
|
|
18
|
+
const [dropdownVisible, setDropdownVisible] = useState(false);
|
|
19
|
+
const debouncedTerm = useDebounce(term, 300);
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
const [results, setResults] = useState<SearchResults<R> | null>(null);
|
|
22
|
+
|
|
23
|
+
const fakeInputRef = useRef<HTMLInputElement | null>(null);
|
|
24
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
25
|
+
const rect = fakeInputRef.current?.getBoundingClientRect() ?? new DOMRect();
|
|
26
|
+
const destRect = new DOMRect(rect.x, rect.y, rect.width, 0);
|
|
27
|
+
const globalSearch = useGlobalSearchRequestHandler();
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (debouncedTerm) {
|
|
31
|
+
globalSearch({
|
|
32
|
+
types: ['Customer', 'ScanGroup'],
|
|
33
|
+
searchTerm: debouncedTerm,
|
|
34
|
+
limit: 5,
|
|
35
|
+
}).then((response) => {
|
|
36
|
+
setResults(response.data as SearchResults<R>);
|
|
37
|
+
setDropdownVisible(true);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}, [globalSearch, debouncedTerm]);
|
|
41
|
+
|
|
42
|
+
const onFocus = useCallback(() => {
|
|
43
|
+
setDropdownVisible(true);
|
|
44
|
+
requestAnimationFrame(() => {
|
|
45
|
+
inputRef.current?.focus();
|
|
46
|
+
});
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<Input type="text" ref={fakeInputRef} value={term} onFocus={onFocus} />
|
|
52
|
+
{dropdownVisible && rect && (
|
|
53
|
+
<Dropdown
|
|
54
|
+
$sourceRect={destRect}
|
|
55
|
+
onClose={() => setDropdownVisible(false)}
|
|
56
|
+
$width={rect.width}
|
|
57
|
+
$height={[results ? 250 : rect.height, 400]}
|
|
58
|
+
$autoPositionY={false}
|
|
59
|
+
>
|
|
60
|
+
<styles.QuickSearchDropdownContainer>
|
|
61
|
+
<Input
|
|
62
|
+
type="text"
|
|
63
|
+
ref={inputRef}
|
|
64
|
+
value={term}
|
|
65
|
+
onChange={(e) => setTerm(e.target.value)}
|
|
66
|
+
onClick={(e) => e.stopPropagation()}
|
|
67
|
+
/>
|
|
68
|
+
{results && (
|
|
69
|
+
<QuickSearchResults
|
|
70
|
+
results={results}
|
|
71
|
+
definitions={definitions}
|
|
72
|
+
term={debouncedTerm}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
</styles.QuickSearchDropdownContainer>
|
|
76
|
+
</Dropdown>
|
|
77
|
+
)}
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as styles from './styles';
|
|
2
|
+
|
|
3
|
+
import { ReactNode, useMemo, useState } from 'react';
|
|
4
|
+
import { ResultArray, SearchResults, SearchTypeDefinitions } from './types';
|
|
5
|
+
|
|
6
|
+
import { pickBy } from 'lodash';
|
|
7
|
+
|
|
8
|
+
type QuickSearchResultsProps<R> = {
|
|
9
|
+
definitions: SearchTypeDefinitions<R>;
|
|
10
|
+
results: SearchResults<R>;
|
|
11
|
+
term: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const QuickSearchResults = <R,>(props: QuickSearchResultsProps<R>) => {
|
|
15
|
+
const { definitions, results: allResults, term } = props;
|
|
16
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
17
|
+
|
|
18
|
+
const notEmptyResults = pickBy<R[keyof R][]>(
|
|
19
|
+
allResults,
|
|
20
|
+
(results) => results.length > 0
|
|
21
|
+
);
|
|
22
|
+
const resultsArray = useMemo(
|
|
23
|
+
() =>
|
|
24
|
+
Object.keys(notEmptyResults).reduce(
|
|
25
|
+
(acc, type) => [
|
|
26
|
+
...acc,
|
|
27
|
+
...notEmptyResults[type].map((result) => ({
|
|
28
|
+
result,
|
|
29
|
+
type: type as keyof R,
|
|
30
|
+
})),
|
|
31
|
+
],
|
|
32
|
+
[] as ResultArray<R>
|
|
33
|
+
),
|
|
34
|
+
[notEmptyResults]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const elements = useMemo(() => {
|
|
38
|
+
const elements: ReactNode[] = [];
|
|
39
|
+
let currentType: keyof R | null = null;
|
|
40
|
+
resultsArray.forEach(({ result, type }, index) => {
|
|
41
|
+
const definition = definitions[type];
|
|
42
|
+
if (currentType !== type) {
|
|
43
|
+
currentType = type;
|
|
44
|
+
elements.push(
|
|
45
|
+
<styles.QuickSearchResultsTitle key={`title-${String(type)}`}>
|
|
46
|
+
{definition.title}
|
|
47
|
+
</styles.QuickSearchResultsTitle>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
elements.push(
|
|
51
|
+
<styles.QuickSearchResultsItem
|
|
52
|
+
key={`result-${index}`}
|
|
53
|
+
onClick={() => definition.onClick?.(result)}
|
|
54
|
+
onMouseEnter={() => setHighlightedIndex(index)}
|
|
55
|
+
className={highlightedIndex === index ? 'highlighted' : ''}
|
|
56
|
+
>
|
|
57
|
+
{definition.quickRenderer(result, term)}
|
|
58
|
+
</styles.QuickSearchResultsItem>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
return elements;
|
|
62
|
+
}, [definitions, highlightedIndex, resultsArray, term]);
|
|
63
|
+
|
|
64
|
+
const highlightedResult = resultsArray[highlightedIndex];
|
|
65
|
+
const highlightedDefinition = highlightedResult
|
|
66
|
+
? definitions[highlightedResult?.type]
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<styles.QuickSearchResultsContainer>
|
|
71
|
+
<styles.QuickSearchResultsListContainer>
|
|
72
|
+
{elements}
|
|
73
|
+
</styles.QuickSearchResultsListContainer>
|
|
74
|
+
{highlightedDefinition && (
|
|
75
|
+
<styles.QuickSearchResultsDetailsContainer>
|
|
76
|
+
{highlightedDefinition.titleRenderer(highlightedResult.result, term)}
|
|
77
|
+
<styles.QuickSearchResultsDetailsDivider />
|
|
78
|
+
{highlightedDefinition.detailsRenderer(
|
|
79
|
+
highlightedResult.result,
|
|
80
|
+
term
|
|
81
|
+
)}
|
|
82
|
+
</styles.QuickSearchResultsDetailsContainer>
|
|
83
|
+
)}
|
|
84
|
+
</styles.QuickSearchResultsContainer>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const QuickSearchDropdownContainer = styled.div.attrs({
|
|
4
|
+
className: 'QuickSearchDropdownContainer',
|
|
5
|
+
})`
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
height: 100%;
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
export const QuickSearchResultsContainer = styled.div.attrs({
|
|
12
|
+
className: 'QuickSearchResultsContainer',
|
|
13
|
+
})`
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: row;
|
|
16
|
+
height: 100%;
|
|
17
|
+
flex: 1;
|
|
18
|
+
padding: var(--space-2) 0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export const QuickSearchResultsListContainer = styled.div.attrs({
|
|
23
|
+
className: 'QuickSearchResultsListContainer',
|
|
24
|
+
})`
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
padding: var(--space-2);
|
|
28
|
+
border-right: 1px solid var(--color-neutral-200);
|
|
29
|
+
flex: 1;
|
|
30
|
+
overflow: auto;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export const QuickSearchResultsTitle = styled.div.attrs({
|
|
34
|
+
className: 'QuickSearchResultsTitle',
|
|
35
|
+
})`
|
|
36
|
+
margin: 0;
|
|
37
|
+
margin-bottom: var(--space-1);
|
|
38
|
+
&:not(:first-child) {
|
|
39
|
+
margin-top: var(--space-2);
|
|
40
|
+
}
|
|
41
|
+
font-weight: bold;
|
|
42
|
+
font-size: var(--text-sm);
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 120%;
|
|
45
|
+
color: var(--color-neutral-500);
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export const QuickSearchResultsItem = styled.div.attrs({
|
|
49
|
+
className: 'QuickSearchResultsItem',
|
|
50
|
+
})`
|
|
51
|
+
padding: var(--space-2) var(--space-3);
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
&:hover {
|
|
55
|
+
background-color: var(--color-neutral-100);
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
export const QuickSearchResultsDetailsContainer = styled.div.attrs({
|
|
60
|
+
className: 'QuickSearchResultsDetailsContainer',
|
|
61
|
+
})`
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
padding: var(--space-2);
|
|
65
|
+
flex: 1;
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
export const QuickSearchResultsDetailsDivider = styled.hr`
|
|
69
|
+
margin: var(--space-2) 0;
|
|
70
|
+
height: 1px;
|
|
71
|
+
border: none;
|
|
72
|
+
background-color: var(--color-neutral-200);
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
export const QuickSearchResultDetailsTitle = styled.div.attrs({
|
|
76
|
+
className: 'QuickSearchResultDetailsTitle',
|
|
77
|
+
})`
|
|
78
|
+
margin: 0;
|
|
79
|
+
`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type SearchTypeDefinition<T> = {
|
|
4
|
+
title: string;
|
|
5
|
+
quickRenderer: (result: T, term: string) => ReactNode;
|
|
6
|
+
titleRenderer: (result: T, term: string) => ReactNode;
|
|
7
|
+
detailsRenderer: (result: T, term: string) => ReactNode;
|
|
8
|
+
onClick?: (result: T) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SearchTypeDefinitions<R> = {
|
|
12
|
+
[K in keyof R]: SearchTypeDefinition<R[K]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SearchResults<R> = {
|
|
16
|
+
[K in keyof R]: Array<R[K]>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type SearchDetailsFC<T> = FC<{
|
|
20
|
+
searchResult: T;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
export type ResultArray<R> = {
|
|
24
|
+
result: R[keyof R];
|
|
25
|
+
type: keyof R;
|
|
26
|
+
}[];
|
package/src/index.ts
CHANGED