@arquimedes.co/eureka-forms 3.0.65 → 3.0.66

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.
@@ -2,6 +2,13 @@ import { EntityValueDataTypes } from '../../constants/FormStepTypes';
2
2
  const push = (step, idStep, into) => {
3
3
  if (!idStep)
4
4
  return;
5
+ /**
6
+ * A draft can point a filter or a path hop at the picker that holds it (the editor lists the
7
+ * step among the pickable ones before the form is saved and the backend rejects it). A step is
8
+ * never its own dependency, and watching itself would close a cycle in the dependency walk.
9
+ */
10
+ if (idStep === step.id)
11
+ return;
5
12
  if (step.dependencies?.includes(idStep))
6
13
  return;
7
14
  if (!into.includes(idStep))
@@ -61,6 +61,13 @@ describe('entityValuePickerWatchedSteps', () => {
61
61
  });
62
62
  expect(entityValuePickerWatchedSteps(step)).toEqual(['shared']);
63
63
  });
64
+ test('ignores a draft filter or path hop pointing back at the picker itself', () => {
65
+ const step = picker({
66
+ path: [stepPath('picker'), stepPath('hop')],
67
+ filters: [stepFilter('picker', true), stepFilter('picker', false), stepFilter('other', false)],
68
+ });
69
+ expect(entityValuePickerWatchedSteps(step)).toEqual(['hop', 'other']);
70
+ });
64
71
  });
65
72
  describe('entityValuePickerBlockingSteps', () => {
66
73
  test('only the path hops and the required filters block the fetch', () => {
@@ -78,4 +85,11 @@ describe('entityValuePickerBlockingSteps', () => {
78
85
  });
79
86
  expect(entityValuePickerBlockingSteps(step)).toEqual([]);
80
87
  });
88
+ test('ignores a draft filter or path hop pointing back at the picker itself', () => {
89
+ const step = picker({
90
+ path: [stepPath('picker')],
91
+ filters: [stepFilter('picker', true)],
92
+ });
93
+ expect(entityValuePickerBlockingSteps(step)).toEqual([]);
94
+ });
81
95
  });
