@wix/himalaya-cli 0.859.0 → 0.861.0
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/dist/cli.mjs +34 -22
- package/guides/himi-test.md +24 -5
- package/guides/mobile-ux.md +14 -6
- package/guides/owner-lite-site-binding.md +5 -0
- package/guides/release-notes.md +13 -6
- package/package.json +2 -2
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/community-club-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/owner-lite/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-analytics/report.ts +52 -15
package/guides/himi-test.md
CHANGED
|
@@ -315,11 +315,30 @@ the relations held.
|
|
|
315
315
|
|
|
316
316
|
- **Measured, not asserted.** A committed kill-rate benchmark records what fraction of planted,
|
|
317
317
|
realistic reducer bugs the crawl actually catches, per bug class, and every new check has to move
|
|
318
|
-
that number.
|
|
319
|
-
|
|
320
|
-
— a crash, a stuck skeleton, a `NaN` —
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
that number. It is **31 of 269 = 11.5%** across all 48 content dirs, and the table is as useful
|
|
319
|
+
for what it misses as for what it catches. Most of the checks judge a *single settled state* and
|
|
320
|
+
look there for an *implausible* value — a crash, a stuck skeleton, a `NaN` — so a bug that
|
|
321
|
+
produces a plausible wrong one walks straight past them: a total off by one is 0/32, the wrong
|
|
322
|
+
row dropped 0/27. What reaches a plausible wrong value is the smaller set of checks that compare
|
|
323
|
+
two states instead of judging one — the relational axis (an action against the action that
|
|
324
|
+
undoes it, a refresh against a fresh boot) and `typed-value-ignored`, which fires when a dispatch
|
|
325
|
+
carrying a real payload changes nothing at all. That last one is what moved `wrong-key` to 12/48,
|
|
326
|
+
by typing the value a UI control actually carries so the half of each reducer that reads its
|
|
327
|
+
payload finally runs. A green run means no invariant fired, not that the app is correct.
|
|
328
|
+
|
|
329
|
+
The benchmark also carries a `control-throw` row at 48/48, and it is **excluded from the rate on
|
|
330
|
+
purpose**: a planted `throw` is caught by construction, so counting it would inflate the figure
|
|
331
|
+
with a class that can never move. It is the harness checking itself — anything below 100% there
|
|
332
|
+
means the run stopped dispatching and no other number in the table should be believed. Read it as
|
|
333
|
+
the harness's self-test — never as evidence that the crawl catches crashes in your app.
|
|
334
|
+
|
|
335
|
+
Two things about that number are worth knowing, because both were learned the hard way. It was
|
|
336
|
+
published as 13.6% (11 of 81) until that denominator turned out to omit a whole bug class, and it
|
|
337
|
+
was measured over 25 sample apps until the starter templates `himi init --template` ships — the
|
|
338
|
+
23 a customer actually adopts, and 97% of the benchmark's worker *files* (90% of its code) —
|
|
339
|
+
turned out never to have been in it at all. Measured over the code customers ship, the crawl is roughly half as effective
|
|
340
|
+
as the sample apps suggested. This page carries the current figure; treat one quoted anywhere
|
|
341
|
+
else, including in a dated release note, as the number on the day it was written.
|
|
323
342
|
- **Zero false positives, corpus-proven.** Held across every sweep of the 40+ app corpus —
|
|
324
343
|
except `poll-action-missing`, which ships at `error` without corpus evidence behind it
|
|
325
344
|
(see the exception noted under "What it checks" above).
|
package/guides/mobile-ux.md
CHANGED
|
@@ -115,8 +115,10 @@ errors block readiness; record tool-coverage limits separately.
|
|
|
115
115
|
|
|
116
116
|
## Evidence
|
|
117
117
|
|
|
118
|
-
- Render
|
|
119
|
-
|
|
118
|
+
- Render each screen at a phone width, at the narrowest width you support, and again at a width
|
|
119
|
+
past the 600px breakpoint. `himi render <screen> --width 320` then `--width 834` writes both
|
|
120
|
+
into `.himi/ux-audit.json`, which is what this marker grades. Record whether a one-column
|
|
121
|
+
layout at the wider size is intentional or the screen adapts.
|
|
120
122
|
- Exercise typed input, submit, completion, cancel/back, and any destructive action; record
|
|
121
123
|
confirmation, undo, or recovery.
|
|
122
124
|
- Drive iOS back/cancel, keyboard submit, Dynamic Type, and VoiceOver; drive Android system Back,
|
|
@@ -143,8 +145,11 @@ itself the evidence — see the strict audit contract.
|
|
|
143
145
|
|
|
144
146
|
**Two commands read this contract.** `himi validate` reports a gap as a **warning**, so a rewrite
|
|
145
147
|
that drops something the gate needs is caught in the loop you are already running rather than at
|
|
146
|
-
the end of it. `himi
|
|
147
|
-
|
|
148
|
+
the end of it. `himi ux audit --strict` promotes every gap to a **release blocker**.
|
|
149
|
+
`himi validate --strict` promotes the structural ones — the two headings and the one-CTA rule — but
|
|
150
|
+
deliberately leaves **width evidence** a warning: a package that has not rendered yet would
|
|
151
|
+
otherwise fail strict validation the moment it is scaffolded, so that gap is the evidence gate's to
|
|
152
|
+
refuse. Both read the same markers. The two HEADINGS are matched verbatim — other
|
|
148
153
|
tooling addresses the document by them, so a heading really is the thing itself:
|
|
149
154
|
|
|
150
155
|
- The heading `## Implementation acceptance`.
|
|
@@ -229,8 +234,11 @@ tried in this order:
|
|
|
229
234
|
icon-834.png` pass. The marker is permissive there on purpose. A gate that rejects real work
|
|
230
235
|
costs build loops; a permissive one costs a line of prose, and a card or drawer width always
|
|
231
236
|
passed anyway.
|
|
232
|
-
- Or
|
|
233
|
-
|
|
237
|
+
- Or, in a package scaffolded before #2590, keep the legacy scaffold sentence,
|
|
238
|
+
`compact (320/360)` followed later by `expanded`, which is still accepted as-is. The template
|
|
239
|
+
stopped shipping that sentence in #2590 — it satisfied this marker on a package that had
|
|
240
|
+
rendered nothing — so a newer scaffold does not carry it, and adding it by hand claims evidence
|
|
241
|
+
you may not have. The two **may wrap across lines** — the check is not line-based — but
|
|
234
242
|
`expanded` must follow within about 400 characters, so keep them in the same sentence or bullet
|
|
235
243
|
rather than in separate sections.
|
|
236
244
|
|
|
@@ -23,6 +23,11 @@ Source of truth: `apps/himi-owner-lite/config.ts`. A management app that rendere
|
|
|
23
23
|
knowing *which owner* is looking at it would be showing someone else's business, which is why
|
|
24
24
|
the gate fails closed rather than falling back to a visitor.
|
|
25
25
|
|
|
26
|
+
The Sites List is the Tier-5 `himi-owner-lite-sites` worker. Its `pick` action calls
|
|
27
|
+
`site.setActiveSite(msid)` and waits for the site-scoped signed instance before the selected
|
|
28
|
+
business is used by subsequent management screens. The worker also clears the Stores catalog
|
|
29
|
+
version around the switch so a catalog resolved for one business is not reused for another.
|
|
30
|
+
|
|
26
31
|
## What owner-lite *does* read
|
|
27
32
|
|
|
28
33
|
Verified against `apps/himi-owner-lite/config.ts` — if a variable is not in this table, owner-lite
|
package/guides/release-notes.md
CHANGED
|
@@ -172,12 +172,19 @@ areas, always in the same order, so you can skim for the one you care about.
|
|
|
172
172
|
the behavioural crawl over any error-level lint issue, and skipped typed journeys entirely while
|
|
173
173
|
still reporting a pass. (#2392)
|
|
174
174
|
- **The `himi test` reference now states what a green run does and does not prove.** A benchmark
|
|
175
|
-
plants realistic reducer bugs and measures the fraction the crawl catches, per bug class
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
plants realistic reducer bugs and measures the fraction the crawl catches, per bug class. The
|
|
176
|
+
useful half is where it is low: bugs that produce an *implausible* value (a crash, a stuck
|
|
177
|
+
skeleton, a `NaN`) are caught, while bugs that produce a *plausible wrong value* — a total off by
|
|
178
|
+
one, the wrong row dropped — largely are not. A green run means no invariant fired, not that your
|
|
179
|
+
app is correct. The figure comes from that benchmark, not from your own runs — `himi test` output
|
|
180
|
+
is unchanged. (#2402)
|
|
181
|
+
|
|
182
|
+
This entry no longer quotes the rate, and that is deliberate. It said "13.6% today", a figure
|
|
183
|
+
corrected shortly afterwards for omitting a whole bug class and moved several times since by
|
|
184
|
+
changes to the corpus and the oracles — so a reader calibrating how far to trust a green run was
|
|
185
|
+
reading a number that was both wrong and getting wronger. A dated note cannot stay right about a
|
|
186
|
+
live measurement. `himi docs himi-test` carries the current figure, how it was measured, and the
|
|
187
|
+
corrections behind it.
|
|
181
188
|
- **`himi validate` no longer answers `ok: true` after a CMS check that never ran.** An unloadable
|
|
182
189
|
`config.ts` was swallowed, the CMS pass returned nothing, and validation reported success. (#2389)
|
|
183
190
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.861.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"artifactId": "himalaya-cli"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
"falconPackageHash": "
|
|
66
|
+
"falconPackageHash": "2d77c6f215930baecdc2a1637d6fb8dd37e4a6ba32421c3a31e94a16"
|
|
67
67
|
}
|
|
@@ -181,11 +181,22 @@ function subjectLabelOf(m: SemanticModelSummary): string {
|
|
|
181
181
|
* renders as a dash rather than as 0. `formatNumber` and not `toLocaleString`,
|
|
182
182
|
* which is ECMA-402 and absent on QuickJS-ng (watchOS/Wear OS).
|
|
183
183
|
*/
|
|
184
|
+
/**
|
|
185
|
+
* The one placeholder a cell falls back to when there is nothing to render.
|
|
186
|
+
*
|
|
187
|
+
* Named, because two things depend on it meaning exactly one thing: `cellText`
|
|
188
|
+
* produces it, and `runQuery` asks whether the server's total came back as it —
|
|
189
|
+
* which is how "the owner asked for a total and got a dash" is told apart from
|
|
190
|
+
* "the server answered". Spelling it twice let those two drift apart once
|
|
191
|
+
* already.
|
|
192
|
+
*/
|
|
193
|
+
const DASH = "\u2014";
|
|
194
|
+
|
|
184
195
|
function cellText(v: SemanticValue | undefined): string {
|
|
185
|
-
if (!v) return
|
|
196
|
+
if (!v) return DASH;
|
|
186
197
|
if (v.formatted !== undefined && v.formatted !== "") return v.formatted;
|
|
187
198
|
const raw = v.value;
|
|
188
|
-
if (raw === null || raw === undefined) return
|
|
199
|
+
if (raw === null || raw === undefined) return DASH;
|
|
189
200
|
if (typeof raw === "number") return formatNumber(raw);
|
|
190
201
|
if (typeof raw === "boolean") return raw ? "Yes" : "No";
|
|
191
202
|
if (typeof raw === "string") return raw;
|
|
@@ -387,6 +398,23 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
387
398
|
? copyParam("ownerReport.missingFields", "No data returned for: ") + result.missingFields.join(", ")
|
|
388
399
|
: "";
|
|
389
400
|
|
|
401
|
+
// Did the SERVER actually answer with a total for the measure asked for?
|
|
402
|
+
// This is NOT the same question as "is a totals row on screen": the row is
|
|
403
|
+
// rendered as a dash when the owner asked and nothing came back, and a dash
|
|
404
|
+
// is a placeholder, not an answer. Keeping the two apart is what stops a
|
|
405
|
+
// placeholder from counting as content — see `hideTable`/`noEmpty` below.
|
|
406
|
+
//
|
|
407
|
+
// A zero is unaffected: `cellText` formats it as "0", not as the dash, and a
|
|
408
|
+
// zero total is a real answer that must keep the table up.
|
|
409
|
+
// Asked of `cellText` rather than re-derived: a `SemanticValue` is an OBJECT
|
|
410
|
+
// even when it carries `value: null`, so a nullish check on the entry itself
|
|
411
|
+
// calls a valueless total an answer. `cellText` already owns the decision of
|
|
412
|
+
// what counts as showable, down to `formatted: ""`, so the question is simply
|
|
413
|
+
// whether it produced the placeholder.
|
|
414
|
+
const serverTotal = result.totals?.[s.measureName];
|
|
415
|
+
const totalText = cellText(serverTotal);
|
|
416
|
+
const answeredTotal = totalText !== DASH;
|
|
417
|
+
|
|
390
418
|
// `noTotals` is the owner's REQUEST and stays theirs; only they change it.
|
|
391
419
|
// When they asked and the server sent no total, that is reported as a dash —
|
|
392
420
|
// an absent number the owner asked for must be visible, not silently dropped
|
|
@@ -395,9 +423,12 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
395
423
|
? null
|
|
396
424
|
: {
|
|
397
425
|
dimension: copyParam("ownerReport.totalsLabel", "Total"),
|
|
398
|
-
measure:
|
|
426
|
+
measure: totalText,
|
|
399
427
|
};
|
|
400
428
|
|
|
429
|
+
// Is there anything to READ? Rows, or a total the server actually sent.
|
|
430
|
+
const hasContent = rows.length > 0 || answeredTotal;
|
|
431
|
+
|
|
401
432
|
const out: ReportState = {
|
|
402
433
|
...s,
|
|
403
434
|
status: "ready",
|
|
@@ -405,16 +436,22 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
405
436
|
+ `${s.measureLabel} ${copyParam("ownerReport.by", "by")} ${s.dimensionLabel}`,
|
|
406
437
|
rows,
|
|
407
438
|
totals,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
439
|
+
// A zero count is stated, not left blank. Silence beside a caption reads
|
|
440
|
+
// as "still loading" — the ambiguity this screen removes everywhere else —
|
|
441
|
+
// so "0 rows" says the query ran and came back with nothing.
|
|
442
|
+
rowCount: `${rows.length > 0 ? result.paging.count || rows.length : 0} ${copyParam("ownerReport.rows", "rows")}`,
|
|
411
443
|
missingFields: gap,
|
|
412
444
|
noGap: gap === "",
|
|
413
|
-
// Rows OR
|
|
414
|
-
// — the SDK has a test for exactly that shape — and gating
|
|
415
|
-
// hid the server's total and put "Nothing to show" over it.
|
|
416
|
-
//
|
|
417
|
-
|
|
445
|
+
// Rows OR a total the SERVER sent. The API can answer `results: []` WITH a
|
|
446
|
+
// `totals` object — the SDK has a test for exactly that shape — and gating
|
|
447
|
+
// on rows alone hid the server's total and put "Nothing to show" over it.
|
|
448
|
+
//
|
|
449
|
+
// But it must be `answeredTotal`, not `totals`: `totals` is non-null for
|
|
450
|
+
// the whole time the owner has Show total on, INCLUDING when the answer is
|
|
451
|
+
// the "—" placeholder. Gating on it meant an empty result with the toggle
|
|
452
|
+
// on rendered a table whose only row was `Total —`, and the empty state
|
|
453
|
+
// could never fire while the toggle was on. Two states each correct alone.
|
|
454
|
+
hideTable: !hasContent,
|
|
418
455
|
notLoading: true,
|
|
419
456
|
notErrored: true,
|
|
420
457
|
hideContent: false,
|
|
@@ -423,10 +460,10 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
423
460
|
// It is NOT conflated with a dropped field: the SDK reports no
|
|
424
461
|
// `missingFields` when nothing came back at all, precisely because the two
|
|
425
462
|
// are indistinguishable there, so this arm never accuses a field.
|
|
426
|
-
noEmpty:
|
|
427
|
-
emptyText:
|
|
428
|
-
?
|
|
429
|
-
: "",
|
|
463
|
+
noEmpty: hasContent,
|
|
464
|
+
emptyText: hasContent
|
|
465
|
+
? ""
|
|
466
|
+
: copyParam("ownerReport.empty", "No data for this range."),
|
|
430
467
|
// Carried through untouched: see the totals comment above.
|
|
431
468
|
noTotals: s.noTotals,
|
|
432
469
|
error: "",
|
|
@@ -181,11 +181,22 @@ function subjectLabelOf(m: SemanticModelSummary): string {
|
|
|
181
181
|
* renders as a dash rather than as 0. `formatNumber` and not `toLocaleString`,
|
|
182
182
|
* which is ECMA-402 and absent on QuickJS-ng (watchOS/Wear OS).
|
|
183
183
|
*/
|
|
184
|
+
/**
|
|
185
|
+
* The one placeholder a cell falls back to when there is nothing to render.
|
|
186
|
+
*
|
|
187
|
+
* Named, because two things depend on it meaning exactly one thing: `cellText`
|
|
188
|
+
* produces it, and `runQuery` asks whether the server's total came back as it —
|
|
189
|
+
* which is how "the owner asked for a total and got a dash" is told apart from
|
|
190
|
+
* "the server answered". Spelling it twice let those two drift apart once
|
|
191
|
+
* already.
|
|
192
|
+
*/
|
|
193
|
+
const DASH = "\u2014";
|
|
194
|
+
|
|
184
195
|
function cellText(v: SemanticValue | undefined): string {
|
|
185
|
-
if (!v) return
|
|
196
|
+
if (!v) return DASH;
|
|
186
197
|
if (v.formatted !== undefined && v.formatted !== "") return v.formatted;
|
|
187
198
|
const raw = v.value;
|
|
188
|
-
if (raw === null || raw === undefined) return
|
|
199
|
+
if (raw === null || raw === undefined) return DASH;
|
|
189
200
|
if (typeof raw === "number") return formatNumber(raw);
|
|
190
201
|
if (typeof raw === "boolean") return raw ? "Yes" : "No";
|
|
191
202
|
if (typeof raw === "string") return raw;
|
|
@@ -387,6 +398,23 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
387
398
|
? copyParam("ownerReport.missingFields", "No data returned for: ") + result.missingFields.join(", ")
|
|
388
399
|
: "";
|
|
389
400
|
|
|
401
|
+
// Did the SERVER actually answer with a total for the measure asked for?
|
|
402
|
+
// This is NOT the same question as "is a totals row on screen": the row is
|
|
403
|
+
// rendered as a dash when the owner asked and nothing came back, and a dash
|
|
404
|
+
// is a placeholder, not an answer. Keeping the two apart is what stops a
|
|
405
|
+
// placeholder from counting as content — see `hideTable`/`noEmpty` below.
|
|
406
|
+
//
|
|
407
|
+
// A zero is unaffected: `cellText` formats it as "0", not as the dash, and a
|
|
408
|
+
// zero total is a real answer that must keep the table up.
|
|
409
|
+
// Asked of `cellText` rather than re-derived: a `SemanticValue` is an OBJECT
|
|
410
|
+
// even when it carries `value: null`, so a nullish check on the entry itself
|
|
411
|
+
// calls a valueless total an answer. `cellText` already owns the decision of
|
|
412
|
+
// what counts as showable, down to `formatted: ""`, so the question is simply
|
|
413
|
+
// whether it produced the placeholder.
|
|
414
|
+
const serverTotal = result.totals?.[s.measureName];
|
|
415
|
+
const totalText = cellText(serverTotal);
|
|
416
|
+
const answeredTotal = totalText !== DASH;
|
|
417
|
+
|
|
390
418
|
// `noTotals` is the owner's REQUEST and stays theirs; only they change it.
|
|
391
419
|
// When they asked and the server sent no total, that is reported as a dash —
|
|
392
420
|
// an absent number the owner asked for must be visible, not silently dropped
|
|
@@ -395,9 +423,12 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
395
423
|
? null
|
|
396
424
|
: {
|
|
397
425
|
dimension: copyParam("ownerReport.totalsLabel", "Total"),
|
|
398
|
-
measure:
|
|
426
|
+
measure: totalText,
|
|
399
427
|
};
|
|
400
428
|
|
|
429
|
+
// Is there anything to READ? Rows, or a total the server actually sent.
|
|
430
|
+
const hasContent = rows.length > 0 || answeredTotal;
|
|
431
|
+
|
|
401
432
|
const out: ReportState = {
|
|
402
433
|
...s,
|
|
403
434
|
status: "ready",
|
|
@@ -405,16 +436,22 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
405
436
|
+ `${s.measureLabel} ${copyParam("ownerReport.by", "by")} ${s.dimensionLabel}`,
|
|
406
437
|
rows,
|
|
407
438
|
totals,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
439
|
+
// A zero count is stated, not left blank. Silence beside a caption reads
|
|
440
|
+
// as "still loading" — the ambiguity this screen removes everywhere else —
|
|
441
|
+
// so "0 rows" says the query ran and came back with nothing.
|
|
442
|
+
rowCount: `${rows.length > 0 ? result.paging.count || rows.length : 0} ${copyParam("ownerReport.rows", "rows")}`,
|
|
411
443
|
missingFields: gap,
|
|
412
444
|
noGap: gap === "",
|
|
413
|
-
// Rows OR
|
|
414
|
-
// — the SDK has a test for exactly that shape — and gating
|
|
415
|
-
// hid the server's total and put "Nothing to show" over it.
|
|
416
|
-
//
|
|
417
|
-
|
|
445
|
+
// Rows OR a total the SERVER sent. The API can answer `results: []` WITH a
|
|
446
|
+
// `totals` object — the SDK has a test for exactly that shape — and gating
|
|
447
|
+
// on rows alone hid the server's total and put "Nothing to show" over it.
|
|
448
|
+
//
|
|
449
|
+
// But it must be `answeredTotal`, not `totals`: `totals` is non-null for
|
|
450
|
+
// the whole time the owner has Show total on, INCLUDING when the answer is
|
|
451
|
+
// the "—" placeholder. Gating on it meant an empty result with the toggle
|
|
452
|
+
// on rendered a table whose only row was `Total —`, and the empty state
|
|
453
|
+
// could never fire while the toggle was on. Two states each correct alone.
|
|
454
|
+
hideTable: !hasContent,
|
|
418
455
|
notLoading: true,
|
|
419
456
|
notErrored: true,
|
|
420
457
|
hideContent: false,
|
|
@@ -423,10 +460,10 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
423
460
|
// It is NOT conflated with a dropped field: the SDK reports no
|
|
424
461
|
// `missingFields` when nothing came back at all, precisely because the two
|
|
425
462
|
// are indistinguishable there, so this arm never accuses a field.
|
|
426
|
-
noEmpty:
|
|
427
|
-
emptyText:
|
|
428
|
-
?
|
|
429
|
-
: "",
|
|
463
|
+
noEmpty: hasContent,
|
|
464
|
+
emptyText: hasContent
|
|
465
|
+
? ""
|
|
466
|
+
: copyParam("ownerReport.empty", "No data for this range."),
|
|
430
467
|
// Carried through untouched: see the totals comment above.
|
|
431
468
|
noTotals: s.noTotals,
|
|
432
469
|
error: "",
|
|
@@ -181,11 +181,22 @@ function subjectLabelOf(m: SemanticModelSummary): string {
|
|
|
181
181
|
* renders as a dash rather than as 0. `formatNumber` and not `toLocaleString`,
|
|
182
182
|
* which is ECMA-402 and absent on QuickJS-ng (watchOS/Wear OS).
|
|
183
183
|
*/
|
|
184
|
+
/**
|
|
185
|
+
* The one placeholder a cell falls back to when there is nothing to render.
|
|
186
|
+
*
|
|
187
|
+
* Named, because two things depend on it meaning exactly one thing: `cellText`
|
|
188
|
+
* produces it, and `runQuery` asks whether the server's total came back as it —
|
|
189
|
+
* which is how "the owner asked for a total and got a dash" is told apart from
|
|
190
|
+
* "the server answered". Spelling it twice let those two drift apart once
|
|
191
|
+
* already.
|
|
192
|
+
*/
|
|
193
|
+
const DASH = "\u2014";
|
|
194
|
+
|
|
184
195
|
function cellText(v: SemanticValue | undefined): string {
|
|
185
|
-
if (!v) return
|
|
196
|
+
if (!v) return DASH;
|
|
186
197
|
if (v.formatted !== undefined && v.formatted !== "") return v.formatted;
|
|
187
198
|
const raw = v.value;
|
|
188
|
-
if (raw === null || raw === undefined) return
|
|
199
|
+
if (raw === null || raw === undefined) return DASH;
|
|
189
200
|
if (typeof raw === "number") return formatNumber(raw);
|
|
190
201
|
if (typeof raw === "boolean") return raw ? "Yes" : "No";
|
|
191
202
|
if (typeof raw === "string") return raw;
|
|
@@ -387,6 +398,23 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
387
398
|
? copyParam("ownerReport.missingFields", "No data returned for: ") + result.missingFields.join(", ")
|
|
388
399
|
: "";
|
|
389
400
|
|
|
401
|
+
// Did the SERVER actually answer with a total for the measure asked for?
|
|
402
|
+
// This is NOT the same question as "is a totals row on screen": the row is
|
|
403
|
+
// rendered as a dash when the owner asked and nothing came back, and a dash
|
|
404
|
+
// is a placeholder, not an answer. Keeping the two apart is what stops a
|
|
405
|
+
// placeholder from counting as content — see `hideTable`/`noEmpty` below.
|
|
406
|
+
//
|
|
407
|
+
// A zero is unaffected: `cellText` formats it as "0", not as the dash, and a
|
|
408
|
+
// zero total is a real answer that must keep the table up.
|
|
409
|
+
// Asked of `cellText` rather than re-derived: a `SemanticValue` is an OBJECT
|
|
410
|
+
// even when it carries `value: null`, so a nullish check on the entry itself
|
|
411
|
+
// calls a valueless total an answer. `cellText` already owns the decision of
|
|
412
|
+
// what counts as showable, down to `formatted: ""`, so the question is simply
|
|
413
|
+
// whether it produced the placeholder.
|
|
414
|
+
const serverTotal = result.totals?.[s.measureName];
|
|
415
|
+
const totalText = cellText(serverTotal);
|
|
416
|
+
const answeredTotal = totalText !== DASH;
|
|
417
|
+
|
|
390
418
|
// `noTotals` is the owner's REQUEST and stays theirs; only they change it.
|
|
391
419
|
// When they asked and the server sent no total, that is reported as a dash —
|
|
392
420
|
// an absent number the owner asked for must be visible, not silently dropped
|
|
@@ -395,9 +423,12 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
395
423
|
? null
|
|
396
424
|
: {
|
|
397
425
|
dimension: copyParam("ownerReport.totalsLabel", "Total"),
|
|
398
|
-
measure:
|
|
426
|
+
measure: totalText,
|
|
399
427
|
};
|
|
400
428
|
|
|
429
|
+
// Is there anything to READ? Rows, or a total the server actually sent.
|
|
430
|
+
const hasContent = rows.length > 0 || answeredTotal;
|
|
431
|
+
|
|
401
432
|
const out: ReportState = {
|
|
402
433
|
...s,
|
|
403
434
|
status: "ready",
|
|
@@ -405,16 +436,22 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
405
436
|
+ `${s.measureLabel} ${copyParam("ownerReport.by", "by")} ${s.dimensionLabel}`,
|
|
406
437
|
rows,
|
|
407
438
|
totals,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
439
|
+
// A zero count is stated, not left blank. Silence beside a caption reads
|
|
440
|
+
// as "still loading" — the ambiguity this screen removes everywhere else —
|
|
441
|
+
// so "0 rows" says the query ran and came back with nothing.
|
|
442
|
+
rowCount: `${rows.length > 0 ? result.paging.count || rows.length : 0} ${copyParam("ownerReport.rows", "rows")}`,
|
|
411
443
|
missingFields: gap,
|
|
412
444
|
noGap: gap === "",
|
|
413
|
-
// Rows OR
|
|
414
|
-
// — the SDK has a test for exactly that shape — and gating
|
|
415
|
-
// hid the server's total and put "Nothing to show" over it.
|
|
416
|
-
//
|
|
417
|
-
|
|
445
|
+
// Rows OR a total the SERVER sent. The API can answer `results: []` WITH a
|
|
446
|
+
// `totals` object — the SDK has a test for exactly that shape — and gating
|
|
447
|
+
// on rows alone hid the server's total and put "Nothing to show" over it.
|
|
448
|
+
//
|
|
449
|
+
// But it must be `answeredTotal`, not `totals`: `totals` is non-null for
|
|
450
|
+
// the whole time the owner has Show total on, INCLUDING when the answer is
|
|
451
|
+
// the "—" placeholder. Gating on it meant an empty result with the toggle
|
|
452
|
+
// on rendered a table whose only row was `Total —`, and the empty state
|
|
453
|
+
// could never fire while the toggle was on. Two states each correct alone.
|
|
454
|
+
hideTable: !hasContent,
|
|
418
455
|
notLoading: true,
|
|
419
456
|
notErrored: true,
|
|
420
457
|
hideContent: false,
|
|
@@ -423,10 +460,10 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
423
460
|
// It is NOT conflated with a dropped field: the SDK reports no
|
|
424
461
|
// `missingFields` when nothing came back at all, precisely because the two
|
|
425
462
|
// are indistinguishable there, so this arm never accuses a field.
|
|
426
|
-
noEmpty:
|
|
427
|
-
emptyText:
|
|
428
|
-
?
|
|
429
|
-
: "",
|
|
463
|
+
noEmpty: hasContent,
|
|
464
|
+
emptyText: hasContent
|
|
465
|
+
? ""
|
|
466
|
+
: copyParam("ownerReport.empty", "No data for this range."),
|
|
430
467
|
// Carried through untouched: see the totals comment above.
|
|
431
468
|
noTotals: s.noTotals,
|
|
432
469
|
error: "",
|
|
@@ -181,11 +181,22 @@ function subjectLabelOf(m: SemanticModelSummary): string {
|
|
|
181
181
|
* renders as a dash rather than as 0. `formatNumber` and not `toLocaleString`,
|
|
182
182
|
* which is ECMA-402 and absent on QuickJS-ng (watchOS/Wear OS).
|
|
183
183
|
*/
|
|
184
|
+
/**
|
|
185
|
+
* The one placeholder a cell falls back to when there is nothing to render.
|
|
186
|
+
*
|
|
187
|
+
* Named, because two things depend on it meaning exactly one thing: `cellText`
|
|
188
|
+
* produces it, and `runQuery` asks whether the server's total came back as it —
|
|
189
|
+
* which is how "the owner asked for a total and got a dash" is told apart from
|
|
190
|
+
* "the server answered". Spelling it twice let those two drift apart once
|
|
191
|
+
* already.
|
|
192
|
+
*/
|
|
193
|
+
const DASH = "\u2014";
|
|
194
|
+
|
|
184
195
|
function cellText(v: SemanticValue | undefined): string {
|
|
185
|
-
if (!v) return
|
|
196
|
+
if (!v) return DASH;
|
|
186
197
|
if (v.formatted !== undefined && v.formatted !== "") return v.formatted;
|
|
187
198
|
const raw = v.value;
|
|
188
|
-
if (raw === null || raw === undefined) return
|
|
199
|
+
if (raw === null || raw === undefined) return DASH;
|
|
189
200
|
if (typeof raw === "number") return formatNumber(raw);
|
|
190
201
|
if (typeof raw === "boolean") return raw ? "Yes" : "No";
|
|
191
202
|
if (typeof raw === "string") return raw;
|
|
@@ -387,6 +398,23 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
387
398
|
? copyParam("ownerReport.missingFields", "No data returned for: ") + result.missingFields.join(", ")
|
|
388
399
|
: "";
|
|
389
400
|
|
|
401
|
+
// Did the SERVER actually answer with a total for the measure asked for?
|
|
402
|
+
// This is NOT the same question as "is a totals row on screen": the row is
|
|
403
|
+
// rendered as a dash when the owner asked and nothing came back, and a dash
|
|
404
|
+
// is a placeholder, not an answer. Keeping the two apart is what stops a
|
|
405
|
+
// placeholder from counting as content — see `hideTable`/`noEmpty` below.
|
|
406
|
+
//
|
|
407
|
+
// A zero is unaffected: `cellText` formats it as "0", not as the dash, and a
|
|
408
|
+
// zero total is a real answer that must keep the table up.
|
|
409
|
+
// Asked of `cellText` rather than re-derived: a `SemanticValue` is an OBJECT
|
|
410
|
+
// even when it carries `value: null`, so a nullish check on the entry itself
|
|
411
|
+
// calls a valueless total an answer. `cellText` already owns the decision of
|
|
412
|
+
// what counts as showable, down to `formatted: ""`, so the question is simply
|
|
413
|
+
// whether it produced the placeholder.
|
|
414
|
+
const serverTotal = result.totals?.[s.measureName];
|
|
415
|
+
const totalText = cellText(serverTotal);
|
|
416
|
+
const answeredTotal = totalText !== DASH;
|
|
417
|
+
|
|
390
418
|
// `noTotals` is the owner's REQUEST and stays theirs; only they change it.
|
|
391
419
|
// When they asked and the server sent no total, that is reported as a dash —
|
|
392
420
|
// an absent number the owner asked for must be visible, not silently dropped
|
|
@@ -395,9 +423,12 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
395
423
|
? null
|
|
396
424
|
: {
|
|
397
425
|
dimension: copyParam("ownerReport.totalsLabel", "Total"),
|
|
398
|
-
measure:
|
|
426
|
+
measure: totalText,
|
|
399
427
|
};
|
|
400
428
|
|
|
429
|
+
// Is there anything to READ? Rows, or a total the server actually sent.
|
|
430
|
+
const hasContent = rows.length > 0 || answeredTotal;
|
|
431
|
+
|
|
401
432
|
const out: ReportState = {
|
|
402
433
|
...s,
|
|
403
434
|
status: "ready",
|
|
@@ -405,16 +436,22 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
405
436
|
+ `${s.measureLabel} ${copyParam("ownerReport.by", "by")} ${s.dimensionLabel}`,
|
|
406
437
|
rows,
|
|
407
438
|
totals,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
439
|
+
// A zero count is stated, not left blank. Silence beside a caption reads
|
|
440
|
+
// as "still loading" — the ambiguity this screen removes everywhere else —
|
|
441
|
+
// so "0 rows" says the query ran and came back with nothing.
|
|
442
|
+
rowCount: `${rows.length > 0 ? result.paging.count || rows.length : 0} ${copyParam("ownerReport.rows", "rows")}`,
|
|
411
443
|
missingFields: gap,
|
|
412
444
|
noGap: gap === "",
|
|
413
|
-
// Rows OR
|
|
414
|
-
// — the SDK has a test for exactly that shape — and gating
|
|
415
|
-
// hid the server's total and put "Nothing to show" over it.
|
|
416
|
-
//
|
|
417
|
-
|
|
445
|
+
// Rows OR a total the SERVER sent. The API can answer `results: []` WITH a
|
|
446
|
+
// `totals` object — the SDK has a test for exactly that shape — and gating
|
|
447
|
+
// on rows alone hid the server's total and put "Nothing to show" over it.
|
|
448
|
+
//
|
|
449
|
+
// But it must be `answeredTotal`, not `totals`: `totals` is non-null for
|
|
450
|
+
// the whole time the owner has Show total on, INCLUDING when the answer is
|
|
451
|
+
// the "—" placeholder. Gating on it meant an empty result with the toggle
|
|
452
|
+
// on rendered a table whose only row was `Total —`, and the empty state
|
|
453
|
+
// could never fire while the toggle was on. Two states each correct alone.
|
|
454
|
+
hideTable: !hasContent,
|
|
418
455
|
notLoading: true,
|
|
419
456
|
notErrored: true,
|
|
420
457
|
hideContent: false,
|
|
@@ -423,10 +460,10 @@ async function runQuery(s: ReportState, days: number): Promise<ReportState | Dec
|
|
|
423
460
|
// It is NOT conflated with a dropped field: the SDK reports no
|
|
424
461
|
// `missingFields` when nothing came back at all, precisely because the two
|
|
425
462
|
// are indistinguishable there, so this arm never accuses a field.
|
|
426
|
-
noEmpty:
|
|
427
|
-
emptyText:
|
|
428
|
-
?
|
|
429
|
-
: "",
|
|
463
|
+
noEmpty: hasContent,
|
|
464
|
+
emptyText: hasContent
|
|
465
|
+
? ""
|
|
466
|
+
: copyParam("ownerReport.empty", "No data for this range."),
|
|
430
467
|
// Carried through untouched: see the totals comment above.
|
|
431
468
|
noTotals: s.noTotals,
|
|
432
469
|
error: "",
|