@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.
- package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.js +7 -0
- package/dist/FormSteps/EntityValueStep/entityValuePickerWatchedSteps.test.js +14 -0
- package/dist/FormSteps/StepHooks.d.ts +14 -0
- package/dist/FormSteps/StepHooks.js +38 -17
- package/dist/FormSteps/StepHooks.test.d.ts +1 -0
- package/dist/FormSteps/StepHooks.test.js +92 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
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",
|