@etsoo/react 1.4.17 → 1.4.21

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.
@@ -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,16 @@ import {
16
17
  DataGridExProps
17
18
  } from './DataGridEx';
18
19
  import { GridMethodRef } from './GridMethodRef';
20
+ import { PullToRefreshUI } from './PullToRefreshUI';
19
21
  import {
20
22
  ScrollerListEx,
21
23
  ScrollerListExInnerItemRendererProps
22
24
  } from './ScrollerListEx';
23
25
  import { SearchBar } from './SearchBar';
24
26
 
27
+ /**
28
+ * ResponsibleContainer props
29
+ */
25
30
  export interface ResponsibleContainerProps<
26
31
  T extends {},
27
32
  F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate
@@ -99,6 +104,11 @@ export interface ResponsibleContainerProps<
99
104
  */
100
105
  mRef?: React.MutableRefObject<GridMethodRef | undefined>;
101
106
 
107
+ /**
108
+ * Pull to refresh data
109
+ */
110
+ pullToRefresh?: boolean;
111
+
102
112
  /**
103
113
  * Searchbox SX
104
114
  */
@@ -111,10 +121,16 @@ export interface ResponsibleContainerProps<
111
121
  }
112
122
 
113
123
  interface LocalRefs {
114
- height?: number;
124
+ rect?: DOMRect;
115
125
  ref?: GridMethodRef;
126
+ mounted?: boolean;
116
127
  }
117
128
 
129
+ /**
130
+ * Responsible container
131
+ * @param props Props
132
+ * @returns Layout
133
+ */
118
134
  export function ResponsibleContainer<
119
135
  T extends {},
120
136
  F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate
@@ -130,11 +146,15 @@ export function ResponsibleContainer<
130
146
  listBoxSx,
131
147
  loadData,
132
148
  mRef,
149
+ pullToRefresh = true,
133
150
  searchBoxSx,
134
151
  sizeReadyMiliseconds = 0,
135
152
  ...rest
136
153
  } = props;
137
154
 
155
+ // Labels
156
+ const labels = Labels.CommonPage;
157
+
138
158
  // Refs
139
159
  const refs = React.useRef<LocalRefs>({});
140
160
 
@@ -143,58 +163,107 @@ export function ResponsibleContainer<
143
163
  refs.current.ref = ref;
144
164
  });
145
165
 
166
+ // Update mounted state
167
+ React.useEffect(() => {
168
+ return () => {
169
+ refs.current.mounted = false;
170
+ };
171
+ }, []);
172
+
146
173
  // Has fields
147
174
  const hasFields = fields != null && fields.length > 0;
148
175
 
176
+ // Load data
177
+ const localLoadData = (props: GridLoadDataProps) => {
178
+ refs.current.mounted = true;
179
+ const data = GridDataGet(props, fieldTemplate);
180
+ return loadData(data);
181
+ };
182
+
183
+ // On submit callback
184
+ const onSubmit = (data: FormData, _reset: boolean) => {
185
+ if (data == null || rect == null || refs.current.ref == null) return;
186
+ refs.current.ref.reset({ data });
187
+ };
188
+
149
189
  // Watch container
150
- const { dimensions } = useDimensions(1, undefined, sizeReadyMiliseconds);
190
+ const { dimensions } = useDimensions(
191
+ 1,
192
+ undefined,
193
+ sizeReadyMiliseconds,
194
+ (_preRect, rect) => {
195
+ // Check
196
+ if (rect == null) return true;
197
+
198
+ // Last rect
199
+ const lastRect = refs.current.rect;
200
+
201
+ // 32 = scroll bar width
202
+ if (
203
+ lastRect != null &&
204
+ refs.current.mounted !== true &&
205
+ Math.abs(rect.width - lastRect.width) <= 32 &&
206
+ Math.abs(rect.height - lastRect.height) <= 32
207
+ )
208
+ return true;
209
+
210
+ // Hold the new rect
211
+ refs.current.rect = rect;
212
+
213
+ return false;
214
+ }
215
+ );
216
+
217
+ // Rect
151
218
  const rect = dimensions[0][2];
152
- const showDataGrid = (rect?.width ?? 0) >= dataGridMinWidth;
153
219
 
