@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 CHANGED
@@ -1,3 +1,51 @@
1
+ ## 3.7.2 (2025-04-23)
2
+
3
+
4
+ ### 🩹 Fixes
5
+
6
+ - package lock ([a3bb210](https://github.com/flowerforce/flower/commit/a3bb210))
7
+
8
+
9
+ ### 🧱 Updated Dependencies
10
+
11
+ - Updated flower-core to 3.5.2
12
+
13
+ ## 3.7.1 (2025-04-19)
14
+
15
+
16
+ ### 🧱 Updated Dependencies
17
+
18
+ - Updated flower-core to 3.5.1
19
+
20
+ ## 3.7.0 (2025-04-19)
21
+
22
+
23
+ ### 🚀 Features
24
+
25
+ - remove data blank from init nodes ([#80](https://github.com/flowerforce/flower/pull/80))
26
+
27
+
28
+ ### 🧱 Updated Dependencies
29
+
30
+ - Updated flower-core to 3.5.0
31
+
32
+ ## 3.6.0 (2025-04-19)
33
+
34
+
35
+ ### 🚀 Features
36
+
37
+ - added remove value on hide element ([#68](https://github.com/flowerforce/flower/pull/68))
38
+
39
+
40
+ ### 🩹 Fixes
41
+
42
+ - avoid validate hidden field ([#67](https://github.com/flowerforce/flower/pull/67))
43
+
44
+
45
+ ### 🧱 Updated Dependencies
46
+
47
+ - Updated flower-core to 3.4.0
48
+
1
49
  ## 3.5.0 (2024-10-08)
2
50
 
3
51
 
package/dist/index.cjs.js CHANGED
@@ -5,17 +5,42 @@ var _keyBy = require('lodash/keyBy');
5
5
  var flowerCore = require('@flowerforce/flower-core');
6
6
  var flowerReactContext = require('@flowerforce/flower-react-context');
7
7
  var _get = require('lodash/get');
8
- var toolkit = require('@reduxjs/toolkit');
9
8
  var flowerReactStore = require('@flowerforce/flower-react-store');
9
+ var toolkit = require('@reduxjs/toolkit');
10
10
  var reselect = require('reselect');
11
+ var flowerReactHistoryContext = require('@flowerforce/flower-react-history-context');
11
12
  var flowerReactShared = require('@flowerforce/flower-react-shared');
12
13
  var set = require('lodash/set');
13
14
 
14
- // eslint-disable-next-line import/prefer-default-export
15
- const convertElements = (nodes) => {
16
- const res = flowerCore.CoreUtils.generateNodesForFlowerJson(nodes);
17
- return res;
15
+ const getRulesExists = (rules) => {
16
+ return Object.keys(rules).length ? flowerCore.FlowUtils.mapEdge(rules) : undefined;
18
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 = flowerCore.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
+ });
19
44
 
20
45
  const flowerReducer = toolkit.createSlice({
21
46
  name: flowerCore.REDUCER_NAME.FLOWER_FLOW,
@@ -42,60 +67,37 @@ const selectFlowerHistory = (name) => reselect.createSelector(selectFlower(name)
42
67
  const makeSelectNodesIds = (name) => reselect.createSelector(selectFlower(name), flowerCore.FlowerCoreStateSelectors.makeSelectNodesIds);
43
68
  const makeSelectStartNodeId = (name) => reselect.createSelector(selectFlower(name), flowerCore.FlowerCoreStateSelectors.makeSelectStartNodeId);
44
69
  const makeSelectCurrentNodeId = (name) => reselect.createSelector(selectFlower(name), makeSelectStartNodeId(name), flowerCore.FlowerCoreStateSelectors.makeSelectCurrentNodeId);
70
+ const makeSelectIsCurrentNode = (name) => reselect.createSelector(selectFlower(name), makeSelectCurrentNodeId(name), (flower, current) => flower.nodes[current]);
45
71
  const makeSelectPrevNodeRetain = (name) => reselect.createSelector(makeSelectNodesIds(name), selectFlowerHistory(name), makeSelectCurrentNodeId(name), flowerCore.FlowerCoreStateSelectors.makeSelectPrevNodeRetain);
46
72
  const makeSelectCurrentNodeDisabled = (name) => reselect.createSelector(makeSelectNodesIds(name), makeSelectCurrentNodeId(name), flowerCore.FlowerCoreStateSelectors.makeSelectCurrentNodeDisabled);
47
73
  // dati nel flow selezionato
48
74
  const makeSelectData = (name) => reselect.createSelector(selectFlowerDataNode(name), (data) => data?.data ?? {});
49
75
  reselect.createSelector(selectGlobalData, flowerCore.FlowerStateUtils.getAllData);
50
76
 
51
- /* eslint-disable no-undef */
52
- /* eslint-disable no-underscore-dangle */
53
- /*
54
- * FlowerClient
55
- */
56
- const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null, initialState = {}, initialData }) => {
57
- const flowName = name;
58
- const { dispatch, store, useSelector } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
59
- const one = React.useRef(false);
60
- const [wsDevtools, setWsDevtools] = React.useState(flowerCore.devtoolState && _get(flowerCore.devtoolState, '__FLOWER_DEVTOOLS_INITIALIZED__', false));
61
- // TODO could make that transformation in CoreUtils.generateNodesForFlowerJson
62
- // eslint-disable-next-line react-hooks/exhaustive-deps, max-len
63
- const nodes = React.useMemo(
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- () => convertElements(React.Children.toArray(children)), [children]);
66
- const nodeById = React.useMemo(() => _keyBy(React.Children.toArray(children), 'props.id'), [children]);
67
- const isInitialized = useSelector(makeSelectStartNodeId(name));
68
- const history = useSelector(selectFlowerHistory(name));
69
- const current = useSelector(makeSelectCurrentNodeId(flowName));
70
- const isDisabled = useSelector(makeSelectCurrentNodeDisabled(flowName));
71
- const prevFlowerNodeId = useSelector(makeSelectPrevNodeRetain(flowName));
77
+ const useInitNodes = ({ one, nodes, name, startId, persist = false, initialData, initialState = {} }) => {
78
+ const { dispatch } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
72
79
  React.useEffect(() => {
73
80
  if (nodes.length > 0 && one.current === false) {
74
81
  one.current = true;
75
82
  dispatch(flowerActions.initNodes({
76
- name: flowName,
77
- // @ts-expect-error FIX ME
83
+ name,
78
84
  nodes,
79
85
  startId: startId ?? '',
80
- persist: destroyOnUnmount === false,
86
+ persist,
81
87
  initialState
82
88
  }));
83
- if (initialData) {
84
- dispatch(flowerReactStore.flowerDataActions.initData({
85
- rootName: flowName,
86
- initialData: initialData ?? {}
87
- }));
88
- }
89
89
  }
90
- }, [
91
- dispatch,
92
- flowName,
93
- nodes,
94
- startId,
95
- destroyOnUnmount,
96
- initialState,
97
- initialData
98
- ]);
90
+ if (initialData) {
91
+ dispatch(flowerReactStore.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 } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
99
101
  React.useEffect(() => {
100
102
  /* istanbul ignore next */
101
103
  const eventCb = (msg) => {
@@ -109,32 +111,39 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
109
111
  dispatch(flowerActions.setCurrentNode({ name: msg.name, node: msg.id }));
110
112
  }
111
113
  // if (msg.action === 'REPLACE_DATA' && msg.name === flowName) {
112
- // dispatch(
113
- // formActions.replaceData({ flowName: msg.name, value: msg.data })
114
- // )
114
+ // dispatch(actions.replaceData({ flowName: msg.name, value: msg.data }))
115
115
  // }
116
116
  // if (msg.action === 'ADD_DATA' && msg.name === flowName) {
117
- // dispatch(formActions.addData({ flowName: msg.name, value: msg.data }))
117
+ // dispatch(actions.addData({ flowName: msg.name, value: msg.data }))
118
118
  // }
119
119
  };
120
120
  /* istanbul ignore next */
121
- if (flowerCore.devtoolState && _get(flowerCore.devtoolState, '__FLOWER_DEVTOOLS__')) {
121
+ if (devtoolState && _get(devtoolState, '__FLOWER_DEVTOOLS__')) {
122
122
  flowerCore.Emitter.on('flower-devtool-to-client', eventCb);
123
123
  }
124
124
  return () => {
125
125
  /* istanbul ignore next */
126
- if (flowerCore.devtoolState && _get(flowerCore.devtoolState, '__FLOWER_DEVTOOLS__')) {
126
+ if (devtoolState && _get(devtoolState, '__FLOWER_DEVTOOLS__')) {
127
127
  flowerCore.Emitter.off('flower-devtool-to-client', eventCb);
128
128
  }
129
129
  };
130
130
  }, [dispatch, flowName]);
131
+ };
132
+
133
+ const useDestroyFlow = ({ flowName, one, persist }) => {
134
+ const { dispatch } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
131
135
  React.useEffect(() => () => {
132
- // unmount function
133
- if (destroyOnUnmount && one.current === true) {
136
+ if (persist)
137
+ return;
138
+ if (one.current === true) {
134
139
  one.current = false;
135
140
  dispatch(flowerActions.destroy({ name: flowName }));
136
141
  }
137
- }, [dispatch, flowName, destroyOnUnmount]);
142
+ }, [dispatch, flowName, persist]);
143
+ };
144
+
145
+ const useClientInitEvent = ({ flowName, isInitialized, wsDevtools }) => {
146
+ const { store } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
138
147
  React.useEffect(() => {
139
148
  /* istanbul ignore next */
140
149
  if (isInitialized &&
@@ -150,7 +159,11 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
150
159
  getState: store.getState
151
160
  });
152
161
  }
153
- }, [dispatch, flowName, wsDevtools, isInitialized, store]);
162
+ }, [flowName, wsDevtools, isInitialized, store]);
163
+ };
164
+ const useSetHistoryEvent = ({ flowName, isInitialized, wsDevtools }) => {
165
+ const { useSelector } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
166
+ const history = useSelector(selectFlowerHistory(flowName));
154
167
  React.useEffect(() => {
155
168
  /* istanbul ignore next */
156
169
  if (isInitialized &&
@@ -164,7 +177,9 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
164
177
  history
165
178
  });
166
179
  }
167
- }, [dispatch, flowName, history, wsDevtools, isInitialized]);
180
+ }, [flowName, history, wsDevtools, isInitialized]);
181
+ };
182
+ const useSetCurrentEvent = ({ current, flowName, isInitialized, wsDevtools }) => {
168
183
  React.useEffect(() => {
169
184
  if (!current)
170
185
  return;
@@ -184,6 +199,9 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
184
199
  });
185
200
  }
186
201
  }, [flowName, current, wsDevtools, isInitialized]);
202
+ };
203
+ const useFlowerNavigateEvent = ({ current, flowName, isInitialized, wsDevtools, isDisabled }) => {
204
+ const { dispatch } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
187
205
  React.useEffect(() => {
188
206
  if (!current)
189
207
  return;
@@ -191,7 +209,10 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
191
209
  if (!isInitialized)
192
210
  return;
193
211
  if (isDisabled) {
194
- dispatch(flowerActions.next({ flowName }));
212
+ dispatch({
213
+ type: `${flowerCore.REDUCER_NAME.FLOWER_FLOW}/next`,
214
+ payload: { flowName, disabled: true }
215
+ });
195
216
  // eslint-disable-next-line no-underscore-dangle, no-undef
196
217
  /* istanbul ignore next */
197
218
  if (wsDevtools &&
@@ -214,7 +235,7 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
214
235
  flowerCore.devtoolState &&
215
236
  _get(flowerCore.devtoolState, '__FLOWER_DEVTOOLS__')) {
216
237
  if (isInitialized === current)
217
- return; // salto il primo event
238
+ return; // salto il primo evento
218
239
  flowerCore.Emitter.emit('flower-devtool-from-client', {
219
240
  source: 'flower-client',
220
241
  action: 'SET_SELECTED',
@@ -223,15 +244,55 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
223
244
  time: new Date()
224
245
  });
225
246
  }
226
- }, [
227
- dispatch,
228
- flowName,
247
+ }, [dispatch, flowName, current, isDisabled, wsDevtools, isInitialized]);
248
+ };
249
+
250
+ const useSelectorsClient = (flowName) => {
251
+ const { useSelector } = flowerReactStore.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 = React.useRef(false);
271
+ const [wsDevtools, setWsDevtools] = React.useState(flowerCore.devtoolState && _get(flowerCore.devtoolState, '__FLOWER_DEVTOOLS_INITIALIZED__', false));
272
+ const nodes = React.useMemo(() => generateNodesForFlowerJson(React.Children.toArray(children)), [children]);
273
+ const nodeById = React.useMemo(() => _keyBy(React.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: flowerCore.devtoolState, setWsDevtools, flowName });
286
+ useClientInitEvent({ flowName, isInitialized, wsDevtools });
287
+ useSetHistoryEvent({ flowName, isInitialized, wsDevtools });
288
+ useSetCurrentEvent({ current, flowName, isInitialized, wsDevtools });
289
+ useFlowerNavigateEvent({
229
290
  current,
291
+ flowName,
230
292
  isDisabled,
231
- store,
232
- wsDevtools,
233
- isInitialized
234
- ]);
293
+ isInitialized,
294
+ wsDevtools
295
+ });
235
296
  const currentNodeId = prevFlowerNodeId || current;
