@etsoo/react 1.5.11 → 1.5.15

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.
@@ -8,6 +8,7 @@ export declare namespace Labels {
8
8
  */
9
9
  const CommonPage: {
10
10
  add: string;
11
+ back: string;
11
12
  delete: string;
12
13
  more: string;
13
14
  pullToRefresh: string;
package/lib/app/Labels.js CHANGED
@@ -9,6 +9,7 @@ export var Labels;
9
9
  */
10
10
  Labels.CommonPage = {
11
11
  add: 'Add',
12
+ back: 'Back',
12
13
  delete: 'Delete',
13
14
  more: 'More',
14
15
  pullToRefresh: 'Pull down to refresh',
@@ -108,19 +108,6 @@ export declare class ReactApp<S extends IAppSettings, D extends IUser, P extends
108
108
  * @param name Application name
109
109
  */
110
110
  constructor(settings: S, name: string);
111
- /**
112
- * Get parsed Url under bridge host
113
- * @param url Url
114
- * @returns Parsed Url
115
- */
116
- protected getHostUrl(url: string): string;
117
- /**
118
- * Get parsed Url under bridge host
119
- * @param url Url
120
- * @param name App name in the host environment
121
- * @returns Parsed Url
122
- */
123
- protected getHostUrlBase(url: string, name: string): string;
124
111
  /**
125
112
  * Override alert action result
126
113
  * @param result Action result
@@ -26,8 +26,8 @@ export function ReactAppStateDetector(props) {
26
26
  // Destruct
27
27
  const { targetFields, update } = props;
28
28
  // Context
29
- const { state } = globalApp == null
30
- ? { state: {} }
29
+ const { state = {} } = globalApp == null
30
+ ? {}
31
31
  : React.useContext(globalApp.userState.context);
32
32
  // Ready
33
33
  React.useEffect(() => {
@@ -66,9 +66,9 @@ export class ReactApp extends CoreApp {
66
66
  */
67
67
  this.userState = new UserState();
68
68
  this.history =
69
- window.location.hostname === ''
70
- ? Utils.getMemoryHistory(this.getHostUrl(window.location.href))
71
- : undefined;
69
+ BridgeUtils.host == null
70
+ ? undefined
71
+ : Utils.getMemoryHistory(BridgeUtils.host.getStartUrl());
72
72
  this.cultureState = new CultureState(settings.currentCulture);
73
73
  this.pageState = new PageState();
74
74
  globalApp = this;
@@ -91,29 +91,6 @@ export class ReactApp extends CoreApp {
91
91
  ReactApp._notifierProvider = NotifierMU.setup();
92
92
  return NotifierMU.instance;
93
93
  }
94
- /**
95
- * Get parsed Url under bridge host
96
- * @param url Url
97
- * @returns Parsed Url
98
- */
99
- getHostUrl(url) {
100
- return this.getHostUrlBase(url, 'core');
101
- }
102
- /**
103
- * Get parsed Url under bridge host
104
- * @param url Url
105
- * @param name App name in the host environment
106
- * @returns Parsed Url
107
- */
108
- getHostUrlBase(url, name) {
109
- if (url.startsWith('/'))
110
- return url;
111
- const identifier = `/${name}/`;
112
- const pos = url.indexOf(identifier);
113
- if (pos === -1)
114
- return '/';
115
- return url.substring(pos + identifier.length - 1);
116
- }
117
94
  /**
118
95
  * Override alert action result
119
96
  * @param result Action result
@@ -33,11 +33,9 @@ export declare class ServiceApp<U extends IServiceUser = IServiceUser, P extends
33
33
  */
34
34
  constructor(settings: S, name: string);
35
35
  /**
36
- * Get parsed Url under bridge host
37
- * @param url Url
38
- * @returns Parsed Url
36
+ * Load SmartERP core
39
37
  */
40
- protected getHostUrl(url: string): string;
38
+ loadSmartERP(): void;
41
39
  /**
42
40
  * Go to the login page
43
41
  * @param tryLogin Try to login again
@@ -66,8 +64,11 @@ export declare class ServiceApp<U extends IServiceUser = IServiceUser, P extends
66
64
  serviceEncrypt(message: string, passphrase?: string, iterations?: number): string;
67
65
  /**
68
66
  * Try login
67
+ * @param data Additional data
68
+ * @param showLoading Show loading bar or not
69
+ * @returns Result
69
70
  */
70
- tryLogin<D extends {} = {}>(data?: D): Promise<boolean>;
71
+ tryLogin<D extends {} = {}>(data?: D, showLoading?: boolean): Promise<boolean>;
71
72
  /**
72
73
  * User login extended
73
74
  * @param user Core system user
@@ -40,12 +40,15 @@ export class ServiceApp extends ReactApp {
40
40
  this._serviceUser = value;
41
41
  }
42
42
  /**
43
- * Get parsed Url under bridge host
44
- * @param url Url
45
- * @returns Parsed Url
43
+ * Load SmartERP core
46
44
  */
47
- getHostUrl(url) {
48
- return this.getHostUrlBase(url, 's' + this.settings.serviceId);
45
+ loadSmartERP() {
46
+ if (BridgeUtils.host == null) {
47
+ window.location.href = this.settings.webUrl;
48
+ }
49
+ else {
50
+ BridgeUtils.host.loadApp('core');
51
+ }
49
52
  }
50
53
  /**
51
54
  * Go to the login page
@@ -206,10 +209,13 @@ export class ServiceApp extends ReactApp {
206
209
  }
207
210
  /**
208
211
  * Try login
212
+ * @param data Additional data
213
+ * @param showLoading Show loading bar or not
214
+ * @returns Result
209
215
  */
210
- async tryLogin(data) {
216
+ async tryLogin(data, showLoading) {
211
217
  // Reset user state
212
- const result = await super.tryLogin(data);
218
+ const result = await super.tryLogin(data, showLoading);
213
219
  if (!result)
214
220
  return false;
215
221
  // Refresh token
@@ -228,6 +234,7 @@ export class ServiceApp extends ReactApp {
228
234
  });
229
235
  },
230
236
  data,
237
+ showLoading,
231
238
  relogin: true
232
239
  });
233
240
  }
@@ -15,7 +15,7 @@ export declare namespace Utils {
15
15
  * @param initialPath Initial path
16
16
  * @returns History
17
17
  */
18
- function getMemoryHistory(initialPath?: string): History;
18
+ function getMemoryHistory(initialPath?: string | null): History;
19
19
  /**
20
20
  * Is safe click
21
21
  * @param event Mouse event
package/lib/app/Utils.js CHANGED
@@ -29,9 +29,9 @@ export var Utils;
29
29
  * @param initialPath Initial path
30
30
  * @returns History
31
31
  */
32
- function getMemoryHistory(initialPath = '/') {
32
+ function getMemoryHistory(initialPath) {
33
33
  if (memoryHistory == null) {
34
- memoryHistory = createHistory(createMemorySource(initialPath));
34
+ memoryHistory = createHistory(createMemorySource(initialPath !== null && initialPath !== void 0 ? initialPath : '/'));
35
35
  }
36
36
  return memoryHistory;
37
37
  }
package/lib/index.d.ts CHANGED
@@ -31,6 +31,7 @@ export * from './mu/texts/DateText';
31
31
  export * from './mu/texts/MoneyText';
32
32
  export * from './mu/texts/NumberText';
33
33
  export * from './mu/AuditDisplay';
34
+ export * from './mu/BackButton';
34
35
  export * from './mu/BridgeCloseButton';
35
36
  export * from './mu/ButtonLink';
36
37
  export * from './mu/ComboBox';
package/lib/index.js CHANGED
@@ -34,6 +34,7 @@ export * from './mu/texts/DateText';
34
34
  export * from './mu/texts/MoneyText';
35
35
  export * from './mu/texts/NumberText';
36
36
  export * from './mu/AuditDisplay';
37
+ export * from './mu/BackButton';
37
38
  export * from './mu/BridgeCloseButton';
38
39
  export * from './mu/ButtonLink';
39
40
  export * from './mu/ComboBox';
@@ -0,0 +1,13 @@
1
+ /// <reference types="react" />
2
+ import { IconButtonProps } from '@mui/material';
3
+ /**
4
+ * BackButton props
5
+ */
6
+ export interface BackButtonProps extends IconButtonProps {
7
+ }
8
+ /**
9
+ * BackButton
10
+ * @param props Props
11
+ * @returns Component
12
+ */
13
+ export declare function BackButton(props: BackButtonProps): JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { IconButton, useTheme } from '@mui/material';
2
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
3
+ import React from 'react';
4
+ import { useNavigate } from '@reach/router';
5
+ /**
6
+ * BackButton
7
+ * @param props Props
8
+ * @returns Component
9
+ */
10
+ export function BackButton(props) {
11
+ // Destruct
12
+ const { color = 'primary', size = 'small', onClick, ...rest } = props;
13
+ // Navigate
14
+ const navigate = useNavigate();
15
+ // Theme
16
+ const theme = useTheme();
17
+ // Color
18
+ const pColor = color != 'inherit' && color != 'default' && color in theme.palette
19
+ ? theme.palette[color]
20
+ : theme.palette.primary;
21
+ // Click handler
22
+ const onClickLocal = (event) => {
23
+ if (onClick)
24
+ onClick(event);
25
+ navigate(-1);
26
+ };
27
+ return (React.createElement(IconButton, { "aria-label": "Back", color: color, size: size, onClick: onClickLocal, sx: {
28
+ backgroundColor: pColor.contrastText,
29
+ border: `1px solid ${pColor.light}`
30
+ }, ...rest },
31
+ React.createElement(ArrowBackIcon, null)));
32
+ }
@@ -1,10 +1,14 @@
1
1
  /// <reference types="react" />
2
2
  import { IBridgeHost } from '@etsoo/appscript';
3
- import { IconButtonProps } from '@mui/material';
3
+ import { BoxProps, IconButtonProps } from '@mui/material';
4
4
  /**
5
5
  * Bridge close button props
6
6
  */
7
7
  export interface BridgeCloseButtonProps extends IconButtonProps {
8
+ /**
9
+ * Box props
10
+ */
11
+ boxProps?: BoxProps;
8
12
  /**
9
13
  * Validate the host
10
14
  * @param host Host
@@ -1,6 +1,6 @@
1
1
  import { BridgeUtils } from '@etsoo/appscript';
2
2
  import CloseIcon from '@mui/icons-material/Close';
3
- import { IconButton } from '@mui/material';
3
+ import { Box, IconButton } from '@mui/material';
4
4
  import React from 'react';
5
5
  import { globalApp } from '../app/ReactApp';
6
6
  /**
@@ -10,7 +10,7 @@ import { globalApp } from '../app/ReactApp';
10
10
  */
11
11
  export function BridgeCloseButton(props) {
12
12
  // Destruct
13
- const { onClick, title = typeof globalApp === 'undefined'
13
+ const { boxProps, onClick, title = typeof globalApp === 'undefined'
14
14
  ? 'Close'
15
15
  : globalApp.get('close'), validate, ...rest } = props;
16
16
  // Host
@@ -24,11 +24,7 @@ export function BridgeCloseButton(props) {
24
24
  onClick(event);
25
25
  host.exit();
26
26
  };
27
- return (React.createElement(IconButton, { "aria-label": "close", color: "secondary", onClick: onClickLocal, title: title, sx: {
28
- position: 'absolute',
29
- zIndex: (theme) => theme.zIndex.appBar + 1,
30
- top: (theme) => theme.spacing(0.5),
31
- right: (theme) => theme.spacing(0.5)
32
- }, ...rest },
33
- React.createElement(CloseIcon, null)));
27
+ return (React.createElement(Box, { ...boxProps },
28
+ React.createElement(IconButton, { "aria-label": "close", onClick: onClickLocal, title: title, ...rest },
29
+ React.createElement(CloseIcon, null))));
34
30
  }
@@ -7,6 +7,7 @@ import { MoreFab } from '../MoreFab';
7
7
  import { ReactAppStateDetector } from '../../app/ReactApp';
8
8
  import { Container, Fab } from '@mui/material';
9
9
  import RefreshIcon from '@mui/icons-material/Refresh';
10
+ import { BackButton } from '../BackButton';
10
11
  /**
11
12
  * Default scroll container
12
13
  */
@@ -17,7 +18,7 @@ export const CommonPageScrollContainer = global;
17
18
  */
18
19
  export function CommonPage(props) {
19
20
  // Destruct
20
- const { children, disableGutters = true, fabButtons, fabColumnDirection, fabPaddingAdjust = 1.5, fabSize = 'small', maxWidth = false, moreActions, onRefresh, onUpdate, onUpdateAll, paddings = MUGlobal.pagePaddings, scrollContainer, targetFields, sx = {}, ...rest } = props;
21
+ const { children, disableGutters = true, fabButtons, fabColumnDirection, fabPaddingAdjust = 1.5, fabSize = 'small', maxWidth = false, moreActions, onRefresh, onUpdate, onUpdateAll, paddings = MUGlobal.pagePaddings, scrollContainer, supportBack = false, targetFields, sx = {}, ...rest } = props;
21
22
  // Merge style
22
23
  Object.assign(sx, {
23
24
  padding: paddings
@@ -53,6 +54,7 @@ export function CommonPage(props) {
53
54
  fabButtons,
54
55
  onRefresh != null && (React.createElement(Fab, { title: labels.refresh, size: fabSize, onClick: onRefresh, sx: { display: { xs: 'none', md: 'inherit' } } },
55
56
  React.createElement(RefreshIcon, null))),
56
- React.createElement(MoreFab, { size: fabSize, title: labels.more, actions: moreActions })),
57
+ React.createElement(MoreFab, { size: fabSize, title: labels.more, actions: moreActions }),
58
+ supportBack && (React.createElement(BackButton, { title: labels.back, size: fabSize }))),
57
59
  children)));
58
60
  }
@@ -18,7 +18,7 @@ export interface CommonPageProps extends Omit<ContainerProps, 'id'> {
18
18
  */
19
19
  fabSize?: CustomFabSize;
20
20
  /**
21
- * Fab flex column direction?
21
+ * Fab flex column direction, undefined to hide it
22
22
  */
23
23
  fabColumnDirection?: boolean;
24
24
  /**
@@ -49,6 +49,10 @@ export interface CommonPageProps extends Omit<ContainerProps, 'id'> {
49
49
  * Scroll container
50
50
  */
51
51
  scrollContainer?: HTMLElement | object;
52
+ /**
53
+ * Support back click
54
+ */
55
+ supportBack?: boolean;
52
56
  /**
53
57
  * State last changed fields
54
58
  */
@@ -16,6 +16,11 @@ export interface EditPageProps extends Omit<CommonPageProps, 'onSubmit'> {
16
16
  * On delete callback
17
17
  */
18
18
  onDelete?: () => Promise<void> | void;
19
+ /**
20
+ * Support back click
21
+ * @default true
22
+ */
23
+ supportBack?: boolean;
19
24
  }
20
25
  /**
21
26
  * Add / Edit page
@@ -5,13 +5,14 @@ import { MUGlobal } from '../MUGlobal';
5
5
  import { CommonPage, CommonPageScrollContainer } from './CommonPage';
6
6
  import SaveIcon from '@mui/icons-material/Save';
7
7
  import DeleteIcon from '@mui/icons-material/Delete';
8
+ import { BackButton } from '../BackButton';
8
9
  /**
9
10
  * Add / Edit page
10
11
  * @param props Props
11
12
  */
12
13
  export function EditPage(props) {
13
14
  // Destruct
14
- const { children, isEditing, onDelete, onSubmit, paddings = MUGlobal.pagePaddings, scrollContainer = CommonPageScrollContainer, ...rest } = props;
15
+ const { children, isEditing, onDelete, onSubmit, paddings = MUGlobal.pagePaddings, scrollContainer = CommonPageScrollContainer, supportBack = true, ...rest } = props;
15
16
  // Labels
16
17
  const labels = Labels.CommonPage;
17
18
  return (React.createElement(CommonPage, { paddings: paddings, scrollContainer: scrollContainer, ...rest },
@@ -23,5 +24,6 @@ export function EditPage(props) {
23
24
  paddingTop: paddings
24
25
  } },
25
26
  isEditing && onDelete && (React.createElement(Button, { color: "primary", variant: "outlined", onClick: () => onDelete(), startIcon: React.createElement(DeleteIcon, { color: "warning" }) }, labels.delete)),
26
- React.createElement(Button, { variant: "contained", type: "submit", startIcon: React.createElement(SaveIcon, null), sx: { flexGrow: 1 } }, labels.save)))));
27
+ React.createElement(Button, { variant: "contained", type: "submit", startIcon: React.createElement(SaveIcon, null), sx: { flexGrow: 1 } }, labels.save),
28
+ supportBack && React.createElement(BackButton, { title: labels.back })))));
27
29
  }
