@wix/himalaya-cli 0.860.0 → 0.862.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 -24
- package/guides/mobile-ux.md +14 -6
- package/guides/owner-lite-site-binding.md +5 -0
- 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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.862.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": "988c32c342c4858fe88297637a696b7452fae42ee373a75995e64c36"
|
|
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: "",
|
|
@@ -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: "",
|