@eintrek/erp-shell 0.1.75 → 0.1.77
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 +52 -0
- package/dist/chunks/{format-CVazK0hP.cjs → format-DQLPHq7h.cjs} +728 -63
- package/dist/chunks/format-DQLPHq7h.cjs.map +1 -0
- package/dist/chunks/{format-DCTuLpBU.esm.js → format-ILrTMV4G.esm.js} +727 -64
- package/dist/chunks/format-ILrTMV4G.esm.js.map +1 -0
- package/dist/guide/core.d.ts +2 -2
- package/dist/guide/guide-store.d.ts +18 -3
- package/dist/guide/guide-ui.d.ts +22 -2
- package/dist/guide/machine.d.ts +85 -7
- package/dist/guide/pages.d.ts +29 -1
- package/dist/guide/progress-sync.d.ts +1 -1
- package/dist/guide/progress.d.ts +5 -3
- package/dist/guide/types.d.ts +59 -0
- package/dist/guide-core.cjs +3 -1
- package/dist/guide-core.cjs.map +1 -1
- package/dist/guide-core.esm.js +1 -1
- package/dist/help/help-button.d.ts +4 -2
- package/dist/index.cjs +11 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +12 -8
- package/dist/index.esm.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunks/format-CVazK0hP.cjs.map +0 -1
- package/dist/chunks/format-DCTuLpBU.esm.js.map +0 -1
|
@@ -357,26 +357,34 @@ function publishGuideEntry(next, openPanel) {
|
|
|
357
357
|
emit();
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
const NO_PAGES = { page: null, listed: 0, sections: [] };
|
|
360
|
+
const NO_PAGES = { page: null, listed: 0, sections: [], flows: [], flowsListed: 0 };
|
|
361
361
|
let pages = NO_PAGES;
|
|
362
362
|
let openPageImpl = null;
|
|
363
363
|
let openPagesImpl = null;
|
|
364
364
|
let openSectionImpl = null;
|
|
365
|
+
let openFlowImpl = null;
|
|
366
|
+
let openFlowsImpl = null;
|
|
365
367
|
const sectionsKey = (x) => (x ?? []).map(s => `${s.key}\u0000${s.title}`).join("\u0001");
|
|
366
368
|
/** Called by the host: this page's guide, the listed count, the sections on screen, and how to open them. */
|
|
367
|
-
function publishPageGuides(next, openPage, openPages, openSection = null) {
|
|
369
|
+
function publishPageGuides(next, openPage, openPages, openSection = null, openFlow = null, openFlows = null) {
|
|
368
370
|
const n = next ?? NO_PAGES;
|
|
369
371
|
openPageImpl = openPage;
|
|
370
372
|
openPagesImpl = openPages;
|
|
371
373
|
openSectionImpl = openSection;
|
|
374
|
+
openFlowImpl = openFlow;
|
|
375
|
+
openFlowsImpl = openFlows;
|
|
372
376
|
const same = n.listed === pages.listed &&
|
|
377
|
+
(n.flowsListed ?? 0) === (pages.flowsListed ?? 0) &&
|
|
373
378
|
(n.page?.title ?? null) === (pages.page?.title ?? null) &&
|
|
374
|
-
sectionsKey(n.sections) === sectionsKey(pages.sections)
|
|
379
|
+
sectionsKey(n.sections) === sectionsKey(pages.sections) &&
|
|
380
|
+
sectionsKey(n.flows) === sectionsKey(pages.flows);
|
|
375
381
|
if (!same) {
|
|
376
382
|
const sections = (n.sections ?? []).map(x => ({ key: x.key, title: x.title }));
|
|
383
|
+
const flows = (n.flows ?? []).map(x => ({ key: x.key, title: x.title }));
|
|
384
|
+
const flowsListed = n.flowsListed ?? 0;
|
|
377
385
|
pages =
|
|
378
|
-
n.page || n.listed || sections.length
|
|
379
|
-
? { page: n.page ? { title: n.page.title } : null, listed: n.listed, sections }
|
|
386
|
+
n.page || n.listed || sections.length || flows.length || flowsListed
|
|
387
|
+
? { page: n.page ? { title: n.page.title } : null, listed: n.listed, sections, flows, flowsListed }
|
|
380
388
|
: NO_PAGES;
|
|
381
389
|
emit();
|
|
382
390
|
}
|
|
@@ -387,10 +395,14 @@ function guideMenuItems(e, p = NO_PAGES) {
|
|
|
387
395
|
items.push("page");
|
|
388
396
|
if ((p.sections ?? []).length > 0)
|
|
389
397
|
items.push("sections");
|
|
398
|
+
if ((p.flows ?? []).length > 0)
|
|
399
|
+
items.push("flowsHere");
|
|
390
400
|
if (e && e.total > 0)
|
|
391
401
|
items.push("missions");
|
|
392
402
|
if (p.listed > 0)
|
|
393
403
|
items.push("pages");
|
|
404
|
+
if ((p.flowsListed ?? 0) > 0)
|
|
405
|
+
items.push("flows");
|
|
394
406
|
if (items.length)
|
|
395
407
|
items.push("help");
|
|
396
408
|
return items;
|
|
@@ -444,6 +456,14 @@ function usePageGuideEntry() {
|
|
|
444
456
|
function openSectionGuide(key) {
|
|
445
457
|
openSectionImpl?.(key);
|
|
446
458
|
}
|
|
459
|
+
/** Starts a flow that starts on this page ("พาทำ · <title>"). */
|
|
460
|
+
function openFlow(key) {
|
|
461
|
+
openFlowImpl?.(key);
|
|
462
|
+
}
|
|
463
|
+
/** Opens the panel on the flows alone ("พาทำงาน"). */
|
|
464
|
+
function openFlowsPanel() {
|
|
465
|
+
openFlowsImpl?.();
|
|
466
|
+
}
|
|
447
467
|
const sectionValues = new Map();
|
|
448
468
|
const valueListeners = new Set();
|
|
449
469
|
let valuesVersion = 0;
|
|
@@ -821,6 +841,144 @@ function sectionShown(section, ctx) {
|
|
|
821
841
|
function sectionsHere(suite, where, ctx) {
|
|
822
842
|
return validSections(suite).filter(s => sectionApplies(s, where) && sectionShown(s, ctx));
|
|
823
843
|
}
|
|
844
|
+
/* ------------------------------------------------------------------ */
|
|
845
|
+
/* Workflow guides ("พาทำงาน", shell 0.1.76) */
|
|
846
|
+
/* ------------------------------------------------------------------ */
|
|
847
|
+
/** The synthetic mission key of a flow: `flow:<key>`. */
|
|
848
|
+
const FLOW_PREFIX = "flow:";
|
|
849
|
+
function isFlowMissionKey(key) {
|
|
850
|
+
return !!key && key.startsWith(FLOW_PREFIX);
|
|
851
|
+
}
|
|
852
|
+
function flowKeyOf(missionKey) {
|
|
853
|
+
return isFlowMissionKey(missionKey) ? missionKey.slice(FLOW_PREFIX.length) : undefined;
|
|
854
|
+
}
|
|
855
|
+
const asList = (r) => (r === undefined ? [] : Array.isArray(r) ? r : [r]);
|
|
856
|
+
/** Why a flow cannot run, or null when it can. */
|
|
857
|
+
function flowProblem(app, flow) {
|
|
858
|
+
if (!flow || typeof flow.key !== "string" || !PAGE_KEY_RE.test(flow.key))
|
|
859
|
+
return "its key is not [a-z0-9-]";
|
|
860
|
+
if (`${app}.flow.${flow.key}`.length > ROW_KEY_MAX)
|
|
861
|
+
return `its row key is over ${ROW_KEY_MAX} characters`;
|
|
862
|
+
if (!Number.isInteger(flow.version) || flow.version < 1)
|
|
863
|
+
return "its version is not a whole number ≥ 1";
|
|
864
|
+
const starts = asList(flow.startRoute);
|
|
865
|
+
if (!starts.length || starts.some(r => typeof r !== "string" || !r.startsWith("/")))
|
|
866
|
+
return "it has no start route";
|
|
867
|
+
if (!Array.isArray(flow.steps) || flow.steps.length === 0)
|
|
868
|
+
return "it has no steps";
|
|
869
|
+
for (const st of flow.steps) {
|
|
870
|
+
const kind = st.kind ?? "click";
|
|
871
|
+
if (kind === "view" || kind === "click")
|
|
872
|
+
continue;
|
|
873
|
+
if (!flow.allowCommit)
|
|
874
|
+
return `step "${st.key}" is a ${kind} step and the flow does not set allowCommit`;
|
|
875
|
+
if (kind === "commit" && !st.abort)
|
|
876
|
+
return `commit step "${st.key}" has no abort`;
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
const validFlowCache = new WeakMap();
|
|
881
|
+
/** The suite's flows that can run (as validPages: the rest dropped, with a warning in development). */
|
|
882
|
+
function validFlows(suite) {
|
|
883
|
+
const hit = validFlowCache.get(suite);
|
|
884
|
+
if (hit)
|
|
885
|
+
return hit;
|
|
886
|
+
const out = [];
|
|
887
|
+
const seen = new Set();
|
|
888
|
+
for (const flow of suite.flows ?? []) {
|
|
889
|
+
let problem = flowProblem(suite.app, flow);
|
|
890
|
+
if (!problem && seen.has(flow.key))
|
|
891
|
+
problem = "another flow has the same key";
|
|
892
|
+
if (problem) {
|
|
893
|
+
const id = `${suite.key}:flow:${flow?.key}:${problem}`;
|
|
894
|
+
if (!warned.has(id)) {
|
|
895
|
+
warned.add(id);
|
|
896
|
+
devWarn(`flow "${flow?.key}" dropped: ${problem}`);
|
|
897
|
+
}
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
seen.add(flow.key);
|
|
901
|
+
out.push(flow);
|
|
902
|
+
}
|
|
903
|
+
validFlowCache.set(suite, out);
|
|
904
|
+
return out;
|
|
905
|
+
}
|
|
906
|
+
function findFlow(suite, key) {
|
|
907
|
+
return key ? validFlows(suite).find(f => f.key === key) : undefined;
|
|
908
|
+
}
|
|
909
|
+
const flowMissionCache = new WeakMap();
|
|
910
|
+
/** The flow as a mission the machine plays (in replay mode): `flow:<key>`, its steps as authored. */
|
|
911
|
+
function flowMission(flow) {
|
|
912
|
+
const hit = flowMissionCache.get(flow);
|
|
913
|
+
if (hit)
|
|
914
|
+
return hit;
|
|
915
|
+
const m = { key: FLOW_PREFIX + flow.key, title: flow.title, minutes: flow.minutes ?? 1, steps: flow.steps };
|
|
916
|
+
flowMissionCache.set(flow, m);
|
|
917
|
+
return m;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* The flow's chain: its start routes, every step's `route` and route
|
|
921
|
+
* trigger, and its `transit` routes. Leaving all of them ends the flow.
|
|
922
|
+
*/
|
|
923
|
+
function flowChain(flow) {
|
|
924
|
+
const out = new Set(asList(flow.startRoute));
|
|
925
|
+
for (const st of flow.steps) {
|
|
926
|
+
if (st.route)
|
|
927
|
+
out.add(st.route);
|
|
928
|
+
const triggers = st.advanceOn ? (Array.isArray(st.advanceOn) ? st.advanceOn : [st.advanceOn]) : [];
|
|
929
|
+
for (const t of triggers)
|
|
930
|
+
if ("route" in t && t.route)
|
|
931
|
+
out.add(t.route);
|
|
932
|
+
}
|
|
933
|
+
for (const t of flow.transit ?? [])
|
|
934
|
+
out.add(t);
|
|
935
|
+
return Array.from(out);
|
|
936
|
+
}
|
|
937
|
+
/** Path patterns only (`?k=v` dropped): the chain is about pages, not their tabs. */
|
|
938
|
+
const pathOf = (pattern) => (pattern.includes("?") ? pattern.slice(0, pattern.indexOf("?")) : pattern);
|
|
939
|
+
/** Whether the person is on one of the flow's pages. */
|
|
940
|
+
function inFlowChain(flow, where) {
|
|
941
|
+
return flowChain(flow).some(r => !!matchRoute(pathOf(r), where.pathname, null, where.code));
|
|
942
|
+
}
|
|
943
|
+
/** Whether the person is on the flow's start page. */
|
|
944
|
+
function onFlowStart(flow, where) {
|
|
945
|
+
return asList(flow.startRoute).some(r => !!matchRoute(r, where.pathname, where.search, where.code));
|
|
946
|
+
}
|
|
947
|
+
/** Whether this person has the flow: `visible` (only true) and `requires`. */
|
|
948
|
+
function flowShown(flow, ctx) {
|
|
949
|
+
try {
|
|
950
|
+
if (flow.visible && flow.visible(ctx) !== true)
|
|
951
|
+
return false;
|
|
952
|
+
if (flow.requires && !ctx.can(flow.requires))
|
|
953
|
+
return false;
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/** The flows this person has (the panel's "พาทำงาน" list), in the suite's order. */
|
|
961
|
+
function shownFlows(suite, ctx) {
|
|
962
|
+
return validFlows(suite).filter(f => flowShown(f, ctx));
|
|
963
|
+
}
|
|
964
|
+
/** Where the panel sends the person to start a flow, or null when it needs an id it does not have. */
|
|
965
|
+
function flowHref(flow, ctx) {
|
|
966
|
+
let raw;
|
|
967
|
+
try {
|
|
968
|
+
raw = flow.href === undefined ? undefined : typeof flow.href === "function" ? flow.href(ctx) : flow.href;
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
raw = undefined;
|
|
972
|
+
}
|
|
973
|
+
if (raw)
|
|
974
|
+
return /^https?:\/\//i.test(raw) ? raw : (fillRoute(raw, { code: ctx.code }) ?? null);
|
|
975
|
+
for (const r of asList(flow.startRoute)) {
|
|
976
|
+
const filled = fillRoute(r, { code: ctx.code });
|
|
977
|
+
if (filled)
|
|
978
|
+
return filled;
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
824
982
|
|
|
825
983
|
/* ------------------------------------------------------------------ */
|
|
826
984
|
/* Copy */
|
|
@@ -836,6 +994,7 @@ const COPY = {
|
|
|
836
994
|
aborted: "ไกด์ไปต่อไม่ได้ ลองอีกครั้งภายหลัง",
|
|
837
995
|
pageDone: "จบคู่มือหน้านี้",
|
|
838
996
|
sectionDone: "จบคำอธิบายส่วนนี้",
|
|
997
|
+
flowDone: "พาทำงานเสร็จแล้ว",
|
|
839
998
|
bridgeTitle: "กดปุ่มเมนู",
|
|
840
999
|
bridgeWhy: (app) => `เมนูทั้งหมดของ ${app} อยู่ในนี้`,
|
|
841
1000
|
};
|
|
@@ -849,9 +1008,11 @@ function initialGuideState(audience, progress, pages) {
|
|
|
849
1008
|
pagesMode: pages?.pagesMode ?? "off",
|
|
850
1009
|
pages: pages?.pages ?? {},
|
|
851
1010
|
sections: pages?.sections ?? {},
|
|
1011
|
+
flows: pages?.flows ?? {},
|
|
852
1012
|
pageRun: null,
|
|
853
1013
|
panelSection: "all",
|
|
854
1014
|
autoPassed: null,
|
|
1015
|
+
counter: null,
|
|
855
1016
|
audience,
|
|
856
1017
|
progress,
|
|
857
1018
|
screen: { name: "idle" },
|
|
@@ -875,6 +1036,10 @@ function findMission(suite, key) {
|
|
|
875
1036
|
const section = findSection(suite, sectionKeyOf(key));
|
|
876
1037
|
return section ? sectionMission(section) : undefined;
|
|
877
1038
|
}
|
|
1039
|
+
if (isFlowMissionKey(key)) {
|
|
1040
|
+
const flow = findFlow(suite, flowKeyOf(key));
|
|
1041
|
+
return flow ? flowMission(flow) : undefined;
|
|
1042
|
+
}
|
|
878
1043
|
return suite.missions.find(m => m.key === key);
|
|
879
1044
|
}
|
|
880
1045
|
/**
|
|
@@ -1226,7 +1391,7 @@ function announceStep(d, suite, env) {
|
|
|
1226
1391
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
1227
1392
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
1228
1393
|
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
1229
|
-
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));
|
|
1230
1395
|
d.effects.push({
|
|
1231
1396
|
type: "announce",
|
|
1232
1397
|
text: `ขั้นที่ ${pos.no} จาก ${pos.total}. ${title}${why ? `. ${why}` : ""}`,
|
|
@@ -1260,6 +1425,54 @@ function stepPosition(mission, index, env, context, autoPassed = []) {
|
|
|
1260
1425
|
});
|
|
1261
1426
|
return { no: Math.max(no, 1), total: Math.max(total, 1) };
|
|
1262
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
|
+
}
|
|
1263
1476
|
/** A step entered now would pass at once: a click or select step whose route or visible outcome holds. */
|
|
1264
1477
|
function wouldAutoPass(step, env) {
|
|
1265
1478
|
const kind = stepKind(step);
|
|
@@ -1323,11 +1536,13 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1323
1536
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
1324
1537
|
const page = pageKeyOf(mission.key);
|
|
1325
1538
|
const section = sectionKeyOf(mission.key);
|
|
1539
|
+
const flow = flowKeyOf(mission.key);
|
|
1326
1540
|
const where = placeRoute(steps, i);
|
|
1327
1541
|
// A page or section guide is on its page: leaving it ends the guide (never "ทำต่อ").
|
|
1328
1542
|
const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
|
|
1329
1543
|
const bridge = !away && needsBridge(step, env, ctx);
|
|
1330
1544
|
const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
|
|
1545
|
+
const pos = countStep(d, env, mission, i);
|
|
1331
1546
|
setScreen(d, {
|
|
1332
1547
|
name: "step",
|
|
1333
1548
|
mission: mission.key,
|
|
@@ -1344,6 +1559,8 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1344
1559
|
replayNext: !away && !bridge && opts.replay && replayCanGoOn(mission, i, env, d.state.progress.context, nts),
|
|
1345
1560
|
...(page ? { page } : {}),
|
|
1346
1561
|
...(section ? { section } : {}),
|
|
1562
|
+
...(flow ? { flow } : {}),
|
|
1563
|
+
pos,
|
|
1347
1564
|
});
|
|
1348
1565
|
if (!opts.replay) {
|
|
1349
1566
|
const cp = checkpointAtOrBefore(steps, i);
|
|
@@ -1508,9 +1725,12 @@ function advance(d, suite, env, capture) {
|
|
|
1508
1725
|
enterStep(d, suite, env, mission, s.index + 1, { replay: s.replay });
|
|
1509
1726
|
}
|
|
1510
1727
|
function finishMission(d, suite, env, mission, replay) {
|
|
1511
|
-
if (isPageMissionKey(mission.key) || isSectionMissionKey(mission.key)) {
|
|
1728
|
+
if (isPageMissionKey(mission.key) || isSectionMissionKey(mission.key) || isFlowMissionKey(mission.key)) {
|
|
1512
1729
|
endPage(d, suite, "completed");
|
|
1513
|
-
d.effects.push({
|
|
1730
|
+
d.effects.push({
|
|
1731
|
+
type: "toast",
|
|
1732
|
+
text: isSectionMissionKey(mission.key) ? COPY.sectionDone : isFlowMissionKey(mission.key) ? COPY.flowDone : COPY.pageDone,
|
|
1733
|
+
});
|
|
1514
1734
|
return;
|
|
1515
1735
|
}
|
|
1516
1736
|
if (replay) {
|
|
@@ -1615,7 +1835,7 @@ function startMission(d, suite, env, key, replay) {
|
|
|
1615
1835
|
const isReplay = replay || entry?.status === "done";
|
|
1616
1836
|
const before = d.state.progress;
|
|
1617
1837
|
// A new run: what passed by itself is counted afresh.
|
|
1618
|
-
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null };
|
|
1838
|
+
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null, counter: null };
|
|
1619
1839
|
if (d.state.progress.welcome === "not_seen")
|
|
1620
1840
|
withProgress(d, { welcome: "started" });
|
|
1621
1841
|
if (!isReplay && d.state.progress.mission_key !== key)
|
|
@@ -1765,24 +1985,33 @@ function pageStillHere(suite, key, env) {
|
|
|
1765
1985
|
const page = findPage(suite, key);
|
|
1766
1986
|
return !!page && pageMatchScore(page, whereOf(env)) !== null;
|
|
1767
1987
|
}
|
|
1768
|
-
/**
|
|
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. */
|
|
1769
2003
|
function recordPage(d, suite, key, status) {
|
|
1770
2004
|
const page = findPage(suite, key);
|
|
1771
|
-
if (!page ||
|
|
2005
|
+
if (!page || !rowWritable(d.state.pages[key], page.version, status))
|
|
1772
2006
|
return;
|
|
1773
2007
|
const prev = d.state.pages[key];
|
|
1774
2008
|
d.state = { ...d.state, pages: { ...d.state.pages, [key]: { version: page.version, status, rev: prev?.rev ?? 0 } } };
|
|
1775
2009
|
d.effects.push({ type: "savePage", key, version: page.version, status });
|
|
1776
2010
|
}
|
|
1777
|
-
/** The section guide's row says this person has ended this version of it. */
|
|
1778
|
-
function sectionRecorded(state, section) {
|
|
1779
|
-
const row = state.sections?.[section.key];
|
|
1780
|
-
return !!row && row.version >= section.version;
|
|
1781
|
-
}
|
|
1782
2011
|
/** Write a section guide's row (once per section and version). */
|
|
1783
2012
|
function recordSection(d, suite, key, status) {
|
|
1784
2013
|
const section = findSection(suite, key);
|
|
1785
|
-
if (!section ||
|
|
2014
|
+
if (!section || !rowWritable(d.state.sections?.[key], section.version, status))
|
|
1786
2015
|
return;
|
|
1787
2016
|
const prev = d.state.sections?.[key];
|
|
1788
2017
|
d.state = { ...d.state, sections: { ...d.state.sections, [key]: { version: section.version, status, rev: prev?.rev ?? 0 } } };
|
|
@@ -1805,18 +2034,115 @@ function readStillHere(suite, r, env) {
|
|
|
1805
2034
|
const section = findSection(suite, r.key);
|
|
1806
2035
|
return !!section && sectionApplies(section, whereOf(env));
|
|
1807
2036
|
}
|
|
1808
|
-
/** The
|
|
2037
|
+
/** The flow's row says this person has ended this version of it. */
|
|
2038
|
+
function flowRecorded(state, flow) {
|
|
2039
|
+
const row = state.flows?.[flow.key];
|
|
2040
|
+
return !!row && row.version >= flow.version;
|
|
2041
|
+
}
|
|
2042
|
+
/** Write a flow's row (once per flow and version). */
|
|
2043
|
+
function recordFlow(d, suite, key, status) {
|
|
2044
|
+
const flow = findFlow(suite, key);
|
|
2045
|
+
if (!flow || !rowWritable(d.state.flows?.[key], flow.version, status))
|
|
2046
|
+
return;
|
|
2047
|
+
const prev = d.state.flows?.[key];
|
|
2048
|
+
d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
|
|
2049
|
+
d.effects.push({ type: "savePage", key, version: flow.version, status, kind: "flow" });
|
|
2050
|
+
}
|
|
2051
|
+
/** The page guide, section guide or flow ends: its row written when it is due one; back to idle. */
|
|
1809
2052
|
function endPage(d, suite, how) {
|
|
1810
2053
|
const run = d.state.pageRun;
|
|
1811
2054
|
if (run?.write) {
|
|
1812
2055
|
if (run.kind === "section")
|
|
1813
2056
|
recordSection(d, suite, run.key, how);
|
|
2057
|
+
else if (run.kind === "flow")
|
|
2058
|
+
recordFlow(d, suite, run.key, how);
|
|
1814
2059
|
else
|
|
1815
2060
|
recordPage(d, suite, run.key, how);
|
|
1816
2061
|
}
|
|
2062
|
+
// A flow's captured ids were its own: the tour's come back.
|
|
2063
|
+
if (run?.kind === "flow" && run.context)
|
|
2064
|
+
withProgress(d, { context: run.context });
|
|
1817
2065
|
d.state = { ...d.state, pageRun: null, replayPage: null };
|
|
1818
2066
|
setScreen(d, { name: "idle" });
|
|
1819
2067
|
}
|
|
2068
|
+
/** The flow on screen, or null. */
|
|
2069
|
+
function flowOf(s) {
|
|
2070
|
+
return s.name === "step" && s.flow ? s.flow : null;
|
|
2071
|
+
}
|
|
2072
|
+
/** The flows this person has that start on this page ("พาทำ · <title>"). */
|
|
2073
|
+
function flowsAvailable(suite, env) {
|
|
2074
|
+
if (!suite.flows?.length)
|
|
2075
|
+
return [];
|
|
2076
|
+
const ctx = guideContext(env, {});
|
|
2077
|
+
const where = whereOf(env);
|
|
2078
|
+
return (suite.flows ?? [])
|
|
2079
|
+
.map(f => findFlow(suite, f?.key))
|
|
2080
|
+
.filter((f) => !!f && onFlowStart(f, where) && flowShown(f, ctx))
|
|
2081
|
+
.map(f => f.key);
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* The flow to offer now, or null: only in "offer" mode, only a flow that
|
|
2085
|
+
* says `offer: true`, on its start page, with no row of this version, when
|
|
2086
|
+
* nothing else is up and this page's own guide is not due (it comes first:
|
|
2087
|
+
* no guide here, offer: false, or already recorded), its first step's
|
|
2088
|
+
* target on screen, no app dialog open. The host adds the welcome and the
|
|
2089
|
+
* idle wait, as for the page guide's offer.
|
|
2090
|
+
*/
|
|
2091
|
+
function flowOfferDue(state, suite, env) {
|
|
2092
|
+
if (state.pagesMode !== "offer" || state.pageAudience === "none")
|
|
2093
|
+
return null;
|
|
2094
|
+
if (state.screen.name !== "idle" || state.panelOpen || (state.pendingCelebrate?.length ?? 0) > 0)
|
|
2095
|
+
return null;
|
|
2096
|
+
const page = pageHere(suite, env);
|
|
2097
|
+
if (page && page.offer !== false && !pageRecorded(state, page))
|
|
2098
|
+
return null;
|
|
2099
|
+
if (safe(() => env.dialogOpen?.() === true, false))
|
|
2100
|
+
return null;
|
|
2101
|
+
for (const key of flowsAvailable(suite, env)) {
|
|
2102
|
+
const flow = findFlow(suite, key);
|
|
2103
|
+
if (flow.offer !== true || flowRecorded(state, flow))
|
|
2104
|
+
continue;
|
|
2105
|
+
const mission = flowMission(flow);
|
|
2106
|
+
const first = mission.steps.find(st => {
|
|
2107
|
+
const ctx = guideContext(env, {});
|
|
2108
|
+
if (st.requires && !safe(() => env.can(st.requires), true))
|
|
2109
|
+
return false;
|
|
2110
|
+
return !(st.skipIf && safe(() => st.skipIf(ctx), false));
|
|
2111
|
+
});
|
|
2112
|
+
if (first && !stepTargetVisible(first, env, guideContext(env, {}), false))
|
|
2113
|
+
continue;
|
|
2114
|
+
return key;
|
|
2115
|
+
}
|
|
2116
|
+
return null;
|
|
2117
|
+
}
|
|
2118
|
+
function startFlow(d, suite, env, key, write, at) {
|
|
2119
|
+
const flow = findFlow(suite, key);
|
|
2120
|
+
if (!flow)
|
|
2121
|
+
return;
|
|
2122
|
+
const mission = flowMission(flow);
|
|
2123
|
+
const saved = d.state.progress.context;
|
|
2124
|
+
d.state = {
|
|
2125
|
+
...d.state,
|
|
2126
|
+
panelOpen: false,
|
|
2127
|
+
justDone: null,
|
|
2128
|
+
pageRun: { key, write, kind: "flow", context: saved },
|
|
2129
|
+
autoPassed: null,
|
|
2130
|
+
counter: null,
|
|
2131
|
+
replayPage: {
|
|
2132
|
+
mission: mission.key,
|
|
2133
|
+
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
2134
|
+
dialog: safe(() => env.dialogOpen?.() === true, false),
|
|
2135
|
+
},
|
|
2136
|
+
};
|
|
2137
|
+
withProgress(d, { context: at?.context ?? {} });
|
|
2138
|
+
let index = 0;
|
|
2139
|
+
if (at) {
|
|
2140
|
+
const stepAt = mission.steps.findIndex(st => st.key === at.step);
|
|
2141
|
+
if (stepAt > 0)
|
|
2142
|
+
index = checkpointAtOrBefore(mission.steps, stepAt);
|
|
2143
|
+
}
|
|
2144
|
+
enterStep(d, suite, env, mission, index, { replay: true });
|
|
2145
|
+
}
|
|
1820
2146
|
/** The section guides on screen here that this person has (the "?" entries, the buttons). */
|
|
1821
2147
|
function sectionsAvailable(suite, env) {
|
|
1822
2148
|
return sectionsHere(suite, whereOf(env), guideContext(env, {})).map(x => x.key);
|
|
@@ -1832,6 +2158,7 @@ function startSection(d, suite, env, key, write) {
|
|
|
1832
2158
|
justDone: null,
|
|
1833
2159
|
pageRun: { key, write, kind: "section" },
|
|
1834
2160
|
autoPassed: null,
|
|
2161
|
+
counter: null,
|
|
1835
2162
|
replayPage: {
|
|
1836
2163
|
mission: mission.key,
|
|
1837
2164
|
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
@@ -1851,6 +2178,7 @@ function startPage(d, suite, env, key, write) {
|
|
|
1851
2178
|
justDone: null,
|
|
1852
2179
|
pageRun: { key, write, kind: "page" },
|
|
1853
2180
|
autoPassed: null,
|
|
2181
|
+
counter: null,
|
|
1854
2182
|
// What is open now is the person's: ending the guide leaves it alone.
|
|
1855
2183
|
replayPage: {
|
|
1856
2184
|
mission: mission.key,
|
|
@@ -1894,6 +2222,28 @@ function backTarget(mission, index, env, context) {
|
|
|
1894
2222
|
}
|
|
1895
2223
|
return prev >= 0 ? { index: prev, close: false } : null;
|
|
1896
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
|
+
}
|
|
1897
2247
|
function reduceGuide(state, action, suite, env) {
|
|
1898
2248
|
const d = { state, effects: [] };
|
|
1899
2249
|
const s = state.screen;
|
|
@@ -1950,7 +2300,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1950
2300
|
break;
|
|
1951
2301
|
// A mission always wins over a page guide: a running one ends (its row
|
|
1952
2302
|
// written if due), an offer goes unrecorded.
|
|
1953
|
-
if (readOf(s))
|
|
2303
|
+
if (readOf(s) || flowOf(s))
|
|
1954
2304
|
endPage(d, suite, "skipped");
|
|
1955
2305
|
else if (s.name === "pageOffer")
|
|
1956
2306
|
setScreen(d, { name: "idle" });
|
|
@@ -1971,13 +2321,21 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1971
2321
|
const stepAt = mission.steps.findIndex(st => st.key === d.state.progress.step_key);
|
|
1972
2322
|
const cpAt = mission.steps.findIndex(st => st.key === d.state.progress.checkpoint_key);
|
|
1973
2323
|
const index = stepAt >= 0 ? checkpointAtOrBefore(mission.steps, stepAt) : Math.max(cpAt, 0);
|
|
1974
|
-
d.state = { ...d.state, autoPassed: null };
|
|
2324
|
+
d.state = { ...d.state, autoPassed: null, counter: null };
|
|
1975
2325
|
enterStep(d, suite, env, mission, index, { replay: false });
|
|
1976
2326
|
break;
|
|
1977
2327
|
}
|
|
1978
2328
|
case "sync": {
|
|
1979
2329
|
if (s.name === "pageOffer") {
|
|
1980
2330
|
// Ignored: the person left the page (or its sub-screen). Recorded.
|
|
2331
|
+
if (s.flow) {
|
|
2332
|
+
const flow = findFlow(suite, s.page);
|
|
2333
|
+
if (!flow || !onFlowStart(flow, whereOf(env))) {
|
|
2334
|
+
recordFlow(d, suite, s.page, "skipped");
|
|
2335
|
+
setScreen(d, { name: "idle" });
|
|
2336
|
+
}
|
|
2337
|
+
break;
|
|
2338
|
+
}
|
|
1981
2339
|
if (!pageStillHere(suite, s.page, env)) {
|
|
1982
2340
|
recordPage(d, suite, s.page, "skipped");
|
|
1983
2341
|
setScreen(d, { name: "idle" });
|
|
@@ -1986,6 +2344,15 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1986
2344
|
}
|
|
1987
2345
|
if (s.name !== "step")
|
|
1988
2346
|
break;
|
|
2347
|
+
if (s.flow) {
|
|
2348
|
+
// Off the flow's pages: it ends, silently. Between them it goes on
|
|
2349
|
+
// (a step on another of its pages shows "ทำต่อ").
|
|
2350
|
+
const flow = findFlow(suite, s.flow);
|
|
2351
|
+
if (!flow || !inFlowChain(flow, env)) {
|
|
2352
|
+
endPage(d, suite, "skipped");
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
1989
2356
|
const read = readOf(s);
|
|
1990
2357
|
if (read && !readStillHere(suite, read, env)) {
|
|
1991
2358
|
// Leaving the page (or the section's root going) ends its guide, silently.
|
|
@@ -1997,6 +2364,15 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1997
2364
|
break;
|
|
1998
2365
|
const step = mission.steps[s.index];
|
|
1999
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
|
+
}
|
|
2000
2376
|
if (s.phase === "away") {
|
|
2001
2377
|
const where = placeRoute(mission.steps, s.index);
|
|
2002
2378
|
if (!where || matchRoute(where, env.pathname, env.search, env.code)) {
|
|
@@ -2175,7 +2551,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2175
2551
|
advance(d, suite, env);
|
|
2176
2552
|
}
|
|
2177
2553
|
else if (policy === "abort") {
|
|
2178
|
-
if (readOf(s))
|
|
2554
|
+
if (readOf(s) || flowOf(s))
|
|
2179
2555
|
endPage(d, suite, "skipped");
|
|
2180
2556
|
else
|
|
2181
2557
|
exitMission(d);
|
|
@@ -2217,7 +2593,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2217
2593
|
case "exit":
|
|
2218
2594
|
if (s.name !== "step")
|
|
2219
2595
|
break;
|
|
2220
|
-
if (readOf(s)) {
|
|
2596
|
+
if (readOf(s) || flowOf(s)) {
|
|
2221
2597
|
// ✕, Escape or ปิดคู่มือ: ends silently.
|
|
2222
2598
|
pushAbort(d, suite, s);
|
|
2223
2599
|
endPage(d, suite, "skipped");
|
|
@@ -2245,6 +2621,11 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2245
2621
|
case "endWithoutSave":
|
|
2246
2622
|
if (s.name !== "step" || !s.replay)
|
|
2247
2623
|
break;
|
|
2624
|
+
if (flowOf(s)) {
|
|
2625
|
+
pushAbort(d, suite, s);
|
|
2626
|
+
endPage(d, suite, "skipped");
|
|
2627
|
+
break;
|
|
2628
|
+
}
|
|
2248
2629
|
pushAbort(d, suite, s);
|
|
2249
2630
|
setScreen(d, { name: "idle" });
|
|
2250
2631
|
d.effects.push({ type: "toast", text: COPY.replayEnded });
|
|
@@ -2293,7 +2674,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2293
2674
|
break;
|
|
2294
2675
|
case "openPanel": {
|
|
2295
2676
|
const section = action.section ?? "all";
|
|
2296
|
-
const allowed = section
|
|
2677
|
+
const allowed = section !== "all" ? state.pageAudience !== "none" : state.audience !== "none" || state.pageAudience !== "none";
|
|
2297
2678
|
if (!allowed || (s.name !== "idle" && s.name !== "pageOffer"))
|
|
2298
2679
|
break;
|
|
2299
2680
|
// The offer gives way to the panel, unrecorded.
|
|
@@ -2346,6 +2727,13 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2346
2727
|
break;
|
|
2347
2728
|
}
|
|
2348
2729
|
case "pageOffer": {
|
|
2730
|
+
if (action.flow) {
|
|
2731
|
+
if (flowOfferDue(state, suite, env) !== action.page)
|
|
2732
|
+
break;
|
|
2733
|
+
setScreen(d, { name: "pageOffer", page: action.page, flow: true });
|
|
2734
|
+
d.effects.push({ type: "announce", text: `พาทำงาน ${findFlow(suite, action.page)?.title ?? ""}` });
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2349
2737
|
if (pageOfferDue(state, suite, env) !== action.page)
|
|
2350
2738
|
break;
|
|
2351
2739
|
setScreen(d, { name: "pageOffer", page: action.page });
|
|
@@ -2355,14 +2743,42 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2355
2743
|
case "pageOfferStart":
|
|
2356
2744
|
if (s.name !== "pageOffer" || state.pageAudience === "none")
|
|
2357
2745
|
break;
|
|
2358
|
-
|
|
2746
|
+
if (s.flow)
|
|
2747
|
+
startFlow(d, suite, env, s.page, true);
|
|
2748
|
+
else
|
|
2749
|
+
startPage(d, suite, env, s.page, true);
|
|
2359
2750
|
break;
|
|
2360
2751
|
case "pageOfferLater":
|
|
2361
2752
|
if (s.name !== "pageOffer")
|
|
2362
2753
|
break;
|
|
2363
|
-
|
|
2754
|
+
if (s.flow)
|
|
2755
|
+
recordFlow(d, suite, s.page, "skipped");
|
|
2756
|
+
else
|
|
2757
|
+
recordPage(d, suite, s.page, "skipped");
|
|
2364
2758
|
setScreen(d, { name: "idle" });
|
|
2365
2759
|
break;
|
|
2760
|
+
case "startFlow": {
|
|
2761
|
+
// Only on its start page; never over a mission step, a welcome, a gate
|
|
2762
|
+
// or a celebration.
|
|
2763
|
+
if (state.pageAudience === "none" || state.pagesMode === "off")
|
|
2764
|
+
break;
|
|
2765
|
+
if (s.name !== "idle" && s.name !== "pageOffer")
|
|
2766
|
+
break;
|
|
2767
|
+
if (!flowsAvailable(suite, env).includes(action.flow))
|
|
2768
|
+
break;
|
|
2769
|
+
const flow = findFlow(suite, action.flow);
|
|
2770
|
+
startFlow(d, suite, env, flow.key, rowStillDue(state.flows?.[flow.key], flow.version));
|
|
2771
|
+
break;
|
|
2772
|
+
}
|
|
2773
|
+
case "resumeFlow": {
|
|
2774
|
+
if (state.pageAudience === "none" || state.pagesMode === "off" || s.name !== "idle")
|
|
2775
|
+
break;
|
|
2776
|
+
const flow = findFlow(suite, action.flow);
|
|
2777
|
+
if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
|
|
2778
|
+
break;
|
|
2779
|
+
startFlow(d, suite, env, flow.key, action.write && rowStillDue(state.flows?.[flow.key], flow.version), { step: action.step, context: action.context });
|
|
2780
|
+
break;
|
|
2781
|
+
}
|
|
2366
2782
|
case "startPage": {
|
|
2367
2783
|
// Never over a mission step, a welcome, a gate or a celebration.
|
|
2368
2784
|
if (state.pageAudience === "none" || state.pagesMode === "off")
|
|
@@ -2372,7 +2788,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2372
2788
|
const page = pageHere(suite, env);
|
|
2373
2789
|
if (!page || page.key !== action.page)
|
|
2374
2790
|
break;
|
|
2375
|
-
startPage(d, suite, env, page.key,
|
|
2791
|
+
startPage(d, suite, env, page.key, rowStillDue(state.pages[page.key], page.version));
|
|
2376
2792
|
break;
|
|
2377
2793
|
}
|
|
2378
2794
|
case "back": {
|
|
@@ -2391,6 +2807,13 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2391
2807
|
break;
|
|
2392
2808
|
}
|
|
2393
2809
|
case "pageRow": {
|
|
2810
|
+
if (action.kind === "flow") {
|
|
2811
|
+
const cur = d.state.flows?.[action.key];
|
|
2812
|
+
if (cur && cur.version > action.row.version)
|
|
2813
|
+
break;
|
|
2814
|
+
d.state = { ...d.state, flows: { ...d.state.flows, [action.key]: action.row } };
|
|
2815
|
+
break;
|
|
2816
|
+
}
|
|
2394
2817
|
if (action.kind === "section") {
|
|
2395
2818
|
const cur = d.state.sections?.[action.key];
|
|
2396
2819
|
if (cur && cur.version > action.row.version)
|
|
@@ -2414,7 +2837,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2414
2837
|
if (!sectionsAvailable(suite, env).includes(action.section))
|
|
2415
2838
|
break;
|
|
2416
2839
|
const section = findSection(suite, action.section);
|
|
2417
|
-
startSection(d, suite, env, section.key,
|
|
2840
|
+
startSection(d, suite, env, section.key, rowStillDue(state.sections?.[section.key], section.version));
|
|
2418
2841
|
break;
|
|
2419
2842
|
}
|
|
2420
2843
|
case "idleCelebrate": {
|
|
@@ -2546,6 +2969,7 @@ function parseGuidesAnswer(payload, suite) {
|
|
|
2546
2969
|
: "none";
|
|
2547
2970
|
const pages = parsePageRows(rows, suite.app);
|
|
2548
2971
|
const sections = parsePageRows(rows, suite.app, "section");
|
|
2972
|
+
const flows = parsePageRows(rows, suite.app, "flow");
|
|
2549
2973
|
let row = null;
|
|
2550
2974
|
for (const r of rows) {
|
|
2551
2975
|
if (isRecord(r) && r.guide_key === suite.key) {
|
|
@@ -2566,9 +2990,9 @@ function parseGuidesAnswer(payload, suite) {
|
|
|
2566
2990
|
progress = row;
|
|
2567
2991
|
newerOnServer = row.guide_version > suite.version;
|
|
2568
2992
|
}
|
|
2569
|
-
return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages, sections };
|
|
2993
|
+
return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages, sections, flows };
|
|
2570
2994
|
}
|
|
2571
|
-
/** The row key of a page guide
|
|
2995
|
+
/** The row key of a page guide `<app>.page.<key>`, a section guide `<app>.section.<key>`, or a flow `<app>.flow.<key>`. */
|
|
2572
2996
|
function pageRowKey(app, key, kind = "page") {
|
|
2573
2997
|
return `${app}.${kind}.${key}`;
|
|
2574
2998
|
}
|
|
@@ -2859,7 +3283,10 @@ async function storePageRow(adapter, app, key, version, status, rev, kind = "pag
|
|
|
2859
3283
|
}
|
|
2860
3284
|
if (current.version > version)
|
|
2861
3285
|
return null;
|
|
2862
|
-
|
|
3286
|
+
// This version already recorded: kept, except a skipped row this run
|
|
3287
|
+
// finished (skipped → completed, once, shell ≥ 0.1.77).
|
|
3288
|
+
const finishing = status === "completed" && current.status === "skipped";
|
|
3289
|
+
if (current.version === version && current.status !== "in_progress" && !finishing)
|
|
2863
3290
|
return current;
|
|
2864
3291
|
at = current.rev;
|
|
2865
3292
|
}
|
|
@@ -3526,6 +3953,9 @@ function useNudge(nudge, ref) {
|
|
|
3526
3953
|
}
|
|
3527
3954
|
function StepLayer(p) {
|
|
3528
3955
|
const { spot, phone, viewport } = p;
|
|
3956
|
+
// Where the bubble may sit: the content column when the app has one.
|
|
3957
|
+
const col = p.bounds && p.bounds.width > 0 ? p.bounds : { x: 0, width: viewport.width || (typeof window !== "undefined" ? window.innerWidth : 0)};
|
|
3958
|
+
const colKey = `${col.x}|${col.width}`;
|
|
3529
3959
|
const bubbleRef = React.useRef(null);
|
|
3530
3960
|
const arrowRef = React.useRef(null);
|
|
3531
3961
|
const ringRef = React.useRef(null);
|
|
@@ -3566,8 +3996,8 @@ function StepLayer(p) {
|
|
|
3566
3996
|
const bw = el.offsetWidth;
|
|
3567
3997
|
const bh = el.offsetHeight;
|
|
3568
3998
|
setPos(phone
|
|
3569
|
-
? { left: m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
3570
|
-
: { left: (
|
|
3999
|
+
? { left: col.x + m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
4000
|
+
: { left: col.x + (col.width - bw) / 2, top: viewport.offsetTop + (viewport.height - bh) / 2, side: "dock" });
|
|
3571
4001
|
return;
|
|
3572
4002
|
}
|
|
3573
4003
|
if (phone) {
|
|
@@ -3588,7 +4018,7 @@ function StepLayer(p) {
|
|
|
3588
4018
|
: overlapBot <= overlapTop
|
|
3589
4019
|
? botT
|
|
3590
4020
|
: topT;
|
|
3591
|
-
setPos({ left: m, top, side: "dock" });
|
|
4021
|
+
setPos({ left: col.x + m, top, side: "dock" });
|
|
3592
4022
|
return;
|
|
3593
4023
|
}
|
|
3594
4024
|
let cancelled = false;
|
|
@@ -3610,7 +4040,7 @@ function StepLayer(p) {
|
|
|
3610
4040
|
placement,
|
|
3611
4041
|
middleware: [
|
|
3612
4042
|
dom.offset(placement.startsWith("bottom") ? gap : 14),
|
|
3613
|
-
dom.shift({ padding: m }),
|
|
4043
|
+
dom.shift({ padding: m, rootBoundary: { x: col.x, y: 0, width: col.width, height: window.innerHeight } }),
|
|
3614
4044
|
...(arrowRef.current ? [dom.arrow({ element: arrowRef.current, padding: 14 })] : []),
|
|
3615
4045
|
],
|
|
3616
4046
|
});
|
|
@@ -3620,11 +4050,11 @@ function StepLayer(p) {
|
|
|
3620
4050
|
return;
|
|
3621
4051
|
const bw = el.offsetWidth;
|
|
3622
4052
|
const bh = el.offsetHeight;
|
|
3623
|
-
const bounds = { x: m, y: viewport.offsetTop + m, width:
|
|
4053
|
+
const bounds = { x: col.x + m, y: viewport.offsetTop + m, width: col.width - 2 * m, height: viewport.height - 2 * m };
|
|
3624
4054
|
const i = pickPlacement(results, bw, bh, a, clear, bounds);
|
|
3625
4055
|
if (i < 0) {
|
|
3626
4056
|
// Nowhere beside it: the bottom-right corner.
|
|
3627
|
-
setPos({ left:
|
|
4057
|
+
setPos({ left: col.x + col.width - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
|
|
3628
4058
|
return;
|
|
3629
4059
|
}
|
|
3630
4060
|
const { x, y, placement, middlewareData } = results[i];
|
|
@@ -3640,7 +4070,7 @@ function StepLayer(p) {
|
|
|
3640
4070
|
};
|
|
3641
4071
|
// The keys stand for spot, the lists, the holes and the bubble's size.
|
|
3642
4072
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3643
|
-
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
4073
|
+
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop, colKey]);
|
|
3644
4074
|
const popovers = p.popovers ?? [];
|
|
3645
4075
|
const covered = !!p.covered;
|
|
3646
4076
|
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
@@ -3661,7 +4091,7 @@ function StepLayer(p) {
|
|
|
3661
4091
|
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: {
|
|
3662
4092
|
left: pos?.left ?? 0,
|
|
3663
4093
|
top: pos?.top ?? 0,
|
|
3664
|
-
width: phone ?
|
|
4094
|
+
width: phone ? col.width - 32 : col.width < 352 ? col.width - 32 : undefined,
|
|
3665
4095
|
visibility: pos ? "visible" : "hidden",
|
|
3666
4096
|
}, 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] }));
|
|
3667
4097
|
}
|
|
@@ -3750,7 +4180,10 @@ function CompleteModal(p) {
|
|
|
3750
4180
|
* corner, not modal (the page stays usable; focus is not taken). Shell ≥ 0.1.74.
|
|
3751
4181
|
*/
|
|
3752
4182
|
function PageOfferCard(p) {
|
|
3753
|
-
return (jsxRuntime.jsxs("section", { className: "eg-card eg-offer", role: "region", "aria-labelledby": "eg-po-t", "data-eg-offer": "", children: [jsxRuntime.jsx("p", { className: "eg-po-e", children: "
|
|
4183
|
+
return (jsxRuntime.jsxs("section", { className: "eg-card eg-offer", role: "region", "aria-labelledby": "eg-po-t", "data-eg-offer": "", children: [jsxRuntime.jsx("p", { className: "eg-po-e", children: p.eyebrow ?? "คู่มือหน้านี้" }), jsxRuntime.jsx("p", { className: "eg-b-t", id: "eg-po-t", children: p.title }), jsxRuntime.jsxs("p", { className: "eg-b-w", children: [p.steps, " \u0E02\u0E31\u0E49\u0E19"] }), jsxRuntime.jsxs("div", { className: "eg-row", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onLater, children: "\u0E44\u0E21\u0E48\u0E15\u0E2D\u0E19\u0E19\u0E35\u0E49" }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onStart, children: "\u0E40\u0E23\u0E34\u0E48\u0E21" })] })] }));
|
|
4184
|
+
}
|
|
4185
|
+
function FlowList({ items, onOpen }) {
|
|
4186
|
+
return (jsxRuntime.jsx("ul", { className: "eg-q-list eg-pg-list", children: items.map(it => (jsxRuntime.jsxs("li", { className: "eg-q-item eg-pg-item", children: [jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: it.title }), jsxRuntime.jsx("small", { children: it.done ? "ทำแล้ว" : it.minutes ? `ประมาณ ${it.minutes} นาที` : "ใหม่" })] }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onOpen(it.key), children: it.done ? "ทำอีกครั้ง" : "เริ่ม" })] }, it.key))) }));
|
|
3754
4187
|
}
|
|
3755
4188
|
function PageList({ items, onOpen }) {
|
|
3756
4189
|
return (jsxRuntime.jsx("ul", { className: "eg-q-list eg-pg-list", children: items.map(it => (jsxRuntime.jsxs("li", { className: "eg-q-item eg-pg-item", children: [jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: it.title }), jsxRuntime.jsx("small", { className: it.seen ? undefined : "eg-new", children: it.seen ? "ดูแล้ว" : "ใหม่" })] }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onOpen(it.key), children: "\u0E40\u0E1B\u0E34\u0E14" })] }, it.key))) }));
|
|
@@ -3777,12 +4210,16 @@ function QuestChip({ done, total, pulse, onOpen }) {
|
|
|
3777
4210
|
}
|
|
3778
4211
|
function QuestPanel(p) {
|
|
3779
4212
|
const ref = useAutoFocus();
|
|
3780
|
-
const pages = p.pages ?? [];
|
|
4213
|
+
const pages = p.only === "flows" ? [] : (p.pages ?? []);
|
|
4214
|
+
const flows = p.only === "pages" ? [] : (p.flows ?? []);
|
|
3781
4215
|
const openPage = p.onOpenPage ?? (() => { });
|
|
4216
|
+
const openFlow = p.onOpenFlow ?? (() => { });
|
|
4217
|
+
const flowSection = flows.length ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("h3", { className: "eg-q-sub", id: "eg-q-flows", children: "\u0E1E\u0E32\u0E17\u0E33\u0E07\u0E32\u0E19" }), jsxRuntime.jsx(FlowList, { items: flows, onOpen: openFlow })] })) : null;
|
|
3782
4218
|
if (p.showMissions === false) {
|
|
3783
|
-
|
|
4219
|
+
const heading = pages.length ? "คู่มือแต่ละหน้า" : "พาทำงาน";
|
|
4220
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxRuntime.jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxRuntime.jsxs("div", { className: "eg-q-h", children: [jsxRuntime.jsx("h2", { id: "eg-q-t", children: heading }), jsxRuntime.jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsxRuntime.jsx(IconX, {}) })] }), pages.length ? jsxRuntime.jsx(PageList, { items: pages, onOpen: openPage }) : null, pages.length ? flowSection : flows.length ? jsxRuntime.jsx(FlowList, { items: flows, onOpen: openFlow }) : null] })] }));
|
|
3784
4221
|
}
|
|
3785
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxRuntime.jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxRuntime.jsxs("div", { className: "eg-q-h", children: [jsxRuntime.jsx("h2", { id: "eg-q-t", children: "\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19" }), jsxRuntime.jsxs("span", { className: "eg-n", children: [p.done, "/", p.total] }), jsxRuntime.jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsxRuntime.jsx(IconX, {}) })] }), jsxRuntime.jsx("div", { className: "eg-prog", children: jsxRuntime.jsx("div", { className: "eg-bar", children: jsxRuntime.jsx("i", { style: { width: `${p.total ? (p.done / p.total) * 100 : 0}%` } }) }) }), jsxRuntime.jsx("ol", { className: "eg-q-list", children: p.items.map((it, i) => (jsxRuntime.jsx(QuestRow, { item: it, n: i + 1, just: p.justDone === it.key, gate: p.gate, onStart: p.onStart, onDismiss: p.onDismiss }, it.key))) }), pages.length ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("h3", { className: "eg-q-sub", id: "eg-q-pages", children: "\u0E04\u0E39\u0E48\u0E21\u0E37\u0E2D\u0E41\u0E15\u0E48\u0E25\u0E30\u0E2B\u0E19\u0E49\u0E32" }), jsxRuntime.jsx(PageList, { items: pages, onOpen: openPage })] })) : null, p.canHideChip ? (jsxRuntime.jsx("div", { className: "eg-q-f", children: jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onHideChip, children: "\u0E0B\u0E48\u0E2D\u0E19\u0E1B\u0E38\u0E48\u0E21\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08" }) })) : null] })] }));
|
|
4222
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxRuntime.jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxRuntime.jsxs("div", { className: "eg-q-h", children: [jsxRuntime.jsx("h2", { id: "eg-q-t", children: "\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19" }), jsxRuntime.jsxs("span", { className: "eg-n", children: [p.done, "/", p.total] }), jsxRuntime.jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsxRuntime.jsx(IconX, {}) })] }), jsxRuntime.jsx("div", { className: "eg-prog", children: jsxRuntime.jsx("div", { className: "eg-bar", children: jsxRuntime.jsx("i", { style: { width: `${p.total ? (p.done / p.total) * 100 : 0}%` } }) }) }), jsxRuntime.jsx("ol", { className: "eg-q-list", children: p.items.map((it, i) => (jsxRuntime.jsx(QuestRow, { item: it, n: i + 1, just: p.justDone === it.key, gate: p.gate, onStart: p.onStart, onDismiss: p.onDismiss }, it.key))) }), pages.length ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("h3", { className: "eg-q-sub", id: "eg-q-pages", children: "\u0E04\u0E39\u0E48\u0E21\u0E37\u0E2D\u0E41\u0E15\u0E48\u0E25\u0E30\u0E2B\u0E19\u0E49\u0E32" }), jsxRuntime.jsx(PageList, { items: pages, onOpen: openPage })] })) : null, flowSection, p.canHideChip ? (jsxRuntime.jsx("div", { className: "eg-q-f", children: jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onHideChip, children: "\u0E0B\u0E48\u0E2D\u0E19\u0E1B\u0E38\u0E48\u0E21\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08" }) })) : null] })] }));
|
|
3786
4223
|
}
|
|
3787
4224
|
function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
3788
4225
|
const v = item.view;
|
|
@@ -3920,8 +4357,7 @@ function writeMirror(orgId, suite, p) {
|
|
|
3920
4357
|
/* private mode: the server copy is enough */
|
|
3921
4358
|
}
|
|
3922
4359
|
}
|
|
3923
|
-
|
|
3924
|
-
const pagesMirrorKey = (orgId, suite, kind = "page") => `erp-guide-${kind === "section" ? "sections" : "pages"}:${orgId}:${suite.app}`;
|
|
4360
|
+
const pagesMirrorKey = (orgId, suite, kind = "page") => `erp-guide-${kind}s:${orgId}:${suite.app}`;
|
|
3925
4361
|
function readPagesMirror(orgId, suite, kind = "page") {
|
|
3926
4362
|
try {
|
|
3927
4363
|
const raw = sessionStorage.getItem(pagesMirrorKey(orgId, suite, kind));
|
|
@@ -3955,6 +4391,8 @@ function mergePageRows(server, local) {
|
|
|
3955
4391
|
}
|
|
3956
4392
|
return out;
|
|
3957
4393
|
}
|
|
4394
|
+
/** A flow in progress in this tab (resumed after a refresh, never across sessions). */
|
|
4395
|
+
const flowMirrorKey = (orgId, suite) => `erp-guide-flow-run:${orgId}:${suite.app}`;
|
|
3958
4396
|
/* ------------------------------------------------------------------ */
|
|
3959
4397
|
/* One tab at a time */
|
|
3960
4398
|
/* ------------------------------------------------------------------ */
|
|
@@ -4021,6 +4459,42 @@ function runAbort(actions) {
|
|
|
4021
4459
|
}, ABORT_DELAY_MS + i * ABORT_GAP_MS);
|
|
4022
4460
|
});
|
|
4023
4461
|
}
|
|
4462
|
+
/** The app's content column (GuideConfig.contentRoot), cut to the viewport; null when none is declared or found. */
|
|
4463
|
+
function contentColumn(selector, vw) {
|
|
4464
|
+
if (!selector || typeof document === "undefined")
|
|
4465
|
+
return null;
|
|
4466
|
+
let el = null;
|
|
4467
|
+
try {
|
|
4468
|
+
el = document.querySelector(selector);
|
|
4469
|
+
}
|
|
4470
|
+
catch {
|
|
4471
|
+
return null;
|
|
4472
|
+
}
|
|
4473
|
+
if (!el)
|
|
4474
|
+
return null;
|
|
4475
|
+
const r = el.getBoundingClientRect();
|
|
4476
|
+
const x = Math.max(0, r.left);
|
|
4477
|
+
const right = Math.min(vw || window.innerWidth, r.right);
|
|
4478
|
+
if (right - x <= 0)
|
|
4479
|
+
return null;
|
|
4480
|
+
return { x, y: 0, width: right - x, height: 0 };
|
|
4481
|
+
}
|
|
4482
|
+
/** The control a form's implicit submit (Enter in a field) presses: its first submit button. */
|
|
4483
|
+
function defaultSubmitControl(form) {
|
|
4484
|
+
for (const el of Array.from(form.elements)) {
|
|
4485
|
+
if (el instanceof HTMLButtonElement && (el.type === "submit" || !el.getAttribute("type")))
|
|
4486
|
+
return el;
|
|
4487
|
+
if (el instanceof HTMLInputElement && (el.type === "submit" || el.type === "image"))
|
|
4488
|
+
return el;
|
|
4489
|
+
}
|
|
4490
|
+
return null;
|
|
4491
|
+
}
|
|
4492
|
+
/** A field where Enter submits its form (not a textarea, a button or a select). */
|
|
4493
|
+
function submitsOnEnter(el) {
|
|
4494
|
+
if (!(el instanceof HTMLInputElement))
|
|
4495
|
+
return false;
|
|
4496
|
+
return !["button", "submit", "reset", "image", "file", "hidden"].includes(el.type);
|
|
4497
|
+
}
|
|
4024
4498
|
/** A step's own target takes presses: not on a "view" step, unless the step allows it. */
|
|
4025
4499
|
function targetPressable(step, bridge) {
|
|
4026
4500
|
if (!step || bridge)
|
|
@@ -4067,7 +4541,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4067
4541
|
const [answer, setAnswer] = React.useState(null);
|
|
4068
4542
|
const missionsReady = config.ready !== false;
|
|
4069
4543
|
const pagesReady = config.pagesReady ?? missionsReady;
|
|
4070
|
-
const hasPages = React.useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0, [suite]);
|
|
4544
|
+
const hasPages = React.useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0 || validFlows(suite).length > 0, [suite]);
|
|
4545
|
+
const hasFlows = React.useMemo(() => validFlows(suite).length > 0, [suite]);
|
|
4071
4546
|
const hasSections = React.useMemo(() => validSections(suite).length > 0, [suite]);
|
|
4072
4547
|
const enabled = primary && (missionsReady || (hasPages && pagesReady));
|
|
4073
4548
|
const loaded = React.useRef(false);
|
|
@@ -4125,6 +4600,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4125
4600
|
pagesMode: answer.pagesMode,
|
|
4126
4601
|
pages: mergePageRows(answer.pages, readPagesMirror(organizationId, suite)),
|
|
4127
4602
|
sections: mergePageRows(answer.sections ?? {}, readPagesMirror(organizationId, suite, "section")),
|
|
4603
|
+
flows: mergePageRows(answer.flows ?? {}, readPagesMirror(organizationId, suite, "flow")),
|
|
4128
4604
|
});
|
|
4129
4605
|
stateRef.current = s;
|
|
4130
4606
|
setState(s);
|
|
@@ -4233,7 +4709,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4233
4709
|
else if (e.type === "savePage") {
|
|
4234
4710
|
// Once per page and version; the machine already holds it as written.
|
|
4235
4711
|
const kind = e.kind ?? "page";
|
|
4236
|
-
const
|
|
4712
|
+
const cur = stateRef.current;
|
|
4713
|
+
const rows = cur ? (kind === "section" ? cur.sections : kind === "flow" ? cur.flows : cur.pages) : undefined;
|
|
4237
4714
|
if (rows)
|
|
4238
4715
|
writePagesMirror(organizationId, suite, rows, kind);
|
|
4239
4716
|
const rev = rows?.[e.key]?.rev ?? 0;
|
|
@@ -4330,7 +4807,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4330
4807
|
/* ---------------- sync on route, sidebar, facts, DOM ---------------- */
|
|
4331
4808
|
// A page guide's step runs while page guides are live; a mission's while
|
|
4332
4809
|
// the missions are. The offer card is watched the same way (left: ignored).
|
|
4333
|
-
const inStep = screen.name === "step" && (screen.page || screen.section ? pagesLive : missionsLive);
|
|
4810
|
+
const inStep = screen.name === "step" && (screen.page || screen.section || screen.flow ? pagesLive : missionsLive);
|
|
4334
4811
|
const offerUp = pagesLive && screen.name === "pageOffer";
|
|
4335
4812
|
const watching = inStep || offerUp;
|
|
4336
4813
|
React.useEffect(() => {
|
|
@@ -4396,6 +4873,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4396
4873
|
const stepId = screen.name === "step" ? `${screen.mission}:${screen.index}:${screen.bridge ? "b" : ""}${screen.nothingToSave ? "n" : ""}` : "";
|
|
4397
4874
|
/* Measure every frame while a step is shown: the spotlight follows
|
|
4398
4875
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
4876
|
+
const contentRoot = config.contentRoot;
|
|
4399
4877
|
const [layout, setLayout] = React.useState({
|
|
4400
4878
|
spot: null,
|
|
4401
4879
|
covered: false,
|
|
@@ -4404,6 +4882,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4404
4882
|
vw: 0,
|
|
4405
4883
|
vh: 0,
|
|
4406
4884
|
oy: 0,
|
|
4885
|
+
col: null,
|
|
4407
4886
|
});
|
|
4408
4887
|
React.useEffect(() => {
|
|
4409
4888
|
if (!inStep) {
|
|
@@ -4412,7 +4891,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4412
4891
|
}
|
|
4413
4892
|
let raf = 0;
|
|
4414
4893
|
let lostSince = null;
|
|
4415
|
-
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
|
|
4894
|
+
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0, col: null };
|
|
4416
4895
|
targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
|
|
4417
4896
|
const allow = stepInfo?.step.allow ?? [];
|
|
4418
4897
|
// The spotlight is the part of the target a person can see: cut by the
|
|
@@ -4472,13 +4951,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4472
4951
|
holes.push(padRect(rectOf(ael), TARGET_PAD));
|
|
4473
4952
|
}
|
|
4474
4953
|
const covered = !!spot && coveredByDialog(targetElRef.current);
|
|
4475
|
-
const
|
|
4954
|
+
const col = contentColumn(contentRoot, vw);
|
|
4955
|
+
const next = { spot, covered, holes, popovers, vw, vh, oy, col };
|
|
4476
4956
|
const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
|
|
4477
4957
|
if (!sameRect(next.spot, last.spot) ||
|
|
4478
4958
|
next.covered !== last.covered ||
|
|
4479
4959
|
next.vw !== last.vw ||
|
|
4480
4960
|
next.vh !== last.vh ||
|
|
4481
4961
|
next.oy !== last.oy ||
|
|
4962
|
+
!sameRect(next.col, last.col) ||
|
|
4482
4963
|
!sameList(next.holes, last.holes) ||
|
|
4483
4964
|
!sameList(next.popovers, last.popovers)) {
|
|
4484
4965
|
last = next;
|
|
@@ -4633,6 +5114,43 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4633
5114
|
}
|
|
4634
5115
|
return false;
|
|
4635
5116
|
};
|
|
5117
|
+
/**
|
|
5118
|
+
* Whether a form may be submitted during this step (shell ≥ 0.1.77): only
|
|
5119
|
+
* when its submit control — the button pressed, or the one Enter in a
|
|
5120
|
+
* field would press — is the step's pressable target or in its `allow`.
|
|
5121
|
+
* Otherwise Enter in an allowed field would save a draft (and take a
|
|
5122
|
+
* document number) behind the guide's back.
|
|
5123
|
+
*/
|
|
5124
|
+
const submitAllowed = (form, submitter) => {
|
|
5125
|
+
const btn = submitter ?? defaultSubmitControl(form);
|
|
5126
|
+
if (!btn)
|
|
5127
|
+
return false;
|
|
5128
|
+
const el = targetElRef.current;
|
|
5129
|
+
const sc = stateRef.current?.screen;
|
|
5130
|
+
const pressable = targetPressable(stepInfoRef.current?.step, sc?.name === "step" && sc.bridge);
|
|
5131
|
+
if (el && pressable && el.contains(btn))
|
|
5132
|
+
return true;
|
|
5133
|
+
for (const a of stepInfoRef.current?.step.allow ?? []) {
|
|
5134
|
+
const ael = findAnchor(a);
|
|
5135
|
+
if (ael && ael.contains(btn))
|
|
5136
|
+
return true;
|
|
5137
|
+
}
|
|
5138
|
+
return false;
|
|
5139
|
+
};
|
|
5140
|
+
const onSubmit = (e) => {
|
|
5141
|
+
const sc = stateRef.current?.screen;
|
|
5142
|
+
if (!guardingRef.current || sc?.name !== "step")
|
|
5143
|
+
return;
|
|
5144
|
+
const form = e.target;
|
|
5145
|
+
if (!(form instanceof HTMLFormElement) || inGuideLayer(form))
|
|
5146
|
+
return;
|
|
5147
|
+
const submitter = e.submitter ?? null;
|
|
5148
|
+
if (submitAllowed(form, submitter))
|
|
5149
|
+
return;
|
|
5150
|
+
e.preventDefault();
|
|
5151
|
+
e.stopPropagation();
|
|
5152
|
+
dispatchRef.current({ type: "blocked" });
|
|
5153
|
+
};
|
|
4636
5154
|
const onPointer = (e) => {
|
|
4637
5155
|
const t = e.target;
|
|
4638
5156
|
if (inGuideLayer(t)) {
|
|
@@ -4752,8 +5270,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4752
5270
|
return;
|
|
4753
5271
|
}
|
|
4754
5272
|
}
|
|
4755
|
-
if (modalRef.current || !allowedTarget(active))
|
|
5273
|
+
if (modalRef.current || !allowedTarget(active)) {
|
|
4756
5274
|
stop();
|
|
5275
|
+
return;
|
|
5276
|
+
}
|
|
5277
|
+
// Enter in a field the step lets the person type in: no implicit
|
|
5278
|
+
// submit unless the form's submit control is allowed now.
|
|
5279
|
+
if (e.key === "Enter" && sc.name === "step" && submitsOnEnter(active) && active.form && !submitAllowed(active.form, null)) {
|
|
5280
|
+
e.preventDefault();
|
|
5281
|
+
}
|
|
4757
5282
|
}
|
|
4758
5283
|
};
|
|
4759
5284
|
window.addEventListener("pointerdown", onPointer, true);
|
|
@@ -4763,7 +5288,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4763
5288
|
window.addEventListener("focusin", onFocusIn, true);
|
|
4764
5289
|
window.addEventListener("focusout", onFocusOut, true);
|
|
4765
5290
|
window.addEventListener("keydown", onKey, true);
|
|
5291
|
+
window.addEventListener("submit", onSubmit, true);
|
|
4766
5292
|
return () => {
|
|
5293
|
+
window.removeEventListener("submit", onSubmit, true);
|
|
4767
5294
|
window.removeEventListener("pointerdown", onPointer, true);
|
|
4768
5295
|
window.removeEventListener("mousedown", onPointer, true);
|
|
4769
5296
|
window.removeEventListener("touchstart", onPointer, true);
|
|
@@ -4944,6 +5471,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4944
5471
|
openingSection.current = null;
|
|
4945
5472
|
}
|
|
4946
5473
|
}, []);
|
|
5474
|
+
const flowsNow = React.useMemo(() => pagesLive && hasState && hasFlows
|
|
5475
|
+
? flowsAvailable(suite, env()).map(k => ({ key: k, title: findFlow(suite, k)?.title ?? "" }))
|
|
5476
|
+
: [],
|
|
5477
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5478
|
+
[pagesLive, hasState, hasFlows, suite, env, pathname, search, factsKey]);
|
|
5479
|
+
const flowsSig = flowsNow.map(x => x.key).join(",");
|
|
5480
|
+
/** The flows the panel lists: this person's, startable here or with somewhere to go. */
|
|
5481
|
+
const flowsListed = React.useMemo(() => (pagesLive && hasFlows && ctxIdle ? shownFlows(suite, ctxIdle).filter(f => onFlowStart(f, { code, pathname, search: new URLSearchParams(search) }) || !!flowHref(f, ctxIdle)) : []), [pagesLive, hasFlows, suite, ctxIdle, code, pathname, search]);
|
|
5482
|
+
const openFlowHere = React.useCallback((key) => dispatchRef.current({ type: "startFlow", flow: key }), []);
|
|
4947
5483
|
const openPageHere = React.useCallback(() => {
|
|
4948
5484
|
const p = pageHere(suite, env());
|
|
4949
5485
|
if (p)
|
|
@@ -4954,10 +5490,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4954
5490
|
publishPageGuides(null, null, null);
|
|
4955
5491
|
return;
|
|
4956
5492
|
}
|
|
4957
|
-
publishPageGuides({
|
|
4958
|
-
|
|
5493
|
+
publishPageGuides({
|
|
5494
|
+
page: pageNow ? { title: pageNow.title } : null,
|
|
5495
|
+
listed: listed.length,
|
|
5496
|
+
sections: sectionsNow,
|
|
5497
|
+
flows: flowsNow,
|
|
5498
|
+
flowsListed: flowsListed.length,
|
|
5499
|
+
}, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }), openSection, openFlowHere, () => dispatchRef.current({ type: "openPanel", section: "flows" }));
|
|
5500
|
+
// sectionsSig / flowsSig stand for sectionsNow / flowsNow.
|
|
4959
5501
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4960
|
-
}, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection]);
|
|
5502
|
+
}, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere]);
|
|
4961
5503
|
React.useEffect(() => () => publishPageGuides(null, null, null), []);
|
|
4962
5504
|
// The first-visit offer: in "offer" mode, nothing else up, the welcome
|
|
4963
5505
|
// not due here, and a moment of nothing loading on this page.
|
|
@@ -4985,14 +5527,21 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4985
5527
|
if (Date.now() - idleSince < PAGE_OFFER_IDLE_MS)
|
|
4986
5528
|
return;
|
|
4987
5529
|
let key = null;
|
|
5530
|
+
let flow = null;
|
|
4988
5531
|
try {
|
|
4989
5532
|
key = pageOfferDue(cur, suite, env());
|
|
5533
|
+
// A flow's offer comes after the page guide's (and only for `offer: true` flows).
|
|
5534
|
+
if (!key)
|
|
5535
|
+
flow = flowOfferDue(cur, suite, env());
|
|
4990
5536
|
}
|
|
4991
5537
|
catch {
|
|
4992
5538
|
key = null;
|
|
5539
|
+
flow = null;
|
|
4993
5540
|
}
|
|
4994
5541
|
if (key)
|
|
4995
5542
|
dispatchRef.current({ type: "pageOffer", page: key });
|
|
5543
|
+
else if (flow)
|
|
5544
|
+
dispatchRef.current({ type: "pageOffer", page: flow, flow: true });
|
|
4996
5545
|
}, PAGE_POLL_MS);
|
|
4997
5546
|
return () => clearInterval(iv);
|
|
4998
5547
|
}, [offerWatch, suite, env, pathname, search]);
|
|
@@ -5044,6 +5593,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5044
5593
|
if (b)
|
|
5045
5594
|
return;
|
|
5046
5595
|
const e = env();
|
|
5596
|
+
if (pending.flow) {
|
|
5597
|
+
const flow = findFlow(suite, pending.key);
|
|
5598
|
+
if (!flow || !onFlowStart(flow, e))
|
|
5599
|
+
return;
|
|
5600
|
+
setPending(null);
|
|
5601
|
+
dispatchRef.current({ type: "startFlow", flow: pending.key });
|
|
5602
|
+
return;
|
|
5603
|
+
}
|
|
5047
5604
|
const page = findPage(suite, pending.key);
|
|
5048
5605
|
if (!page || !pageMatchScore(page, { code, pathname: e.pathname, search: e.search, isVisible: a => e.isVisible(a) }))
|
|
5049
5606
|
return;
|
|
@@ -5052,6 +5609,93 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5052
5609
|
}, PAGE_POLL_MS);
|
|
5053
5610
|
return () => clearInterval(iv);
|
|
5054
5611
|
}, [pending, pagesLive, suite, env, code]);
|
|
5612
|
+
// A flow in the panel's list: start it here, or go to its start page and start it there.
|
|
5613
|
+
const openListedFlow = React.useCallback((key) => {
|
|
5614
|
+
const flow = findFlow(suite, key);
|
|
5615
|
+
if (!flow)
|
|
5616
|
+
return;
|
|
5617
|
+
const e = env();
|
|
5618
|
+
if (onFlowStart(flow, e)) {
|
|
5619
|
+
dispatchRef.current({ type: "startFlow", flow: key });
|
|
5620
|
+
return;
|
|
5621
|
+
}
|
|
5622
|
+
dispatchRef.current({ type: "closePanel" });
|
|
5623
|
+
const href = flowHref(flow, guideContext(e, {}));
|
|
5624
|
+
if (!href)
|
|
5625
|
+
return;
|
|
5626
|
+
setPending({ key, until: Date.now() + PAGE_ARRIVE_MS, flow: true });
|
|
5627
|
+
try {
|
|
5628
|
+
if (/^https?:\/\//i.test(href))
|
|
5629
|
+
window.location.assign(href);
|
|
5630
|
+
else
|
|
5631
|
+
navigateRef.current(href);
|
|
5632
|
+
}
|
|
5633
|
+
catch {
|
|
5634
|
+
/* never breaks the app */
|
|
5635
|
+
}
|
|
5636
|
+
}, [suite, env]);
|
|
5637
|
+
// A flow in progress is kept in this tab's session (sessionStorage), so a
|
|
5638
|
+
// refresh or a full page load on one of its pages picks it up again at
|
|
5639
|
+
// its checkpoint ("ทำต่อ" when its step is on another page). Never across
|
|
5640
|
+
// sessions; a flow that ends is forgotten.
|
|
5641
|
+
const flowRun = state?.pageRun?.kind === "flow" ? state.pageRun : null;
|
|
5642
|
+
const flowStepKey = screen.name === "step" && screen.flow ? (findMission(suite, screen.mission)?.steps[screen.index]?.key ?? null) : null;
|
|
5643
|
+
React.useEffect(() => {
|
|
5644
|
+
if (!hasFlows)
|
|
5645
|
+
return;
|
|
5646
|
+
const k = flowMirrorKey(organizationId, suite);
|
|
5647
|
+
try {
|
|
5648
|
+
if (!flowRun) {
|
|
5649
|
+
if (state)
|
|
5650
|
+
sessionStorage.removeItem(k);
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5653
|
+
if (!flowStepKey)
|
|
5654
|
+
return;
|
|
5655
|
+
const flow = findFlow(suite, flowRun.key);
|
|
5656
|
+
sessionStorage.setItem(k, JSON.stringify({ flow: flowRun.key, version: flow?.version, step: flowStepKey, write: flowRun.write, context: state?.progress.context ?? {} }));
|
|
5657
|
+
}
|
|
5658
|
+
catch {
|
|
5659
|
+
/* private mode: no resume after a refresh */
|
|
5660
|
+
}
|
|
5661
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5662
|
+
}, [hasFlows, flowRun?.key, flowRun?.write, flowStepKey, organizationId, suite]);
|
|
5663
|
+
const flowResumed = React.useRef(false);
|
|
5664
|
+
React.useEffect(() => {
|
|
5665
|
+
if (flowResumed.current || !pagesLive || !state || !hasFlows)
|
|
5666
|
+
return;
|
|
5667
|
+
// The missions resume first.
|
|
5668
|
+
if (missionsLive && !resumed.current)
|
|
5669
|
+
return;
|
|
5670
|
+
flowResumed.current = true;
|
|
5671
|
+
const k = flowMirrorKey(organizationId, suite);
|
|
5672
|
+
let saved = null;
|
|
5673
|
+
try {
|
|
5674
|
+
saved = JSON.parse(sessionStorage.getItem(k) ?? "null");
|
|
5675
|
+
}
|
|
5676
|
+
catch {
|
|
5677
|
+
saved = null;
|
|
5678
|
+
}
|
|
5679
|
+
if (!saved?.flow || !saved.step)
|
|
5680
|
+
return;
|
|
5681
|
+
const flow = findFlow(suite, saved.flow);
|
|
5682
|
+
const ok = !!flow && flow.version === saved.version && stateRef.current?.screen.name === "idle" && inFlowChain(flow, { code, pathname });
|
|
5683
|
+
if (!ok) {
|
|
5684
|
+
try {
|
|
5685
|
+
sessionStorage.removeItem(k);
|
|
5686
|
+
}
|
|
5687
|
+
catch {
|
|
5688
|
+
/* ignore */
|
|
5689
|
+
}
|
|
5690
|
+
return;
|
|
5691
|
+
}
|
|
5692
|
+
const context = {};
|
|
5693
|
+
for (const [ck, cv] of Object.entries(saved.context ?? {}))
|
|
5694
|
+
if (typeof cv === "string")
|
|
5695
|
+
context[ck] = cv;
|
|
5696
|
+
dispatchRef.current({ type: "resumeFlow", flow: saved.flow, step: saved.step, context, write: saved.write === true });
|
|
5697
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5698
|
+
}, [pagesLive, !!state, hasFlows, missionsLive]);
|
|
5055
5699
|
if (!live || !state || !layer || !ctxIdle)
|
|
5056
5700
|
return null;
|
|
5057
5701
|
/* ---------------- render ---------------- */
|
|
@@ -5074,18 +5718,29 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5074
5718
|
const missionNo = missionNumber(suite, mission.key, ctxIdle);
|
|
5075
5719
|
const kind = stepKind(step);
|
|
5076
5720
|
const bridge = screen.bridge;
|
|
5077
|
-
|
|
5721
|
+
// What is ahead now decides เสร็จ; the number shown is the run's counter,
|
|
5722
|
+
// which never goes down (0.1.77).
|
|
5723
|
+
const raw = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
|
|
5724
|
+
const last = raw.no >= raw.total;
|
|
5725
|
+
const pos = screen.pos ?? raw;
|
|
5078
5726
|
const page = screen.page ? findPage(suite, screen.page) : undefined;
|
|
5079
5727
|
const section = screen.section ? findSection(suite, screen.section) : undefined;
|
|
5728
|
+
const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
|
|
5080
5729
|
const read = page ? { heading: `คู่มือหน้านี้ · ${page.title}`, exitLabel: "ปิดคู่มือ" } : section ? { heading: `อธิบายส่วนนี้ · ${section.title}`, exitLabel: "ปิดคำอธิบาย" } : null;
|
|
5081
5730
|
const pageBits = read
|
|
5082
5731
|
? {
|
|
5083
5732
|
...read,
|
|
5084
5733
|
showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
|
|
5085
|
-
nextLabel:
|
|
5734
|
+
nextLabel: last ? "เสร็จ" : undefined,
|
|
5086
5735
|
}
|
|
5087
|
-
:
|
|
5088
|
-
|
|
5736
|
+
: flow
|
|
5737
|
+
? {
|
|
5738
|
+
heading: `พาทำ · ${flow.title}`,
|
|
5739
|
+
exitLabel: "ออกจากการพาทำ",
|
|
5740
|
+
nextLabel: kind === "view" && last ? "เสร็จ" : undefined,
|
|
5741
|
+
}
|
|
5742
|
+
: {};
|
|
5743
|
+
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: {
|
|
5089
5744
|
missionNo,
|
|
5090
5745
|
missionTitle: mission.title,
|
|
5091
5746
|
replay: screen.replay,
|
|
@@ -5101,6 +5756,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5101
5756
|
...pageBits,
|
|
5102
5757
|
}, hasTarget: bridge || !!step.target, targetPressable: targetPressable(step, bridge), phase: screen.phase, confirmExit: screen.confirmExit, reopenHint: suite.reopenHint, nudge: state.nudge, moving: moving, onExit: () => dispatch({ type: "exit" }), onNext: () => dispatch({ type: "next" }), onBack: () => dispatch({ type: "back" }), onEndWithoutSave: () => dispatch({ type: "endWithoutSave" }), onRetry: () => dispatch({ type: "retry" }), onGoResume: () => dispatch({ type: "goResume" }), onExitConfirm: () => dispatch({ type: "exitConfirm" }), onExitCancel: () => dispatch({ type: "exitCancel" }) }));
|
|
5103
5758
|
}
|
|
5759
|
+
else if (screen.name === "pageOffer" && pagesLive && screen.flow) {
|
|
5760
|
+
const flow = findFlow(suite, screen.page);
|
|
5761
|
+
const mission = flow ? findMission(suite, FLOW_PREFIX + flow.key) : undefined;
|
|
5762
|
+
if (flow && mission) {
|
|
5763
|
+
content = (jsxRuntime.jsx(PageOfferCard, { eyebrow: "\u0E1E\u0E32\u0E17\u0E33\u0E07\u0E32\u0E19", title: flow.title, steps: stepPosition(mission, 0, env(), {}).total, onStart: () => dispatch({ type: "pageOfferStart" }), onLater: () => dispatch({ type: "pageOfferLater" }) }));
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5104
5766
|
else if (screen.name === "pageOffer" && pagesLive) {
|
|
5105
5767
|
const page = findPage(suite, screen.page);
|
|
5106
5768
|
if (page) {
|
|
@@ -5144,12 +5806,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5144
5806
|
const showChip = missionsLive && chipVisible(state, suite, ctxIdle);
|
|
5145
5807
|
// The panel: the missions (when this person has them, and not "คู่มือทุกหน้า"),
|
|
5146
5808
|
// then the page guides they have.
|
|
5147
|
-
const panelMissions = missionsLive && total > 0 && state.panelSection
|
|
5148
|
-
const pageItems = pagesLive
|
|
5149
|
-
|
|
5809
|
+
const panelMissions = missionsLive && total > 0 && state.panelSection === "all";
|
|
5810
|
+
const pageItems = pagesLive && state.panelSection !== "flows" ? listed.map(p => ({ key: p.key, title: p.title, seen: pageRecorded(state, p) })) : [];
|
|
5811
|
+
const flowItems = pagesLive && state.panelSection !== "pages"
|
|
5812
|
+
? flowsListed.map(f => ({ key: f.key, title: f.title, minutes: f.minutes, done: flowRecorded(state, f) && state.flows[f.key]?.status === "completed" }))
|
|
5150
5813
|
: [];
|
|
5151
|
-
const panelShown = state.panelOpen && screen.name === "idle" && (panelMissions || pageItems.length > 0);
|
|
5152
|
-
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("style", { children: GUIDE_CSS }), jsxRuntime.jsx("div", { className: "eg-sr", "aria-live": "polite", role: "status", children: liveText }), content, showChip ? jsxRuntime.jsx(QuestChip, { done: done, total: total, pulse: state.chipPulse, onOpen: () => dispatch({ type: "openPanel" }) }) : null, panelShown ? (jsxRuntime.jsx(QuestPanel, { items: items, done: done, total: total, justDone: state.justDone, gate: gate && gateFailing ? { label: gate.action.label, pending: gate.pending, href: gateHref } : null, canHideChip: !state.progress.chip_hidden && state.progress.status !== "completed", onClose: () => dispatch({ type: "closePanel" }), onStart: (key, replayIt) => dispatch({ type: "start", mission: key, replay: replayIt }), onDismiss: key => dispatch({ type: "dismissMission", mission: key }), onHideChip: () => dispatch({ type: "hideChip" }), pages: pageItems, showMissions: panelMissions, onOpenPage: openListedPage })) : null, toast ? jsxRuntime.jsx(GuideToast, { text: toast }) : null] }), layer);
|
|
5814
|
+
const panelShown = state.panelOpen && screen.name === "idle" && (panelMissions || pageItems.length > 0 || flowItems.length > 0);
|
|
5815
|
+
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("style", { children: GUIDE_CSS }), jsxRuntime.jsx("div", { className: "eg-sr", "aria-live": "polite", role: "status", children: liveText }), content, showChip ? jsxRuntime.jsx(QuestChip, { done: done, total: total, pulse: state.chipPulse, onOpen: () => dispatch({ type: "openPanel" }) }) : null, panelShown ? (jsxRuntime.jsx(QuestPanel, { items: items, done: done, total: total, justDone: state.justDone, gate: gate && gateFailing ? { label: gate.action.label, pending: gate.pending, href: gateHref } : null, canHideChip: !state.progress.chip_hidden && state.progress.status !== "completed", onClose: () => dispatch({ type: "closePanel" }), onStart: (key, replayIt) => dispatch({ type: "start", mission: key, replay: replayIt }), onDismiss: key => dispatch({ type: "dismissMission", mission: key }), onHideChip: () => dispatch({ type: "hideChip" }), pages: pageItems, showMissions: panelMissions, onOpenPage: openListedPage, flows: flowItems, onOpenFlow: openListedFlow })) : null, toast ? jsxRuntime.jsx(GuideToast, { text: toast }) : null] }), layer);
|
|
5153
5816
|
}
|
|
5154
5817
|
/** A stable key for the facts object, so effects re-run when they change. */
|
|
5155
5818
|
function useFactsKey(facts) {
|
|
@@ -5277,6 +5940,8 @@ exports.guideMenuShown = guideMenuShown;
|
|
|
5277
5940
|
exports.loadGuides = loadGuides;
|
|
5278
5941
|
exports.matchApiPath = matchApiPath;
|
|
5279
5942
|
exports.matchRoute = matchRoute;
|
|
5943
|
+
exports.openFlow = openFlow;
|
|
5944
|
+
exports.openFlowsPanel = openFlowsPanel;
|
|
5280
5945
|
exports.openGuidePanel = openGuidePanel;
|
|
5281
5946
|
exports.openPageGuide = openPageGuide;
|
|
5282
5947
|
exports.openPagesPanel = openPagesPanel;
|
|
@@ -5291,4 +5956,4 @@ exports.useGuideEntry = useGuideEntry;
|
|
|
5291
5956
|
exports.usePageGuideEntry = usePageGuideEntry;
|
|
5292
5957
|
exports.usePageGuides = usePageGuides;
|
|
5293
5958
|
exports.useSectionGuideValues = useSectionGuideValues;
|
|
5294
|
-
//# sourceMappingURL=format-
|
|
5959
|
+
//# sourceMappingURL=format-DQLPHq7h.cjs.map
|