@wizzard-packages/core 0.1.0
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/LICENSE +21 -0
- package/dist/index.cjs +854 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +499 -0
- package/dist/index.d.ts +499 -0
- package/dist/index.js +848 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
// src/utils/data.ts
|
|
2
|
+
var pathCache = /* @__PURE__ */ new Map();
|
|
3
|
+
function toPath(path) {
|
|
4
|
+
if (!path) return [];
|
|
5
|
+
if (pathCache.has(path)) {
|
|
6
|
+
return pathCache.get(path);
|
|
7
|
+
}
|
|
8
|
+
let keys;
|
|
9
|
+
if (path.includes("[")) {
|
|
10
|
+
keys = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
11
|
+
} else {
|
|
12
|
+
keys = path.split(".").filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
pathCache.set(path, keys);
|
|
15
|
+
return keys;
|
|
16
|
+
}
|
|
17
|
+
function getByPath(obj, path, defaultValue) {
|
|
18
|
+
if (!path || obj === void 0 || obj === null) return defaultValue ?? obj;
|
|
19
|
+
if (!path.includes(".") && !path.includes("[")) {
|
|
20
|
+
const val = obj[path];
|
|
21
|
+
return val !== void 0 ? val : defaultValue;
|
|
22
|
+
}
|
|
23
|
+
const keys = toPath(path);
|
|
24
|
+
let result = obj;
|
|
25
|
+
for (let i = 0; i < keys.length; i++) {
|
|
26
|
+
if (result === void 0 || result === null) return defaultValue;
|
|
27
|
+
result = result[keys[i]];
|
|
28
|
+
}
|
|
29
|
+
return result !== void 0 ? result : defaultValue;
|
|
30
|
+
}
|
|
31
|
+
function setByPath(obj, path, value) {
|
|
32
|
+
if (!path) return value;
|
|
33
|
+
if (!path.includes(".") && !path.includes("[")) {
|
|
34
|
+
if (Array.isArray(obj)) {
|
|
35
|
+
const copy = [...obj];
|
|
36
|
+
copy[path] = value;
|
|
37
|
+
return copy;
|
|
38
|
+
}
|
|
39
|
+
return { ...obj, [path]: value };
|
|
40
|
+
}
|
|
41
|
+
const keys = toPath(path);
|
|
42
|
+
if (keys.length === 0) return value;
|
|
43
|
+
const root = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
44
|
+
let current = root;
|
|
45
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
46
|
+
const key = keys[i];
|
|
47
|
+
const nextKey = keys[i + 1];
|
|
48
|
+
const existing = current[key];
|
|
49
|
+
let nextLevel;
|
|
50
|
+
if (existing && typeof existing === "object") {
|
|
51
|
+
nextLevel = Array.isArray(existing) ? [...existing] : { ...existing };
|
|
52
|
+
} else {
|
|
53
|
+
const isNumeric = /^\d+$/.test(nextKey);
|
|
54
|
+
nextLevel = isNumeric ? [] : {};
|
|
55
|
+
}
|
|
56
|
+
current[key] = nextLevel;
|
|
57
|
+
current = nextLevel;
|
|
58
|
+
}
|
|
59
|
+
const lastKey = keys[keys.length - 1];
|
|
60
|
+
current[lastKey] = value;
|
|
61
|
+
return root;
|
|
62
|
+
}
|
|
63
|
+
function shallowEqual(a, b) {
|
|
64
|
+
if (Object.is(a, b)) return true;
|
|
65
|
+
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const keysA = Object.keys(a);
|
|
69
|
+
const keysB = Object.keys(b);
|
|
70
|
+
if (keysA.length !== keysB.length) return false;
|
|
71
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
72
|
+
const key = keysA[i];
|
|
73
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) || !Object.is(a[key], b[key])) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/store/WizardStore.ts
|
|
81
|
+
var WizardStore = class {
|
|
82
|
+
constructor(initialData, middlewares = []) {
|
|
83
|
+
this.dirtyFields = /* @__PURE__ */ new Set();
|
|
84
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
85
|
+
this.actionListeners = /* @__PURE__ */ new Set();
|
|
86
|
+
this.errorsMap = /* @__PURE__ */ new Map();
|
|
87
|
+
this.persistenceDebounceTimers = /* @__PURE__ */ new Map();
|
|
88
|
+
this.stepsMap = /* @__PURE__ */ new Map();
|
|
89
|
+
/**
|
|
90
|
+
* Returns the current immutable snapshot of the wizard state.
|
|
91
|
+
*/
|
|
92
|
+
this.getSnapshot = () => {
|
|
93
|
+
return this.state;
|
|
94
|
+
};
|
|
95
|
+
this.subscribe = (listener) => {
|
|
96
|
+
this.listeners.add(listener);
|
|
97
|
+
return () => this.listeners.delete(listener);
|
|
98
|
+
};
|
|
99
|
+
// Caching for condition resolution
|
|
100
|
+
this.conditionCache = /* @__PURE__ */ new Map();
|
|
101
|
+
this.initialData = typeof structuredClone === "function" ? structuredClone(initialData) : JSON.parse(JSON.stringify(initialData));
|
|
102
|
+
this.state = {
|
|
103
|
+
data: initialData,
|
|
104
|
+
errors: {},
|
|
105
|
+
isDirty: false,
|
|
106
|
+
dirtyFields: this.dirtyFields,
|
|
107
|
+
visitedSteps: /* @__PURE__ */ new Set(),
|
|
108
|
+
completedSteps: /* @__PURE__ */ new Set(),
|
|
109
|
+
errorSteps: /* @__PURE__ */ new Set(),
|
|
110
|
+
currentStep: null,
|
|
111
|
+
currentStepId: "",
|
|
112
|
+
currentStepIndex: 0,
|
|
113
|
+
isFirstStep: true,
|
|
114
|
+
isLastStep: false,
|
|
115
|
+
isLoading: true,
|
|
116
|
+
isPending: false,
|
|
117
|
+
isBusy: false,
|
|
118
|
+
activeSteps: [],
|
|
119
|
+
history: [],
|
|
120
|
+
busySteps: /* @__PURE__ */ new Set(),
|
|
121
|
+
progress: 0,
|
|
122
|
+
activeStepsCount: 0,
|
|
123
|
+
breadcrumbs: [],
|
|
124
|
+
config: {},
|
|
125
|
+
goToStepResult: null
|
|
126
|
+
};
|
|
127
|
+
this.middlewareChain = this.setupMiddlewares(middlewares);
|
|
128
|
+
}
|
|
129
|
+
// Cache for O(1) step lookup
|
|
130
|
+
subscribeToActions(listener) {
|
|
131
|
+
this.actionListeners.add(listener);
|
|
132
|
+
return () => this.actionListeners.delete(listener);
|
|
133
|
+
}
|
|
134
|
+
notifyActions(action) {
|
|
135
|
+
this.actionListeners.forEach((l) => l(action));
|
|
136
|
+
}
|
|
137
|
+
setupMiddlewares(middlewares) {
|
|
138
|
+
const middlewareAPI = {
|
|
139
|
+
getState: () => this.state.data,
|
|
140
|
+
getSnapshot: () => this.getSnapshot(),
|
|
141
|
+
dispatch: (action) => this.dispatch(action)
|
|
142
|
+
};
|
|
143
|
+
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
|
|
144
|
+
return chain.reduceRight(
|
|
145
|
+
(next, middleware) => middleware(next),
|
|
146
|
+
this.internalDispatch.bind(this)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Processes an action through the middleware chain and updates the state.
|
|
151
|
+
* This is the primary way to trigger any state change in the wizard.
|
|
152
|
+
*
|
|
153
|
+
* @param action The action to perform
|
|
154
|
+
*/
|
|
155
|
+
dispatch(action) {
|
|
156
|
+
this.middlewareChain(action);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Internal dispatch that actually performs the state updates
|
|
160
|
+
*/
|
|
161
|
+
internalDispatch(action) {
|
|
162
|
+
this.notifyActions(action);
|
|
163
|
+
switch (action.type) {
|
|
164
|
+
case "INIT":
|
|
165
|
+
this.initialData = typeof structuredClone === "function" ? structuredClone(action.payload.data) : JSON.parse(JSON.stringify(action.payload.data));
|
|
166
|
+
const initialActiveSteps = action.payload.config.steps.filter((s) => !s.condition);
|
|
167
|
+
this.stepsMap.clear();
|
|
168
|
+
action.payload.config.steps.forEach((step) => {
|
|
169
|
+
this.stepsMap.set(step.id, step);
|
|
170
|
+
});
|
|
171
|
+
this.state = {
|
|
172
|
+
...this.state,
|
|
173
|
+
data: action.payload.data,
|
|
174
|
+
config: action.payload.config,
|
|
175
|
+
activeSteps: initialActiveSteps,
|
|
176
|
+
activeStepsCount: initialActiveSteps.length
|
|
177
|
+
};
|
|
178
|
+
if (action.payload.config.persistence?.adapter) {
|
|
179
|
+
this.persistenceAdapter = action.payload.config.persistence.adapter;
|
|
180
|
+
}
|
|
181
|
+
this.notify();
|
|
182
|
+
break;
|
|
183
|
+
case "SET_CURRENT_STEP_ID":
|
|
184
|
+
if (this.state.currentStepId) {
|
|
185
|
+
this.handleStepChangePersistence(this.state.currentStepId);
|
|
186
|
+
}
|
|
187
|
+
this.state = {
|
|
188
|
+
...this.state,
|
|
189
|
+
currentStepId: action.payload.stepId
|
|
190
|
+
};
|
|
191
|
+
this.notify();
|
|
192
|
+
break;
|
|
193
|
+
case "SET_HISTORY":
|
|
194
|
+
this.state = {
|
|
195
|
+
...this.state,
|
|
196
|
+
history: action.payload.history
|
|
197
|
+
};
|
|
198
|
+
this.notify();
|
|
199
|
+
break;
|
|
200
|
+
case "SET_ACTIVE_STEPS":
|
|
201
|
+
this.state = {
|
|
202
|
+
...this.state,
|
|
203
|
+
activeSteps: action.payload.steps,
|
|
204
|
+
activeStepsCount: action.payload.steps.length
|
|
205
|
+
};
|
|
206
|
+
this.notify();
|
|
207
|
+
break;
|
|
208
|
+
case "SET_DATA":
|
|
209
|
+
this.updateDataByPath(action.payload.path, action.payload.value, action.payload.options);
|
|
210
|
+
break;
|
|
211
|
+
case "UPDATE_DATA":
|
|
212
|
+
this.updateBulkData(action.payload.data, action.payload.options);
|
|
213
|
+
break;
|
|
214
|
+
case "VALIDATE_START":
|
|
215
|
+
this.state.busySteps.add(action.payload.stepId);
|
|
216
|
+
this.state = {
|
|
217
|
+
...this.state,
|
|
218
|
+
busySteps: new Set(this.state.busySteps)
|
|
219
|
+
};
|
|
220
|
+
break;
|
|
221
|
+
case "VALIDATE_END":
|
|
222
|
+
this.state.busySteps.delete(action.payload.stepId);
|
|
223
|
+
this.state = {
|
|
224
|
+
...this.state,
|
|
225
|
+
busySteps: new Set(this.state.busySteps)
|
|
226
|
+
};
|
|
227
|
+
if (action.payload.result.errors) {
|
|
228
|
+
this.setStepErrors(action.payload.stepId, action.payload.result.errors);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
case "SET_STEP_ERRORS":
|
|
232
|
+
this.setStepErrors(action.payload.stepId, action.payload.errors);
|
|
233
|
+
break;
|
|
234
|
+
case "RESET":
|
|
235
|
+
this.setInitialData(action.payload.data);
|
|
236
|
+
break;
|
|
237
|
+
case "SET_ERROR_STEPS":
|
|
238
|
+
this.state = { ...this.state, errorSteps: action.payload.steps };
|
|
239
|
+
break;
|
|
240
|
+
case "SET_VISITED_STEPS":
|
|
241
|
+
this.state = { ...this.state, visitedSteps: action.payload.steps };
|
|
242
|
+
this.saveMeta();
|
|
243
|
+
break;
|
|
244
|
+
case "SET_COMPLETED_STEPS":
|
|
245
|
+
this.state = { ...this.state, completedSteps: action.payload.steps };
|
|
246
|
+
this.saveMeta();
|
|
247
|
+
break;
|
|
248
|
+
case "RESTORE_SNAPSHOT":
|
|
249
|
+
this.state = { ...action.payload.snapshot };
|
|
250
|
+
this.saveMeta();
|
|
251
|
+
break;
|
|
252
|
+
case "GO_TO_STEP":
|
|
253
|
+
this.state = {
|
|
254
|
+
...this.state,
|
|
255
|
+
goToStepResult: action.payload.result
|
|
256
|
+
};
|
|
257
|
+
if (action.payload.result === true) {
|
|
258
|
+
if (action.payload.nextVisitedSteps) {
|
|
259
|
+
this.dispatch({
|
|
260
|
+
type: "SET_VISITED_STEPS",
|
|
261
|
+
payload: {
|
|
262
|
+
steps: action.payload.nextVisitedSteps
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (action.payload.nextHistory) {
|
|
267
|
+
this.dispatch({
|
|
268
|
+
type: "SET_HISTORY",
|
|
269
|
+
payload: {
|
|
270
|
+
history: action.payload.nextHistory
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (action.payload.to) {
|
|
275
|
+
this.dispatch({
|
|
276
|
+
type: "SET_CURRENT_STEP_ID",
|
|
277
|
+
payload: {
|
|
278
|
+
stepId: action.payload.to
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
case "UPDATE_META":
|
|
285
|
+
this.state = {
|
|
286
|
+
...this.state,
|
|
287
|
+
...action.payload.meta
|
|
288
|
+
};
|
|
289
|
+
if (action.payload.meta.visitedSteps || action.payload.meta.completedSteps || action.payload.meta.history || action.payload.meta.currentStepId) {
|
|
290
|
+
this.saveMeta();
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
this.syncDerivedState();
|
|
295
|
+
this.notify();
|
|
296
|
+
}
|
|
297
|
+
// Refactor update methods to be called from internalDispatch
|
|
298
|
+
updateDataByPath(path, value, _options) {
|
|
299
|
+
const newData = setByPath(this.state.data, path, value);
|
|
300
|
+
if (newData === this.state.data) return;
|
|
301
|
+
this.update(newData, path);
|
|
302
|
+
}
|
|
303
|
+
updateBulkData(data, options) {
|
|
304
|
+
let newData;
|
|
305
|
+
if (options?.replace) {
|
|
306
|
+
newData = data;
|
|
307
|
+
} else {
|
|
308
|
+
newData = JSON.parse(JSON.stringify(this.state.data));
|
|
309
|
+
Object.keys(data).forEach((key) => {
|
|
310
|
+
newData[key] = data[key];
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
this.update(newData, Object.keys(data));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Performs a granular data update at a specific path.
|
|
317
|
+
* Automatically calculates dirty fields and triggers auto-save if configured.
|
|
318
|
+
*
|
|
319
|
+
* @param newData Full new data object
|
|
320
|
+
* @param changedPath Path(s) that were modified
|
|
321
|
+
*/
|
|
322
|
+
update(newData, changedPath) {
|
|
323
|
+
if (changedPath) {
|
|
324
|
+
const paths = Array.isArray(changedPath) ? changedPath : [changedPath];
|
|
325
|
+
paths.forEach((path) => {
|
|
326
|
+
const initialValue = getByPath(this.initialData, path);
|
|
327
|
+
const newValue = getByPath(newData, path);
|
|
328
|
+
if (JSON.stringify(initialValue) !== JSON.stringify(newValue)) {
|
|
329
|
+
this.dirtyFields.add(path);
|
|
330
|
+
} else {
|
|
331
|
+
this.dirtyFields.delete(path);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
this.state = {
|
|
336
|
+
...this.state,
|
|
337
|
+
data: newData,
|
|
338
|
+
isDirty: this.dirtyFields.size > 0,
|
|
339
|
+
dirtyFields: new Set(this.dirtyFields)
|
|
340
|
+
};
|
|
341
|
+
if (changedPath) {
|
|
342
|
+
const paths = Array.isArray(changedPath) ? changedPath : [changedPath];
|
|
343
|
+
this.checkAutoSave(paths);
|
|
344
|
+
} else {
|
|
345
|
+
this.checkAutoSave(Object.keys(newData));
|
|
346
|
+
}
|
|
347
|
+
this.notify();
|
|
348
|
+
}
|
|
349
|
+
updateMeta(newMeta) {
|
|
350
|
+
this.dispatch({
|
|
351
|
+
type: "UPDATE_META",
|
|
352
|
+
payload: { meta: newMeta }
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
syncDerivedState() {
|
|
356
|
+
const { activeSteps, currentStepId, visitedSteps, completedSteps, errorSteps } = this.state;
|
|
357
|
+
const currentStepIndex = Math.max(
|
|
358
|
+
0,
|
|
359
|
+
activeSteps.findIndex((s) => s.id === currentStepId)
|
|
360
|
+
);
|
|
361
|
+
const currentStep = activeSteps[currentStepIndex] || null;
|
|
362
|
+
const breadcrumbs = activeSteps.map((step) => {
|
|
363
|
+
let status = "upcoming";
|
|
364
|
+
if (step.id === currentStepId) status = "current";
|
|
365
|
+
else if (errorSteps.has(step.id)) status = "error";
|
|
366
|
+
else if (completedSteps.has(step.id)) status = "completed";
|
|
367
|
+
else if (visitedSteps.has(step.id)) status = "visited";
|
|
368
|
+
return {
|
|
369
|
+
id: step.id,
|
|
370
|
+
label: step.label,
|
|
371
|
+
status
|
|
372
|
+
};
|
|
373
|
+
});
|
|
374
|
+
this.state = {
|
|
375
|
+
...this.state,
|
|
376
|
+
currentStep,
|
|
377
|
+
currentStepIndex,
|
|
378
|
+
isFirstStep: currentStepIndex === 0,
|
|
379
|
+
isLastStep: activeSteps.length > 0 && currentStepIndex === activeSteps.length - 1,
|
|
380
|
+
progress: activeSteps.length > 0 ? Math.round((currentStepIndex + 1) / activeSteps.length * 100) : 0,
|
|
381
|
+
breadcrumbs
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Sets the initial data for the wizard.
|
|
386
|
+
* Resets dirty tracking based on this new data.
|
|
387
|
+
*/
|
|
388
|
+
setInitialData(data) {
|
|
389
|
+
this.initialData = typeof structuredClone === "function" ? structuredClone(data) : JSON.parse(JSON.stringify(data));
|
|
390
|
+
this.dirtyFields.clear();
|
|
391
|
+
this.state = {
|
|
392
|
+
...this.state,
|
|
393
|
+
data,
|
|
394
|
+
isDirty: false,
|
|
395
|
+
dirtyFields: /* @__PURE__ */ new Set()
|
|
396
|
+
};
|
|
397
|
+
this.notify();
|
|
398
|
+
}
|
|
399
|
+
syncErrors() {
|
|
400
|
+
const newErrorsObj = {};
|
|
401
|
+
for (const [stepId, fieldErrors] of this.errorsMap.entries()) {
|
|
402
|
+
if (fieldErrors.size > 0) {
|
|
403
|
+
newErrorsObj[stepId] = Object.fromEntries(fieldErrors);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
this.state = { ...this.state, errors: newErrorsObj };
|
|
407
|
+
this.notify();
|
|
408
|
+
}
|
|
409
|
+
updateErrors(newErrors) {
|
|
410
|
+
this.errorsMap.clear();
|
|
411
|
+
for (const [stepId, fieldErrors] of Object.entries(newErrors)) {
|
|
412
|
+
const stepMap = /* @__PURE__ */ new Map();
|
|
413
|
+
for (const [field, msg] of Object.entries(fieldErrors)) {
|
|
414
|
+
stepMap.set(field, msg);
|
|
415
|
+
}
|
|
416
|
+
if (stepMap.size > 0) this.errorsMap.set(stepId, stepMap);
|
|
417
|
+
}
|
|
418
|
+
this.state = { ...this.state, errors: newErrors };
|
|
419
|
+
this.notify();
|
|
420
|
+
}
|
|
421
|
+
setStepErrors(stepId, errors) {
|
|
422
|
+
if (!errors || Object.keys(errors).length === 0) {
|
|
423
|
+
if (this.errorsMap.has(stepId)) {
|
|
424
|
+
this.errorsMap.delete(stepId);
|
|
425
|
+
this.syncErrors();
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
const stepMap = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const [field, msg] of Object.entries(errors)) {
|
|
432
|
+
stepMap.set(field, msg);
|
|
433
|
+
}
|
|
434
|
+
this.errorsMap.set(stepId, stepMap);
|
|
435
|
+
this.syncErrors();
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
deleteError(stepId, path) {
|
|
439
|
+
const stepErrors = this.errorsMap.get(stepId);
|
|
440
|
+
if (!stepErrors) return false;
|
|
441
|
+
if (stepErrors.has(path)) {
|
|
442
|
+
stepErrors.delete(path);
|
|
443
|
+
if (stepErrors.size === 0) {
|
|
444
|
+
this.errorsMap.delete(stepId);
|
|
445
|
+
}
|
|
446
|
+
this.syncErrors();
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
notify() {
|
|
452
|
+
this.listeners.forEach((l) => l());
|
|
453
|
+
}
|
|
454
|
+
injectPersistence(adapter) {
|
|
455
|
+
this.persistenceAdapter = adapter;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Restores wizard state from persistence storage.
|
|
459
|
+
* Implements "latest wins" conflict resolution based on step timestamps.
|
|
460
|
+
*/
|
|
461
|
+
hydrate() {
|
|
462
|
+
if (!this.persistenceAdapter) return;
|
|
463
|
+
let latestTimestamp = -1;
|
|
464
|
+
let latestData = null;
|
|
465
|
+
let hasHydrated = false;
|
|
466
|
+
const config = this.state.config;
|
|
467
|
+
config.steps.forEach((step) => {
|
|
468
|
+
const adapter = step.persistenceAdapter || this.persistenceAdapter;
|
|
469
|
+
if (!adapter) return;
|
|
470
|
+
try {
|
|
471
|
+
let candidateData;
|
|
472
|
+
let candidateTimestamp = 0;
|
|
473
|
+
if (adapter.getStepWithMeta) {
|
|
474
|
+
const result = adapter.getStepWithMeta(step.id);
|
|
475
|
+
if (result) {
|
|
476
|
+
candidateData = result.data;
|
|
477
|
+
candidateTimestamp = result.timestamp;
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
candidateData = adapter.getStep(step.id);
|
|
481
|
+
}
|
|
482
|
+
if (candidateData) {
|
|
483
|
+
if (candidateTimestamp >= latestTimestamp) {
|
|
484
|
+
latestTimestamp = candidateTimestamp;
|
|
485
|
+
latestData = candidateData;
|
|
486
|
+
hasHydrated = true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch (e) {
|
|
490
|
+
console.warn(`[WizardStore] \u26A0\uFE0F Failed to hydrate step ${step.id}:`, e);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
if (hasHydrated && latestData) {
|
|
494
|
+
this.updateBulkData(latestData, { replace: true });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
saveMeta() {
|
|
498
|
+
const config = this.state.config;
|
|
499
|
+
const persistenceMode = config.persistence?.mode || "onStepChange";
|
|
500
|
+
if (persistenceMode !== "manual" && this.persistenceAdapter) {
|
|
501
|
+
this.persistenceAdapter.saveStep("__wizzard_meta__", {
|
|
502
|
+
currentStepId: this.state.currentStepId,
|
|
503
|
+
visited: Array.from(this.state.visitedSteps),
|
|
504
|
+
completed: Array.from(this.state.completedSteps),
|
|
505
|
+
history: this.state.history
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
clearStepStorage(stepId) {
|
|
510
|
+
const step = this.stepsMap.get(stepId) || this.state.config.steps.find((s) => s.id === stepId);
|
|
511
|
+
const adapter = step?.persistenceAdapter || this.persistenceAdapter;
|
|
512
|
+
if (adapter && adapter.clearStep) {
|
|
513
|
+
adapter.clearStep(stepId);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Manually triggers data persistence for specific steps or the current step.
|
|
518
|
+
*
|
|
519
|
+
* @param stepId Optional ID of step to save. If omitted, saves current step.
|
|
520
|
+
*/
|
|
521
|
+
save(stepId) {
|
|
522
|
+
if (stepId) {
|
|
523
|
+
this.saveStepData(stepId);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (this.state.currentStepId) {
|
|
527
|
+
this.saveStepData(this.state.currentStepId);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
saveStepData(stepId) {
|
|
531
|
+
const step = this.stepsMap.get(stepId) || this.state.config.steps.find((s) => s.id === stepId);
|
|
532
|
+
if (!step) return;
|
|
533
|
+
const adapter = step.persistenceAdapter || this.persistenceAdapter;
|
|
534
|
+
if (!adapter) return;
|
|
535
|
+
adapter.saveStep(stepId, this.state.data);
|
|
536
|
+
}
|
|
537
|
+
handleStepChangePersistence(stepId) {
|
|
538
|
+
const step = this.stepsMap.get(stepId) || this.state.config.steps.find((s) => s.id === stepId);
|
|
539
|
+
if (!step) return;
|
|
540
|
+
const mode = step.persistenceMode || this.state.config.persistence?.mode || "onStepChange";
|
|
541
|
+
if (mode === "onStepChange") {
|
|
542
|
+
this.saveStepData(stepId);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Internal helper to handle auto-save on data change
|
|
546
|
+
checkAutoSave(_changedPaths) {
|
|
547
|
+
const { config, currentStepId } = this.state;
|
|
548
|
+
if (!currentStepId) return;
|
|
549
|
+
const step = this.stepsMap.get(currentStepId) || config.steps.find((s) => s.id === currentStepId);
|
|
550
|
+
if (!step) return;
|
|
551
|
+
const mode = step.persistenceMode || config.persistence?.mode || "onStepChange";
|
|
552
|
+
if (mode !== "onChange") return;
|
|
553
|
+
const debounceTime = config.persistence?.debounceTime ?? 300;
|
|
554
|
+
const timerKey = currentStepId;
|
|
555
|
+
if (this.persistenceDebounceTimers.has(timerKey)) {
|
|
556
|
+
clearTimeout(this.persistenceDebounceTimers.get(timerKey));
|
|
557
|
+
}
|
|
558
|
+
this.persistenceDebounceTimers.set(
|
|
559
|
+
timerKey,
|
|
560
|
+
setTimeout(() => {
|
|
561
|
+
this.saveStepData(currentStepId);
|
|
562
|
+
}, debounceTime)
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Evaluates visibility conditions for all steps and returns only those that should be active.
|
|
567
|
+
* Uses memoization to avoid redundant async calls if dependencies haven't changed.
|
|
568
|
+
*
|
|
569
|
+
* @param data Optional data override for evaluation
|
|
570
|
+
*/
|
|
571
|
+
async resolveActiveSteps(data) {
|
|
572
|
+
const currentData = data || this.state.data;
|
|
573
|
+
const config = this.state.config;
|
|
574
|
+
this.updateMeta({ isBusy: true });
|
|
575
|
+
try {
|
|
576
|
+
const steps = config.steps || [];
|
|
577
|
+
const results = await Promise.all(
|
|
578
|
+
steps.map(async (step) => {
|
|
579
|
+
if (!step.condition) return { step, ok: true };
|
|
580
|
+
if (step.conditionDependsOn) {
|
|
581
|
+
const currentDepsValues = step.conditionDependsOn.map(
|
|
582
|
+
(path) => getByPath(currentData, path)
|
|
583
|
+
);
|
|
584
|
+
const cached = this.conditionCache.get(step.id);
|
|
585
|
+
if (cached && cached.depsValues.length === currentDepsValues.length && cached.depsValues.every((val, idx) => val === currentDepsValues[idx])) {
|
|
586
|
+
return { step, ok: cached.result };
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const res = step.condition(currentData || {}, this.getSnapshot());
|
|
590
|
+
const ok = res instanceof Promise ? await res : res;
|
|
591
|
+
this.conditionCache.set(step.id, {
|
|
592
|
+
result: ok,
|
|
593
|
+
depsValues: currentDepsValues
|
|
594
|
+
});
|
|
595
|
+
return { step, ok };
|
|
596
|
+
} catch (e) {
|
|
597
|
+
console.error(`[Wizard] Condition failed for ${step.id}:`, e);
|
|
598
|
+
return { step, ok: false };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const nextBusyStart = new Set(this.state.busySteps);
|
|
602
|
+
nextBusyStart.add(step.id);
|
|
603
|
+
this.updateMeta({
|
|
604
|
+
busySteps: nextBusyStart,
|
|
605
|
+
isBusy: true
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
const res = step.condition(currentData || {}, this.getSnapshot());
|
|
609
|
+
const ok = res instanceof Promise ? await res : res;
|
|
610
|
+
return { step, ok };
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error(`[Wizard] Condition failed for ${step.id}:`, e);
|
|
613
|
+
return { step, ok: false };
|
|
614
|
+
} finally {
|
|
615
|
+
const currentSnapshot = this.getSnapshot();
|
|
616
|
+
const nextBusyEnd = new Set(currentSnapshot.busySteps);
|
|
617
|
+
nextBusyEnd.delete(step.id);
|
|
618
|
+
this.updateMeta({
|
|
619
|
+
busySteps: nextBusyEnd,
|
|
620
|
+
isBusy: nextBusyEnd.size > 0
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
return results.filter((r) => r.ok).map((r) => r.step);
|
|
626
|
+
} finally {
|
|
627
|
+
const currentSnapshot = this.getSnapshot();
|
|
628
|
+
if (currentSnapshot.busySteps.size === 0) {
|
|
629
|
+
this.updateMeta({ isBusy: false });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async validateStep(stepId) {
|
|
634
|
+
const steps = this.state.config.steps || [];
|
|
635
|
+
const step = this.stepsMap.get(stepId) || steps.find((s) => s.id === stepId);
|
|
636
|
+
if (!step || !step.validationAdapter) return true;
|
|
637
|
+
this.dispatch({ type: "VALIDATE_START", payload: { stepId } });
|
|
638
|
+
let isValid = true;
|
|
639
|
+
try {
|
|
640
|
+
const result = await step.validationAdapter.validate(this.state.data);
|
|
641
|
+
isValid = result.isValid;
|
|
642
|
+
if (result.isValid) {
|
|
643
|
+
this.setStepErrors(stepId, null);
|
|
644
|
+
const nextErrorSteps = new Set(this.state.errorSteps);
|
|
645
|
+
nextErrorSteps.delete(stepId);
|
|
646
|
+
this.dispatch({
|
|
647
|
+
type: "SET_ERROR_STEPS",
|
|
648
|
+
payload: { steps: nextErrorSteps }
|
|
649
|
+
});
|
|
650
|
+
return true;
|
|
651
|
+
} else {
|
|
652
|
+
this.setStepErrors(stepId, result.errors || null);
|
|
653
|
+
if (this.state.config.analytics?.onEvent) {
|
|
654
|
+
this.state.config.analytics.onEvent("validation_error", {
|
|
655
|
+
stepId,
|
|
656
|
+
errors: result.errors,
|
|
657
|
+
timestamp: Date.now()
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const nextErrorSteps = new Set(this.state.errorSteps);
|
|
661
|
+
nextErrorSteps.add(stepId);
|
|
662
|
+
this.dispatch({
|
|
663
|
+
type: "SET_ERROR_STEPS",
|
|
664
|
+
payload: { steps: nextErrorSteps }
|
|
665
|
+
});
|
|
666
|
+
const nextCompleted = new Set(this.state.completedSteps);
|
|
667
|
+
if (nextCompleted.has(stepId)) {
|
|
668
|
+
nextCompleted.delete(stepId);
|
|
669
|
+
this.dispatch({
|
|
670
|
+
type: "SET_COMPLETED_STEPS",
|
|
671
|
+
payload: { steps: nextCompleted }
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
} finally {
|
|
677
|
+
this.dispatch({
|
|
678
|
+
type: "VALIDATE_END",
|
|
679
|
+
payload: { stepId, result: { isValid } }
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async validateAll() {
|
|
684
|
+
this.updateMeta({ isBusy: true });
|
|
685
|
+
try {
|
|
686
|
+
const data = this.getSnapshot().data;
|
|
687
|
+
const activeSteps = await this.resolveActiveSteps(data);
|
|
688
|
+
const results = await Promise.all(
|
|
689
|
+
activeSteps.map((step) => this.validateStep(step.id))
|
|
690
|
+
);
|
|
691
|
+
return {
|
|
692
|
+
isValid: results.every(Boolean),
|
|
693
|
+
errors: this.getSnapshot().errors
|
|
694
|
+
};
|
|
695
|
+
} finally {
|
|
696
|
+
const currentSnapshot = this.getSnapshot();
|
|
697
|
+
if (currentSnapshot.busySteps.size === 0) {
|
|
698
|
+
this.updateMeta({ isBusy: false });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async goToStep(stepId, options = { validate: true }) {
|
|
703
|
+
const { currentStepId, config, data: currentData } = this.state;
|
|
704
|
+
const allSteps = config.steps || [];
|
|
705
|
+
const currentIdx = allSteps.findIndex((s) => s.id === currentStepId);
|
|
706
|
+
const targetIdx = allSteps.findIndex((s) => s.id === stepId);
|
|
707
|
+
this.dispatch({
|
|
708
|
+
type: "GO_TO_STEP",
|
|
709
|
+
payload: { result: "init", to: stepId, from: currentStepId }
|
|
710
|
+
});
|
|
711
|
+
if (this.getSnapshot().goToStepResult !== "init") {
|
|
712
|
+
console.warn("[WizardStore] Navigation aborted by middleware interceptor");
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
if (targetIdx === currentIdx) return true;
|
|
716
|
+
if (targetIdx > currentIdx && currentStepId && options.validate) {
|
|
717
|
+
const step = this.stepsMap.get(currentStepId) || allSteps.find((s) => s.id === currentStepId);
|
|
718
|
+
const shouldVal = step?.autoValidate ?? config.autoValidate ?? !!step?.validationAdapter;
|
|
719
|
+
if (shouldVal) {
|
|
720
|
+
const ok = await this.validateStep(currentStepId);
|
|
721
|
+
if (!ok) {
|
|
722
|
+
this.dispatch({
|
|
723
|
+
type: "GO_TO_STEP",
|
|
724
|
+
payload: { result: false, to: stepId, from: currentStepId }
|
|
725
|
+
});
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
this.updateMeta({ isBusy: true });
|
|
731
|
+
try {
|
|
732
|
+
const resolvedSteps = options.providedActiveSteps || await this.resolveActiveSteps(currentData);
|
|
733
|
+
const target = resolvedSteps.find((s) => s.id === stepId);
|
|
734
|
+
if (!target) {
|
|
735
|
+
this.dispatch({
|
|
736
|
+
type: "GO_TO_STEP",
|
|
737
|
+
payload: { result: false, to: stepId, from: currentStepId }
|
|
738
|
+
});
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
if (target.canNavigateTo) {
|
|
742
|
+
const snapshot = this.getSnapshot();
|
|
743
|
+
const canNavigate = await target.canNavigateTo(currentData, snapshot);
|
|
744
|
+
if (!canNavigate) {
|
|
745
|
+
console.warn(
|
|
746
|
+
`[WizardStore] Navigation to step "${stepId}" blocked by canNavigateTo function`
|
|
747
|
+
);
|
|
748
|
+
this.dispatch({
|
|
749
|
+
type: "GO_TO_STEP",
|
|
750
|
+
payload: { result: false, to: stepId, from: currentStepId }
|
|
751
|
+
});
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
const navigationMode = config.navigationMode || "visited";
|
|
756
|
+
const currentSnapshot2 = this.getSnapshot();
|
|
757
|
+
const currentIndex = resolvedSteps.findIndex((s) => s.id === currentStepId);
|
|
758
|
+
const targetIndex = resolvedSteps.findIndex((s) => s.id === stepId);
|
|
759
|
+
const isAdjacent = Math.abs(targetIndex - currentIndex) === 1;
|
|
760
|
+
if (stepId !== currentStepId) {
|
|
761
|
+
switch (navigationMode) {
|
|
762
|
+
case "sequential":
|
|
763
|
+
if (!isAdjacent) {
|
|
764
|
+
console.warn(
|
|
765
|
+
`[WizardStore] Navigation to step "${stepId}" blocked: sequential mode allows only adjacent steps`
|
|
766
|
+
);
|
|
767
|
+
this.dispatch({
|
|
768
|
+
type: "GO_TO_STEP",
|
|
769
|
+
payload: {
|
|
770
|
+
result: false,
|
|
771
|
+
to: stepId,
|
|
772
|
+
from: currentStepId
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
break;
|
|
778
|
+
case "visited":
|
|
779
|
+
const isVisitedOrCompleted = currentSnapshot2.visitedSteps.has(stepId) || currentSnapshot2.completedSteps.has(stepId);
|
|
780
|
+
if (!isAdjacent && !isVisitedOrCompleted) {
|
|
781
|
+
console.warn(
|
|
782
|
+
`[WizardStore] Navigation to step "${stepId}" blocked: step not visited or completed`
|
|
783
|
+
);
|
|
784
|
+
this.dispatch({
|
|
785
|
+
type: "GO_TO_STEP",
|
|
786
|
+
payload: {
|
|
787
|
+
result: false,
|
|
788
|
+
to: stepId,
|
|
789
|
+
from: currentStepId
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
break;
|
|
795
|
+
case "free":
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const step = this.stepsMap.get(currentStepId) || allSteps.find((s) => s.id === currentStepId);
|
|
801
|
+
if (step?.beforeLeave) {
|
|
802
|
+
const snapshot = this.getSnapshot();
|
|
803
|
+
const direction = targetIdx > currentIdx ? "next" : "prev";
|
|
804
|
+
const ok = await step.beforeLeave(currentData, direction, snapshot);
|
|
805
|
+
if (ok === false) {
|
|
806
|
+
this.dispatch({
|
|
807
|
+
type: "GO_TO_STEP",
|
|
808
|
+
payload: { result: false, to: stepId, from: currentStepId }
|
|
809
|
+
});
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const currentSnapshot = this.getSnapshot();
|
|
814
|
+
const nextVisited = new Set(currentSnapshot.visitedSteps);
|
|
815
|
+
if (currentStepId) nextVisited.add(currentStepId);
|
|
816
|
+
nextVisited.add(stepId);
|
|
817
|
+
const nextHistory = [...currentSnapshot.history, stepId];
|
|
818
|
+
this.dispatch({
|
|
819
|
+
type: "GO_TO_STEP",
|
|
820
|
+
payload: {
|
|
821
|
+
result: true,
|
|
822
|
+
to: stepId,
|
|
823
|
+
from: currentStepId,
|
|
824
|
+
nextHistory,
|
|
825
|
+
nextVisitedSteps: nextVisited
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
this.saveMeta();
|
|
829
|
+
if (config.onStepChange) {
|
|
830
|
+
config.onStepChange(currentStepId || null, stepId, currentData);
|
|
831
|
+
}
|
|
832
|
+
if (config.analytics?.onEvent) {
|
|
833
|
+
config.analytics.onEvent("step_change", {
|
|
834
|
+
from: currentStepId || null,
|
|
835
|
+
to: stepId,
|
|
836
|
+
timestamp: Date.now()
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
return true;
|
|
840
|
+
} finally {
|
|
841
|
+
this.updateMeta({ isBusy: false });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
export { WizardStore, getByPath, setByPath, shallowEqual, toPath };
|
|
847
|
+
//# sourceMappingURL=index.js.map
|
|
848
|
+
//# sourceMappingURL=index.js.map
|