@eintrek/erp-shell 0.1.66 → 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/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.66";
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 actions = abortActionsFor(mission, s.index);
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 kind = stepKind(step);
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 step = mission.steps[s.index];
4576
- const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
4577
- if (stepTargetVisible(step, env, ctx, s.bridge))
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 (!s.bridge && offRoute(mission.steps, s.index, env)) {
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, s.index);
4584
- if (cp < s.index && !s.bridge) {
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: s.replay, noAutoPass: false });
4687
+ enterStep(d, suite, env, mission, cp, { replay: cur.replay, noAutoPass: false });
4587
4688
  }
4588
4689
  else {
4589
- setScreen(d, { ...s, phase: "waiting" });
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 step = mission.steps[screen.index];
6396
- if (!step)
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.step).flatMap(t => ("event" in t ? [t.event] : []));
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));