@eintrek/erp-shell 0.1.76 → 0.1.78
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/README.md +44 -0
- package/dist/chunks/{format-BVoVc7Yk.cjs → format-BmTSD3DB.cjs} +439 -70
- package/dist/chunks/format-BmTSD3DB.cjs.map +1 -0
- package/dist/chunks/{format-BWUKNKxg.esm.js → format-Wgj2sKpK.esm.js} +439 -70
- package/dist/chunks/format-Wgj2sKpK.esm.js.map +1 -0
- package/dist/guide/guide-ui.d.ts +6 -0
- package/dist/guide/machine.d.ts +54 -0
- package/dist/guide/progress-sync.d.ts +6 -1
- package/dist/guide/types.d.ts +6 -0
- package/dist/guide-core.cjs +1 -1
- package/dist/guide-core.esm.js +1 -1
- package/dist/index.cjs +4 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +5 -4
- package/dist/index.esm.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunks/format-BVoVc7Yk.cjs.map +0 -1
- package/dist/chunks/format-BWUKNKxg.esm.js.map +0 -1
|
@@ -1012,6 +1012,7 @@ function initialGuideState(audience, progress, pages) {
|
|
|
1012
1012
|
pageRun: null,
|
|
1013
1013
|
panelSection: "all",
|
|
1014
1014
|
autoPassed: null,
|
|
1015
|
+
counter: null,
|
|
1015
1016
|
audience,
|
|
1016
1017
|
progress,
|
|
1017
1018
|
screen: { name: "idle" },
|
|
@@ -1390,7 +1391,7 @@ function announceStep(d, suite, env) {
|
|
|
1390
1391
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
1391
1392
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
1392
1393
|
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
1393
|
-
const pos = stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
|
|
1394
|
+
const pos = s.pos ?? stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
|
|
1394
1395
|
d.effects.push({
|
|
1395
1396
|
type: "announce",
|
|
1396
1397
|
text: `ขั้นที่ ${pos.no} จาก ${pos.total}. ${title}${why ? `. ${why}` : ""}`,
|
|
@@ -1424,6 +1425,54 @@ function stepPosition(mission, index, env, context, autoPassed = []) {
|
|
|
1424
1425
|
});
|
|
1425
1426
|
return { no: Math.max(no, 1), total: Math.max(total, 1) };
|
|
1426
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* The steps after `index` this person will see, as far as can be known from
|
|
1430
|
+
* here: on this page, everything is read (requires, skipIf, a click whose
|
|
1431
|
+
* outcome already holds); on another page, a step whose showing depends on
|
|
1432
|
+
* what is on that page (`skipIf`, `onMissing: "skip"`) is not counted until
|
|
1433
|
+
* its page is reached.
|
|
1434
|
+
*/
|
|
1435
|
+
function stepsAhead(mission, index, env, context) {
|
|
1436
|
+
let n = 0;
|
|
1437
|
+
for (let j = index + 1; j < mission.steps.length; j++) {
|
|
1438
|
+
const step = mission.steps[j];
|
|
1439
|
+
if (step.requires && !safe(() => env.can(step.requires), true))
|
|
1440
|
+
continue;
|
|
1441
|
+
const home = stepHomeRoute(mission.steps, j);
|
|
1442
|
+
const here = !home || !!matchRoute(home, env.pathname, env.search, env.code);
|
|
1443
|
+
if (here) {
|
|
1444
|
+
const ctx = guideContext(env, context, routeParamsFor(step, env));
|
|
1445
|
+
if (step.skipIf && safe(() => step.skipIf(ctx), false))
|
|
1446
|
+
continue;
|
|
1447
|
+
if (wouldAutoPass(step, env))
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
else if (step.skipIf || step.onMissing === "skip") {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
n++;
|
|
1454
|
+
}
|
|
1455
|
+
return n;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* The counter as shown when entering step `index` (shell ≥ 0.1.77). `no`
|
|
1459
|
+
* counts the steps actually shown in this run up to this one. The total is
|
|
1460
|
+
* worked out on entering a step on a new page — the shown steps so far plus
|
|
1461
|
+
* stepsAhead — and kept while the run stays on that page; it never goes
|
|
1462
|
+
* down (the larger of the old and the new), and is never below `no`.
|
|
1463
|
+
*/
|
|
1464
|
+
function countStep(d, env, mission, index) {
|
|
1465
|
+
const prev = d.state.counter?.mission === mission.key ? d.state.counter : null;
|
|
1466
|
+
const shown = prev ? (prev.shown.includes(index) ? prev.shown : [...prev.shown, index]) : [index];
|
|
1467
|
+
const no = Math.max(1, shown.filter(i => i <= index).length);
|
|
1468
|
+
let total;
|
|
1469
|
+
if (prev && prev.path === env.pathname)
|
|
1470
|
+
total = Math.max(prev.total, no);
|
|
1471
|
+
else
|
|
1472
|
+
total = Math.max(prev?.total ?? 0, no + stepsAhead(mission, index, env, d.state.progress.context), no);
|
|
1473
|
+
d.state = { ...d.state, counter: { mission: mission.key, shown, total, path: env.pathname } };
|
|
1474
|
+
return { no, total };
|
|
1475
|
+
}
|
|
1427
1476
|
/** A step entered now would pass at once: a click or select step whose route or visible outcome holds. */
|
|
1428
1477
|
function wouldAutoPass(step, env) {
|
|
1429
1478
|
const kind = stepKind(step);
|
|
@@ -1493,6 +1542,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1493
1542
|
const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
|
|
1494
1543
|
const bridge = !away && needsBridge(step, env, ctx);
|
|
1495
1544
|
const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
|
|
1545
|
+
const pos = countStep(d, env, mission, i);
|
|
1496
1546
|
setScreen(d, {
|
|
1497
1547
|
name: "step",
|
|
1498
1548
|
mission: mission.key,
|
|
@@ -1510,6 +1560,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1510
1560
|
...(page ? { page } : {}),
|
|
1511
1561
|
...(section ? { section } : {}),
|
|
1512
1562
|
...(flow ? { flow } : {}),
|
|
1563
|
+
pos,
|
|
1513
1564
|
});
|
|
1514
1565
|
if (!opts.replay) {
|
|
1515
1566
|
const cp = checkpointAtOrBefore(steps, i);
|
|
@@ -1784,7 +1835,7 @@ function startMission(d, suite, env, key, replay) {
|
|
|
1784
1835
|
const isReplay = replay || entry?.status === "done";
|
|
1785
1836
|
const before = d.state.progress;
|
|
1786
1837
|
// A new run: what passed by itself is counted afresh.
|
|
1787
|
-
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null };
|
|
1838
|
+
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null, counter: null };
|
|
1788
1839
|
if (d.state.progress.welcome === "not_seen")
|
|
1789
1840
|
withProgress(d, { welcome: "started" });
|
|
1790
1841
|
if (!isReplay && d.state.progress.mission_key !== key)
|
|
@@ -1934,24 +1985,33 @@ function pageStillHere(suite, key, env) {
|
|
|
1934
1985
|
const page = findPage(suite, key);
|
|
1935
1986
|
return !!page && pageMatchScore(page, whereOf(env)) !== null;
|
|
1936
1987
|
}
|
|
1937
|
-
/**
|
|
1988
|
+
/**
|
|
1989
|
+
* Whether a row may be written now: none of this version yet, or — once —
|
|
1990
|
+
* a skipped one that the person has now finished (skipped → completed,
|
|
1991
|
+
* shell ≥ 0.1.77). A completed row is final; skipped again changes nothing.
|
|
1992
|
+
*/
|
|
1993
|
+
function rowWritable(row, version, status) {
|
|
1994
|
+
if (!row || row.version < version)
|
|
1995
|
+
return true;
|
|
1996
|
+
return row.version === version && row.status === "skipped" && status === "completed";
|
|
1997
|
+
}
|
|
1998
|
+
/** Whether a run should keep its row to write at its end: no row yet, or only a skipped one. */
|
|
1999
|
+
function rowStillDue(row, version) {
|
|
2000
|
+
return rowWritable(row, version, "completed");
|
|
2001
|
+
}
|
|
2002
|
+
/** Write a page guide's row (once per page and version; skipped → completed once): recorded here at once, saved by the host. */
|
|
1938
2003
|
function recordPage(d, suite, key, status) {
|
|
1939
2004
|
const page = findPage(suite, key);
|
|
1940
|
-
if (!page ||
|
|
2005
|
+
if (!page || !rowWritable(d.state.pages[key], page.version, status))
|
|
1941
2006
|
return;
|
|
1942
2007
|
const prev = d.state.pages[key];
|
|
1943
2008
|
d.state = { ...d.state, pages: { ...d.state.pages, [key]: { version: page.version, status, rev: prev?.rev ?? 0 } } };
|
|
1944
2009
|
d.effects.push({ type: "savePage", key, version: page.version, status });
|
|
1945
2010
|
}
|
|
1946
|
-
/** The section guide's row says this person has ended this version of it. */
|
|
1947
|
-
function sectionRecorded(state, section) {
|
|
1948
|
-
const row = state.sections?.[section.key];
|
|
1949
|
-
return !!row && row.version >= section.version;
|
|
1950
|
-
}
|
|
1951
2011
|
/** Write a section guide's row (once per section and version). */
|
|
1952
2012
|
function recordSection(d, suite, key, status) {
|
|
1953
2013
|
const section = findSection(suite, key);
|
|
1954
|
-
if (!section ||
|
|
2014
|
+
if (!section || !rowWritable(d.state.sections?.[key], section.version, status))
|
|
1955
2015
|
return;
|
|
1956
2016
|
const prev = d.state.sections?.[key];
|
|
1957
2017
|
d.state = { ...d.state, sections: { ...d.state.sections, [key]: { version: section.version, status, rev: prev?.rev ?? 0 } } };
|
|
@@ -1982,7 +2042,7 @@ function flowRecorded(state, flow) {
|
|
|
1982
2042
|
/** Write a flow's row (once per flow and version). */
|
|
1983
2043
|
function recordFlow(d, suite, key, status) {
|
|
1984
2044
|
const flow = findFlow(suite, key);
|
|
1985
|
-
if (!flow ||
|
|
2045
|
+
if (!flow || !rowWritable(d.state.flows?.[key], flow.version, status))
|
|
1986
2046
|
return;
|
|
1987
2047
|
const prev = d.state.flows?.[key];
|
|
1988
2048
|
d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
|
|
@@ -2067,6 +2127,7 @@ function startFlow(d, suite, env, key, write, at) {
|
|
|
2067
2127
|
justDone: null,
|
|
2068
2128
|
pageRun: { key, write, kind: "flow", context: saved },
|
|
2069
2129
|
autoPassed: null,
|
|
2130
|
+
counter: null,
|
|
2070
2131
|
replayPage: {
|
|
2071
2132
|
mission: mission.key,
|
|
2072
2133
|
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
@@ -2097,6 +2158,7 @@ function startSection(d, suite, env, key, write) {
|
|
|
2097
2158
|
justDone: null,
|
|
2098
2159
|
pageRun: { key, write, kind: "section" },
|
|
2099
2160
|
autoPassed: null,
|
|
2161
|
+
counter: null,
|
|
2100
2162
|
replayPage: {
|
|
2101
2163
|
mission: mission.key,
|
|
2102
2164
|
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
@@ -2116,6 +2178,7 @@ function startPage(d, suite, env, key, write) {
|
|
|
2116
2178
|
justDone: null,
|
|
2117
2179
|
pageRun: { key, write, kind: "page" },
|
|
2118
2180
|
autoPassed: null,
|
|
2181
|
+
counter: null,
|
|
2119
2182
|
// What is open now is the person's: ending the guide leaves it alone.
|
|
2120
2183
|
replayPage: {
|
|
2121
2184
|
mission: mission.key,
|
|
@@ -2159,6 +2222,28 @@ function backTarget(mission, index, env, context) {
|
|
|
2159
2222
|
}
|
|
2160
2223
|
return prev >= 0 ? { index: prev, close: false } : null;
|
|
2161
2224
|
}
|
|
2225
|
+
/**
|
|
2226
|
+
* A flow's next step (the next one this person sees) when the person is on
|
|
2227
|
+
* its page and no longer on the current step's: they moved on by
|
|
2228
|
+
* themselves. -1 otherwise (still on this step's page, or elsewhere).
|
|
2229
|
+
*/
|
|
2230
|
+
function flowStepArrivedAt(mission, index, env, context) {
|
|
2231
|
+
const here = (route) => !!route && !!matchRoute(route, env.pathname, env.search, env.code);
|
|
2232
|
+
const curHome = stepHomeRoute(mission.steps, index);
|
|
2233
|
+
if (!curHome || here(curHome))
|
|
2234
|
+
return -1;
|
|
2235
|
+
for (let j = index + 1; j < mission.steps.length; j++) {
|
|
2236
|
+
const st = mission.steps[j];
|
|
2237
|
+
const ctx = guideContext(env, context, routeParamsFor(st, env));
|
|
2238
|
+
if (st.requires && !safe(() => env.can(st.requires), true))
|
|
2239
|
+
continue;
|
|
2240
|
+
if (st.skipIf && safe(() => st.skipIf(ctx), false))
|
|
2241
|
+
continue;
|
|
2242
|
+
const where = st.route ?? stepHomeRoute(mission.steps, j);
|
|
2243
|
+
return here(where) ? j : -1;
|
|
2244
|
+
}
|
|
2245
|
+
return -1;
|
|
2246
|
+
}
|
|
2162
2247
|
function reduceGuide(state, action, suite, env) {
|
|
2163
2248
|
const d = { state, effects: [] };
|
|
2164
2249
|
const s = state.screen;
|
|
@@ -2236,7 +2321,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2236
2321
|
const stepAt = mission.steps.findIndex(st => st.key === d.state.progress.step_key);
|
|
2237
2322
|
const cpAt = mission.steps.findIndex(st => st.key === d.state.progress.checkpoint_key);
|
|
2238
2323
|
const index = stepAt >= 0 ? checkpointAtOrBefore(mission.steps, stepAt) : Math.max(cpAt, 0);
|
|
2239
|
-
d.state = { ...d.state, autoPassed: null };
|
|
2324
|
+
d.state = { ...d.state, autoPassed: null, counter: null };
|
|
2240
2325
|
enterStep(d, suite, env, mission, index, { replay: false });
|
|
2241
2326
|
break;
|
|
2242
2327
|
}
|
|
@@ -2279,6 +2364,15 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2279
2364
|
break;
|
|
2280
2365
|
const step = mission.steps[s.index];
|
|
2281
2366
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
2367
|
+
if (s.flow && !s.confirmExit) {
|
|
2368
|
+
// Within the chain, the person went on to the next step's page by
|
|
2369
|
+
// themselves: carry on there, no "ทำต่อ" (shell ≥ 0.1.77).
|
|
2370
|
+
const next = flowStepArrivedAt(mission, s.index, env, d.state.progress.context);
|
|
2371
|
+
if (next >= 0) {
|
|
2372
|
+
enterStep(d, suite, env, mission, next, { replay: s.replay });
|
|
2373
|
+
break;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2282
2376
|
if (s.phase === "away") {
|
|
2283
2377
|
const where = placeRoute(mission.steps, s.index);
|
|
2284
2378
|
if (!where || matchRoute(where, env.pathname, env.search, env.code)) {
|
|
@@ -2673,7 +2767,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2673
2767
|
if (!flowsAvailable(suite, env).includes(action.flow))
|
|
2674
2768
|
break;
|
|
2675
2769
|
const flow = findFlow(suite, action.flow);
|
|
2676
|
-
startFlow(d, suite, env, flow.key,
|
|
2770
|
+
startFlow(d, suite, env, flow.key, rowStillDue(state.flows?.[flow.key], flow.version));
|
|
2677
2771
|
break;
|
|
2678
2772
|
}
|
|
2679
2773
|
case "resumeFlow": {
|
|
@@ -2682,7 +2776,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2682
2776
|
const flow = findFlow(suite, action.flow);
|
|
2683
2777
|
if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
|
|
2684
2778
|
break;
|
|
2685
|
-
startFlow(d, suite, env, flow.key, action.write &&
|
|
2779
|
+
startFlow(d, suite, env, flow.key, action.write && rowStillDue(state.flows?.[flow.key], flow.version), { step: action.step, context: action.context });
|
|
2686
2780
|
break;
|
|
2687
2781
|
}
|
|
2688
2782
|
case "startPage": {
|
|
@@ -2694,7 +2788,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2694
2788
|
const page = pageHere(suite, env);
|
|
2695
2789
|
if (!page || page.key !== action.page)
|
|
2696
2790
|
break;
|
|
2697
|
-
startPage(d, suite, env, page.key,
|
|
2791
|
+
startPage(d, suite, env, page.key, rowStillDue(state.pages[page.key], page.version));
|
|
2698
2792
|
break;
|
|
2699
2793
|
}
|
|
2700
2794
|
case "back": {
|
|
@@ -2733,6 +2827,37 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2733
2827
|
d.state = { ...d.state, pages: { ...d.state.pages, [action.key]: action.row } };
|
|
2734
2828
|
break;
|
|
2735
2829
|
}
|
|
2830
|
+
case "yield": {
|
|
2831
|
+
const run = d.state.pageRun;
|
|
2832
|
+
if (run?.kind === "flow" && run.context)
|
|
2833
|
+
withProgress(d, { context: run.context });
|
|
2834
|
+
d.state = { ...d.state, pageRun: null, replayPage: null, panelOpen: false, justDone: null, pendingCelebrate: [] };
|
|
2835
|
+
setScreen(d, { name: "idle" });
|
|
2836
|
+
break;
|
|
2837
|
+
}
|
|
2838
|
+
case "refresh": {
|
|
2839
|
+
const p = action.progress;
|
|
2840
|
+
const local = d.state.progress;
|
|
2841
|
+
// The server's row when it is this version and not behind this tab's.
|
|
2842
|
+
if (p.guide_version === local.guide_version && p.rev >= local.rev)
|
|
2843
|
+
d.state = { ...d.state, progress: p };
|
|
2844
|
+
const newer = (server, mine) => {
|
|
2845
|
+
const out = { ...(mine ?? {}) };
|
|
2846
|
+
for (const [k, r] of Object.entries(server ?? {})) {
|
|
2847
|
+
const m = out[k];
|
|
2848
|
+
if (!m || r.version > m.version || (r.version === m.version && r.rev >= m.rev))
|
|
2849
|
+
out[k] = r;
|
|
2850
|
+
}
|
|
2851
|
+
return out;
|
|
2852
|
+
};
|
|
2853
|
+
d.state = {
|
|
2854
|
+
...d.state,
|
|
2855
|
+
pages: newer(action.pages, d.state.pages),
|
|
2856
|
+
sections: newer(action.sections, d.state.sections),
|
|
2857
|
+
flows: newer(action.flows, d.state.flows),
|
|
2858
|
+
};
|
|
2859
|
+
break;
|
|
2860
|
+
}
|
|
2736
2861
|
case "startSection": {
|
|
2737
2862
|
// Never offered: only on request. Never over a mission step, a
|
|
2738
2863
|
// welcome, a gate or a celebration; only on screen, only theirs.
|
|
@@ -2743,7 +2868,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2743
2868
|
if (!sectionsAvailable(suite, env).includes(action.section))
|
|
2744
2869
|
break;
|
|
2745
2870
|
const section = findSection(suite, action.section);
|
|
2746
|
-
startSection(d, suite, env, section.key,
|
|
2871
|
+
startSection(d, suite, env, section.key, rowStillDue(state.sections?.[section.key], section.version));
|
|
2747
2872
|
break;
|
|
2748
2873
|
}
|
|
2749
2874
|
case "idleCelebrate": {
|
|
@@ -3102,9 +3227,9 @@ function conflictOf(err) {
|
|
|
3102
3227
|
return null;
|
|
3103
3228
|
}
|
|
3104
3229
|
/** The GET, parsed for this suite; null (show nothing) on any failure or odd answer. */
|
|
3105
|
-
async function loadGuides(adapter, suite, signal) {
|
|
3230
|
+
async function loadGuides(adapter, suite, signal, fresh = false) {
|
|
3106
3231
|
try {
|
|
3107
|
-
const raw = await adapter.fetchProgress({ app: suite.app, guideKey: suite.key, signal });
|
|
3232
|
+
const raw = await adapter.fetchProgress({ app: suite.app, guideKey: suite.key, signal, ...(fresh ? { fresh: true } : {}) });
|
|
3108
3233
|
return parseGuidesAnswer(raw, suite);
|
|
3109
3234
|
}
|
|
3110
3235
|
catch {
|
|
@@ -3189,7 +3314,10 @@ async function storePageRow(adapter, app, key, version, status, rev, kind = "pag
|
|
|
3189
3314
|
}
|
|
3190
3315
|
if (current.version > version)
|
|
3191
3316
|
return null;
|
|
3192
|
-
|
|
3317
|
+
// This version already recorded: kept, except a skipped row this run
|
|
3318
|
+
// finished (skipped → completed, once, shell ≥ 0.1.77).
|
|
3319
|
+
const finishing = status === "completed" && current.status === "skipped";
|
|
3320
|
+
if (current.version === version && current.status !== "in_progress" && !finishing)
|
|
3193
3321
|
return current;
|
|
3194
3322
|
at = current.rev;
|
|
3195
3323
|
}
|
|
@@ -3856,6 +3984,9 @@ function useNudge(nudge, ref) {
|
|
|
3856
3984
|
}
|
|
3857
3985
|
function StepLayer(p) {
|
|
3858
3986
|
const { spot, phone, viewport } = p;
|
|
3987
|
+
// Where the bubble may sit: the content column when the app has one.
|
|
3988
|
+
const col = p.bounds && p.bounds.width > 0 ? p.bounds : { x: 0, width: viewport.width || (typeof window !== "undefined" ? window.innerWidth : 0)};
|
|
3989
|
+
const colKey = `${col.x}|${col.width}`;
|
|
3859
3990
|
const bubbleRef = React.useRef(null);
|
|
3860
3991
|
const arrowRef = React.useRef(null);
|
|
3861
3992
|
const ringRef = React.useRef(null);
|
|
@@ -3896,8 +4027,8 @@ function StepLayer(p) {
|
|
|
3896
4027
|
const bw = el.offsetWidth;
|
|
3897
4028
|
const bh = el.offsetHeight;
|
|
3898
4029
|
setPos(phone
|
|
3899
|
-
? { left: m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
3900
|
-
: { left: (
|
|
4030
|
+
? { left: col.x + m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
4031
|
+
: { left: col.x + (col.width - bw) / 2, top: viewport.offsetTop + (viewport.height - bh) / 2, side: "dock" });
|
|
3901
4032
|
return;
|
|
3902
4033
|
}
|
|
3903
4034
|
if (phone) {
|
|
@@ -3918,7 +4049,7 @@ function StepLayer(p) {
|
|
|
3918
4049
|
: overlapBot <= overlapTop
|
|
3919
4050
|
? botT
|
|
3920
4051
|
: topT;
|
|
3921
|
-
setPos({ left: m, top, side: "dock" });
|
|
4052
|
+
setPos({ left: col.x + m, top, side: "dock" });
|
|
3922
4053
|
return;
|
|
3923
4054
|
}
|
|
3924
4055
|
let cancelled = false;
|
|
@@ -3940,7 +4071,7 @@ function StepLayer(p) {
|
|
|
3940
4071
|
placement,
|
|
3941
4072
|
middleware: [
|
|
3942
4073
|
dom.offset(placement.startsWith("bottom") ? gap : 14),
|
|
3943
|
-
dom.shift({ padding: m }),
|
|
4074
|
+
dom.shift({ padding: m, rootBoundary: { x: col.x, y: 0, width: col.width, height: window.innerHeight } }),
|
|
3944
4075
|
...(arrowRef.current ? [dom.arrow({ element: arrowRef.current, padding: 14 })] : []),
|
|
3945
4076
|
],
|
|
3946
4077
|
});
|
|
@@ -3950,11 +4081,11 @@ function StepLayer(p) {
|
|
|
3950
4081
|
return;
|
|
3951
4082
|
const bw = el.offsetWidth;
|
|
3952
4083
|
const bh = el.offsetHeight;
|
|
3953
|
-
const bounds = { x: m, y: viewport.offsetTop + m, width:
|
|
4084
|
+
const bounds = { x: col.x + m, y: viewport.offsetTop + m, width: col.width - 2 * m, height: viewport.height - 2 * m };
|
|
3954
4085
|
const i = pickPlacement(results, bw, bh, a, clear, bounds);
|
|
3955
4086
|
if (i < 0) {
|
|
3956
4087
|
// Nowhere beside it: the bottom-right corner.
|
|
3957
|
-
setPos({ left:
|
|
4088
|
+
setPos({ left: col.x + col.width - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
|
|
3958
4089
|
return;
|
|
3959
4090
|
}
|
|
3960
4091
|
const { x, y, placement, middlewareData } = results[i];
|
|
@@ -3970,7 +4101,7 @@ function StepLayer(p) {
|
|
|
3970
4101
|
};
|
|
3971
4102
|
// The keys stand for spot, the lists, the holes and the bubble's size.
|
|
3972
4103
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3973
|
-
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
4104
|
+
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop, colKey]);
|
|
3974
4105
|
const popovers = p.popovers ?? [];
|
|
3975
4106
|
const covered = !!p.covered;
|
|
3976
4107
|
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
@@ -3991,7 +4122,7 @@ function StepLayer(p) {
|
|
|
3991
4122
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Scrim, { holes: p.phase === "ready" ? holes : [], open: p.phase === "ready" ? openHoles : [], width: viewport.width, height: Math.max(viewport.height + viewport.offsetTop, 0) }), spot && ringShown ? (jsxRuntime.jsx("div", { ref: ringRef, className: "eg-ring" + (p.moving ? " eg-move" : ""), style: { left: spot.x, top: spot.y, width: spot.width, height: spot.height }, "aria-hidden": "true" })) : null, hand ? (jsxRuntime.jsx("div", { className: "eg-hand" + (hand.flip ? " eg-flip" : ""), style: { left: hand.left, top: hand.top }, "aria-hidden": "true", children: jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { d: HAND_PATH }) }) })) : null, showBubble ? (jsxRuntime.jsxs("div", { ref: bubbleRef, className: "eg-bubble" + (p.bubble.error ? " eg-err" : ""), role: "dialog", "aria-modal": "false", "aria-labelledby": BUBBLE_TITLE_ID, "aria-describedby": BUBBLE_WHY_ID, tabIndex: -1, "data-eg-bubble": "", style: {
|
|
3992
4123
|
left: pos?.left ?? 0,
|
|
3993
4124
|
top: pos?.top ?? 0,
|
|
3994
|
-
width: phone ?
|
|
4125
|
+
width: phone ? col.width - 32 : col.width < 352 ? col.width - 32 : undefined,
|
|
3995
4126
|
visibility: pos ? "visible" : "hidden",
|
|
3996
4127
|
}, children: [jsxRuntime.jsx(BubbleBody, { ...p }), jsxRuntime.jsx("span", { ref: arrowRef, className: "eg-arrow" + (pos?.side === "bottom" ? " eg-top" : ""), style: arrowStyle ?? { display: "none" }, "aria-hidden": "true" })] })) : null, p.phase === "waiting" ? (jsxRuntime.jsxs("div", { className: "eg-card", role: "status", "data-eg-card": "", children: [jsxRuntime.jsxs("div", { className: "eg-loading", children: [jsxRuntime.jsx("span", { className: "eg-spin", "aria-hidden": "true" }), "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14\u2026"] }), jsxRuntime.jsx("div", { className: "eg-row", children: jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }) })] })) : null, p.phase === "missing" ? (jsxRuntime.jsxs("div", { className: "eg-card eg-err", role: "alertdialog", "aria-labelledby": "eg-missing-t", "data-eg-card": "", tabIndex: -1, children: [jsxRuntime.jsx("p", { className: "eg-b-t", id: "eg-missing-t", children: "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E2A\u0E48\u0E27\u0E19\u0E19\u0E35\u0E49\u0E1A\u0E19\u0E2B\u0E19\u0E49\u0E32" }), jsxRuntime.jsxs("div", { className: "eg-row", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onRetry, children: "\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07" })] })] })) : null, p.phase === "away" ? (jsxRuntime.jsxs("div", { className: "eg-card", role: "dialog", "aria-modal": "false", "aria-labelledby": "eg-away-t", "data-eg-card": "", tabIndex: -1, children: [jsxRuntime.jsxs("div", { className: "eg-b-top", children: [jsxRuntime.jsx("span", { className: "eg-mis", children: headingOf(p.bubble) }), jsxRuntime.jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", p.bubble.index + 1, "/", p.bubble.total] })] }), jsxRuntime.jsx("p", { className: "eg-b-t", id: "eg-away-t", style: { marginTop: 8 }, children: jsxRuntime.jsx(GuideTitle, { text: p.bubble.title }) }), jsxRuntime.jsxs("div", { className: "eg-row", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onGoResume, children: "\u0E17\u0E33\u0E15\u0E48\u0E2D" })] })] })) : null, p.confirmExit ? (jsxRuntime.jsx(ConfirmModal, { title: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E44\u0E01\u0E14\u0E4C\u0E15\u0E2D\u0E19\u0E19\u0E35\u0E49?", line: p.reopenHint || REOPEN_HINT, leaveLabel: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E44\u0E01\u0E14\u0E4C", stayLabel: "\u0E17\u0E33\u0E15\u0E48\u0E2D", onLeave: p.onExitConfirm, onStay: p.onExitCancel })) : null] }));
|
|
3997
4128
|
}
|
|
@@ -4297,36 +4428,81 @@ const flowMirrorKey = (orgId, suite) => `erp-guide-flow-run:${orgId}:${suite.app
|
|
|
4297
4428
|
/* One tab at a time */
|
|
4298
4429
|
/* ------------------------------------------------------------------ */
|
|
4299
4430
|
/**
|
|
4300
|
-
* Whether this tab runs the tour
|
|
4301
|
-
*
|
|
4431
|
+
* Whether this tab runs the tour: one tab at a time holds a Web Lock and
|
|
4432
|
+
* runs steps, offers, the welcome and saves. Every other tab still reads
|
|
4433
|
+
* progress and shows the "?" menu (shell ≥ 0.1.78); pressing one of its
|
|
4434
|
+
* guide items calls `takeover`, which steals the lock. The tab it was
|
|
4435
|
+
* stolen from goes back to waiting its turn (and yields what it was running).
|
|
4436
|
+
* Without Web Locks every tab runs, as before.
|
|
4302
4437
|
*/
|
|
4303
4438
|
function usePrimaryTab(name) {
|
|
4304
4439
|
const [primary, setPrimary] = React.useState(false);
|
|
4440
|
+
const primaryRef = React.useRef(false);
|
|
4441
|
+
const takeoverRef = React.useRef(() => Promise.resolve());
|
|
4305
4442
|
React.useEffect(() => {
|
|
4306
4443
|
const locks = (typeof navigator !== "undefined" ? navigator.locks : undefined);
|
|
4444
|
+
const set = (v) => {
|
|
4445
|
+
primaryRef.current = v;
|
|
4446
|
+
setPrimary(v);
|
|
4447
|
+
};
|
|
4307
4448
|
if (!locks?.request) {
|
|
4308
|
-
|
|
4449
|
+
set(true);
|
|
4450
|
+
takeoverRef.current = () => Promise.resolve();
|
|
4309
4451
|
return;
|
|
4310
4452
|
}
|
|
4311
|
-
|
|
4453
|
+
let alive = true;
|
|
4454
|
+
let gen = 0;
|
|
4455
|
+
let pending = null;
|
|
4312
4456
|
let release = null;
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4457
|
+
let waiters = [];
|
|
4458
|
+
const queue = (steal) => {
|
|
4459
|
+
const my = ++gen;
|
|
4460
|
+
const ac = new AbortController();
|
|
4461
|
+
pending = steal ? null : ac;
|
|
4462
|
+
locks
|
|
4463
|
+
.request(name, steal ? { steal: true } : { signal: ac.signal }, () => {
|
|
4464
|
+
if (!alive || my !== gen)
|
|
4465
|
+
return undefined;
|
|
4466
|
+
pending = null;
|
|
4467
|
+
set(true);
|
|
4468
|
+
const w = waiters;
|
|
4469
|
+
waiters = [];
|
|
4470
|
+
w.forEach(f => f());
|
|
4471
|
+
return new Promise(resolve => {
|
|
4472
|
+
release = resolve;
|
|
4473
|
+
});
|
|
4474
|
+
})
|
|
4475
|
+
.catch(() => {
|
|
4476
|
+
// Stolen by another tab (or this request aborted): not the runner;
|
|
4477
|
+
// wait for the lock again.
|
|
4478
|
+
if (!alive || my !== gen)
|
|
4479
|
+
return;
|
|
4480
|
+
release = null;
|
|
4481
|
+
set(false);
|
|
4482
|
+
queue(false);
|
|
4318
4483
|
});
|
|
4319
|
-
}
|
|
4320
|
-
|
|
4321
|
-
|
|
4484
|
+
};
|
|
4485
|
+
queue(false);
|
|
4486
|
+
takeoverRef.current = () => new Promise(resolve => {
|
|
4487
|
+
if (primaryRef.current) {
|
|
4488
|
+
resolve();
|
|
4489
|
+
return;
|
|
4490
|
+
}
|
|
4491
|
+
waiters.push(resolve);
|
|
4492
|
+
pending?.abort();
|
|
4493
|
+
queue(true);
|
|
4322
4494
|
});
|
|
4323
4495
|
return () => {
|
|
4324
|
-
|
|
4496
|
+
alive = false;
|
|
4497
|
+
pending?.abort();
|
|
4325
4498
|
release?.();
|
|
4326
|
-
|
|
4499
|
+
waiters.forEach(f => f());
|
|
4500
|
+
waiters = [];
|
|
4501
|
+
set(false);
|
|
4327
4502
|
};
|
|
4328
4503
|
}, [name]);
|
|
4329
|
-
|
|
4504
|
+
const takeover = React.useCallback(() => takeoverRef.current(), []);
|
|
4505
|
+
return { primary, takeover };
|
|
4330
4506
|
}
|
|
4331
4507
|
/* ------------------------------------------------------------------ */
|
|
4332
4508
|
/* Putting the page back after a replay */
|
|
@@ -4359,6 +4535,42 @@ function runAbort(actions) {
|
|
|
4359
4535
|
}, ABORT_DELAY_MS + i * ABORT_GAP_MS);
|
|
4360
4536
|
});
|
|
4361
4537
|
}
|
|
4538
|
+
/** The app's content column (GuideConfig.contentRoot), cut to the viewport; null when none is declared or found. */
|
|
4539
|
+
function contentColumn(selector, vw) {
|
|
4540
|
+
if (!selector || typeof document === "undefined")
|
|
4541
|
+
return null;
|
|
4542
|
+
let el = null;
|
|
4543
|
+
try {
|
|
4544
|
+
el = document.querySelector(selector);
|
|
4545
|
+
}
|
|
4546
|
+
catch {
|
|
4547
|
+
return null;
|
|
4548
|
+
}
|
|
4549
|
+
if (!el)
|
|
4550
|
+
return null;
|
|
4551
|
+
const r = el.getBoundingClientRect();
|
|
4552
|
+
const x = Math.max(0, r.left);
|
|
4553
|
+
const right = Math.min(vw || window.innerWidth, r.right);
|
|
4554
|
+
if (right - x <= 0)
|
|
4555
|
+
return null;
|
|
4556
|
+
return { x, y: 0, width: right - x, height: 0 };
|
|
4557
|
+
}
|
|
4558
|
+
/** The control a form's implicit submit (Enter in a field) presses: its first submit button. */
|
|
4559
|
+
function defaultSubmitControl(form) {
|
|
4560
|
+
for (const el of Array.from(form.elements)) {
|
|
4561
|
+
if (el instanceof HTMLButtonElement && (el.type === "submit" || !el.getAttribute("type")))
|
|
4562
|
+
return el;
|
|
4563
|
+
if (el instanceof HTMLInputElement && (el.type === "submit" || el.type === "image"))
|
|
4564
|
+
return el;
|
|
4565
|
+
}
|
|
4566
|
+
return null;
|
|
4567
|
+
}
|
|
4568
|
+
/** A field where Enter submits its form (not a textarea, a button or a select). */
|
|
4569
|
+
function submitsOnEnter(el) {
|
|
4570
|
+
if (!(el instanceof HTMLInputElement))
|
|
4571
|
+
return false;
|
|
4572
|
+
return !["button", "submit", "reset", "image", "file", "hidden"].includes(el.type);
|
|
4573
|
+
}
|
|
4362
4574
|
/** A step's own target takes presses: not on a "view" step, unless the step allows it. */
|
|
4363
4575
|
function targetPressable(step, bridge) {
|
|
4364
4576
|
if (!step || bridge)
|
|
@@ -4399,7 +4611,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4399
4611
|
busyRef.current = busy;
|
|
4400
4612
|
// No menu: never a menu step (the machine sees it as always open).
|
|
4401
4613
|
const sidebarOpen = menu ? !!menu.open : true;
|
|
4402
|
-
const primary = usePrimaryTab(`erp-guide:${organizationId}:${suite.key}`);
|
|
4614
|
+
const { primary, takeover } = usePrimaryTab(`erp-guide:${organizationId}:${suite.key}`);
|
|
4615
|
+
const primaryRef = React.useRef(primary);
|
|
4616
|
+
primaryRef.current = primary;
|
|
4403
4617
|
// Progress: read once this tab runs the tour and the app is ready (for
|
|
4404
4618
|
// the missions, or for the page guides alone: config.pagesReady).
|
|
4405
4619
|
const [answer, setAnswer] = React.useState(null);
|
|
@@ -4408,7 +4622,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4408
4622
|
const hasPages = React.useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0 || validFlows(suite).length > 0, [suite]);
|
|
4409
4623
|
const hasFlows = React.useMemo(() => validFlows(suite).length > 0, [suite]);
|
|
4410
4624
|
const hasSections = React.useMemo(() => validSections(suite).length > 0, [suite]);
|
|
4411
|
-
|
|
4625
|
+
// Every tab reads progress (0.1.78): the "?" menu is shown in all of them.
|
|
4626
|
+
const enabled = missionsReady || (hasPages && pagesReady);
|
|
4412
4627
|
const loaded = React.useRef(false);
|
|
4413
4628
|
React.useEffect(() => {
|
|
4414
4629
|
if (!enabled || loaded.current)
|
|
@@ -4432,8 +4647,12 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4432
4647
|
}, [enabled, suite]);
|
|
4433
4648
|
// The missions run for their audience; the page guides for theirs (every
|
|
4434
4649
|
// member while pages are on), each with its own ready.
|
|
4435
|
-
|
|
4436
|
-
|
|
4650
|
+
// On: this person has them here (every tab, for the "?" menu). Live: this
|
|
4651
|
+
// tab runs them (the primary tab only): steps, offers, the welcome, saves.
|
|
4652
|
+
const missionsOn = !!answer && answer.audience !== "none" && !answer.newerOnServer && missionsReady && !hidden;
|
|
4653
|
+
const pagesOn = !!answer && hasPages && answer.pageAudience !== "none" && pagesReady && !hidden;
|
|
4654
|
+
const missionsLive = missionsOn && primary;
|
|
4655
|
+
const pagesLive = pagesOn && primary;
|
|
4437
4656
|
const live = missionsLive || pagesLive;
|
|
4438
4657
|
/* ---------------- machine ---------------- */
|
|
4439
4658
|
const [state, setState] = React.useState(null);
|
|
@@ -4526,6 +4745,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4526
4745
|
const row = await storeGuides(adapterRef.current, suite, cur.progress);
|
|
4527
4746
|
if (row)
|
|
4528
4747
|
savedKey.current = payloadKey(row, suite.app);
|
|
4748
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4749
|
+
if (row)
|
|
4750
|
+
announceSaved();
|
|
4529
4751
|
if (row && stateRef.current) {
|
|
4530
4752
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4531
4753
|
dispatchRef.current({ type: "serverRow", progress: row });
|
|
@@ -4582,6 +4804,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4582
4804
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4583
4805
|
if (row)
|
|
4584
4806
|
dispatchRef.current({ type: "pageRow", key: e.key, row, kind });
|
|
4807
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4808
|
+
if (row)
|
|
4809
|
+
announceSaved();
|
|
4585
4810
|
});
|
|
4586
4811
|
}
|
|
4587
4812
|
}
|
|
@@ -4628,6 +4853,95 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4628
4853
|
clearTimeout(toastTimer.current);
|
|
4629
4854
|
};
|
|
4630
4855
|
}, [flushSave]);
|
|
4856
|
+
/* ---------------- other tabs (0.1.78) ---------------- */
|
|
4857
|
+
// Progress saved in one tab reaches the others: a message on a channel
|
|
4858
|
+
// per organisation and app, and a fresh read when a tab comes back into
|
|
4859
|
+
// view. Only the server's rows are read, never another tab's state.
|
|
4860
|
+
const channelRef = React.useRef(null);
|
|
4861
|
+
const refreshNow = React.useCallback(async () => {
|
|
4862
|
+
const a = await loadGuides(adapterRef.current, suite, undefined, true);
|
|
4863
|
+
const cur = stateRef.current;
|
|
4864
|
+
if (!a || !cur)
|
|
4865
|
+
return;
|
|
4866
|
+
// The runner's own state wins while it is running something.
|
|
4867
|
+
if (primaryRef.current && (cur.screen.name !== "idle" || saving.current))
|
|
4868
|
+
return;
|
|
4869
|
+
if (a.progress.guide_version === cur.progress.guide_version && a.progress.rev >= cur.progress.rev) {
|
|
4870
|
+
savedKey.current = a.progress.rev > 0 ? payloadKey(a.progress, suite.app) : null;
|
|
4871
|
+
}
|
|
4872
|
+
dispatchRef.current({ type: "refresh", progress: a.progress, pages: a.pages, sections: a.sections, flows: a.flows });
|
|
4873
|
+
}, [suite]);
|
|
4874
|
+
const refreshRef = React.useRef(refreshNow);
|
|
4875
|
+
refreshRef.current = refreshNow;
|
|
4876
|
+
React.useEffect(() => {
|
|
4877
|
+
if (typeof BroadcastChannel === "undefined")
|
|
4878
|
+
return;
|
|
4879
|
+
let ch;
|
|
4880
|
+
try {
|
|
4881
|
+
ch = new BroadcastChannel(`erp-guide:${organizationId}:${suite.app}`);
|
|
4882
|
+
}
|
|
4883
|
+
catch {
|
|
4884
|
+
return;
|
|
4885
|
+
}
|
|
4886
|
+
channelRef.current = ch;
|
|
4887
|
+
ch.onmessage = (e) => {
|
|
4888
|
+
if (e.data?.type === "saved")
|
|
4889
|
+
void refreshRef.current();
|
|
4890
|
+
};
|
|
4891
|
+
return () => {
|
|
4892
|
+
channelRef.current = null;
|
|
4893
|
+
ch.close();
|
|
4894
|
+
};
|
|
4895
|
+
}, [organizationId, suite.app]);
|
|
4896
|
+
React.useEffect(() => {
|
|
4897
|
+
const onVisible = () => {
|
|
4898
|
+
if (document.visibilityState === "visible" && stateRef.current)
|
|
4899
|
+
void refreshRef.current();
|
|
4900
|
+
};
|
|
4901
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
4902
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
4903
|
+
}, []);
|
|
4904
|
+
function announceSaved() {
|
|
4905
|
+
try {
|
|
4906
|
+
channelRef.current?.postMessage({ type: "saved" });
|
|
4907
|
+
}
|
|
4908
|
+
catch {
|
|
4909
|
+
/* a closed channel: the next visibility change reads again */
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
// The tab that lost the lock stops cleanly: whatever it was running ends
|
|
4913
|
+
// silently, without writing.
|
|
4914
|
+
React.useEffect(() => {
|
|
4915
|
+
if (primary || !stateRef.current)
|
|
4916
|
+
return;
|
|
4917
|
+
const cur = stateRef.current;
|
|
4918
|
+
if (saveTimer.current) {
|
|
4919
|
+
clearTimeout(saveTimer.current);
|
|
4920
|
+
saveTimer.current = null;
|
|
4921
|
+
}
|
|
4922
|
+
if (cur.screen.name !== "idle" || cur.panelOpen)
|
|
4923
|
+
dispatchRef.current({ type: "yield" });
|
|
4924
|
+
}, [primary]);
|
|
4925
|
+
/**
|
|
4926
|
+
* A guide item pressed in this tab: run it here. In a tab that is not the
|
|
4927
|
+
* runner, take the lock first (the other tab stops), read progress fresh,
|
|
4928
|
+
* then do it. Nothing resumes by itself in a tab taken over this way.
|
|
4929
|
+
*/
|
|
4930
|
+
const viaRunner = React.useCallback((fn) => {
|
|
4931
|
+
if (primaryRef.current) {
|
|
4932
|
+
fn();
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
resumed.current = true;
|
|
4936
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4937
|
+
flowResumed.current = true;
|
|
4938
|
+
void takeover()
|
|
4939
|
+
.then(() => refreshRef.current().catch(() => undefined))
|
|
4940
|
+
.then(() => {
|
|
4941
|
+
if (primaryRef.current)
|
|
4942
|
+
fn();
|
|
4943
|
+
});
|
|
4944
|
+
}, [takeover]);
|
|
4631
4945
|
/* ---------------- boot: resume, welcome ---------------- */
|
|
4632
4946
|
const screen = state?.screen ?? { name: "idle" };
|
|
4633
4947
|
const resumed = React.useRef(false);
|
|
@@ -4737,6 +5051,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4737
5051
|
const stepId = screen.name === "step" ? `${screen.mission}:${screen.index}:${screen.bridge ? "b" : ""}${screen.nothingToSave ? "n" : ""}` : "";
|
|
4738
5052
|
/* Measure every frame while a step is shown: the spotlight follows
|
|
4739
5053
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
5054
|
+
const contentRoot = config.contentRoot;
|
|
4740
5055
|
const [layout, setLayout] = React.useState({
|
|
4741
5056
|
spot: null,
|
|
4742
5057
|
covered: false,
|
|
@@ -4745,6 +5060,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4745
5060
|
vw: 0,
|
|
4746
5061
|
vh: 0,
|
|
4747
5062
|
oy: 0,
|
|
5063
|
+
col: null,
|
|
4748
5064
|
});
|
|
4749
5065
|
React.useEffect(() => {
|
|
4750
5066
|
if (!inStep) {
|
|
@@ -4753,7 +5069,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4753
5069
|
}
|
|
4754
5070
|
let raf = 0;
|
|
4755
5071
|
let lostSince = null;
|
|
4756
|
-
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
|
|
5072
|
+
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0, col: null };
|
|
4757
5073
|
targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
|
|
4758
5074
|
const allow = stepInfo?.step.allow ?? [];
|
|
4759
5075
|
// The spotlight is the part of the target a person can see: cut by the
|
|
@@ -4813,13 +5129,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4813
5129
|
holes.push(padRect(rectOf(ael), TARGET_PAD));
|
|
4814
5130
|
}
|
|
4815
5131
|
const covered = !!spot && coveredByDialog(targetElRef.current);
|
|
4816
|
-
const
|
|
5132
|
+
const col = contentColumn(contentRoot, vw);
|
|
5133
|
+
const next = { spot, covered, holes, popovers, vw, vh, oy, col };
|
|
4817
5134
|
const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
|
|
4818
5135
|
if (!sameRect(next.spot, last.spot) ||
|
|
4819
5136
|
next.covered !== last.covered ||
|
|
4820
5137
|
next.vw !== last.vw ||
|
|
4821
5138
|
next.vh !== last.vh ||
|
|
4822
5139
|
next.oy !== last.oy ||
|
|
5140
|
+
!sameRect(next.col, last.col) ||
|
|
4823
5141
|
!sameList(next.holes, last.holes) ||
|
|
4824
5142
|
!sameList(next.popovers, last.popovers)) {
|
|
4825
5143
|
last = next;
|
|
@@ -4974,6 +5292,43 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4974
5292
|
}
|
|
4975
5293
|
return false;
|
|
4976
5294
|
};
|
|
5295
|
+
/**
|
|
5296
|
+
* Whether a form may be submitted during this step (shell ≥ 0.1.77): only
|
|
5297
|
+
* when its submit control — the button pressed, or the one Enter in a
|
|
5298
|
+
* field would press — is the step's pressable target or in its `allow`.
|
|
5299
|
+
* Otherwise Enter in an allowed field would save a draft (and take a
|
|
5300
|
+
* document number) behind the guide's back.
|
|
5301
|
+
*/
|
|
5302
|
+
const submitAllowed = (form, submitter) => {
|
|
5303
|
+
const btn = submitter ?? defaultSubmitControl(form);
|
|
5304
|
+
if (!btn)
|
|
5305
|
+
return false;
|
|
5306
|
+
const el = targetElRef.current;
|
|
5307
|
+
const sc = stateRef.current?.screen;
|
|
5308
|
+
const pressable = targetPressable(stepInfoRef.current?.step, sc?.name === "step" && sc.bridge);
|
|
5309
|
+
if (el && pressable && el.contains(btn))
|
|
5310
|
+
return true;
|
|
5311
|
+
for (const a of stepInfoRef.current?.step.allow ?? []) {
|
|
5312
|
+
const ael = findAnchor(a);
|
|
5313
|
+
if (ael && ael.contains(btn))
|
|
5314
|
+
return true;
|
|
5315
|
+
}
|
|
5316
|
+
return false;
|
|
5317
|
+
};
|
|
5318
|
+
const onSubmit = (e) => {
|
|
5319
|
+
const sc = stateRef.current?.screen;
|
|
5320
|
+
if (!guardingRef.current || sc?.name !== "step")
|
|
5321
|
+
return;
|
|
5322
|
+
const form = e.target;
|
|
5323
|
+
if (!(form instanceof HTMLFormElement) || inGuideLayer(form))
|
|
5324
|
+
return;
|
|
5325
|
+
const submitter = e.submitter ?? null;
|
|
5326
|
+
if (submitAllowed(form, submitter))
|
|
5327
|
+
return;
|
|
5328
|
+
e.preventDefault();
|
|
5329
|
+
e.stopPropagation();
|
|
5330
|
+
dispatchRef.current({ type: "blocked" });
|
|
5331
|
+
};
|
|
4977
5332
|
const onPointer = (e) => {
|
|
4978
5333
|
const t = e.target;
|
|
4979
5334
|
if (inGuideLayer(t)) {
|
|
@@ -5093,8 +5448,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5093
5448
|
return;
|
|
5094
5449
|
}
|
|
5095
5450
|
}
|
|
5096
|
-
if (modalRef.current || !allowedTarget(active))
|
|
5451
|
+
if (modalRef.current || !allowedTarget(active)) {
|
|
5097
5452
|
stop();
|
|
5453
|
+
return;
|
|
5454
|
+
}
|
|
5455
|
+
// Enter in a field the step lets the person type in: no implicit
|
|
5456
|
+
// submit unless the form's submit control is allowed now.
|
|
5457
|
+
if (e.key === "Enter" && sc.name === "step" && submitsOnEnter(active) && active.form && !submitAllowed(active.form, null)) {
|
|
5458
|
+
e.preventDefault();
|
|
5459
|
+
}
|
|
5098
5460
|
}
|
|
5099
5461
|
};
|
|
5100
5462
|
window.addEventListener("pointerdown", onPointer, true);
|
|
@@ -5104,7 +5466,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5104
5466
|
window.addEventListener("focusin", onFocusIn, true);
|
|
5105
5467
|
window.addEventListener("focusout", onFocusOut, true);
|
|
5106
5468
|
window.addEventListener("keydown", onKey, true);
|
|
5469
|
+
window.addEventListener("submit", onSubmit, true);
|
|
5107
5470
|
return () => {
|
|
5471
|
+
window.removeEventListener("submit", onSubmit, true);
|
|
5108
5472
|
window.removeEventListener("pointerdown", onPointer, true);
|
|
5109
5473
|
window.removeEventListener("mousedown", onPointer, true);
|
|
5110
5474
|
window.removeEventListener("touchstart", onPointer, true);
|
|
@@ -5226,12 +5590,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5226
5590
|
!due;
|
|
5227
5591
|
useAfterIdle(passiveDue, PASSIVE_IDLE_MS, busy, () => dispatchRef.current({ type: "idleCelebrate" }), [state?.pendingCelebrate?.join(",")]);
|
|
5228
5592
|
React.useEffect(() => {
|
|
5229
|
-
|
|
5593
|
+
// Every tab shows "ไกด์เริ่มต้น n/N"; pressing it runs it in this tab.
|
|
5594
|
+
if (!missionsOn || !state) {
|
|
5230
5595
|
publishGuideEntry(null, null);
|
|
5231
5596
|
return;
|
|
5232
5597
|
}
|
|
5233
|
-
publishGuideEntry({ done, total }, () => dispatchRef.current({ type: "openPanel" }));
|
|
5234
|
-
}, [
|
|
5598
|
+
publishGuideEntry({ done, total }, () => viaRunner(() => dispatchRef.current({ type: "openPanel" })));
|
|
5599
|
+
}, [missionsOn, state, done, total, viaRunner]);
|
|
5235
5600
|
React.useEffect(() => () => publishGuideEntry(null, null), []);
|
|
5236
5601
|
/* ---------------- page guides: this page's, the offer, the list ---------------- */
|
|
5237
5602
|
// `when` anchors (LIFF's sub-screens) come and go without a route change:
|
|
@@ -5239,7 +5604,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5239
5604
|
const hasWhen = React.useMemo(() => validPages(suite).some(p => !!p.when) || validSections(suite).length > 0, [suite]);
|
|
5240
5605
|
const [domTick, setDomTick] = React.useState(0);
|
|
5241
5606
|
React.useEffect(() => {
|
|
5242
|
-
if (!
|
|
5607
|
+
if (!pagesOn || !hasWhen || typeof MutationObserver === "undefined")
|
|
5243
5608
|
return;
|
|
5244
5609
|
let t = null;
|
|
5245
5610
|
const obs = new MutationObserver(records => {
|
|
@@ -5263,18 +5628,18 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5263
5628
|
if (t)
|
|
5264
5629
|
clearTimeout(t);
|
|
5265
5630
|
};
|
|
5266
|
-
}, [
|
|
5631
|
+
}, [pagesOn, hasWhen]);
|
|
5267
5632
|
const hasState = !!state;
|
|
5268
|
-
const pageNow = React.useMemo(() => (
|
|
5633
|
+
const pageNow = React.useMemo(() => (pagesOn && hasState ? pageHere(suite, env()) : null),
|
|
5269
5634
|
// pathname/search/factsKey/domTick make it current.
|
|
5270
5635
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5271
|
-
[
|
|
5272
|
-
const listed = React.useMemo(() => (
|
|
5273
|
-
const sectionsNow = React.useMemo(() =>
|
|
5636
|
+
[pagesOn, hasState, suite, env, pathname, search, factsKey, domTick]);
|
|
5637
|
+
const listed = React.useMemo(() => (pagesOn && ctxIdle ? listedPages(suite, ctxIdle) : []), [pagesOn, suite, ctxIdle]);
|
|
5638
|
+
const sectionsNow = React.useMemo(() => pagesOn && hasState && hasSections
|
|
5274
5639
|
? sectionsAvailable(suite, env()).map(k => ({ key: k, title: findSection(suite, k)?.title ?? "" }))
|
|
5275
5640
|
: [],
|
|
5276
5641
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5277
|
-
[
|
|
5642
|
+
[pagesOn, hasState, hasSections, suite, env, pathname, search, factsKey, domTick]);
|
|
5278
5643
|
const sectionsSig = sectionsNow.map(x => x.key).join(",");
|
|
5279
5644
|
const openSection = React.useCallback((key) => {
|
|
5280
5645
|
openingSection.current = key;
|
|
@@ -5285,14 +5650,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5285
5650
|
openingSection.current = null;
|
|
5286
5651
|
}
|
|
5287
5652
|
}, []);
|
|
5288
|
-
const flowsNow = React.useMemo(() =>
|
|
5653
|
+
const flowsNow = React.useMemo(() => pagesOn && hasState && hasFlows
|
|
5289
5654
|
? flowsAvailable(suite, env()).map(k => ({ key: k, title: findFlow(suite, k)?.title ?? "" }))
|
|
5290
5655
|
: [],
|
|
5291
5656
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5292
|
-
[
|
|
5657
|
+
[pagesOn, hasState, hasFlows, suite, env, pathname, search, factsKey]);
|
|
5293
5658
|
const flowsSig = flowsNow.map(x => x.key).join(",");
|
|
5294
5659
|
/** The flows the panel lists: this person's, startable here or with somewhere to go. */
|
|
5295
|
-
const flowsListed = React.useMemo(() => (
|
|
5660
|
+
const flowsListed = React.useMemo(() => (pagesOn && hasFlows && ctxIdle ? shownFlows(suite, ctxIdle).filter(f => onFlowStart(f, { code, pathname, search: new URLSearchParams(search) }) || !!flowHref(f, ctxIdle)) : []), [pagesOn, hasFlows, suite, ctxIdle, code, pathname, search]);
|
|
5296
5661
|
const openFlowHere = React.useCallback((key) => dispatchRef.current({ type: "startFlow", flow: key }), []);
|
|
5297
5662
|
const openPageHere = React.useCallback(() => {
|
|
5298
5663
|
const p = pageHere(suite, env());
|
|
@@ -5300,7 +5665,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5300
5665
|
dispatchRef.current({ type: "startPage", page: p.key });
|
|
5301
5666
|
}, [suite, env]);
|
|
5302
5667
|
React.useEffect(() => {
|
|
5303
|
-
if (!
|
|
5668
|
+
if (!pagesOn || !hasState) {
|
|
5304
5669
|
publishPageGuides(null, null, null);
|
|
5305
5670
|
return;
|
|
5306
5671
|
}
|
|
@@ -5310,10 +5675,10 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5310
5675
|
sections: sectionsNow,
|
|
5311
5676
|
flows: flowsNow,
|
|
5312
5677
|
flowsListed: flowsListed.length,
|
|
5313
|
-
}, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }), openSection, openFlowHere, () => dispatchRef.current({ type: "openPanel", section: "flows" }));
|
|
5678
|
+
}, () => viaRunner(openPageHere), () => viaRunner(() => dispatchRef.current({ type: "openPanel", section: "pages" })), (key) => viaRunner(() => openSection(key)), (key) => viaRunner(() => openFlowHere(key)), () => viaRunner(() => dispatchRef.current({ type: "openPanel", section: "flows" })));
|
|
5314
5679
|
// sectionsSig / flowsSig stand for sectionsNow / flowsNow.
|
|
5315
5680
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5316
|
-
}, [
|
|
5681
|
+
}, [pagesOn, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere, viaRunner]);
|
|
5317
5682
|
React.useEffect(() => () => publishPageGuides(null, null, null), []);
|
|
5318
5683
|
// The first-visit offer: in "offer" mode, nothing else up, the welcome
|
|
5319
5684
|
// not due here, and a moment of nothing loading on this page.
|
|
@@ -5532,7 +5897,11 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5532
5897
|
const missionNo = missionNumber(suite, mission.key, ctxIdle);
|
|
5533
5898
|
const kind = stepKind(step);
|
|
5534
5899
|
const bridge = screen.bridge;
|
|
5535
|
-
|
|
5900
|
+
// What is ahead now decides เสร็จ; the number shown is the run's counter,
|
|
5901
|
+
// which never goes down (0.1.77).
|
|
5902
|
+
const raw = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
|
|
5903
|
+
const last = raw.no >= raw.total;
|
|
5904
|
+
const pos = screen.pos ?? raw;
|
|
5536
5905
|
const page = screen.page ? findPage(suite, screen.page) : undefined;
|
|
5537
5906
|
const section = screen.section ? findSection(suite, screen.section) : undefined;
|
|
5538
5907
|
const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
|
|
@@ -5541,16 +5910,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5541
5910
|
? {
|
|
5542
5911
|
...read,
|
|
5543
5912
|
showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
|
|
5544
|
-
nextLabel:
|
|
5913
|
+
nextLabel: last ? "เสร็จ" : undefined,
|
|
5545
5914
|
}
|
|
5546
5915
|
: flow
|
|
5547
5916
|
? {
|
|
5548
5917
|
heading: `พาทำ · ${flow.title}`,
|
|
5549
5918
|
exitLabel: "ออกจากการพาทำ",
|
|
5550
|
-
nextLabel: kind === "view" &&
|
|
5919
|
+
nextLabel: kind === "view" && last ? "เสร็จ" : undefined,
|
|
5551
5920
|
}
|
|
5552
5921
|
: {};
|
|
5553
|
-
content = (jsxRuntime.jsx(StepLayer, { spot: layout.spot, holes: layout.holes, popovers: layout.popovers, covered: layout.covered, viewport: { width: layout.vw || window.innerWidth, height: layout.vh || window.innerHeight, offsetTop: layout.oy }, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
5922
|
+
content = (jsxRuntime.jsx(StepLayer, { spot: layout.spot, holes: layout.holes, popovers: layout.popovers, covered: layout.covered, viewport: { width: layout.vw || window.innerWidth, height: layout.vh || window.innerHeight, offsetTop: layout.oy }, bounds: layout.col, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
5554
5923
|
missionNo,
|
|
5555
5924
|
missionTitle: mission.title,
|
|
5556
5925
|
replay: screen.replay,
|
|
@@ -5766,4 +6135,4 @@ exports.useGuideEntry = useGuideEntry;
|
|
|
5766
6135
|
exports.usePageGuideEntry = usePageGuideEntry;
|
|
5767
6136
|
exports.usePageGuides = usePageGuides;
|
|
5768
6137
|
exports.useSectionGuideValues = useSectionGuideValues;
|
|
5769
|
-
//# sourceMappingURL=format-
|
|
6138
|
+
//# sourceMappingURL=format-BmTSD3DB.cjs.map
|