@flowerforce/flower-react 4.0.11-beta.0 → 4.0.11-beta.2
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/CHANGELOG.md +48 -0
- package/dist/index.cjs.js +280 -85
- package/dist/index.esm.js +276 -85
- package/dist/src/abacProvider/abacProvider.d.ts +18 -0
- package/dist/src/abacProvider/index.d.ts +1 -0
- package/dist/src/components/Flower.d.ts +3 -15
- package/dist/src/components/hooks/eventsHandlers.d.ts +5 -0
- package/dist/src/components/hooks/index.d.ts +5 -0
- package/dist/src/components/hooks/types/index.d.ts +29 -0
- package/dist/src/components/hooks/useBrowserNavigationSync.d.ts +12 -0
- package/dist/src/components/hooks/useDestroyFlow.d.ts +2 -0
- package/dist/src/components/hooks/useInitDevtools.d.ts +2 -0
- package/dist/src/components/hooks/useInitNodes.d.ts +2 -0
- package/dist/src/components/hooks/useSelectorsClient.d.ts +6 -0
- package/dist/src/components/useFlower/utils.d.ts +2 -0
- package/dist/src/features/selectors/selectors.d.ts +175 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/types/Flower.d.ts +15 -0
- package/dist/src/types/utilsTypes.d.ts +4 -0
- package/dist/src/utils.d.ts +2 -10
- package/package.json +12 -7
package/dist/index.esm.js
CHANGED
@@ -1,20 +1,46 @@
|
|
1
|
-
import React, { memo, useRef, useState, useMemo, Children,
|
1
|
+
import React, { useEffect, memo, useRef, useState, useMemo, Children, useContext, useCallback, createContext } from 'react';
|
2
2
|
import _keyBy from 'lodash/keyBy';
|
3
|
-
import {
|
3
|
+
import { FlowUtils, FlowerCoreBaseReducers, REDUCER_NAME, FlowerCoreStateSelectors, FlowerStateUtils, Emitter, devtoolState, isAbacInitialized, getRawRules, initAbac, getSubject, getAbacEngine } from '@flowerforce/flower-core';
|
4
4
|
import { FlowerReactProvider, FlowerReactContext } from '@flowerforce/flower-react-context';
|
5
5
|
import _get from 'lodash/get';
|
6
|
-
import { createSlice, configureStore, combineReducers, createAction } from '@reduxjs/toolkit';
|
7
6
|
import { reducerData, ReduxFlowerProvider, flowerDataActions, middlewares } from '@flowerforce/flower-react-store';
|
8
7
|
export { createApi } from '@flowerforce/flower-react-store';
|
8
|
+
import { createSlice, configureStore, combineReducers, createAction } from '@reduxjs/toolkit';
|
9
9
|
import { createSelector } from 'reselect';
|
10
|
+
import { useHistoryContext } from '@flowerforce/flower-react-history-context';
|
11
|
+
export { HistoryContextProvider } from '@flowerforce/flower-react-history-context';
|
10
12
|
import { FlowerRule } from '@flowerforce/flower-react-shared';
|
11
13
|
import set from 'lodash/set';
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
const res = CoreUtils.generateNodesForFlowerJson(nodes);
|
16
|
-
return res;
|
15
|
+
const getRulesExists = (rules) => {
|
16
|
+
return Object.keys(rules).length ? FlowUtils.mapEdge(rules) : undefined;
|
17
17
|
};
|
18
|
+
function hasDisplayName(type) {
|
19
|
+
return (!!type &&
|
20
|
+
typeof type === 'object' &&
|
21
|
+
'displayName' in type &&
|
22
|
+
typeof type.displayName === 'string');
|
23
|
+
}
|
24
|
+
const generateNodesForFlowerJson = (nodes) => nodes
|
25
|
+
.filter((child) => typeof child === 'object' && child !== null && 'props' in child)
|
26
|
+
.filter((e) => !!_get(e, 'props.id'))
|
27
|
+
.map((e) => {
|
28
|
+
const rules = FlowUtils.makeRules(e.props.to ?? {});
|
29
|
+
const nextRules = getRulesExists(rules);
|
30
|
+
const children = e.props.data?.children;
|
31
|
+
const nodeType = hasDisplayName(e.type)
|
32
|
+
? e.type.displayName
|
33
|
+
: e.props.as || 'FlowerNode';
|
34
|
+
return {
|
35
|
+
nodeId: e.props.id,
|
36
|
+
nodeType,
|
37
|
+
nodeTitle: _get(e.props, 'data.title'),
|
38
|
+
children,
|
39
|
+
nextRules,
|
40
|
+
retain: e.props.retain,
|
41
|
+
disabled: e.props.disabled
|
42
|
+
};
|
43
|
+
});
|
18
44
|
|
19
45
|
const flowerReducer = createSlice({
|
20
46
|
name: REDUCER_NAME.FLOWER_FLOW,
|
@@ -41,60 +67,37 @@ const selectFlowerHistory = (name) => createSelector(selectFlower(name), FlowerC
|
|
41
67
|
const makeSelectNodesIds = (name) => createSelector(selectFlower(name), FlowerCoreStateSelectors.makeSelectNodesIds);
|
42
68
|
const makeSelectStartNodeId = (name) => createSelector(selectFlower(name), FlowerCoreStateSelectors.makeSelectStartNodeId);
|
43
69
|
const makeSelectCurrentNodeId = (name) => createSelector(selectFlower(name), makeSelectStartNodeId(name), FlowerCoreStateSelectors.makeSelectCurrentNodeId);
|
70
|
+
const makeSelectIsCurrentNode = (name) => createSelector(selectFlower(name), makeSelectCurrentNodeId(name), (flower, current) => flower.nodes[current]);
|
44
71
|
const makeSelectPrevNodeRetain = (name) => createSelector(makeSelectNodesIds(name), selectFlowerHistory(name), makeSelectCurrentNodeId(name), FlowerCoreStateSelectors.makeSelectPrevNodeRetain);
|
45
72
|
const makeSelectCurrentNodeDisabled = (name) => createSelector(makeSelectNodesIds(name), makeSelectCurrentNodeId(name), FlowerCoreStateSelectors.makeSelectCurrentNodeDisabled);
|
46
73
|
// dati nel flow selezionato
|
47
74
|
const makeSelectData = (name) => createSelector(selectFlowerDataNode(name), (data) => data?.data ?? {});
|
48
75
|
createSelector(selectGlobalData, FlowerStateUtils.getAllData);
|
49
76
|
|
50
|
-
|
51
|
-
|
52
|
-
/*
|
53
|
-
* FlowerClient
|
54
|
-
*/
|
55
|
-
const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null, initialState = {}, initialData }) => {
|
56
|
-
const flowName = name;
|
57
|
-
const { dispatch, store, useSelector } = ReduxFlowerProvider.getReduxHooks();
|
58
|
-
const one = useRef(false);
|
59
|
-
const [wsDevtools, setWsDevtools] = useState(devtoolState && _get(devtoolState, '__FLOWER_DEVTOOLS_INITIALIZED__', false));
|
60
|
-
// TODO could make that transformation in CoreUtils.generateNodesForFlowerJson
|
61
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps, max-len
|
62
|
-
const nodes = useMemo(
|
63
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
64
|
-
() => convertElements(Children.toArray(children)), [children]);
|
65
|
-
const nodeById = useMemo(() => _keyBy(Children.toArray(children), 'props.id'), [children]);
|
66
|
-
const isInitialized = useSelector(makeSelectStartNodeId(name));
|
67
|
-
const history = useSelector(selectFlowerHistory(name));
|
68
|
-
const current = useSelector(makeSelectCurrentNodeId(flowName));
|
69
|
-
const isDisabled = useSelector(makeSelectCurrentNodeDisabled(flowName));
|
70
|
-
const prevFlowerNodeId = useSelector(makeSelectPrevNodeRetain(flowName));
|
77
|
+
const useInitNodes = ({ one, nodes, name, startId, persist = false, initialData, initialState = {} }) => {
|
78
|
+
const { dispatch } = ReduxFlowerProvider.getReduxHooks();
|
71
79
|
useEffect(() => {
|
72
80
|
if (nodes.length > 0 && one.current === false) {
|
73
81
|
one.current = true;
|
74
82
|
dispatch(flowerActions.initNodes({
|
75
|
-
name
|
76
|
-
// @ts-expect-error FIX ME
|
83
|
+
name,
|
77
84
|
nodes,
|
78
85
|
startId: startId ?? '',
|
79
|
-
persist
|
86
|
+
persist,
|
80
87
|
initialState
|
81
88
|
}));
|
82
|
-
if (initialData) {
|
83
|
-
dispatch(flowerDataActions.initData({
|
84
|
-
rootName: flowName,
|
85
|
-
initialData: initialData ?? {}
|
86
|
-
}));
|
87
|
-
}
|
88
89
|
}
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
90
|
+
if (initialData) {
|
91
|
+
dispatch(flowerDataActions.initData({
|
92
|
+
rootName: name,
|
93
|
+
initialData: initialData ?? {}
|
94
|
+
}));
|
95
|
+
}
|
96
|
+
}, [dispatch, name, nodes, startId, initialData, initialState, persist]);
|
97
|
+
};
|
98
|
+
|
99
|
+
const useInitDevtools = ({ devtoolState, setWsDevtools, flowName }) => {
|
100
|
+
const { dispatch } = ReduxFlowerProvider.getReduxHooks();
|
98
101
|
useEffect(() => {
|
99
102
|
/* istanbul ignore next */
|
100
103
|
const eventCb = (msg) => {
|
@@ -108,12 +111,10 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
108
111
|
dispatch(flowerActions.setCurrentNode({ name: msg.name, node: msg.id }));
|
109
112
|
}
|
110
113
|
// if (msg.action === 'REPLACE_DATA' && msg.name === flowName) {
|
111
|
-
// dispatch(
|
112
|
-
// formActions.replaceData({ flowName: msg.name, value: msg.data })
|
113
|
-
// )
|
114
|
+
// dispatch(actions.replaceData({ flowName: msg.name, value: msg.data }))
|
114
115
|
// }
|
115
116
|
// if (msg.action === 'ADD_DATA' && msg.name === flowName) {
|
116
|
-
// dispatch(
|
117
|
+
// dispatch(actions.addData({ flowName: msg.name, value: msg.data }))
|
117
118
|
// }
|
118
119
|
};
|
119
120
|
/* istanbul ignore next */
|
@@ -127,13 +128,22 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
127
128
|
}
|
128
129
|
};
|
129
130
|
}, [dispatch, flowName]);
|
131
|
+
};
|
132
|
+
|
133
|
+
const useDestroyFlow = ({ flowName, one, persist }) => {
|
134
|
+
const { dispatch } = ReduxFlowerProvider.getReduxHooks();
|
130
135
|
useEffect(() => () => {
|
131
|
-
|
132
|
-
|
136
|
+
if (persist)
|
137
|
+
return;
|
138
|
+
if (one.current === true) {
|
133
139
|
one.current = false;
|
134
140
|
dispatch(flowerActions.destroy({ name: flowName }));
|
135
141
|
}
|
136
|
-
}, [dispatch, flowName,
|
142
|
+
}, [dispatch, flowName, persist]);
|
143
|
+
};
|
144
|
+
|
145
|
+
const useClientInitEvent = ({ flowName, isInitialized, wsDevtools }) => {
|
146
|
+
const { store } = ReduxFlowerProvider.getReduxHooks();
|
137
147
|
useEffect(() => {
|
138
148
|
/* istanbul ignore next */
|
139
149
|
if (isInitialized &&
|
@@ -149,7 +159,11 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
149
159
|
getState: store.getState
|
150
160
|
});
|
151
161
|
}
|
152
|
-
}, [
|
162
|
+
}, [flowName, wsDevtools, isInitialized, store]);
|
163
|
+
};
|
164
|
+
const useSetHistoryEvent = ({ flowName, isInitialized, wsDevtools }) => {
|
165
|
+
const { useSelector } = ReduxFlowerProvider.getReduxHooks();
|
166
|
+
const history = useSelector(selectFlowerHistory(flowName));
|
153
167
|
useEffect(() => {
|
154
168
|
/* istanbul ignore next */
|
155
169
|
if (isInitialized &&
|
@@ -163,7 +177,9 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
163
177
|
history
|
164
178
|
});
|
165
179
|
}
|
166
|
-
}, [
|
180
|
+
}, [flowName, history, wsDevtools, isInitialized]);
|
181
|
+
};
|
182
|
+
const useSetCurrentEvent = ({ current, flowName, isInitialized, wsDevtools }) => {
|
167
183
|
useEffect(() => {
|
168
184
|
if (!current)
|
169
185
|
return;
|
@@ -183,6 +199,9 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
183
199
|
});
|
184
200
|
}
|
185
201
|
}, [flowName, current, wsDevtools, isInitialized]);
|
202
|
+
};
|
203
|
+
const useFlowerNavigateEvent = ({ current, flowName, isInitialized, wsDevtools, isDisabled }) => {
|
204
|
+
const { dispatch } = ReduxFlowerProvider.getReduxHooks();
|
186
205
|
useEffect(() => {
|
187
206
|
if (!current)
|
188
207
|
return;
|
@@ -190,7 +209,10 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
190
209
|
if (!isInitialized)
|
191
210
|
return;
|
192
211
|
if (isDisabled) {
|
193
|
-
dispatch(
|
212
|
+
dispatch({
|
213
|
+
type: `${REDUCER_NAME.FLOWER_FLOW}/next`,
|
214
|
+
payload: { flowName, disabled: true }
|
215
|
+
});
|
194
216
|
// eslint-disable-next-line no-underscore-dangle, no-undef
|
195
217
|
/* istanbul ignore next */
|
196
218
|
if (wsDevtools &&
|
@@ -213,7 +235,7 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
213
235
|
devtoolState &&
|
214
236
|
_get(devtoolState, '__FLOWER_DEVTOOLS__')) {
|
215
237
|
if (isInitialized === current)
|
216
|
-
return; // salto il primo
|
238
|
+
return; // salto il primo evento
|
217
239
|
Emitter.emit('flower-devtool-from-client', {
|
218
240
|
source: 'flower-client',
|
219
241
|
action: 'SET_SELECTED',
|
@@ -222,15 +244,55 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
222
244
|
time: new Date()
|
223
245
|
});
|
224
246
|
}
|
225
|
-
}, [
|
226
|
-
|
227
|
-
|
247
|
+
}, [dispatch, flowName, current, isDisabled, wsDevtools, isInitialized]);
|
248
|
+
};
|
249
|
+
|
250
|
+
const useSelectorsClient = (flowName) => {
|
251
|
+
const { useSelector } = ReduxFlowerProvider.getReduxHooks();
|
252
|
+
const isInitialized = useSelector(makeSelectStartNodeId(flowName));
|
253
|
+
const current = useSelector(makeSelectCurrentNodeId(flowName));
|
254
|
+
const isDisabled = useSelector(makeSelectCurrentNodeDisabled(flowName));
|
255
|
+
const prevFlowerNodeId = useSelector(makeSelectPrevNodeRetain(flowName));
|
256
|
+
return {
|
257
|
+
isInitialized,
|
258
|
+
current,
|
259
|
+
isDisabled,
|
260
|
+
prevFlowerNodeId
|
261
|
+
};
|
262
|
+
};
|
263
|
+
|
264
|
+
/*
|
265
|
+
* FlowerClient
|
266
|
+
*/
|
267
|
+
const FlowerClient = ({ children, name, destroyOnUnmount = true, persist = false, startId = null, initialState = {}, initialData }) => {
|
268
|
+
const flowName = name;
|
269
|
+
const _persist = persist || !destroyOnUnmount;
|
270
|
+
const one = useRef(false);
|
271
|
+
const [wsDevtools, setWsDevtools] = useState(devtoolState && _get(devtoolState, '__FLOWER_DEVTOOLS_INITIALIZED__', false));
|
272
|
+
const nodes = useMemo(() => generateNodesForFlowerJson(Children.toArray(children)), [children]);
|
273
|
+
const nodeById = useMemo(() => _keyBy(Children.toArray(children), 'props.id'), [children]);
|
274
|
+
const { current, isDisabled, isInitialized, prevFlowerNodeId } = useSelectorsClient(flowName);
|
275
|
+
useInitNodes({
|
276
|
+
name,
|
277
|
+
nodes,
|
278
|
+
one,
|
279
|
+
initialData,
|
280
|
+
initialState,
|
281
|
+
persist: _persist,
|
282
|
+
startId
|
283
|
+
});
|
284
|
+
useDestroyFlow({ flowName, one, persist: _persist });
|
285
|
+
useInitDevtools({ devtoolState, setWsDevtools, flowName });
|
286
|
+
useClientInitEvent({ flowName, isInitialized, wsDevtools });
|
287
|
+
useSetHistoryEvent({ flowName, isInitialized, wsDevtools });
|
288
|
+
useSetCurrentEvent({ current, flowName, isInitialized, wsDevtools });
|
289
|
+
useFlowerNavigateEvent({
|
228
290
|
current,
|
291
|
+
flowName,
|
229
292
|
isDisabled,
|
230
|
-
|
231
|
-
wsDevtools
|
232
|
-
|
233
|
-
]);
|
293
|
+
isInitialized,
|
294
|
+
wsDevtools
|
295
|
+
});
|
234
296
|
const currentNodeId = prevFlowerNodeId || current;
|
235
297
|
const contextValues = useMemo(() => ({
|
236
298
|
name: flowName,
|
@@ -244,10 +306,10 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
|
|
244
306
|
React.createElement(FlowerReactProvider, { value: prevContextValues }, nodeById[currentNodeId]),
|
245
307
|
React.createElement(FlowerReactProvider, { value: contextValues }, !isDisabled && current !== currentNodeId && nodeById[current]))) : null;
|
246
308
|
};
|
247
|
-
const component$
|
248
|
-
component$
|
309
|
+
const component$8 = memo(FlowerClient);
|
310
|
+
component$8.displayName = 'Flower';
|
249
311
|
// workaround for let typescript read JSX component as a valid JSX element using react 19(?)
|
250
|
-
const Flower = component$
|
312
|
+
const Flower = component$8;
|
251
313
|
|
252
314
|
const FlowAction = ({ children, onEnter, onExit }) => {
|
253
315
|
useEffect(() => {
|
@@ -258,13 +320,12 @@ const FlowAction = ({ children, onEnter, onExit }) => {
|
|
258
320
|
}, [onEnter, onExit]);
|
259
321
|
return React.createElement(React.Fragment, null, children);
|
260
322
|
};
|
261
|
-
const component$
|
262
|
-
component$
|
263
|
-
const FlowerAction = component$
|
323
|
+
const component$7 = React.memo(FlowAction);
|
324
|
+
component$7.displayName = 'FlowerAction';
|
325
|
+
const FlowerAction = component$7;
|
264
326
|
|
265
327
|
const _FlowerComponent = ({ children }) => children;
|
266
|
-
|
267
|
-
const FlowerComponent = component$7;
|
328
|
+
memo(_FlowerComponent);
|
268
329
|
|
269
330
|
const _FlowerFlow = ({ children, onEnter, onExit }) => {
|
270
331
|
useEffect(() => {
|
@@ -310,6 +371,43 @@ const makeActionPayloadOnReset = makeActionPayload(ACTION_TYPES.reset, PAYLOAD_K
|
|
310
371
|
const makeActionPayloadOnNode = makeActionPayload(ACTION_TYPES.jump, PAYLOAD_KEYS_NEEDED.jump);
|
311
372
|
const makeActionPayloadOnNext = makeActionPayload(ACTION_TYPES.next, PAYLOAD_KEYS_NEEDED.next);
|
312
373
|
const makeActionPayloadOnRestart = makeActionPayload(ACTION_TYPES.restart, PAYLOAD_KEYS_NEEDED.restart);
|
374
|
+
const handleHistoryStackChange = (currentIndex, currentNode, flowName, withUrl) => {
|
375
|
+
const historyNode = `/${flowName}/${currentNode.nodeId}`;
|
376
|
+
if (currentNode.nodeType === 'FlowerAction')
|
377
|
+
return currentIndex;
|
378
|
+
const nextIndex = currentIndex + 1;
|
379
|
+
if (history.state?.index !== nextIndex) {
|
380
|
+
window.history.pushState({
|
381
|
+
index: nextIndex,
|
382
|
+
stack: [...(window.history.state.stack ?? []), historyNode]
|
383
|
+
}, '', withUrl ? historyNode : '');
|
384
|
+
}
|
385
|
+
return nextIndex;
|
386
|
+
};
|
387
|
+
|
388
|
+
const useHistorySync = ({ backAction, nextAction }) => {
|
389
|
+
const { dispatch } = ReduxFlowerProvider.getReduxHooks();
|
390
|
+
const { index, isActive, setIndex, withUrl } = useHistoryContext();
|
391
|
+
useEffect(() => {
|
392
|
+
if (!isActive)
|
393
|
+
return;
|
394
|
+
const initialIndex = window.history.state?.index ?? 0;
|
395
|
+
setIndex(initialIndex);
|
396
|
+
window.history.replaceState({ index: initialIndex, stack: [...(window.history.state?.stack ?? [])] }, withUrl ? '/' : '', '');
|
397
|
+
const onPopState = (event) => {
|
398
|
+
const newIndex = window.history.state?.index ?? 0;
|
399
|
+
if (newIndex > index) {
|
400
|
+
nextAction();
|
401
|
+
}
|
402
|
+
if (newIndex < index) {
|
403
|
+
backAction();
|
404
|
+
}
|
405
|
+
setIndex(newIndex);
|
406
|
+
};
|
407
|
+
window.addEventListener('popstate', onPopState);
|
408
|
+
return () => window.removeEventListener('popstate', onPopState);
|
409
|
+
}, [dispatch, backAction, nextAction]);
|
410
|
+
};
|
313
411
|
|
314
412
|
/** This hook allows you to read flow informations, such as the flowName and ID of the current node.
|
315
413
|
*
|
@@ -331,10 +429,18 @@ const makeActionPayloadOnRestart = makeActionPayload(ACTION_TYPES.restart, PAYLO
|
|
331
429
|
*/
|
332
430
|
const useFlower = ({ flowName: customFlowName, name } = {}) => {
|
333
431
|
const { name: flowNameDefault, initialData } = useContext(FlowerReactContext);
|
432
|
+
const { index, isActive, setIndex, withUrl } = useHistoryContext();
|
334
433
|
const { store, dispatch, useSelector } = ReduxFlowerProvider.getReduxHooks();
|
335
434
|
const flowName = (customFlowName || name || flowNameDefault);
|
336
435
|
const nodeId = useSelector(makeSelectCurrentNodeId(flowName ?? ''));
|
436
|
+
const currentNode = useSelector(makeSelectIsCurrentNode(flowName ?? ''));
|
337
437
|
const startId = useSelector(makeSelectStartNodeId(flowName ?? ''));
|
438
|
+
useEffect(() => {
|
439
|
+
if (currentNode.nodeType === 'FlowerAction')
|
440
|
+
return;
|
441
|
+
window.history.replaceState({ ...window.history.state }, '', withUrl ? `/${flowName}/${nodeId}` : '');
|
442
|
+
}, [nodeId, flowName, withUrl, currentNode]);
|
443
|
+
const stack = useMemo(() => window.history.state?.stack?.map((path) => path.split('/')[1]), [window.history.state?.stack]);
|
338
444
|
const emitNavigateEvent = useCallback(
|
339
445
|
//TODO check this function is needed
|
340
446
|
(params) => {
|
@@ -356,18 +462,37 @@ const useFlower = ({ flowName: customFlowName, name } = {}) => {
|
|
356
462
|
const { type, payload } = makeActionPayloadOnNext(flowName, params);
|
357
463
|
dispatch({
|
358
464
|
type: `${REDUCER_NAME.FLOWER_FLOW}/${type}`,
|
359
|
-
payload: {
|
360
|
-
...payload,
|
361
|
-
data: store.getState()
|
362
|
-
}
|
465
|
+
payload: { ...payload, data: store.getState() }
|
363
466
|
});
|
467
|
+
if (isActive) {
|
468
|
+
setIndex(handleHistoryStackChange(index, currentNode, flowName, withUrl));
|
469
|
+
}
|
364
470
|
emitNavigateEvent({ type, payload });
|
365
471
|
}, [dispatch, emitNavigateEvent, flowName, store]);
|
472
|
+
/**
|
473
|
+
* By doing this, we have a full control over flower navigation from both our buttons and browser back and forward navigation
|
474
|
+
* In order to trigger back correctly, trigger a real history back
|
475
|
+
* If you use replaceState({ index: 2 }) while at index 3,
|
476
|
+
* you visually move to step 2, but the browser still sees you at step 3.
|
477
|
+
* As a result:
|
478
|
+
* - The browser's Forward button stays disabled
|
479
|
+
* - No popstate event is triggered
|
480
|
+
* - The history flow is broken
|
481
|
+
* Use history.back() instead to preserve proper browser navigation.
|
482
|
+
*/
|
483
|
+
const interceptBack = () => {
|
484
|
+
window.history.back();
|
485
|
+
};
|
366
486
|
const back = useCallback((param) => {
|
367
|
-
const { type, payload } = makeActionPayloadOnBack(flowName, param);
|
368
|
-
dispatch({
|
487
|
+
const { type, payload } = makeActionPayloadOnBack(isActive ? (stack?.[stack?.length - 1] ?? flowName) : flowName, param);
|
488
|
+
dispatch({
|
489
|
+
type: `${REDUCER_NAME.FLOWER_FLOW}/${type}`,
|
490
|
+
payload
|
491
|
+
});
|
492
|
+
setIndex(index - 1);
|
369
493
|
emitNavigateEvent({ type, payload });
|
370
|
-
}, [dispatch, emitNavigateEvent, flowName]);
|
494
|
+
}, [dispatch, emitNavigateEvent, flowName, stack]);
|
495
|
+
useHistorySync({ backAction: back, nextAction: next });
|
371
496
|
const restart = useCallback((param) => {
|
372
497
|
const { type, payload } = makeActionPayloadOnRestart(flowName, param);
|
373
498
|
dispatch({ type: `${REDUCER_NAME.FLOWER_FLOW}/${type}`, payload });
|
@@ -376,10 +501,7 @@ const useFlower = ({ flowName: customFlowName, name } = {}) => {
|
|
376
501
|
const reset = useCallback((param) => {
|
377
502
|
const { type, payload } = makeActionPayloadOnReset(flowName, typeof param === 'string'
|
378
503
|
? { node: param, initialData }
|
379
|
-
: {
|
380
|
-
...param,
|
381
|
-
initialData
|
382
|
-
});
|
504
|
+
: { ...param, initialData });
|
383
505
|
dispatch({ type: `${REDUCER_NAME.FLOWER_FLOW}/${type}`, payload });
|
384
506
|
emitNavigateEvent({ type, payload });
|
385
507
|
}, [dispatch, emitNavigateEvent, flowName, initialData]);
|
@@ -402,7 +524,7 @@ const useFlower = ({ flowName: customFlowName, name } = {}) => {
|
|
402
524
|
startId,
|
403
525
|
next,
|
404
526
|
jump,
|
405
|
-
back,
|
527
|
+
back: isActive ? interceptBack : back,
|
406
528
|
reset,
|
407
529
|
restart
|
408
530
|
};
|
@@ -642,4 +764,73 @@ const createSliceWithFlower = (createSliceOptions) => {
|
|
642
764
|
return slice;
|
643
765
|
};
|
644
766
|
|
645
|
-
|
767
|
+
const AbacContext = createContext({
|
768
|
+
can: () => false,
|
769
|
+
allowedActions: () => [],
|
770
|
+
loaded: false,
|
771
|
+
rawRules: null
|
772
|
+
});
|
773
|
+
const AbacProvider = ({ children, rules, rulesPath, fetchOptions, denyByDefault = true }) => {
|
774
|
+
const [loaded, setLoaded] = useState(isAbacInitialized());
|
775
|
+
const [localRawRules, setLocalRawRules] = useState(getRawRules());
|
776
|
+
useEffect(() => {
|
777
|
+
if (rules) {
|
778
|
+
initAbac(rules);
|
779
|
+
setLocalRawRules(rules);
|
780
|
+
setLoaded(true);
|
781
|
+
return;
|
782
|
+
}
|
783
|
+
if (!rulesPath) {
|
784
|
+
setLoaded(false);
|
785
|
+
setLocalRawRules(null);
|
786
|
+
return;
|
787
|
+
}
|
788
|
+
let mounted = true;
|
789
|
+
const init = async () => {
|
790
|
+
try {
|
791
|
+
const res = await fetch(rulesPath, fetchOptions);
|
792
|
+
if (!res.ok)
|
793
|
+
throw new Error(`HTTP ${res.status}`);
|
794
|
+
const json = (await res.json());
|
795
|
+
if (!mounted)
|
796
|
+
return;
|
797
|
+
initAbac(json);
|
798
|
+
setLocalRawRules(json);
|
799
|
+
setLoaded(true);
|
800
|
+
}
|
801
|
+
catch (err) {
|
802
|
+
console.error('ABAC: failed to load rules from', rulesPath, err);
|
803
|
+
setLocalRawRules(null);
|
804
|
+
setLoaded(false);
|
805
|
+
}
|
806
|
+
};
|
807
|
+
init();
|
808
|
+
return () => {
|
809
|
+
mounted = false;
|
810
|
+
};
|
811
|
+
}, [rules, rulesPath, fetchOptions]);
|
812
|
+
const can = useCallback((ctx) => {
|
813
|
+
if (!isAbacInitialized())
|
814
|
+
return !denyByDefault;
|
815
|
+
const subject = getSubject();
|
816
|
+
return (getAbacEngine()?.decide({ subject: subject ?? {}, ...ctx }) === 'Permit');
|
817
|
+
}, []);
|
818
|
+
const allowedActions = useCallback((resource, actions = ['read', 'create', 'update', 'delete']) => {
|
819
|
+
if (!isAbacInitialized())
|
820
|
+
return denyByDefault ? [] : actions.slice();
|
821
|
+
const subject = getSubject();
|
822
|
+
return getAbacEngine()?.allowedActions({ subject: subject ?? {}, resource, environment: undefined }, actions);
|
823
|
+
}, []);
|
824
|
+
const value = useMemo(() => ({
|
825
|
+
can,
|
826
|
+
allowedActions,
|
827
|
+
loaded,
|
828
|
+
rawRules: localRawRules
|
829
|
+
}), [loaded, localRawRules]);
|
830
|
+
return React.createElement(AbacContext.Provider, { value: value }, children);
|
831
|
+
};
|
832
|
+
function useAbac() {
|
833
|
+
return useContext(AbacContext);
|
834
|
+
}
|
835
|
+
|
836
|
+
export { AbacProvider, Flower, FlowerAction, FlowerFlow, FlowerNavigate, FlowerNode, FlowerProvider, FlowerRoute, FlowerServer, FlowerStart, createSliceWithFlower, createStoreWithFlower, flowerReducers, makeSelectData, reducerFlower, useAbac, useFlower, useFlowerNavigate };
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { RuleInput, AbacCtx, Resource, Action } from '@flowerforce/flower-core';
|
3
|
+
type AbacProviderProps = {
|
4
|
+
children: React.ReactNode;
|
5
|
+
rules?: RuleInput[];
|
6
|
+
rulesPath?: string;
|
7
|
+
fetchOptions?: RequestInit;
|
8
|
+
denyByDefault?: boolean;
|
9
|
+
};
|
10
|
+
type AbacContextValue = {
|
11
|
+
can: (args: Omit<AbacCtx, 'subject'>) => boolean;
|
12
|
+
allowedActions: (resource: Resource | undefined, actions?: Action[]) => Action[] | undefined;
|
13
|
+
loaded: boolean;
|
14
|
+
rawRules?: RuleInput[] | null;
|
15
|
+
};
|
16
|
+
export declare const AbacProvider: React.FC<AbacProviderProps>;
|
17
|
+
export declare function useAbac(): AbacContextValue;
|
18
|
+
export {};
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './abacProvider';
|
@@ -1,15 +1,3 @@
|
|
1
|
-
import React
|
2
|
-
|
3
|
-
|
4
|
-
current?: string;
|
5
|
-
history?: string[];
|
6
|
-
};
|
7
|
-
type FlowerClientProps = PropsWithChildren<{
|
8
|
-
name: string;
|
9
|
-
destroyOnUnmount?: boolean;
|
10
|
-
startId?: string | null;
|
11
|
-
initialState?: FlowerInitialState;
|
12
|
-
initialData?: Record<string, unknown>;
|
13
|
-
}>;
|
14
|
-
export declare const Flower: ({ children, name, destroyOnUnmount, startId, initialState, initialData }: FlowerClientProps) => React.JSX.Element | null;
|
15
|
-
export {};
|
1
|
+
import React from 'react';
|
2
|
+
import { FlowerClientProps } from '../types/Flower';
|
3
|
+
export declare const Flower: ({ children, name, destroyOnUnmount, persist, startId, initialState, initialData }: FlowerClientProps) => React.JSX.Element | null;
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { UseClientInitEventProps, UseFlowerNavigateEventProps, UseSetCurrentEventProps, UseSetHistoryEventProps } from './types';
|
2
|
+
export declare const useClientInitEvent: ({ flowName, isInitialized, wsDevtools }: UseClientInitEventProps) => void;
|
3
|
+
export declare const useSetHistoryEvent: ({ flowName, isInitialized, wsDevtools }: UseSetHistoryEventProps) => void;
|
4
|
+
export declare const useSetCurrentEvent: ({ current, flowName, isInitialized, wsDevtools }: UseSetCurrentEventProps) => void;
|
5
|
+
export declare const useFlowerNavigateEvent: ({ current, flowName, isInitialized, wsDevtools, isDisabled }: UseFlowerNavigateEventProps) => void;
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { NodeConfig } from '@flowerforce/flower-core';
|
2
|
+
import { FlowerClientProps } from '../../../types/Flower';
|
3
|
+
import { MutableRefObject } from 'react';
|
4
|
+
export type UseClientInitEventProps = {
|
5
|
+
isInitialized: string;
|
6
|
+
wsDevtools: boolean;
|
7
|
+
flowName: string;
|
8
|
+
};
|
9
|
+
export type UseSetHistoryEventProps = UseClientInitEventProps;
|
10
|
+
export type UseSetCurrentEventProps = UseClientInitEventProps & {
|
11
|
+
current: string;
|
12
|
+
};
|
13
|
+
export type UseFlowerNavigateEventProps = UseSetCurrentEventProps & {
|
14
|
+
isDisabled: boolean;
|
15
|
+
};
|
16
|
+
export type UseDestroyFlowProps = {
|
17
|
+
one: MutableRefObject<boolean>;
|
18
|
+
persist: boolean;
|
19
|
+
flowName: string;
|
20
|
+
};
|
21
|
+
export type UseInitDevtoolsProps = {
|
22
|
+
devtoolState: Object;
|
23
|
+
flowName: string;
|
24
|
+
setWsDevtools: (value: boolean) => void;
|
25
|
+
};
|
26
|
+
export type UseInitNodesProps = {
|
27
|
+
one: MutableRefObject<boolean>;
|
28
|
+
nodes: NodeConfig[];
|
29
|
+
} & Omit<FlowerClientProps, 'destroyOnUnmount'>;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { useFlowerActions } from '../../types';
|
2
|
+
/**
|
3
|
+
* Hook centralizzato per sincronizzare la navigazione browser con Redux
|
4
|
+
* @param {function} backAction - Azione Redux da chiamare quando si fa "indietro"
|
5
|
+
* @param {function} nextAction - Azione Redux da chiamare quando si fa "avanti"
|
6
|
+
*/
|
7
|
+
type UseHistorySyncProps = {
|
8
|
+
backAction: () => ReturnType<useFlowerActions['back']>;
|
9
|
+
nextAction: () => ReturnType<useFlowerActions['next']>;
|
10
|
+
};
|
11
|
+
export declare const useHistorySync: ({ backAction, nextAction }: UseHistorySyncProps) => void;
|
12
|
+
export {};
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { INode } from '@flowerforce/flower-core';
|
1
2
|
export declare const makeActionPayloadOnBack: (flowName: string | undefined, params: any) => {
|
2
3
|
type: string;
|
3
4
|
payload: Record<string, any>;
|
@@ -18,3 +19,4 @@ export declare const makeActionPayloadOnRestart: (flowName: string | undefined,
|
|
18
19
|
type: string;
|
19
20
|
payload: Record<string, any>;
|
20
21
|
};
|
22
|
+
export declare const handleHistoryStackChange: (currentIndex: number, currentNode: INode, flowName: string, withUrl?: boolean) => number;
|