@teamscala/service-boot 0.23.376 → 0.23.378
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/package.json
CHANGED
|
@@ -85,3 +85,45 @@ export function resolveTemplateTitle(
|
|
|
85
85
|
});
|
|
86
86
|
return result || undefined;
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* THE ONE SUBJECT DERIVATION. What a page is ABOUT — the entity whose name the
|
|
91
|
+
* browser tab should carry.
|
|
92
|
+
*
|
|
93
|
+
* This existed as THREE hand-written `||` chains, one per render branch in
|
|
94
|
+
* resolve-page-data, and they had already diverged: two ordered form before
|
|
95
|
+
* proposal, the third ordered proposal first. So which resource named the page
|
|
96
|
+
* depended on which branch happened to render it, silently.
|
|
97
|
+
*
|
|
98
|
+
* The divergence is the cheap half of the cost. The expensive half is that a
|
|
99
|
+
* correction has to be applied three times to work, and applying it twice looks
|
|
100
|
+
* exactly like applying it correctly — measured 2026-08-17, when a fix to the
|
|
101
|
+
* quiz subject was shipped, installed and verified present in the bundle while
|
|
102
|
+
* the page kept its old title, because the branch that rendered it held the
|
|
103
|
+
* second copy.
|
|
104
|
+
*
|
|
105
|
+
* One chain, one precedence, one place to change. Adding a resource type is an
|
|
106
|
+
* edit here rather than a fourth branch in three files.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveEntityTitle(resolved: Record<string, unknown>): string | undefined {
|
|
109
|
+
const pick = (key: string, ...fields: string[]): string | undefined => {
|
|
110
|
+
const row = resolved[key] as Record<string, unknown> | undefined;
|
|
111
|
+
if (!row) return undefined;
|
|
112
|
+
for (const field of fields) {
|
|
113
|
+
const value = row[field];
|
|
114
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
};
|
|
118
|
+
return (
|
|
119
|
+
pick("form", "name") ||
|
|
120
|
+
pick("proposal", "name") ||
|
|
121
|
+
// A report surface carries its own document name in `report_title`
|
|
122
|
+
// ("Personalised Authority Blueprint"); `title` names the DIAGNOSTIC
|
|
123
|
+
// ("The Launcha Authority Index"), which is the wrong subject for the tab
|
|
124
|
+
// of a document sent to a client. The landing page has no report_title
|
|
125
|
+
// and correctly falls through to the quiz title.
|
|
126
|
+
pick("quiz", "report_title", "title") ||
|
|
127
|
+
pick("agent", "name")
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Locks the single subject derivation that replaced three divergent copies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { resolveEntityTitle } from "./meta-title.ts";
|
|
11
|
+
|
|
12
|
+
describe("resolveEntityTitle", () => {
|
|
13
|
+
test("a report is named by its own document name, not the diagnostic", () => {
|
|
14
|
+
// The Launcha Blueprint: `title` names the QUIZ, `report_title` names the
|
|
15
|
+
// DOCUMENT the client receives. The tab must carry the document.
|
|
16
|
+
expect(
|
|
17
|
+
resolveEntityTitle({
|
|
18
|
+
quiz: {
|
|
19
|
+
title: "The Launcha Authority Index",
|
|
20
|
+
report_title: "Personalised Authority Blueprint",
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
).toBe("Personalised Authority Blueprint");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a quiz with no report name falls through to its title", () => {
|
|
27
|
+
expect(resolveEntityTitle({ quiz: { title: "The Launcha Authority Index" } })).toBe(
|
|
28
|
+
"The Launcha Authority Index",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("precedence is single and fixed across every caller", () => {
|
|
33
|
+
// The three former copies disagreed: two put form first, one put proposal
|
|
34
|
+
// first, so the subject depended on which render branch ran. One order now.
|
|
35
|
+
const all = {
|
|
36
|
+
form: { name: "Form" },
|
|
37
|
+
proposal: { name: "Proposal" },
|
|
38
|
+
quiz: { report_title: "Report" },
|
|
39
|
+
agent: { name: "Agent" },
|
|
40
|
+
};
|
|
41
|
+
expect(resolveEntityTitle(all)).toBe("Form");
|
|
42
|
+
expect(resolveEntityTitle({ ...all, form: undefined })).toBe("Proposal");
|
|
43
|
+
expect(resolveEntityTitle({ ...all, form: undefined, proposal: undefined })).toBe("Report");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("blank and whitespace-only names do not win", () => {
|
|
47
|
+
expect(resolveEntityTitle({ form: { name: " " }, agent: { name: "Agent" } })).toBe("Agent");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("undefined when nothing names the page", () => {
|
|
51
|
+
expect(resolveEntityTitle({})).toBeUndefined();
|
|
52
|
+
expect(resolveEntityTitle({ quiz: {} })).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { resolveThemeMode } from "../resolve-theme-mode.ts";
|
|
12
12
|
import { composePageTitle } from "../compose-page-title.ts";
|
|
13
|
+
import { resolveEntityTitle } from "./meta-title.ts";
|
|
13
14
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -124,11 +125,7 @@ export function resolvedToPageData(
|
|
|
124
125
|
// name in <title>/OG, not the generic "Portal". Falls back to the website
|
|
125
126
|
// name, then "Portal". Without this, the region-slots branch returned
|
|
126
127
|
// early with a hardcoded "Portal" title (gpt-test: SSR/DOM/OG title FAIL).
|
|
127
|
-
const entityTitle =
|
|
128
|
-
(resolved.form as { name?: string })?.name ||
|
|
129
|
-
(resolved.quiz as { title?: string })?.title ||
|
|
130
|
-
(resolved.proposal as { name?: string })?.name ||
|
|
131
|
-
(resolved.agent as { name?: string })?.name;
|
|
128
|
+
const entityTitle = resolveEntityTitle(resolved);
|
|
132
129
|
|
|
133
130
|
const regions: RegionEntry[] = [];
|
|
134
131
|
for (const slot of regionSlots) {
|
|
@@ -306,11 +303,7 @@ export function resolvedToPageData(
|
|
|
306
303
|
const overrideBody = pickRootProp<string>(regions, "bodyFont");
|
|
307
304
|
|
|
308
305
|
const pageName = (page.name as string) || null;
|
|
309
|
-
const entityTitle =
|
|
310
|
-
(resolved.proposal as { name?: string })?.name ||
|
|
311
|
-
(resolved.form as { name?: string })?.name ||
|
|
312
|
-
(resolved.quiz as { title?: string })?.title ||
|
|
313
|
-
(resolved.agent as { name?: string })?.name;
|
|
306
|
+
const entityTitle = resolveEntityTitle(resolved);
|
|
314
307
|
const bodyMetaTitle = regions.find((r) => r.slot === "body")?.metaTitle;
|
|
315
308
|
const pageRootTitle = pickRootProp<string>(regions, "title");
|
|
316
309
|
const pageDescription =
|
|
@@ -438,11 +431,7 @@ export function resolvedToPageData(
|
|
|
438
431
|
const screenMode = pageRoot?.screenMode as string | undefined;
|
|
439
432
|
|
|
440
433
|
const pageName = (page?.name as string) || null;
|
|
441
|
-
const entityTitle =
|
|
442
|
-
(resolved.form as { name?: string })?.name ||
|
|
443
|
-
(resolved.quiz as { title?: string })?.title ||
|
|
444
|
-
(resolved.proposal as { name?: string })?.name ||
|
|
445
|
-
(resolved.agent as { name?: string })?.name;
|
|
434
|
+
const entityTitle = resolveEntityTitle(resolved);
|
|
446
435
|
const entityDescription =
|
|
447
436
|
((resolved.form as Record<string, unknown>)?.description as string) ||
|
|
448
437
|
((resolved.quiz as Record<string, unknown>)?.description as string) ||
|