diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts index 5f35149da..3bec34b5a 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry.ts @@ -18,17 +18,21 @@ import { showConfirmDialog, showToast, } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money"; -import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api"; -import type { CfListItem, CfQuery } from "./M02_DF_Api"; -import type { CfEvalResult, CfValues } from "./M02_DF_Eval"; -import type { CfKind, CfLogic, CfPort } from "./M02_DF_Model"; +import type { CfListItem } from "./M02_DF_Api"; +import type { CfBreak, CfEvalOptions, CfEvalResult, CfValues } from "./M02_DF_Eval"; +import type { CfKind, CfLogic, CfPort, CfStep } from "./M02_DF_Model"; +import type { CfSel } from "./M02_DF_Verify"; +import type { DfPillSel } from "./M02_DF_Diagram_Parts"; import { detailFrame, drawHead, drawWarnings, - lineBody, - pillBody, + fetchValues, + loadLogics, + queriesOf, + unitChoices, type DfPick, } from "./M02_DF_Entry_Detail"; import "./M02_DF_Entry.css"; @@ -45,8 +49,10 @@ const loadMods = () => import("./M02_DF_Summary"), import("./M02_DF_Sample"), import("./M02_DF_InputPanel"), - import("./M02_DF_Rule"), - ]).then(([model, ev, api, diagram, func, output, summary, sample, input, rule]) => ({ + import("./M02_DF_PillPanel"), + import("./M02_DF_LinkPanel"), + import("./M02_DF_Standards"), + ]).then(([model, ev, api, diagram, func, output, summary, sample, input, pill, line, std]) => ({ model, ev, api, @@ -56,7 +62,9 @@ const loadMods = () => summary, sample, input, - rule, + pill, + line, + std, })); type Mods = Awaited>; @@ -81,6 +89,8 @@ const TEXT = { Diagram: ["다이어그램", "Diagram"], Summary: ["원가", "Cost"], Color: ["상자 색", "Box colour"], + Standards: ["표준 표", "Standards"], + Logics: ["다른 로직을 못 읽음 — {v}", "Couldn't read other logics — {v}"], NoName: ["로직 이름을 넣어 주세요", "Enter a logic name"], SaveAnyway: [ "고칠 곳이 있음 — {v} · 그래도 저장할까요?", @@ -113,63 +123,6 @@ const te = (key: TextKey, vars: Record = {}): string => const muted = (text: string): HTMLElement => el("p", { className: "m02-df__muted", text }); -/** 결과 단위 후보 — 품셈 로직에 다섯 번 넘게 쓰인 「원/…」 차례(설명 붙은 드문 단위는 뺌) */ -async function unitChoices(): Promise { - const count = new Map(); - try { - for (const x of await fetchLogics()) { - if (x.결과단위.startsWith("원/")) count.set(x.결과단위, (count.get(x.결과단위) ?? 0) + 1); - } - } catch { - /* 못 받으면 기본 하나 */ - } - const units = [...count.entries()] - .filter(([u, n]) => n >= 5 && !/[\s()「·]/.test(u)) - .sort((a, b) => b[1] - a[1]) - .map(([u]) => u); - return units.length ? units : ["원/㎡"]; -} - -/** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */ -function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string }[] { - return lg.boxes.flatMap((b) => { - const inp = b.input; - if (b.kind !== "input" || !inp || inp.mode !== "group" || !inp.group) return []; - const group = inp.group; - const els = inp.elements - .filter((e) => e.ref && !e.ref.startsWith("UE-")) - .map((e) => { - // 기계 요소는 「키#값 칸」 — 서버에는 키와 칸(field)을 따로 넘김 - const [ref, field] = group === "기계" ? e.ref.split("#") : [e.ref, undefined]; - const q: CfQuery = { id: `${b.id}\u0001${e.name}`, group, ref, unit: e.unit }; - if (field) q.field = field; - return { q, box: b.id, port: e.name }; - }); - const table = inp.table - ? [{ q: { id: `${b.id}\u0001표`, group, table: inp.table }, box: b.id, port: "표" }] - : []; - return [...els, ...table]; - }); -} - -/** 입력 값 받기 — 받은 것만 묶고 못 받은 것은 쉬운 말 */ -async function fetchValues(M: Mods, lg: CfLogic): Promise<{ values: CfValues; errors: string[] }> { - const asks = queriesOf(lg); - if (!asks.length) return { values: {}, errors: [] }; - const answers = await M.api.cfValues( - asks.map((a) => a.q), - lg.userElements as never, - ); - const values: CfValues = {}; - const errors: string[] = []; - for (const a of asks) { - const got = answers.find((x) => x.id === a.q.id); - if (!got?.ok) errors.push(`${a.port}${got?.reason ? ` — ${got.reason}` : ""}`); - else (values[a.box] ??= {})[a.port] = got.rows ?? got.value ?? null; - } - return { values, errors }; -} - /** M01 단가산출 막대의 다른 시험판 단추 옆에 놓는 단추 — 누르면 시험판 페이지 */ export function createDataFlowButton(): HTMLButtonElement { return createButton({ label: te("Open"), variant: "filled", size: "sm", onClick: openDataFlow }); @@ -330,6 +283,10 @@ function openDataFlow(): void { const pickedBox = (): string | null => (pick?.kind === "box" ? pick.id : null); let values: CfValues = {}; let valueSig = ""; + let logics: NonNullable = {}; + let logicSig = ""; + let logicNotes: string[] = []; + let broken: CfBreak[] = []; let notes: string[] = []; let result: CfEvalResult; let busy = false; @@ -355,15 +312,15 @@ function openDataFlow(): void { const outHost = el("div"); const inHost = el("div"); const summaryHost = el("div"); - const lineHost = el("div"); + const linkHost = el("div"); const pillHost = el("div"); const detail = detailFrame(); detail.settings.append(colorRow); - detail.body.append(inHost, funcHost, outHost, lineHost, pillHost); + detail.body.append(inHost, funcHost, outHost, linkHost, pillHost); const evalNow = (): CfEvalResult => { try { - return M.ev.evalLogic(lg, values); + return M.ev.evalLogic(lg, values, { logics }); } catch (e) { const cost = { 노무비: 0, 재료비: 0, 경비: 0, 값들: {}, 계: 0 }; return { ports: {}, outputs: {}, cost, errors: [M.api.cfPlain(e)] }; @@ -387,7 +344,7 @@ function openDataFlow(): void { const sig = JSON.stringify(queriesOf(lg).map((a) => a.q)); if (sig === valueSig) return; valueSig = sig; - void fetchValues(M, lg).then( + void fetchValues(M.api.cfValues, lg).then( (got) => { if (sig !== valueSig) return; values = got.values; @@ -401,6 +358,54 @@ function openDataFlow(): void { ); }; + /** 다른 로직 입력이 부르는 로직들 다시 읽기 — 부르는 열쇠가 바뀐 때만 */ + const refreshLogics = (): void => { + const sig = JSON.stringify(lg.boxes.map((b) => b.input?.logic?.key ?? "")); + if (sig === logicSig) return; + logicSig = sig; + const get = async (key: string): Promise => + key.startsWith("DF-") ? M.api.cfGet(key) : M.sample.sampleLoad(key); + void loadLogics(lg, get).then((got) => { + if (sig !== logicSig) return; + logics = got.logics; + logicNotes = got.errors.length ? [te("Logics", { v: got.errors.join(" · ") })] : []; + redraw(); + }); + }; + + /** 고름 — 검증 칸 줄 · 알약 판의 상자 이름 누름 */ + const choose = (sel: CfSel): void => { + const box = lg.boxes.find((b) => b.id === sel.box); + pick = sel.link + ? { kind: "link", id: sel.link } + : box && sel.port + ? { kind: "pill", box: box.id, port: sel.port, dir: M.model.portDir(box, sel.port) } + : box + ? { kind: "box", id: box.id } + : null; + redraw(); + }; + + /** 흐름 길 — 알약 = 그 알약에서 앞뒤 · 선 = 보낸 알약 앞 · 받는 알약 뒤 */ + const traceNow = (): { back: CfStep[]; forward: CfStep[] } | null => { + try { + if (pick?.kind === "pill") { + const p = { box: pick.box, name: pick.port }; + return { back: M.model.traceBack(lg, p), forward: M.model.traceForward(lg, p) }; + } + const id = pick?.kind === "link" ? pick.id : ""; + const link = lg.links.find((l) => l.id === id); + if (link) + return { + back: M.model.traceBack(lg, link.from), + forward: M.model.traceForward(lg, link.to), + }; + } catch { + /* 따라갈 수 없으면 칠하지 않음 */ + } + return null; + }; + const name = createInputField({ label: te("Name"), placeholder: te("NamePlaceholder"), @@ -417,18 +422,16 @@ function openDataFlow(): void { }); const diagramOn = { - select: (id: string | null): void => { - pick = !id - ? null - : lg.links.some((l) => l.id === id) - ? { kind: "link", id } - : { kind: "box", id }; - drawPanel(); - }, - /** 알약 고름 — 다이어그램(sub8)이 알려 주면 상세 내용이 알약 판으로 */ - pill: (p: { box: string; name: string; dir: "in" | "out" } | null): void => { - pick = p ? { kind: "pill", ...p } : null; - drawPanel(); + select: (id: string | null, pill?: DfPillSel): void => { + pick = pill + ? { kind: "pill", ...pill } + : !id + ? null + : lg.links.some((l) => l.id === id) + ? { kind: "link", id } + : { kind: "box", id }; + // 흐름 길 · 판을 새로 — 다이어그램이 제 그림을 그린 뒤 + setTimeout(redraw); }, move: (id: string, x: number, y: number): void => edit(M.model.moveBox(lg, id, x, y)), link: (from: CfPort, to: CfPort): string | null => { @@ -455,35 +458,20 @@ function openDataFlow(): void { const box = lg.boxes.find((b) => b.id === pickedBox()); drawHead(detail, lg, pick); colorRow.hidden = !box; - lineHost.hidden = pick?.kind !== "link"; + linkHost.hidden = pick?.kind !== "link"; pillHost.hidden = pick?.kind !== "pill"; - if (pick?.kind === "link") { - const linkId = pick.id; - const rule = lg.links.find((l) => l.id === linkId)?.rule; - let text: string | null = null; - try { - text = rule ? M.rule.ruleLabel(rule) : null; - } catch { - text = rule ? rule.kind : null; - } - lineBody(lineHost, lg, pick.id, text); - } - if (pick?.kind === "pill") { - const port = { box: pick.box, name: pick.name }; - pillBody( + if (pick?.kind === "link") + M.line.renderLinkPanel(linkHost, lg, pick.id, { change: edit }, { values, logics }); + if (pick?.kind === "pill") + M.pill.renderPillPanel( pillHost, lg, - () => M.model.traceBack(lg, port), - () => M.model.traceForward(lg, port), + pick.box, + pick.port, + { pick: (id) => choose({ box: id }) }, + { values, logics }, ); - } - let checks: string[] = []; - try { - checks = M.ev.checkLogic(lg, values); - } catch { - checks = []; - } - drawWarnings(detail, lg, pick, [...checks, ...(result?.errors ?? [])]); + drawWarnings(detail, pick, broken); if (box && /^#[0-9a-f]{6}$/i.test(box.color)) colorBox.value = box.color; funcHost.hidden = box?.kind !== "function"; outHost.hidden = box?.kind !== "output"; @@ -497,23 +485,34 @@ function openDataFlow(): void { const redraw = (): void => { result = evalNow(); refreshValues(); + refreshLogics(); + const linkId = pick?.kind === "link" ? pick.id : ""; + if (linkId && !lg.links.some((l) => l.id === linkId)) pick = null; + try { + broken = M.ev.breaks(lg, values, { logics }); + } catch { + broken = result.breaks ?? []; + } unsaved.hidden = lg === savedLg; undo.disabled = !stack.length; deleteButton.hidden = !lg.key; - const selected = - pick?.kind === "box" || pick?.kind === "link" ? pick.id : (pick?.box ?? null); - M.diagram.renderDiagram(diagramHost, lg, diagramOn, { values: result.ports, selected }); + M.diagram.renderDiagram(diagramHost, lg, diagramOn, { + values: result.ports, + selected: pick?.kind === "box" || pick?.kind === "link" ? pick.id : null, + selectedPill: + pick?.kind === "pill" ? { box: pick.box, port: pick.port, dir: pick.dir } : null, + trace: traceNow(), + breaks: broken, + }); drawPanel(); - // 「고칠 것」 검사가 받은 입력 값으로 하도록 값도 넘김 — Summary 가 opts.values 를 받으면 쓰임 - // ponytail: 지금 Summary 는 checkLogic(lg) 을 값 없이 불러 「값이 아직 없음」 이 남음(sub5 고침 뒤 이 꼴 그대로 둠) - (M.summary.renderSummary as (...args: unknown[]) => void)( + M.summary.renderSummary( summaryHost, lg, result, - { change: edit }, + { change: edit, select: choose }, { values }, ); - note.textContent = [...notes, ...result.errors].slice(0, 4).join("\n"); + note.textContent = [...notes, ...logicNotes, ...result.errors].slice(0, 4).join("\n"); }; const save = async (): Promise => { @@ -524,7 +523,7 @@ function openDataFlow(): void { } let problems: string[] = []; try { - problems = M.ev.checkLogic(lg, values); + problems = M.ev.checkLogic(lg, values, { logics }); } catch { problems = []; } @@ -588,6 +587,18 @@ function openDataFlow(): void { el("div", { className: "m02-df__actions", children: [ + createButton({ + label: te("Standards"), + variant: "ghost", + size: "sm", + onClick: () => + openModal({ + title: te("Standards"), + closeLabel: te("Close"), + mount: (host: HTMLElement) => + M.std.renderStandards(host, { change: () => redraw() }), + }), + }), undo, createButton({ label: te("Save"), size: "sm", onClick: () => void save() }), deleteButton, diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts index be13d2dd1..fc27df74b 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Entry_Detail.ts @@ -2,12 +2,15 @@ * M02_DF_Entry_Detail.ts * 단가산출 흐름도 — 「상세 내용」 칸 틀(정함 9): 머리(왼쪽 「상세 내용」 · 오른쪽 고른 상자 제목 · 알약이면 그 옆 알약 이름) * · 설정 · 본문 · 경고 차례. 상자 · 알약 · 선 무엇을 골라도 바뀜 — 본문은 Entry 가 종류별 판을 끼움. - * 선 · 알약 본문은 규칙 판 · 알약 판(sub2)이 오기 전까지 이 파일이 흐름 줄(보내는 곳 → 받는 곳 · 출처 사슬 · 가는 곳)만 보임. + * 경고 = 끊긴 곳 가운데 고른 것 몫 · 다른 로직 입력이 부르는 로직 읽기(겹쳐 부름) · 입력 값 물음 · 결과 단위 후보. * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { el } from "@ui/ui_template_elements"; -import type { CfLogic, CfPort, CfStep } from "./M02_DF_Model"; +import { fetchLogics } from "../../M01_MasterData/M01_MasterData_UI_Logic_Api"; +import type { CfAnswer, CfQuery } from "./M02_DF_Api"; +import type { CfBreak, CfValues } from "./M02_DF_Eval"; +import type { CfLogic } from "./M02_DF_Model"; const TEXT = { Detail: ["상세 내용", "Details"], @@ -20,14 +23,6 @@ const TEXT = { "Choose a box, pill or line in the diagram", ], Line: ["선", "Line"], - From: ["보내는 곳", "From"], - To: ["받는 곳", "To"], - Rule: ["선 규칙", "Line rule"], - RuleNone: ["없음 — 값을 그대로 보냄", "None — sends the value as is"], - Back: ["온 길(출처)", "Came from"], - Forward: ["가는 길", "Goes to"], - NoTrace: ["아직 따라갈 수 없음", "Can't trace yet"], - Nowhere: ["이어진 곳 없음", "Not connected"], In: ["받는 알약", "Incoming pill"], Out: ["내보내는 알약", "Outgoing pill"], } as const; @@ -39,7 +34,7 @@ const tt = (key: keyof typeof TEXT): string => export type DfPick = | { kind: "box"; id: string } | { kind: "link"; id: string } - | { kind: "pill"; box: string; name: string; dir: "in" | "out" } + | { kind: "pill"; box: string; port: string; dir: "in" | "out" } | null; export interface DetailParts { @@ -91,7 +86,6 @@ export function detailFrame(): DetailParts { } const boxName = (lg: CfLogic, id: string): string => lg.boxes.find((b) => b.id === id)?.name ?? id; -const portText = (lg: CfLogic, p: CfPort): string => `${boxName(lg, p.box)} · ${p.name}`; /** 머리 · 빈 자리 — 고른 것에 맞춤 · 설정 · 경고 줄은 Entry 가 채움 */ export function drawHead(d: DetailParts, lg: CfLogic, pick: DfPick): void { @@ -107,77 +101,113 @@ export function drawHead(d: DetailParts, lg: CfLogic, pick: DfPick): void { ? `${tt("Line")} · ${boxName(lg, link.from.box)} → ${boxName(lg, link.to.box)}` : ""; d.pill.textContent = - pick?.kind === "pill" ? `${tt(pick.dir === "in" ? "In" : "Out")} · ${pick.name}` : ""; + pick?.kind === "pill" ? `${tt(pick.dir === "in" ? "In" : "Out")} · ${pick.port}` : ""; d.pill.hidden = pick?.kind !== "pill"; } -/** 경고 — 화면 계산 · 검사 글 가운데 고른 것(상자 이름 · 알약 이름)이 든 줄만 */ -export function drawWarnings(d: DetailParts, lg: CfLogic, pick: DfPick, all: string[]): void { - const names: string[] = []; - if (pick?.kind === "box") names.push(`「${boxName(lg, pick.id)}」`); - if (pick?.kind === "pill") names.push(`「${boxName(lg, pick.box)}」`); - if (pick?.kind === "link") { - const l = lg.links.find((x) => x.id === pick.id); - if (l) names.push(`「${boxName(lg, l.from.box)}」`, `「${boxName(lg, l.to.box)}」`); - } - const mine = [...new Set(all)].filter( - (w) => names.some((n) => w.includes(n)) && (pick?.kind !== "pill" || w.includes(pick.name)), +/** 경고 — 끊긴 곳(Eval breaks) 가운데 고른 것 몫만 · 상자 = 그 상자 · 알약 = 그 알약 · 선 = 그 선 */ +export function drawWarnings(d: DetailParts, pick: DfPick, all: CfBreak[]): void { + const mine = all.filter((k) => + pick?.kind === "box" + ? k.box === pick.id + : pick?.kind === "pill" + ? k.box === pick.box && k.port === pick.port && !k.link + : pick?.kind === "link" + ? k.link === pick.id + : false, ); d.warn.replaceChildren( ...(mine.length - ? mine.map((w) => el("li", { text: w })) + ? [...new Set(mine.map((k) => k.text))].map((w) => el("li", { text: w })) : [el("li", { className: "m02-df__muted", text: tt("WarnNone") })]), ); } -/** 선 본문 — 보내는 곳 → 받는 곳 · 선 규칙 글(규칙 판이 오면 그쪽이 그림) */ -export function lineBody( - host: HTMLElement, +/** 다른 로직 입력이 부르는 로직들 — 그 로직이 또 부르는 것까지(겹쳐 부름 · 한 번씩) · 못 읽은 것은 빼고 까닭 */ +export async function loadLogics( lg: CfLogic, - id: string, - ruleText: string | null, -): void { - const l = lg.links.find((x) => x.id === id); - if (!l) return void host.replaceChildren(); - host.replaceChildren( - el("dl", { - className: "m02-df__detail-dl", - children: [ - el("dt", { text: tt("From") }), - el("dd", { text: portText(lg, l.from) }), - el("dt", { text: tt("To") }), - el("dd", { text: portText(lg, l.to) }), - el("dt", { text: tt("Rule") }), - el("dd", { text: ruleText ?? tt("RuleNone") }), - ], - }), - ); + get: (key: string) => Promise, +): Promise<{ logics: Record; errors: string[] }> { + const logics: Record = {}; + const errors: string[] = []; + const keysOf = (x: CfLogic): string[] => + x.boxes.flatMap((b) => + b.input?.mode === "logic" && b.input.logic?.key ? [b.input.logic.key] : [], + ); + const seen = new Set(lg.key ? [lg.key] : []); + let todo = keysOf(lg); + while (todo.length) { + const key = todo.shift()!; + if (seen.has(key)) continue; + seen.add(key); + try { + const got = await get(key); + if (!got) throw new Error(key); + logics[key] = got; + todo = [...todo, ...keysOf(got)]; + } catch (e) { + errors.push(`${key} — ${e instanceof Error ? e.message : String(e)}`); + } + } + return { logics, errors }; } -/** 알약 본문 — 온 길(출처 사슬) · 가는 길 · 흐름 따라가기(Model traceBack · traceForward)가 아직이면 한 줄 */ -export function pillBody( - host: HTMLElement, - lg: CfLogic, - back: () => CfStep[], - forward: () => CfStep[], -): void { - const list = (steps: () => CfStep[]): HTMLElement => { - let got: CfStep[]; - try { - got = steps(); - } catch { - return el("p", { className: "m02-df__muted", text: tt("NoTrace") }); +/** 결과 단위 후보 — 품셈 로직에 다섯 번 넘게 쓰인 「원/…」 차례(설명 붙은 드문 단위는 뺌) */ +export async function unitChoices(): Promise { + const count = new Map(); + try { + for (const x of await fetchLogics()) { + if (x.결과단위.startsWith("원/")) count.set(x.결과단위, (count.get(x.결과단위) ?? 0) + 1); } - if (!got.length) return el("p", { className: "m02-df__muted", text: tt("Nowhere") }); - return el("ol", { - className: "m02-df__detail-trace", - children: got.map((s) => el("li", { text: portText(lg, { box: s.box, name: s.port }) })), - }); - }; - host.replaceChildren( - el("h5", { text: tt("Back") }), - list(back), - el("h5", { text: tt("Forward") }), - list(forward), + } catch { + /* 못 받으면 기본 하나 */ + } + const units = [...count.entries()] + .filter(([u, n]) => n >= 5 && !/[\s()「·]/.test(u)) + .sort((a, b) => b[1] - a[1]) + .map(([u]) => u); + return units.length ? units : ["원/㎡"]; +} + +/** 입력 값 물음 — 그룹 입력 상자의 요소(알약 = 요소 이름) · 표(알약 「표」) · 사용자 요소 · 변수는 Eval 이 스스로 봄 */ +export function queriesOf(lg: CfLogic): { q: CfQuery; box: string; port: string }[] { + return lg.boxes.flatMap((b) => { + const inp = b.input; + if (b.kind !== "input" || !inp || inp.mode !== "group" || !inp.group) return []; + const group = inp.group; + const els = inp.elements + .filter((e) => e.ref && !e.ref.startsWith("UE-")) + .map((e) => { + // 기계 요소는 「키#값 칸」 — 서버에는 키와 칸(field)을 따로 넘김 + const [ref, field] = group === "기계" ? e.ref.split("#") : [e.ref, undefined]; + const q: CfQuery = { id: `${b.id}\u0001${e.name}`, group, ref, unit: e.unit }; + if (field) q.field = field; + return { q, box: b.id, port: e.name }; + }); + const table = inp.table + ? [{ q: { id: `${b.id}\u0001표`, group, table: inp.table }, box: b.id, port: "표" }] + : []; + return [...els, ...table]; + }); +} + +/** 입력 값 받기 — 받은 것만 묶고 못 받은 것은 쉬운 말 */ +export async function fetchValues( + ask: (qs: CfQuery[], els: never) => Promise, + lg: CfLogic, +): Promise<{ values: CfValues; errors: string[] }> { + const asks = queriesOf(lg); + if (!asks.length) return { values: {}, errors: [] }; + const answers = await ask( + asks.map((a) => a.q), + lg.userElements as never, ); + const values: CfValues = {}; + const errors: string[] = []; + for (const a of asks) { + const got = answers.find((x) => x.id === a.q.id); + if (!got?.ok) errors.push(`${a.port}${got?.reason ? ` — ${got.reason}` : ""}`); + else (values[a.box] ??= {})[a.port] = got.rows ?? got.value ?? null; + } + return { values, errors }; }