236
297
  const contextValues = React.useMemo(() => ({
237
298
  name: flowName,
@@ -245,10 +306,10 @@ const FlowerClient = ({ children, name, destroyOnUnmount = true, startId = null,
245
306
  React.createElement(flowerReactContext.FlowerReactProvider, { value: prevContextValues }, nodeById[currentNodeId]),
246
307
  React.createElement(flowerReactContext.FlowerReactProvider, { value: contextValues }, !isDisabled && current !== currentNodeId && nodeById[current]))) : null;
247
308
  };
248
- const component$9 = React.memo(FlowerClient);
249
- component$9.displayName = 'Flower';
309
+ const component$8 = React.memo(FlowerClient);
310
+ component$8.displayName = 'Flower';
250
311
  // workaround for let typescript read JSX component as a valid JSX element using react 19(?)
251
- const Flower = component$9;
312
+ const Flower = component$8;
252
313
 
253
314
  const FlowAction = ({ children, onEnter, onExit }) => {
254
315
  React.useEffect(() => {
@@ -259,13 +320,12 @@ const FlowAction = ({ children, onEnter, onExit }) => {
259
320
  }, [onEnter, onExit]);
260
321
  return React.createElement(React.Fragment, null, children);
261
322
  };
262
- const component$8 = React.memo(FlowAction);
263
- component$8.displayName = 'FlowerAction';
264
- const FlowerAction = component$8;
323
+ const component$7 = React.memo(FlowAction);
324
+ component$7.displayName = 'FlowerAction';
325
+ const FlowerAction = component$7;
265
326
 
266
327
  const _FlowerComponent = ({ children }) => children;
267
- const component$7 = React.memo(_FlowerComponent);
268
- const FlowerComponent = component$7;
328
+ React.memo(_FlowerComponent);
269
329
 
270
330
  const _FlowerFlow = ({ children, onEnter, onExit }) => {
271
331
  React.useEffect(() => {
@@ -311,6 +371,43 @@ const makeActionPayloadOnReset = makeActionPayload(ACTION_TYPES.reset, PAYLOAD_K
311
371
  const makeActionPayloadOnNode = makeActionPayload(ACTION_TYPES.jump, PAYLOAD_KEYS_NEEDED.jump);
312
372
  const makeActionPayloadOnNext = makeActionPayload(ACTION_TYPES.next, PAYLOAD_KEYS_NEEDED.next);
313
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 } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
390
+ const { index, isActive, setIndex, withUrl } = flowerReactHistoryContext.useHistoryContext();
391
+ React.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
+ };
314
411
 
315
412
  /** This hook allows you to read flow informations, such as the flowName and ID of the current node.
316
413
  *
@@ -332,10 +429,18 @@ const makeActionPayloadOnRestart = makeActionPayload(ACTION_TYPES.restart, PAYLO
332
429
  */
333
430
  const useFlower = ({ flowName: customFlowName, name } = {}) => {
334
431
  const { name: flowNameDefault, initialData } = React.useContext(flowerReactContext.FlowerReactContext);
432
+ const { index, isActive, setIndex, withUrl } = flowerReactHistoryContext.useHistoryContext();
335
433
  const { store, dispatch, useSelector } = flowerReactStore.ReduxFlowerProvider.getReduxHooks();
336
434
  const flowName = (customFlowName || name || flowNameDefault);
337
435
  const nodeId = useSelector(makeSelectCurrentNodeId(flowName ?? ''));
436
+ const currentNode = useSelector(makeSelectIsCurrentNode(flowName ?? ''));
338
437
  const startId = useSelector(makeSelectStartNodeId(flowName ?? ''));
438
+ React.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 = React.useMemo(() => window.history.state?.stack?.map((path) => path.split('/')[1]), [window.history.state?.stack]);
339
444
  const emitNavigateEvent = React.useCallback(
340
445
  //TODO check this function is needed
341
446
  (params) => {
@@ -357,18 +462,37 @@ const useFlower = ({ flowName: customFlowName, name } = {}) => {
357
462
  const { type, payload } = makeActionPayloadOnNext(flowName, params);
358
463
  dispatch({
359
464
  type: `${flowerCore.REDUCER_NAME.FLOWER_FLOW}/${type}`,
360
- payload: {
361
- ...payload,
362
- data: store.getState()
363
- }
465
+ payload: { ...payload, data: store.getState() }
364
466
  });
467
+ if (isActive) {
468
+ setIndex(handleHistoryStackChange(index, currentNode, flowName, withUrl));
469
+ }
365
470
  emitNavigateEvent({ type, payload });
366
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
+ };
367
486
  const back = React.useCallback((param) => {
368
- const { type, payload } = makeActionPayloadOnBack(flowName, param);
369
- dispatch({ type: `${flowerCore.REDUCER_NAME.FLOWER_FLOW}/${type}`, payload });
487
+ const { type, payload } = makeActionPayloadOnBack(isActive ? (stack?.[stack?.length - 1] ?? flowName) : flowName, param);
488
+ dispatch({
489
+ type: `${flowerCore.REDUCER_NAME.FLOWER_FLOW}/${type}`,
490
+ payload
491
+ });
492
+ setIndex(index - 1);
370
493
  emitNavigateEvent({ type, payload });
371
- }, [dispatch, emitNavigateEvent, flowName]);
494
+ }, [dispatch, emitNavigateEvent, flowName, stack]);
495
+ useHistorySync({ backAction: back, nextAction: next });
372
496
  const restart = React.useCallback((param) => {
373
497
  const { type, payload } = makeActionPayloadOnRestart(flowName, param);
374
498
  dispatch({ type: `${flowerCore.REDUCER_NAME.FLOWER_FLOW}/${type}`, payload });
@@ -377,10 +501,7 @@ const useFlower = ({ flowName: customFlowName, name } = {}) => {
377
501
  const reset = React.useCallback((param) => {
378
502
  const { type, payload } = makeActionPayloadOnReset(flowName, typeof param === 'string'
379
503
  ? { node: param, initialData }
380
- : {
381
- ...param,
382
- initialData
383
- });
504
+ : { ...param, initialData });
384
505
  dispatch({ type: `${flowerCore.REDUCER_NAME.FLOWER_FLOW}/${type}`, payload });
385
506
  emitNavigateEvent({ type, payload });
386
507
  }, [dispatch, emitNavigateEvent, flowName, initialData]);
@@ -403,7 +524,7 @@ const useFlower = ({ flowName: customFlowName, name } = {}) => {
403
524
  startId,
404
525
  next,
405
526
  jump,
406
- back,
527
+ back: isActive ? interceptBack : back,
407
528
  reset,
408
529
  restart
409
530
  };
@@ -643,13 +764,86 @@ const createSliceWithFlower = (createSliceOptions) => {
643
764
  return slice;
644
765
  };
645
766
 
767
+ const AbacContext = React.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] = React.useState(flowerCore.isAbacInitialized());
775
+ const [localRawRules, setLocalRawRules] = React.useState(flowerCore.getRawRules());
776
+ React.useEffect(() => {
777
+ if (rules) {
778
+ flowerCore.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
+ flowerCore.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 = React.useCallback((ctx) => {
813
+ if (!flowerCore.isAbacInitialized())
814
+ return !denyByDefault;
815
+ const subject = flowerCore.getSubject();
816
+ return (flowerCore.getAbacEngine()?.decide({ subject: subject ?? {}, ...ctx }) === 'Permit');
817
+ }, []);
818
+ const allowedActions = React.useCallback((resource, actions = ['read', 'create', 'update', 'delete']) => {
819
+ if (!flowerCore.isAbacInitialized())
820
+ return denyByDefault ? [] : actions.slice();
821
+ const subject = flowerCore.getSubject();
822
+ return flowerCore.getAbacEngine()?.allowedActions({ subject: subject ?? {}, resource, environment: undefined }, actions);
823
+ }, []);
824
+ const value = React.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 React.useContext(AbacContext);
834
+ }
835
+
646
836
  Object.defineProperty(exports, "createApi", {
647
837
  enumerable: true,
648
838
  get: function () { return flowerReactStore.createApi; }
649
839
  });
840
+ Object.defineProperty(exports, "HistoryContextProvider", {
841
+ enumerable: true,
842
+ get: function () { return flowerReactHistoryContext.HistoryContextProvider; }
843
+ });
844
+ exports.AbacProvider = AbacProvider;
650
845
  exports.Flower = Flower;
651
846
  exports.FlowerAction = FlowerAction;
652
- exports.FlowerComponent = FlowerComponent;
653
847
  exports.FlowerFlow = FlowerFlow;
654
848
  exports.FlowerNavigate = FlowerNavigate;
655
849
  exports.FlowerNode = FlowerNode;
@@ -662,5 +856,6 @@ exports.createStoreWithFlower = createStoreWithFlower;
662
856
  exports.flowerReducers = flowerReducers;
663
857
  exports.makeSelectData = makeSelectData;
664
858
  exports.reducerFlower = reducerFlower;
859
+ exports.useAbac = useAbac;
665
860
  exports.useFlower = useFlower;
666
861
  exports.useFlowerNavigate = useFlowerNavigate;