@applica-software-guru/react-admin 1.3.163 → 1.3.165
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.
- package/dist/components/Onboarding/OnboardingTip.d.ts +5 -0
- package/dist/components/Onboarding/OnboardingTip.d.ts.map +1 -0
- package/dist/components/Onboarding/Provider.d.ts +49 -0
- package/dist/components/Onboarding/Provider.d.ts.map +1 -0
- package/dist/components/Onboarding/enums.d.ts +7 -0
- package/dist/components/Onboarding/enums.d.ts.map +1 -0
- package/dist/components/Onboarding/hooks.d.ts +10 -0
- package/dist/components/Onboarding/hooks.d.ts.map +1 -0
- package/dist/components/Onboarding/index.d.ts +7 -0
- package/dist/components/Onboarding/index.d.ts.map +1 -0
- package/dist/components/Onboarding/onboardingDataProvider.d.ts +8 -0
- package/dist/components/Onboarding/onboardingDataProvider.d.ts.map +1 -0
- package/dist/components/Onboarding/schemas.d.ts +5 -0
- package/dist/components/Onboarding/schemas.d.ts.map +1 -0
- package/dist/components/Onboarding/types.d.ts +8 -0
- package/dist/components/Onboarding/types.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/hooks/index.d.ts +14 -14
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useLocalStorage.d.ts +2 -1
- package/dist/hooks/useLocalStorage.d.ts.map +1 -1
- package/dist/react-admin.cjs.js +69 -66
- package/dist/react-admin.cjs.js.map +1 -1
- package/dist/react-admin.es.js +13683 -11875
- package/dist/react-admin.es.js.map +1 -1
- package/dist/react-admin.umd.js +70 -67
- package/dist/react-admin.umd.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/localStorage.d.ts +16 -0
- package/dist/utils/localStorage.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/components/Onboarding/OnboardingTip.tsx +52 -0
- package/src/components/Onboarding/Provider.tsx +145 -0
- package/src/components/Onboarding/enums.ts +7 -0
- package/src/components/Onboarding/hooks.tsx +80 -0
- package/src/components/Onboarding/index.ts +6 -0
- package/src/components/Onboarding/onboardingDataProvider.tsx +128 -0
- package/src/components/Onboarding/schemas.ts +6 -0
- package/src/components/Onboarding/types.ts +8 -0
- package/src/components/index.jsx +1 -0
- package/src/hooks/useLocalStorage.tsx +51 -0
- package/src/playground/components/pages/CustomPage.jsx +23 -8
- package/src/utils/index.ts +1 -0
- package/src/utils/localStorage.ts +73 -0
- package/src/hooks/useLocalStorage.jsx +0 -31
- /package/src/hooks/{index.jsx → index.ts} +0 -0
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type IListener = (value: unknown) => void;
|
|
2
|
+
interface ILocalStorage {
|
|
3
|
+
get: (key: string) => unknown | null;
|
|
4
|
+
set: (key: string, value: unknown) => unknown | null;
|
|
5
|
+
watch: (key: string, callback: IListener) => void;
|
|
6
|
+
unwatch: (key: string, callback?: IListener) => void;
|
|
7
|
+
}
|
|
8
|
+
declare class LocalStorage implements ILocalStorage {
|
|
9
|
+
#private;
|
|
10
|
+
get(key: string): unknown | null;
|
|
11
|
+
set(key: string, value: unknown): unknown | null;
|
|
12
|
+
watch(key: string, callback: IListener): void;
|
|
13
|
+
unwatch(key: string, callback?: IListener | undefined): void;
|
|
14
|
+
}
|
|
15
|
+
export { LocalStorage };
|
|
16
|
+
//# sourceMappingURL=localStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localStorage.d.ts","sourceRoot":"","sources":["../../../src/utils/localStorage.ts"],"names":[],"mappings":"AAOA,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;AAC1C,UAAU,aAAa;IACrB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IACrC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI,CAAC;IACrD,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;IAClD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC;CACtD;AAED,cAAM,YAAa,YAAW,aAAa;;IAOzC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAUhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI;IAUhD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,IAAI;IAa7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI;CAe7D;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applica-software-guru/react-admin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.165",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"react-router-dom": "^6.1.0",
|
|
69
69
|
"react-sticky-box": "2.0.4",
|
|
70
70
|
"simplebar": "6.2.5",
|
|
71
|
-
"simplebar-react": "3.2.4"
|
|
71
|
+
"simplebar-react": "3.2.4",
|
|
72
|
+
"yup": "^1.3.3"
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
|
74
75
|
"@applica-software-guru/crud-client": "^1.1",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { ITipDescriptor } from './types';
|
|
3
|
+
import { useOnboardingTip } from './hooks';
|
|
4
|
+
import { Optional } from 'src/types';
|
|
5
|
+
import { Button, Stack, TooltipProps, Typography, buttonClasses, styled, tooltipClasses } from '@mui/material';
|
|
6
|
+
import { Tooltip } from '../@extended';
|
|
7
|
+
|
|
8
|
+
type IOnboardingTipProps = TooltipProps & Optional<Pick<ITipDescriptor, 'id' | 'step' | 'completeOnboarding'>, 'step'>;
|
|
9
|
+
|
|
10
|
+
function BaseOnboardingTip(props: IOnboardingTipProps) {
|
|
11
|
+
const { id, step = 0, title, completeOnboarding } = props,
|
|
12
|
+
{ tip, dismiss } = useOnboardingTip({ id: id, step: step, completeOnboarding: completeOnboarding }),
|
|
13
|
+
{ open } = tip,
|
|
14
|
+
passProps = _.omit(props, ['id', 'step', 'title', 'completeOnboarding']);
|
|
15
|
+
return (
|
|
16
|
+
<Tooltip
|
|
17
|
+
{...passProps}
|
|
18
|
+
arrow={true}
|
|
19
|
+
open={open}
|
|
20
|
+
title={
|
|
21
|
+
<Stack gap={1}>
|
|
22
|
+
<Typography>{title}</Typography>
|
|
23
|
+
<Button onClick={dismiss} variant="text" color="primary" size="small">
|
|
24
|
+
Got it!
|
|
25
|
+
</Button>
|
|
26
|
+
</Stack>
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
{props.children}
|
|
30
|
+
</Tooltip>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const OnboardingTip = styled(BaseOnboardingTip)(({ theme }) => {
|
|
35
|
+
const { palette, spacing } = theme,
|
|
36
|
+
{ primary, background } = palette;
|
|
37
|
+
return {
|
|
38
|
+
[`& .${tooltipClasses.tooltip}`]: {
|
|
39
|
+
padding: `${spacing(1)} ${spacing(1.5)}`,
|
|
40
|
+
backgroundColor: primary.main,
|
|
41
|
+
color: primary.contrastText,
|
|
42
|
+
[`& .${buttonClasses.root}`]: {
|
|
43
|
+
backgroundColor: background.paper
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[`& .${tooltipClasses.arrow}`]: {
|
|
47
|
+
color: primary.main
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export { OnboardingTip };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { PropsWithChildren, createContext, useEffect, useMemo, useReducer } from 'react';
|
|
3
|
+
import { OnboardingModes } from './enums';
|
|
4
|
+
import { useOnboardingDataProvider } from './onboardingDataProvider';
|
|
5
|
+
import { ITipDescriptor } from './types';
|
|
6
|
+
|
|
7
|
+
enum OnboardingActionType {
|
|
8
|
+
RESTART = 'RESTART',
|
|
9
|
+
SET_SHOW = 'SET_SHOW',
|
|
10
|
+
REGISTER_TIP = 'REGISTER_TIP',
|
|
11
|
+
UNREGISTER_TIP = 'UNREGISTER_TIP',
|
|
12
|
+
DISMISS_TIP = 'DISMISS_TIP',
|
|
13
|
+
SET_ON_COMPLETE = 'SET_ON_COMPLETE'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type IOnboardingProviderProps = PropsWithChildren<{
|
|
17
|
+
mode: OnboardingModes;
|
|
18
|
+
topic?: string;
|
|
19
|
+
}>;
|
|
20
|
+
type IOnCompleteCallback = () => void;
|
|
21
|
+
type IOnboardingState = {
|
|
22
|
+
show: boolean;
|
|
23
|
+
tips: Array<ITipDescriptor>;
|
|
24
|
+
currentStep: number;
|
|
25
|
+
onComplete: IOnCompleteCallback;
|
|
26
|
+
};
|
|
27
|
+
type IOnboardingAction =
|
|
28
|
+
| { type: OnboardingActionType.RESTART }
|
|
29
|
+
| { type: OnboardingActionType.SET_SHOW; payload: boolean }
|
|
30
|
+
| { type: OnboardingActionType.REGISTER_TIP; payload: ITipDescriptor }
|
|
31
|
+
| { type: OnboardingActionType.UNREGISTER_TIP; payload: ITipDescriptor | string }
|
|
32
|
+
| { type: OnboardingActionType.DISMISS_TIP; payload: ITipDescriptor | string }
|
|
33
|
+
| { type: OnboardingActionType.SET_ON_COMPLETE; payload: IOnCompleteCallback };
|
|
34
|
+
type IOnboardingContext = { state: IOnboardingState; dispatch: (action: IOnboardingAction) => void };
|
|
35
|
+
|
|
36
|
+
function reducer(state: IOnboardingState, action: IOnboardingAction): IOnboardingState {
|
|
37
|
+
const newState = _.clone(state),
|
|
38
|
+
{ type } = action;
|
|
39
|
+
|
|
40
|
+
switch (type) {
|
|
41
|
+
case OnboardingActionType.SET_SHOW: {
|
|
42
|
+
const { payload } = action;
|
|
43
|
+
return _.extend(newState, { show: payload });
|
|
44
|
+
}
|
|
45
|
+
case OnboardingActionType.REGISTER_TIP: {
|
|
46
|
+
const { payload } = action,
|
|
47
|
+
{ id } = payload,
|
|
48
|
+
tips = _.chain(newState.tips)
|
|
49
|
+
.clone()
|
|
50
|
+
.reject((t) => t.id === id)
|
|
51
|
+
.value(),
|
|
52
|
+
existingTip = _.find(newState.tips, { id: id }),
|
|
53
|
+
newTip = _.chain(existingTip ?? {})
|
|
54
|
+
.clone()
|
|
55
|
+
.extend(payload)
|
|
56
|
+
.value();
|
|
57
|
+
|
|
58
|
+
tips.push(newTip);
|
|
59
|
+
return _.extend(newState, { tips: _.sortBy(tips, ['step']) });
|
|
60
|
+
}
|
|
61
|
+
case OnboardingActionType.UNREGISTER_TIP: {
|
|
62
|
+
const { payload } = action,
|
|
63
|
+
id = _.isString(payload) ? payload : payload.id,
|
|
64
|
+
tips = _.chain(newState.tips)
|
|
65
|
+
.reject((t) => t.id === id)
|
|
66
|
+
.sortBy(['step'])
|
|
67
|
+
.value();
|
|
68
|
+
return _.extend(newState, { tips: tips });
|
|
69
|
+
}
|
|
70
|
+
case OnboardingActionType.DISMISS_TIP: {
|
|
71
|
+
const { payload } = action,
|
|
72
|
+
id = _.isString(payload) ? payload : payload.id,
|
|
73
|
+
tips = _.chain(newState.tips)
|
|
74
|
+
.reject((t) => t.id === id)
|
|
75
|
+
.value(),
|
|
76
|
+
tip = _.find(newState.tips, { id: id });
|
|
77
|
+
|
|
78
|
+
if (_.isNil(tip)) {
|
|
79
|
+
return newState;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const newTip = _.chain(tip).clone().extend({ dismissed: true }).value();
|
|
83
|
+
tips.push(newTip);
|
|
84
|
+
const updatedStep =
|
|
85
|
+
_.chain(tips)
|
|
86
|
+
.reject((t) => t.dismissed ?? false)
|
|
87
|
+
.minBy('step')
|
|
88
|
+
.value()?.step ?? 0,
|
|
89
|
+
isOnboardingComplete =
|
|
90
|
+
newTip?.completeOnboarding &&
|
|
91
|
+
_.chain(tips)
|
|
92
|
+
.reject((t) => t.step < updatedStep)
|
|
93
|
+
.reject((t) => t.dismissed ?? false)
|
|
94
|
+
.isEmpty()
|
|
95
|
+
.value();
|
|
96
|
+
|
|
97
|
+
if (isOnboardingComplete) {
|
|
98
|
+
state.onComplete();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return _.extend(newState, { tips: _.sortBy(tips, ['step']), currentStep: updatedStep });
|
|
102
|
+
}
|
|
103
|
+
case OnboardingActionType.SET_ON_COMPLETE: {
|
|
104
|
+
const { payload } = action;
|
|
105
|
+
return _.extend(newState, { onComplete: payload });
|
|
106
|
+
}
|
|
107
|
+
case OnboardingActionType.RESTART: {
|
|
108
|
+
const tips = _.chain(newState.tips)
|
|
109
|
+
.clone()
|
|
110
|
+
.map((t) => _.omit(t, 'dismissed'))
|
|
111
|
+
.value();
|
|
112
|
+
return _.extend(newState, { tips: tips, currentStep: 0 });
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
return newState;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const OnboardingContext = createContext<IOnboardingContext | undefined>(undefined);
|
|
120
|
+
|
|
121
|
+
function OnboardingProvider(props: IOnboardingProviderProps) {
|
|
122
|
+
const { mode, topic = 'default' } = props,
|
|
123
|
+
{ onboardingRequired, acknowledgeOnboarding } = useOnboardingDataProvider(mode, topic),
|
|
124
|
+
[state, dispatch] = useReducer(reducer, { show: onboardingRequired, tips: [], currentStep: 0, onComplete: () => {} }),
|
|
125
|
+
value = useMemo(() => ({ state, dispatch }), [state, dispatch]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
dispatch({ type: OnboardingActionType.SET_SHOW, payload: onboardingRequired });
|
|
129
|
+
}, [onboardingRequired, dispatch]);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
function onComplete() {
|
|
133
|
+
acknowledgeOnboarding().then(() => {
|
|
134
|
+
dispatch({ type: OnboardingActionType.SET_SHOW, payload: false });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
dispatch({ type: OnboardingActionType.SET_ON_COMPLETE, payload: onComplete });
|
|
138
|
+
}, [acknowledgeOnboarding, dispatch]);
|
|
139
|
+
|
|
140
|
+
return <OnboardingContext.Provider value={value}>{props.children}</OnboardingContext.Provider>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type { IOnboardingContext, IOnboardingState, IOnboardingAction };
|
|
144
|
+
|
|
145
|
+
export { OnboardingProvider, OnboardingContext, OnboardingActionType };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { useCallback, useContext, useEffect } from 'react';
|
|
3
|
+
import { ITipDescriptor } from './types';
|
|
4
|
+
import * as yup from 'yup';
|
|
5
|
+
import { IOnboardingAction, IOnboardingContext, IOnboardingState, OnboardingActionType, OnboardingContext } from './Provider';
|
|
6
|
+
|
|
7
|
+
function useOnboardingContext(): IOnboardingContext {
|
|
8
|
+
const context = useContext(OnboardingContext);
|
|
9
|
+
if (context === undefined) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
'[Onboarding] useOnboardingContext: context undefined. Please provide a valid OnboardingContext using OnboardingProvider'
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return context;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function useOnboardingState(): IOnboardingState {
|
|
18
|
+
const context = useOnboardingContext();
|
|
19
|
+
return context.state;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function useOnboardingDispatch(): React.Dispatch<IOnboardingAction> {
|
|
23
|
+
const context = useOnboardingContext();
|
|
24
|
+
return context.dispatch;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useOnboardingTip(config: ITipDescriptor): {
|
|
28
|
+
tip: ITipDescriptor & { open: boolean };
|
|
29
|
+
dismiss: () => void;
|
|
30
|
+
} {
|
|
31
|
+
yup.object({ id: yup.string().required(), step: yup.number().required() }).validateSync(config);
|
|
32
|
+
|
|
33
|
+
const { id, step, completeOnboarding } = config,
|
|
34
|
+
{ show, tips, currentStep } = useOnboardingState(),
|
|
35
|
+
tip = _.find(tips, { id: id }),
|
|
36
|
+
{ dismissed } = tip ?? {},
|
|
37
|
+
dispatch = useOnboardingDispatch(),
|
|
38
|
+
dismiss = useCallback(() => {
|
|
39
|
+
dispatch({
|
|
40
|
+
type: OnboardingActionType.DISMISS_TIP,
|
|
41
|
+
payload: id
|
|
42
|
+
});
|
|
43
|
+
}, [dispatch, id]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (_.isNil(tip) || step !== tip?.step) {
|
|
47
|
+
dispatch({
|
|
48
|
+
type: OnboardingActionType.REGISTER_TIP,
|
|
49
|
+
payload: {
|
|
50
|
+
id: id,
|
|
51
|
+
step: step,
|
|
52
|
+
completeOnboarding: completeOnboarding
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}, [id, step, completeOnboarding, tip]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
return () => {
|
|
60
|
+
dispatch({ type: OnboardingActionType.UNREGISTER_TIP, payload: id });
|
|
61
|
+
};
|
|
62
|
+
}, [id, dispatch]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
tip: _.chain(tip ?? config)
|
|
66
|
+
.clone()
|
|
67
|
+
.extend({ open: show && !_.isNil(tip) && currentStep === step && !dismissed })
|
|
68
|
+
.value(),
|
|
69
|
+
dismiss: dismiss
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function useRestartOnboarding(): () => void {
|
|
74
|
+
const dispatch = useOnboardingDispatch();
|
|
75
|
+
return useCallback(() => {
|
|
76
|
+
dispatch({ type: OnboardingActionType.RESTART });
|
|
77
|
+
}, [dispatch]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { useOnboardingTip, useRestartOnboarding };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { useLocalStorage } from '../../hooks';
|
|
3
|
+
import { OnboardingModes } from './enums';
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import * as yup from 'yup';
|
|
6
|
+
import { onboardingModesSchema, onboardingTopicSchema } from './schemas';
|
|
7
|
+
import { useDataProvider, useGetIdentity } from 'ra-core';
|
|
8
|
+
import { useQuery, useQueryClient, useMutation } from 'react-query';
|
|
9
|
+
|
|
10
|
+
type IOnboardingDataProvider = {
|
|
11
|
+
onboardingRequired: boolean;
|
|
12
|
+
acknowledgeOnboarding: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function useUserProfileDataProvider(topic: string): (IOnboardingDataProvider | object) & { loading: boolean; error: boolean } {
|
|
16
|
+
const queryClient = useQueryClient(),
|
|
17
|
+
[onboardingRequired, setOnboardingRequired] = useState(false),
|
|
18
|
+
[acknowledgeOnboarding, setAcknowledgeOnboarding] = useState(() => () => Promise.resolve()),
|
|
19
|
+
identity = useGetIdentity(),
|
|
20
|
+
user = identity?.data?.email,
|
|
21
|
+
dataProvider = useDataProvider(),
|
|
22
|
+
fetch = useCallback(() => {
|
|
23
|
+
if (!user) {
|
|
24
|
+
return Promise.reject();
|
|
25
|
+
} else {
|
|
26
|
+
return dataProvider.get('onboarding').then((response: any) => {
|
|
27
|
+
return response?.data?.value;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}, [dataProvider, user]),
|
|
31
|
+
{ data, isLoading, isError } = useQuery({
|
|
32
|
+
queryKey: ['GET', 'onboarding', user],
|
|
33
|
+
queryFn: fetch,
|
|
34
|
+
staleTime: 24 * 60 * 60 * 1000,
|
|
35
|
+
retry: user !== undefined
|
|
36
|
+
}),
|
|
37
|
+
[post, setPost] = useState(() => () => Promise.resolve()),
|
|
38
|
+
mutation = useMutation({ mutationFn: post }),
|
|
39
|
+
mutateAsync = mutation.mutateAsync;
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
function post() {
|
|
43
|
+
const newValue = _.chain(data ?? {})
|
|
44
|
+
.clone()
|
|
45
|
+
.extend({ [topic]: Date.now() })
|
|
46
|
+
.value();
|
|
47
|
+
return dataProvider.post('onboarding', newValue).then(() => {
|
|
48
|
+
queryClient.invalidateQueries(['GET', 'onboarding', user]);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
setPost(() => post);
|
|
52
|
+
}, [dataProvider, data, topic, queryClient, user]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setOnboardingRequired(_.isNil((data ?? {})[topic]));
|
|
56
|
+
}, [data, topic, setOnboardingRequired]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setAcknowledgeOnboarding(() => () => {
|
|
60
|
+
return mutateAsync();
|
|
61
|
+
});
|
|
62
|
+
}, [mutateAsync]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
loading: isLoading,
|
|
66
|
+
error: isError,
|
|
67
|
+
onboardingRequired: onboardingRequired,
|
|
68
|
+
acknowledgeOnboarding: acknowledgeOnboarding
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function useLocalStorageDataProvider(topic: string): IOnboardingDataProvider {
|
|
73
|
+
const [localStorageValue, setLocalStorageValue] = useLocalStorage('onboarding', {}) as [
|
|
74
|
+
{ [key: string]: number },
|
|
75
|
+
(value: (oldValue: { [key: string]: number }) => { [key: string]: number }) => { [key: string]: number }
|
|
76
|
+
],
|
|
77
|
+
[onboardingRequired, setOnboardingRequired] = useState(false),
|
|
78
|
+
[acknowledgeOnboarding, setAcknowledgeOnboarding] = useState(() => () => Promise.resolve());
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setOnboardingRequired(_.isNil((localStorageValue ?? {})[topic]));
|
|
82
|
+
}, [localStorageValue, topic, setOnboardingRequired]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
setAcknowledgeOnboarding(() => () => {
|
|
86
|
+
setLocalStorageValue((oldValue) => _.extend(oldValue, { [topic]: Date.now() }));
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
});
|
|
89
|
+
}, [topic, setLocalStorageValue, setAcknowledgeOnboarding]);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
onboardingRequired: onboardingRequired,
|
|
93
|
+
acknowledgeOnboarding: acknowledgeOnboarding
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function useFakeLocalStorageDataProvider(): IOnboardingDataProvider {
|
|
98
|
+
const setterRef = useRef(() => Promise.resolve());
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
onboardingRequired: false,
|
|
102
|
+
acknowledgeOnboarding: setterRef.current
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function useOnboardingDataProvider(mode: OnboardingModes, topic: string): IOnboardingDataProvider {
|
|
107
|
+
yup.object({ mode: onboardingModesSchema, topic: onboardingTopicSchema }).validateSync({ mode: mode, topic: topic });
|
|
108
|
+
const userProfileDataProvider = useUserProfileDataProvider(topic),
|
|
109
|
+
localStorageDataProvider = useLocalStorageDataProvider(topic),
|
|
110
|
+
fakeLocalStorageDataProvider = useFakeLocalStorageDataProvider();
|
|
111
|
+
|
|
112
|
+
switch (mode) {
|
|
113
|
+
case OnboardingModes.USER:
|
|
114
|
+
if (userProfileDataProvider.loading) {
|
|
115
|
+
return fakeLocalStorageDataProvider;
|
|
116
|
+
} else if (userProfileDataProvider.error) {
|
|
117
|
+
return localStorageDataProvider;
|
|
118
|
+
} else {
|
|
119
|
+
return _.pick(userProfileDataProvider, ['onboardingRequired', 'acknowledgeOnboarding']) as IOnboardingDataProvider;
|
|
120
|
+
}
|
|
121
|
+
case OnboardingModes.BROWSER:
|
|
122
|
+
return localStorageDataProvider;
|
|
123
|
+
default:
|
|
124
|
+
return fakeLocalStorageDataProvider;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { useOnboardingDataProvider };
|
package/src/components/index.jsx
CHANGED
|
@@ -12,6 +12,7 @@ import ScrollX from './ScrollX';
|
|
|
12
12
|
import SmallIcon from './SmallIcon';
|
|
13
13
|
export { MainIcon, ActionsMenu, Loadable, Loader, Layout, Logo, MainCard, ScrollTop, ScrollX, MenuPopover, Notification, SmallIcon };
|
|
14
14
|
export * from './Layout';
|
|
15
|
+
export { OnboardingModes, OnboardingProvider, OnboardingTip, useRestartOnboarding } from './Onboarding';
|
|
15
16
|
|
|
16
17
|
export * from './@extended';
|
|
17
18
|
export * from './third-party';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import * as yup from 'yup';
|
|
4
|
+
import { LocalStorage } from '../utils/localStorage';
|
|
5
|
+
|
|
6
|
+
const localStorageInstance = new LocalStorage();
|
|
7
|
+
|
|
8
|
+
function useLocalStorage(key: string, defaultValue?: unknown): [unknown, (newValue: unknown) => unknown] {
|
|
9
|
+
yup.string().required().validateSync(key);
|
|
10
|
+
|
|
11
|
+
function buildSetter(key: string, currentValue: unknown, callback: (newValue: unknown) => unknown): (newValue: unknown) => unknown {
|
|
12
|
+
return function (newValue: unknown) {
|
|
13
|
+
const updatedValue = _.isFunction(newValue) ? newValue(currentValue) : newValue;
|
|
14
|
+
localStorageInstance.set(key, updatedValue);
|
|
15
|
+
callback(updatedValue);
|
|
16
|
+
return updatedValue;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const [fallbackValue, updateFallbackValue] = useState(defaultValue),
|
|
21
|
+
[state, updateState] = useState(localStorageInstance.get(key) ?? defaultValue),
|
|
22
|
+
[setter, updateSetter] = useState(() => buildSetter(key, state, updateState));
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!_.isEqual(defaultValue, fallbackValue)) {
|
|
26
|
+
updateFallbackValue(defaultValue);
|
|
27
|
+
}
|
|
28
|
+
}, [defaultValue, fallbackValue, updateFallbackValue]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
updateState(localStorageInstance.get(key) ?? fallbackValue);
|
|
32
|
+
}, [key, fallbackValue, updateState]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
function listener(value: unknown) {
|
|
36
|
+
updateState(value);
|
|
37
|
+
}
|
|
38
|
+
localStorageInstance.watch(key, listener);
|
|
39
|
+
return () => {
|
|
40
|
+
localStorageInstance.unwatch(key, listener);
|
|
41
|
+
};
|
|
42
|
+
}, [key, updateState]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
updateSetter(() => buildSetter(key, state, updateState));
|
|
46
|
+
}, [key, state, updateState, updateSetter]);
|
|
47
|
+
|
|
48
|
+
return [state, setter];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default useLocalStorage;
|
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { OnboardingTip, OnboardingProvider, OnboardingModes, useRestartOnboarding } from '@applica-software-guru/react-admin';
|
|
2
|
+
import { Button, Stack, Typography } from '@mui/material';
|
|
4
3
|
|
|
5
4
|
const CustomPage = () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
return (
|
|
6
|
+
<OnboardingProvider mode={OnboardingModes.BROWSER}>
|
|
7
|
+
<Stack direction="row" justifyContent="space-between">
|
|
8
|
+
<OnboardingTip title="Test 1" id="test1">
|
|
9
|
+
<Typography>Test 1</Typography>
|
|
10
|
+
</OnboardingTip>
|
|
11
|
+
<OnboardingTip title="Test 2" id="test2">
|
|
12
|
+
<Typography>Test 2</Typography>
|
|
13
|
+
</OnboardingTip>
|
|
14
|
+
<OnboardingTip title="Test 3" id="test3" step={1}>
|
|
15
|
+
<Typography>Test 3</Typography>
|
|
16
|
+
</OnboardingTip>
|
|
17
|
+
</Stack>
|
|
18
|
+
<RestartButton />
|
|
19
|
+
</OnboardingProvider>
|
|
20
|
+
);
|
|
11
21
|
};
|
|
12
22
|
|
|
23
|
+
export function RestartButton() {
|
|
24
|
+
const restartOnboarding = useRestartOnboarding();
|
|
25
|
+
return <Button onClick={restartOnboarding}>RESTART</Button>;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
export default CustomPage;
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import * as yup from 'yup';
|
|
3
|
+
|
|
4
|
+
const listenerSchema = yup.mixed().test('function', 'Invalid function', (value) => {
|
|
5
|
+
return _.isFunction(value);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
type IListener = (value: unknown) => void;
|
|
9
|
+
interface ILocalStorage {
|
|
10
|
+
get: (key: string) => unknown | null;
|
|
11
|
+
set: (key: string, value: unknown) => unknown | null;
|
|
12
|
+
watch: (key: string, callback: IListener) => void;
|
|
13
|
+
unwatch: (key: string, callback?: IListener) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class LocalStorage implements ILocalStorage {
|
|
17
|
+
#listeners: Array<{ key: string; callback: IListener }> = [];
|
|
18
|
+
|
|
19
|
+
#canAccessLocalStorage(): boolean {
|
|
20
|
+
return typeof window !== 'undefined';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(key: string): unknown | null {
|
|
24
|
+
yup.string().required().validateSync(key);
|
|
25
|
+
if (!this.#canAccessLocalStorage()) {
|
|
26
|
+
return null;
|
|
27
|
+
} else {
|
|
28
|
+
const value = localStorage.getItem(key);
|
|
29
|
+
return _.isNil(value) ? null : JSON.parse(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set(key: string, value: unknown): unknown | null {
|
|
34
|
+
yup.string().required().validateSync(key);
|
|
35
|
+
if (this.#canAccessLocalStorage()) {
|
|
36
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
37
|
+
return value;
|
|
38
|
+
} else {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
watch(key: string, callback: IListener): void {
|
|
44
|
+
yup.string().required().validateSync(key);
|
|
45
|
+
listenerSchema.validateSync(callback);
|
|
46
|
+
if (this.#canAccessLocalStorage()) {
|
|
47
|
+
this.#listeners.push({ key: key, callback: callback });
|
|
48
|
+
window.addEventListener('storage', (e) => {
|
|
49
|
+
if (e.storageArea === localStorage && e.key === key) {
|
|
50
|
+
callback(this.get(key));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
unwatch(key: string, callback?: IListener | undefined): void {
|
|
57
|
+
yup.string().required().validateSync(key);
|
|
58
|
+
listenerSchema.optional().default(_.identity).validateSync(callback);
|
|
59
|
+
if (this.#canAccessLocalStorage()) {
|
|
60
|
+
if (callback !== undefined) {
|
|
61
|
+
this.#listeners = _.reject(this.#listeners, (listener) => {
|
|
62
|
+
return listener.key === key && listener.callback === callback;
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
this.#listeners = _.reject(this.#listeners, (listener) => {
|
|
66
|
+
return listener.key === key;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { LocalStorage };
|