@etsoo/react 1.4.10 → 1.4.14

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.
@@ -96,7 +96,7 @@ export class ReactApp extends CoreApp {
96
96
  if (startPos > 0) {
97
97
  const main = message.substring(0, startPos).trim();
98
98
  const tip = message.substring(startPos);
99
- const titleNode = React.createElement(React.Fragment, null, main, React.createElement('div', { style: { fontSize: '9px' } }, tip));
99
+ const titleNode = React.createElement(React.Fragment, null, main, React.createElement('br'), React.createElement('span', { style: { fontSize: '9px' } }, tip));
100
100
  this.notifier.alert(titleNode, callback);
101
101
  return;
102
102
  }
package/lib/index.d.ts CHANGED
@@ -48,6 +48,7 @@ export * from './mu/ItemList';
48
48
  export * from './mu/ListItemRightIcon';
49
49
  export * from './mu/LoadingButton';
50
50
  export * from './mu/MaskInput';
51
+ export * from './mu/MobileListItemRenderer';
51
52
  export * from './mu/MoreFab';
52
53
  export * from './mu/MUGlobal';
53
54
  export * from './mu/NotifierMU';
@@ -55,6 +56,7 @@ export * from './mu/OptionGroup';
55
56
  export * from './mu/PList';
56
57
  export * from './mu/ProgressCount';
57
58
  export * from './mu/PullToRefreshUI';
59
+ export * from './mu/ResponsibleContainer';
58
60
  export * from './mu/RLink';
59
61
  export * from './mu/ScrollerListEx';
60
62
  export * from './mu/ScrollTopFab';
package/lib/index.js CHANGED
@@ -51,6 +51,7 @@ export * from './mu/ItemList';
51
51
  export * from './mu/ListItemRightIcon';
52
52
  export * from './mu/LoadingButton';
53
53
  export * from './mu/MaskInput';
54
+ export * from './mu/MobileListItemRenderer';
54
55
  export * from './mu/MoreFab';
55
56
  export * from './mu/MUGlobal';
56
57
  export * from './mu/NotifierMU';
@@ -58,6 +59,7 @@ export * from './mu/OptionGroup';
58
59
  export * from './mu/PList';
59
60
  export * from './mu/ProgressCount';
60
61
  export * from './mu/PullToRefreshUI';
62
+ export * from './mu/ResponsibleContainer';
61
63
  export * from './mu/RLink';
62
64
  export * from './mu/ScrollerListEx';
63
65
  export * from './mu/ScrollTopFab';
@@ -36,10 +36,14 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
36
36
  * Data change handler
37
37
  */
38
38
  onChange?: (items: D[]) => void;
39
+ /**
40
+ * Drag end handler
41
+ */
42
+ onDragEnd?: (items: D[]) => void;
39
43
  /**
40
44
  * Top and bottom sides renderer
41
45
  */
42
- sideRenderer?: (top: boolean, addItem: (item: D) => boolean, addItems: (items: D[]) => number) => React.ReactNode;
46
+ sideRenderer?: (top: boolean, addItem: (item: D) => boolean, addItems: (items: D[]) => number, reloadItems: () => void) => React.ReactNode;
43
47
  /**
44
48
  * Name for hidden form input
45
49
  */
package/lib/mu/DnDList.js CHANGED
@@ -9,7 +9,7 @@ import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
9
9
  */
10
10
  export function DnDList(props) {
11
11
  // Destruct
12
- const { children, Component = 'div', componentProps, getItemStyle = (_isDragging) => ({}), getListStyle = () => undefined, labelField, loadData, name, onChange, sideRenderer } = props;
12
+ const { children, Component = 'div', componentProps, getItemStyle = (_isDragging) => ({}), getListStyle = () => undefined, labelField, loadData, name, onChange, onDragEnd, sideRenderer } = props;
13
13
  // State
14
14
  const [items, setItems] = React.useState([]);
15
15
  const changeItems = (items) => {
@@ -20,7 +20,7 @@ export function DnDList(props) {
20
20
  setItems(items);
21
21
  };
22
22
  // Drag end handler
23
- const onDragEnd = (result) => {
23
+ const onDragEndLocal = (result) => {
24
24
  // Dropped outside the list
25
25
  if (!result.destination) {
26
26
  return;
@@ -33,6 +33,9 @@ export function DnDList(props) {
33
33
  newItems.splice(result.destination.index, 0, removed);
34
34
  // Update the state
35
35
  changeItems(newItems);
36
+ // Drag end handler
37
+ if (onDragEnd)
38
+ onDragEnd(newItems);
36
39
  };
37
40
  // Add item
38
41
  const addItem = (newItem) => {
@@ -87,15 +90,20 @@ export function DnDList(props) {
87
90
  // Update the state
88
91
  changeItems(newItems);
89
92
  };
90
- React.useEffect(() => {
93
+ // Reload items
94
+ const reloadItems = () => {
91
95
  // Load data
92
96
  loadData(name).then((items) => setItems(items));
97
+ };
98
+ React.useEffect(() => {
99
+ // Load data
100
+ reloadItems();
93
101
  }, [name]);
94
102
  // Layout
95
103
  return (React.createElement(React.Fragment, null,
96
- sideRenderer && sideRenderer(true, addItem, addItems),
104
+ sideRenderer && sideRenderer(true, addItem, addItems, reloadItems),
97
105
  React.createElement(Component, { ...componentProps },
98
- React.createElement(DragDropContext, { onDragEnd: onDragEnd },
106
+ React.createElement(DragDropContext, { onDragEnd: onDragEndLocal },
99
107
  React.createElement(Droppable, { droppableId: name }, (provided, snapshot) => (React.createElement("div", { ...provided.droppableProps, ref: provided.innerRef, style: getListStyle(snapshot.isDraggingOver) },
100
108
  items.map((item, index) => {
101
109
  // Id
@@ -110,5 +118,6 @@ export function DnDList(props) {
110
118
  } }, children(item, index, deleteItem, editItem)))));
111
119
  }),
112
120
  provided.placeholder))))),
113
- sideRenderer && sideRenderer(false, addItem, addItems)));
121
+ sideRenderer &&
122
+ sideRenderer(false, addItem, addItems, reloadItems)));
114
123
  }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { ListItemReact } from '../components/ListItemReact';
3
+ import { ScrollerListExInnerItemRendererProps } from './ScrollerListEx';
4
+ /**
5
+ * Default mobile list item renderer
6
+ * @param param0 List renderer props
7
+ * @param margin Margin
8
+ * @param renderer Renderer for card content
9
+ * @returns Component
10
+ */
11
+ export declare function MobileListItemRenderer<T>({ data, itemHeight }: ScrollerListExInnerItemRendererProps<T>, margin: {}, renderer: (data: T) => [
12
+ string,
13
+ string | undefined,
14
+ React.ReactNode | (ListItemReact | boolean)[],
15
+ React.ReactNode
16
+ ]): JSX.Element;
@@ -0,0 +1,70 @@
1
+ import { Card, CardContent, CardHeader, LinearProgress } from '@mui/material';
2
+ import React from 'react';
3
+ import { MoreFab } from './MoreFab';
4
+ import { MUGlobal } from './MUGlobal';
5
+ function getActions(input) {
6
+ // Actions
7
+ const actions = [];
8
+ input.forEach((action) => {
9
+ if (typeof action === 'boolean')
10
+ return;
11
+ actions.push(action);
12
+ });
13
+ return actions;
14
+ }
15
+ /**
16
+ * Default mobile list item renderer
17
+ * @param param0 List renderer props
18
+ * @param margin Margin
19
+ * @param renderer Renderer for card content
20
+ * @returns Component
21
+ */
22
+ export function MobileListItemRenderer({ data, itemHeight }, margin, renderer) {
23
+ // Loading
24
+ if (data == null)
25
+ return React.createElement(LinearProgress, null);
26
+ // Elements
27
+ const [title, subheader, actions, children] = renderer(data);
28
+ // Half
29
+ const halfMargin = MUGlobal.half(margin);
30
+ return (React.createElement(Card, { sx: {
31
+ height: (theme) => MUGlobal.adjustWithTheme(itemHeight, margin, theme.spacing),
32
+ marginLeft: margin,
33
+ marginRight: margin,
34
+ marginTop: halfMargin,
35
+ marginBottom: halfMargin
36
+ } },
37
+ React.createElement(CardHeader, { sx: { paddingBottom: 0.5 }, action: Array.isArray(actions) ? (React.createElement(MoreFab, { iconButton: true, size: "small", anchorOrigin: {
38
+ vertical: 'bottom',
39
+ horizontal: 'right'
40
+ }, transformOrigin: {
41
+ vertical: 'top',
42
+ horizontal: 'right'
43
+ }, actions: getActions(actions), PaperProps: {
44
+ elevation: 0,
45
+ sx: {
46
+ overflow: 'visible',
47
+ filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
48
+ mt: -0.4,
49
+ '& .MuiAvatar-root': {
50
+ width: 32,
51
+ height: 32,
52
+ ml: -0.5,
53
+ mr: 1
54
+ },
55
+ '&:before': {
56
+ content: '""',
57
+ display: 'block',
58
+ position: 'absolute',
59
+ top: 0,
60
+ right: 14,
61
+ width: 10,
62
+ height: 10,
63
+ bgcolor: 'background.paper',
64
+ transform: 'translateY(-50%) rotate(45deg)',
65
+ zIndex: 0
66
+ }
67
+ }
68
+ } })) : (actions), title: title, titleTypographyProps: { variant: 'body2' }, subheader: subheader, subheaderTypographyProps: { variant: 'caption' } }),
69
+ React.createElement(CardContent, { sx: { paddingTop: 0 } }, children)));
70
+ }
@@ -0,0 +1,69 @@
1
+ import { DataTypes } from '@etsoo/shared';
2
+ import { SxProps, Theme } from '@mui/material';
3
+ import React from 'react';
4
+ import { ListChildComponentProps } from 'react-window';
5
+ import { GridColumn } from '../components/GridColumn';
6
+ import { GridJsonData } from '../components/GridLoader';
7
+ import { DataGridExProps } from './DataGridEx';
8
+ import { GridMethodRef } from './GridMethodRef';
9
+ import { ScrollerListExInnerItemRendererProps } from './ScrollerListEx';
10
+ export interface ResponsibleContainerProps<T extends {}, F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate> extends Omit<DataGridExProps<T>, 'height' | 'itemKey' | 'loadData' | 'mRef' | 'onScroll' | 'onItemsRendered'> {
11
+ /**
12
+ * Height will be deducted
13
+ * @param height Current calcuated height
14
+ */
15
+ adjustHeight?: (height: number) => number;
16
+ /**
17
+ * Columns
18
+ */
19
+ columns: GridColumn<T>[];
20
+ /**
21
+ * Min width to show Datagrid
22
+ */
23
+ dataGridMinWidth?: number;
24
+ /**
25
+ * Search fields
26
+ */
27
+ fields?: React.ReactElement[];
28
+ /**
29
+ * Search field template
30
+ */
31
+ fieldTemplate?: F;
32
+ /**
33
+ * Grid height
34
+ */
35
+ height?: number;
36
+ /**
37
+ * Inner item renderer
38
+ */
39
+ innerItemRenderer: (props: ScrollerListExInnerItemRendererProps<T>) => React.ReactNode;
40
+ /**
41
+ * Item renderer
42
+ */
43
+ itemRenderer?: (props: ListChildComponentProps<T>) => React.ReactElement;
44
+ /**
45
+ * Item size, a function indicates its a variable size list
46
+ */
47
+ itemSize: ((index: number) => number) | number;
48
+ /**
49
+ * Listbox SX (dataGrid determines the case)
50
+ */
51
+ listBoxSx?: (dataGrid: boolean) => SxProps<Theme>;
52
+ /**
53
+ * Load data callback
54
+ */
55
+ loadData: (data: GridJsonData & DataTypes.BasicTemplateType<F>) => PromiseLike<T[] | null | undefined>;
56
+ /**
57
+ * Methods
58
+ */
59
+ mRef?: React.MutableRefObject<GridMethodRef | undefined>;
60
+ /**
61
+ * Searchbox SX
62
+ */
63
+ searchBoxSx?: SxProps<Theme>;
64
+ /**
65
+ * Size ready to read miliseconds span
66
+ */
67
+ sizeReadyMiliseconds?: number;
68
+ }
69
+ export declare function ResponsibleContainer<T extends {}, F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate>(props: ResponsibleContainerProps<T, F>): JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { Box, Stack } from '@mui/material';
2
+ import React from 'react';
3
+ import { GridDataGet } from '../components/GridLoader';
4
+ import useCombinedRefs from '../uses/useCombinedRefs';
5
+ import { useDimensions } from '../uses/useDimensions';
6
+ import { DataGridEx, DataGridExCalColumns } from './DataGridEx';
7
+ import { ScrollerListEx } from './ScrollerListEx';
8
+ import { SearchBar } from './SearchBar';
9
+ export function ResponsibleContainer(props) {
10
+ var _a;
11
+ // Destruct
12
+ const { adjustHeight, columns, dataGridMinWidth = DataGridExCalColumns(columns).total, fields, fieldTemplate, height, listBoxSx, loadData, mRef, searchBoxSx, sizeReadyMiliseconds = 0, ...rest } = props;
13
+ // Refs
14
+ const refs = React.useRef({});
15
+ const mRefs = useCombinedRefs(mRef, (ref) => {
16
+ if (ref == null)
17
+ return;
18
+ refs.current.ref = ref;
19
+ });
20
+ // Has fields
21
+ const hasFields = fields != null && fields.length > 0;
22
+ // Watch container
23
+ const { dimensions } = useDimensions(1, undefined, sizeReadyMiliseconds);
24
+ const rect = dimensions[0][2];
25
+ const showDataGrid = ((_a = rect === null || rect === void 0 ? void 0 : rect.width) !== null && _a !== void 0 ? _a : 0) >= dataGridMinWidth;
26
+ React.useEffect(() => {
27
+ if (rect != null && rect.height > 50 && height == null) {
28
+ let gridHeight = window.innerHeight - Math.round(rect.top + rect.height + 1);
29
+ const style = window.getComputedStyle(dimensions[0][1]);
30
+ const boxPadding = parseFloat(style.paddingLeft);
31
+ if (!isNaN(boxPadding))
32
+ gridHeight -= 2 * boxPadding;
33
+ if (adjustHeight != null) {
34
+ gridHeight -= adjustHeight(gridHeight);
35
+ }
36
+ if (gridHeight !== refs.current.height) {
37
+ refs.current.height = gridHeight;
38
+ }
39
+ }
40
+ }, [rect]);
41
+ const localLoadData = (props) => {
42
+ const data = GridDataGet(props, fieldTemplate);
43
+ return loadData(data);
44
+ };
45
+ const gridHeight = refs.current.height;
46
+ const list = React.useMemo(() => {
47
+ if (gridHeight == null)
48
+ return;
49
+ if (showDataGrid) {
50
+ // Delete
51
+ delete rest.itemRenderer;
52
+ return (React.createElement(Box, { sx: listBoxSx == null ? undefined : listBoxSx(true) },
53
+ React.createElement(DataGridEx, { autoLoad: !hasFields, height: gridHeight, loadData: localLoadData, mRef: mRefs, columns: columns, ...rest })));
54
+ }
55
+ // Delete
56
+ delete rest.checkable;
57
+ delete rest.borderRowsCount;
58
+ delete rest.bottomHeight;
59
+ delete rest.footerItemRenderer;
60
+ delete rest.headerHeight;
61
+ delete rest.hideFooter;
62
+ delete rest.hoverColor;
63
+ delete rest.selectable;
64
+ return (React.createElement(Box, { sx: listBoxSx == null ? undefined : listBoxSx(false) },
65
+ React.createElement(ScrollerListEx, { autoLoad: !hasFields, height: gridHeight, loadData: localLoadData, mRef: mRefs, ...rest })));
66
+ }, [gridHeight, showDataGrid]);
67
+ // On submit callback
68
+ const onSubmit = (data, _reset) => {
69
+ if (data == null || rect == null || refs.current.ref == null)
70
+ return;
71
+ refs.current.ref.reset({ data });
72
+ };
73
+ // Layout
74
+ return (React.createElement(Stack, null,
75
+ hasFields && (React.createElement(Box, { ref: dimensions[0][0], sx: searchBoxSx },
76
+ React.createElement(SearchBar, { fields: fields, onSubmit: onSubmit }))),
77
+ list));
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/react",
3
- "version": "1.4.10",
3
+ "version": "1.4.14",
4
4
  "description": "TypeScript ReactJs framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -50,7 +50,7 @@
50
50
  "@emotion/react": "^11.7.1",
51
51
  "@emotion/style": "^0.8.0",
52
52
  "@emotion/styled": "^11.6.0",
53
- "@etsoo/appscript": "^1.2.18",
53
+ "@etsoo/appscript": "^1.2.20",
54
54
  "@etsoo/notificationbase": "^1.1.0",
55
55
  "@etsoo/shared": "^1.1.0",
56
56
  "@mui/icons-material": "^5.2.5",
@@ -89,7 +89,7 @@
89
89
  "eslint-config-airbnb-base": "^15.0.0",
90
90
  "eslint-plugin-import": "^2.25.4",
91
91
  "eslint-plugin-react": "^7.28.0",
92
- "jest": "^27.4.5",
92
+ "jest": "^27.4.7",
93
93
  "react-test-renderer": "^17.0.2",
94
94
  "ts-jest": "^27.1.2",
95
95
  "typescript": "^4.5.4"
@@ -223,8 +223,9 @@ export class ReactApp<
223
223
  React.Fragment,
224
224
  null,
225
225
  main,
226
+ React.createElement('br'),
226
227
  React.createElement(
227
- 'div',
228
+ 'span',
228
229
  { style: { fontSize: '9px' } },
229
230
  tip
230
231
  )
package/src/index.ts CHANGED
@@ -55,6 +55,7 @@ export * from './mu/ItemList';
55
55
  export * from './mu/ListItemRightIcon';
56
56
  export * from './mu/LoadingButton';
57
57
  export * from './mu/MaskInput';
58
+ export * from './mu/MobileListItemRenderer';
58
59
  export * from './mu/MoreFab';
59
60
  export * from './mu/MUGlobal';
60
61
  export * from './mu/NotifierMU';
@@ -62,6 +63,7 @@ export * from './mu/OptionGroup';
62
63
  export * from './mu/PList';
63
64
  export * from './mu/ProgressCount';
64
65
  export * from './mu/PullToRefreshUI';
66
+ export * from './mu/ResponsibleContainer';
65
67
  export * from './mu/RLink';
66
68
  export * from './mu/ScrollerListEx';
67
69
  export * from './mu/ScrollTopFab';
@@ -57,13 +57,19 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
57
57
  */
58
58
  onChange?: (items: D[]) => void;
59
59
 
60
+ /**
61
+ * Drag end handler
62
+ */
63
+ onDragEnd?: (items: D[]) => void;
64
+
60
65
  /**
61
66
  * Top and bottom sides renderer
62
67
  */
63
68
  sideRenderer?: (
64
69
  top: boolean,
65
70
  addItem: (item: D) => boolean,
66
- addItems: (items: D[]) => number
71
+ addItems: (items: D[]) => number,
72
+ reloadItems: () => void
67
73
  ) => React.ReactNode;
68
74
 
69
75
  /**
@@ -93,6 +99,7 @@ export function DnDList<
93
99
  loadData,
94
100
  name,
95
101
  onChange,
102
+ onDragEnd,
96
103
  sideRenderer
97
104
  } = props;
98
105
 
@@ -108,7 +115,7 @@ export function DnDList<
108
115
  };
109
116
 
110
117
  // Drag end handler
111
- const onDragEnd = (result: DropResult) => {
118
+ const onDragEndLocal = (result: DropResult) => {
112
119
  // Dropped outside the list
113
120
  if (!result.destination) {
114
121
  return;
@@ -125,6 +132,9 @@ export function DnDList<
125
132
 
126
133
  // Update the state
127
134
  changeItems(newItems);
135
+
136
+ // Drag end handler
137
+ if (onDragEnd) onDragEnd(newItems);
128
138
  };
129
139
 
130
140
  // Add item
@@ -201,17 +211,23 @@ export function DnDList<
201
211
  changeItems(newItems);
202
212
  };
203
213
 
204
- React.useEffect(() => {
214
+ // Reload items
215
+ const reloadItems = () => {
205
216
  // Load data
206
217
  loadData(name).then((items) => setItems(items));
218
+ };
219
+
220
+ React.useEffect(() => {
221
+ // Load data
222
+ reloadItems();
207
223
  }, [name]);
208
224
 
209
225
  // Layout
210
226
  return (
211
227
  <React.Fragment>
212
- {sideRenderer && sideRenderer(true, addItem, addItems)}
228
+ {sideRenderer && sideRenderer(true, addItem, addItems, reloadItems)}
213
229
  <Component {...componentProps}>
214
- <DragDropContext onDragEnd={onDragEnd}>
230
+ <DragDropContext onDragEnd={onDragEndLocal}>
215
231
  <Droppable droppableId={name}>
216
232
  {(provided, snapshot) => (
217
233
  <div
@@ -265,7 +281,8 @@ export function DnDList<
265
281
  </Droppable>
266
282
  </DragDropContext>
267
283
  </Component>
268
- {sideRenderer && sideRenderer(false, addItem, addItems)}
284
+ {sideRenderer &&
285
+ sideRenderer(false, addItem, addItems, reloadItems)}
269
286
  </React.Fragment>
270
287
  );
271
288
  }
@@ -0,0 +1,113 @@
1
+ import { Card, CardContent, CardHeader, LinearProgress } from '@mui/material';
2
+ import React from 'react';
3
+ import { ListItemReact } from '../components/ListItemReact';
4
+ import { MoreFab } from './MoreFab';
5
+ import { MUGlobal } from './MUGlobal';
6
+ import { ScrollerListExInnerItemRendererProps } from './ScrollerListEx';
7
+
8
+ function getActions(input: (ListItemReact | boolean)[]): ListItemReact[] {
9
+ // Actions
10
+ const actions: ListItemReact[] = [];
11
+ input.forEach((action) => {
12
+ if (typeof action === 'boolean') return;
13
+ actions.push(action);
14
+ });
15
+ return actions;
16
+ }
17
+
18
+ /**
19
+ * Default mobile list item renderer
20
+ * @param param0 List renderer props
21
+ * @param margin Margin
22
+ * @param renderer Renderer for card content
23
+ * @returns Component
24
+ */
25
+ export function MobileListItemRenderer<T>(
26
+ { data, itemHeight }: ScrollerListExInnerItemRendererProps<T>,
27
+ margin: {},
28
+ renderer: (
29
+ data: T
30
+ ) => [
31
+ string,
32
+ string | undefined,
33
+ React.ReactNode | (ListItemReact | boolean)[],
34
+ React.ReactNode
35
+ ]
36
+ ) {
37
+ // Loading
38
+ if (data == null) return <LinearProgress />;
39
+
40
+ // Elements
41
+ const [title, subheader, actions, children] = renderer(data);
42
+
43
+ // Half
44
+ const halfMargin = MUGlobal.half(margin);
45
+
46
+ return (
47
+ <Card
48
+ sx={{
49
+ height: (theme) =>
50
+ MUGlobal.adjustWithTheme(itemHeight, margin, theme.spacing),
51
+ marginLeft: margin,
52
+ marginRight: margin,
53
+ marginTop: halfMargin,
54
+ marginBottom: halfMargin
55
+ }}
56
+ >
57
+ <CardHeader
58
+ sx={{ paddingBottom: 0.5 }}
59
+ action={
60
+ Array.isArray(actions) ? (
61
+ <MoreFab
62
+ iconButton
63
+ size="small"
64
+ anchorOrigin={{
65
+ vertical: 'bottom',
66
+ horizontal: 'right'
67
+ }}
68
+ transformOrigin={{
69
+ vertical: 'top',
70
+ horizontal: 'right'
71
+ }}
72
+ actions={getActions(actions)}
73
+ PaperProps={{
74
+ elevation: 0,
75
+ sx: {
76
+ overflow: 'visible',
77
+ filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
78
+ mt: -0.4,
79
+ '& .MuiAvatar-root': {
80
+ width: 32,
81
+ height: 32,
82
+ ml: -0.5,
83
+ mr: 1
84
+ },
85
+ '&:before': {
86
+ content: '""',
87
+ display: 'block',
88
+ position: 'absolute',
89
+ top: 0,
90
+ right: 14,
91
+ width: 10,
92
+ height: 10,
93
+ bgcolor: 'background.paper',
94
+ transform:
95
+ 'translateY(-50%) rotate(45deg)',
96
+ zIndex: 0
97
+ }
98
+ }
99
+ }}
100
+ />
101
+ ) : (
102
+ actions
103
+ )
104
+ }
105
+ title={title}
106
+ titleTypographyProps={{ variant: 'body2' }}
107
+ subheader={subheader}
108
+ subheaderTypographyProps={{ variant: 'caption' }}
109
+ />
110
+ <CardContent sx={{ paddingTop: 0 }}>{children}</CardContent>
111
+ </Card>
112
+ );
113
+ }
@@ -0,0 +1,240 @@
1
+ import { DataTypes } from '@etsoo/shared';
2
+ import { Box, Stack, SxProps, Theme } from '@mui/material';
3
+ import React from 'react';
4
+ import { ListChildComponentProps } from 'react-window';
5
+ import { GridColumn } from '../components/GridColumn';
6
+ import {
7
+ GridDataGet,
8
+ GridJsonData,
9
+ GridLoadDataProps
10
+ } from '../components/GridLoader';
11
+ import useCombinedRefs from '../uses/useCombinedRefs';
12
+ import { useDimensions } from '../uses/useDimensions';
13
+ import {
14
+ DataGridEx,
15
+ DataGridExCalColumns,
16
+ DataGridExProps
17
+ } from './DataGridEx';
18
+ import { GridMethodRef } from './GridMethodRef';
19
+ import {
20
+ ScrollerListEx,
21
+ ScrollerListExInnerItemRendererProps
22
+ } from './ScrollerListEx';
23
+ import { SearchBar } from './SearchBar';
24
+
25
+ export interface ResponsibleContainerProps<
26
+ T extends {},
27
+ F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate
28
+ > extends Omit<
29
+ DataGridExProps<T>,
30
+ | 'height'
31
+ | 'itemKey'
32
+ | 'loadData'
33
+ | 'mRef'
34
+ | 'onScroll'
35
+ | 'onItemsRendered'
36
+ > {
37
+ /**
38
+ * Height will be deducted
39
+ * @param height Current calcuated height
40
+ */
41
+ adjustHeight?: (height: number) => number;
42
+
43
+ /**
44
+ * Columns
45
+ */
46
+ columns: GridColumn<T>[];
47
+
48
+ /**
49
+ * Min width to show Datagrid
50
+ */
51
+ dataGridMinWidth?: number;
52
+
53
+ /**
54
+ * Search fields
55
+ */
56
+ fields?: React.ReactElement[];
57
+
58
+ /**
59
+ * Search field template
60
+ */
61
+ fieldTemplate?: F;
62
+
63
+ /**
64
+ * Grid height
65
+ */
66
+ height?: number;
67
+
68
+ /**
69
+ * Inner item renderer
70
+ */
71
+ innerItemRenderer: (
72
+ props: ScrollerListExInnerItemRendererProps<T>
73
+ ) => React.ReactNode;
74
+
75
+ /**
76
+ * Item renderer
77
+ */
78
+ itemRenderer?: (props: ListChildComponentProps<T>) => React.ReactElement;
79
+
80
+ /**
81
+ * Item size, a function indicates its a variable size list
82
+ */
83
+ itemSize: ((index: number) => number) | number;
84
+
85
+ /**
86
+ * Listbox SX (dataGrid determines the case)
87
+ */
88
+ listBoxSx?: (dataGrid: boolean) => SxProps<Theme>;
89
+
90
+ /**
91
+ * Load data callback
92
+ */
93
+ loadData: (
94
+ data: GridJsonData & DataTypes.BasicTemplateType<F>
95
+ ) => PromiseLike<T[] | null | undefined>;
96
+
97
+ /**
98
+ * Methods
99
+ */
100
+ mRef?: React.MutableRefObject<GridMethodRef | undefined>;
101
+
102
+ /**
103
+ * Searchbox SX
104
+ */
105
+ searchBoxSx?: SxProps<Theme>;
106
+
107
+ /**
108
+ * Size ready to read miliseconds span
109
+ */
110
+ sizeReadyMiliseconds?: number;
111
+ }
112
+
113
+ interface LocalRefs {
114
+ height?: number;
115
+ ref?: GridMethodRef;
116
+ }
117
+
118
+ export function ResponsibleContainer<
119
+ T extends {},
120
+ F extends DataTypes.BasicTemplate = DataTypes.BasicTemplate
121
+ >(props: ResponsibleContainerProps<T, F>) {
122
+ // Destruct
123
+ const {
124
+ adjustHeight,
125
+ columns,
126
+ dataGridMinWidth = DataGridExCalColumns(columns).total,
127
+ fields,
128
+ fieldTemplate,
129
+ height,
130
+ listBoxSx,
131
+ loadData,
132
+ mRef,
133
+ searchBoxSx,
134
+ sizeReadyMiliseconds = 0,
135
+ ...rest
136
+ } = props;
137
+
138
+ // Refs
139
+ const refs = React.useRef<LocalRefs>({});
140
+
141
+ const mRefs = useCombinedRefs(mRef, (ref: GridMethodRef) => {
142
+ if (ref == null) return;
143
+ refs.current.ref = ref;
144
+ });
145
+
146
+ // Has fields
147
+ const hasFields = fields != null && fields.length > 0;
148
+
149
+ // Watch container
150
+ const { dimensions } = useDimensions(1, undefined, sizeReadyMiliseconds);
151
+ const rect = dimensions[0][2];
152
+ const showDataGrid = (rect?.width ?? 0) >= dataGridMinWidth;
153
+
154
+ React.useEffect(() => {
155
+ if (rect != null && rect.height > 50 && height == null) {
156
+ let gridHeight =
157
+ window.innerHeight - Math.round(rect.top + rect.height + 1);
158
+
159
+ const style = window.getComputedStyle(dimensions[0][1]!);
160
+ const boxPadding = parseFloat(style.paddingLeft);
161
+ if (!isNaN(boxPadding)) gridHeight -= 2 * boxPadding;
162
+
163
+ if (adjustHeight != null) {
164
+ gridHeight -= adjustHeight(gridHeight);
165
+ }
166
+
167
+ if (gridHeight !== refs.current.height) {
168
+ refs.current.height = gridHeight;
169
+ }
170
+ }
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
+
182
+ if (showDataGrid) {
183
+ // Delete
184
+ delete rest.itemRenderer;
185
+
186
+ return (
187
+ <Box sx={listBoxSx == null ? undefined : listBoxSx(true)}>
188
+ <DataGridEx<T>
189
+ autoLoad={!hasFields}
190
+ height={gridHeight}
191
+ loadData={localLoadData}
192
+ mRef={mRefs}
193
+ columns={columns}
194
+ {...rest}
195
+ />
196
+ </Box>
197
+ );
198
+ }
199
+
200
+ // Delete
201
+ delete rest.checkable;
202
+ delete rest.borderRowsCount;
203
+ delete rest.bottomHeight;
204
+ delete rest.footerItemRenderer;
205
+ delete rest.headerHeight;
206
+ delete rest.hideFooter;
207
+ delete rest.hoverColor;
208
+ delete rest.selectable;
209
+
210
+ return (
211
+ <Box sx={listBoxSx == null ? undefined : listBoxSx(false)}>
212
+ <ScrollerListEx<T>
213
+ autoLoad={!hasFields}
214
+ height={gridHeight}
215
+ loadData={localLoadData}
216
+ mRef={mRefs}
217
+ {...rest}
218
+ />
219
+ </Box>
220
+ );
221
+ }, [gridHeight, showDataGrid]);
222
+
223
+ // On submit callback
224
+ const onSubmit = (data: FormData, _reset: boolean) => {
225
+ if (data == null || rect == null || refs.current.ref == null) return;
226
+ refs.current.ref.reset({ data });
227
+ };
228
+
229
+ // Layout
230
+ return (
231
+ <Stack>
232
+ {hasFields && (
233
+ <Box ref={dimensions[0][0]} sx={searchBoxSx}>
234
+ <SearchBar fields={fields} onSubmit={onSubmit} />
235
+ </Box>
236
+ )}
237
+ {list}
238
+ </Stack>
239
+ );
240
+ }