@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
|
@@ -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: "",
|