154
- React.useEffect(() => {
155
- if (rect != null && rect.height > 50 && height == null) {
156
- let gridHeight =
220
+ // Create list
221
+ const [list, showDataGrid] = (() => {
222
+ // No layout
223
+ if (rect == null) return [null, false];
224
+
225
+ // Width
226
+ const width = rect.width;
227
+
228
+ // Show DataGrid or List dependng on width
229
+ const showDataGrid = width >= dataGridMinWidth;
230
+
231
+ // Height
232
+ let heightLocal: number;
233
+ if (height != null) {
234
+ heightLocal = height;
235
+ } else {
236
+ // Auto calculation
237
+ heightLocal =
157
238
  window.innerHeight - Math.round(rect.top + rect.height + 1);
158
239
 
159
240
  const style = window.getComputedStyle(dimensions[0][1]!);
160
241
  const boxPadding = parseFloat(style.paddingLeft);
161
- if (!isNaN(boxPadding)) gridHeight -= 2 * boxPadding;
242
+ if (!isNaN(boxPadding)) heightLocal -= 2 * boxPadding;
162
243
 
163
244
  if (adjustHeight != null) {
164
- gridHeight -= adjustHeight(gridHeight);
165
- }
166
-
167
- if (gridHeight !== refs.current.height) {
168
- refs.current.height = gridHeight;
245
+ heightLocal -= adjustHeight(heightLocal);
169
246
  }
170
247
  }
171
- }, [rect]);
172
-
173
- const localLoadData = (props: GridLoadDataProps) => {
174
- const data = GridDataGet(props, fieldTemplate);
175
- return loadData(data);
176
- };
177
-
178
- const gridHeight = refs.current.height;
179
- const list = React.useMemo(() => {
180
- if (gridHeight == null) return;
181
248
 
182
249
  if (showDataGrid) {
183
250
  // Delete
184
251
  delete rest.itemRenderer;
185
252
 
186
- return (
253
+ return [
187
254
  <Box sx={listBoxSx == null ? undefined : listBoxSx(true)}>
188
255
  <DataGridEx<T>
189
256
  autoLoad={!hasFields}
190
- height={gridHeight}
257
+ height={heightLocal}
258
+ width={rect.width}
191
259
  loadData={localLoadData}
192
260
  mRef={mRefs}
193
261
  columns={columns}
194
262
  {...rest}
195
263
  />
196
- </Box>
197
- );
264
+ </Box>,
265
+ true
266
+ ];
198
267
  }
199
268
 
200
269
  // Delete
@@ -207,39 +276,53 @@ export function ResponsibleContainer<
207
276
  delete rest.hoverColor;
208
277
  delete rest.selectable;
209
278
 
210
- return (
279
+ return [
211
280
  <Box sx={listBoxSx == null ? undefined : listBoxSx(false)}>
212
281
  <ScrollerListEx<T>
213
282
  autoLoad={!hasFields}
214
- height={gridHeight}
283
+ width={rect.width}
284
+ height={heightLocal}
215
285
  loadData={localLoadData}
216
286
  mRef={mRefs}
217
287
  {...rest}
218
288
  />
219
- </Box>
220
- );
221
- }, [gridHeight, showDataGrid]);
222
-
223
- // On submit callback
224
- const onSubmit = (data: FormData, _reset: boolean) => {
225
- console.log(data, rect, refs.current.ref, showDataGrid);
226
- if (data == null || rect == null || refs.current.ref == null) return;
227
- refs.current.ref.reset({ data });
228
- };
229
-
230
- React.useEffect(() => {
231
- console.log('useEffect', rect, refs.current.ref, showDataGrid);
232
- }, []);
289
+ </Box>,
290
+ false
291
+ ];
292
+ })();
293
+
294
+ // Pull container
295
+ const pullContainer = list
296
+ ? showDataGrid
297
+ ? '.DataGridEx-Body'
298
+ : '.ScrollerListEx-Body'
299
+ : undefined;
233
300
 
234
301
  // Layout
235
302
  return (
236
- <Stack>
237
- {hasFields && (
303
+ <React.Fragment>
304
+ <Stack>
238
305
  <Box ref={dimensions[0][0]} sx={searchBoxSx}>
239
- <SearchBar fields={fields} onSubmit={onSubmit} />
306
+ {hasFields && (
307
+ <SearchBar fields={fields} onSubmit={onSubmit} />
308
+ )}
240
309
  </Box>
310
+ {list}
311
+ </Stack>
312
+ {pullToRefresh && pullContainer && (
313
+ <PullToRefreshUI
314
+ mainElement={pullContainer}
315
+ triggerElement={pullContainer}
316
+ instructionsPullToRefresh={labels.pullToRefresh}
317
+ instructionsReleaseToRefresh={labels.releaseToRefresh}
318
+ instructionsRefreshing={labels.refreshing}
319
+ onRefresh={() => refs.current.ref?.reset()}
320
+ shouldPullToRefresh={() => {
321
+ const container = document.querySelector(pullContainer);
322
+ return !container?.scrollTop;
323
+ }}
324
+ />
241
325
  )}
242
- {list}
243
- </Stack>
326
+ </React.Fragment>
244
327
  );
245
328
  }
@@ -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
 
@@ -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[];