@@ -48,6 +48,10 @@ export interface ViewPageProps<T extends {}> extends Exclude<CommonPageProps, 'c
48
48
  * Load data
49
49
  */
50
50
  loadData: () => PromiseLike<T | undefined>;
51
+ /**
52
+ * Pull to refresh data
53
+ */
54
+ pullToRefresh?: boolean;
51
55
  /**
52
56
  * Support refresh
53
57
  */
@@ -1,9 +1,11 @@
1
1
  import { Utils } from '@etsoo/shared';
2
2
  import { Grid, LinearProgress, Stack, Typography } from '@mui/material';
3
3
  import React from 'react';
4
+ import { Labels } from '../../app/Labels';
4
5
  import { globalApp } from '../../app/ReactApp';
5
6
  import { GridDataFormat } from '../GridDataFormat';
6
7
  import { MUGlobal } from '../MUGlobal';
8
+ import { PullToRefreshUI } from '../PullToRefreshUI';
7
9
  import { CommonPage } from './CommonPage';
8
10
  function formatItemData(fieldData) {
9
11
  if (fieldData == null)
@@ -62,9 +64,13 @@ function getItemField(field, data) {
62
64
  */
63
65
  export function ViewPage(props) {
64
66
  // Destruct
65
- const { actions, children, fields, loadData, paddings = MUGlobal.pagePaddings, supportRefresh = true, ...rest } = props;
67
+ const { actions, children, fields, loadData, paddings = MUGlobal.pagePaddings, supportRefresh = true, fabColumnDirection = true, supportBack = true, pullToRefresh = true, ...rest } = props;
66
68
  // Data
67
69
  const [data, setData] = React.useState();
70
+ // Labels
71
+ const labels = Labels.CommonPage;
72
+ // Container
73
+ const pullContainer = '#page-container';
68
74
  // Load data
69
75
  const refresh = async () => {
70
76
  const result = await loadData();
@@ -72,7 +78,7 @@ export function ViewPage(props) {
72
78
  return;
73
79
  setData(result);
74
80
  };
75
- 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,
81
+ return (React.createElement(CommonPage, { paddings: paddings, onRefresh: supportRefresh ? refresh : undefined, onUpdate: supportRefresh ? undefined : refresh, ...rest, scrollContainer: globalThis, fabColumnDirection: fabColumnDirection, supportBack: supportBack }, data == null ? (React.createElement(LinearProgress, null)) : (React.createElement(React.Fragment, null,
76
82
  React.createElement(Grid, { container: true, justifyContent: "left", spacing: paddings, className: "ET-ViewPage", sx: {
77
83
  '.MuiTypography-subtitle2': {
78
84
  fontWeight: 'bold'
@@ -91,5 +97,9 @@ export function ViewPage(props) {
91
97
  React.createElement(Typography, { variant: "subtitle2" }, itemData)));
92
98
  })),
93
99
  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))),
94
- Utils.getResult(children, data, refresh)))));
100
+ Utils.getResult(children, data, refresh),
101
+ pullToRefresh && (React.createElement(PullToRefreshUI, { mainElement: pullContainer, triggerElement: pullContainer, instructionsPullToRefresh: labels.pullToRefresh, instructionsReleaseToRefresh: labels.releaseToRefresh, instructionsRefreshing: labels.refreshing, onRefresh: refresh, shouldPullToRefresh: () => {
102
+ const container = document.querySelector(pullContainer);
103
+ return !(container === null || container === void 0 ? void 0 : container.scrollTop);
104
+ } }))))));
95
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/react",
3
- "version": "1.5.11",
3
+ "version": "1.5.15",
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.48",
53
+ "@etsoo/appscript": "^1.2.50",
54
54
  "@etsoo/notificationbase": "^1.1.1",
