@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
|
@@ -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,6 +1008,7 @@ 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,
|
|
@@ -875,6 +1035,10 @@ function findMission(suite, key) {
|
|
|
875
1035
|
const section = findSection(suite, sectionKeyOf(key));
|
|
876
1036
|
return section ? sectionMission(section) : undefined;
|
|
877
1037
|
}
|
|
1038
|
+
if (isFlowMissionKey(key)) {
|
|
1039
|
+
const flow = findFlow(suite, flowKeyOf(key));
|
|
1040
|
+
return flow ? flowMission(flow) : undefined;
|
|
1041
|
+
}
|
|
878
1042
|
return suite.missions.find(m => m.key === key);
|
|
879
1043
|
}
|
|
880
1044
|
/**
|
|
@@ -1323,6 +1487,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1323
1487
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
1324
1488
|
const page = pageKeyOf(mission.key);
|
|
1325
1489
|
const section = sectionKeyOf(mission.key);
|
|
1490
|
+
const flow = flowKeyOf(mission.key);
|
|
1326
1491
|
const where = placeRoute(steps, i);
|
|
1327
1492
|
// A page or section guide is on its page: leaving it ends the guide (never "ทำต่อ").
|
|
1328
1493
|
const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
|
|
@@ -1344,6 +1509,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1344
1509
|
replayNext: !away && !bridge && opts.replay && replayCanGoOn(mission, i, env, d.state.progress.context, nts),
|
|
1345
1510
|
...(page ? { page } : {}),
|
|
1346
1511
|
...(section ? { section } : {}),
|
|
1512
|
+
...(flow ? { flow } : {}),
|
|
1347
1513
|
});
|
|
1348
1514
|
if (!opts.replay) {
|
|
1349
1515
|
const cp = checkpointAtOrBefore(steps, i);
|
|
@@ -1508,9 +1674,12 @@ function advance(d, suite, env, capture) {
|
|
|
1508
1674
|
enterStep(d, suite, env, mission, s.index + 1, { replay: s.replay });
|
|
1509
1675
|
}
|
|
1510
1676
|
function finishMission(d, suite, env, mission, replay) {
|
|
1511
|
-
if (isPageMissionKey(mission.key) || isSectionMissionKey(mission.key)) {
|
|
1677
|
+
if (isPageMissionKey(mission.key) || isSectionMissionKey(mission.key) || isFlowMissionKey(mission.key)) {
|
|
1512
1678
|
endPage(d, suite, "completed");
|
|
1513
|
-
d.effects.push({
|
|
1679
|
+
d.effects.push({
|
|
1680
|
+
type: "toast",
|
|
1681
|
+
text: isSectionMissionKey(mission.key) ? COPY.sectionDone : isFlowMissionKey(mission.key) ? COPY.flowDone : COPY.pageDone,
|
|
1682
|
+
});
|
|
1514
1683
|
return;
|
|
1515
1684
|
}
|
|
1516
1685
|
if (replay) {
|
|
@@ -1805,18 +1974,114 @@ function readStillHere(suite, r, env) {
|
|
|
1805
1974
|
const section = findSection(suite, r.key);
|
|
1806
1975
|
return !!section && sectionApplies(section, whereOf(env));
|
|
1807
1976
|
}
|
|
1808
|
-
/** The
|
|
1977
|
+
/** The flow's row says this person has ended this version of it. */
|
|
1978
|
+
function flowRecorded(state, flow) {
|
|
1979
|
+
const row = state.flows?.[flow.key];
|
|
1980
|
+
return !!row && row.version >= flow.version;
|
|
1981
|
+
}
|
|
1982
|
+
/** Write a flow's row (once per flow and version). */
|
|
1983
|
+
function recordFlow(d, suite, key, status) {
|
|
1984
|
+
const flow = findFlow(suite, key);
|
|
1985
|
+
if (!flow || flowRecorded(d.state, flow))
|
|
1986
|
+
return;
|
|
1987
|
+
const prev = d.state.flows?.[key];
|
|
1988
|
+
d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
|
|
1989
|
+
d.effects.push({ type: "savePage", key, version: flow.version, status, kind: "flow" });
|
|
1990
|
+
}
|
|
1991
|
+
/** The page guide, section guide or flow ends: its row written when it is due one; back to idle. */
|
|
1809
1992
|
function endPage(d, suite, how) {
|
|
1810
1993
|
const run = d.state.pageRun;
|
|
1811
1994
|
if (run?.write) {
|
|
1812
1995
|
if (run.kind === "section")
|
|
1813
1996
|
recordSection(d, suite, run.key, how);
|
|
1997
|
+
else if (run.kind === "flow")
|
|
1998
|
+
recordFlow(d, suite, run.key, how);
|
|
1814
1999
|
else
|
|
1815
2000
|
recordPage(d, suite, run.key, how);
|
|
1816
2001
|
}
|
|
2002
|
+
// A flow's captured ids were its own: the tour's come back.
|
|
2003
|
+
if (run?.kind === "flow" && run.context)
|
|
2004
|
+
withProgress(d, { context: run.context });
|
|
1817
2005
|
d.state = { ...d.state, pageRun: null, replayPage: null };
|
|
1818
2006
|
setScreen(d, { name: "idle" });
|
|
1819
2007
|
}
|
|
2008
|
+
/** The flow on screen, or null. */
|
|
2009
|
+
function flowOf(s) {
|
|
2010
|
+
return s.name === "step" && s.flow ? s.flow : null;
|
|
2011
|
+
}
|
|
2012
|
+
/** The flows this person has that start on this page ("พาทำ · <title>"). */
|
|
2013
|
+
function flowsAvailable(suite, env) {
|
|
2014
|
+
if (!suite.flows?.length)
|
|
2015
|
+
return [];
|
|
2016
|
+
const ctx = guideContext(env, {});
|
|
2017
|
+
const where = whereOf(env);
|
|
2018
|
+
return (suite.flows ?? [])
|
|
2019
|
+
.map(f => findFlow(suite, f?.key))
|
|
2020
|
+
.filter((f) => !!f && onFlowStart(f, where) && flowShown(f, ctx))
|
|
2021
|
+
.map(f => f.key);
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* The flow to offer now, or null: only in "offer" mode, only a flow that
|
|
2025
|
+
* says `offer: true`, on its start page, with no row of this version, when
|
|
2026
|
+
* nothing else is up and this page's own guide is not due (it comes first:
|
|
2027
|
+
* no guide here, offer: false, or already recorded), its first step's
|
|
2028
|
+
* target on screen, no app dialog open. The host adds the welcome and the
|
|
2029
|
+
* idle wait, as for the page guide's offer.
|
|
2030
|
+
*/
|
|
2031
|
+
function flowOfferDue(state, suite, env) {
|
|
2032
|
+
if (state.pagesMode !== "offer" || state.pageAudience === "none")
|
|
2033
|
+
return null;
|
|
2034
|
+
if (state.screen.name !== "idle" || state.panelOpen || (state.pendingCelebrate?.length ?? 0) > 0)
|
|
2035
|
+
return null;
|
|
2036
|
+
const page = pageHere(suite, env);
|
|
2037
|
+
if (page && page.offer !== false && !pageRecorded(state, page))
|
|
2038
|
+
return null;
|
|
2039
|
+
if (safe(() => env.dialogOpen?.() === true, false))
|
|
2040
|
+
return null;
|
|
2041
|
+
for (const key of flowsAvailable(suite, env)) {
|
|
2042
|
+
const flow = findFlow(suite, key);
|
|
2043
|
+
if (flow.offer !== true || flowRecorded(state, flow))
|
|
2044
|
+
continue;
|
|
2045
|
+
const mission = flowMission(flow);
|
|
2046
|
+
const first = mission.steps.find(st => {
|
|
2047
|
+
const ctx = guideContext(env, {});
|
|
2048
|
+
if (st.requires && !safe(() => env.can(st.requires), true))
|
|
2049
|
+
return false;
|
|
2050
|
+
return !(st.skipIf && safe(() => st.skipIf(ctx), false));
|
|
2051
|
+
});
|
|
2052
|
+
if (first && !stepTargetVisible(first, env, guideContext(env, {}), false))
|
|
2053
|
+
continue;
|
|
2054
|
+
return key;
|
|
2055
|
+
}
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
function startFlow(d, suite, env, key, write, at) {
|
|
2059
|
+
const flow = findFlow(suite, key);
|
|
2060
|
+
if (!flow)
|
|
2061
|
+
return;
|
|
2062
|
+
const mission = flowMission(flow);
|
|
2063
|
+
const saved = d.state.progress.context;
|
|
2064
|
+
d.state = {
|
|
2065
|
+
...d.state,
|
|
2066
|
+
panelOpen: false,
|
|
2067
|
+
justDone: null,
|
|
2068
|
+
pageRun: { key, write, kind: "flow", context: saved },
|
|
2069
|
+
autoPassed: null,
|
|
2070
|
+
replayPage: {
|
|
2071
|
+
mission: mission.key,
|
|
2072
|
+
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
2073
|
+
dialog: safe(() => env.dialogOpen?.() === true, false),
|
|
2074
|
+
},
|
|
2075
|
+
};
|
|
2076
|
+
withProgress(d, { context: at?.context ?? {} });
|
|
2077
|
+
let index = 0;
|
|
2078
|
+
if (at) {
|
|
2079
|
+
const stepAt = mission.steps.findIndex(st => st.key === at.step);
|
|
2080
|
+
if (stepAt > 0)
|
|
2081
|
+
index = checkpointAtOrBefore(mission.steps, stepAt);
|
|
2082
|
+
}
|
|
2083
|
+
enterStep(d, suite, env, mission, index, { replay: true });
|
|
2084
|
+
}
|
|
1820
2085
|
/** The section guides on screen here that this person has (the "?" entries, the buttons). */
|
|
1821
2086
|
function sectionsAvailable(suite, env) {
|
|
1822
2087
|
return sectionsHere(suite, whereOf(env), guideContext(env, {})).map(x => x.key);
|
|
@@ -1950,7 +2215,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1950
2215
|
break;
|
|
1951
2216
|
// A mission always wins over a page guide: a running one ends (its row
|
|
1952
2217
|
// written if due), an offer goes unrecorded.
|
|
1953
|
-
if (readOf(s))
|
|
2218
|
+
if (readOf(s) || flowOf(s))
|
|
1954
2219
|
endPage(d, suite, "skipped");
|
|
1955
2220
|
else if (s.name === "pageOffer")
|
|
1956
2221
|
setScreen(d, { name: "idle" });
|
|
@@ -1978,6 +2243,14 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1978
2243
|
case "sync": {
|
|
1979
2244
|
if (s.name === "pageOffer") {
|
|
1980
2245
|
// Ignored: the person left the page (or its sub-screen). Recorded.
|
|
2246
|
+
if (s.flow) {
|
|
2247
|
+
const flow = findFlow(suite, s.page);
|
|
2248
|
+
if (!flow || !onFlowStart(flow, whereOf(env))) {
|
|
2249
|
+
recordFlow(d, suite, s.page, "skipped");
|
|
2250
|
+
setScreen(d, { name: "idle" });
|
|
2251
|
+
}
|
|
2252
|
+
break;
|
|
2253
|
+
}
|
|
1981
2254
|
if (!pageStillHere(suite, s.page, env)) {
|
|
1982
2255
|
recordPage(d, suite, s.page, "skipped");
|
|
1983
2256
|
setScreen(d, { name: "idle" });
|
|
@@ -1986,6 +2259,15 @@ function reduceGuide(state, action, suite, env) {
|
|
|
1986
2259
|
}
|
|
1987
2260
|
if (s.name !== "step")
|
|
1988
2261
|
break;
|
|
2262
|
+
if (s.flow) {
|
|
2263
|
+
// Off the flow's pages: it ends, silently. Between them it goes on
|
|
2264
|
+
// (a step on another of its pages shows "ทำต่อ").
|
|
2265
|
+
const flow = findFlow(suite, s.flow);
|
|
2266
|
+
if (!flow || !inFlowChain(flow, env)) {
|
|
2267
|
+
endPage(d, suite, "skipped");
|
|
2268
|
+
break;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
1989
2271
|
const read = readOf(s);
|
|
1990
2272
|
if (read && !readStillHere(suite, read, env)) {
|
|
1991
2273
|
// Leaving the page (or the section's root going) ends its guide, silently.
|
|
@@ -2175,7 +2457,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2175
2457
|
advance(d, suite, env);
|
|
2176
2458
|
}
|
|
2177
2459
|
else if (policy === "abort") {
|
|
2178
|
-
if (readOf(s))
|
|
2460
|
+
if (readOf(s) || flowOf(s))
|
|
2179
2461
|
endPage(d, suite, "skipped");
|
|
2180
2462
|
else
|
|
2181
2463
|
exitMission(d);
|
|
@@ -2217,7 +2499,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2217
2499
|
case "exit":
|
|
2218
2500
|
if (s.name !== "step")
|
|
2219
2501
|
break;
|
|
2220
|
-
if (readOf(s)) {
|
|
2502
|
+
if (readOf(s) || flowOf(s)) {
|
|
2221
2503
|
// ✕, Escape or ปิดคู่มือ: ends silently.
|
|
2222
2504
|
pushAbort(d, suite, s);
|
|
2223
2505
|
endPage(d, suite, "skipped");
|
|
@@ -2245,6 +2527,11 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2245
2527
|
case "endWithoutSave":
|
|
2246
2528
|
if (s.name !== "step" || !s.replay)
|
|
2247
2529
|
break;
|
|
2530
|
+
if (flowOf(s)) {
|
|
2531
|
+
pushAbort(d, suite, s);
|
|
2532
|
+
endPage(d, suite, "skipped");
|
|
2533
|
+
break;
|
|
2534
|
+
}
|
|
2248
2535
|
pushAbort(d, suite, s);
|
|
2249
2536
|
setScreen(d, { name: "idle" });
|
|
2250
2537
|
d.effects.push({ type: "toast", text: COPY.replayEnded });
|
|
@@ -2293,7 +2580,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2293
2580
|
break;
|
|
2294
2581
|
case "openPanel": {
|
|
2295
2582
|
const section = action.section ?? "all";
|
|
2296
|
-
const allowed = section
|
|
2583
|
+
const allowed = section !== "all" ? state.pageAudience !== "none" : state.audience !== "none" || state.pageAudience !== "none";
|
|
2297
2584
|
if (!allowed || (s.name !== "idle" && s.name !== "pageOffer"))
|
|
2298
2585
|
break;
|
|
2299
2586
|
// The offer gives way to the panel, unrecorded.
|
|
@@ -2346,6 +2633,13 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2346
2633
|
break;
|
|
2347
2634
|
}
|
|
2348
2635
|
case "pageOffer": {
|
|
2636
|
+
if (action.flow) {
|
|
2637
|
+
if (flowOfferDue(state, suite, env) !== action.page)
|
|
2638
|
+
break;
|
|
2639
|
+
setScreen(d, { name: "pageOffer", page: action.page, flow: true });
|
|
2640
|
+
d.effects.push({ type: "announce", text: `พาทำงาน ${findFlow(suite, action.page)?.title ?? ""}` });
|
|
2641
|
+
break;
|
|
2642
|
+
}
|
|
2349
2643
|
if (pageOfferDue(state, suite, env) !== action.page)
|
|
2350
2644
|
break;
|
|
2351
2645
|
setScreen(d, { name: "pageOffer", page: action.page });
|
|
@@ -2355,14 +2649,42 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2355
2649
|
case "pageOfferStart":
|
|
2356
2650
|
if (s.name !== "pageOffer" || state.pageAudience === "none")
|
|
2357
2651
|
break;
|
|
2358
|
-
|
|
2652
|
+
if (s.flow)
|
|
2653
|
+
startFlow(d, suite, env, s.page, true);
|
|
2654
|
+
else
|
|
2655
|
+
startPage(d, suite, env, s.page, true);
|
|
2359
2656
|
break;
|
|
2360
2657
|
case "pageOfferLater":
|
|
2361
2658
|
if (s.name !== "pageOffer")
|
|
2362
2659
|
break;
|
|
2363
|
-
|
|
2660
|
+
if (s.flow)
|
|
2661
|
+
recordFlow(d, suite, s.page, "skipped");
|
|
2662
|
+
else
|
|
2663
|
+
recordPage(d, suite, s.page, "skipped");
|
|
2364
2664
|
setScreen(d, { name: "idle" });
|
|
2365
2665
|
break;
|
|
2666
|
+
case "startFlow": {
|
|
2667
|
+
// Only on its start page; never over a mission step, a welcome, a gate
|
|
2668
|
+
// or a celebration.
|
|
2669
|
+
if (state.pageAudience === "none" || state.pagesMode === "off")
|
|
2670
|
+
break;
|
|
2671
|
+
if (s.name !== "idle" && s.name !== "pageOffer")
|
|
2672
|
+
break;
|
|
2673
|
+
if (!flowsAvailable(suite, env).includes(action.flow))
|
|
2674
|
+
break;
|
|
2675
|
+
const flow = findFlow(suite, action.flow);
|
|
2676
|
+
startFlow(d, suite, env, flow.key, !flowRecorded(state, flow));
|
|
2677
|
+
break;
|
|
2678
|
+
}
|
|
2679
|
+
case "resumeFlow": {
|
|
2680
|
+
if (state.pageAudience === "none" || state.pagesMode === "off" || s.name !== "idle")
|
|
2681
|
+
break;
|
|
2682
|
+
const flow = findFlow(suite, action.flow);
|
|
2683
|
+
if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
|
|
2684
|
+
break;
|
|
2685
|
+
startFlow(d, suite, env, flow.key, action.write && !flowRecorded(state, flow), { step: action.step, context: action.context });
|
|
2686
|
+
break;
|
|
2687
|
+
}
|
|
2366
2688
|
case "startPage": {
|
|
2367
2689
|
// Never over a mission step, a welcome, a gate or a celebration.
|
|
2368
2690
|
if (state.pageAudience === "none" || state.pagesMode === "off")
|
|
@@ -2391,6 +2713,13 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2391
2713
|
break;
|
|
2392
2714
|
}
|
|
2393
2715
|
case "pageRow": {
|
|
2716
|
+
if (action.kind === "flow") {
|
|
2717
|
+
const cur = d.state.flows?.[action.key];
|
|
2718
|
+
if (cur && cur.version > action.row.version)
|
|
2719
|
+
break;
|
|
2720
|
+
d.state = { ...d.state, flows: { ...d.state.flows, [action.key]: action.row } };
|
|
2721
|
+
break;
|
|
2722
|
+
}
|
|
2394
2723
|
if (action.kind === "section") {
|
|
2395
2724
|
const cur = d.state.sections?.[action.key];
|
|
2396
2725
|
if (cur && cur.version > action.row.version)
|
|
@@ -2546,6 +2875,7 @@ function parseGuidesAnswer(payload, suite) {
|
|
|
2546
2875
|
: "none";
|
|
2547
2876
|
const pages = parsePageRows(rows, suite.app);
|
|
2548
2877
|
const sections = parsePageRows(rows, suite.app, "section");
|
|
2878
|
+
const flows = parsePageRows(rows, suite.app, "flow");
|
|
2549
2879
|
let row = null;
|
|
2550
2880
|
for (const r of rows) {
|
|
2551
2881
|
if (isRecord(r) && r.guide_key === suite.key) {
|
|
@@ -2566,9 +2896,9 @@ function parseGuidesAnswer(payload, suite) {
|
|
|
2566
2896
|
progress = row;
|
|
2567
2897
|
newerOnServer = row.guide_version > suite.version;
|
|
2568
2898
|
}
|
|
2569
|
-
return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages, sections };
|
|
2899
|
+
return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages, sections, flows };
|
|
2570
2900
|
}
|
|
2571
|
-
/** The row key of a page guide
|
|
2901
|
+
/** The row key of a page guide `<app>.page.<key>`, a section guide `<app>.section.<key>`, or a flow `<app>.flow.<key>`. */
|
|
2572
2902
|
function pageRowKey(app, key, kind = "page") {
|
|
2573
2903
|
return `${app}.${kind}.${key}`;
|
|
2574
2904
|
}
|
|
@@ -3750,7 +4080,10 @@ function CompleteModal(p) {
|
|
|
3750
4080
|
* corner, not modal (the page stays usable; focus is not taken). Shell ≥ 0.1.74.
|
|
3751
4081
|
*/
|
|
3752
4082
|
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: "
|
|
4083
|
+
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" })] })] }));
|
|
4084
|
+
}
|
|
4085
|
+
function FlowList({ items, onOpen }) {
|
|
4086
|
+
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
4087
|
}
|
|
3755
4088
|
function PageList({ items, onOpen }) {
|
|
3756
4089
|
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 +4110,16 @@ function QuestChip({ done, total, pulse, onOpen }) {
|
|
|
3777
4110
|
}
|
|
3778
4111
|
function QuestPanel(p) {
|
|
3779
4112
|
const ref = useAutoFocus();
|
|
3780
|
-
const pages = p.pages ?? [];
|
|
4113
|
+
const pages = p.only === "flows" ? [] : (p.pages ?? []);
|
|
4114
|
+
const flows = p.only === "pages" ? [] : (p.flows ?? []);
|
|
3781
4115
|
const openPage = p.onOpenPage ?? (() => { });
|
|
4116
|
+
const openFlow = p.onOpenFlow ?? (() => { });
|
|
4117
|
+
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
4118
|
if (p.showMissions === false) {
|
|
3783
|
-
|
|
4119
|
+
const heading = pages.length ? "คู่มือแต่ละหน้า" : "พาทำงาน";
|
|
4120
|
+
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
4121
|
}
|
|
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] })] }));
|
|
4122
|
+
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
4123
|
}
|
|
3787
4124
|
function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
3788
4125
|
const v = item.view;
|
|
@@ -3920,8 +4257,7 @@ function writeMirror(orgId, suite, p) {
|
|
|
3920
4257
|
/* private mode: the server copy is enough */
|
|
3921
4258
|
}
|
|
3922
4259
|
}
|
|
3923
|
-
|
|
3924
|
-
const pagesMirrorKey = (orgId, suite, kind = "page") => `erp-guide-${kind === "section" ? "sections" : "pages"}:${orgId}:${suite.app}`;
|
|
4260
|
+
const pagesMirrorKey = (orgId, suite, kind = "page") => `erp-guide-${kind}s:${orgId}:${suite.app}`;
|
|
3925
4261
|
function readPagesMirror(orgId, suite, kind = "page") {
|
|
3926
4262
|
try {
|
|
3927
4263
|
const raw = sessionStorage.getItem(pagesMirrorKey(orgId, suite, kind));
|
|
@@ -3955,6 +4291,8 @@ function mergePageRows(server, local) {
|
|
|
3955
4291
|
}
|
|
3956
4292
|
return out;
|
|
3957
4293
|
}
|
|
4294
|
+
/** A flow in progress in this tab (resumed after a refresh, never across sessions). */
|
|
4295
|
+
const flowMirrorKey = (orgId, suite) => `erp-guide-flow-run:${orgId}:${suite.app}`;
|
|
3958
4296
|
/* ------------------------------------------------------------------ */
|
|
3959
4297
|
/* One tab at a time */
|
|
3960
4298
|
/* ------------------------------------------------------------------ */
|
|
@@ -4067,7 +4405,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4067
4405
|
const [answer, setAnswer] = React.useState(null);
|
|
4068
4406
|
const missionsReady = config.ready !== false;
|
|
4069
4407
|
const pagesReady = config.pagesReady ?? missionsReady;
|
|
4070
|
-
const hasPages = React.useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0, [suite]);
|
|
4408
|
+
const hasPages = React.useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0 || validFlows(suite).length > 0, [suite]);
|
|
4409
|
+
const hasFlows = React.useMemo(() => validFlows(suite).length > 0, [suite]);
|
|
4071
4410
|
const hasSections = React.useMemo(() => validSections(suite).length > 0, [suite]);
|
|
4072
4411
|
const enabled = primary && (missionsReady || (hasPages && pagesReady));
|
|
4073
4412
|
const loaded = React.useRef(false);
|
|
@@ -4125,6 +4464,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4125
4464
|
pagesMode: answer.pagesMode,
|
|
4126
4465
|
pages: mergePageRows(answer.pages, readPagesMirror(organizationId, suite)),
|
|
4127
4466
|
sections: mergePageRows(answer.sections ?? {}, readPagesMirror(organizationId, suite, "section")),
|
|
4467
|
+
flows: mergePageRows(answer.flows ?? {}, readPagesMirror(organizationId, suite, "flow")),
|
|
4128
4468
|
});
|
|
4129
4469
|
stateRef.current = s;
|
|
4130
4470
|
setState(s);
|
|
@@ -4233,7 +4573,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4233
4573
|
else if (e.type === "savePage") {
|
|
4234
4574
|
// Once per page and version; the machine already holds it as written.
|
|
4235
4575
|
const kind = e.kind ?? "page";
|
|
4236
|
-
const
|
|
4576
|
+
const cur = stateRef.current;
|
|
4577
|
+
const rows = cur ? (kind === "section" ? cur.sections : kind === "flow" ? cur.flows : cur.pages) : undefined;
|
|
4237
4578
|
if (rows)
|
|
4238
4579
|
writePagesMirror(organizationId, suite, rows, kind);
|
|
4239
4580
|
const rev = rows?.[e.key]?.rev ?? 0;
|
|
@@ -4330,7 +4671,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4330
4671
|
/* ---------------- sync on route, sidebar, facts, DOM ---------------- */
|
|
4331
4672
|
// A page guide's step runs while page guides are live; a mission's while
|
|
4332
4673
|
// 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);
|
|
4674
|
+
const inStep = screen.name === "step" && (screen.page || screen.section || screen.flow ? pagesLive : missionsLive);
|
|
4334
4675
|
const offerUp = pagesLive && screen.name === "pageOffer";
|
|
4335
4676
|
const watching = inStep || offerUp;
|
|
4336
4677
|
React.useEffect(() => {
|
|
@@ -4944,6 +5285,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4944
5285
|
openingSection.current = null;
|
|
4945
5286
|
}
|
|
4946
5287
|
}, []);
|
|
5288
|
+
const flowsNow = React.useMemo(() => pagesLive && hasState && hasFlows
|
|
5289
|
+
? flowsAvailable(suite, env()).map(k => ({ key: k, title: findFlow(suite, k)?.title ?? "" }))
|
|
5290
|
+
: [],
|
|
5291
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5292
|
+
[pagesLive, hasState, hasFlows, suite, env, pathname, search, factsKey]);
|
|
5293
|
+
const flowsSig = flowsNow.map(x => x.key).join(",");
|
|
5294
|
+
/** The flows the panel lists: this person's, startable here or with somewhere to go. */
|
|
5295
|
+
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]);
|
|
5296
|
+
const openFlowHere = React.useCallback((key) => dispatchRef.current({ type: "startFlow", flow: key }), []);
|
|
4947
5297
|
const openPageHere = React.useCallback(() => {
|
|
4948
5298
|
const p = pageHere(suite, env());
|
|
4949
5299
|
if (p)
|
|
@@ -4954,10 +5304,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4954
5304
|
publishPageGuides(null, null, null);
|
|
4955
5305
|
return;
|
|
4956
5306
|
}
|
|
4957
|
-
publishPageGuides({
|
|
4958
|
-
|
|
5307
|
+
publishPageGuides({
|
|
5308
|
+
page: pageNow ? { title: pageNow.title } : null,
|
|
5309
|
+
listed: listed.length,
|
|
5310
|
+
sections: sectionsNow,
|
|
5311
|
+
flows: flowsNow,
|
|
5312
|
+
flowsListed: flowsListed.length,
|
|
5313
|
+
}, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }), openSection, openFlowHere, () => dispatchRef.current({ type: "openPanel", section: "flows" }));
|
|
5314
|
+
// sectionsSig / flowsSig stand for sectionsNow / flowsNow.
|
|
4959
5315
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4960
|
-
}, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection]);
|
|
5316
|
+
}, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere]);
|
|
4961
5317
|
React.useEffect(() => () => publishPageGuides(null, null, null), []);
|
|
4962
5318
|
// The first-visit offer: in "offer" mode, nothing else up, the welcome
|
|
4963
5319
|
// not due here, and a moment of nothing loading on this page.
|
|
@@ -4985,14 +5341,21 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4985
5341
|
if (Date.now() - idleSince < PAGE_OFFER_IDLE_MS)
|
|
4986
5342
|
return;
|
|
4987
5343
|
let key = null;
|
|
5344
|
+
let flow = null;
|
|
4988
5345
|
try {
|
|
4989
5346
|
key = pageOfferDue(cur, suite, env());
|
|
5347
|
+
// A flow's offer comes after the page guide's (and only for `offer: true` flows).
|
|
5348
|
+
if (!key)
|
|
5349
|
+
flow = flowOfferDue(cur, suite, env());
|
|
4990
5350
|
}
|
|
4991
5351
|
catch {
|
|
4992
5352
|
key = null;
|
|
5353
|
+
flow = null;
|
|
4993
5354
|
}
|
|
4994
5355
|
if (key)
|
|
4995
5356
|
dispatchRef.current({ type: "pageOffer", page: key });
|
|
5357
|
+
else if (flow)
|
|
5358
|
+
dispatchRef.current({ type: "pageOffer", page: flow, flow: true });
|
|
4996
5359
|
}, PAGE_POLL_MS);
|
|
4997
5360
|
return () => clearInterval(iv);
|
|
4998
5361
|
}, [offerWatch, suite, env, pathname, search]);
|
|
@@ -5044,6 +5407,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5044
5407
|
if (b)
|
|
5045
5408
|
return;
|
|
5046
5409
|
const e = env();
|
|
5410
|
+
if (pending.flow) {
|
|
5411
|
+
const flow = findFlow(suite, pending.key);
|
|
5412
|
+
if (!flow || !onFlowStart(flow, e))
|
|
5413
|
+
return;
|
|
5414
|
+
setPending(null);
|
|
5415
|
+
dispatchRef.current({ type: "startFlow", flow: pending.key });
|
|
5416
|
+
return;
|
|
5417
|
+
}
|
|
5047
5418
|
const page = findPage(suite, pending.key);
|
|
5048
5419
|
if (!page || !pageMatchScore(page, { code, pathname: e.pathname, search: e.search, isVisible: a => e.isVisible(a) }))
|
|
5049
5420
|
return;
|
|
@@ -5052,6 +5423,93 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5052
5423
|
}, PAGE_POLL_MS);
|
|
5053
5424
|
return () => clearInterval(iv);
|
|
5054
5425
|
}, [pending, pagesLive, suite, env, code]);
|
|
5426
|
+
// A flow in the panel's list: start it here, or go to its start page and start it there.
|
|
5427
|
+
const openListedFlow = React.useCallback((key) => {
|
|
5428
|
+
const flow = findFlow(suite, key);
|
|
5429
|
+
if (!flow)
|
|
5430
|
+
return;
|
|
5431
|
+
const e = env();
|
|
5432
|
+
if (onFlowStart(flow, e)) {
|
|
5433
|
+
dispatchRef.current({ type: "startFlow", flow: key });
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
5436
|
+
dispatchRef.current({ type: "closePanel" });
|
|
5437
|
+
const href = flowHref(flow, guideContext(e, {}));
|
|
5438
|
+
if (!href)
|
|
5439
|
+
return;
|
|
5440
|
+
setPending({ key, until: Date.now() + PAGE_ARRIVE_MS, flow: true });
|
|
5441
|
+
try {
|
|
5442
|
+
if (/^https?:\/\//i.test(href))
|
|
5443
|
+
window.location.assign(href);
|
|
5444
|
+
else
|
|
5445
|
+
navigateRef.current(href);
|
|
5446
|
+
}
|
|
5447
|
+
catch {
|
|
5448
|
+
/* never breaks the app */
|
|
5449
|
+
}
|
|
5450
|
+
}, [suite, env]);
|
|
5451
|
+
// A flow in progress is kept in this tab's session (sessionStorage), so a
|
|
5452
|
+
// refresh or a full page load on one of its pages picks it up again at
|
|
5453
|
+
// its checkpoint ("ทำต่อ" when its step is on another page). Never across
|
|
5454
|
+
// sessions; a flow that ends is forgotten.
|
|
5455
|
+
const flowRun = state?.pageRun?.kind === "flow" ? state.pageRun : null;
|
|
5456
|
+
const flowStepKey = screen.name === "step" && screen.flow ? (findMission(suite, screen.mission)?.steps[screen.index]?.key ?? null) : null;
|
|
5457
|
+
React.useEffect(() => {
|
|
5458
|
+
if (!hasFlows)
|
|
5459
|
+
return;
|
|
5460
|
+
const k = flowMirrorKey(organizationId, suite);
|
|
5461
|
+
try {
|
|
5462
|
+
if (!flowRun) {
|
|
5463
|
+
if (state)
|
|
5464
|
+
sessionStorage.removeItem(k);
|
|
5465
|
+
return;
|
|
5466
|
+
}
|
|
5467
|
+
if (!flowStepKey)
|
|
5468
|
+
return;
|
|
5469
|
+
const flow = findFlow(suite, flowRun.key);
|
|
5470
|
+
sessionStorage.setItem(k, JSON.stringify({ flow: flowRun.key, version: flow?.version, step: flowStepKey, write: flowRun.write, context: state?.progress.context ?? {} }));
|
|
5471
|
+
}
|
|
5472
|
+
catch {
|
|
5473
|
+
/* private mode: no resume after a refresh */
|
|
5474
|
+
}
|
|
5475
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5476
|
+
}, [hasFlows, flowRun?.key, flowRun?.write, flowStepKey, organizationId, suite]);
|
|
5477
|
+
const flowResumed = React.useRef(false);
|
|
5478
|
+
React.useEffect(() => {
|
|
5479
|
+
if (flowResumed.current || !pagesLive || !state || !hasFlows)
|
|
5480
|
+
return;
|
|
5481
|
+
// The missions resume first.
|
|
5482
|
+
if (missionsLive && !resumed.current)
|
|
5483
|
+
return;
|
|
5484
|
+
flowResumed.current = true;
|
|
5485
|
+
const k = flowMirrorKey(organizationId, suite);
|
|
5486
|
+
let saved = null;
|
|
5487
|
+
try {
|
|
5488
|
+
saved = JSON.parse(sessionStorage.getItem(k) ?? "null");
|
|
5489
|
+
}
|
|
5490
|
+
catch {
|
|
5491
|
+
saved = null;
|
|
5492
|
+
}
|
|
5493
|
+
if (!saved?.flow || !saved.step)
|
|
5494
|
+
return;
|
|
5495
|
+
const flow = findFlow(suite, saved.flow);
|
|
5496
|
+
const ok = !!flow && flow.version === saved.version && stateRef.current?.screen.name === "idle" && inFlowChain(flow, { code, pathname });
|
|
5497
|
+
if (!ok) {
|
|
5498
|
+
try {
|
|
5499
|
+
sessionStorage.removeItem(k);
|
|
5500
|
+
}
|
|
5501
|
+
catch {
|
|
5502
|
+
/* ignore */
|
|
5503
|
+
}
|
|
5504
|
+
return;
|
|
5505
|
+
}
|
|
5506
|
+
const context = {};
|
|
5507
|
+
for (const [ck, cv] of Object.entries(saved.context ?? {}))
|
|
5508
|
+
if (typeof cv === "string")
|
|
5509
|
+
context[ck] = cv;
|
|
5510
|
+
dispatchRef.current({ type: "resumeFlow", flow: saved.flow, step: saved.step, context, write: saved.write === true });
|
|
5511
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5512
|
+
}, [pagesLive, !!state, hasFlows, missionsLive]);
|
|
5055
5513
|
if (!live || !state || !layer || !ctxIdle)
|
|
5056
5514
|
return null;
|
|
5057
5515
|
/* ---------------- render ---------------- */
|
|
@@ -5077,6 +5535,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5077
5535
|
const pos = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
|
|
5078
5536
|
const page = screen.page ? findPage(suite, screen.page) : undefined;
|
|
5079
5537
|
const section = screen.section ? findSection(suite, screen.section) : undefined;
|
|
5538
|
+
const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
|
|
5080
5539
|
const read = page ? { heading: `คู่มือหน้านี้ · ${page.title}`, exitLabel: "ปิดคู่มือ" } : section ? { heading: `อธิบายส่วนนี้ · ${section.title}`, exitLabel: "ปิดคำอธิบาย" } : null;
|
|
5081
5540
|
const pageBits = read
|
|
5082
5541
|
? {
|
|
@@ -5084,7 +5543,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5084
5543
|
showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
|
|
5085
5544
|
nextLabel: pos.no >= pos.total ? "เสร็จ" : undefined,
|
|
5086
5545
|
}
|
|
5087
|
-
:
|
|
5546
|
+
: flow
|
|
5547
|
+
? {
|
|
5548
|
+
heading: `พาทำ · ${flow.title}`,
|
|
5549
|
+
exitLabel: "ออกจากการพาทำ",
|
|
5550
|
+
nextLabel: kind === "view" && pos.no >= pos.total ? "เสร็จ" : undefined,
|
|
5551
|
+
}
|
|
5552
|
+
: {};
|
|
5088
5553
|
content = (jsxRuntime.jsx(StepLayer, { spot: layout.spot, holes: layout.holes, popovers: layout.popovers, covered: layout.covered, viewport: { width: layout.vw || window.innerWidth, height: layout.vh || window.innerHeight, offsetTop: layout.oy }, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
5089
5554
|
missionNo,
|
|
5090
5555
|
missionTitle: mission.title,
|
|
@@ -5101,6 +5566,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5101
5566
|
...pageBits,
|
|
5102
5567
|
}, 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
5568
|
}
|
|
5569
|
+
else if (screen.name === "pageOffer" && pagesLive && screen.flow) {
|
|
5570
|
+
const flow = findFlow(suite, screen.page);
|
|
5571
|
+
const mission = flow ? findMission(suite, FLOW_PREFIX + flow.key) : undefined;
|
|
5572
|
+
if (flow && mission) {
|
|
5573
|
+
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" }) }));
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5104
5576
|
else if (screen.name === "pageOffer" && pagesLive) {
|
|
5105
5577
|
const page = findPage(suite, screen.page);
|
|
5106
5578
|
if (page) {
|
|
@@ -5144,12 +5616,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5144
5616
|
const showChip = missionsLive && chipVisible(state, suite, ctxIdle);
|
|
5145
5617
|
// The panel: the missions (when this person has them, and not "คู่มือทุกหน้า"),
|
|
5146
5618
|
// then the page guides they have.
|
|
5147
|
-
const panelMissions = missionsLive && total > 0 && state.panelSection
|
|
5148
|
-
const pageItems = pagesLive
|
|
5149
|
-
|
|
5619
|
+
const panelMissions = missionsLive && total > 0 && state.panelSection === "all";
|
|
5620
|
+
const pageItems = pagesLive && state.panelSection !== "flows" ? listed.map(p => ({ key: p.key, title: p.title, seen: pageRecorded(state, p) })) : [];
|
|
5621
|
+
const flowItems = pagesLive && state.panelSection !== "pages"
|
|
5622
|
+
? flowsListed.map(f => ({ key: f.key, title: f.title, minutes: f.minutes, done: flowRecorded(state, f) && state.flows[f.key]?.status === "completed" }))
|
|
5150
5623
|
: [];
|
|
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);
|
|
5624
|
+
const panelShown = state.panelOpen && screen.name === "idle" && (panelMissions || pageItems.length > 0 || flowItems.length > 0);
|
|
5625
|
+
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
5626
|
}
|
|
5154
5627
|
/** A stable key for the facts object, so effects re-run when they change. */
|
|
5155
5628
|
function useFactsKey(facts) {
|
|
@@ -5277,6 +5750,8 @@ exports.guideMenuShown = guideMenuShown;
|
|
|
5277
5750
|
exports.loadGuides = loadGuides;
|
|
5278
5751
|
exports.matchApiPath = matchApiPath;
|
|
5279
5752
|
exports.matchRoute = matchRoute;
|
|
5753
|
+
exports.openFlow = openFlow;
|
|
5754
|
+
exports.openFlowsPanel = openFlowsPanel;
|
|
5280
5755
|
exports.openGuidePanel = openGuidePanel;
|
|
5281
5756
|
exports.openPageGuide = openPageGuide;
|
|
5282
5757
|
exports.openPagesPanel = openPagesPanel;
|
|
@@ -5291,4 +5766,4 @@ exports.useGuideEntry = useGuideEntry;
|
|
|
5291
5766
|
exports.usePageGuideEntry = usePageGuideEntry;
|
|
5292
5767
|
exports.usePageGuides = usePageGuides;
|
|
5293
5768
|
exports.useSectionGuideValues = useSectionGuideValues;
|
|
5294
|
-
//# sourceMappingURL=format-
|
|
5769
|
+
//# sourceMappingURL=format-BVoVc7Yk.cjs.map
|