@@ -67,6 +67,20 @@ export declare const selectStepDependencies: ((state: RootState, _step: GBaseSte
67
67
  memoize: typeof import("reselect").weakMapMemoize;
68
68
  argsMemoize: typeof import("reselect").weakMapMemoize;
69
69
  };
70
+ /**
71
+ * The steps `idStep` depends on, transitively: every dependency's own dependencies come before it,
72
+ * and every id is answered exactly once. `idStep` itself is never listed — a step is not a
73
+ * dependency of itself, even when a draft points it back at itself.
74
+ *
75
+ * A saved form is a DAG: the backend recomputes and validates `step.dependencies` on save. The
76
+ * editor's live preview renders unsaved drafts though, where a filter or a path hop can still point
77
+ * at the step that holds it, or close a cycle with another step. The walk must therefore terminate
78
+ * on any graph, cyclic included: a step is marked visited before it is walked and is never walked
79
+ * or listed twice, so every edge is followed at most once.
80
+ *
81
+ * Exported for the unit test; it is internal to this module and not part of the package's surface.
82
+ */
83
+ export declare const calcDeepDependencies: (idStep: string, form: Form) => string[];
70
84
  export interface UseFormStepOptions<ValueType> {
71
85
  defaultValue: ValueType;
72
86
  /** If true, will not trigger onChange until the user has finished typing */
@@ -66,24 +66,45 @@ export const selectStepDependencies = createSelector([
66
66
  }
67
67
  return { invalids, emptyDep };
68
68
  });
69
- const calcDeepDependencies = (idStep, form, deps = []) => {
70
- const step = form.steps[idStep];
71
- if (!step)
72
- return deps;
73
- /**
74
- * An entity picker also gives up (its options resolve to null) while a path hop or a required
75
- * STEP filter holds no value. Those are in `step.dependencies` once the form has been saved,
76
- * but not in an unsaved draft, so they are added here to read as a missing dependency instead
77
- * of as a failed load.
78
- */
79
- const idDeps = step.type === FormStepTypes.ENTITYVALUEPICKER
80
- ? [...(step.dependencies ?? []), ...entityValuePickerBlockingSteps(step)]
81
- : step.dependencies;
82
- for (const dep of idDeps ?? []) {
83
- if (!deps.includes(dep)) {
84
- deps.push(...calcDeepDependencies(dep, form, deps), dep);
69
+ /**
70
+ * The steps `idStep` depends on, transitively: every dependency's own dependencies come before it,
71
+ * and every id is answered exactly once. `idStep` itself is never listed — a step is not a
72
+ * dependency of itself, even when a draft points it back at itself.
73
+ *
74
+ * A saved form is a DAG: the backend recomputes and validates `step.dependencies` on save. The
75
+ * editor's live preview renders unsaved drafts though, where a filter or a path hop can still point
76
+ * at the step that holds it, or close a cycle with another step. The walk must therefore terminate
77
+ * on any graph, cyclic included: a step is marked visited before it is walked and is never walked
78
+ * or listed twice, so every edge is followed at most once.
79
+ *
80
+ * Exported for the unit test; it is internal to this module and not part of the package's surface.
81
+ */
82
+ export const calcDeepDependencies = (idStep, form) => {
83
+ const deps = [];
84
+ /** `idStep` starts out visited so a cycle back to it stops there and never lists it. */
85
+ const visited = new Set([idStep]);
86
+ const walk = (id) => {
87
+ const step = form.steps[id];
88
+ if (!step)
89
+ return;
90
+ /**
91
+ * An entity picker also gives up (its options resolve to null) while a path hop or a
92
+ * required STEP filter holds no value. Those are in `step.dependencies` once the form has
93
+ * been saved, but not in an unsaved draft, so they are added here to read as a missing
94
+ * dependency instead of as a failed load.
95
+ */
96
+ const idDeps = step.type === FormStepTypes.ENTITYVALUEPICKER
97
+ ? [...(step.dependencies ?? []), ...entityValuePickerBlockingSteps(step)]
98
+ : step.dependencies;
99
+ for (const dep of idDeps ?? []) {
100
+ if (visited.has(dep))
101
+ continue;
102
+ visited.add(dep);
103
+ walk(dep);
104
+ deps.push(dep);
85
105
  }
86
- }
106
+ };
107
+ walk(idStep);
87
108
  return deps;
88
109
  };
89
110
  export const useFormStep = (step, { rules, debounce, sizeChange, defaultValue }) => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import FormStepTypes, { EntityValueDataTypes } from '../constants/FormStepTypes';
3
+ import { calcDeepDependencies } from './StepHooks';
4
+ const form = (steps) => ({
5
+ steps: Object.entries(steps).reduce((acc, [id, step]) => ({
6
+ ...acc,
7
+ [id]: { id, idSection: 'section', type: FormStepTypes.TEXTINPUT, ...step },
8
+ }), {}),
9
+ });
10
+ const picker = (step) => ({
11
+ type: FormStepTypes.ENTITYVALUEPICKER,
12
+ idEntity: 'entity',
13
+ path: [],
14
+ filters: [],
15
+ options: {},
16
+ ...step,
17
+ });
18
+ const stepFilter = (idStep, required) => ({
19
+ idProperty: 'prop-' + idStep,
20
+ type: EntityValueDataTypes.STEP,
21
+ idStep,
22
+ any: false,
23
+ required,
24
+ });
25
+ describe('calcDeepDependencies', () => {
26
+ test('walks a linear chain deepest first, listing each id once', () => {
27
+ const deps = calcDeepDependencies('a', form({
28
+ a: { dependencies: ['b'] },
29
+ b: { dependencies: ['c'] },
30
+ c: {},
31
+ }));
32
+ expect(deps).toEqual(['c', 'b']);
33
+ });
34
+ test('lists a dependency shared by two branches once', () => {
35
+ const deps = calcDeepDependencies('a', form({
36
+ a: { dependencies: ['b', 'c'] },
37
+ b: { dependencies: ['d'] },
38
+ c: { dependencies: ['d'] },
39
+ d: {},
40
+ }));
41
+ expect(deps).toEqual(['d', 'b', 'c']);
42
+ });
43
+ test('terminates on an A -> B -> A cycle without listing the step itself', () => {
44
+ const deps = calcDeepDependencies('a', form({
45
+ a: { dependencies: ['b'] },
46
+ b: { dependencies: ['a'] },
47
+ }));
48
+ expect(deps).toEqual(['b']);
49
+ });
50
+ test('terminates on a cycle reached deeper in the walk', () => {
51
+ const deps = calcDeepDependencies('a', form({
52
+ a: { dependencies: ['b'] },
53
+ b: { dependencies: ['c'] },
54
+ c: { dependencies: ['b'] },
55
+ }));
56
+ expect(deps).toEqual(['c', 'b']);
57
+ });
58
+ test('terminates on a step that depends on itself', () => {
59
+ const deps = calcDeepDependencies('a', form({
60
+ a: { dependencies: ['b'] },
61
+ b: { dependencies: ['b'] },
62
+ }));
63
+ expect(deps).toEqual(['b']);
64
+ });
65
+ /**
66
+ * A draft an entity picker points back at itself with a required STEP filter: the filter is not
67
+ * in the saved `dependencies` yet, so it only reaches the walk through
68
+ * `entityValuePickerBlockingSteps`.
69
+ */
70
+ test('terminates on a self reference through a required STEP filter of an entity picker', () => {
71
+ const deps = calcDeepDependencies('a', form({
72
+ a: { dependencies: ['picker'] },
73
+ picker: picker({ filters: [stepFilter('picker', true)] }),
74
+ }));
75
+ expect(deps).toEqual(['picker']);
76
+ });
77
+ test('terminates on a cycle closed by a required STEP filter of an entity picker', () => {
78
+ const deps = calcDeepDependencies('a', form({
79
+ a: { dependencies: ['picker'] },
80
+ picker: picker({ filters: [stepFilter('other', true)] }),
81
+ other: { dependencies: ['picker'] },
82
+ }));
83
+ expect(deps).toEqual(['other', 'picker']);
84
+ });
85
+ test('keeps a dependency whose step the draft has already deleted', () => {
86
+ const deps = calcDeepDependencies('a', form({ a: { dependencies: ['gone'] } }));
87
+ expect(deps).toEqual(['gone']);
88
+ });
89
+ test('answers nothing for a step the form does not hold', () => {
90
+ expect(calcDeepDependencies('missing', form({}))).toEqual([]);
91
+ });
92
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arquimedes.co/eureka-forms",
3
3
  "repository": "git://github.com/Arquimede5/Eureka-Forms.git",
4
- "version": "3.0.65",
4
+ "version": "3.0.66",
5
5
  "scripts": {
6
6
  "watch": "node node_modules/@typescript/native/bin/tsc --noEmit --watch --project tsconfig.app.json",
7
7
  "start": "vite",