@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.
Files changed (47) hide show
  1. package/dist/components/Onboarding/OnboardingTip.d.ts +5 -0
  2. package/dist/components/Onboarding/OnboardingTip.d.ts.map +1 -0
  3. package/dist/components/Onboarding/Provider.d.ts +49 -0
  4. package/dist/components/Onboarding/Provider.d.ts.map +1 -0
  5. package/dist/components/Onboarding/enums.d.ts +7 -0
  6. package/dist/components/Onboarding/enums.d.ts.map +1 -0
  7. package/dist/components/Onboarding/hooks.d.ts +10 -0
  8. package/dist/components/Onboarding/hooks.d.ts.map +1 -0
  9. package/dist/components/Onboarding/index.d.ts +7 -0
  10. package/dist/components/Onboarding/index.d.ts.map +1 -0
  11. package/dist/components/Onboarding/onboardingDataProvider.d.ts +8 -0
  12. package/dist/components/Onboarding/onboardingDataProvider.d.ts.map +1 -0
  13. package/dist/components/Onboarding/schemas.d.ts +5 -0
  14. package/dist/components/Onboarding/schemas.d.ts.map +1 -0
  15. package/dist/components/Onboarding/types.d.ts +8 -0
  16. package/dist/components/Onboarding/types.d.ts.map +1 -0
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/hooks/index.d.ts +14 -14
  19. package/dist/hooks/index.d.ts.map +1 -1
  20. package/dist/hooks/useLocalStorage.d.ts +2 -1
  21. package/dist/hooks/useLocalStorage.d.ts.map +1 -1
  22. package/dist/react-admin.cjs.js +69 -66
  23. package/dist/react-admin.cjs.js.map +1 -1
  24. package/dist/react-admin.es.js +13683 -11875
  25. package/dist/react-admin.es.js.map +1 -1
  26. package/dist/react-admin.umd.js +70 -67
  27. package/dist/react-admin.umd.js.map +1 -1
  28. package/dist/utils/index.d.ts +1 -0
  29. package/dist/utils/index.d.ts.map +1 -1
  30. package/dist/utils/localStorage.d.ts +16 -0
  31. package/dist/utils/localStorage.d.ts.map +1 -0
  32. package/package.json +3 -2
  33. package/src/components/Onboarding/OnboardingTip.tsx +52 -0
  34. package/src/components/Onboarding/Provider.tsx +145 -0
  35. package/src/components/Onboarding/enums.ts +7 -0
  36. package/src/components/Onboarding/hooks.tsx +80 -0
  37. package/src/components/Onboarding/index.ts +6 -0
  38. package/src/components/Onboarding/onboardingDataProvider.tsx +128 -0
  39. package/src/components/Onboarding/schemas.ts +6 -0
  40. package/src/components/Onboarding/types.ts +8 -0
  41. package/src/components/index.jsx +1 -0
  42. package/src/hooks/useLocalStorage.tsx +51 -0
  43. package/src/playground/components/pages/CustomPage.jsx +23 -8
  44. package/src/utils/index.ts +1 -0
  45. package/src/utils/localStorage.ts +73 -0
  46. package/src/hooks/useLocalStorage.jsx +0 -31
  47. /package/src/hooks/{index.jsx → index.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  export * from './time';
2
2
  export * from './lang';
3
3
  export * from './localizedValue';
4
+ export * from './localStorage';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -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.163",
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,7 @@
1
+ enum OnboardingModes {
2
+ NONE = 'none',
3
+ BROWSER = 'browser',
4
+ USER = 'user'
5
+ }
6
+
7
+ export { OnboardingModes };
@@ -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,6 @@
1
+ export * from './enums';
2
+ export * from './hooks';
3
+ export * from './onboardingDataProvider';
4
+ export * from './OnboardingTip';
5
+ export * from './Provider';
6
+ export * from './schemas';
@@ -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 };
@@ -0,0 +1,6 @@
1
+ import * as yup from 'yup';
2
+ import { OnboardingModes } from './enums';
3
+ const onboardingTopicSchema = yup.string().required(),
4
+ onboardingModesSchema = yup.string().oneOf(Object.values(OnboardingModes));
5
+
6
+ export { onboardingModesSchema, onboardingTopicSchema };
@@ -0,0 +1,8 @@
1
+ type ITipDescriptor = {
2
+ id: string;
3
+ step: number;
4
+ dismissed?: boolean;
5
+ completeOnboarding?: boolean;
6
+ };
7
+
8
+ export type { ITipDescriptor };
@@ -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 { Typography } from '@mui/material';
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
- useEffect(() => {
7
- throw new Error('Custom Page not implemented yet');
8
- }, []);
9
-
10
- return <Typography>Hello, Custom Page!</Typography>;
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;
@@ -1,3 +1,4 @@
1
1
  export * from './time';
2
2
  export * from './lang';
3
3
  export * from './localizedValue';
4
+ export * from './localStorage';
@@ -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 };