55
55
  "@etsoo/shared": "^1.1.11",
56
56
  "@mui/icons-material": "^5.4.1",
package/src/app/Labels.ts CHANGED
@@ -9,6 +9,7 @@ export namespace Labels {
9
9
  */
10
10
  export const CommonPage = {
11
11
  add: 'Add',
12
+ back: 'Back',
12
13
  delete: 'Delete',
13
14
  more: 'More',
14
15
  pullToRefresh: 'Pull down to refresh',
@@ -56,12 +56,10 @@ export function ReactAppStateDetector(props: IStateProps) {
56
56
  const { targetFields, update } = props;
57
57
 
58
58
  // Context
59
- const { state } =
59
+ const { state = {} } =
60
60
  globalApp == null
61
- ? ({ state: {} } as UserCalls<IUser>)
62
- : React.useContext(
63
- (globalApp.userState as UserState<IUser>).context
64
- );
61
+ ? ({} as UserCalls<IUser>)
62
+ : React.useContext(globalApp.userState.context);
65
63
 
66
64
  // Ready
67
65
  React.useEffect(() => {
@@ -221,41 +219,18 @@ export class ReactApp<
221
219
  new WindowStorage(),
222
220
  name
223
221
  );
222
+
224
223
  this.history =
225
- window.location.hostname === ''
226
- ? Utils.getMemoryHistory(this.getHostUrl(window.location.href))
227
- : undefined;
224
+ BridgeUtils.host == null
225
+ ? undefined
226
+ : Utils.getMemoryHistory(BridgeUtils.host.getStartUrl());
227
+
228
228
  this.cultureState = new CultureState(settings.currentCulture);
229
229
  this.pageState = new PageState<P>();
230
230
 
231
231
  globalApp = this;
232
232
  }
233
233
 
234
- /**
235
- * Get parsed Url under bridge host
236
- * @param url Url
237
- * @returns Parsed Url
238
- */
239
- protected getHostUrl(url: string) {
240
- return this.getHostUrlBase(url, 'core');
241
- }
242
-
243
- /**
244
- * Get parsed Url under bridge host
245
- * @param url Url
246
- * @param name App name in the host environment
247
- * @returns Parsed Url
248
- */
249
- protected getHostUrlBase(url: string, name: string) {
250
- if (url.startsWith('/')) return url;
251
-
252
- const identifier = `/${name}/`;
253
- const pos = url.indexOf(identifier);
254
- if (pos === -1) return '/';
255
-
256
- return url.substring(pos + identifier.length - 1);
257
- }
258
-
259
234
  /**
260
235
  * Override alert action result
261
236
  * @param result Action result
@@ -69,12 +69,14 @@ export class ServiceApp<
69
69
  }
70
70
 
71
71
  /**
72
- * Get parsed Url under bridge host
73
- * @param url Url
74
- * @returns Parsed Url
72
+ * Load SmartERP core
75
73
  */
76
- protected override getHostUrl(url: string) {
77
- return this.getHostUrlBase(url, 's' + this.settings.serviceId);
74
+ loadSmartERP() {
75
+ if (BridgeUtils.host == null) {
76
+ window.location.href = this.settings.webUrl;
77
+ } else {
78
+ BridgeUtils.host.loadApp('core');
79
+ }
78
80
  }
79
81
 
80
82
  /**
@@ -300,10 +302,16 @@ export class ServiceApp<
300
302
 
301
303
  /**
302
304
  * Try login
305
+ * @param data Additional data
306
+ * @param showLoading Show loading bar or not
307
+ * @returns Result
303
308
  */
304
- override async tryLogin<D extends {} = {}>(data?: D) {
309
+ override async tryLogin<D extends {} = {}>(
310
+ data?: D,
311
+ showLoading?: boolean
312
+ ) {
305
313
  // Reset user state
306
- const result = await super.tryLogin(data);
314
+ const result = await super.tryLogin(data, showLoading);
307
315
  if (!result) return false;
308
316
 
309
317
  // Refresh token
@@ -323,6 +331,7 @@ export class ServiceApp<
323
331
  });
324
332
  },
325
333
  data,
334
+ showLoading,
326
335
  relogin: true
327
336
  });
328
337
  }
