@etsoo/react 1.4.76 → 1.4.80

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.
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  /**
2
3
  * React utils
3
4
  */
@@ -8,6 +9,12 @@ export declare namespace Utils {
8
9
  * @returns Formatted value
9
10
  */
10
11
  function formatInputValue(value: unknown): string | number | any[] | undefined;
12
+ /**
13
+ * Is safe click
14
+ * @param event Mouse event
15
+ * @returns Result
16
+ */
17
+ function isSafeClick(event: React.MouseEvent<HTMLElement>): boolean;
11
18
  /**
12
19
  * Trigger input change event
13
20
  * @param input Form input
package/lib/app/Utils.js CHANGED
@@ -20,6 +20,32 @@ export var Utils;
20
20
  return String(value);
21
21
  }
22
22
  Utils.formatInputValue = formatInputValue;
23
+ /**
24
+ * Is safe click
25
+ * @param event Mouse event
26
+ * @returns Result
27
+ */
28
+ function isSafeClick(event) {
29
+ // No target
30
+ // HTMLElement <= Element, SVGElement <= Element
31
+ if (!(event.target instanceof Element))
32
+ return true;
33
+ // Outside of the currentTarget
34
+ let target = event.target;
35
+ if (!event.currentTarget.contains(target))
36
+ return false;
37
+ while (target != null && target != event.currentTarget) {
38
+ const nodeName = target.nodeName.toUpperCase();
39
+ if (nodeName === 'INPUT' ||
40
+ nodeName === 'BUTTON' ||
41
+ nodeName === 'A' ||
42
+ target.hasAttribute('onClick'))
43
+ return false;
44
+ target = target.parentElement;
45
+ }
46
+ return true;
47
+ }
48
+ Utils.isSafeClick = isSafeClick;
23
49
  /**
24
50
  * Trigger input change event
25
51
  * @param input Form input
@@ -123,7 +123,7 @@ export interface GridColumn<T> {
123
123
  /**
124
124
  * The column identifier. It's used to map with row data
125
125
  */
126
- field?: string;
126
+ field?: string & keyof T;
127
127
  /**
128
128
  * The title of the column rendered in the column header cell
129
129
  */
@@ -2,6 +2,7 @@ import { Keyboard } from '@etsoo/shared';
2
2
  import { Autocomplete } from '@mui/material';
3
3
  import React from 'react';
4
4
  import { Utils } from '../app/Utils';
5
+ import { Utils as SharedUtils } from '@etsoo/shared';
5
6
  import { InputField } from './InputField';
6
7
  import { SearchField } from './SearchField';
7
8
  /**
@@ -81,6 +82,9 @@ export function ComboBox(props) {
81
82
  return;
82
83
  if (onLoadData)
83
84
  onLoadData(result);
85
+ if (search) {
86
+ SharedUtils.addBlankItem(result, idField, labelField);
87
+ }
84
88
  setOptions(result);
85
89
  });
86
90
  }
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { GridColumn } from '../components/GridColumn';
3
3
  import { GridLoaderStates } from '../components/GridLoader';
4
4
  import { ScrollerGridProps } from '../components/ScrollerGrid';
5
+ import { MouseEventWithDataHandler } from './MUGlobal';
5
6
  /**
6
7
  * Footer item renderer props
7
8
  */
@@ -55,6 +56,14 @@ export interface DataGridExProps<T extends Record<string, any>> extends Omit<Scr
55
56
  * Hover color
56
57
  */
57
58
  hoverColor?: string;
59
+ /**
60
+ * Double click handler
61
+ */
62
+ onDoubleClick?: MouseEventWithDataHandler<T>;
63
+ /**
64
+ * Click handler
65
+ */
66
+ onClick?: MouseEventWithDataHandler<T>;
58
67
  /**
59
68
  * Selectable to support hover over and out effect and row clickable
60
69
  * @default true
@@ -126,7 +126,7 @@ export function DataGridEx(props) {
126
126
  })));
127
127
  }
128
128
  // Destruct
129
- const { alternatingColors = [theme.palette.grey[100], undefined], borderRowsCount, bottomHeight = 53, checkable = false, className, columns, defaultOrderBy, height, headerHeight = 56, headerRenderer = defaultHeaderRenderer, footerRenderer = defaultFooterRenderer, footerItemRenderer = DataGridRenderers.defaultFooterItemRenderer, hideFooter = false, hoverColor = '#f6f9fb', idField = 'id', mRef = React.createRef(), selectable = true, selectedColor = '#edf4fb', width, ...rest } = props;
129
+ const { alternatingColors = [theme.palette.grey[100], undefined], borderRowsCount, bottomHeight = 53, checkable = false, className, columns, defaultOrderBy, height, headerHeight = 56, headerRenderer = defaultHeaderRenderer, footerRenderer = defaultFooterRenderer, footerItemRenderer = DataGridRenderers.defaultFooterItemRenderer, hideFooter = false, hoverColor = '#f6f9fb', idField = 'id', mRef = React.createRef(), onClick, onDoubleClick, selectable = true, selectedColor = '#edf4fb', width, ...rest } = props;
130
130
  if (checkable) {
131
131
  const cbColumn = {
132
132
  field: 'selected',
@@ -261,7 +261,7 @@ export function DataGridEx(props) {
261
261
  cellProps,
262
262
  renderProps
263
263
  });
264
- return (React.createElement("div", { className: rowClass, style: style, "data-row": rowIndex, "data-column": columnIndex, onMouseDown: selectable && !checkable ? handleMouseDown : undefined, onMouseOver: selectable ? handleMouseOver : undefined, onMouseOut: selectable ? handleMouseOut : undefined },
264
+ return (React.createElement("div", { className: rowClass, style: style, "data-row": rowIndex, "data-column": columnIndex, onMouseDown: selectable && !checkable ? handleMouseDown : undefined, onMouseOver: selectable ? handleMouseOver : undefined, onMouseOut: selectable ? handleMouseOut : undefined, onClick: (event) => onClick && data != null && onClick(event, data), onDoubleClick: (event) => onDoubleClick && data != null && onDoubleClick(event, data) },
265
265
  React.createElement(Box, { ...cellProps, onMouseEnter: handleMouseEnter }, child)));
266
266
  };
267
267
  // Column width calculator
@@ -1,4 +1,12 @@
1
+ /// <reference types="react" />
1
2
  import { Theme } from '@mui/material';
3
+ /**
4
+ * Mouse event handler with data
5
+ */
6
+ export declare type MouseEventWithDataHandler<T> = (event: React.MouseEvent<HTMLDivElement>, data: T) => void;
7
+ /**
8
+ * MUGlobal for global configurations
9
+ */
2
10
  export declare class MUGlobal {
3
11
  /**
4
12
  * Search field shrink
@@ -1,4 +1,7 @@
1
1
  import { NumberUtils } from '@etsoo/shared';
2
+ /**
3
+ * MUGlobal for global configurations
4
+ */
2
5
  export class MUGlobal {
3
6
  /**
4
7
  * Update object number properties with half of it
@@ -72,6 +72,10 @@ export interface ResponsibleContainerProps<T extends {}, F extends DataTypes.Bas
72
72
  * Pull to refresh data
73
73
  */
74
74
  pullToRefresh?: boolean;
75
+ /**
76
+ * Quick action for double click or click under mobile
77
+ */
78
+ quickAction?: (data: T) => void;
75
79
  /**
76
80
  * Size ready to read miliseconds span
77
81
  */
@@ -1,6 +1,7 @@
1
1
  import { Box, Stack } from '@mui/material';
2
2
  import React from 'react';
3
3
  import { Labels } from '../app/Labels';
4
+ import { Utils } from '../app/Utils';
4
5
  import { GridDataGet } from '../components/GridLoader';
5
6
  import useCombinedRefs from '../uses/useCombinedRefs';
6
7
  import { useDimensions } from '../uses/useDimensions';
@@ -24,7 +25,7 @@ function defaultContainerBoxSx(paddings, hasField, _dataGrid) {
24
25
  */
25
26
  export function ResponsibleContainer(props) {
26
27
  // Destruct
27
- const { adjustHeight, columns, containerBoxSx = defaultContainerBoxSx, dataGridMinWidth = Math.max(576, DataGridExCalColumns(columns).total), elementReady, fields, fieldTemplate, height, loadData, mRef, paddings = MUGlobal.pagePaddings, pullToRefresh = true, sizeReadyMiliseconds = 0, ...rest } = props;
28
+ const { adjustHeight, columns, containerBoxSx = defaultContainerBoxSx, dataGridMinWidth = Math.max(576, DataGridExCalColumns(columns).total), elementReady, fields, fieldTemplate, height, loadData, mRef, paddings = MUGlobal.pagePaddings, pullToRefresh = true, quickAction, sizeReadyMiliseconds = 0, ...rest } = props;
28
29
  // Labels
29
30
  const labels = Labels.CommonPage;
30
31
  // Refs
@@ -106,7 +107,7 @@ export function ResponsibleContainer(props) {
106
107
  delete rest.itemRenderer;
107
108
  return [
108
109
  React.createElement(Box, { className: "DataGridBox" },
109
- React.createElement(DataGridEx, { autoLoad: !hasFields, height: heightLocal, width: rect.width, loadData: localLoadData, mRef: mRefs, outerRef: (element) => {
110
+ React.createElement(DataGridEx, { autoLoad: !hasFields, height: heightLocal, width: rect.width, loadData: localLoadData, mRef: mRefs, onDoubleClick: (_, data) => quickAction && quickAction(data), outerRef: (element) => {
110
111
  if (element != null && elementReady)
111
112
  elementReady(element, true);
112
113
  }, columns: columns, ...rest })),
@@ -124,7 +125,9 @@ export function ResponsibleContainer(props) {
124
125
  delete rest.selectable;
125
126
  return [
126
127
  React.createElement(Box, { className: "ListBox", sx: { height: heightLocal } },
127
- React.createElement(ScrollerListEx, { autoLoad: !hasFields, height: heightLocal, loadData: localLoadData, mRef: mRefs, oRef: (element) => {
128
+ React.createElement(ScrollerListEx, { autoLoad: !hasFields, height: heightLocal, loadData: localLoadData, mRef: mRefs, onClick: (event, data) => quickAction &&
129
+ Utils.isSafeClick(event) &&
130
+ quickAction(data), oRef: (element) => {
128
131
  if (element != null && elementReady)
129
132
  elementReady(element, false);
130
133
  }, ...rest })),
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { ListChildComponentProps } from 'react-window';
3
3
  import { ScrollerListProps } from '../components/ScrollerList';
4
+ import { MouseEventWithDataHandler } from './MUGlobal';
4
5
  /**
5
6
  * Extended ScrollerList inner item renderer props
6
7
  */
@@ -53,6 +54,14 @@ export interface ScrollerListExProps<T> extends Omit<ScrollerListProps<T>, 'item
53
54
  * Item size, a function indicates its a variable size list
54
55
  */
55
56
  itemSize: ScrollerListExItemSize;
57
+ /**
58
+ * Double click handler
59
+ */
60
+ onDoubleClick?: MouseEventWithDataHandler<T>;
61
+ /**
62
+ * Click handler
63
+ */
64
+ onClick?: MouseEventWithDataHandler<T>;
56
65
  /**
57
66
  * On items select change
58
67
  */
@@ -66,7 +66,7 @@ const defaultMargin = (margin, isNarrow) => {
66
66
  };
67
67
  };
68
68
  // Default itemRenderer
69
- function defaultItemRenderer({ index, innerItemRenderer, data, onMouseDown, selected, style, itemHeight, space, margins }) {
69
+ function defaultItemRenderer({ index, innerItemRenderer, data, onMouseDown, selected, style, itemHeight, onClick, onDoubleClick, space, margins }) {
70
70
  // Child
71
71
  const child = innerItemRenderer({
72
72
  index,
@@ -81,7 +81,7 @@ function defaultItemRenderer({ index, innerItemRenderer, data, onMouseDown, sele
81
81
  if (selected)
82
82
  rowClass += ` ${selectedClassName}`;
83
83
  // Layout
84
- return (React.createElement("div", { className: rowClass, style: style, onMouseDown: (event) => onMouseDown(event.currentTarget, data) }, child));
84
+ return (React.createElement("div", { className: rowClass, style: style, onMouseDown: (event) => onMouseDown(event.currentTarget, data), onClick: (event) => onClick && onClick(event, data), onDoubleClick: (event) => onDoubleClick && onDoubleClick(event, data) }, child));
85
85
  }
86
86
  /**
87
87
  * Extended ScrollerList
@@ -118,12 +118,14 @@ export function ScrollerListEx(props) {
118
118
  itemHeight,
119
119
  innerItemRenderer,
120
120
  onMouseDown,
121
+ onClick,
122
+ onDoubleClick,
121
123
  space,
122
124
  margins,
123
125
  selected: isSelected(itemProps.data),
124
126
  ...itemProps
125
127
  });
126
- }, onSelectChange, selectedColor = '#edf4fb', ...rest } = props;
128
+ }, onClick, onDoubleClick, onSelectChange, selectedColor = '#edf4fb', ...rest } = props;
127
129
  // Theme
128
130
  const theme = useTheme();
129
131
  // Cache calculation
@@ -1,3 +1,4 @@
1
+ import { Utils } from '@etsoo/shared';
1
2
  import React from 'react';
2
3
  import { globalApp } from '..';
3
4
  import { SelectEx } from './SelectEx';
@@ -15,7 +16,7 @@ export function SelectBool(props) {
15
16
  { id: 'true', label: globalApp.get('yes') }
16
17
  ];
17
18
  if (search)
18
- options.unshift({ id: '', label: '---' });
19
+ Utils.addBlankItem(options);
19
20
  // Layout
20
21
  return React.createElement(SelectEx, { options: options, search: search, ...rest });
21
22
  }
@@ -3,6 +3,7 @@ import { Checkbox, FormControl, InputLabel, ListItemText, MenuItem, OutlinedInpu
3
3
  import React from 'react';
4
4
  import { MUGlobal } from './MUGlobal';
5
5
  import { ListItemRightIcon } from './ListItemRightIcon';
6
+ import { Utils } from '@etsoo/shared';
6
7
  /**
7
8
  * Extended select component
8
9
  * @param props Props
@@ -68,6 +69,16 @@ export function SelectEx(props) {
68
69
  }
69
70
  }
70
71
  };
72
+ // Get option id
73
+ const getId = (option) => {
74
+ return Reflect.get(option, idField);
75
+ };
76
+ // Get option label
77
+ const getLabel = (option) => {
78
+ return typeof labelField === 'function'
79
+ ? labelField(option)
80
+ : Reflect.get(option, labelField);
81
+ };
71
82
  // Refs
72
83
  const divRef = React.useRef();
73
84
  // When layout ready
@@ -86,6 +97,9 @@ export function SelectEx(props) {
86
97
  return;
87
98
  if (onLoadData)
88
99
  onLoadData(result);
100
+ if (search) {
101
+ Utils.addBlankItem(result, idField, labelField);
102
+ }
89
103
  setOptions(result);
90
104
  });
91
105
  }
@@ -107,18 +121,19 @@ export function SelectEx(props) {
107
121
  }, renderValue: (selected) => {
108
122
  // The text shows up
109
123
  return localOptions
110
- .filter((option) => Array.isArray(selected)
111
- ? selected.indexOf(option.id) !== -1
112
- : selected === option.id)
113
- .map((option) => option.label)
124
+ .filter((option) => {
125
+ const id = getId(option);
126
+ return Array.isArray(selected)
127
+ ? selected.indexOf(id) !== -1
128
+ : selected === id;
129
+ })
130
+ .map((option) => getLabel(option))
114
131
  .join(', ');
115
132
  }, sx: { minWidth: '150px' }, ...rest }, localOptions.map((option) => {
116
133
  // Option id
117
- const id = option[idField];
134
+ const id = getId(option);
118
135
  // Option label
119
- const label = typeof labelField === 'function'
120
- ? labelField(option)
121
- : option[labelField];
136
+ const label = getLabel(option);
122
137
  // Option
123
138
  return (React.createElement(MenuItem, { key: id, value: id, onClick: (event) => {
124
139
  if (onItemClick) {
@@ -32,4 +32,8 @@ export interface ResponsePageProps<T, F extends DataTypes.BasicTemplate> extends
32
32
  * Pull to refresh data
33
33
  */
34
34
  pullToRefresh?: boolean;
35
+ /**
36
+ * Quick action for double click or click under mobile
37
+ */
38
+ quickAction?: (data: T) => void;
35
39
  }
@@ -35,11 +35,11 @@ export interface ViewPageProps<T extends {}> extends Exclude<CommonPageProps, 'c
35
35
  /**
36
36
  * Actions
37
37
  */
38
- actions?: React.ReactNode | ((data: T) => React.ReactNode);
38
+ actions?: React.ReactNode | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
39
39
  /**
40
40
  * Children
41
41
  */
42
- children?: React.ReactNode | ((data: T) => React.ReactNode);
42
+ children?: React.ReactNode | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
43
43
  /**
44
44
  * Fields to display
45
45
  */
@@ -63,13 +63,13 @@ export function ViewPage(props) {
63
63
  // Data
64
64
  const [data, setData] = React.useState();
65
65
  // Load data
66
- const onRefresh = async () => {
66
+ const refresh = async () => {
67
67
  const result = await loadData();
68
68
  if (result == null)
69
69
  return;
70
70
  setData(result);
71
71
  };
72
- return (React.createElement(CommonPage, { paddings: paddings, onRefresh: supportRefresh ? onRefresh : undefined, onUpdate: supportRefresh ? undefined : onRefresh, ...rest, scrollContainer: global }, data == null ? (React.createElement(LinearProgress, null)) : (React.createElement(React.Fragment, null,
72
+ return (React.createElement(CommonPage, { paddings: paddings, onRefresh: supportRefresh ? refresh : undefined, onUpdate: supportRefresh ? undefined : refresh, ...rest, scrollContainer: global }, data == null ? (React.createElement(LinearProgress, null)) : (React.createElement(React.Fragment, null,
73
73
  React.createElement(Grid, { container: true, justifyContent: "left", spacing: paddings, className: "ET-ViewPage", sx: {
74
74
  '.MuiTypography-subtitle2': {
75
75
  fontWeight: 'bold'
@@ -77,7 +77,8 @@ export function ViewPage(props) {
77
77
  } }, fields.map((field, index) => {
78
78
  // Get data
79
79
  const [itemData, itemLabel, gridProps] = getItemField(field, data);
80
- if (itemData == null)
80
+ // Some callback function may return '' instead of undefined
81
+ if (itemData == null || itemData === '')
81
82
  return undefined;
82
83
  // Layout
83
84
  return (React.createElement(Grid, { item: true, ...gridProps, key: index },
@@ -86,6 +87,6 @@ export function ViewPage(props) {
86
87
  ":"),
87
88
  React.createElement(Typography, { variant: "subtitle2" }, itemData)));
88
89
  })),
89
- actions != null && (React.createElement(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: paddings, paddingBottom: paddings, gap: paddings }, Utils.getResult(actions, data))),
90
- Utils.getResult(children, data)))));
90
+ actions != null && (React.createElement(Stack, { className: "ET-ViewPage-Actions", direction: "row", width: "100%", flexWrap: "wrap", justifyContent: "flex-end", paddingTop: paddings, paddingBottom: paddings, gap: paddings }, Utils.getResult(actions, data, refresh))),
91
+ Utils.getResult(children, data, refresh)))));
91
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/react",
3
- "version": "1.4.76",
3
+ "version": "1.4.80",
4
4
  "description": "TypeScript ReactJs framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -50,9 +50,9 @@
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.28",
53
+ "@etsoo/appscript": "^1.2.29",
54
54
  "@etsoo/notificationbase": "^1.1.0",
55
- "@etsoo/shared": "^1.1.6",
55
+ "@etsoo/shared": "^1.1.9",
56
56
  "@mui/icons-material": "^5.3.1",
57
57
  "@mui/material": "^5.3.1",
58
58
  "@reach/router": "^1.3.4",
@@ -85,7 +85,7 @@
85
85
  "@types/react-test-renderer": "^17.0.1",
86
86
  "@typescript-eslint/eslint-plugin": "^5.10.1",
87
87
  "@typescript-eslint/parser": "^5.10.1",
88
- "eslint": "^8.7.0",
88
+ "eslint": "^8.8.0",
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",
package/src/app/Utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import React from 'react';
2
+
1
3
  /**
2
4
  * React utils
3
5
  */
@@ -19,6 +21,36 @@ export namespace Utils {
19
21
  return String(value);
20
22
  }
21
23
 
24
+ /**
25
+ * Is safe click
26
+ * @param event Mouse event
27
+ * @returns Result
28
+ */
29
+ export function isSafeClick(event: React.MouseEvent<HTMLElement>) {
30
+ // No target
31
+ // HTMLElement <= Element, SVGElement <= Element
32
+ if (!(event.target instanceof Element)) return true;
33
+
34
+ // Outside of the currentTarget
35
+ let target: Element | null = event.target;
36
+ if (!event.currentTarget.contains(target)) return false;
37
+
38
+ while (target != null && target != event.currentTarget) {
39
+ const nodeName = target.nodeName.toUpperCase();
40
+ if (
41
+ nodeName === 'INPUT' ||
42
+ nodeName === 'BUTTON' ||
43
+ nodeName === 'A' ||
44
+ target.hasAttribute('onClick')
45
+ )
46
+ return false;
47
+
48
+ target = target.parentElement;
49
+ }
50
+
51
+ return true;
52
+ }
53
+
22
54
  /**
23
55
  * Trigger input change event
24
56
  * @param input Form input
@@ -160,7 +160,7 @@ export interface GridColumn<T> {
160
160
  /**
161
161
  * The column identifier. It's used to map with row data
162
162
  */
163
- field?: string;
163
+ field?: string & keyof T;
164
164
 
165
165
  /**
166
166
  * The title of the column rendered in the column header cell
@@ -3,6 +3,7 @@ import { Keyboard } from '@etsoo/shared';
3
3
  import { Autocomplete, AutocompleteRenderInputParams } from '@mui/material';
4
4
  import React from 'react';
5
5
  import { Utils } from '../app/Utils';
6
+ import { Utils as SharedUtils } from '@etsoo/shared';
6
7
  import { AutocompleteExtendedProps } from './AutocompleteExtendedProps';
7
8
  import { InputField } from './InputField';
8
9
  import { SearchField } from './SearchField';
@@ -155,6 +156,9 @@ export function ComboBox<T extends {} = IdLabelDto>(props: ComboBoxProps<T>) {
155
156
  loadData().then((result) => {
156
157
  if (result == null || !isMounted.current) return;
157
158
  if (onLoadData) onLoadData(result);
159
+ if (search) {
160
+ SharedUtils.addBlankItem(result, idField, labelField);
161
+ }
158
162
  setOptions(result);
159
163
  });
160
164
  }
@@ -25,6 +25,7 @@ import {
25
25
  } from '../components/ScrollerGrid';
26
26
  import useCombinedRefs from '../uses/useCombinedRefs';
27
27
  import { DataGridRenderers } from './DataGridRenderers';
28
+ import { MouseEventWithDataHandler } from './MUGlobal';
28
29
 
29
30
  /**
30
31
  * Footer item renderer props
@@ -98,6 +99,16 @@ export interface DataGridExProps<T extends Record<string, any>>
98
99
  */
99
100
  hoverColor?: string;
100
101
 
102
+ /**
103
+ * Double click handler
104
+ */
105
+ onDoubleClick?: MouseEventWithDataHandler<T>;
106
+
107
+ /**
108
+ * Click handler
109
+ */
110
+ onClick?: MouseEventWithDataHandler<T>;
111
+
101
112
  /**
102
113
  * Selectable to support hover over and out effect and row clickable
103
114
  * @default true
@@ -351,6 +362,8 @@ export function DataGridEx<T extends Record<string, any>>(
351
362
  hoverColor = '#f6f9fb',
352
363
  idField = 'id',
353
364
  mRef = React.createRef(),
365
+ onClick,
366
+ onDoubleClick,
354
367
  selectable = true,
355
368
  selectedColor = '#edf4fb',
356
369
  width,
@@ -567,6 +580,12 @@ export function DataGridEx<T extends Record<string, any>>(
567
580
  }
568
581
  onMouseOver={selectable ? handleMouseOver : undefined}
569
582
  onMouseOut={selectable ? handleMouseOut : undefined}
583
+ onClick={(event) =>
584
+ onClick && data != null && onClick(event, data)
585
+ }
586
+ onDoubleClick={(event) =>
587
+ onDoubleClick && data != null && onDoubleClick(event, data)
588
+ }
570
589
  >
571
590
  <Box {...cellProps} onMouseEnter={handleMouseEnter}>
572
591
  {child}
@@ -1,6 +1,17 @@
1
1
  import { NumberUtils } from '@etsoo/shared';
2
2
  import { Breakpoint, Theme } from '@mui/material';
3
3
 
4
+ /**
5
+ * Mouse event handler with data
6
+ */
7
+ export type MouseEventWithDataHandler<T> = (
8
+ event: React.MouseEvent<HTMLDivElement>,
9
+ data: T
10
+ ) => void;
11
+
12
+ /**
13
+ * MUGlobal for global configurations
14
+ */
4
15
  export class MUGlobal {
5
16
  /**
6
17
  * Search field shrink
@@ -3,6 +3,7 @@ import { Box, Stack, SxProps, Theme } from '@mui/material';
3
3
  import React from 'react';
4
4
  import { ListChildComponentProps } from 'react-window';
5
5
  import { Labels } from '../app/Labels';
6
+ import { Utils } from '../app/Utils';
6
7
  import { GridColumn } from '../components/GridColumn';
7
8
  import {
8
9
  GridDataGet,
@@ -125,6 +126,11 @@ export interface ResponsibleContainerProps<
125
126
  */
126
127
  pullToRefresh?: boolean;
127
128
 
129
+ /**
130
+ * Quick action for double click or click under mobile
131
+ */
132
+ quickAction?: (data: T) => void;
133
+
128
134
  /**
129
135
  * Size ready to read miliseconds span
130
136
  */
@@ -173,6 +179,7 @@ export function ResponsibleContainer<
173
179
  mRef,
174
180
  paddings = MUGlobal.pagePaddings,
175
181
  pullToRefresh = true,
182
+ quickAction,
176
183
  sizeReadyMiliseconds = 0,
177
184
  ...rest
178
185
  } = props;
@@ -285,6 +292,9 @@ export function ResponsibleContainer<
285
292
  width={rect.width}
286
293
  loadData={localLoadData}
287
294
  mRef={mRefs}
295
+ onDoubleClick={(_, data) =>
296
+ quickAction && quickAction(data)
297
+ }
288
298
  outerRef={(element?: HTMLDivElement) => {
289
299
  if (element != null && elementReady)
290
300
  elementReady(element, true);
@@ -314,6 +324,11 @@ export function ResponsibleContainer<
314
324
  height={heightLocal}
315
325
  loadData={localLoadData}
316
326
  mRef={mRefs}
327
+ onClick={(event, data) =>
328
+ quickAction &&
329
+ Utils.isSafeClick(event) &&
330
+ quickAction(data)
331
+ }
317
332
  oRef={(element) => {
318
333
  if (element != null && elementReady)
319
334
  elementReady(element, false);
@@ -4,7 +4,7 @@ import { useTheme } from '@mui/material';
4
4
  import React from 'react';
5
5
  import { ListChildComponentProps } from 'react-window';
6
6
  import { ScrollerList, ScrollerListProps } from '../components/ScrollerList';
7
- import { MUGlobal } from './MUGlobal';
7
+ import { MouseEventWithDataHandler, MUGlobal } from './MUGlobal';
8
8
 
9
9
  // Scroll bar size
10
10
  const scrollbarSize = 16;
@@ -146,6 +146,16 @@ export interface ScrollerListExProps<T>
146
146
  */
147
147
  itemSize: ScrollerListExItemSize;
148
148
 
149
+ /**
150
+ * Double click handler
151
+ */
152
+ onDoubleClick?: MouseEventWithDataHandler<T>;
153
+
154
+ /**
155
+ * Click handler
156
+ */
157
+ onClick?: MouseEventWithDataHandler<T>;
158
+
149
159
  /**
150
160
  * On items select change
151
161
  */
@@ -175,6 +185,16 @@ interface defaultItemRendererProps<T> extends ListChildComponentProps<T> {
175
185
  */
176
186
  itemHeight: number;
177
187
 
188
+ /**
189
+ * Double click handler
190
+ */
191
+ onDoubleClick?: MouseEventWithDataHandler<T>;
192
+
193
+ /**
194
+ * Click handler
195
+ */
196
+ onClick?: MouseEventWithDataHandler<T>;
197
+
178
198
  /**
179
199
  * Item space
180
200
  */
@@ -200,6 +220,8 @@ function defaultItemRenderer<T>({
200
220
  selected,
201
221
  style,
202
222
  itemHeight,
223
+ onClick,
224
+ onDoubleClick,
203
225
  space,
204
226
  margins
205
227
  }: defaultItemRendererProps<T>) {
@@ -223,6 +245,10 @@ function defaultItemRenderer<T>({
223
245
  className={rowClass}
224
246
  style={style}
225
247
  onMouseDown={(event) => onMouseDown(event.currentTarget, data)}
248
+ onClick={(event) => onClick && onClick(event, data)}
249
+ onDoubleClick={(event) =>
250
+ onDoubleClick && onDoubleClick(event, data)
251
+ }
226
252
  >
227
253
  {child}
228
254
  </div>
@@ -282,12 +308,16 @@ export function ScrollerListEx<T extends Record<string, unknown>>(
282
308
  itemHeight,
283
309
  innerItemRenderer,
284
310
  onMouseDown,
311
+ onClick,
312
+ onDoubleClick,
285
313
  space,
286
314
  margins,
287
315
  selected: isSelected(itemProps.data),
288
316
  ...itemProps
289
317
  });
290
318
  },
319
+ onClick,
320
+ onDoubleClick,
291
321
  onSelectChange,
292
322
  selectedColor = '#edf4fb',
293
323
  ...rest
@@ -1,4 +1,5 @@
1
1
  import { IdLabelDto } from '@etsoo/appscript';
2
+ import { Utils } from '@etsoo/shared';
2
3
  import React from 'react';
3
4
  import { globalApp } from '..';
4
5
  import { SelectEx, SelectExProps } from './SelectEx';
@@ -24,7 +25,7 @@ export function SelectBool(props: SelectBoolProps) {
24
25
  { id: 'true', label: globalApp.get('yes')! }
25
26
  ];
26
27
 
27
- if (search) options.unshift({ id: '', label: '---' });
28
+ if (search) Utils.addBlankItem(options);
28
29
 
29
30
  // Layout
30
31
  return <SelectEx options={options} search={search} {...rest} />;
@@ -14,6 +14,7 @@ import React from 'react';
14
14
  import { MUGlobal } from './MUGlobal';
15
15
  import { IdLabelDto } from '@etsoo/appscript';
16
16
  import { ListItemRightIcon } from './ListItemRightIcon';
17
+ import { Utils } from '@etsoo/shared';
17
18
 
18
19
  /**
19
20
  * Extended select component props
@@ -143,6 +144,18 @@ export function SelectEx<T extends {} = IdLabelDto>(props: SelectExProps<T>) {
143
144
  }
144
145
  };
145
146
 
147
+ // Get option id
148
+ const getId = (option: T) => {
149
+ return Reflect.get(option, idField);
150
+ };
151
+
152
+ // Get option label
153
+ const getLabel = (option: T) => {
154
+ return typeof labelField === 'function'
155
+ ? labelField(option)
156
+ : Reflect.get(option, labelField);
157
+ };
158
+
146
159
  // Refs
147
160
  const divRef = React.useRef<HTMLDivElement>();
148
161
 
@@ -159,6 +172,9 @@ export function SelectEx<T extends {} = IdLabelDto>(props: SelectExProps<T>) {
159
172
  loadData().then((result) => {
160
173
  if (result == null || !isMounted.current) return;
161
174
  if (onLoadData) onLoadData(result);
175
+ if (search) {
176
+ Utils.addBlankItem(result, idField, labelField);
177
+ }
162
178
  setOptions(result);
163
179
  });
164
180
  }
@@ -198,26 +214,24 @@ export function SelectEx<T extends {} = IdLabelDto>(props: SelectExProps<T>) {
198
214
  renderValue={(selected) => {
199
215
  // The text shows up
200
216
  return localOptions
201
- .filter((option: any) =>
202
- Array.isArray(selected)
203
- ? selected.indexOf(option.id) !== -1
204
- : selected === option.id
205
- )
206
- .map((option: any) => option.label)
217
+ .filter((option) => {
218
+ const id = getId(option);
219
+ return Array.isArray(selected)
220
+ ? selected.indexOf(id) !== -1
221
+ : selected === id;
222
+ })
223
+ .map((option) => getLabel(option))
207
224
  .join(', ');
208
225
  }}
209
226
  sx={{ minWidth: '150px' }}
210
227
  {...rest}
211
228
  >
212
- {localOptions.map((option: any) => {
229
+ {localOptions.map((option) => {
213
230
  // Option id
214
- const id = option[idField];
231
+ const id = getId(option);
215
232
 
216
233
  // Option label
217
- const label =
218
- typeof labelField === 'function'
219
- ? labelField(option)
220
- : option[labelField];
234
+ const label = getLabel(option);
221
235
 
222
236
  // Option
223
237
  return (
@@ -46,4 +46,9 @@ export interface ResponsePageProps<T, F extends DataTypes.BasicTemplate>
46
46
  * Pull to refresh data
47
47
  */
48
48
  pullToRefresh?: boolean;
49
+
50
+ /**
51
+ * Quick action for double click or click under mobile
52
+ */
53
+ quickAction?: (data: T) => void;
49
54
  }
@@ -60,12 +60,16 @@ export interface ViewPageProps<T extends {}>
60
60
  /**
61
61
  * Actions
62
62
  */
63
- actions?: React.ReactNode | ((data: T) => React.ReactNode);
63
+ actions?:
64
+ | React.ReactNode
65
+ | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
64
66
 
65
67
  /**
66
68
  * Children
67
69
  */
68
- children?: React.ReactNode | ((data: T) => React.ReactNode);
70
+ children?:
71
+ | React.ReactNode
72
+ | ((data: T, refresh: () => PromiseLike<void>) => React.ReactNode);
69
73
 
70
74
  /**
71
75
  * Fields to display
@@ -164,7 +168,7 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
164
168
  const [data, setData] = React.useState<T>();
165
169
 
166
170
  // Load data
167
- const onRefresh = async () => {
171
+ const refresh = async () => {
168
172
  const result = await loadData();
169
173
  if (result == null) return;
170
174
  setData(result);
@@ -173,8 +177,8 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
173
177
  return (
174
178
  <CommonPage
175
179
  paddings={paddings}
176
- onRefresh={supportRefresh ? onRefresh : undefined}
177
- onUpdate={supportRefresh ? undefined : onRefresh}
180
+ onRefresh={supportRefresh ? refresh : undefined}
181
+ onUpdate={supportRefresh ? undefined : refresh}
178
182
  {...rest}
179
183
  scrollContainer={global}
180
184
  >
@@ -198,7 +202,9 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
198
202
  const [itemData, itemLabel, gridProps] =
199
203
  getItemField(field, data);
200
204
 
201
- if (itemData == null) return undefined;
205
+ // Some callback function may return '' instead of undefined
206
+ if (itemData == null || itemData === '')
207
+ return undefined;
202
208
 
203
209
  // Layout
204
210
  return (
@@ -227,10 +233,10 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
227
233
  paddingBottom={paddings}
228
234
  gap={paddings}
229
235
  >
230
- {Utils.getResult(actions, data)}
236
+ {Utils.getResult(actions, data, refresh)}
231
237
  </Stack>
232
238
  )}
233
- {Utils.getResult(children, data)}
239
+ {Utils.getResult(children, data, refresh)}
234
240
  </React.Fragment>
235
241
  )}
236
242
  </CommonPage>