@eintrek/erp-shell 0.1.65 → 0.1.67
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/guide/dom.d.ts +2 -0
- package/dist/guide/machine.d.ts +35 -0
- package/dist/guide/types.d.ts +25 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +139 -32
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +139 -32
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1975,7 +1975,7 @@ function platformApiBase(app) {
|
|
|
1975
1975
|
* This package's version, attached to help messages so the back office can
|
|
1976
1976
|
* tell which shell a report came from. Keep in step with package.json.
|
|
1977
1977
|
*/
|
|
1978
|
-
const SHELL_VERSION = "0.1.
|
|
1978
|
+
const SHELL_VERSION = "0.1.67";
|
|
1979
1979
|
|
|
1980
1980
|
const HELP_APP_LABEL = {
|
|
1981
1981
|
platform: "แพลตฟอร์ม",
|
|
@@ -3832,6 +3832,7 @@ function initialGuideState(audience, progress) {
|
|
|
3832
3832
|
chipPulse: 0,
|
|
3833
3833
|
nudge: 0,
|
|
3834
3834
|
gateDismissed: false,
|
|
3835
|
+
replayPage: null,
|
|
3835
3836
|
};
|
|
3836
3837
|
}
|
|
3837
3838
|
function findMission(suite, key) {
|
|
@@ -3889,6 +3890,38 @@ function targetIdOf(step, ctx) {
|
|
|
3889
3890
|
return safe(() => step.targetId(ctx), undefined);
|
|
3890
3891
|
return step.targetId;
|
|
3891
3892
|
}
|
|
3893
|
+
/**
|
|
3894
|
+
* A commit step with nothing to save: its button is not on the page, and what
|
|
3895
|
+
* is saved is (GuideStep.nothingToSave).
|
|
3896
|
+
*/
|
|
3897
|
+
function nothingToSaveHolds(step, env, ctx) {
|
|
3898
|
+
const n = step.nothingToSave;
|
|
3899
|
+
if (!n || !step.target)
|
|
3900
|
+
return false;
|
|
3901
|
+
return !env.isVisible(step.target, targetIdOf(step, ctx)) && env.isVisible(n.target);
|
|
3902
|
+
}
|
|
3903
|
+
/**
|
|
3904
|
+
* The step as it is shown: a commit step with nothing to save is a read step
|
|
3905
|
+
* on what is saved, with its own title (ถัดไป moves on).
|
|
3906
|
+
*/
|
|
3907
|
+
function shownStep(step, nothingToSave) {
|
|
3908
|
+
const n = step.nothingToSave;
|
|
3909
|
+
if (!nothingToSave || !n)
|
|
3910
|
+
return step;
|
|
3911
|
+
return {
|
|
3912
|
+
key: step.key,
|
|
3913
|
+
kind: "view",
|
|
3914
|
+
target: n.target,
|
|
3915
|
+
title: n.title,
|
|
3916
|
+
why: n.why,
|
|
3917
|
+
allow: n.allow,
|
|
3918
|
+
route: step.route,
|
|
3919
|
+
resumeUrl: step.resumeUrl,
|
|
3920
|
+
checkpoint: step.checkpoint,
|
|
3921
|
+
timeoutMs: step.timeoutMs,
|
|
3922
|
+
abort: step.abort,
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3892
3925
|
/** Whether the step's target (or the bridge's) is on screen. */
|
|
3893
3926
|
function stepTargetVisible(step, env, ctx, bridge) {
|
|
3894
3927
|
if (bridge)
|
|
@@ -3957,6 +3990,30 @@ function abortActionsFor(mission, index) {
|
|
|
3957
3990
|
return [];
|
|
3958
3991
|
return [{ escape: true }];
|
|
3959
3992
|
}
|
|
3993
|
+
/** Every anchor the mission's abort actions press. */
|
|
3994
|
+
function abortAnchorsOf(mission) {
|
|
3995
|
+
const out = new Set();
|
|
3996
|
+
const add = (a) => {
|
|
3997
|
+
for (const x of a ? (Array.isArray(a) ? a : [a]) : [])
|
|
3998
|
+
if ("click" in x)
|
|
3999
|
+
out.add(x.click);
|
|
4000
|
+
};
|
|
4001
|
+
add(mission.abort);
|
|
4002
|
+
for (const st of mission.steps)
|
|
4003
|
+
add(st.abort);
|
|
4004
|
+
return Array.from(out);
|
|
4005
|
+
}
|
|
4006
|
+
/**
|
|
4007
|
+
* What the replay may do to put the page back: not press what the user had
|
|
4008
|
+
* open before it started, not Escape out of a dialog that was already open,
|
|
4009
|
+
* and no app event while anything of theirs was open.
|
|
4010
|
+
*/
|
|
4011
|
+
function abortActionsLeft(actions, before) {
|
|
4012
|
+
if (!before)
|
|
4013
|
+
return actions;
|
|
4014
|
+
const busy = before.anchors.length > 0 || before.dialog;
|
|
4015
|
+
return actions.filter(a => "click" in a ? !before.anchors.includes(a.click) : "escape" in a ? !before.dialog : !busy);
|
|
4016
|
+
}
|
|
3960
4017
|
function routeParamsFor(step, env) {
|
|
3961
4018
|
if (!step.route)
|
|
3962
4019
|
return {};
|
|
@@ -4095,7 +4152,7 @@ function announceStep(d, suite, env) {
|
|
|
4095
4152
|
const mission = findMission(suite, s.mission);
|
|
4096
4153
|
if (!mission)
|
|
4097
4154
|
return;
|
|
4098
|
-
const step = mission.steps[s.index];
|
|
4155
|
+
const step = shownStep(mission.steps[s.index], s.nothingToSave);
|
|
4099
4156
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4100
4157
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
4101
4158
|
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
@@ -4163,6 +4220,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
4163
4220
|
confirmExit: false,
|
|
4164
4221
|
misses: 0,
|
|
4165
4222
|
armed: hiddenAnchorShown(step, env),
|
|
4223
|
+
nothingToSave: !away && !bridge && nothingToSaveHolds(step, env, ctx),
|
|
4166
4224
|
});
|
|
4167
4225
|
if (!opts.replay) {
|
|
4168
4226
|
const cp = checkpointAtOrBefore(steps, i);
|
|
@@ -4180,6 +4238,28 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
4180
4238
|
markReadyIfVisible(d, suite, env);
|
|
4181
4239
|
}
|
|
4182
4240
|
}
|
|
4241
|
+
/**
|
|
4242
|
+
* The save button came (a change) or went (nothing left to save): show the
|
|
4243
|
+
* commit step's other form. A ready step whose new target is not there is
|
|
4244
|
+
* left to the target-lost path (back to the checkpoint).
|
|
4245
|
+
*/
|
|
4246
|
+
function setNothingToSave(d, suite, env, on) {
|
|
4247
|
+
const s = d.state.screen;
|
|
4248
|
+
if (s.name !== "step" || s.nothingToSave === on)
|
|
4249
|
+
return;
|
|
4250
|
+
setScreen(d, { ...s, nothingToSave: on, error: null });
|
|
4251
|
+
if (s.phase !== "ready") {
|
|
4252
|
+
markReadyIfVisible(d, suite, env);
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
4255
|
+
const mission = findMission(suite, s.mission);
|
|
4256
|
+
if (!mission)
|
|
4257
|
+
return;
|
|
4258
|
+
const step = shownStep(mission.steps[s.index], on);
|
|
4259
|
+
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4260
|
+
if (stepTargetVisible(step, env, ctx, s.bridge))
|
|
4261
|
+
announceStep(d, suite, env);
|
|
4262
|
+
}
|
|
4183
4263
|
function markReadyIfVisible(d, suite, env) {
|
|
4184
4264
|
const s = d.state.screen;
|
|
4185
4265
|
if (s.name !== "step" || s.phase !== "waiting")
|
|
@@ -4187,7 +4267,7 @@ function markReadyIfVisible(d, suite, env) {
|
|
|
4187
4267
|
const mission = findMission(suite, s.mission);
|
|
4188
4268
|
if (!mission)
|
|
4189
4269
|
return;
|
|
4190
|
-
const step = mission.steps[s.index];
|
|
4270
|
+
const step = shownStep(mission.steps[s.index], s.nothingToSave);
|
|
4191
4271
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4192
4272
|
if (stepTargetVisible(step, env, ctx, s.bridge)) {
|
|
4193
4273
|
setScreen(d, { ...s, phase: "ready" });
|
|
@@ -4309,6 +4389,17 @@ function startMission(d, suite, env, key, replay) {
|
|
|
4309
4389
|
if (at > 0)
|
|
4310
4390
|
index = checkpointAtOrBefore(mission.steps, at);
|
|
4311
4391
|
}
|
|
4392
|
+
if (isReplay) {
|
|
4393
|
+
// What is open now is the user's: ending the replay leaves it alone.
|
|
4394
|
+
d.state = {
|
|
4395
|
+
...d.state,
|
|
4396
|
+
replayPage: {
|
|
4397
|
+
mission: key,
|
|
4398
|
+
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
4399
|
+
dialog: safe(() => env.dialogOpen?.() === true, false),
|
|
4400
|
+
},
|
|
4401
|
+
};
|
|
4402
|
+
}
|
|
4312
4403
|
enterStep(d, suite, env, mission, index, { replay: isReplay });
|
|
4313
4404
|
// A replay never writes progress; only what starting it changed (the
|
|
4314
4405
|
// welcome, first seen through a replay) is saved.
|
|
@@ -4369,7 +4460,8 @@ function pushAbort(d, suite, s) {
|
|
|
4369
4460
|
const mission = findMission(suite, s.mission);
|
|
4370
4461
|
if (!mission)
|
|
4371
4462
|
return;
|
|
4372
|
-
const
|
|
4463
|
+
const before = d.state.replayPage?.mission === s.mission ? d.state.replayPage : null;
|
|
4464
|
+
const actions = abortActionsLeft(abortActionsFor(mission, s.index), before);
|
|
4373
4465
|
if (actions.length)
|
|
4374
4466
|
d.effects.push({ type: "abort", actions });
|
|
4375
4467
|
}
|
|
@@ -4462,7 +4554,12 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4462
4554
|
}
|
|
4463
4555
|
if (s.confirmExit)
|
|
4464
4556
|
break;
|
|
4465
|
-
const
|
|
4557
|
+
const nts = !s.bridge && nothingToSaveHolds(step, env, ctx);
|
|
4558
|
+
if (nts !== s.nothingToSave) {
|
|
4559
|
+
setNothingToSave(d, suite, env, nts);
|
|
4560
|
+
break;
|
|
4561
|
+
}
|
|
4562
|
+
const kind = stepKind(shownStep(step, s.nothingToSave));
|
|
4466
4563
|
const armed = s.armed || hiddenAnchorShown(step, env);
|
|
4467
4564
|
if (kind !== "input" && kind !== "view") {
|
|
4468
4565
|
const held = syncTriggerHolds(step, env, armed);
|
|
@@ -4471,7 +4568,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4471
4568
|
break;
|
|
4472
4569
|
}
|
|
4473
4570
|
}
|
|
4474
|
-
if (!s.bridge && !stepTargetVisible(step, env, ctx, false) && offRoute(mission.steps, s.index, env)) {
|
|
4571
|
+
if (!s.bridge && !stepTargetVisible(shownStep(step, s.nothingToSave), env, ctx, false) && offRoute(mission.steps, s.index, env)) {
|
|
4475
4572
|
// The target is missing because the page is another one.
|
|
4476
4573
|
recoverOffRoute(d, suite, env, false);
|
|
4477
4574
|
break;
|
|
@@ -4542,8 +4639,9 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4542
4639
|
if (!mission)
|
|
4543
4640
|
break;
|
|
4544
4641
|
const step = mission.steps[s.index];
|
|
4545
|
-
const kind = stepKind(step);
|
|
4642
|
+
const kind = stepKind(shownStep(step, s.nothingToSave));
|
|
4546
4643
|
if (kind === "view") {
|
|
4644
|
+
// A read step, or a commit step with nothing to save.
|
|
4547
4645
|
advance(d, suite, env);
|
|
4548
4646
|
break;
|
|
4549
4647
|
}
|
|
@@ -4572,21 +4670,24 @@ function reduceGuide(state, action, suite, env) {
|
|
|
4572
4670
|
const mission = findMission(suite, s.mission);
|
|
4573
4671
|
if (!mission)
|
|
4574
4672
|
break;
|
|
4575
|
-
const
|
|
4576
|
-
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(
|
|
4577
|
-
|
|
4673
|
+
const raw = mission.steps[s.index];
|
|
4674
|
+
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(raw, env));
|
|
4675
|
+
// The save went with nothing left to save, or came back: the other form.
|
|
4676
|
+
setNothingToSave(d, suite, env, !s.bridge && nothingToSaveHolds(raw, env, ctx));
|
|
4677
|
+
const cur = d.state.screen;
|
|
4678
|
+
if (stepTargetVisible(shownStep(raw, cur.nothingToSave), env, ctx, cur.bridge))
|
|
4578
4679
|
break;
|
|
4579
|
-
if (!
|
|
4680
|
+
if (!cur.bridge && offRoute(mission.steps, cur.index, env)) {
|
|
4580
4681
|
recoverOffRoute(d, suite, env, false);
|
|
4581
4682
|
break;
|
|
4582
4683
|
}
|
|
4583
|
-
const cp = checkpointAtOrBefore(mission.steps,
|
|
4584
|
-
if (cp <
|
|
4684
|
+
const cp = checkpointAtOrBefore(mission.steps, cur.index);
|
|
4685
|
+
if (cp < cur.index && !cur.bridge) {
|
|
4585
4686
|
// A dialog or a menu closed under the step: back to what opens it.
|
|
4586
|
-
enterStep(d, suite, env, mission, cp, { replay:
|
|
4687
|
+
enterStep(d, suite, env, mission, cp, { replay: cur.replay, noAutoPass: false });
|
|
4587
4688
|
}
|
|
4588
4689
|
else {
|
|
4589
|
-
setScreen(d, { ...
|
|
4690
|
+
setScreen(d, { ...cur, phase: "waiting" });
|
|
4590
4691
|
}
|
|
4591
4692
|
break;
|
|
4592
4693
|
}
|
|
@@ -5178,6 +5279,12 @@ function coveredByDialog(el) {
|
|
|
5178
5279
|
}
|
|
5179
5280
|
return false;
|
|
5180
5281
|
}
|
|
5282
|
+
/** An app dialog (not the tour's) is open on the page. */
|
|
5283
|
+
function appDialogOpen() {
|
|
5284
|
+
if (!hasDocument())
|
|
5285
|
+
return false;
|
|
5286
|
+
return Array.from(document.querySelectorAll(OPEN_DIALOG_SELECTOR)).some(d => !inGuideLayer(d) && isPresent(d));
|
|
5287
|
+
}
|
|
5181
5288
|
function openPoppers() {
|
|
5182
5289
|
if (!hasDocument())
|
|
5183
5290
|
return [];
|
|
@@ -6216,6 +6323,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6216
6323
|
isVisible: (anchor, id) => anchorVisible(anchor, id),
|
|
6217
6324
|
sidebarOpen: pg.sidebarOpen,
|
|
6218
6325
|
targetValue: () => fieldValue(targetElRef.current),
|
|
6326
|
+
dialogOpen: () => appDialogOpen(),
|
|
6219
6327
|
};
|
|
6220
6328
|
}, [code]);
|
|
6221
6329
|
/* ---------------- effects: save, announce, toast, navigate ---------------- */
|
|
@@ -6392,11 +6500,13 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6392
6500
|
const mission = findMission(suite, screen.mission);
|
|
6393
6501
|
if (!mission)
|
|
6394
6502
|
return null;
|
|
6395
|
-
const
|
|
6396
|
-
if (!
|
|
6503
|
+
const raw = mission.steps[screen.index];
|
|
6504
|
+
if (!raw)
|
|
6397
6505
|
return null;
|
|
6506
|
+
// A commit step with nothing to save is shown as its read form.
|
|
6507
|
+
const step = shownStep(raw, screen.nothingToSave);
|
|
6398
6508
|
const missionNo = suite.missions.indexOf(mission) + 1;
|
|
6399
|
-
return { mission, step, missionNo };
|
|
6509
|
+
return { mission, step, raw, missionNo };
|
|
6400
6510
|
}, [state, screen, suite]);
|
|
6401
6511
|
const stepCtx = React.useMemo(() => {
|
|
6402
6512
|
if (!state)
|
|
@@ -6412,7 +6522,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6412
6522
|
: null;
|
|
6413
6523
|
const targetKey = target ? `${target.anchor}|${target.id ?? ""}` : "";
|
|
6414
6524
|
const phase = screen.name === "step" ? screen.phase : null;
|
|
6415
|
-
const stepId = screen.name === "step" ? `${screen.mission}:${screen.index}:${screen.bridge ? "b" : ""}` : "";
|
|
6525
|
+
const stepId = screen.name === "step" ? `${screen.mission}:${screen.index}:${screen.bridge ? "b" : ""}${screen.nothingToSave ? "n" : ""}` : "";
|
|
6416
6526
|
/* Measure every frame while a step is shown: the spotlight follows
|
|
6417
6527
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
6418
6528
|
const [layout, setLayout] = React.useState({
|
|
@@ -6575,7 +6685,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6575
6685
|
dispatchRef.current({ type: "signal", signal: { kind: "api", method: d.method, path: d.path, status: d.status, id: d.id, message: d.message } });
|
|
6576
6686
|
};
|
|
6577
6687
|
window.addEventListener(API_EVENT, onApi);
|
|
6578
|
-
const names = triggersOf(stepInfo.
|
|
6688
|
+
const names = triggersOf(stepInfo.raw).flatMap(t => ("event" in t ? [t.event] : []));
|
|
6579
6689
|
const onApp = (name) => (e) => dispatchRef.current({ type: "signal", signal: { kind: "event", name, detail: e.detail ?? undefined } });
|
|
6580
6690
|
const appHandlers = names.map(n => [GUIDE_EVENT_PREFIX + n, onApp(n)]);
|
|
6581
6691
|
appHandlers.forEach(([n, h]) => window.addEventListener(n, h));
|
|
@@ -6807,21 +6917,18 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6807
6917
|
React.useEffect(() => {
|
|
6808
6918
|
const el = document.createElement("div");
|
|
6809
6919
|
el.setAttribute(GUIDE_LAYER_ATTRIBUTE, "");
|
|
6810
|
-
// A Radix modal (dialog, select, menu) hides everything outside it from
|
|
6811
|
-
// screen readers with aria-hidden's hideOthers. It leaves [aria-live]
|
|
6812
|
-
// elements and their insides alone, but walks into their ancestors and
|
|
6813
|
-
// hides the siblings: with only the status line live, the bubble and its
|
|
6814
|
-
// buttons were hidden. "off" announces nothing; it only makes the whole
|
|
6815
|
-
// layer one of the kept elements.
|
|
6816
|
-
el.setAttribute("aria-live", "off");
|
|
6817
6920
|
document.body.appendChild(el);
|
|
6818
6921
|
layerRef.current = el;
|
|
6819
6922
|
setLayer(el);
|
|
6820
|
-
//
|
|
6821
|
-
//
|
|
6822
|
-
//
|
|
6823
|
-
//
|
|
6824
|
-
//
|
|
6923
|
+
// A Radix modal (dialog, select, menu) hides everything outside it from
|
|
6924
|
+
// screen readers with aria-hidden's hideOthers. It keeps [aria-live]
|
|
6925
|
+
// elements but walks into their ancestors and hides the other children:
|
|
6926
|
+
// with the status line live, the bubble and its buttons were hidden.
|
|
6927
|
+
// Take off what it adds, at once. hideOthers marks what it hides with
|
|
6928
|
+
// data-aria-hidden (the layer never sets that, or inert); aria-hidden goes
|
|
6929
|
+
// only where it was just added, so the layer's own decorative aria-hidden
|
|
6930
|
+
// stays. (aria-live="off" on the layer would make hideOthers skip it, but
|
|
6931
|
+
// it changed what test tools match by text.)
|
|
6825
6932
|
const obs = new MutationObserver(records => {
|
|
6826
6933
|
for (const r of records) {
|
|
6827
6934
|
const n = r.target;
|