package/src/app/Utils.ts CHANGED
@@ -31,9 +31,11 @@ export namespace Utils {
31
31
  * @param initialPath Initial path
32
32
  * @returns History
33
33
  */
34
- export function getMemoryHistory(initialPath: string = '/') {
34
+ export function getMemoryHistory(initialPath?: string | null) {
35
35
  if (memoryHistory == null) {
36
- memoryHistory = createHistory(createMemorySource(initialPath));
36
+ memoryHistory = createHistory(
37
+ createMemorySource(initialPath ?? '/')
38
+ );
37
39
  }
38
40
  return memoryHistory;
39
41
  }
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ export * from './mu/texts/MoneyText';
38
38
  export * from './mu/texts/NumberText';
39
39
 
40
40
  export * from './mu/AuditDisplay';
41
+ export * from './mu/BackButton';
41
42
  export * from './mu/BridgeCloseButton';
42
43
  export * from './mu/ButtonLink';
43
44
  export * from './mu/ComboBox';
@@ -0,0 +1,53 @@
1
+ import { IconButton, IconButtonProps, useTheme } from '@mui/material';
2
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
3
+ import React from 'react';
4
+ import { useNavigate } from '@reach/router';
5
+
6
+ /**
7
+ * BackButton props
8
+ */
9
+ export interface BackButtonProps extends IconButtonProps {}
10
+
11
+ /**
12
+ * BackButton
13
+ * @param props Props
14
+ * @returns Component
15
+ */
16
+ export function BackButton(props: BackButtonProps) {
17
+ // Destruct
18
+ const { color = 'primary', size = 'small', onClick, ...rest } = props;
19
+
20
+ // Navigate
21
+ const navigate = useNavigate();
22
+
23
+ // Theme
24
+ const theme = useTheme();
25
+
26
+ // Color
27
+ const pColor =
28
+ color != 'inherit' && color != 'default' && color in theme.palette
29
+ ? theme.palette[color]
30
+ : theme.palette.primary;
31
+
32
+ // Click handler
33
+ const onClickLocal = (event: React.MouseEvent<HTMLButtonElement>) => {
34
+ if (onClick) onClick(event);
35
+ navigate(-1);
36
+ };
37
+
38
+ return (
39
+ <IconButton
40
+ aria-label="Back"
41
+ color={color}
42
+ size={size}
43
+ onClick={onClickLocal}
44
+ sx={{
45
+ backgroundColor: pColor.contrastText,
46
+ border: `1px solid ${pColor.light}`
47
+ }}
48
+ {...rest}
49
+ >
50
+ <ArrowBackIcon />
51
+ </IconButton>
52
+ );
53
+ }
@@ -1,6 +1,6 @@
1
1
  import { BridgeUtils, IBridgeHost } from '@etsoo/appscript';
2
2
  import CloseIcon from '@mui/icons-material/Close';
3
- import { IconButton, IconButtonProps } from '@mui/material';
3
+ import { Box, BoxProps, IconButton, IconButtonProps } from '@mui/material';
4
4
  import React from 'react';
5
5
  import { globalApp } from '../app/ReactApp';
6
6
 
@@ -8,6 +8,11 @@ import { globalApp } from '../app/ReactApp';
8
8
  * Bridge close button props
9
9
  */
10
10
  export interface BridgeCloseButtonProps extends IconButtonProps {
11
+ /**
12
+ * Box props
13
+ */
14
+ boxProps?: BoxProps;
15
+
11
16
  /**
12
17
  * Validate the host
13
18
  * @param host Host
@@ -23,6 +28,7 @@ export interface BridgeCloseButtonProps extends IconButtonProps {
23
28
  export function BridgeCloseButton(props: BridgeCloseButtonProps) {
24
29
  // Destruct
25
30
  const {
31
+ boxProps,
26
32
  onClick,
27
33
  title = typeof globalApp === 'undefined'
28
34
  ? 'Close'
@@ -45,20 +51,15 @@ export function BridgeCloseButton(props: BridgeCloseButtonProps) {
45
51
  };
46
52
 
47
53
  return (
48
- <IconButton
49
- aria-label="close"
50
- color="secondary"
51
- onClick={onClickLocal}
52
- title={title}
53
- sx={{
54
- position: 'absolute',
55
- zIndex: (theme) => theme.zIndex.appBar + 1,
56
- top: (theme) => theme.spacing(0.5),
57
- right: (theme) => theme.spacing(0.5)
58
- }}
59
- {...rest}
60
- >
61
- <CloseIcon />
62
- </IconButton>
54
+ <Box {...boxProps}>
55
+ <IconButton
56
+ aria-label="close"
57
+ onClick={onClickLocal}
58
+ title={title}
59
+ {...rest}
60
+ >
61
+ <CloseIcon />
62
+ </IconButton>
63
+ </Box>
63
64
  );
64
65
  }
@@ -8,6 +8,7 @@ import { MoreFab } from '../MoreFab';
8
8
  import { ReactAppStateDetector } from '../../app/ReactApp';
9
9
  import { Container, Fab } from '@mui/material';
10
10
  import RefreshIcon from '@mui/icons-material/Refresh';
11
+ import { BackButton } from '../BackButton';
11
12
 
12
13
  /**
13
14
  * Default scroll container
@@ -34,6 +35,7 @@ export function CommonPage(props: CommonPageProps) {
34
35
  onUpdateAll,
35
36
  paddings = MUGlobal.pagePaddings,
36
37
  scrollContainer,
38
+ supportBack = false,
37
39
  targetFields,
38
40
  sx = {},
39
41
  ...rest
@@ -115,6 +117,9 @@ export function CommonPage(props: CommonPageProps) {
115
117
  title={labels.more}
116
118
  actions={moreActions}
117
119
  />
120
+ {supportBack && (
121
+ <BackButton title={labels.back} size={fabSize} />
122
+ )}
118
123
  </FabBox>
119
124
  {children}
120
125
  </Container>
@@ -20,7 +20,7 @@ export interface CommonPageProps extends Omit<ContainerProps, 'id'> {
20
20
  fabSize?: CustomFabSize;
21
21
 
22
22
  /**
23
- * Fab flex column direction?
23
+ * Fab flex column direction, undefined to hide it
24
24
  */
25
25
  fabColumnDirection?: boolean;
26
26
 
@@ -59,6 +59,11 @@ export interface CommonPageProps extends Omit<ContainerProps, 'id'> {
59
59
  */
60
60
  scrollContainer?: HTMLElement | object;
61
61
 
62
+ /**
63
+ * Support back click
64
+ */
65
+ supportBack?: boolean;
66
+
62
67
  /**
63
68
  * State last changed fields
64
69
  */
@@ -6,6 +6,7 @@ import { CommonPage, CommonPageScrollContainer } from './CommonPage';
6
6
  import { CommonPageProps } from './CommonPageProps';
7
7
  import SaveIcon from '@mui/icons-material/Save';
8
8
  import DeleteIcon from '@mui/icons-material/Delete';
9
+ import { BackButton } from '../BackButton';
9
10
 
10
11
  /**
11
12
  * Add / Edit page props
@@ -25,6 +26,12 @@ export interface EditPageProps extends Omit<CommonPageProps, 'onSubmit'> {
25
26
  * On delete callback
26
27
  */
27
28
  onDelete?: () => Promise<void> | void;
29
+
30
+ /**
31
+ * Support back click
32
+ * @default true
33
+ */
34
+ supportBack?: boolean;
28
35
  }
29
36
 
30
37
  /**
@@ -40,6 +47,7 @@ export function EditPage(props: EditPageProps) {
40
47
  onSubmit,
41
48
  paddings = MUGlobal.pagePaddings,
42
49
  scrollContainer = CommonPageScrollContainer,
50
+ supportBack = true,
43
51
  ...rest
44
52
  } = props;
45
53
 
@@ -91,6 +99,7 @@ export function EditPage(props: EditPageProps) {
91
99
  >
92
100
  {labels.save}
93
101
  </Button>
102
+ {supportBack && <BackButton title={labels.back} />}
94
103
  </Grid>
95
104
  </form>
96
105
  </CommonPage>
@@ -7,6 +7,7 @@ import {
7
7
  Typography
8
8
  } from '@mui/material';
9
9
  import React from 'react';
10
+ import { Labels } from '../../app/Labels';
10
11
  import { globalApp } from '../../app/ReactApp';
11
12
  import {
12
13
  GridColumnRenderProps,
@@ -14,6 +15,7 @@ import {
14
15
  } from '../../components/GridColumn';
15
16
  import { GridDataFormat } from '../GridDataFormat';
16
17
  import { MUGlobal } from '../MUGlobal';
18
+ import { PullToRefreshUI } from '../PullToRefreshUI';
17
19
  import { CommonPage } from './CommonPage';
18
20
  import { CommonPageProps } from './CommonPageProps';
19
21
 
@@ -81,6 +83,11 @@ export interface ViewPageProps<T extends {}>
81
83
  */
82
84
  loadData: () => PromiseLike<T | undefined>;
83
85
 
86
+ /**
87
+ * Pull to refresh data
88
+ */
89
+ pullToRefresh?: boolean;
90
+
84
91
  /**
85
92
  * Support refresh
86
93
  */
@@ -165,12 +172,21 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
165
172
  loadData,
166
173
  paddings = MUGlobal.pagePaddings,
167
174
  supportRefresh = true,
175
+ fabColumnDirection = true,
176
+ supportBack = true,
177
+ pullToRefresh = true,
168
178
  ...rest
169
179
  } = props;
170
180
 
171
181
  // Data
172
182
  const [data, setData] = React.useState<T>();
173
183
 
184
+ // Labels
185
+ const labels = Labels.CommonPage;
186
+
187
+ // Container
188
+ const pullContainer = '#page-container';
189
+
174
190
  // Load data
175
191
  const refresh = async () => {
176
192
  const result = await loadData();
@@ -184,7 +200,9 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
184
200
  onRefresh={supportRefresh ? refresh : undefined}
185
201
  onUpdate={supportRefresh ? undefined : refresh}
186
202
  {...rest}
187
- scrollContainer={global}
203
+ scrollContainer={globalThis}
204
+ fabColumnDirection={fabColumnDirection}
205
+ supportBack={supportBack}
188
206
  >
189
207
  {data == null ? (
190
208
  <LinearProgress />
@@ -241,6 +259,23 @@ export function ViewPage<T extends {}>(props: ViewPageProps<T>) {
241
259
  </Stack>
242
260
  )}
243
261
  {Utils.getResult(children, data, refresh)}
262
+ {pullToRefresh && (
263
+ <PullToRefreshUI
264
+ mainElement={pullContainer}
265
+ triggerElement={pullContainer}
266
+ instructionsPullToRefresh={labels.pullToRefresh}
267
+ instructionsReleaseToRefresh={
268
+ labels.releaseToRefresh
269
+ }
270
+ instructionsRefreshing={labels.refreshing}
271
+ onRefresh={refresh}
272
+ shouldPullToRefresh={() => {
273
+ const container =
274
+ document.querySelector(pullContainer);
275
+ return !container?.scrollTop;
276
+ }}
277
+ />
278
+ )}
244
279
  </React.Fragment>
245
280
  )}
246
281
  </CommonPage>