@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
|
@@ -992,6 +992,7 @@ function initialGuideState(audience, progress, pages) {
|
|
|
992
992
|
pageRun: null,
|
|
993
993
|
panelSection: "all",
|
|
994
994
|
autoPassed: null,
|
|
995
|
+
counter: null,
|
|
995
996
|
audience,
|
|
996
997
|
progress,
|
|
997
998
|
screen: { name: "idle" },
|
|
@@ -1370,7 +1371,7 @@ function announceStep(d, suite, env) {
|
|
|
1370
1371
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
1371
1372
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
1372
1373
|
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
1373
|
-
const pos = stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
|
|
1374
|
+
const pos = s.pos ?? stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
|
|
1374
1375
|
d.effects.push({
|
|
1375
1376
|
type: "announce",
|
|
1376
1377
|
text: `ขั้นที่ ${pos.no} จาก ${pos.total}. ${title}${why ? `. ${why}` : ""}`,
|
|
@@ -1404,6 +1405,54 @@ function stepPosition(mission, index, env, context, autoPassed = []) {
|
|
|
1404
1405
|
});
|
|
1405
1406
|
return { no: Math.max(no, 1), total: Math.max(total, 1) };
|
|
1406
1407
|
}
|
|
1408
|
+
/**
|
|
1409
|
+
* The steps after `index` this person will see, as far as can be known from
|
|
1410
|
+
* here: on this page, everything is read (requires, skipIf, a click whose
|
|
1411
|
+
* outcome already holds); on another page, a step whose showing depends on
|
|
1412
|
+
* what is on that page (`skipIf`, `onMissing: "skip"`) is not counted until
|
|
1413
|
+
* its page is reached.
|
|
1414
|
+
*/
|
|
1415
|
+
function stepsAhead(mission, index, env, context) {
|
|
1416
|
+
let n = 0;
|
|
1417
|
+
for (let j = index + 1; j < mission.steps.length; j++) {
|
|
1418
|
+
const step = mission.steps[j];
|
|
1419
|
+
if (step.requires && !safe(() => env.can(step.requires), true))
|
|
1420
|
+
continue;
|
|
1421
|
+
const home = stepHomeRoute(mission.steps, j);
|
|
1422
|
+
const here = !home || !!matchRoute(home, env.pathname, env.search, env.code);
|
|
1423
|
+
if (here) {
|
|
1424
|
+
const ctx = guideContext(env, context, routeParamsFor(step, env));
|
|
1425
|
+
if (step.skipIf && safe(() => step.skipIf(ctx), false))
|
|
1426
|
+
continue;
|
|
1427
|
+
if (wouldAutoPass(step, env))
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
else if (step.skipIf || step.onMissing === "skip") {
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
n++;
|
|
1434
|
+
}
|
|
1435
|
+
return n;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* The counter as shown when entering step `index` (shell ≥ 0.1.77). `no`
|
|
1439
|
+
* counts the steps actually shown in this run up to this one. The total is
|
|
1440
|
+
* worked out on entering a step on a new page — the shown steps so far plus
|
|
1441
|
+
* stepsAhead — and kept while the run stays on that page; it never goes
|
|
1442
|
+
* down (the larger of the old and the new), and is never below `no`.
|
|
1443
|
+
*/
|
|
1444
|
+
function countStep(d, env, mission, index) {
|
|
1445
|
+
const prev = d.state.counter?.mission === mission.key ? d.state.counter : null;
|
|
1446
|
+
const shown = prev ? (prev.shown.includes(index) ? prev.shown : [...prev.shown, index]) : [index];
|
|
1447
|
+
const no = Math.max(1, shown.filter(i => i <= index).length);
|
|
1448
|
+
let total;
|
|
1449
|
+
if (prev && prev.path === env.pathname)
|
|
1450
|
+
total = Math.max(prev.total, no);
|
|
1451
|
+
else
|
|
1452
|
+
total = Math.max(prev?.total ?? 0, no + stepsAhead(mission, index, env, d.state.progress.context), no);
|
|
1453
|
+
d.state = { ...d.state, counter: { mission: mission.key, shown, total, path: env.pathname } };
|
|
1454
|
+
return { no, total };
|
|
1455
|
+
}
|
|
1407
1456
|
/** A step entered now would pass at once: a click or select step whose route or visible outcome holds. */
|
|
1408
1457
|
function wouldAutoPass(step, env) {
|
|
1409
1458
|
const kind = stepKind(step);
|
|
@@ -1473,6 +1522,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1473
1522
|
const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
|
|
1474
1523
|
const bridge = !away && needsBridge(step, env, ctx);
|
|
1475
1524
|
const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
|
|
1525
|
+
const pos = countStep(d, env, mission, i);
|
|
1476
1526
|
setScreen(d, {
|
|
1477
1527
|
name: "step",
|
|
1478
1528
|
mission: mission.key,
|
|
@@ -1490,6 +1540,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1490
1540
|
...(page ? { page } : {}),
|
|
1491
1541
|
...(section ? { section } : {}),
|
|
1492
1542
|
...(flow ? { flow } : {}),
|
|
1543
|
+
pos,
|
|
1493
1544
|
});
|
|
1494
1545
|
if (!opts.replay) {
|
|
1495
1546
|
const cp = checkpointAtOrBefore(steps, i);
|
|
@@ -1764,7 +1815,7 @@ function startMission(d, suite, env, key, replay) {
|
|
|
1764
1815
|
const isReplay = replay || entry?.status === "done";
|
|
1765
1816
|
const before = d.state.progress;
|
|
1766
1817
|
// A new run: what passed by itself is counted afresh.
|
|
1767
|
-
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null };
|
|
1818
|
+
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null, counter: null };
|
|
1768
1819
|
if (d.state.progress.welcome === "not_seen")
|
|
1769
1820
|
withProgress(d, { welcome: "started" });
|
|
1770
1821
|
if (!isReplay && d.state.progress.mission_key !== key)
|
|
@@ -1914,24 +1965,33 @@ function pageStillHere(suite, key, env) {
|
|
|
1914
1965
|
const page = findPage(suite, key);
|
|
1915
1966
|
return !!page && pageMatchScore(page, whereOf(env)) !== null;
|
|
1916
1967
|
}
|
|
1917
|
-
/**
|
|
1968
|
+
/**
|
|
1969
|
+
* Whether a row may be written now: none of this version yet, or — once —
|
|
1970
|
+
* a skipped one that the person has now finished (skipped → completed,
|
|
1971
|
+
* shell ≥ 0.1.77). A completed row is final; skipped again changes nothing.
|
|
1972
|
+
*/
|
|
1973
|
+
function rowWritable(row, version, status) {
|
|
1974
|
+
if (!row || row.version < version)
|
|
1975
|
+
return true;
|
|
1976
|
+
return row.version === version && row.status === "skipped" && status === "completed";
|
|
1977
|
+
}
|
|
1978
|
+
/** Whether a run should keep its row to write at its end: no row yet, or only a skipped one. */
|
|
1979
|
+
function rowStillDue(row, version) {
|
|
1980
|
+
return rowWritable(row, version, "completed");
|
|
1981
|
+
}
|
|
1982
|
+
/** Write a page guide's row (once per page and version; skipped → completed once): recorded here at once, saved by the host. */
|
|
1918
1983
|
function recordPage(d, suite, key, status) {
|
|
1919
1984
|
const page = findPage(suite, key);
|
|
1920
|
-
if (!page ||
|
|
1985
|
+
if (!page || !rowWritable(d.state.pages[key], page.version, status))
|
|
1921
1986
|
return;
|
|
1922
1987
|
const prev = d.state.pages[key];
|
|
1923
1988
|
d.state = { ...d.state, pages: { ...d.state.pages, [key]: { version: page.version, status, rev: prev?.rev ?? 0 } } };
|
|
1924
1989
|
d.effects.push({ type: "savePage", key, version: page.version, status });
|
|
1925
1990
|
}
|
|
1926
|
-
/** The section guide's row says this person has ended this version of it. */
|
|
1927
|
-
function sectionRecorded(state, section) {
|
|
1928
|
-
const row = state.sections?.[section.key];
|
|
1929
|
-
return !!row && row.version >= section.version;
|
|
1930
|
-
}
|
|
1931
1991
|
/** Write a section guide's row (once per section and version). */
|
|
1932
1992
|
function recordSection(d, suite, key, status) {
|
|
1933
1993
|
const section = findSection(suite, key);
|
|
1934
|
-
if (!section ||
|
|
1994
|
+
if (!section || !rowWritable(d.state.sections?.[key], section.version, status))
|
|
1935
1995
|
return;
|
|
1936
1996
|
const prev = d.state.sections?.[key];
|
|
1937
1997
|
d.state = { ...d.state, sections: { ...d.state.sections, [key]: { version: section.version, status, rev: prev?.rev ?? 0 } } };
|
|
@@ -1962,7 +2022,7 @@ function flowRecorded(state, flow) {
|
|
|
1962
2022
|
/** Write a flow's row (once per flow and version). */
|
|
1963
2023
|
function recordFlow(d, suite, key, status) {
|
|
1964
2024
|
const flow = findFlow(suite, key);
|
|
1965
|
-
if (!flow ||
|
|
2025
|
+
if (!flow || !rowWritable(d.state.flows?.[key], flow.version, status))
|
|
1966
2026
|
return;
|
|
1967
2027
|
const prev = d.state.flows?.[key];
|
|
1968
2028
|
d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
|
|
@@ -2047,6 +2107,7 @@ function startFlow(d, suite, env, key, write, at) {
|
|
|
2047
2107
|
justDone: null,
|
|
2048
2108
|
pageRun: { key, write, kind: "flow", context: saved },
|
|
2049
2109
|
autoPassed: null,
|
|
2110
|
+
counter: null,
|
|
2050
2111
|
replayPage: {
|
|
2051
2112
|
mission: mission.key,
|
|
2052
2113
|
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
@@ -2077,6 +2138,7 @@ function startSection(d, suite, env, key, write) {
|
|
|
2077
2138
|
justDone: null,
|
|
2078
2139
|
pageRun: { key, write, kind: "section" },
|
|
2079
2140
|
autoPassed: null,
|
|
2141
|
+
counter: null,
|
|
2080
2142
|
replayPage: {
|
|
2081
2143
|
mission: mission.key,
|
|
2082
2144
|
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
@@ -2096,6 +2158,7 @@ function startPage(d, suite, env, key, write) {
|
|
|
2096
2158
|
justDone: null,
|
|
2097
2159
|
pageRun: { key, write, kind: "page" },
|
|
2098
2160
|
autoPassed: null,
|
|
2161
|
+
counter: null,
|
|
2099
2162
|
// What is open now is the person's: ending the guide leaves it alone.
|
|
2100
2163
|
replayPage: {
|
|
2101
2164
|
mission: mission.key,
|
|
@@ -2139,6 +2202,28 @@ function backTarget(mission, index, env, context) {
|
|
|
2139
2202
|
}
|
|
2140
2203
|
return prev >= 0 ? { index: prev, close: false } : null;
|
|
2141
2204
|
}
|
|
2205
|
+
/**
|
|
2206
|
+
* A flow's next step (the next one this person sees) when the person is on
|
|
2207
|
+
* its page and no longer on the current step's: they moved on by
|
|
2208
|
+
* themselves. -1 otherwise (still on this step's page, or elsewhere).
|
|
2209
|
+
*/
|
|
2210
|
+
function flowStepArrivedAt(mission, index, env, context) {
|
|
2211
|
+
const here = (route) => !!route && !!matchRoute(route, env.pathname, env.search, env.code);
|
|
2212
|
+
const curHome = stepHomeRoute(mission.steps, index);
|
|
2213
|
+
if (!curHome || here(curHome))
|
|
2214
|
+
return -1;
|
|
2215
|
+
for (let j = index + 1; j < mission.steps.length; j++) {
|
|
2216
|
+
const st = mission.steps[j];
|
|
2217
|
+
const ctx = guideContext(env, context, routeParamsFor(st, env));
|
|
2218
|
+
if (st.requires && !safe(() => env.can(st.requires), true))
|
|
2219
|
+
continue;
|
|
2220
|
+
if (st.skipIf && safe(() => st.skipIf(ctx), false))
|
|
2221
|
+
continue;
|
|
2222
|
+
const where = st.route ?? stepHomeRoute(mission.steps, j);
|
|
2223
|
+
return here(where) ? j : -1;
|
|
2224
|
+
}
|
|
2225
|
+
return -1;
|
|
2226
|
+
}
|
|
2142
2227
|
function reduceGuide(state, action, suite, env) {
|
|
2143
2228
|
const d = { state, effects: [] };
|
|
2144
2229
|
const s = state.screen;
|
|
@@ -2216,7 +2301,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2216
2301
|
const stepAt = mission.steps.findIndex(st => st.key === d.state.progress.step_key);
|
|
2217
2302
|
const cpAt = mission.steps.findIndex(st => st.key === d.state.progress.checkpoint_key);
|
|
2218
2303
|
const index = stepAt >= 0 ? checkpointAtOrBefore(mission.steps, stepAt) : Math.max(cpAt, 0);
|
|
2219
|
-
d.state = { ...d.state, autoPassed: null };
|
|
2304
|
+
d.state = { ...d.state, autoPassed: null, counter: null };
|
|
2220
2305
|
enterStep(d, suite, env, mission, index, { replay: false });
|
|
2221
2306
|
break;
|
|
2222
2307
|
}
|
|
@@ -2259,6 +2344,15 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2259
2344
|
break;
|
|
2260
2345
|
const step = mission.steps[s.index];
|
|
2261
2346
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
2347
|
+
if (s.flow && !s.confirmExit) {
|
|
2348
|
+
// Within the chain, the person went on to the next step's page by
|
|
2349
|
+
// themselves: carry on there, no "ทำต่อ" (shell ≥ 0.1.77).
|
|
2350
|
+
const next = flowStepArrivedAt(mission, s.index, env, d.state.progress.context);
|
|
2351
|
+
if (next >= 0) {
|
|
2352
|
+
enterStep(d, suite, env, mission, next, { replay: s.replay });
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2262
2356
|
if (s.phase === "away") {
|
|
2263
2357
|
const where = placeRoute(mission.steps, s.index);
|
|
2264
2358
|
if (!where || matchRoute(where, env.pathname, env.search, env.code)) {
|
|
@@ -2653,7 +2747,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2653
2747
|
if (!flowsAvailable(suite, env).includes(action.flow))
|
|
2654
2748
|
break;
|
|
2655
2749
|
const flow = findFlow(suite, action.flow);
|
|
2656
|
-
startFlow(d, suite, env, flow.key,
|
|
2750
|
+
startFlow(d, suite, env, flow.key, rowStillDue(state.flows?.[flow.key], flow.version));
|
|
2657
2751
|
break;
|
|
2658
2752
|
}
|
|
2659
2753
|
case "resumeFlow": {
|
|
@@ -2662,7 +2756,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2662
2756
|
const flow = findFlow(suite, action.flow);
|
|
2663
2757
|
if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
|
|
2664
2758
|
break;
|
|
2665
|
-
startFlow(d, suite, env, flow.key, action.write &&
|
|
2759
|
+
startFlow(d, suite, env, flow.key, action.write && rowStillDue(state.flows?.[flow.key], flow.version), { step: action.step, context: action.context });
|
|
2666
2760
|
break;
|
|
2667
2761
|
}
|
|
2668
2762
|
case "startPage": {
|
|
@@ -2674,7 +2768,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2674
2768
|
const page = pageHere(suite, env);
|
|
2675
2769
|
if (!page || page.key !== action.page)
|
|
2676
2770
|
break;
|
|
2677
|
-
startPage(d, suite, env, page.key,
|
|
2771
|
+
startPage(d, suite, env, page.key, rowStillDue(state.pages[page.key], page.version));
|
|
2678
2772
|
break;
|
|
2679
2773
|
}
|
|
2680
2774
|
case "back": {
|
|
@@ -2713,6 +2807,37 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2713
2807
|
d.state = { ...d.state, pages: { ...d.state.pages, [action.key]: action.row } };
|
|
2714
2808
|
break;
|
|
2715
2809
|
}
|
|
2810
|
+
case "yield": {
|
|
2811
|
+
const run = d.state.pageRun;
|
|
2812
|
+
if (run?.kind === "flow" && run.context)
|
|
2813
|
+
withProgress(d, { context: run.context });
|
|
2814
|
+
d.state = { ...d.state, pageRun: null, replayPage: null, panelOpen: false, justDone: null, pendingCelebrate: [] };
|
|
2815
|
+
setScreen(d, { name: "idle" });
|
|
2816
|
+
break;
|
|
2817
|
+
}
|
|
2818
|
+
case "refresh": {
|
|
2819
|
+
const p = action.progress;
|
|
2820
|
+
const local = d.state.progress;
|
|
2821
|
+
// The server's row when it is this version and not behind this tab's.
|
|
2822
|
+
if (p.guide_version === local.guide_version && p.rev >= local.rev)
|
|
2823
|
+
d.state = { ...d.state, progress: p };
|
|
2824
|
+
const newer = (server, mine) => {
|
|
2825
|
+
const out = { ...(mine ?? {}) };
|
|
2826
|
+
for (const [k, r] of Object.entries(server ?? {})) {
|
|
2827
|
+
const m = out[k];
|
|
2828
|
+
if (!m || r.version > m.version || (r.version === m.version && r.rev >= m.rev))
|
|
2829
|
+
out[k] = r;
|
|
2830
|
+
}
|
|
2831
|
+
return out;
|
|
2832
|
+
};
|
|
2833
|
+
d.state = {
|
|
2834
|
+
...d.state,
|
|
2835
|
+
pages: newer(action.pages, d.state.pages),
|
|
2836
|
+
sections: newer(action.sections, d.state.sections),
|
|
2837
|
+
flows: newer(action.flows, d.state.flows),
|
|
2838
|
+
};
|
|
2839
|
+
break;
|
|
2840
|
+
}
|
|
2716
2841
|
case "startSection": {
|
|
2717
2842
|
// Never offered: only on request. Never over a mission step, a
|
|
2718
2843
|
// welcome, a gate or a celebration; only on screen, only theirs.
|
|
@@ -2723,7 +2848,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2723
2848
|
if (!sectionsAvailable(suite, env).includes(action.section))
|
|
2724
2849
|
break;
|
|
2725
2850
|
const section = findSection(suite, action.section);
|
|
2726
|
-
startSection(d, suite, env, section.key,
|
|
2851
|
+
startSection(d, suite, env, section.key, rowStillDue(state.sections?.[section.key], section.version));
|
|
2727
2852
|
break;
|
|
2728
2853
|
}
|
|
2729
2854
|
case "idleCelebrate": {
|
|
@@ -3082,9 +3207,9 @@ function conflictOf(err) {
|
|
|
3082
3207
|
return null;
|
|
3083
3208
|
}
|
|
3084
3209
|
/** The GET, parsed for this suite; null (show nothing) on any failure or odd answer. */
|
|
3085
|
-
async function loadGuides(adapter, suite, signal) {
|
|
3210
|
+
async function loadGuides(adapter, suite, signal, fresh = false) {
|
|
3086
3211
|
try {
|
|
3087
|
-
const raw = await adapter.fetchProgress({ app: suite.app, guideKey: suite.key, signal });
|
|
3212
|
+
const raw = await adapter.fetchProgress({ app: suite.app, guideKey: suite.key, signal, ...(fresh ? { fresh: true } : {}) });
|
|
3088
3213
|
return parseGuidesAnswer(raw, suite);
|
|
3089
3214
|
}
|
|
3090
3215
|
catch {
|
|
@@ -3169,7 +3294,10 @@ async function storePageRow(adapter, app, key, version, status, rev, kind = "pag
|
|
|
3169
3294
|
}
|
|
3170
3295
|
if (current.version > version)
|
|
3171
3296
|
return null;
|
|
3172
|
-
|
|
3297
|
+
// This version already recorded: kept, except a skipped row this run
|
|
3298
|
+
// finished (skipped → completed, once, shell ≥ 0.1.77).
|
|
3299
|
+
const finishing = status === "completed" && current.status === "skipped";
|
|
3300
|
+
if (current.version === version && current.status !== "in_progress" && !finishing)
|
|
3173
3301
|
return current;
|
|
3174
3302
|
at = current.rev;
|
|
3175
3303
|
}
|
|
@@ -3836,6 +3964,9 @@ function useNudge(nudge, ref) {
|
|
|
3836
3964
|
}
|
|
3837
3965
|
function StepLayer(p) {
|
|
3838
3966
|
const { spot, phone, viewport } = p;
|
|
3967
|
+
// Where the bubble may sit: the content column when the app has one.
|
|
3968
|
+
const col = p.bounds && p.bounds.width > 0 ? p.bounds : { x: 0, width: viewport.width || (typeof window !== "undefined" ? window.innerWidth : 0)};
|
|
3969
|
+
const colKey = `${col.x}|${col.width}`;
|
|
3839
3970
|
const bubbleRef = useRef(null);
|
|
3840
3971
|
const arrowRef = useRef(null);
|
|
3841
3972
|
const ringRef = useRef(null);
|
|
@@ -3876,8 +4007,8 @@ function StepLayer(p) {
|
|
|
3876
4007
|
const bw = el.offsetWidth;
|
|
3877
4008
|
const bh = el.offsetHeight;
|
|
3878
4009
|
setPos(phone
|
|
3879
|
-
? { left: m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
3880
|
-
: { left: (
|
|
4010
|
+
? { left: col.x + m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
4011
|
+
: { left: col.x + (col.width - bw) / 2, top: viewport.offsetTop + (viewport.height - bh) / 2, side: "dock" });
|
|
3881
4012
|
return;
|
|
3882
4013
|
}
|
|
3883
4014
|
if (phone) {
|
|
@@ -3898,7 +4029,7 @@ function StepLayer(p) {
|
|
|
3898
4029
|
: overlapBot <= overlapTop
|
|
3899
4030
|
? botT
|
|
3900
4031
|
: topT;
|
|
3901
|
-
setPos({ left: m, top, side: "dock" });
|
|
4032
|
+
setPos({ left: col.x + m, top, side: "dock" });
|
|
3902
4033
|
return;
|
|
3903
4034
|
}
|
|
3904
4035
|
let cancelled = false;
|
|
@@ -3920,7 +4051,7 @@ function StepLayer(p) {
|
|
|
3920
4051
|
placement,
|
|
3921
4052
|
middleware: [
|
|
3922
4053
|
offset(placement.startsWith("bottom") ? gap : 14),
|
|
3923
|
-
shift({ padding: m }),
|
|
4054
|
+
shift({ padding: m, rootBoundary: { x: col.x, y: 0, width: col.width, height: window.innerHeight } }),
|
|
3924
4055
|
...(arrowRef.current ? [arrow({ element: arrowRef.current, padding: 14 })] : []),
|
|
3925
4056
|
],
|
|
3926
4057
|
});
|
|
@@ -3930,11 +4061,11 @@ function StepLayer(p) {
|
|
|
3930
4061
|
return;
|
|
3931
4062
|
const bw = el.offsetWidth;
|
|
3932
4063
|
const bh = el.offsetHeight;
|
|
3933
|
-
const bounds = { x: m, y: viewport.offsetTop + m, width:
|
|
4064
|
+
const bounds = { x: col.x + m, y: viewport.offsetTop + m, width: col.width - 2 * m, height: viewport.height - 2 * m };
|
|
3934
4065
|
const i = pickPlacement(results, bw, bh, a, clear, bounds);
|
|
3935
4066
|
if (i < 0) {
|
|
3936
4067
|
// Nowhere beside it: the bottom-right corner.
|
|
3937
|
-
setPos({ left:
|
|
4068
|
+
setPos({ left: col.x + col.width - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
|
|
3938
4069
|
return;
|
|
3939
4070
|
}
|
|
3940
4071
|
const { x, y, placement, middlewareData } = results[i];
|
|
@@ -3950,7 +4081,7 @@ function StepLayer(p) {
|
|
|
3950
4081
|
};
|
|
3951
4082
|
// The keys stand for spot, the lists, the holes and the bubble's size.
|
|
3952
4083
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3953
|
-
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
4084
|
+
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop, colKey]);
|
|
3954
4085
|
const popovers = p.popovers ?? [];
|
|
3955
4086
|
const covered = !!p.covered;
|
|
3956
4087
|
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
@@ -3971,7 +4102,7 @@ function StepLayer(p) {
|
|
|
3971
4102
|
return (jsxs(Fragment, { children: [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 ? (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 ? (jsx("div", { className: "eg-hand" + (hand.flip ? " eg-flip" : ""), style: { left: hand.left, top: hand.top }, "aria-hidden": "true", children: jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: HAND_PATH }) }) })) : null, showBubble ? (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: {
|
|
3972
4103
|
left: pos?.left ?? 0,
|
|
3973
4104
|
top: pos?.top ?? 0,
|
|
3974
|
-
width: phone ?
|
|
4105
|
+
width: phone ? col.width - 32 : col.width < 352 ? col.width - 32 : undefined,
|
|
3975
4106
|
visibility: pos ? "visible" : "hidden",
|
|
3976
4107
|
}, children: [jsx(BubbleBody, { ...p }), jsx("span", { ref: arrowRef, className: "eg-arrow" + (pos?.side === "bottom" ? " eg-top" : ""), style: arrowStyle ?? { display: "none" }, "aria-hidden": "true" })] })) : null, p.phase === "waiting" ? (jsxs("div", { className: "eg-card", role: "status", "data-eg-card": "", children: [jsxs("div", { className: "eg-loading", children: [jsx("span", { className: "eg-spin", "aria-hidden": "true" }), "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14\u2026"] }), jsx("div", { className: "eg-row", children: jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }) })] })) : null, p.phase === "missing" ? (jsxs("div", { className: "eg-card eg-err", role: "alertdialog", "aria-labelledby": "eg-missing-t", "data-eg-card": "", tabIndex: -1, children: [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" }), jsxs("div", { className: "eg-row", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), 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" ? (jsxs("div", { className: "eg-card", role: "dialog", "aria-modal": "false", "aria-labelledby": "eg-away-t", "data-eg-card": "", tabIndex: -1, children: [jsxs("div", { className: "eg-b-top", children: [jsx("span", { className: "eg-mis", children: headingOf(p.bubble) }), jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", p.bubble.index + 1, "/", p.bubble.total] })] }), jsx("p", { className: "eg-b-t", id: "eg-away-t", style: { marginTop: 8 }, children: jsx(GuideTitle, { text: p.bubble.title }) }), jsxs("div", { className: "eg-row", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onGoResume, children: "\u0E17\u0E33\u0E15\u0E48\u0E2D" })] })] })) : null, p.confirmExit ? (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] }));
|
|
3977
4108
|
}
|
|
@@ -4277,36 +4408,81 @@ const flowMirrorKey = (orgId, suite) => `erp-guide-flow-run:${orgId}:${suite.app
|
|
|
4277
4408
|
/* One tab at a time */
|
|
4278
4409
|
/* ------------------------------------------------------------------ */
|
|
4279
4410
|
/**
|
|
4280
|
-
* Whether this tab runs the tour
|
|
4281
|
-
*
|
|
4411
|
+
* Whether this tab runs the tour: one tab at a time holds a Web Lock and
|
|
4412
|
+
* runs steps, offers, the welcome and saves. Every other tab still reads
|
|
4413
|
+
* progress and shows the "?" menu (shell ≥ 0.1.78); pressing one of its
|
|
4414
|
+
* guide items calls `takeover`, which steals the lock. The tab it was
|
|
4415
|
+
* stolen from goes back to waiting its turn (and yields what it was running).
|
|
4416
|
+
* Without Web Locks every tab runs, as before.
|
|
4282
4417
|
*/
|
|
4283
4418
|
function usePrimaryTab(name) {
|
|
4284
4419
|
const [primary, setPrimary] = useState(false);
|
|
4420
|
+
const primaryRef = useRef(false);
|
|
4421
|
+
const takeoverRef = useRef(() => Promise.resolve());
|
|
4285
4422
|
useEffect(() => {
|
|
4286
4423
|
const locks = (typeof navigator !== "undefined" ? navigator.locks : undefined);
|
|
4424
|
+
const set = (v) => {
|
|
4425
|
+
primaryRef.current = v;
|
|
4426
|
+
setPrimary(v);
|
|
4427
|
+
};
|
|
4287
4428
|
if (!locks?.request) {
|
|
4288
|
-
|
|
4429
|
+
set(true);
|
|
4430
|
+
takeoverRef.current = () => Promise.resolve();
|
|
4289
4431
|
return;
|
|
4290
4432
|
}
|
|
4291
|
-
|
|
4433
|
+
let alive = true;
|
|
4434
|
+
let gen = 0;
|
|
4435
|
+
let pending = null;
|
|
4292
4436
|
let release = null;
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4437
|
+
let waiters = [];
|
|
4438
|
+
const queue = (steal) => {
|
|
4439
|
+
const my = ++gen;
|
|
4440
|
+
const ac = new AbortController();
|
|
4441
|
+
pending = steal ? null : ac;
|
|
4442
|
+
locks
|
|
4443
|
+
.request(name, steal ? { steal: true } : { signal: ac.signal }, () => {
|
|
4444
|
+
if (!alive || my !== gen)
|
|
4445
|
+
return undefined;
|
|
4446
|
+
pending = null;
|
|
4447
|
+
set(true);
|
|
4448
|
+
const w = waiters;
|
|
4449
|
+
waiters = [];
|
|
4450
|
+
w.forEach(f => f());
|
|
4451
|
+
return new Promise(resolve => {
|
|
4452
|
+
release = resolve;
|
|
4453
|
+
});
|
|
4454
|
+
})
|
|
4455
|
+
.catch(() => {
|
|
4456
|
+
// Stolen by another tab (or this request aborted): not the runner;
|
|
4457
|
+
// wait for the lock again.
|
|
4458
|
+
if (!alive || my !== gen)
|
|
4459
|
+
return;
|
|
4460
|
+
release = null;
|
|
4461
|
+
set(false);
|
|
4462
|
+
queue(false);
|
|
4298
4463
|
});
|
|
4299
|
-
}
|
|
4300
|
-
|
|
4301
|
-
|
|
4464
|
+
};
|
|
4465
|
+
queue(false);
|
|
4466
|
+
takeoverRef.current = () => new Promise(resolve => {
|
|
4467
|
+
if (primaryRef.current) {
|
|
4468
|
+
resolve();
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
waiters.push(resolve);
|
|
4472
|
+
pending?.abort();
|
|
4473
|
+
queue(true);
|
|
4302
4474
|
});
|
|
4303
4475
|
return () => {
|
|
4304
|
-
|
|
4476
|
+
alive = false;
|
|
4477
|
+
pending?.abort();
|
|
4305
4478
|
release?.();
|
|
4306
|
-
|
|
4479
|
+
waiters.forEach(f => f());
|
|
4480
|
+
waiters = [];
|
|
4481
|
+
set(false);
|
|
4307
4482
|
};
|
|
4308
4483
|
}, [name]);
|
|
4309
|
-
|
|
4484
|
+
const takeover = useCallback(() => takeoverRef.current(), []);
|
|
4485
|
+
return { primary, takeover };
|
|
4310
4486
|
}
|
|
4311
4487
|
/* ------------------------------------------------------------------ */
|
|
4312
4488
|
/* Putting the page back after a replay */
|
|
@@ -4339,6 +4515,42 @@ function runAbort(actions) {
|
|
|
4339
4515
|
}, ABORT_DELAY_MS + i * ABORT_GAP_MS);
|
|
4340
4516
|
});
|
|
4341
4517
|
}
|
|
4518
|
+
/** The app's content column (GuideConfig.contentRoot), cut to the viewport; null when none is declared or found. */
|
|
4519
|
+
function contentColumn(selector, vw) {
|
|
4520
|
+
if (!selector || typeof document === "undefined")
|
|
4521
|
+
return null;
|
|
4522
|
+
let el = null;
|
|
4523
|
+
try {
|
|
4524
|
+
el = document.querySelector(selector);
|
|
4525
|
+
}
|
|
4526
|
+
catch {
|
|
4527
|
+
return null;
|
|
4528
|
+
}
|
|
4529
|
+
if (!el)
|
|
4530
|
+
return null;
|
|
4531
|
+
const r = el.getBoundingClientRect();
|
|
4532
|
+
const x = Math.max(0, r.left);
|
|
4533
|
+
const right = Math.min(vw || window.innerWidth, r.right);
|
|
4534
|
+
if (right - x <= 0)
|
|
4535
|
+
return null;
|
|
4536
|
+
return { x, y: 0, width: right - x, height: 0 };
|
|
4537
|
+
}
|
|
4538
|
+
/** The control a form's implicit submit (Enter in a field) presses: its first submit button. */
|
|
4539
|
+
function defaultSubmitControl(form) {
|
|
4540
|
+
for (const el of Array.from(form.elements)) {
|
|
4541
|
+
if (el instanceof HTMLButtonElement && (el.type === "submit" || !el.getAttribute("type")))
|
|
4542
|
+
return el;
|
|
4543
|
+
if (el instanceof HTMLInputElement && (el.type === "submit" || el.type === "image"))
|
|
4544
|
+
return el;
|
|
4545
|
+
}
|
|
4546
|
+
return null;
|
|
4547
|
+
}
|
|
4548
|
+
/** A field where Enter submits its form (not a textarea, a button or a select). */
|
|
4549
|
+
function submitsOnEnter(el) {
|
|
4550
|
+
if (!(el instanceof HTMLInputElement))
|
|
4551
|
+
return false;
|
|
4552
|
+
return !["button", "submit", "reset", "image", "file", "hidden"].includes(el.type);
|
|
4553
|
+
}
|
|
4342
4554
|
/** A step's own target takes presses: not on a "view" step, unless the step allows it. */
|
|
4343
4555
|
function targetPressable(step, bridge) {
|
|
4344
4556
|
if (!step || bridge)
|
|
@@ -4379,7 +4591,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4379
4591
|
busyRef.current = busy;
|
|
4380
4592
|
// No menu: never a menu step (the machine sees it as always open).
|
|
4381
4593
|
const sidebarOpen = menu ? !!menu.open : true;
|
|
4382
|
-
const primary = usePrimaryTab(`erp-guide:${organizationId}:${suite.key}`);
|
|
4594
|
+
const { primary, takeover } = usePrimaryTab(`erp-guide:${organizationId}:${suite.key}`);
|
|
4595
|
+
const primaryRef = useRef(primary);
|
|
4596
|
+
primaryRef.current = primary;
|
|
4383
4597
|
// Progress: read once this tab runs the tour and the app is ready (for
|
|
4384
4598
|
// the missions, or for the page guides alone: config.pagesReady).
|
|
4385
4599
|
const [answer, setAnswer] = useState(null);
|
|
@@ -4388,7 +4602,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4388
4602
|
const hasPages = useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0 || validFlows(suite).length > 0, [suite]);
|
|
4389
4603
|
const hasFlows = useMemo(() => validFlows(suite).length > 0, [suite]);
|
|
4390
4604
|
const hasSections = useMemo(() => validSections(suite).length > 0, [suite]);
|
|
4391
|
-
|
|
4605
|
+
// Every tab reads progress (0.1.78): the "?" menu is shown in all of them.
|
|
4606
|
+
const enabled = missionsReady || (hasPages && pagesReady);
|
|
4392
4607
|
const loaded = useRef(false);
|
|
4393
4608
|
useEffect(() => {
|
|
4394
4609
|
if (!enabled || loaded.current)
|
|
@@ -4412,8 +4627,12 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4412
4627
|
}, [enabled, suite]);
|
|
4413
4628
|
// The missions run for their audience; the page guides for theirs (every
|
|
4414
4629
|
// member while pages are on), each with its own ready.
|
|
4415
|
-
|
|
4416
|
-
|
|
4630
|
+
// On: this person has them here (every tab, for the "?" menu). Live: this
|
|
4631
|
+
// tab runs them (the primary tab only): steps, offers, the welcome, saves.
|
|
4632
|
+
const missionsOn = !!answer && answer.audience !== "none" && !answer.newerOnServer && missionsReady && !hidden;
|
|
4633
|
+
const pagesOn = !!answer && hasPages && answer.pageAudience !== "none" && pagesReady && !hidden;
|
|
4634
|
+
const missionsLive = missionsOn && primary;
|
|
4635
|
+
const pagesLive = pagesOn && primary;
|
|
4417
4636
|
const live = missionsLive || pagesLive;
|
|
4418
4637
|
/* ---------------- machine ---------------- */
|
|
4419
4638
|
const [state, setState] = useState(null);
|
|
@@ -4506,6 +4725,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4506
4725
|
const row = await storeGuides(adapterRef.current, suite, cur.progress);
|
|
4507
4726
|
if (row)
|
|
4508
4727
|
savedKey.current = payloadKey(row, suite.app);
|
|
4728
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4729
|
+
if (row)
|
|
4730
|
+
announceSaved();
|
|
4509
4731
|
if (row && stateRef.current) {
|
|
4510
4732
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4511
4733
|
dispatchRef.current({ type: "serverRow", progress: row });
|
|
@@ -4562,6 +4784,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4562
4784
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4563
4785
|
if (row)
|
|
4564
4786
|
dispatchRef.current({ type: "pageRow", key: e.key, row, kind });
|
|
4787
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4788
|
+
if (row)
|
|
4789
|
+
announceSaved();
|
|
4565
4790
|
});
|
|
4566
4791
|
}
|
|
4567
4792
|
}
|
|
@@ -4608,6 +4833,95 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4608
4833
|
clearTimeout(toastTimer.current);
|
|
4609
4834
|
};
|
|
4610
4835
|
}, [flushSave]);
|
|
4836
|
+
/* ---------------- other tabs (0.1.78) ---------------- */
|
|
4837
|
+
// Progress saved in one tab reaches the others: a message on a channel
|
|
4838
|
+
// per organisation and app, and a fresh read when a tab comes back into
|
|
4839
|
+
// view. Only the server's rows are read, never another tab's state.
|
|
4840
|
+
const channelRef = useRef(null);
|
|
4841
|
+
const refreshNow = useCallback(async () => {
|
|
4842
|
+
const a = await loadGuides(adapterRef.current, suite, undefined, true);
|
|
4843
|
+
const cur = stateRef.current;
|
|
4844
|
+
if (!a || !cur)
|
|
4845
|
+
return;
|
|
4846
|
+
// The runner's own state wins while it is running something.
|
|
4847
|
+
if (primaryRef.current && (cur.screen.name !== "idle" || saving.current))
|
|
4848
|
+
return;
|
|
4849
|
+
if (a.progress.guide_version === cur.progress.guide_version && a.progress.rev >= cur.progress.rev) {
|
|
4850
|
+
savedKey.current = a.progress.rev > 0 ? payloadKey(a.progress, suite.app) : null;
|
|
4851
|
+
}
|
|
4852
|
+
dispatchRef.current({ type: "refresh", progress: a.progress, pages: a.pages, sections: a.sections, flows: a.flows });
|
|
4853
|
+
}, [suite]);
|
|
4854
|
+
const refreshRef = useRef(refreshNow);
|
|
4855
|
+
refreshRef.current = refreshNow;
|
|
4856
|
+
useEffect(() => {
|
|
4857
|
+
if (typeof BroadcastChannel === "undefined")
|
|
4858
|
+
return;
|
|
4859
|
+
let ch;
|
|
4860
|
+
try {
|
|
4861
|
+
ch = new BroadcastChannel(`erp-guide:${organizationId}:${suite.app}`);
|
|
4862
|
+
}
|
|
4863
|
+
catch {
|
|
4864
|
+
return;
|
|
4865
|
+
}
|
|
4866
|
+
channelRef.current = ch;
|
|
4867
|
+
ch.onmessage = (e) => {
|
|
4868
|
+
if (e.data?.type === "saved")
|
|
4869
|
+
void refreshRef.current();
|
|
4870
|
+
};
|
|
4871
|
+
return () => {
|
|
4872
|
+
channelRef.current = null;
|
|
4873
|
+
ch.close();
|
|
4874
|
+
};
|
|
4875
|
+
}, [organizationId, suite.app]);
|
|
4876
|
+
useEffect(() => {
|
|
4877
|
+
const onVisible = () => {
|
|
4878
|
+
if (document.visibilityState === "visible" && stateRef.current)
|
|
4879
|
+
void refreshRef.current();
|
|
4880
|
+
};
|
|
4881
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
4882
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
4883
|
+
}, []);
|
|
4884
|
+
function announceSaved() {
|
|
4885
|
+
try {
|
|
4886
|
+
channelRef.current?.postMessage({ type: "saved" });
|
|
4887
|
+
}
|
|
4888
|
+
catch {
|
|
4889
|
+
/* a closed channel: the next visibility change reads again */
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
// The tab that lost the lock stops cleanly: whatever it was running ends
|
|
4893
|
+
// silently, without writing.
|
|
4894
|
+
useEffect(() => {
|
|
4895
|
+
if (primary || !stateRef.current)
|
|
4896
|
+
return;
|
|
4897
|
+
const cur = stateRef.current;
|
|
4898
|
+
if (saveTimer.current) {
|
|
4899
|
+
clearTimeout(saveTimer.current);
|
|
4900
|
+
saveTimer.current = null;
|
|
4901
|
+
}
|
|
4902
|
+
if (cur.screen.name !== "idle" || cur.panelOpen)
|
|
4903
|
+
dispatchRef.current({ type: "yield" });
|
|
4904
|
+
}, [primary]);
|
|
4905
|
+
/**
|
|
4906
|
+
* A guide item pressed in this tab: run it here. In a tab that is not the
|
|
4907
|
+
* runner, take the lock first (the other tab stops), read progress fresh,
|
|
4908
|
+
* then do it. Nothing resumes by itself in a tab taken over this way.
|
|
4909
|
+
*/
|
|
4910
|
+
const viaRunner = useCallback((fn) => {
|
|
4911
|
+
if (primaryRef.current) {
|
|
4912
|
+
fn();
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
resumed.current = true;
|
|
4916
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
4917
|
+
flowResumed.current = true;
|
|
4918
|
+
void takeover()
|
|
4919
|
+
.then(() => refreshRef.current().catch(() => undefined))
|
|
4920
|
+
.then(() => {
|
|
4921
|
+
if (primaryRef.current)
|
|
4922
|
+
fn();
|
|
4923
|
+
});
|
|
4924
|
+
}, [takeover]);
|
|
4611
4925
|
/* ---------------- boot: resume, welcome ---------------- */
|
|
4612
4926
|
const screen = state?.screen ?? { name: "idle" };
|
|
4613
4927
|
const resumed = useRef(false);
|
|
@@ -4717,6 +5031,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4717
5031
|
const stepId = screen.name === "step" ? `${screen.mission}:${screen.index}:${screen.bridge ? "b" : ""}${screen.nothingToSave ? "n" : ""}` : "";
|
|
4718
5032
|
/* Measure every frame while a step is shown: the spotlight follows
|
|
4719
5033
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
5034
|
+
const contentRoot = config.contentRoot;
|
|
4720
5035
|
const [layout, setLayout] = useState({
|
|
4721
5036
|
spot: null,
|
|
4722
5037
|
covered: false,
|
|
@@ -4725,6 +5040,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4725
5040
|
vw: 0,
|
|
4726
5041
|
vh: 0,
|
|
4727
5042
|
oy: 0,
|
|
5043
|
+
col: null,
|
|
4728
5044
|
});
|
|
4729
5045
|
useEffect(() => {
|
|
4730
5046
|
if (!inStep) {
|
|
@@ -4733,7 +5049,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4733
5049
|
}
|
|
4734
5050
|
let raf = 0;
|
|
4735
5051
|
let lostSince = null;
|
|
4736
|
-
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
|
|
5052
|
+
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0, col: null };
|
|
4737
5053
|
targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
|
|
4738
5054
|
const allow = stepInfo?.step.allow ?? [];
|
|
4739
5055
|
// The spotlight is the part of the target a person can see: cut by the
|
|
@@ -4793,13 +5109,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4793
5109
|
holes.push(padRect(rectOf(ael), TARGET_PAD));
|
|
4794
5110
|
}
|
|
4795
5111
|
const covered = !!spot && coveredByDialog(targetElRef.current);
|
|
4796
|
-
const
|
|
5112
|
+
const col = contentColumn(contentRoot, vw);
|
|
5113
|
+
const next = { spot, covered, holes, popovers, vw, vh, oy, col };
|
|
4797
5114
|
const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
|
|
4798
5115
|
if (!sameRect(next.spot, last.spot) ||
|
|
4799
5116
|
next.covered !== last.covered ||
|
|
4800
5117
|
next.vw !== last.vw ||
|
|
4801
5118
|
next.vh !== last.vh ||
|
|
4802
5119
|
next.oy !== last.oy ||
|
|
5120
|
+
!sameRect(next.col, last.col) ||
|
|
4803
5121
|
!sameList(next.holes, last.holes) ||
|
|
4804
5122
|
!sameList(next.popovers, last.popovers)) {
|
|
4805
5123
|
last = next;
|
|
@@ -4954,6 +5272,43 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4954
5272
|
}
|
|
4955
5273
|
return false;
|
|
4956
5274
|
};
|
|
5275
|
+
/**
|
|
5276
|
+
* Whether a form may be submitted during this step (shell ≥ 0.1.77): only
|
|
5277
|
+
* when its submit control — the button pressed, or the one Enter in a
|
|
5278
|
+
* field would press — is the step's pressable target or in its `allow`.
|
|
5279
|
+
* Otherwise Enter in an allowed field would save a draft (and take a
|
|
5280
|
+
* document number) behind the guide's back.
|
|
5281
|
+
*/
|
|
5282
|
+
const submitAllowed = (form, submitter) => {
|
|
5283
|
+
const btn = submitter ?? defaultSubmitControl(form);
|
|
5284
|
+
if (!btn)
|
|
5285
|
+
return false;
|
|
5286
|
+
const el = targetElRef.current;
|
|
5287
|
+
const sc = stateRef.current?.screen;
|
|
5288
|
+
const pressable = targetPressable(stepInfoRef.current?.step, sc?.name === "step" && sc.bridge);
|
|
5289
|
+
if (el && pressable && el.contains(btn))
|
|
5290
|
+
return true;
|
|
5291
|
+
for (const a of stepInfoRef.current?.step.allow ?? []) {
|
|
5292
|
+
const ael = findAnchor(a);
|
|
5293
|
+
if (ael && ael.contains(btn))
|
|
5294
|
+
return true;
|
|
5295
|
+
}
|
|
5296
|
+
return false;
|
|
5297
|
+
};
|
|
5298
|
+
const onSubmit = (e) => {
|
|
5299
|
+
const sc = stateRef.current?.screen;
|
|
5300
|
+
if (!guardingRef.current || sc?.name !== "step")
|
|
5301
|
+
return;
|
|
5302
|
+
const form = e.target;
|
|
5303
|
+
if (!(form instanceof HTMLFormElement) || inGuideLayer(form))
|
|
5304
|
+
return;
|
|
5305
|
+
const submitter = e.submitter ?? null;
|
|
5306
|
+
if (submitAllowed(form, submitter))
|
|
5307
|
+
return;
|
|
5308
|
+
e.preventDefault();
|
|
5309
|
+
e.stopPropagation();
|
|
5310
|
+
dispatchRef.current({ type: "blocked" });
|
|
5311
|
+
};
|
|
4957
5312
|
const onPointer = (e) => {
|
|
4958
5313
|
const t = e.target;
|
|
4959
5314
|
if (inGuideLayer(t)) {
|
|
@@ -5073,8 +5428,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5073
5428
|
return;
|
|
5074
5429
|
}
|
|
5075
5430
|
}
|
|
5076
|
-
if (modalRef.current || !allowedTarget(active))
|
|
5431
|
+
if (modalRef.current || !allowedTarget(active)) {
|
|
5077
5432
|
stop();
|
|
5433
|
+
return;
|
|
5434
|
+
}
|
|
5435
|
+
// Enter in a field the step lets the person type in: no implicit
|
|
5436
|
+
// submit unless the form's submit control is allowed now.
|
|
5437
|
+
if (e.key === "Enter" && sc.name === "step" && submitsOnEnter(active) && active.form && !submitAllowed(active.form, null)) {
|
|
5438
|
+
e.preventDefault();
|
|
5439
|
+
}
|
|
5078
5440
|
}
|
|
5079
5441
|
};
|
|
5080
5442
|
window.addEventListener("pointerdown", onPointer, true);
|
|
@@ -5084,7 +5446,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5084
5446
|
window.addEventListener("focusin", onFocusIn, true);
|
|
5085
5447
|
window.addEventListener("focusout", onFocusOut, true);
|
|
5086
5448
|
window.addEventListener("keydown", onKey, true);
|
|
5449
|
+
window.addEventListener("submit", onSubmit, true);
|
|
5087
5450
|
return () => {
|
|
5451
|
+
window.removeEventListener("submit", onSubmit, true);
|
|
5088
5452
|
window.removeEventListener("pointerdown", onPointer, true);
|
|
5089
5453
|
window.removeEventListener("mousedown", onPointer, true);
|
|
5090
5454
|
window.removeEventListener("touchstart", onPointer, true);
|
|
@@ -5206,12 +5570,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5206
5570
|
!due;
|
|
5207
5571
|
useAfterIdle(passiveDue, PASSIVE_IDLE_MS, busy, () => dispatchRef.current({ type: "idleCelebrate" }), [state?.pendingCelebrate?.join(",")]);
|
|
5208
5572
|
useEffect(() => {
|
|
5209
|
-
|
|
5573
|
+
// Every tab shows "ไกด์เริ่มต้น n/N"; pressing it runs it in this tab.
|
|
5574
|
+
if (!missionsOn || !state) {
|
|
5210
5575
|
publishGuideEntry(null, null);
|
|
5211
5576
|
return;
|
|
5212
5577
|
}
|
|
5213
|
-
publishGuideEntry({ done, total }, () => dispatchRef.current({ type: "openPanel" }));
|
|
5214
|
-
}, [
|
|
5578
|
+
publishGuideEntry({ done, total }, () => viaRunner(() => dispatchRef.current({ type: "openPanel" })));
|
|
5579
|
+
}, [missionsOn, state, done, total, viaRunner]);
|
|
5215
5580
|
useEffect(() => () => publishGuideEntry(null, null), []);
|
|
5216
5581
|
/* ---------------- page guides: this page's, the offer, the list ---------------- */
|
|
5217
5582
|
// `when` anchors (LIFF's sub-screens) come and go without a route change:
|
|
@@ -5219,7 +5584,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5219
5584
|
const hasWhen = useMemo(() => validPages(suite).some(p => !!p.when) || validSections(suite).length > 0, [suite]);
|
|
5220
5585
|
const [domTick, setDomTick] = useState(0);
|
|
5221
5586
|
useEffect(() => {
|
|
5222
|
-
if (!
|
|
5587
|
+
if (!pagesOn || !hasWhen || typeof MutationObserver === "undefined")
|
|
5223
5588
|
return;
|
|
5224
5589
|
let t = null;
|
|
5225
5590
|
const obs = new MutationObserver(records => {
|
|
@@ -5243,18 +5608,18 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5243
5608
|
if (t)
|
|
5244
5609
|
clearTimeout(t);
|
|
5245
5610
|
};
|
|
5246
|
-
}, [
|
|
5611
|
+
}, [pagesOn, hasWhen]);
|
|
5247
5612
|
const hasState = !!state;
|
|
5248
|
-
const pageNow = useMemo(() => (
|
|
5613
|
+
const pageNow = useMemo(() => (pagesOn && hasState ? pageHere(suite, env()) : null),
|
|
5249
5614
|
// pathname/search/factsKey/domTick make it current.
|
|
5250
5615
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5251
|
-
[
|
|
5252
|
-
const listed = useMemo(() => (
|
|
5253
|
-
const sectionsNow = useMemo(() =>
|
|
5616
|
+
[pagesOn, hasState, suite, env, pathname, search, factsKey, domTick]);
|
|
5617
|
+
const listed = useMemo(() => (pagesOn && ctxIdle ? listedPages(suite, ctxIdle) : []), [pagesOn, suite, ctxIdle]);
|
|
5618
|
+
const sectionsNow = useMemo(() => pagesOn && hasState && hasSections
|
|
5254
5619
|
? sectionsAvailable(suite, env()).map(k => ({ key: k, title: findSection(suite, k)?.title ?? "" }))
|
|
5255
5620
|
: [],
|
|
5256
5621
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5257
|
-
[
|
|
5622
|
+
[pagesOn, hasState, hasSections, suite, env, pathname, search, factsKey, domTick]);
|
|
5258
5623
|
const sectionsSig = sectionsNow.map(x => x.key).join(",");
|
|
5259
5624
|
const openSection = useCallback((key) => {
|
|
5260
5625
|
openingSection.current = key;
|
|
@@ -5265,14 +5630,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5265
5630
|
openingSection.current = null;
|
|
5266
5631
|
}
|
|
5267
5632
|
}, []);
|
|
5268
|
-
const flowsNow = useMemo(() =>
|
|
5633
|
+
const flowsNow = useMemo(() => pagesOn && hasState && hasFlows
|
|
5269
5634
|
? flowsAvailable(suite, env()).map(k => ({ key: k, title: findFlow(suite, k)?.title ?? "" }))
|
|
5270
5635
|
: [],
|
|
5271
5636
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5272
|
-
[
|
|
5637
|
+
[pagesOn, hasState, hasFlows, suite, env, pathname, search, factsKey]);
|
|
5273
5638
|
const flowsSig = flowsNow.map(x => x.key).join(",");
|
|
5274
5639
|
/** The flows the panel lists: this person's, startable here or with somewhere to go. */
|
|
5275
|
-
const flowsListed = useMemo(() => (
|
|
5640
|
+
const flowsListed = 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]);
|
|
5276
5641
|
const openFlowHere = useCallback((key) => dispatchRef.current({ type: "startFlow", flow: key }), []);
|
|
5277
5642
|
const openPageHere = useCallback(() => {
|
|
5278
5643
|
const p = pageHere(suite, env());
|
|
@@ -5280,7 +5645,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5280
5645
|
dispatchRef.current({ type: "startPage", page: p.key });
|
|
5281
5646
|
}, [suite, env]);
|
|
5282
5647
|
useEffect(() => {
|
|
5283
|
-
if (!
|
|
5648
|
+
if (!pagesOn || !hasState) {
|
|
5284
5649
|
publishPageGuides(null, null, null);
|
|
5285
5650
|
return;
|
|
5286
5651
|
}
|
|
@@ -5290,10 +5655,10 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5290
5655
|
sections: sectionsNow,
|
|
5291
5656
|
flows: flowsNow,
|
|
5292
5657
|
flowsListed: flowsListed.length,
|
|
5293
|
-
}, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }), openSection, openFlowHere, () => dispatchRef.current({ type: "openPanel", section: "flows" }));
|
|
5658
|
+
}, () => viaRunner(openPageHere), () => viaRunner(() => dispatchRef.current({ type: "openPanel", section: "pages" })), (key) => viaRunner(() => openSection(key)), (key) => viaRunner(() => openFlowHere(key)), () => viaRunner(() => dispatchRef.current({ type: "openPanel", section: "flows" })));
|
|
5294
5659
|
// sectionsSig / flowsSig stand for sectionsNow / flowsNow.
|
|
5295
5660
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5296
|
-
}, [
|
|
5661
|
+
}, [pagesOn, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere, viaRunner]);
|
|
5297
5662
|
useEffect(() => () => publishPageGuides(null, null, null), []);
|
|
5298
5663
|
// The first-visit offer: in "offer" mode, nothing else up, the welcome
|
|
5299
5664
|
// not due here, and a moment of nothing loading on this page.
|
|
@@ -5512,7 +5877,11 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5512
5877
|
const missionNo = missionNumber(suite, mission.key, ctxIdle);
|
|
5513
5878
|
const kind = stepKind(step);
|
|
5514
5879
|
const bridge = screen.bridge;
|
|
5515
|
-
|
|
5880
|
+
// What is ahead now decides เสร็จ; the number shown is the run's counter,
|
|
5881
|
+
// which never goes down (0.1.77).
|
|
5882
|
+
const raw = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
|
|
5883
|
+
const last = raw.no >= raw.total;
|
|
5884
|
+
const pos = screen.pos ?? raw;
|
|
5516
5885
|
const page = screen.page ? findPage(suite, screen.page) : undefined;
|
|
5517
5886
|
const section = screen.section ? findSection(suite, screen.section) : undefined;
|
|
5518
5887
|
const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
|
|
@@ -5521,16 +5890,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5521
5890
|
? {
|
|
5522
5891
|
...read,
|
|
5523
5892
|
showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
|
|
5524
|
-
nextLabel:
|
|
5893
|
+
nextLabel: last ? "เสร็จ" : undefined,
|
|
5525
5894
|
}
|
|
5526
5895
|
: flow
|
|
5527
5896
|
? {
|
|
5528
5897
|
heading: `พาทำ · ${flow.title}`,
|
|
5529
5898
|
exitLabel: "ออกจากการพาทำ",
|
|
5530
|
-
nextLabel: kind === "view" &&
|
|
5899
|
+
nextLabel: kind === "view" && last ? "เสร็จ" : undefined,
|
|
5531
5900
|
}
|
|
5532
5901
|
: {};
|
|
5533
|
-
content = (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: {
|
|
5902
|
+
content = (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: {
|
|
5534
5903
|
missionNo,
|
|
5535
5904
|
missionTitle: mission.title,
|
|
5536
5905
|
replay: screen.replay,
|
|
@@ -5713,4 +6082,4 @@ function thaiMonthYear(m, opts = {}) {
|
|
|
5713
6082
|
}
|
|
5714
6083
|
|
|
5715
6084
|
export { API_EVENT as A, useGuideEntry as B, usePageGuideEntry as C, usePageGuides as D, useSectionGuideValues as E, GUIDE_EVENT_PREFIX as G, SectionGuideButton as S, THAI_MONTHS_SHORT as T, GUIDE_LAYER_ATTRIBUTE as a, GuideErrorBoundary as b, GuideProgressConflict as c, GuideRuntime as d, apiCallDetail as e, emitGuideEvent as f, fillRoute as g, formatBaht as h, guideAnchor as i, guideMenuItems as j, guideMenuShown as k, loadGuides as l, matchApiPath as m, matchRoute as n, openFlow as o, openFlowsPanel as p, openGuidePanel as q, openPageGuide as r, openPagesPanel as s, openSectionGuide as t, reportApiCall as u, reportApiEvent as v, setSectionGuideValues as w, storeGuides as x, thaiMonth as y, thaiMonthYear as z };
|
|
5716
|
-
//# sourceMappingURL=format-
|
|
6085
|
+
//# sourceMappingURL=format-Wgj2sKpK.esm.js.map
|