@etsoo/react 1.4.20 → 1.4.24

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.
@@ -112,25 +112,27 @@ export namespace DataGridRenderers {
112
112
 
113
113
  /**
114
114
  * Default footer item renderer
115
- * @param param Props
115
+ * @param rows Rows
116
+ * @param props Renderer props
117
+ * @param location Renderer location (column index)
116
118
  * @returns Component
117
119
  */
118
- export function defaultFooterItemRenderer<T>({
119
- index,
120
- states,
121
- checkable
122
- }: DataGridExFooterItemRendererProps<T>) {
123
- const { selectedItems, rows, hasNextPage } = states;
124
-
125
- if (checkable && index === 1) {
126
- return [
127
- selectedItems.length,
128
- rows.length.toLocaleString() + (hasNextPage ? '+' : '')
129
- ].join(' / ');
130
- }
131
-
132
- if (!checkable && index === 0) {
133
- return rows.length.toLocaleString() + (hasNextPage ? '+' : '');
120
+ export function defaultFooterItemRenderer<T>(
121
+ _rows: T[],
122
+ { index, states, checkable }: DataGridExFooterItemRendererProps<T>,
123
+ location: number = 0
124
+ ) {
125
+ const { selectedItems, loadedItems, hasNextPage } = states;
126
+
127
+ if (index === location) {
128
+ if (checkable) {
129
+ return [
130
+ selectedItems.length,
131
+ loadedItems.toLocaleString() + (hasNextPage ? '+' : '')
132
+ ].join(' / ');
133
+ } else {
134
+ return loadedItems.toLocaleString() + (hasNextPage ? '+' : '');
135
+ }
134
136
  }
135
137
 
136
138
  return undefined;
@@ -1,3 +1,5 @@
1
+ import { GridLoaderStates } from '../components/GridLoader';
2
+
1
3
  /**
2
4
  * Grid method ref
3
5
  */
@@ -6,5 +8,5 @@ export interface GridMethodRef {
6
8
  * Reset
7
9
  * @param add Additional data
8
10
  */
9
- reset(add?: {}): void;
11
+ reset(add?: Partial<GridLoaderStates<unknown>>): void;
10
12
  }
@@ -2,6 +2,7 @@ import { DataTypes } from '@etsoo/shared';
2
2
  import { Box, Stack, SxProps, Theme } from '@mui/material';
3
3
  import React from 'react';
4
4
  import { ListChildComponentProps } from 'react-window';
5
+ import { Labels } from '../app/Labels';
5
6
  import { GridColumn } from '../components/GridColumn';
6
7
  import {
7
8
  GridDataGet,
@@ -16,12 +17,17 @@ import {
16
17
  DataGridExProps
17
18
  } from './DataGridEx';
18
19
  import { GridMethodRef } from './GridMethodRef';
20
+ import { MUGlobal } from './MUGlobal';
21
+ import { PullToRefreshUI } from './PullToRefreshUI';
19
22
  import {
20
23
  ScrollerListEx,
21
24
  ScrollerListExInnerItemRendererProps
22
25
  } from './ScrollerListEx';
23
26
  import { SearchBar } from './SearchBar';
24
27
 
28
+ /**
29
+ * ResponsibleContainer props
30
+ */
25
31
  export interface ResponsibleContainerProps<
26
32
  T extends {},
27
33
  F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate
@@ -99,6 +105,16 @@ export interface ResponsibleContainerProps<
99
105
  */
100
106
  mRef?: React.MutableRefObject<GridMethodRef | undefined>;
101
107
 
108
+ /**
109
+ * Paddings
110
+ */
111
+ paddings?: {};
112
+
113
+ /**
114
+ * Pull to refresh data
115
+ */
116
+ pullToRefresh?: boolean;
117
+
102
118
  /**
103
119
  * Searchbox SX
104
120
  */
@@ -111,10 +127,16 @@ export interface ResponsibleContainerProps<
111
127
  }
112
128
 
113
129
  interface LocalRefs {
114
- height?: number;
130
+ rect?: DOMRect;
115
131
  ref?: GridMethodRef;
132
+ mounted?: boolean;
116
133
  }
117
134
 
135
+ /**
136
+ * Responsible container
137
+ * @param props Props
138
+ * @returns Layout
139
+ */
118
140
  export function ResponsibleContainer<
119
141
  T extends {},
120
142
  F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate
@@ -130,78 +152,125 @@ export function ResponsibleContainer<
130
152
  listBoxSx,
131
153
  loadData,
132
154
  mRef,
133
- searchBoxSx,
155
+ paddings = MUGlobal.pagePaddings,
156
+ pullToRefresh = true,
157
+ searchBoxSx = { marginBottom: MUGlobal.half(paddings) },
134
158
  sizeReadyMiliseconds = 0,
135
159
  ...rest
136
160
  } = props;
137
161
 
162
+ // Labels
163
+ const labels = Labels.CommonPage;
164
+
138
165
  // Refs
139
166
  const refs = React.useRef<LocalRefs>({});
167
+ const state = refs.current;
140
168
 
141
169
  const mRefs = useCombinedRefs(mRef, (ref: GridMethodRef) => {
142
170
  if (ref == null) return;
143
- refs.current.ref = ref;
171
+ state.ref = ref;
144
172
  });
145
173
 
174
+ // Update mounted state
175
+ React.useEffect(() => {
176
+ return () => {
177
+ state.mounted = false;
178
+ };
179
+ }, []);
180
+
146
181
  // Has fields
147
182
  const hasFields = fields != null && fields.length > 0;
148
183
 
149
- // Watch container
150
- const { dimensions } = useDimensions(1, undefined, sizeReadyMiliseconds);
151
- const rect = dimensions[0][2];
152
-
184
+ // Load data
153
185
  const localLoadData = (props: GridLoadDataProps) => {
186
+ state.mounted = true;
154
187
  const data = GridDataGet(props, fieldTemplate);
155
188
  return loadData(data);
156
189
  };
157
190
 
158
- const list = React.useMemo(() => {
159
- // No rect
160
- if (rect == null) return;
191
+ // On submit callback
192
+ const onSubmit = (data: FormData, _reset: boolean) => {
193
+ if (data == null || state.ref == null) return;
194
+ state.ref.reset({ data });
195
+ };
161
196
 
162
- // If height is the same
197
+ // Watch container
198
+ const { dimensions } = useDimensions(
199
+ 1,
200
+ undefined,
201
+ sizeReadyMiliseconds,
202
+ (_preRect, rect) => {
203
+ // Check
204
+ if (rect == null) return true;
205
+
206
+ // Last rect
207
+ const lastRect = state.rect;
208
+
209
+ // 32 = scroll bar width
210
+ if (
211
+ lastRect != null &&
212
+ state.mounted !== true &&
213
+ Math.abs(rect.width - lastRect.width) <= 32 &&
214
+ Math.abs(rect.height - lastRect.height) <= 32
215
+ )
216
+ return true;
217
+
218
+ // Hold the new rect
219
+ state.rect = rect;
220
+
221
+ return false;
222
+ }
223
+ );
224
+
225
+ // Rect
226
+ const rect = dimensions[0][2];
227
+
228
+ // Create list
229
+ const [list, showDataGrid] = (() => {
230
+ // No layout
231
+ if (rect == null) return [null, false];
232
+
233
+ // Width
234
+ const width = rect.width;
235
+
236
+ // Show DataGrid or List dependng on width
237
+ const showDataGrid = width >= dataGridMinWidth;
238
+
239
+ // Height
163
240
  let heightLocal: number;
164
241
  if (height != null) {
165
242
  heightLocal = height;
166
243
  } else {
167
244
  // Auto calculation
168
- heightLocal =
169
- window.innerHeight - Math.round(rect.top + rect.height + 1);
245
+ heightLocal = window.innerHeight - Math.round(rect.bottom + 1);
170
246
 
171
247
  const style = window.getComputedStyle(dimensions[0][1]!);
172
- const boxPadding = parseFloat(style.paddingLeft);
173
- if (!isNaN(boxPadding)) heightLocal -= 2 * boxPadding;
248
+ const boxMargin = parseFloat(style.marginBottom);
249
+ if (!isNaN(boxMargin)) heightLocal -= 3 * boxMargin; // 1 - Box, 2 - Page bottom
174
250
 
175
251
  if (adjustHeight != null) {
176
252
  heightLocal -= adjustHeight(heightLocal);
177
253
  }
178
254
  }
179
255
 
180
- console.log(rect, heightLocal, refs.current.height);
181
-
182
- // Same height
183
- if (heightLocal === refs.current.height) return;
184
- refs.current.height = heightLocal;
185
-
186
- // Show DataGrid or List dependng on width
187
- const showDataGrid = rect.width >= dataGridMinWidth;
188
-
189
256
  if (showDataGrid) {
190
257
  // Delete
191
258
  delete rest.itemRenderer;
192
259
 
193
- return (
260
+ return [
194
261
  <Box sx={listBoxSx == null ? undefined : listBoxSx(true)}>
195
262
  <DataGridEx<T>
196
263
  autoLoad={!hasFields}
197
264
  height={heightLocal}
265
+ width={rect.width}
198
266
  loadData={localLoadData}
199
267
  mRef={mRefs}
200
268
  columns={columns}
201
269
  {...rest}
202
270
  />
203
- </Box>
204
- );
271
+ </Box>,
272
+ true
273
+ ];
205
274
  }
206
275
 
207
276
  // Delete
@@ -214,34 +283,53 @@ export function ResponsibleContainer<
214
283
  delete rest.hoverColor;
215
284
  delete rest.selectable;
216
285
 
217
- return (
286
+ return [
218
287
  <Box sx={listBoxSx == null ? undefined : listBoxSx(false)}>
219
288
  <ScrollerListEx<T>
220
289
  autoLoad={!hasFields}
290
+ width={rect.width}
221
291
  height={heightLocal}
222
292
  loadData={localLoadData}
223
293
  mRef={mRefs}
224
294
  {...rest}
225
295
  />
226
- </Box>
227
- );
228
- }, [rect, height]);
229
-
230
- // On submit callback
231
- const onSubmit = (data: FormData, _reset: boolean) => {
232
- if (data == null || rect == null || refs.current.ref == null) return;
233
- refs.current.ref.reset({ data });
234
- };
296
+ </Box>,
297
+ false
298
+ ];
299
+ })();
300
+
301
+ // Pull container
302
+ const pullContainer = list
303
+ ? showDataGrid
304
+ ? '.DataGridEx-Body'
305
+ : '.ScrollerListEx-Body'
306
+ : undefined;
235
307
 
236
308
  // Layout
237
309
  return (
238
- <Stack ref={hasFields ? undefined : dimensions[0][0]}>
239
- {hasFields && (
310
+ <React.Fragment>
311
+ <Stack>
240
312
  <Box ref={dimensions[0][0]} sx={searchBoxSx}>
241
- <SearchBar fields={fields} onSubmit={onSubmit} />
313
+ {hasFields && (
314
+ <SearchBar fields={fields} onSubmit={onSubmit} />
315
+ )}
242
316
  </Box>
317
+ {list}
318
+ </Stack>
319
+ {pullToRefresh && pullContainer && (
320
+ <PullToRefreshUI
321
+ mainElement={pullContainer}
322
+ triggerElement={pullContainer}
323
+ instructionsPullToRefresh={labels.pullToRefresh}
324
+ instructionsReleaseToRefresh={labels.releaseToRefresh}
325
+ instructionsRefreshing={labels.refreshing}
326
+ onRefresh={() => state.ref?.reset()}
327
+ shouldPullToRefresh={() => {
328
+ const container = document.querySelector(pullContainer);
329
+ return !container?.scrollTop;
330
+ }}
331
+ />
243
332
  )}
244
- {list}
245
- </Stack>
333
+ </React.Fragment>
246
334
  );
247
335
  }
@@ -174,8 +174,7 @@ export function ScrollerListEx<T extends Record<string, unknown>>(
174
174
  if (selectedData != null && selectedData[idField] === data[idField])
175
175
  return;
176
176
 
177
- if (selectedDiv != null)
178
- selectedDiv.classList.remove(selectedClassName);
177
+ selectedDiv?.classList.remove(selectedClassName);
179
178
 
180
179
  div.classList.add(selectedClassName);
181
180
 
@@ -271,13 +271,13 @@ export function SearchBar(props: SearchBarProps) {
271
271
  };
272
272
 
273
273
  // Submit
274
- const handleSubmit = () => {
274
+ const handleSubmit = (delay = 480) => {
275
275
  if (forms.submitSeed != null) {
276
276
  clearTimeout(forms.submitSeed);
277
277
  }
278
278
 
279
279
  // Delay the change
280
- forms.submitSeed = window.setTimeout(handleSubmitInstant, 480);
280
+ forms.submitSeed = window.setTimeout(handleSubmitInstant, delay);
281
281
  };
282
282
 
283
283
  // Submit at once
@@ -297,7 +297,7 @@ export function SearchBar(props: SearchBarProps) {
297
297
  // First loading
298
298
  React.useEffect(() => {
299
299
  // Delayed way
300
- handleSubmit();
300
+ handleSubmit(100);
301
301
 
302
302
  return () => {
303
303
  // Clear the seeds
@@ -145,43 +145,41 @@ export function TableEx<T extends Record<string, unknown>>(
145
145
  rowsPerPageLocal = 10;
146
146
  }
147
147
 
148
+ // Rows
149
+ const [rows, updateRows] = React.useState<T[]>([]);
150
+ const setRows = (rows: T[]) => {
151
+ state.loadedItems = rows.length;
152
+ updateRows(rows);
153
+ };
154
+
148
155
  // States
149
- const [state, stateUpdate] = React.useReducer(
150
- (
151
- currentState: GridLoaderStates<T>,
152
- newState: Partial<GridLoaderStates<T>>
153
- ) => {
154
- return { ...currentState, ...newState };
155
- },
156
- {
157
- autoLoad,
158
- currentPage: 0,
159
- hasNextPage: true,
160
- isNextPageLoading: false,
161
- orderBy: defaultOrderBy,
162
- orderByAsc: defaultOrderBy
163
- ? columns.find((column) => column.field === defaultOrderBy)
164
- ?.sortAsc
165
- : undefined,
166
- rows: [],
167
- batchSize: rowsPerPageLocal,
168
- selectedItems: []
169
- }
170
- );
171
- const isMounted = React.useRef(true);
156
+ const stateRefs = React.useRef<GridLoaderStates<T>>({
157
+ autoLoad,
158
+ currentPage: 0,
159
+ loadedItems: 0,
160
+ hasNextPage: true,
161
+ isNextPageLoading: false,
162
+ orderBy: defaultOrderBy,
163
+ orderByAsc: defaultOrderBy
164
+ ? columns.find((column) => column.field === defaultOrderBy)?.sortAsc
165
+ : undefined,
166
+ batchSize: rowsPerPageLocal,
167
+ selectedItems: []
168
+ });
169
+ const state = stateRefs.current;
172
170
 
173
171
  // Reset the state and load again
174
- const reset = (add?: {}) => {
175
- const state = {
172
+ const reset = (add?: Partial<GridLoaderStates<T>>) => {
173
+ const resetState: Partial<GridLoaderStates<T>> = {
176
174
  autoLoad: true,
177
175
  currentPage: 0,
176
+ loadedItems: 0,
178
177
  hasNextPage: true,
179
178
  isNextPageLoading: false,
180
179
  lastLoadedItems: undefined,
181
- rows: [],
182
180
  ...add
183
181
  };
184
- stateUpdate(state);
182
+ Object.assign(state, resetState);
185
183
  };
186
184
 
187
185
  React.useImperativeHandle(
@@ -211,7 +209,8 @@ export function TableEx<T extends Record<string, unknown>>(
211
209
  state.isNextPageLoading = true;
212
210
 
213
211
  // Parameters
214
- const { currentPage, batchSize, orderBy, orderByAsc, data } = state;
212
+ const { currentPage, batchSize, orderBy, orderByAsc, data, isMounted } =
213
+ state;
215
214
 
216
215
  const loadProps: GridLoadDataProps = {
217
216
  currentPage,
@@ -222,18 +221,18 @@ export function TableEx<T extends Record<string, unknown>>(
222
221
  };
223
222
 
224
223
  loadData(loadProps).then((result) => {
225
- if (!isMounted.current || result == null) {
224
+ state.isMounted = true;
225
+ if (result == null || isMounted === false) {
226
226
  return;
227
227
  }
228
228
 
229
229
  const newItems = result.length;
230
+ state.lastLoadedItems = newItems;
231
+ state.hasNextPage = newItems >= batchSize;
232
+ state.isNextPageLoading = false;
230
233
 
231
- stateUpdate({
232
- rows: result,
233
- lastLoadedItems: newItems,
234
- hasNextPage: newItems >= batchSize,
235
- isNextPageLoading: false
236
- });
234
+ // Update rows
235
+ setRows(result);
237
236
  });
238
237
  };
239
238
 
@@ -246,12 +245,12 @@ export function TableEx<T extends Record<string, unknown>>(
246
245
  const handleChangeRowsPerPage = (
247
246
  event: React.ChangeEvent<HTMLInputElement>
248
247
  ) => {
249
- const rowsPerPage = parseInt(event.target.value);
250
- reset({ rowsPerPage });
248
+ const batchSize = parseInt(event.target.value);
249
+ reset({ batchSize });
251
250
  };
252
251
 
253
252
  const handleSelect = (item: T, checked: Boolean) => {
254
- const selectedItems = [...state.selectedItems];
253
+ const selectedItems = state.selectedItems;
255
254
 
256
255
  const index = selectedItems.findIndex(
257
256
  (selectedItem) => selectedItem[idField] === item[idField]
@@ -265,14 +264,12 @@ export function TableEx<T extends Record<string, unknown>>(
265
264
  if (onSelectChange != null) {
266
265
  onSelectChange(selectedItems);
267
266
  }
268
-
269
- stateUpdate({ selectedItems });
270
267
  };
271
268
 
272
269
  const handleSelectAll = (checked: boolean) => {
273
- const selectedItems = [...state.selectedItems];
270
+ const selectedItems = state.selectedItems;
274
271
 
275
- state.rows.forEach((row) => {
272
+ rows.forEach((row) => {
276
273
  const index = selectedItems.findIndex(
277
274
  (selectedItem) => selectedItem[idField] === row[idField]
278
275
  );
@@ -287,8 +284,6 @@ export function TableEx<T extends Record<string, unknown>>(
287
284
  if (onSelectChange != null) {
288
285
  onSelectChange(selectedItems);
289
286
  }
290
-
291
- stateUpdate({ selectedItems });
292
287
  };
293
288
 
294
289
  // New sort
@@ -303,7 +298,6 @@ export function TableEx<T extends Record<string, unknown>>(
303
298
  hasNextPage,
304
299
  lastLoadedItems,
305
300
  orderBy,
306
- rows,
307
301
  batchSize,
308
302
  selectedItems
309
303
  } = state;
@@ -333,7 +327,7 @@ export function TableEx<T extends Record<string, unknown>>(
333
327
 
334
328
  React.useEffect(() => {
335
329
  return () => {
336
- isMounted.current = false;
330
+ state.isMounted = false;
337
331
  };
338
332
  }, []);
339
333
 
@@ -1,17 +1,6 @@
1
1
  import { DomUtils } from '@etsoo/shared';
2
2
  import React from 'react';
3
3
 
4
- const createRef = (
5
- source: [React.RefCallback<Element>, Element?, DOMRect?][],
6
- index: number
7
- ): [React.RefCallback<Element>] => {
8
- return [
9
- (instance) => {
10
- if (instance != null) source[index][1] = instance;
11
- }
12
- ];
13
- };
14
-
15
4
  interface states {
16
5
  count: number;
17
6
  indices: number[];