@addev-be/ui 0.5.5 → 0.6.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addev-be/ui",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "watch": "tsc -b --watch",
@@ -2,28 +2,29 @@ import * as styles from './styles';
2
2
 
3
3
  import { DataGridCell } from './DataGridCell';
4
4
  import { DataGridRowTemplateProps } from './types';
5
+ import { VirtualScrollerTemplateProps } from '../VirtualScroller/types';
5
6
  import { useContext } from 'react';
6
7
 
7
8
  export const DataGridRowTemplate = <R,>({
8
- row,
9
- rowIndex,
9
+ item,
10
+ index,
10
11
  context,
11
- }: DataGridRowTemplateProps<R>) => {
12
+ }: VirtualScrollerTemplateProps<R, DataGridRowTemplateProps<R>>) => {
12
13
  const { visibleColumns, rowKeyGetter, toggleSelection, ...props } =
13
14
  useContext(context);
14
15
 
15
- if (!row) {
16
+ if (!item) {
16
17
  return (
17
- <styles.DataGridRow key={`loading-row-${rowIndex}`}>
18
+ <styles.DataGridRow key={`loading-row-${index}`}>
18
19
  {!!props.selectable && (
19
20
  <styles.LoadingCell className="animate-pulse">
20
21
  <div />
21
22
  </styles.LoadingCell>
22
23
  )}
23
- {visibleColumns.map((_, index) => (
24
+ {visibleColumns.map((_, colIndex) => (
24
25
  <styles.LoadingCell
25
26
  className="animate-pulse"
26
- key={`loading-${rowIndex}-${index}`}
27
+ key={`loading-${index}-${colIndex}`}
27
28
  >
28
29
  <div />
29
30
  </styles.LoadingCell>
@@ -31,9 +32,9 @@ export const DataGridRowTemplate = <R,>({
31
32
  </styles.DataGridRow>
32
33
  );
33
34
  }
34
- const key = rowKeyGetter(row);
35
+ const key = rowKeyGetter(item);
35
36
  const selected = props.selectedKeys.includes(key);
36
- const { className, style } = props.rowClassNameGetter?.(row) ?? {
37
+ const { className, style } = props.rowClassNameGetter?.(item) ?? {
37
38
  className: '',
38
39
  style: undefined,
39
40
  };
@@ -54,10 +55,10 @@ export const DataGridRowTemplate = <R,>({
54
55
  )}
55
56
  {visibleColumns.map(([key, col], index) => (
56
57
  <DataGridCell
57
- key={`loading-${rowIndex}-${index}`}
58
+ key={`loading-${index}-${index}`}
58
59
  {...(index === 0 ? { className, style } : {})}
59
- row={row}
60
- rowIndex={rowIndex}
60
+ row={item}
61
+ rowIndex={index}
61
62
  column={col}
62
63
  columnIndex={index}
63
64
  context={context}
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
 
4
- import { DEFAULT_ROW_HEIGHT, VIRTUAL_SCROLL_TOLERANCE } from '../styles';
5
4
  import {
6
5
  DataGridColumns,
7
6
  DataGridContext,
@@ -24,7 +23,6 @@ import { useDataGridCopy, useSortedColumns } from '.';
24
23
  import { applyFilter } from '../helpers';
25
24
  import { merge } from 'lodash';
26
25
  import { useDataGridSettings } from './useDataGridSettings';
27
- import { useElementSize } from '../../../../hooks';
28
26
 
29
27
  export const useDataGrid = <R,>(
30
28
  props: DataGridProps<R>,
@@ -35,8 +33,6 @@ export const useDataGrid = <R,>(
35
33
  onFiltersChanged,
36
34
  onSortsChanged,
37
35
  loadCopyRows,
38
- rowHeight = DEFAULT_ROW_HEIGHT,
39
- onVisibleRowsChange,
40
36
  onSelectionChange,
41
37
  selectable,
42
38
  initialSorts,
@@ -161,30 +157,7 @@ export const useDataGrid = <R,>(
161
157
  }, [sorts, onSortsChanged]);
162
158
 
163
159
  /** VIRTUAL SCROLLING */
164
-
165
- const scrollableRef = useRef<HTMLTableSectionElement | null>(null);
166
- const { height } = useElementSize(scrollableRef.current);
167
-
168
- const [scrollTop, setScrollTop] = useState(0);
169
- const [index, length] =
170
- // props.showAllRows ? [0, sortedRows.length] :
171
- [Math.floor(scrollTop / rowHeight), Math.ceil(height / rowHeight)];
172
-
173
- const onScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
174
- const target = e.target as HTMLDivElement;
175
- setScrollTop(target.scrollTop);
176
- }, []);
177
-
178
- const indexWithTolerance = Math.max(0, index - VIRTUAL_SCROLL_TOLERANCE);
179
- const lengthWithTolerance = length + 2 * VIRTUAL_SCROLL_TOLERANCE;
180
- const visibleRows = sortedRows.slice(
181
- indexWithTolerance,
182
- indexWithTolerance + lengthWithTolerance
183
- );
184
-
185
- useEffect(() => {
186
- onVisibleRowsChange?.(indexWithTolerance, lengthWithTolerance);
187
- }, [indexWithTolerance, lengthWithTolerance, onVisibleRowsChange]);
160
+ const scrollableRef = useRef<HTMLDivElement>(null);
188
161
 
189
162
  /** FOOTERS */
190
163
  const [footers, setFooters] = useState<Record<string, string>>(
@@ -241,10 +214,7 @@ export const useDataGrid = <R,>(
241
214
  settings,
242
215
  setSettings,
243
216
  saveSettings,
244
- visibleRows,
245
217
  scrollableRef,
246
- onScroll,
247
- index,
248
218
  length,
249
219
  rowKeyGetter,
250
220
  gridTemplateColumns,
@@ -269,10 +239,7 @@ export const useDataGrid = <R,>(
269
239
  settings,
270
240
  setSettings,
271
241
  saveSettings,
272
- visibleRows,
273
- onScroll,
274
- index,
275
- length,
242
+ scrollableRef,
276
243
  rowKeyGetter,
277
244
  gridTemplateColumns,
278
245
  footers,
@@ -303,10 +270,6 @@ export const useDataGrid = <R,>(
303
270
  saveSettings: () => {},
304
271
  settings: {},
305
272
  setSettings: () => {},
306
- visibleRows: [],
307
- scrollableRef: { current: null },
308
- onScroll: () => {},
309
- index: 0,
310
273
  length: 0,
311
274
  gridTemplateColumns: '',
312
275
  footers: {},
@@ -1,11 +1,12 @@
1
1
  import * as styles from './styles';
2
2
 
3
+ import { Context, useRef } from 'react';
3
4
  import { DataGridContextProps, DataGridProps } from './types';
4
5
 
5
6
  import { DataGridFooter } from './DataGridFooter';
6
7
  import { DataGridHeader } from './DataGridHeader';
7
8
  import { DataGridRowTemplate } from './DataGridRowTemplate';
8
- import { VirtualScroller } from './VirtualScroller';
9
+ import { VirtualScroller } from '../VirtualScroller';
9
10
  import { useDataGrid } from './hooks';
10
11
 
11
12
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -17,18 +18,16 @@ export const DataGrid = <R,>({
17
18
  }: DataGridProps<R> & {
18
19
  contextOverride?: Partial<DataGridContextProps<R>>;
19
20
  }) => {
20
- const {
21
- className,
22
- // onRowDoubleClick,
23
- onVisibleRowsChange,
24
- } = props;
21
+ const scrollableRef = useRef<HTMLDivElement>(null);
22
+ const { className } = props;
25
23
  const [contextProps, DataGridContext] = useDataGrid(props, contextOverride);
26
24
  const {
27
25
  columns,
28
26
  rowHeight = 32,
29
27
  headerRowHeight = 40,
30
- scrollableRef,
31
- onScroll,
28
+ gridTemplateColumns,
29
+ sortedRows,
30
+ onVisibleRowsChange,
32
31
  } = contextProps;
33
32
 
34
33
  const hasFooter = Object.values(columns).some((col) => col.footer);
@@ -39,7 +38,6 @@ export const DataGrid = <R,>({
39
38
  <DataGridContext.Provider value={contextProps}>
40
39
  <styles.DataGridContainer
41
40
  ref={scrollableRef}
42
- onScroll={onScroll}
43
41
  $headerRowHeight={headerRowHeight}
44
42
  $rowHeight={rowHeight}
45
43
  $rowsCount={contextProps.sortedRows.length}
@@ -47,11 +45,13 @@ export const DataGrid = <R,>({
47
45
  className={className}
48
46
  >
49
47
  <DataGridHeader context={DataGridContext} />
50
- <VirtualScroller
51
- onRangeChange={onVisibleRowsChange}
52
- hasFooter={hasFooter}
53
- rowTemplate={rowTemplate}
54
- context={DataGridContext}
48
+ <VirtualScroller<R, { context: Context<DataGridContextProps<R>> }>
49
+ gridTemplateColumns={gridTemplateColumns}
50
+ items={sortedRows}
51
+ itemTemplate={rowTemplate}
52
+ itemProps={{ context: DataGridContext }}
53
+ rowHeightInPx={styles.DEFAULT_ROW_HEIGHT}
54
+ onRangeChanged={onVisibleRowsChange}
55
55
  />
56
56
  {hasFooter && <DataGridFooter context={DataGridContext} />}
57
57
  </styles.DataGridContainer>
@@ -1,52 +1,13 @@
1
1
  import styled, { css } from 'styled-components';
2
2
 
3
3
  import { ThemeColor } from '../../../providers/ThemeProvider/types';
4
+ import { VirtualScrollerContainer } from '../VirtualScroller/styles';
4
5
 
5
- export const VIRTUAL_SCROLL_TOLERANCE = 20;
6
6
  export const TOOLBAR_HEIGHT = 40;
7
7
  export const DEFAULT_HEADER_ROW_HEIGHT = 40;
8
8
  export const DEFAULT_FOOTER_ROW_HEIGHT = 40;
9
9
  export const DEFAULT_ROW_HEIGHT = 32;
10
10
 
11
- export const TopPaddingRow = styled.div``;
12
- export const BottomPaddingRow = styled.div``;
13
-
14
- export const VirtualScrollerContainer = styled.div<{
15
- $height: number;
16
- }>`
17
- position: relative;
18
- overflow: visible;
19
- height: ${({ $height }) => `${$height}px`};
20
- `;
21
- export const VirtualScrollerRowsContainer = styled.div.attrs<{
22
- $gridTemplateColumns: string;
23
- $topPadding: number;
24
- $rowHeight?: number;
25
- }>(({ $gridTemplateColumns, $topPadding, $rowHeight = DEFAULT_ROW_HEIGHT }) => {
26
- const rowHeightValue = `${$rowHeight}px`;
27
- return {
28
- style: {
29
- top: `${$topPadding}px`,
30
- gridTemplateColumns: $gridTemplateColumns,
31
- gridAutoRows: rowHeightValue,
32
- },
33
- };
34
- })`
35
- display: grid;
36
- position: absolute;
37
-
38
- ${TopPaddingRow} {
39
- grid-column-start: 1;
40
- grid-column-end: -1;
41
- grid-row: 1;
42
- }
43
- ${BottomPaddingRow} {
44
- grid-column-start: 1;
45
- grid-column-end: -1;
46
- grid-row: -1;
47
- }
48
- `;
49
-
50
11
  export const DataGridResizeGrip = styled.div<{ $headerColor?: ThemeColor }>`
51
12
  position: absolute;
52
13
  top: 0;
@@ -204,6 +165,7 @@ export const DataGridContainer = styled.div<{
204
165
  ${DataGridCell} {
205
166
  height: ${({ $rowHeight = DEFAULT_ROW_HEIGHT }) => `${$rowHeight}px`};
206
167
  line-height: ${({ $rowHeight = DEFAULT_ROW_HEIGHT }) => `${$rowHeight}px`};
168
+ box-sizing: border-box;
207
169
  }
208
170
 
209
171
  ${VirtualScrollerContainer} {
@@ -124,10 +124,6 @@ export type DataGridContextProps<R> = DataGridProps<R> & {
124
124
  saveSettings: (newSettings?: DataGridSettings) => void;
125
125
  settings: DataGridSettings;
126
126
  setSettings: Dispatch<SetStateAction<DataGridSettings>>;
127
- visibleRows: R[];
128
- scrollableRef: React.RefObject<HTMLDivElement>;
129
- onScroll: (e: React.UIEvent<HTMLDivElement>) => void;
130
- index: number;
131
127
  length: number;
132
128
  rowKeyGetter: (row: R) => string;
133
129
  gridTemplateColumns: string;
@@ -268,8 +264,6 @@ export type DataGridFilterCheckbox = {
268
264
  };
269
265
 
270
266
  export type DataGridRowTemplateProps<R> = {
271
- row: R | null;
272
- rowIndex: number;
273
267
  selected?: boolean;
274
268
  toggleSelection?: () => void;
275
269
  context: DataGridContext<R>;
@@ -0,0 +1,277 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { SqlRequestDataGridColumn, SqlRequestDataGridColumns } from '../types';
4
+ import {
5
+ buildExcelFormat,
6
+ numberFilter,
7
+ textFilter,
8
+ } from '../../DataGrid/helpers';
9
+ import {
10
+ formatMoney,
11
+ formatNumber,
12
+ formatNumberInvariant,
13
+ formatPercentage,
14
+ } from '../../../../helpers/numbers';
15
+
16
+ import { formatDate } from '../../../../helpers/dates';
17
+
18
+ export const sqlTextColumn = <R extends Record<string, any>>(
19
+ key: string,
20
+ title: string,
21
+ options?: Partial<SqlRequestDataGridColumn<R>>
22
+ ): SqlRequestDataGridColumns<R> => ({
23
+ [key]: {
24
+ name: title,
25
+ render: (row) => row[key] ?? '',
26
+ getter: (row) => row[key] ?? '',
27
+ sortGetter: (row) => row[key] ?? '',
28
+ filter: { ...textFilter(key), getter: (value) => value[key] ?? '' },
29
+ ...options,
30
+ footer: (rows) => `${rows[0][key]} éléments`,
31
+ },
32
+ });
33
+
34
+ /**
35
+ * Creates a column with a composed value from multiple fields,
36
+ * and filtered by a text filter on the first field
37
+ */
38
+ export const sqlComposedColumn = <R extends Record<string, any>>(
39
+ key: string,
40
+ title: string,
41
+ fields: string[],
42
+ options?: Partial<SqlRequestDataGridColumn<R>>
43
+ ): SqlRequestDataGridColumns<R> => ({
44
+ [key]: {
45
+ field: {
46
+ fieldAlias: key,
47
+ operator: 'jsonObject',
48
+ operands: fields.flatMap((field) => [
49
+ { constantValue: field },
50
+ { fieldName: field },
51
+ ]),
52
+ },
53
+ name: title,
54
+ render: (row) => row[key] ?? '',
55
+ getter: (row) => row[key] ?? '',
56
+ sortGetter: (row) => row[key] ?? '',
57
+ filter: {
58
+ ...textFilter(fields[0]),
59
+ getter: (value) => value[fields[0]] ?? 0,
60
+ },
61
+ filterField: fields[0],
62
+ sortField: fields[0],
63
+ footer: (rows) => `${rows[0][key]} éléments`,
64
+ ...options,
65
+ },
66
+ });
67
+
68
+ export const sqlMailColumn = <R extends Record<string, any>>(
69
+ key: string,
70
+ title: string,
71
+ options?: Partial<SqlRequestDataGridColumn<R>>
72
+ ): SqlRequestDataGridColumns<R> => ({
73
+ [key]: {
74
+ name: title,
75
+ render: (row) => <a href={`mailto:${row[key]}`}>{row[key] ?? ''}</a>,
76
+ getter: (row) => row[key] ?? '',
77
+ sortGetter: (row) => row[key] ?? '',
78
+ filter: { ...textFilter(key), getter: (value) => value[key] ?? '' },
79
+ footer: (rows) => `${rows[0][key]} éléments`,
80
+ ...options,
81
+ },
82
+ });
83
+
84
+ export const sqlPhoneColumn = <R extends Record<string, any>>(
85
+ key: string,
86
+ title: string,
87
+ options?: Partial<SqlRequestDataGridColumn<R>>
88
+ ): SqlRequestDataGridColumns<R> => ({
89
+ [key]: {
90
+ name: title,
91
+ render: (row) => <a href={`tel:${row[key]}`}>{row[key] ?? ''}</a>,
92
+ getter: (row) => row[key] ?? '',
93
+ sortGetter: (row) => row[key] ?? '',
94
+ filter: { ...textFilter(key), getter: (value) => value[key] ?? '' },
95
+ footer: (rows) => `${rows[0][key]} éléments`,
96
+ ...options,
97
+ },
98
+ });
99
+
100
+ export const sqlDateColumn = <R extends Record<string, any>>(
101
+ key: string,
102
+ title: string,
103
+ options?: Partial<SqlRequestDataGridColumn<R>>
104
+ ): SqlRequestDataGridColumns<R> => ({
105
+ [key]: {
106
+ name: title,
107
+ type: 'date',
108
+ render: (row) => formatDate(row[key]),
109
+ getter: (row) => row[key] ?? '',
110
+ sortGetter: (row) => row[key] ?? '',
111
+ excelFormatter: () => 'dd/mm/yyyy',
112
+ excelValue: (value) => formatDate(value, 'YYYY-MM-DD'),
113
+ filter: {
114
+ ...textFilter(key),
115
+ getter: (value) => value[key] ?? '',
116
+ formatter: (value) => formatDate(value),
117
+ renderer: (value) => formatDate(value),
118
+ },
119
+ footer: (rows) => `${rows[0][key]} éléments`,
120
+ ...options,
121
+ },
122
+ });
123
+
124
+ export const sqlMonthColumn = <R extends Record<string, any>>(
125
+ key: string,
126
+ title: string,
127
+ options?: Partial<SqlRequestDataGridColumn<R>>
128
+ ): SqlRequestDataGridColumns<R> => ({
129
+ [key]: {
130
+ name: title,
131
+ render: (row) => (row[key] ? `${row[key]} mois ` : ''),
132
+ getter: (row) => row[key] ?? '',
133
+ sortGetter: (row) => row[key] ?? '',
134
+ filter: { ...textFilter(key), getter: (value) => value[key] ?? '' },
135
+ footer: (rows) => `${rows[0][key]} éléments`,
136
+ ...options,
137
+ },
138
+ });
139
+
140
+ export const sqlNumberColumn = <R extends Record<string, any>>(
141
+ key: string,
142
+ title: string,
143
+ decimals = 2,
144
+ options?: Partial<SqlRequestDataGridColumn<R>>
145
+ ): SqlRequestDataGridColumns<R> => ({
146
+ [key]: {
147
+ name: title,
148
+ render: (row) => formatNumber(row[key], decimals) ?? '',
149
+ excelFormatter: () => buildExcelFormat(decimals),
150
+ excelValue: (value) => formatNumberInvariant(value, decimals),
151
+ getter: (row) => row[key] ?? '',
152
+ sortGetter: (row) => row[key] ?? '',
153
+ filter: {
154
+ ...numberFilter(key),
155
+ getter: (value) => value[key] ?? 0,
156
+ renderer: (value) => formatNumber(value, decimals) ?? '',
157
+ },
158
+ footer: {
159
+ sum: null,
160
+ avg: null,
161
+ count: null,
162
+ max: null,
163
+ min: null,
164
+ },
165
+ ...options,
166
+ },
167
+ });
168
+
169
+ export const sqlMoneyColumn = <R extends Record<string, any>>(
170
+ key: string,
171
+ title: string,
172
+ decimals = 2,
173
+ options?: Partial<SqlRequestDataGridColumn<R>>
174
+ ): SqlRequestDataGridColumns<R> => ({
175
+ [key]: {
176
+ name: title,
177
+ type: 'number',
178
+ render: (row) => formatMoney(row[key], decimals) ?? '',
179
+ excelFormatter: () => buildExcelFormat(decimals, ' €'),
180
+ excelValue: (value) => formatNumberInvariant(value, decimals),
181
+ getter: (row) => row[key] ?? '',
182
+ sortGetter: (row) => row[key] ?? '',
183
+ filter: {
184
+ ...numberFilter(key),
185
+ getter: (value) => value[key] ?? 0,
186
+ renderer: (value) => formatMoney(value, decimals) ?? '',
187
+ },
188
+ footer: {
189
+ sum: null,
190
+ avg: null,
191
+ count: null,
192
+ max: null,
193
+ min: null,
194
+ },
195
+ ...options,
196
+ },
197
+ });
198
+
199
+ export const sqlPercentageColumn = <R extends Record<string, any>>(
200
+ key: string,
201
+ title: string,
202
+ decimals = 2,
203
+ options?: Partial<SqlRequestDataGridColumn<R>>
204
+ ): SqlRequestDataGridColumns<R> => ({
205
+ [key]: {
206
+ name: title,
207
+ render: (row) => formatPercentage(row[key]) ?? '',
208
+ excelFormatter: () => buildExcelFormat(decimals, '%'),
209
+ excelValue: (value) => formatNumberInvariant(value, decimals),
210
+ getter: (row) => row[key] ?? '',
211
+ sortGetter: (row) => row[key] ?? '',
212
+ filter: {
213
+ ...numberFilter(key),
214
+ getter: (value) => value[key] ?? 0,
215
+ renderer: (value) => formatPercentage(value, decimals) ?? '',
216
+ },
217
+ ...options,
218
+ },
219
+ });
220
+
221
+ export const sqlCheckboxColumn = <R extends Record<string, any>>(
222
+ key: string,
223
+ title: string,
224
+ options?: Partial<SqlRequestDataGridColumn<R>>
225
+ ): SqlRequestDataGridColumns<R> => ({
226
+ [key]: {
227
+ name: title,
228
+ render: (row) => (
229
+ <>
230
+ <input type="checkbox" checked={row[key]} />
231
+ <span>{row[key] ? ' Oui' : ' Non'}</span>
232
+ </>
233
+ ),
234
+ getter: (row) => row[key] ?? '',
235
+ sortGetter: (row) => row[key] ?? '',
236
+ filter: { ...numberFilter(key), getter: (value) => value[key] ?? 0 },
237
+ footer: (rows) => `${rows[0][key]} éléments`,
238
+ ...options,
239
+ },
240
+ });
241
+
242
+ export const sqlColorColumn = <R extends Record<string, any>>(
243
+ key: string,
244
+ title: string,
245
+ options?: Partial<SqlRequestDataGridColumn<R>>
246
+ ): SqlRequestDataGridColumns<R> => ({
247
+ [key]: {
248
+ name: title,
249
+ render: (row) => (
250
+ <div
251
+ style={{ position: 'absolute', inset: 0, backgroundColor: row[key] }}
252
+ >
253
+ &nbsp;
254
+ </div>
255
+ ),
256
+ excelValue: () => '',
257
+ excelBackgroundColor: (value) => value,
258
+ getter: (row) => row[key] ?? '',
259
+ sortGetter: (row) => row[key] ?? '',
260
+ filter: {
261
+ ...textFilter(key),
262
+ getter: (value) => value[key] ?? '',
263
+ renderer: (value) => (
264
+ <div
265
+ style={{
266
+ backgroundColor: value,
267
+ width: 'var(--space-16)',
268
+ height: '1em',
269
+ }}
270
+ >
271
+ &nbsp;
272
+ </div>
273
+ ),
274
+ },
275
+ ...options,
276
+ },
277
+ });
@@ -0,0 +1,2 @@
1
+ export * from './sqlRequests';
2
+ export * from './columns';
@@ -0,0 +1,16 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { ConditionDTO } from '../../../../services/sqlRequests';
4
+ import { SqlRequestDataGridFilters } from '../types';
5
+ import _ from 'lodash';
6
+
7
+ export const convertSqlFiltersToConditions = (
8
+ filters: SqlRequestDataGridFilters
9
+ ): Record<string, ConditionDTO> =>
10
+ _.mapValues(filters, (filter, columnKey) => ({
11
+ field: columnKey,
12
+ operator: filter.operator as any,
13
+ value: ['inArray', 'inRange'].includes(filter.operator)
14
+ ? filter.values
15
+ : _.castArray<string | number | null>(filter.values)[0],
16
+ }));
@@ -0,0 +1,138 @@
1
+ import * as styles from './styles';
2
+
3
+ import {
4
+ ConditionDTO,
5
+ OrderByDTO,
6
+ useSqlRequestHandler,
7
+ } from '../../../services/sqlRequests';
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
+
10
+ import { SqlRequestGridProps } from './types';
11
+ import { VirtualScroller } from '../VirtualScroller';
12
+ import { debounce } from 'lodash';
13
+ import { isColumnVisible } from '../DataGrid/helpers';
14
+
15
+ export const SqlRequestGrid = <R,>({
16
+ rowHeight = styles.DEFAULT_ROW_HEIGHT,
17
+ itemsPerRow,
18
+ itemTemplate,
19
+ gap,
20
+ ...props
21
+ }: SqlRequestGridProps<R>) => {
22
+ const currentRows = useRef<R[]>([]);
23
+ const [rows, setRows] = useState<R[]>([]);
24
+ const [start, setStart] = useState(0);
25
+ const [length, setLength] = useState(50);
26
+ const [count, setCount] = useState(-1);
27
+ const [sqlRequest] = useSqlRequestHandler<R>(props.type);
28
+
29
+ const [columnsKeys, visibleColumnsKeys] = useMemo(
30
+ () => [
31
+ [...Object.keys(props.fields), ...(props.additionalFields ?? [])],
32
+ [
33
+ ...Object.keys(props.fields).filter((key) =>
34
+ isColumnVisible(props.fields[key])
35
+ ),
36
+ ...(props.additionalFields ?? []),
37
+ ],
38
+ ],
39
+ [props.fields, props.additionalFields]
40
+ );
41
+ const columnTypes = useMemo(
42
+ () =>
43
+ visibleColumnsKeys.map((key) =>
44
+ String(props.fields[key]?.type ?? 'text')
45
+ ),
46
+ [visibleColumnsKeys, props.fields]
47
+ );
48
+
49
+ const loadRows = useRef(
50
+ debounce(
51
+ (
52
+ columns: string[],
53
+ returnColumns: string[],
54
+ conditions: ConditionDTO[] = [],
55
+ orderBy: OrderByDTO[] = [],
56
+ start = 0,
57
+ length = 100,
58
+ getCount = false
59
+ ) => {
60
+ sqlRequest({
61
+ columns: columns.includes('Id') ? columns : [...columns, 'Id'],
62
+ returnColumns: returnColumns.includes('Id')
63
+ ? returnColumns
64
+ : [...returnColumns, 'Id'],
65
+ columnTypes: columnTypes.includes('Id')
66
+ ? columnTypes
67
+ : [...columnTypes, 'Id'],
68
+ conditions,
69
+ orderBy,
70
+ start,
71
+ length,
72
+ getCount,
73
+ }).then((response) => {
74
+ if (getCount) {
75
+ currentRows.current = Array(response.count).fill(null);
76
+ if (getCount) setCount(response.count ?? 0);
77
+ }
78
+ const parsedRows = props.parser
79
+ ? response.data.map(props.parser)
80
+ : (response.data as R[]);
81
+ currentRows.current.splice(start, length, ...parsedRows);
82
+ setRows([...currentRows.current]);
83
+ });
84
+ },
85
+ 100,
86
+ {
87
+ leading: true,
88
+ trailing: true,
89
+ }
90
+ )
91
+ );
92
+
93
+ useEffect(
94
+ () =>
95
+ loadRows.current(
96
+ columnsKeys,
97
+ visibleColumnsKeys,
98
+ [],
99
+ [],
100
+ start,
101
+ length,
102
+ count < 0
103
+ ),
104
+ [
105
+ props.fields,
106
+ start,
107
+ length,
108
+ count,
109
+ props.conditions,
110
+ columnsKeys,
111
+ visibleColumnsKeys,
112
+ ]
113
+ );
114
+
115
+ const onVisibleRowsChanged = useCallback(
116
+ (newStart: number, newLength: number) => {
117
+ if (newStart !== start || newLength !== length) {
118
+ setStart(newStart);
119
+ setLength(newLength);
120
+ }
121
+ },
122
+ [length, start]
123
+ );
124
+
125
+ return (
126
+ <styles.SqlRequestGridContainer>
127
+ <VirtualScroller
128
+ gridTemplateColumns={`repeat(${itemsPerRow}, 1fr)`}
129
+ items={rows}
130
+ itemTemplate={itemTemplate}
131
+ itemsPerRow={itemsPerRow}
132
+ rowHeightInPx={rowHeight}
133
+ onRangeChanged={onVisibleRowsChanged}
134
+ gap={gap}
135
+ />
136
+ </styles.SqlRequestGridContainer>
137
+ );
138
+ };
@@ -0,0 +1,50 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const DEFAULT_ROW_HEIGHT = 200;
4
+
5
+ export const SqlRequestGridContainer = styled.div.attrs({
6
+ className: 'SqlRequestGridContainer',
7
+ })`
8
+ height: 100%;
9
+ width: 100%;
10
+ overflow: auto;
11
+ `;
12
+
13
+ export const TopPaddingRow = styled.div``;
14
+ export const BottomPaddingRow = styled.div``;
15
+
16
+ export const VirtualScrollerContainer = styled.div<{
17
+ $height: number;
18
+ }>`
19
+ position: relative;
20
+ overflow: visible;
21
+ height: ${({ $height }) => `${$height}px`};
22
+ `;
23
+ export const VirtualScrollerRowsContainer = styled.div.attrs<{
24
+ $gridTemplateColumns: string;
25
+ $topPadding: number;
26
+ $rowHeight?: number;
27
+ }>(({ $gridTemplateColumns, $topPadding, $rowHeight = DEFAULT_ROW_HEIGHT }) => {
28
+ const rowHeightValue = `${$rowHeight}px`;
29
+ return {
30
+ style: {
31
+ top: `${$topPadding}px`,
32
+ gridTemplateColumns: $gridTemplateColumns,
33
+ gridAutoRows: rowHeightValue,
34
+ },
35
+ };
36
+ })`
37
+ display: grid;
38
+ position: absolute;
39
+
40
+ ${TopPaddingRow} {
41
+ grid-column-start: 1;
42
+ grid-column-end: -1;
43
+ grid-row: 1;
44
+ }
45
+ ${BottomPaddingRow} {
46
+ grid-column-start: 1;
47
+ grid-column-end: -1;
48
+ grid-row: -1;
49
+ }
50
+ `;
@@ -0,0 +1,43 @@
1
+ import {
2
+ ConditionDTO,
3
+ FieldDTO,
4
+ OrderByDTO,
5
+ SqlRequestRow,
6
+ } from '../../../services/sqlRequests';
7
+ import {
8
+ DataGridColumn,
9
+ DataGridFilter,
10
+ DataGridFilterType,
11
+ } from '../DataGrid/types';
12
+
13
+ import { VirtualScrollerTemplateFC } from '../VirtualScroller/types';
14
+
15
+ export type SqlRequestGridFilter<
16
+ T extends DataGridFilterType = DataGridFilterType
17
+ > = DataGridFilter<T> & {
18
+ field?: FieldDTO;
19
+ };
20
+
21
+ export type SqlRequestGridFilters = Record<string, SqlRequestGridFilter>;
22
+
23
+ export type SqlRequestGridColumn<R> = DataGridColumn<R> & {
24
+ filter?: SqlRequestGridFilter;
25
+ field?: FieldDTO;
26
+ filterField?: string;
27
+ sortField?: string;
28
+ };
29
+
30
+ export type SqlRequestGridColumns<R> = Record<string, SqlRequestGridColumn<R>>;
31
+
32
+ export type SqlRequestGridProps<R> = {
33
+ itemTemplate: VirtualScrollerTemplateFC<R>;
34
+ itemsPerRow?: number;
35
+ rowHeight?: number;
36
+ gap?: string;
37
+ fields: SqlRequestGridColumns<R>;
38
+ additionalFields?: string[];
39
+ type: string;
40
+ orderBy?: OrderByDTO[];
41
+ conditions?: ConditionDTO[];
42
+ parser?: (row: SqlRequestRow<R>) => R;
43
+ };
@@ -0,0 +1,71 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ import { useElementSize } from '../../../hooks';
4
+
5
+ export const VIRTUAL_SCROLL_TOLERANCE = 20;
6
+
7
+ type VirtualScrollingProps<R> = {
8
+ scrollableElement: HTMLElement | null;
9
+ rowHeightInPx: number;
10
+ items: R[];
11
+ itemsPerRow?: number;
12
+ tolerance?: number;
13
+ };
14
+
15
+ export const useVirtualScrolling = <R>({
16
+ scrollableElement,
17
+ rowHeightInPx,
18
+ items,
19
+ itemsPerRow = 1,
20
+ tolerance = VIRTUAL_SCROLL_TOLERANCE,
21
+ }: VirtualScrollingProps<R>) => {
22
+ const { height } = useElementSize(scrollableElement);
23
+
24
+ const [scrollTop, setScrollTop] = useState(0);
25
+ const [rowIndex, rowLength] = [
26
+ Math.floor(scrollTop / rowHeightInPx),
27
+ Math.ceil(height / rowHeightInPx),
28
+ ];
29
+ const [index, length] = [rowIndex * itemsPerRow, rowLength * itemsPerRow];
30
+ const rowTolerance = Math.ceil(tolerance / itemsPerRow);
31
+
32
+ const totalRows = Math.ceil((items?.length ?? 0) / itemsPerRow);
33
+ const totalHeight = totalRows * rowHeightInPx;
34
+ const topPadding = Math.max(0, rowIndex - rowTolerance) * rowHeightInPx;
35
+
36
+ const onScroll = useCallback(() => {
37
+ if (scrollableElement) {
38
+ setScrollTop(scrollableElement.scrollTop);
39
+ }
40
+ }, [scrollableElement]);
41
+
42
+ useEffect(() => {
43
+ const scrollable = scrollableElement;
44
+ if (scrollable) {
45
+ scrollable.addEventListener('scroll', onScroll);
46
+ return () => {
47
+ scrollable.removeEventListener('scroll', onScroll);
48
+ };
49
+ }
50
+ }, [onScroll, scrollableElement]);
51
+
52
+ const rowIndexWithTolerance = Math.max(0, rowIndex - rowTolerance);
53
+ const rowLengthWithTolerance = rowLength + 2 * rowTolerance;
54
+ const indexWithTolerance = rowIndexWithTolerance * itemsPerRow;
55
+ const lengthWithTolerance = rowLengthWithTolerance * itemsPerRow;
56
+
57
+ const visibleItems = items.slice(
58
+ indexWithTolerance,
59
+ indexWithTolerance + lengthWithTolerance
60
+ );
61
+
62
+ return {
63
+ index,
64
+ length,
65
+ indexWithTolerance,
66
+ lengthWithTolerance,
67
+ visibleItems,
68
+ totalHeight,
69
+ topPadding,
70
+ };
71
+ };
@@ -0,0 +1,75 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
2
+
3
+ import * as styles from './styles';
4
+
5
+ import { useEffect, useState } from 'react';
6
+
7
+ import { VirtualScrollerTemplateFC } from './types';
8
+ import { useVirtualScrolling } from './hooks';
9
+
10
+ type VirtualScrollerProps<R> = {
11
+ gridTemplateColumns: string;
12
+ items: R[];
13
+ itemTemplate: VirtualScrollerTemplateFC<R>;
14
+ rowHeightInPx?: number;
15
+ gap?: string;
16
+ itemsPerRow?: number;
17
+ tolerance?: number;
18
+ itemProps?: object;
19
+ onRangeChanged?: (index: number, length: number) => void;
20
+ };
21
+
22
+ export const VirtualScroller = <R,>({
23
+ gridTemplateColumns,
24
+ items,
25
+ rowHeightInPx = styles.DEFAULT_ROW_HEIGHT,
26
+ itemTemplate: ItemTemplate,
27
+ itemProps = {},
28
+ onRangeChanged,
29
+ gap,
30
+ itemsPerRow,
31
+ tolerance,
32
+ }: VirtualScrollerProps<R>) => {
33
+ const [scrollableElement, setScrollableElement] =
34
+ useState<HTMLElement | null>(null);
35
+ const {
36
+ indexWithTolerance,
37
+ lengthWithTolerance,
38
+ visibleItems,
39
+ topPadding,
40
+ totalHeight,
41
+ } = useVirtualScrolling<R>({
42
+ scrollableElement,
43
+ rowHeightInPx,
44
+ items,
45
+ itemsPerRow,
46
+ tolerance,
47
+ });
48
+
49
+ useEffect(() => {
50
+ onRangeChanged?.(indexWithTolerance, lengthWithTolerance);
51
+ }, [indexWithTolerance, lengthWithTolerance, onRangeChanged]);
52
+
53
+ return (
54
+ <styles.VirtualScrollerContainer
55
+ $height={totalHeight}
56
+ ref={(element) => setScrollableElement(element?.parentElement ?? null)}
57
+ >
58
+ <styles.VirtualScrollerItemsContainer
59
+ $gridTemplateColumns={gridTemplateColumns}
60
+ $topPadding={topPadding}
61
+ $itemHeight={rowHeightInPx}
62
+ $gap={gap}
63
+ >
64
+ {visibleItems.map((item, currentIndex) => (
65
+ <ItemTemplate
66
+ key={indexWithTolerance + currentIndex}
67
+ item={item}
68
+ index={currentIndex}
69
+ {...itemProps}
70
+ />
71
+ ))}
72
+ </styles.VirtualScrollerItemsContainer>
73
+ </styles.VirtualScrollerContainer>
74
+ );
75
+ };
@@ -0,0 +1,51 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const DEFAULT_ROW_HEIGHT = 32;
4
+
5
+ export const TopPaddingItem = styled.div``;
6
+ export const BottomPaddingItem = styled.div``;
7
+
8
+ export const VirtualScrollerContainer = styled.div.attrs<{
9
+ $height: number;
10
+ }>(({ $height }) => ({
11
+ className: 'VirtualScrollerFiller',
12
+ style: {
13
+ height: `${$height}px`,
14
+ },
15
+ }))`
16
+ position: relative;
17
+ overflow: visible;
18
+ `;
19
+
20
+ export const VirtualScrollerItemsContainer = styled.div.attrs<{
21
+ $gridTemplateColumns: string;
22
+ $gap?: string;
23
+ $topPadding: number;
24
+ $itemHeight: number;
25
+ }>(({ $gridTemplateColumns, $topPadding, $itemHeight }) => {
26
+ const itemHeightValue = `${$itemHeight}px`;
27
+ return {
28
+ className: 'VirtualScrollerItemsContainer',
29
+ style: {
30
+ top: `${$topPadding}px`,
31
+ gridTemplateColumns: $gridTemplateColumns,
32
+ gridAutoItems: itemHeightValue,
33
+ },
34
+ };
35
+ })`
36
+ display: grid;
37
+ position: absolute;
38
+ min-width: 100%;
39
+ grid-gap: ${({ $gap }) => $gap};
40
+
41
+ ${TopPaddingItem} {
42
+ grid-column-start: 1;
43
+ grid-column-end: -1;
44
+ grid-row: 1;
45
+ }
46
+ ${BottomPaddingItem} {
47
+ grid-column-start: 1;
48
+ grid-column-end: -1;
49
+ grid-row: -1;
50
+ }
51
+ `;
@@ -0,0 +1,8 @@
1
+ import { FC } from 'react';
2
+
3
+ export type VirtualScrollerTemplateProps<R> = {
4
+ item: R | null;
5
+ index: number;
6
+ };
7
+
8
+ export type VirtualScrollerTemplateFC<R> = FC<VirtualScrollerTemplateProps<R>>;
@@ -5,4 +5,6 @@ export * from './AdvancedRequestDataGrid';
5
5
  export * from './SqlRequestDataGrid';
6
6
  export * from './SqlRequestDataGrid/helpers';
7
7
  export * from './SqlRequestDataGrid/types';
8
+ export * from './SqlRequestGrid';
9
+ export * from './SqlRequestGrid/types';
8
10
  export * from './AdvancedRequestDataGrid/helpers';
@@ -6,8 +6,8 @@ type CardFC = FC<HTMLAttributes<HTMLDivElement>> & {
6
6
  Footer: typeof CardFooter;
7
7
  };
8
8
 
9
- export const Card: CardFC = ({ children }) => {
10
- return <CardContainer>{children}</CardContainer>;
9
+ export const Card: CardFC = ({ children, ...props }) => {
10
+ return <CardContainer {...props}>{children}</CardContainer>;
11
11
  };
12
12
 
13
13
  Card.Header = CardHeader;
@@ -1,47 +0,0 @@
1
- import * as styles from './styles';
2
-
3
- import { DataGridContext, DataGridRowTemplateProps } from './types';
4
- import { FC, useContext } from 'react';
5
-
6
- type VirtualScrollerProps<R> = {
7
- showAllRows?: boolean;
8
- rowTemplate: FC<DataGridRowTemplateProps<R>>;
9
- hasFooter?: boolean;
10
- context: DataGridContext<R>;
11
- onRangeChange?: (startIndex: number, length: number) => void;
12
- };
13
-
14
- export const VirtualScroller = <R,>(props: VirtualScrollerProps<R>) => {
15
- const {
16
- rowHeight = styles.DEFAULT_ROW_HEIGHT,
17
- // headerRowHeight = styles.DEFAULT_HEADER_ROW_HEIGHT,
18
- sortedRows,
19
- index,
20
- visibleRows,
21
- gridTemplateColumns,
22
- } = useContext(props.context);
23
- const {
24
- rowTemplate: RowTemplate,
25
- // hasFooter, onRangeChange
26
- } = props;
27
-
28
- const totalHeight = sortedRows.length * rowHeight;
29
- const topPadding =
30
- Math.max(0, index - styles.VIRTUAL_SCROLL_TOLERANCE) * rowHeight;
31
- // const headerAndFooterHeight =
32
- // 2 * headerRowHeight + (hasFooter ? rowHeight : 0) + 2;
33
-
34
- return (
35
- <styles.VirtualScrollerContainer $height={totalHeight}>
36
- <styles.VirtualScrollerRowsContainer
37
- $gridTemplateColumns={gridTemplateColumns}
38
- $topPadding={topPadding}
39
- $rowHeight={rowHeight}
40
- >
41
- {visibleRows.map((row, index) => (
42
- <RowTemplate row={row} rowIndex={index} context={props.context} />
43
- ))}
44
- </styles.VirtualScrollerRowsContainer>
45
- </styles.VirtualScrollerContainer>
46
- );
47
- };