@eintrek/erp-shell 0.1.75 → 0.1.76
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 +25 -0
- package/dist/chunks/{format-CVazK0hP.cjs → format-BVoVc7Yk.cjs} +510 -35
- package/dist/chunks/format-BVoVc7Yk.cjs.map +1 -0
- package/dist/chunks/{format-DCTuLpBU.esm.js → format-BWUKNKxg.esm.js} +509 -36
- package/dist/chunks/format-BWUKNKxg.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 +16 -2
- package/dist/guide/machine.d.ts +46 -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 +53 -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
|
@@ -337,26 +337,34 @@ function publishGuideEntry(next, openPanel) {
|
|
|
337
337
|
emit();
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
|
-
const NO_PAGES = { page: null, listed: 0, sections: [] };
|
|
340
|
+
const NO_PAGES = { page: null, listed: 0, sections: [], flows: [], flowsListed: 0 };
|
|
341
341
|
let pages = NO_PAGES;
|
|
342
342
|
let openPageImpl = null;
|
|
343
343
|
let openPagesImpl = null;
|
|
344
344
|
let openSectionImpl = null;
|
|
345
|
+
let openFlowImpl = null;
|
|
346
|
+
let openFlowsImpl = null;
|
|
345
347
|
const sectionsKey = (x) => (x ?? []).map(s => `${s.key}\u0000${s.title}`).join("\u0001");
|
|
346
348
|
/** Called by the host: this page's guide, the listed count, the sections on screen, and how to open them. */
|
|
347
|
-
function publishPageGuides(next, openPage, openPages, openSection = null) {
|
|
349
|
+
function publishPageGuides(next, openPage, openPages, openSection = null, openFlow = null, openFlows = null) {
|
|
348
350
|
const n = next ?? NO_PAGES;
|
|
349
351
|
openPageImpl = openPage;
|
|
350
352
|
openPagesImpl = openPages;
|
|
351
353
|
openSectionImpl = openSection;
|
|
354
|
+
openFlowImpl = openFlow;
|
|
355
|
+
openFlowsImpl = openFlows;
|
|
352
356
|
const same = n.listed === pages.listed &&
|
|
357
|
+
(n.flowsListed ?? 0) === (pages.flowsListed ?? 0) &&
|
|
353
358
|
(n.page?.title ?? null) === (pages.page?.title ?? null) &&
|
|
354
|
-
sectionsKey(n.sections) === sectionsKey(pages.sections)
|
|
359
|
+
sectionsKey(n.sections) === sectionsKey(pages.sections) &&
|
|
360
|
+
sectionsKey(n.flows) === sectionsKey(pages.flows);
|
|
355
361
|
if (!same) {
|
|
356
362
|
const sections = (n.sections ?? []).map(x => ({ key: x.key, title: x.title }));
|
|
363
|
+
const flows = (n.flows ?? []).map(x => ({ key: x.key, title: x.title }));
|
|
364
|
+
const flowsListed = n.flowsListed ?? 0;
|
|
357
365
|
pages =
|
|
358
|
-
n.page || n.listed || sections.length
|
|
359
|
-
? { page: n.page ? { title: n.page.title } : null, listed: n.listed, sections }
|
|
366
|
+
n.page || n.listed || sections.length || flows.length || flowsListed
|
|
367
|
+
? { page: n.page ? { title: n.page.title } : null, listed: n.listed, sections, flows, flowsListed }
|
|
360
368
|
: NO_PAGES;
|
|
361
369
|
emit();
|
|
362
370
|
}
|
|
@@ -367,10 +375,14 @@ function guideMenuItems(e, p = NO_PAGES) {
|
|
|
367
375
|
items.push("page");
|
|
368
376
|
if ((p.sections ?? []).length > 0)
|
|
369
377
|
items.push("sections");
|
|
378
|
+
if ((p.flows ?? []).length > 0)
|
|
379
|
+
items.push("flowsHere");
|
|
370
380
|
if (e && e.total > 0)
|
|
371
381
|
items.push("missions");
|
|
372
382
|
if (p.listed > 0)
|
|
373
383
|
items.push("pages");
|
|
384
|
+
if ((p.flowsListed ?? 0) > 0)
|
|
385
|
+
items.push("flows");
|
|
374
386
|
if (items.length)
|
|
375
387
|
items.push("help");
|
|
376
388
|
return items;
|
|
@@ -424,6 +436,14 @@ function usePageGuideEntry() {
|
|
|
424
436
|
function openSectionGuide(key) {
|
|
425
437
|
openSectionImpl?.(key);
|
|
426
438
|
}
|
|
439
|
+
/** Starts a flow that starts on this page ("พาทำ · <title>"). */
|
|
440
|
+
function openFlow(key) {
|
|
441
|
+
openFlowImpl?.(key);
|
|
442
|
+
}
|
|
443
|
+
/** Opens the panel on the flows alone ("พาทำงาน"). */
|
|
444
|
+
function openFlowsPanel() {
|
|
445
|
+
openFlowsImpl?.();
|
|
446
|
+
}
|
|
427
447
|
const sectionValues = new Map();
|
|
428
448
|
const valueListeners = new Set();
|
|
429
449
|
let valuesVersion = 0;
|
|
@@ -801,6 +821,144 @@ function sectionShown(section, ctx) {
|
|
|
801
821
|
function sectionsHere(suite, where, ctx) {
|
|
802
822
|
return validSections(suite).filter(s => sectionApplies(s, where) && sectionShown(s, ctx));
|
|
803
823
|
}
|
|
824
|
+
/* ------------------------------------------------------------------ */
|
|
825
|
+
/* Workflow guides ("พาทำงาน", shell 0.1.76) */
|
|
826
|
+
/* ------------------------------------------------------------------ */
|
|
827
|
+
/** The synthetic mission key of a flow: `flow:<key>`. */
|
|
828
|
+
const FLOW_PREFIX = "flow:";
|
|
829
|
+
function isFlowMissionKey(key) {
|
|
830
|
+
return !!key && key.startsWith(FLOW_PREFIX);
|
|
831
|
+
}
|
|
832
|
+
function flowKeyOf(missionKey) {
|
|
833
|
+
return isFlowMissionKey(missionKey) ? missionKey.slice(FLOW_PREFIX.length) : undefined;
|
|
834
|
+
}
|
|
835
|
+
const asList = (r) => (r === undefined ? [] : Array.isArray(r) ? r : [r]);
|
|
836
|
+
/** Why a flow cannot run, or null when it can. */
|
|
837
|
+
function flowProblem(app, flow) {
|
|
838
|
+
if (!flow || typeof flow.key !== "string" || !PAGE_KEY_RE.test(flow.key))
|
|
839
|
+
return "its key is not [a-z0-9-]";
|
|
840
|
+
if (`${app}.flow.${flow.key}`.length > ROW_KEY_MAX)
|
|
841
|
+
return `its row key is over ${ROW_KEY_MAX} characters`;
|
|
842
|
+
if (!Number.isInteger(flow.version) || flow.version < 1)
|
|
843
|
+
return "its version is not a whole number ≥ 1";
|
|
844
|
+
const starts = asList(flow.startRoute);
|
|
845
|
+
if (!starts.length || starts.some(r => typeof r !== "string" || !r.startsWith("/")))
|
|
846
|
+
return "it has no start route";
|
|
847
|
+
if (!Array.isArray(flow.steps) || flow.steps.length === 0)
|
|
848
|
+
return "it has no steps";
|
|
849
|
+
for (const st of flow.steps) {
|
|
850
|
+
const kind = st.kind ?? "click";
|
|
851
|
+
if (kind === "view" || kind === "click")
|
|
852
|
+
continue;
|
|
853
|
+
if (!flow.allowCommit)
|
|
854
|
+
return `step "${st.key}" is a ${kind} step and the flow does not set allowCommit`;
|
|
855
|
+
if (kind === "commit" && !st.abort)
|
|
856
|
+
return `commit step "${st.key}" has no abort`;
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
const validFlowCache = new WeakMap();
|
|
861
|
+
/** The suite's flows that can run (as validPages: the rest dropped, with a warning in development). */
|
|
862
|
+
function validFlows(suite) {
|
|
863
|
+
const hit = validFlowCache.get(suite);
|
|
864
|
+
if (hit)
|
|
865
|
+
return hit;
|
|
866
|
+
const out = [];
|
|
867
|
+
const seen = new Set();
|
|
868
|
+
for (const flow of suite.flows ?? []) {
|
|
869
|
+
let problem = flowProblem(suite.app, flow);
|
|
870
|
+
if (!problem && seen.has(flow.key))
|
|
871
|
+
problem = "another flow has the same key";
|
|
872
|
+
if (problem) {
|
|
873
|
+
const id = `${suite.key}:flow:${flow?.key}:${problem}`;
|
|
874
|
+
if (!warned.has(id)) {
|
|
875
|
+
warned.add(id);
|
|
876
|
+
devWarn(`flow "${flow?.key}" dropped: ${problem}`);
|
|
877
|
+
}
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
seen.add(flow.key);
|
|
881
|
+
out.push(flow);
|
|
882
|
+
}
|
|
883
|
+
validFlowCache.set(suite, out);
|
|
884
|
+
return out;
|
|
885
|
+
}
|
|
886
|
+
function findFlow(suite, key) {
|
|
887
|
+
return key ? validFlows(suite).find(f => f.key === key) : undefined;
|
|
888
|
+
}
|
|
889
|
+
const flowMissionCache = new WeakMap();
|
|
890
|
+
/** The flow as a mission the machine plays (in replay mode): `flow:<key>`, its steps as authored. */
|
|
891
|
+
function flowMission(flow) {
|
|
892
|
+
const hit = flowMissionCache.get(flow);
|
|
893
|
+
if (hit)
|
|
894
|
+
return hit;
|
|
895
|
+
const m = { key: FLOW_PREFIX + flow.key, title: flow.title, minutes: flow.minutes ?? 1, steps: flow.steps };
|
|
896
|
+
flowMissionCache.set(flow, m);
|
|
897
|
+
return m;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* The flow's chain: its start routes, every step's `route` and route
|
|
901
|
+
* trigger, and its `transit` routes. Leaving all of them ends the flow.
|
|
902
|
+
*/
|
|
903
|
+
function flowChain(flow) {
|
|
904
|
+
const out = new Set(asList(flow.startRoute));
|
|
905
|
+
for (const st of flow.steps) {
|
|
906
|
+
if (st.route)
|
|
907
|
+
out.add(st.route);
|
|
908
|
+
const triggers = st.advanceOn ? (Array.isArray(st.advanceOn) ? st.advanceOn : [st.advanceOn]) : [];
|
|
909
|
+
for (const t of triggers)
|
|
910
|
+
if ("route" in t && t.route)
|
|
911
|
+
out.add(t.route);
|
|
912
|
+
}
|
|
913
|
+
for (const t of flow.transit ?? [])
|
|
914
|
+
out.add(t);
|
|
915
|
+
return Array.from(out);
|
|
916
|
+
}
|
|
917
|
+
/** Path patterns only (`?k=v` dropped): the chain is about pages, not their tabs. */
|
|
918
|
+
const pathOf = (pattern) => (pattern.includes("?") ? pattern.slice(0, pattern.indexOf("?")) : pattern);
|
|
919
|
+
/** Whether the person is on one of the flow's pages. */
|
|
920
|
+
function inFlowChain(flow, where) {
|
|
921
|
+
return flowChain(flow).some(r => !!matchRoute(pathOf(r), where.pathname, null, where.code));
|
|
922
|
+
}
|
|
923
|
+
/** Whether the person is on the flow's start page. */
|
|
924
|
+
function onFlowStart(flow, where) {
|
|
925
|
+
return asList(flow.startRoute).some(r => !!matchRoute(r, where.pathname, where.search, where.code));
|
|
926
|
+
}
|
|
927
|
+
/** Whether this person has the flow: `visible` (only true) and `requires`. */
|
|
928
|
+
function flowShown(flow, ctx) {
|
|
929
|
+
try {
|
|
930
|
+
if (flow.visible && flow.visible(ctx) !== true)
|
|
931
|
+
return false;
|
|
932
|
+
if (flow.requires && !ctx.can(flow.requires))
|
|
933
|
+
return false;
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/** The flows this person has (the panel's "พาทำงาน" list), in the suite's order. */
|
|
941
|
+
function shownFlows(suite, ctx) {
|
|
942
|
+
return validFlows(suite).filter(f => flowShown(f, ctx));
|
|
943
|
+
}
|
|
944
|
+
/** Where the panel sends the person to start a flow, or null when it needs an id it does not have. */
|
|
945
|
+
function flowHref(flow, ctx) {
|
|
946
|
+
let raw;
|
|
947
|
+
try {
|
|
948
|
+
raw = flow.href === undefined ? undefined : typeof flow.href === "function" ? flow.href(ctx) : flow.href;
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
raw = undefined;
|
|
952
|
+
}
|
|
953
|
+
if (raw)
|
|
954
|
+
return /^https?:\/\//i.test(raw) ? raw : (fillRoute(raw, { code: ctx.code }) ?? null);
|
|
955
|
+
for (const r of asList(flow.startRoute)) {
|
|
956
|
+
const filled = fillRoute(r, { code: ctx.code });
|
|
957
|
+
if (filled)
|
|
958
|
+
return filled;
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
804
962
|
|
|
805
963
|
/* ------------------------------------------------------------------ */
|
|
806
964
|
/* Copy */
|
|
@@ -816,6 +974,7 @@ const COPY = {
|
|
|
816
974
|
aborted: "ไกด์ไปต่อไม่ได้ ลองอีกครั้งภายหลัง",
|
|
817
975
|
pageDone: "จบคู่มือหน้านี้",
|
|
818
976
|
sectionDone: "จบคำอธิบายส่วนนี้",
|
|
977
|
+
flowDone: "พาทำงานเสร็จแล้ว",
|
|
819
978
|
bridgeTitle: "กดปุ่มเมนู",
|
|
820
979
|
bridgeWhy: (app) => `เมนูทั้งหมดของ ${app} อยู่ในนี้`,
|
|
821
980
|
};
|
|
@@ -829,6 +988,7 @@ function initialGuideState(audience, progress, pages) {
|
|
|
829
988
|
pagesMode: pages?.pagesMode ?? "off",
|
|
830
989
|
pages: pages?.pages ?? {},
|
|
831
990
|
sections: pages?.sections ?? {},
|
|
991
|
+
flows: pages?.flows ?? {},
|
|
832
992
|
pageRun: null,
|
|
833
993
|
panelSection: "all",
|
|
834
994
|
autoPassed: null,
|
|
@@ -855,6 +1015,10 @@ function findMission(suite, key) {
|
|
|
855
1015
|
const section = findSection(suite, sectionKeyOf(key));
|
|
856
1016
|
return section ? sectionMission(section) : undefined;
|
|
857
1017
|
}
|
|
1018
|
+
if (isFlowMissionKey(key)) {
|
|
1019
|
+
const flow = findFlow(suite, flowKeyOf(key));
|
|
1020
|
+
return flow ? flowMission(flow) : undefined;
|
|
1021
|
+
}
|
|
858
1022
|
return suite.missions.find(m => m.key === key);
|
|
859
1023
|
}
|
|
860
1024
|
/**
|
|
@@ -1303,6 +1467,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1303
1467
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
1304
1468
|
const page = pageKeyOf(mission.key);
|
|
1305
1469
|
const section = sectionKeyOf(mission.key);
|
|
1470
|
+
const flow = flowKeyOf(mission.key);
|
|
1306
1471
|
const where = placeRoute(steps, i);
|
|
1307
1472
|
// A page or section guide is on its page: leaving it ends the guide (never "ทำต่อ").
|
|
1308
1473
|
const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
|
|
@@ -1324,6 +1489,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1324
1489
|
replayNext: !away && !bridge && opts.replay && replayCanGoOn(mission, i, env, d.state.progress.context, nts),
|
|
1325
1490
|
...(page ? { page } : {}),
|
|
1326
1491
|
...(section ? { section } : {}),
|
|
1492
|
+
...(flow ? { flow } : {}),
|
|
1327
1493
|
});
|
|
1328
1494
|
if (!opts.replay) {
|
|
1329
1495
|
const cp = checkpointAtOrBefore(steps, i);
|
|
@@ -1488,9 +1654,12 @@ function advance(d, suite, env, capture) {
|
|
|
1488
1654
|
enterStep(d, suite, env, mission, s.index + 1, { replay: s.replay });
|
|
1489
1655
|
}
|
|
1490
1656
|
function finishMission(d, suite, env, mission, replay) {
|
|
1491
|
-
if (isPageMissionKey(mission.key) || isSectionMissionKey(mission.key)) {
|
|
1657
|
+
if (isPageMissionKey(mission.key) || isSectionMissionKey(mission.key) || isFlowMissionKey(mission.key)) {
|
|
1492
1658
|
endPage(d, suite, "completed");
|
|
1493
|
-
d.effects.push({
|
|
1659
|
+
d.effects.push({
|
|
1660
|
+
type: "toast",
|
|
1661
|
+
text: isSectionMissionKey(mission.key) ? COPY.sectionDone : isFlowMissionKey(mission.key) ? COPY.flowDone : COPY.pageDone,
|
|
1662
|
+
});
|
|
1494
1663
|
return;
|
|
1495
1664
|
}
|
|
1496
1665
|
if (replay) {
|
|
@@ -1785,18 +1954,114 @@ function readStillHere(suite, r, env) {
|
|
|
1785
1954
|
const section = findSection(suite, r.key);
|
|
1786
1955
|
return !!section && sectionApplies(section, whereOf(env));
|
|
1787
1956
|
}
|
|
1788
|
-
/** The
|
|
1957
|
+
/** The flow's row says this person has ended this version of it. */
|
|
1958
|
+
function flowRecorded(state, flow) {
|
|
1959
|
+
const row = state.flows?.[flow.key];
|
|
1960
|
+
return !!row && row.version >= flow.version;
|
|
1961
|
+
}
|
|
1962
|
+
/** Write a flow's row (once per flow and version). */
|
|
1963
|
+
function recordFlow(d, suite, key, status) {
|
|
1964
|
+
const flow = findFlow(suite, key);
|
|
1965
|
+
if (!flow || flowRecorded(d.state, flow))
|
|
1966
|
+
return;
|
|
1967
|
+
const prev = d.state.flows?.[key];
|
|
1968
|
+
d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
|
|
1969
|
+
d.effects.push({ type: "savePage", key, version: flow.version, status, kind: "flow" });
|
|
1970
|
+
}
|
|
1971
|
+
/** The page guide, section guide or flow ends: its row written when it is due one; back to idle. */
|
|
1789
1972
|
function endPage(d, suite, how) {
|
|
1790
1973
|
const run = d.state.pageRun;
|
|
1791
1974
|
if (run?.write) {
|
|
1792
1975
|
if (run.kind === "section")
|
|
1793
1976
|
recordSection(d, suite, run.key, how);
|
|
1977
|
+
else if (run.kind === "flow")
|
|
1978
|
+
recordFlow(d, suite, run.key, how);
|
|
1794
1979
|
else
|
|
1795
1980
|
recordPage(d, suite, run.key, how);
|
|
1796
1981
|
}
|
|
1982
|
+
// A flow's captured ids were its own: the tour's come back.
|
|
1983
|
+
if (run?.kind === "flow" && run.context)
|
|
1984
|
+
withProgress(d, { context: run.context });
|
|
1797
1985
|
d.state = { ...d.state, pageRun: null, replayPage: null };
|
|
1798
1986
|
setScreen(d, { name: "idle" });
|
|
1799
1987
|
}
|
|
1988
|
+
/** The flow on screen, or null. */
|
|
1989
|
+
function flowOf(s) {
|
|
1990
|
+
return s.name === "step" && s.flow ? s.flow : null;
|
|
1991
|
+
}
|
|
1992
|
+
/** The flows this person has that start on this page ("พาทำ · <title>"). */
|
|
1993
|
+
function flowsAvailable(suite, env) {
|
|
1994
|
+
if (!suite.flows?.length)
|
|
1995
|
+
return [];
|
|
1996
|
+
const ctx = guideContext(env, {});
|
|
1997
|
+
const where = whereOf(env);
|
|
1998
|
+
return (suite.flows ?? [])
|
|
1999
|
+
.map(f => findFlow(suite, f?.key))
|
|
2000
|
+
.filter((f) => !!f && onFlowStart(f, where) && flowShown(f, ctx))
|
|
2001
|
+
.map(f => f.key);
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* The flow to offer now, or null: only in "offer" mode, only a flow that
|
|
2005
|
+
* says `offer: true`, on its start page, with no row of this version, when
|
|
2006
|
+
* nothing else is up and this page's own guide is not due (it comes first:
|
|
2007
|
+
* no guide here, offer: false, or already recorded), its first step's
|
|
2008
|
+
* target on screen, no app dialog open. The host adds the welcome and the
|
|
2009
|
+
* idle wait, as for the page guide's offer.
|
|
2010
|
+
*/
|
|
2011
|
+
function flowOfferDue(state, suite, env) {
|
|
2012
|
+
if (state.pagesMode !== "offer" || state.pageAudience === "none")
|
|
2013
|
+
return null;
|
|
2014
|
+
if (state.screen.name !== "idle" || state.panelOpen || (state.pendingCelebrate?.length ?? 0) > 0)
|
|
2015
|
+
return null;
|
|
2016
|
+
const page = pageHere(suite, env);
|
|
2017
|
+
if (page && page.offer !== false && !pageRecorded(state, page))
|
|
2018
|
+
return null;
|
|
2019
|
+
if (safe(() => env.dialogOpen?.() === true, false))
|
|
2020
|
+
return null;
|
|
2021
|
+
for (const key of flowsAvailable(suite, env)) {
|
|
2022
|
+
const flow = findFlow(suite, key);
|
|
2023
|
+
if (flow.offer !== true || flowRecorded(state, flow))
|
|
2024
|
+
continue;
|
|
2025
|
+
const mission = flowMission(flow);
|
|
2026
|
+
const first = mission.steps.find(st => {
|
|
2027
|
+
const ctx = guideContext(env, {});
|
|
2028
|
+
if (st.requires && !safe(() => env.can(st.requires), true))
|
|
2029
|
+
return false;
|
|
2030
|
+
return !(st.skipIf && safe(() => st.skipIf(ctx), false));
|
|
2031
|
+
});
|
|
2032
|
+
if (first && !stepTargetVisible(first, env, guideContext(env, {}), false))
|
|
2033
|
+
continue;
|
|
2034
|
+
return key;
|
|
2035
|
+
}
|
|
2036
|
+
return null;
|
|
2037
|
+
}
|
|
2038
|
+
function startFlow(d, suite, env, key, write, at) {
|
|
2039
|
+
const flow = findFlow(suite, key);
|
|
2040
|
+
if (!flow)
|
|
2041
|
+
return;
|
|
2042
|
+
const mission = flowMission(flow);
|
|
2043
|
+
const saved = d.state.progress.context;
|
|
2044
|
+
d.state = {
|
|
2045
|
+
...d.state,
|
|
2046
|
+
panelOpen: false,
|
|
2047
|
+
justDone: null,
|
|
2048
|
+
pageRun: { key, write, kind: "flow", context: saved },
|
|
2049
|
+
autoPassed: null,
|
|
2050
|
+
replayPage: {
|
|
2051
|
+
mission: mission.key,
|
|
2052
|
+
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
2053
|
+
dialog: safe(() => env.dialogOpen?.() === true, false),
|
|
2054
|
+
},
|
|
2055
|
+
};
|
|
2056
|
+
withProgress(d, { context: at?.context ?? {} });
|
|
2057
|
+
let index = 0;
|
|
2058
|
+
if (at) {
|
|
2059
|
+
const stepAt = mission.steps.findIndex(st => st.key === at.step);
|
|
2060
|
+
if (stepAt > 0)
|
|
2061
|
+
index = checkpointAtOrBefore(mission.steps, stepAt);
|
|
2062
|
+
}
|
|
2063
|
+
enterStep(d, suite, env, mission, index, { replay: true });
|
|
2064
|
+
}
|
|
1800
2065
|
/** The section guides on screen here that this person has (the "?" entries, the buttons). */
|
|
1801
2066
|
function sectionsAvailable(suite, env) {
|
|
1802
2067
|
return sectionsHere(suite, whereOf(env), guideContext(env, {})).map(x => x.key);
|
|
@@ -1930,7 +2195,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1930
2195
|
break;
|
|
1931
2196
|
// A mission always wins over a page guide: a running one ends (its row
|
|
1932
2197
|
// written if due), an offer goes unrecorded.
|
|
1933
|
-
if (readOf(s))
|
|
2198
|
+
if (readOf(s) || flowOf(s))
|
|
1934
2199
|
endPage(d, suite, "skipped");
|
|
1935
2200
|
else if (s.name === "pageOffer")
|
|
1936
2201
|
setScreen(d, { name: "idle" });
|
|
@@ -1958,6 +2223,14 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1958
2223
|
case "sync": {
|
|
1959
2224
|
if (s.name === "pageOffer") {
|
|
1960
2225
|
// Ignored: the person left the page (or its sub-screen). Recorded.
|
|
2226
|
+
if (s.flow) {
|
|
2227
|
+
const flow = findFlow(suite, s.page);
|
|
2228
|
+
if (!flow || !onFlowStart(flow, whereOf(env))) {
|
|
2229
|
+
recordFlow(d, suite, s.page, "skipped");
|
|
2230
|
+
setScreen(d, { name: "idle" });
|
|
2231
|
+
}
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
1961
2234
|
if (!pageStillHere(suite, s.page, env)) {
|
|
1962
2235
|
recordPage(d, suite, s.page, "skipped");
|
|
1963
2236
|
setScreen(d, { name: "idle" });
|
|
@@ -1966,6 +2239,15 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1966
2239
|
}
|
|
1967
2240
|
if (s.name !== "step")
|
|
1968
2241
|
break;
|
|
2242
|
+
if (s.flow) {
|
|
2243
|
+
// Off the flow's pages: it ends, silently. Between them it goes on
|
|
2244
|
+
// (a step on another of its pages shows "ทำต่อ").
|
|
2245
|
+
const flow = findFlow(suite, s.flow);
|
|
2246
|
+
if (!flow || !inFlowChain(flow, env)) {
|
|
2247
|
+
endPage(d, suite, "skipped");
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
1969
2251
|
const read = readOf(s);
|
|
1970
2252
|
if (read && !readStillHere(suite, read, env)) {
|
|
1971
2253
|
// Leaving the page (or the section's root going) ends its guide, silently.
|
|
@@ -2155,7 +2437,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2155
2437
|
advance(d, suite, env);
|
|
2156
2438
|
}
|
|
2157
2439
|
else if (policy === "abort") {
|
|
2158
|
-
if (readOf(s))
|
|
2440
|
+
if (readOf(s) || flowOf(s))
|
|
2159
2441
|
endPage(d, suite, "skipped");
|
|
2160
2442
|
else
|
|
2161
2443
|
exitMission(d);
|
|
@@ -2197,7 +2479,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2197
2479
|
case "exit":
|
|
2198
2480
|
if (s.name !== "step")
|
|
2199
2481
|
break;
|
|
2200
|
-
if (readOf(s)) {
|
|
2482
|
+
if (readOf(s) || flowOf(s)) {
|
|
2201
2483
|
// ✕, Escape or ปิดคู่มือ: ends silently.
|
|
2202
2484
|
pushAbort(d, suite, s);
|
|
2203
2485
|
endPage(d, suite, "skipped");
|
|
@@ -2225,6 +2507,11 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2225
2507
|
case "endWithoutSave":
|
|
2226
2508
|
if (s.name !== "step" || !s.replay)
|
|
2227
2509
|
break;
|
|
2510
|
+
if (flowOf(s)) {
|
|
2511
|
+
pushAbort(d, suite, s);
|
|
2512
|
+
endPage(d, suite, "skipped");
|
|
2513
|
+
break;
|
|
2514
|
+
}
|
|
2228
2515
|
pushAbort(d, suite, s);
|
|
2229
2516
|
setScreen(d, { name: "idle" });
|
|
2230
2517
|
d.effects.push({ type: "toast", text: COPY.replayEnded });
|
|
@@ -2273,7 +2560,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2273
2560
|
break;
|
|
2274
2561
|
case "openPanel": {
|
|
2275
2562
|
const section = action.section ?? "all";
|
|
2276
|
-
const allowed = section
|
|
2563
|
+
const allowed = section !== "all" ? state.pageAudience !== "none" : state.audience !== "none" || state.pageAudience !== "none";
|
|
2277
2564
|
if (!allowed || (s.name !== "idle" && s.name !== "pageOffer"))
|
|
2278
2565
|
break;
|
|
2279
2566
|
// The offer gives way to the panel, unrecorded.
|
|
@@ -2326,6 +2613,13 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2326
2613
|
break;
|
|
2327
2614
|
}
|
|
2328
2615
|
case "pageOffer": {
|
|
2616
|
+
if (action.flow) {
|
|
2617
|
+
if (flowOfferDue(state, suite, env) !== action.page)
|
|
2618
|
+
break;
|
|
2619
|
+
setScreen(d, { name: "pageOffer", page: action.page, flow: true });
|
|
2620
|
+
d.effects.push({ type: "announce", text: `พาทำงาน ${findFlow(suite, action.page)?.title ?? ""}` });
|
|
2621
|
+
break;
|
|
2622
|
+
}
|
|
2329
2623
|
if (pageOfferDue(state, suite, env) !== action.page)
|
|
2330
2624
|
break;
|
|
2331
2625
|
setScreen(d, { name: "pageOffer", page: action.page });
|
|
@@ -2335,14 +2629,42 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2335
2629
|
case "pageOfferStart":
|
|
2336
2630
|
if (s.name !== "pageOffer" || state.pageAudience === "none")
|
|
2337
2631
|
break;
|
|
2338
|
-
|
|
2632
|
+
if (s.flow)
|
|
2633
|
+
startFlow(d, suite, env, s.page, true);
|
|
2634
|
+
else
|
|
2635
|
+
startPage(d, suite, env, s.page, true);
|
|
2339
2636
|
break;
|
|
2340
2637
|
case "pageOfferLater":
|
|
2341
2638
|
if (s.name !== "pageOffer")
|
|
2342
2639
|
break;
|
|
2343
|
-
|
|
2640
|
+
if (s.flow)
|
|
2641
|
+
recordFlow(d, suite, s.page, "skipped");
|
|
2642
|
+
else
|
|
2643
|
+
recordPage(d, suite, s.page, "skipped");
|
|
2344
2644
|
setScreen(d, { name: "idle" });
|
|
2345
2645
|
break;
|
|
2646
|
+
case "startFlow": {
|
|
2647
|
+
// Only on its start page; never over a mission step, a welcome, a gate
|
|
2648
|
+
// or a celebration.
|
|
2649
|
+
if (state.pageAudience === "none" || state.pagesMode === "off")
|
|
2650
|
+
break;
|
|
2651
|
+
if (s.name !== "idle" && s.name !== "pageOffer")
|
|
2652
|
+
break;
|
|
2653
|
+
if (!flowsAvailable(suite, env).includes(action.flow))
|
|
2654
|
+
break;
|
|
2655
|
+
const flow = findFlow(suite, action.flow);
|
|
2656
|
+
startFlow(d, suite, env, flow.key, !flowRecorded(state, flow));
|
|
2657
|
+
break;
|
|
2658
|
+
}
|
|
2659
|
+
case "resumeFlow": {
|
|
2660
|
+
if (state.pageAudience === "none" || state.pagesMode === "off" || s.name !== "idle")
|
|
2661
|
+
break;
|
|
2662
|
+
const flow = findFlow(suite, action.flow);
|
|
2663
|
+
if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
|
|
2664
|
+
break;
|
|
2665
|
+
startFlow(d, suite, env, flow.key, action.write && !flowRecorded(state, flow), { step: action.step, context: action.context });
|
|
2666
|
+
break;
|
|
2667
|
+
}
|
|
2346
2668
|
case "startPage": {
|
|
2347
2669
|
// Never over a mission step, a welcome, a gate or a celebration.
|
|
2348
2670
|
if (state.pageAudience === "none" || state.pagesMode === "off")
|
|
@@ -2371,6 +2693,13 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2371
2693
|
break;
|
|
2372
2694
|
}
|
|
2373
2695
|
case "pageRow": {
|
|
2696
|
+
if (action.kind === "flow") {
|
|
2697
|
+
const cur = d.state.flows?.[action.key];
|
|
2698
|
+
if (cur && cur.version > action.row.version)
|
|
2699
|
+
break;
|
|
2700
|
+
d.state = { ...d.state, flows: { ...d.state.flows, [action.key]: action.row } };
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2374
2703
|
if (action.kind === "section") {
|
|
2375
2704
|
const cur = d.state.sections?.[action.key];
|
|
2376
2705
|
if (cur && cur.version > action.row.version)
|
|
@@ -2526,6 +2855,7 @@ function parseGuidesAnswer(payload, suite) {
|
|
|
2526
2855
|
: "none";
|
|
2527
2856
|
const pages = parsePageRows(rows, suite.app);
|
|
2528
2857
|
const sections = parsePageRows(rows, suite.app, "section");
|
|
2858
|
+
const flows = parsePageRows(rows, suite.app, "flow");
|
|
2529
2859
|
let row = null;
|
|
2530
2860
|
for (const r of rows) {
|
|
2531
2861
|
if (isRecord(r) && r.guide_key === suite.key) {
|
|
@@ -2546,9 +2876,9 @@ function parseGuidesAnswer(payload, suite) {
|
|
|
2546
2876
|
progress = row;
|
|
2547
2877
|
newerOnServer = row.guide_version > suite.version;
|
|
2548
2878
|
}
|
|
2549
|
-
return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages, sections };
|
|
2879
|
+
return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages, sections, flows };
|
|
2550
2880
|
}
|
|
2551
|
-
/** The row key of a page guide
|
|
2881
|
+
/** The row key of a page guide `<app>.page.<key>`, a section guide `<app>.section.<key>`, or a flow `<app>.flow.<key>`. */
|
|
2552
2882
|
function pageRowKey(app, key, kind = "page") {
|
|
2553
2883
|
return `${app}.${kind}.${key}`;
|
|
2554
2884
|
}
|
|
@@ -3730,7 +4060,10 @@ function CompleteModal(p) {
|
|
|
3730
4060
|
* corner, not modal (the page stays usable; focus is not taken). Shell ≥ 0.1.74.
|
|
3731
4061
|
*/
|
|
3732
4062
|
function PageOfferCard(p) {
|
|
3733
|
-
return (jsxs("section", { className: "eg-card eg-offer", role: "region", "aria-labelledby": "eg-po-t", "data-eg-offer": "", children: [jsx("p", { className: "eg-po-e", children: "
|
|
4063
|
+
return (jsxs("section", { className: "eg-card eg-offer", role: "region", "aria-labelledby": "eg-po-t", "data-eg-offer": "", children: [jsx("p", { className: "eg-po-e", children: p.eyebrow ?? "คู่มือหน้านี้" }), jsx("p", { className: "eg-b-t", id: "eg-po-t", children: p.title }), jsxs("p", { className: "eg-b-w", children: [p.steps, " \u0E02\u0E31\u0E49\u0E19"] }), jsxs("div", { className: "eg-row", children: [jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onLater, children: "\u0E44\u0E21\u0E48\u0E15\u0E2D\u0E19\u0E19\u0E35\u0E49" }), jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onStart, children: "\u0E40\u0E23\u0E34\u0E48\u0E21" })] })] }));
|
|
4064
|
+
}
|
|
4065
|
+
function FlowList({ items, onOpen }) {
|
|
4066
|
+
return (jsx("ul", { className: "eg-q-list eg-pg-list", children: items.map(it => (jsxs("li", { className: "eg-q-item eg-pg-item", children: [jsxs("div", { className: "eg-t", children: [jsx("b", { children: it.title }), jsx("small", { children: it.done ? "ทำแล้ว" : it.minutes ? `ประมาณ ${it.minutes} นาที` : "ใหม่" })] }), jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onOpen(it.key), children: it.done ? "ทำอีกครั้ง" : "เริ่ม" })] }, it.key))) }));
|
|
3734
4067
|
}
|
|
3735
4068
|
function PageList({ items, onOpen }) {
|
|
3736
4069
|
return (jsx("ul", { className: "eg-q-list eg-pg-list", children: items.map(it => (jsxs("li", { className: "eg-q-item eg-pg-item", children: [jsxs("div", { className: "eg-t", children: [jsx("b", { children: it.title }), jsx("small", { className: it.seen ? undefined : "eg-new", children: it.seen ? "ดูแล้ว" : "ใหม่" })] }), jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onOpen(it.key), children: "\u0E40\u0E1B\u0E34\u0E14" })] }, it.key))) }));
|
|
@@ -3757,12 +4090,16 @@ function QuestChip({ done, total, pulse, onOpen }) {
|
|
|
3757
4090
|
}
|
|
3758
4091
|
function QuestPanel(p) {
|
|
3759
4092
|
const ref = useAutoFocus();
|
|
3760
|
-
const pages = p.pages ?? [];
|
|
4093
|
+
const pages = p.only === "flows" ? [] : (p.pages ?? []);
|
|
4094
|
+
const flows = p.only === "pages" ? [] : (p.flows ?? []);
|
|
3761
4095
|
const openPage = p.onOpenPage ?? (() => { });
|
|
4096
|
+
const openFlow = p.onOpenFlow ?? (() => { });
|
|
4097
|
+
const flowSection = flows.length ? (jsxs(Fragment, { children: [jsx("h3", { className: "eg-q-sub", id: "eg-q-flows", children: "\u0E1E\u0E32\u0E17\u0E33\u0E07\u0E32\u0E19" }), jsx(FlowList, { items: flows, onOpen: openFlow })] })) : null;
|
|
3762
4098
|
if (p.showMissions === false) {
|
|
3763
|
-
|
|
4099
|
+
const heading = pages.length ? "คู่มือแต่ละหน้า" : "พาทำงาน";
|
|
4100
|
+
return (jsxs(Fragment, { children: [jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxs("div", { className: "eg-q-h", children: [jsx("h2", { id: "eg-q-t", children: heading }), jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsx(IconX, {}) })] }), pages.length ? jsx(PageList, { items: pages, onOpen: openPage }) : null, pages.length ? flowSection : flows.length ? jsx(FlowList, { items: flows, onOpen: openFlow }) : null] })] }));
|
|
3764
4101
|
}
|
|
3765
|
-
return (jsxs(Fragment, { children: [jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxs("div", { className: "eg-q-h", children: [jsx("h2", { id: "eg-q-t", children: "\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19" }), jsxs("span", { className: "eg-n", children: [p.done, "/", p.total] }), jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsx(IconX, {}) })] }), jsx("div", { className: "eg-prog", children: jsx("div", { className: "eg-bar", children: jsx("i", { style: { width: `${p.total ? (p.done / p.total) * 100 : 0}%` } }) }) }), jsx("ol", { className: "eg-q-list", children: p.items.map((it, i) => (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 ? (jsxs(Fragment, { children: [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" }), jsx(PageList, { items: pages, onOpen: openPage })] })) : null, p.canHideChip ? (jsx("div", { className: "eg-q-f", children: 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] })] }));
|
|
4102
|
+
return (jsxs(Fragment, { children: [jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxs("div", { className: "eg-q-h", children: [jsx("h2", { id: "eg-q-t", children: "\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19" }), jsxs("span", { className: "eg-n", children: [p.done, "/", p.total] }), jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsx(IconX, {}) })] }), jsx("div", { className: "eg-prog", children: jsx("div", { className: "eg-bar", children: jsx("i", { style: { width: `${p.total ? (p.done / p.total) * 100 : 0}%` } }) }) }), jsx("ol", { className: "eg-q-list", children: p.items.map((it, i) => (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 ? (jsxs(Fragment, { children: [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" }), jsx(PageList, { items: pages, onOpen: openPage })] })) : null, flowSection, p.canHideChip ? (jsx("div", { className: "eg-q-f", children: 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] })] }));
|
|
3766
4103
|
}
|
|
3767
4104
|
function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
3768
4105
|
const v = item.view;
|
|
@@ -3900,8 +4237,7 @@ function writeMirror(orgId, suite, p) {
|
|
|
3900
4237
|
/* private mode: the server copy is enough */
|
|
3901
4238
|
}
|
|
3902
4239
|
}
|
|
3903
|
-
|
|
3904
|
-
const pagesMirrorKey = (orgId, suite, kind = "page") => `erp-guide-${kind === "section" ? "sections" : "pages"}:${orgId}:${suite.app}`;
|
|
4240
|
+
const pagesMirrorKey = (orgId, suite, kind = "page") => `erp-guide-${kind}s:${orgId}:${suite.app}`;
|
|
3905
4241
|
function readPagesMirror(orgId, suite, kind = "page") {
|
|
3906
4242
|
try {
|
|
3907
4243
|
const raw = sessionStorage.getItem(pagesMirrorKey(orgId, suite, kind));
|
|
@@ -3935,6 +4271,8 @@ function mergePageRows(server, local) {
|
|
|
3935
4271
|
}
|
|
3936
4272
|
return out;
|
|
3937
4273
|
}
|
|
4274
|
+
/** A flow in progress in this tab (resumed after a refresh, never across sessions). */
|
|
4275
|
+
const flowMirrorKey = (orgId, suite) => `erp-guide-flow-run:${orgId}:${suite.app}`;
|
|
3938
4276
|
/* ------------------------------------------------------------------ */
|
|
3939
4277
|
/* One tab at a time */
|
|
3940
4278
|
/* ------------------------------------------------------------------ */
|
|
@@ -4047,7 +4385,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4047
4385
|
const [answer, setAnswer] = useState(null);
|
|
4048
4386
|
const missionsReady = config.ready !== false;
|
|
4049
4387
|
const pagesReady = config.pagesReady ?? missionsReady;
|
|
4050
|
-
const hasPages = useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0, [suite]);
|
|
4388
|
+
const hasPages = useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0 || validFlows(suite).length > 0, [suite]);
|
|
4389
|
+
const hasFlows = useMemo(() => validFlows(suite).length > 0, [suite]);
|
|
4051
4390
|
const hasSections = useMemo(() => validSections(suite).length > 0, [suite]);
|
|
4052
4391
|
const enabled = primary && (missionsReady || (hasPages && pagesReady));
|
|
4053
4392
|
const loaded = useRef(false);
|
|
@@ -4105,6 +4444,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4105
4444
|
pagesMode: answer.pagesMode,
|
|
4106
4445
|
pages: mergePageRows(answer.pages, readPagesMirror(organizationId, suite)),
|
|
4107
4446
|
sections: mergePageRows(answer.sections ?? {}, readPagesMirror(organizationId, suite, "section")),
|
|
4447
|
+
flows: mergePageRows(answer.flows ?? {}, readPagesMirror(organizationId, suite, "flow")),
|
|
4108
4448
|
});
|
|
4109
4449
|
stateRef.current = s;
|
|
4110
4450
|
setState(s);
|
|
@@ -4213,7 +4553,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4213
4553
|
else if (e.type === "savePage") {
|
|
4214
4554
|
// Once per page and version; the machine already holds it as written.
|
|
4215
4555
|
const kind = e.kind ?? "page";
|
|
4216
|
-
const
|
|
4556
|
+
const cur = stateRef.current;
|
|
4557
|
+
const rows = cur ? (kind === "section" ? cur.sections : kind === "flow" ? cur.flows : cur.pages) : undefined;
|
|
4217
4558
|
if (rows)
|
|
4218
4559
|
writePagesMirror(organizationId, suite, rows, kind);
|
|
4219
4560
|
const rev = rows?.[e.key]?.rev ?? 0;
|
|
@@ -4310,7 +4651,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4310
4651
|
/* ---------------- sync on route, sidebar, facts, DOM ---------------- */
|
|
4311
4652
|
// A page guide's step runs while page guides are live; a mission's while
|
|
4312
4653
|
// the missions are. The offer card is watched the same way (left: ignored).
|
|
4313
|
-
const inStep = screen.name === "step" && (screen.page || screen.section ? pagesLive : missionsLive);
|
|
4654
|
+
const inStep = screen.name === "step" && (screen.page || screen.section || screen.flow ? pagesLive : missionsLive);
|
|
4314
4655
|
const offerUp = pagesLive && screen.name === "pageOffer";
|
|
4315
4656
|
const watching = inStep || offerUp;
|
|
4316
4657
|
useEffect(() => {
|
|
@@ -4924,6 +5265,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4924
5265
|
openingSection.current = null;
|
|
4925
5266
|
}
|
|
4926
5267
|
}, []);
|
|
5268
|
+
const flowsNow = useMemo(() => pagesLive && hasState && hasFlows
|
|
5269
|
+
? flowsAvailable(suite, env()).map(k => ({ key: k, title: findFlow(suite, k)?.title ?? "" }))
|
|
5270
|
+
: [],
|
|
5271
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5272
|
+
[pagesLive, hasState, hasFlows, suite, env, pathname, search, factsKey]);
|
|
5273
|
+
const flowsSig = flowsNow.map(x => x.key).join(",");
|
|
5274
|
+
/** The flows the panel lists: this person's, startable here or with somewhere to go. */
|
|
5275
|
+
const flowsListed = 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]);
|
|
5276
|
+
const openFlowHere = useCallback((key) => dispatchRef.current({ type: "startFlow", flow: key }), []);
|
|
4927
5277
|
const openPageHere = useCallback(() => {
|
|
4928
5278
|
const p = pageHere(suite, env());
|
|
4929
5279
|
if (p)
|
|
@@ -4934,10 +5284,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4934
5284
|
publishPageGuides(null, null, null);
|
|
4935
5285
|
return;
|
|
4936
5286
|
}
|
|
4937
|
-
publishPageGuides({
|
|
4938
|
-
|
|
5287
|
+
publishPageGuides({
|
|
5288
|
+
page: pageNow ? { title: pageNow.title } : null,
|
|
5289
|
+
listed: listed.length,
|
|
5290
|
+
sections: sectionsNow,
|
|
5291
|
+
flows: flowsNow,
|
|
5292
|
+
flowsListed: flowsListed.length,
|
|
5293
|
+
}, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }), openSection, openFlowHere, () => dispatchRef.current({ type: "openPanel", section: "flows" }));
|
|
5294
|
+
// sectionsSig / flowsSig stand for sectionsNow / flowsNow.
|
|
4939
5295
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4940
|
-
}, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection]);
|
|
5296
|
+
}, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere]);
|
|
4941
5297
|
useEffect(() => () => publishPageGuides(null, null, null), []);
|
|
4942
5298
|
// The first-visit offer: in "offer" mode, nothing else up, the welcome
|
|
4943
5299
|
// not due here, and a moment of nothing loading on this page.
|
|
@@ -4965,14 +5321,21 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4965
5321
|
if (Date.now() - idleSince < PAGE_OFFER_IDLE_MS)
|
|
4966
5322
|
return;
|
|
4967
5323
|
let key = null;
|
|
5324
|
+
let flow = null;
|
|
4968
5325
|
try {
|
|
4969
5326
|
key = pageOfferDue(cur, suite, env());
|
|
5327
|
+
// A flow's offer comes after the page guide's (and only for `offer: true` flows).
|
|
5328
|
+
if (!key)
|
|
5329
|
+
flow = flowOfferDue(cur, suite, env());
|
|
4970
5330
|
}
|
|
4971
5331
|
catch {
|
|
4972
5332
|
key = null;
|
|
5333
|
+
flow = null;
|
|
4973
5334
|
}
|
|
4974
5335
|
if (key)
|
|
4975
5336
|
dispatchRef.current({ type: "pageOffer", page: key });
|
|
5337
|
+
else if (flow)
|
|
5338
|
+
dispatchRef.current({ type: "pageOffer", page: flow, flow: true });
|
|
4976
5339
|
}, PAGE_POLL_MS);
|
|
4977
5340
|
return () => clearInterval(iv);
|
|
4978
5341
|
}, [offerWatch, suite, env, pathname, search]);
|
|
@@ -5024,6 +5387,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5024
5387
|
if (b)
|
|
5025
5388
|
return;
|
|
5026
5389
|
const e = env();
|
|
5390
|
+
if (pending.flow) {
|
|
5391
|
+
const flow = findFlow(suite, pending.key);
|
|
5392
|
+
if (!flow || !onFlowStart(flow, e))
|
|
5393
|
+
return;
|
|
5394
|
+
setPending(null);
|
|
5395
|
+
dispatchRef.current({ type: "startFlow", flow: pending.key });
|
|
5396
|
+
return;
|
|
5397
|
+
}
|
|
5027
5398
|
const page = findPage(suite, pending.key);
|
|
5028
5399
|
if (!page || !pageMatchScore(page, { code, pathname: e.pathname, search: e.search, isVisible: a => e.isVisible(a) }))
|
|
5029
5400
|
return;
|
|
@@ -5032,6 +5403,93 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5032
5403
|
}, PAGE_POLL_MS);
|
|
5033
5404
|
return () => clearInterval(iv);
|
|
5034
5405
|
}, [pending, pagesLive, suite, env, code]);
|
|
5406
|
+
// A flow in the panel's list: start it here, or go to its start page and start it there.
|
|
5407
|
+
const openListedFlow = useCallback((key) => {
|
|
5408
|
+
const flow = findFlow(suite, key);
|
|
5409
|
+
if (!flow)
|
|
5410
|
+
return;
|
|
5411
|
+
const e = env();
|
|
5412
|
+
if (onFlowStart(flow, e)) {
|
|
5413
|
+
dispatchRef.current({ type: "startFlow", flow: key });
|
|
5414
|
+
return;
|
|
5415
|
+
}
|
|
5416
|
+
dispatchRef.current({ type: "closePanel" });
|
|
5417
|
+
const href = flowHref(flow, guideContext(e, {}));
|
|
5418
|
+
if (!href)
|
|
5419
|
+
return;
|
|
5420
|
+
setPending({ key, until: Date.now() + PAGE_ARRIVE_MS, flow: true });
|
|
5421
|
+
try {
|
|
5422
|
+
if (/^https?:\/\//i.test(href))
|
|
5423
|
+
window.location.assign(href);
|
|
5424
|
+
else
|
|
5425
|
+
navigateRef.current(href);
|
|
5426
|
+
}
|
|
5427
|
+
catch {
|
|
5428
|
+
/* never breaks the app */
|
|
5429
|
+
}
|
|
5430
|
+
}, [suite, env]);
|
|
5431
|
+
// A flow in progress is kept in this tab's session (sessionStorage), so a
|
|
5432
|
+
// refresh or a full page load on one of its pages picks it up again at
|
|
5433
|
+
// its checkpoint ("ทำต่อ" when its step is on another page). Never across
|
|
5434
|
+
// sessions; a flow that ends is forgotten.
|
|
5435
|
+
const flowRun = state?.pageRun?.kind === "flow" ? state.pageRun : null;
|
|
5436
|
+
const flowStepKey = screen.name === "step" && screen.flow ? (findMission(suite, screen.mission)?.steps[screen.index]?.key ?? null) : null;
|
|
5437
|
+
useEffect(() => {
|
|
5438
|
+
if (!hasFlows)
|
|
5439
|
+
return;
|
|
5440
|
+
const k = flowMirrorKey(organizationId, suite);
|
|
5441
|
+
try {
|
|
5442
|
+
if (!flowRun) {
|
|
5443
|
+
if (state)
|
|
5444
|
+
sessionStorage.removeItem(k);
|
|
5445
|
+
return;
|
|
5446
|
+
}
|
|
5447
|
+
if (!flowStepKey)
|
|
5448
|
+
return;
|
|
5449
|
+
const flow = findFlow(suite, flowRun.key);
|
|
5450
|
+
sessionStorage.setItem(k, JSON.stringify({ flow: flowRun.key, version: flow?.version, step: flowStepKey, write: flowRun.write, context: state?.progress.context ?? {} }));
|
|
5451
|
+
}
|
|
5452
|
+
catch {
|
|
5453
|
+
/* private mode: no resume after a refresh */
|
|
5454
|
+
}
|
|
5455
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5456
|
+
}, [hasFlows, flowRun?.key, flowRun?.write, flowStepKey, organizationId, suite]);
|
|
5457
|
+
const flowResumed = useRef(false);
|
|
5458
|
+
useEffect(() => {
|
|
5459
|
+
if (flowResumed.current || !pagesLive || !state || !hasFlows)
|
|
5460
|
+
return;
|
|
5461
|
+
// The missions resume first.
|
|
5462
|
+
if (missionsLive && !resumed.current)
|
|
5463
|
+
return;
|
|
5464
|
+
flowResumed.current = true;
|
|
5465
|
+
const k = flowMirrorKey(organizationId, suite);
|
|
5466
|
+
let saved = null;
|
|
5467
|
+
try {
|
|
5468
|
+
saved = JSON.parse(sessionStorage.getItem(k) ?? "null");
|
|
5469
|
+
}
|
|
5470
|
+
catch {
|
|
5471
|
+
saved = null;
|
|
5472
|
+
}
|
|
5473
|
+
if (!saved?.flow || !saved.step)
|
|
5474
|
+
return;
|
|
5475
|
+
const flow = findFlow(suite, saved.flow);
|
|
5476
|
+
const ok = !!flow && flow.version === saved.version && stateRef.current?.screen.name === "idle" && inFlowChain(flow, { code, pathname });
|
|
5477
|
+
if (!ok) {
|
|
5478
|
+
try {
|
|
5479
|
+
sessionStorage.removeItem(k);
|
|
5480
|
+
}
|
|
5481
|
+
catch {
|
|
5482
|
+
/* ignore */
|
|
5483
|
+
}
|
|
5484
|
+
return;
|
|
5485
|
+
}
|
|
5486
|
+
const context = {};
|
|
5487
|
+
for (const [ck, cv] of Object.entries(saved.context ?? {}))
|
|
5488
|
+
if (typeof cv === "string")
|
|
5489
|
+
context[ck] = cv;
|
|
5490
|
+
dispatchRef.current({ type: "resumeFlow", flow: saved.flow, step: saved.step, context, write: saved.write === true });
|
|
5491
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5492
|
+
}, [pagesLive, !!state, hasFlows, missionsLive]);
|
|
5035
5493
|
if (!live || !state || !layer || !ctxIdle)
|
|
5036
5494
|
return null;
|
|
5037
5495
|
/* ---------------- render ---------------- */
|
|
@@ -5057,6 +5515,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5057
5515
|
const pos = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
|
|
5058
5516
|
const page = screen.page ? findPage(suite, screen.page) : undefined;
|
|
5059
5517
|
const section = screen.section ? findSection(suite, screen.section) : undefined;
|
|
5518
|
+
const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
|
|
5060
5519
|
const read = page ? { heading: `คู่มือหน้านี้ · ${page.title}`, exitLabel: "ปิดคู่มือ" } : section ? { heading: `อธิบายส่วนนี้ · ${section.title}`, exitLabel: "ปิดคำอธิบาย" } : null;
|
|
5061
5520
|
const pageBits = read
|
|
5062
5521
|
? {
|
|
@@ -5064,7 +5523,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5064
5523
|
showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
|
|
5065
5524
|
nextLabel: pos.no >= pos.total ? "เสร็จ" : undefined,
|
|
5066
5525
|
}
|
|
5067
|
-
:
|
|
5526
|
+
: flow
|
|
5527
|
+
? {
|
|
5528
|
+
heading: `พาทำ · ${flow.title}`,
|
|
5529
|
+
exitLabel: "ออกจากการพาทำ",
|
|
5530
|
+
nextLabel: kind === "view" && pos.no >= pos.total ? "เสร็จ" : undefined,
|
|
5531
|
+
}
|
|
5532
|
+
: {};
|
|
5068
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: {
|
|
5069
5534
|
missionNo,
|
|
5070
5535
|
missionTitle: mission.title,
|
|
@@ -5081,6 +5546,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5081
5546
|
...pageBits,
|
|
5082
5547
|
}, 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" }) }));
|
|
5083
5548
|
}
|
|
5549
|
+
else if (screen.name === "pageOffer" && pagesLive && screen.flow) {
|
|
5550
|
+
const flow = findFlow(suite, screen.page);
|
|
5551
|
+
const mission = flow ? findMission(suite, FLOW_PREFIX + flow.key) : undefined;
|
|
5552
|
+
if (flow && mission) {
|
|
5553
|
+
content = (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" }) }));
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5084
5556
|
else if (screen.name === "pageOffer" && pagesLive) {
|
|
5085
5557
|
const page = findPage(suite, screen.page);
|
|
5086
5558
|
if (page) {
|
|
@@ -5124,12 +5596,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5124
5596
|
const showChip = missionsLive && chipVisible(state, suite, ctxIdle);
|
|
5125
5597
|
// The panel: the missions (when this person has them, and not "คู่มือทุกหน้า"),
|
|
5126
5598
|
// then the page guides they have.
|
|
5127
|
-
const panelMissions = missionsLive && total > 0 && state.panelSection
|
|
5128
|
-
const pageItems = pagesLive
|
|
5129
|
-
|
|
5599
|
+
const panelMissions = missionsLive && total > 0 && state.panelSection === "all";
|
|
5600
|
+
const pageItems = pagesLive && state.panelSection !== "flows" ? listed.map(p => ({ key: p.key, title: p.title, seen: pageRecorded(state, p) })) : [];
|
|
5601
|
+
const flowItems = pagesLive && state.panelSection !== "pages"
|
|
5602
|
+
? flowsListed.map(f => ({ key: f.key, title: f.title, minutes: f.minutes, done: flowRecorded(state, f) && state.flows[f.key]?.status === "completed" }))
|
|
5130
5603
|
: [];
|
|
5131
|
-
const panelShown = state.panelOpen && screen.name === "idle" && (panelMissions || pageItems.length > 0);
|
|
5132
|
-
return createPortal(jsxs(Fragment, { children: [jsx("style", { children: GUIDE_CSS }), jsx("div", { className: "eg-sr", "aria-live": "polite", role: "status", children: liveText }), content, showChip ? jsx(QuestChip, { done: done, total: total, pulse: state.chipPulse, onOpen: () => dispatch({ type: "openPanel" }) }) : null, panelShown ? (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 ? jsx(GuideToast, { text: toast }) : null] }), layer);
|
|
5604
|
+
const panelShown = state.panelOpen && screen.name === "idle" && (panelMissions || pageItems.length > 0 || flowItems.length > 0);
|
|
5605
|
+
return createPortal(jsxs(Fragment, { children: [jsx("style", { children: GUIDE_CSS }), jsx("div", { className: "eg-sr", "aria-live": "polite", role: "status", children: liveText }), content, showChip ? jsx(QuestChip, { done: done, total: total, pulse: state.chipPulse, onOpen: () => dispatch({ type: "openPanel" }) }) : null, panelShown ? (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 ? jsx(GuideToast, { text: toast }) : null] }), layer);
|
|
5133
5606
|
}
|
|
5134
5607
|
/** A stable key for the facts object, so effects re-run when they change. */
|
|
5135
5608
|
function useFactsKey(facts) {
|
|
@@ -5239,5 +5712,5 @@ function thaiMonthYear(m, opts = {}) {
|
|
|
5239
5712
|
return `${name} ${opts.year === "full" ? be : String(be).slice(-2)}`;
|
|
5240
5713
|
}
|
|
5241
5714
|
|
|
5242
|
-
export { API_EVENT as A,
|
|
5243
|
-
//# sourceMappingURL=format-
|
|
5715
|
+
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-BWUKNKxg.esm.js.map
|