Skip to content

Commit 8194907

Browse files
committed
PR_26174_ALFA_010-game-hub-source-idea-child-table-polish
1 parent 06a1b61 commit 8194907

9 files changed

Lines changed: 200 additions & 183 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Branch Validation: PASS
2+
3+
PASS - Current branch: pr/26174-ALFA-010-game-hub-source-idea-child-table-polish.
4+
PASS - Stack base: pr/26174-ALFA-009-game-hub-parent-child-table-layout.
5+
PASS - Changes are scoped to Source Idea child table rendering, targeted tests, and required reports.
6+
PASS - No merge to main performed.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Manual Validation Notes: PASS
2+
3+
PASS - Confirmed the Idea Board-created Lantern Reef project expands in Game Hub.
4+
PASS - Confirmed Source Idea child table shows Idea, Pitch, and Note 1 rows.
5+
PASS - Confirmed Source Idea child table has no input, textarea, select, or button controls.
6+
PASS - Confirmed source details still come from the existing Game Hub project record contract.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Requirement Checklist: PASS
2+
3+
PASS - Source Idea appears as a child table under the game parent row.
4+
PASS - Source Idea includes source idea details.
5+
PASS - Source Idea includes notes as read-only child/context rows where data supports it.
6+
PASS - Source Idea is read-only.
7+
PASS - No edit/delete controls for source idea context.
8+
PASS - Creator-facing labels are clear and non-technical.
9+
PASS - Existing API/service contract is preserved.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Validation Lane: PASS
2+
3+
PASS - npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -g "Idea Board uses accordion table ideas and notes"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# PR_26174_ALFA_010-game-hub-source-idea-child-table-polish
2+
3+
## Purpose
4+
5+
Make Source Idea a dedicated child table under the expanded game row.
6+
7+
## Summary
8+
9+
- Added a Source Idea child table to expanded Game Hub parent rows.
10+
- Displays source idea, pitch, and read-only source note rows when data is present.
11+
- Kept source idea context free of edit/delete controls.
12+
- Preserved the existing Local API/service contract and source idea normalization.
13+
14+
## Validation
15+
16+
PASS - `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -g "Idea Board uses accordion table ideas and notes"`
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
toolbox/game-hub/game-hub.js
2-
tests/playwright/tools/GameHubMockRepository.spec.mjs
2+
tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
33
docs_build/dev/reports/codex_review.diff
44
docs_build/dev/reports/codex_changed_files.txt
5-
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout.md
6-
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-branch-validation.txt
7-
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-requirement-checklist.txt
8-
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-validation-lane.txt
9-
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-manual-validation-notes.txt
5+
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish.md
6+
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-branch-validation.txt
7+
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-requirement-checklist.txt
8+
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-validation-lane.txt
9+
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-manual-validation-notes.txt
Lines changed: 88 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,195 +1,116 @@
1-
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
2-
index e11631269..695b0a609 100644
3-
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
4-
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
5-
@@ -264,10 +264,32 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
6-
await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo");
7-
await expect(page.locator("[data-game-list]")).toContainText("Collision Demo");
8-
await expect(page.locator("[data-game-list]")).toContainText("Camera Follow Demo");
9-
+ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games");
10-
+ await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games");
11-
+ await expect(page.locator("[data-game-parent-table='open-games'] thead th")).toHaveText([
12-
+ "Game",
13-
+ "Purpose",
14-
+ "Status",
15-
+ "Owner",
16-
+ "Actions",
1+
diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
2+
index 722595ae9..f861f1f44 100644
3+
--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
4+
+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
5+
@@ -392,6 +392,18 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
6+
await expect(page.locator("[data-source-idea-notes]")).toContainText("Use dusk tide changes as the first Game Hub planning note.");
7+
await expect(page.locator("[data-source-idea-section] :is(input, textarea, select, button)")).toHaveCount(0);
8+
await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0);
9+
+ const activeGameRow = page.locator("[data-game-row][data-game-active='true']");
10+
+ await expect(activeGameRow).toContainText("Lantern Reef");
11+
+ await activeGameRow.locator("[data-game-toggle]").click();
12+
+ const sourceIdeaChildTable = page.locator("[data-game-expanded-row] [data-game-child-table='source-idea']");
13+
+ await expect(sourceIdeaChildTable.locator("caption")).toHaveText("Source Idea");
14+
+ await expect(sourceIdeaChildTable.locator("thead th")).toHaveText(["Context", "Details"]);
15+
+ await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([
16+
+ "IdeaLantern Reef",
17+
+ "PitchGuide light through a reef that rearranges at dusk.",
18+
+ "Note 1Use dusk tide changes as the first Game Hub planning note.",
1719
+ ]);
18-
const demoGameRow = page.locator("[data-game-row='demo-game']");
19-
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
20-
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
21-
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveClass(/primary/);
22-
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveAttribute("aria-current", "true");
23-
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
24-
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true");
25-
+ await expect(page.locator("[data-game-row='demo-game'] + [data-game-expanded-row='demo-game']")).toHaveCount(1);
26-
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toContainText("Game Summary");
27-
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary'] tbody tr")).toHaveText([
28-
+ "ProjectDemo Game",
29-
+ "PurposeGame",
30-
+ "StatusUnder Construction",
31-
+ "OwnerUser 1",
32-
+ ]);
33-
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
34-
+ await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0);
35-
36-
await page.getByLabel("Game Name").fill("Launch Test Game");
37-
await page.getByRole("button", { name: "Create Game" }).click();
20+
+ await expect(sourceIdeaChildTable.locator(":is(input, textarea, select, button)")).toHaveCount(0);
21+
await page.reload({ waitUntil: "networkidle" });
22+
await expect(page.locator("[data-active-game-name]")).toHaveText("Lantern Reef");
23+
await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef");
3824
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
39-
index 2d6f321e4..121e82562 100644
25+
index 121e82562..67f56ca41 100644
4026
--- a/toolbox/game-hub/game-hub.js
4127
+++ b/toolbox/game-hub/game-hub.js
42-
@@ -39,6 +39,10 @@ const elements = {
43-
tableCounts: document.querySelector("[data-game-table-counts]"),
44-
};
45-
46-
+const state = {
47-
+ expandedGameId: "",
48-
+};
49-
+
50-
function setText(element, value) {
51-
if (element && typeof element.forEach === "function" && !element.nodeType) {
52-
element.forEach((item) => {
53-
@@ -225,6 +229,90 @@ function createGameListStatus(message, state) {
54-
return emptyState;
28+
@@ -271,6 +271,54 @@ function renderGameSummaryChildTable(parent, game) {
29+
parent.append(wrapper);
5530
}
5631

57-
+function createCell(value, tagName = "td") {
58-
+ const cell = document.createElement(tagName);
59-
+ cell.textContent = value;
60-
+ return cell;
32+
+function gameSourceIdeaDetails(game) {
33+
+ const sourceIdea = isRecord(game?.sourceIdea) ? game.sourceIdea : null;
34+
+ const name = String(sourceIdea?.idea || "").trim();
35+
+ const pitch = String(sourceIdea?.pitch || "").trim();
36+
+ const notes = Array.isArray(sourceIdea?.notes)
37+
+ ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean)
38+
+ : [];
39+
+ return {
40+
+ name,
41+
+ notes,
42+
+ pitch,
43+
+ };
6144
+}
6245
+
63-
+function createGameToggleButton(game, expanded) {
64-
+ const button = document.createElement("button");
65-
+ button.className = expanded ? "btn btn--compact primary" : "btn btn--compact";
66-
+ button.type = "button";
67-
+ button.dataset.gameToggle = game.id;
68-
+ button.setAttribute("aria-expanded", String(expanded));
69-
+ button.setAttribute("aria-controls", `game-child-${game.id}`);
70-
+ button.textContent = game.name;
71-
+ return button;
72-
+}
73-
+
74-
+function renderGameSummaryChildTable(parent, game) {
46+
+function renderSourceIdeaChildTable(parent, game) {
47+
+ const sourceIdea = gameSourceIdeaDetails(game);
7548
+ const wrapper = document.createElement("div");
7649
+ wrapper.className = "table-wrapper";
7750
+ const table = document.createElement("table");
7851
+ table.className = "data-table data-table--fixed";
79-
+ table.dataset.gameChildTable = "summary";
80-
+ table.setAttribute("aria-label", `${game.name} game summary`);
81-
+ table.innerHTML = "<caption>Game Summary</caption><thead><tr><th scope=\"col\">Field</th><th scope=\"col\">Value</th></tr></thead>";
52+
+ table.dataset.gameChildTable = "source-idea";
53+
+ table.setAttribute("aria-label", `${game.name} source idea`);
54+
+ table.innerHTML = "<caption>Source Idea</caption><thead><tr><th scope=\"col\">Context</th><th scope=\"col\">Details</th></tr></thead>";
8255
+ const body = document.createElement("tbody");
8356
+ [
84-
+ ["Project", game.name],
85-
+ ["Purpose", game.purpose],
86-
+ ["Status", game.status],
87-
+ ["Owner", game.ownerDisplayName],
57+
+ ["Idea", sourceIdea.name || "No source idea yet"],
58+
+ ["Pitch", sourceIdea.pitch || "Create a project from Idea Board to see source details."],
8859
+ ].forEach(([label, value]) => {
8960
+ const row = document.createElement("tr");
90-
+ row.append(createCell(label, "th"), createCell(value || "Not set"));
61+
+ row.append(createCell(label, "th"), createCell(value));
62+
+ row.firstElementChild.scope = "row";
63+
+ body.append(row);
64+
+ });
65+
+
66+
+ const notes = sourceIdea.notes.length ? sourceIdea.notes : ["No source notes."];
67+
+ notes.forEach((note, index) => {
68+
+ const row = document.createElement("tr");
69+
+ row.dataset.sourceIdeaNoteRow = String(index + 1);
70+
+ row.append(createCell(`Note ${index + 1}`, "th"), createCell(note));
9171
+ row.firstElementChild.scope = "row";
9272
+ body.append(row);
9373
+ });
74+
+
9475
+ table.append(body);
9576
+ wrapper.append(table);
9677
+ parent.append(wrapper);
9778
+}
9879
+
99-
+function renderExpandedGameRow(tbody, game) {
100-
+ const row = document.createElement("tr");
101-
+ row.dataset.gameExpandedRow = game.id;
102-
+ row.id = `game-child-${game.id}`;
103-
+ const content = document.createElement("td");
104-
+ content.colSpan = 5;
105-
+ const stack = document.createElement("div");
106-
+ stack.className = "content-stack content-stack--compact";
107-
+ renderGameSummaryChildTable(stack, game);
108-
+ content.append(stack);
109-
+ row.append(content);
110-
+ tbody.append(row);
111-
+}
112-
+
113-
+function renderGameParentRow(tbody, game, activeGame) {
114-
+ const expanded = state.expandedGameId === game.id;
115-
+ const row = document.createElement("tr");
116-
+ row.dataset.gameRow = game.id;
117-
+ if (activeGame?.id === game.id) {
118-
+ row.dataset.gameActive = "true";
119-
+ }
120-
+
121-
+ const nameCell = document.createElement("th");
122-
+ nameCell.scope = "row";
123-
+ nameCell.append(createGameToggleButton(game, expanded));
124-
+ row.append(
125-
+ nameCell,
126-
+ createCell(game.purpose || "Game"),
127-
+ createCell(game.status || "No status"),
128-
+ createCell(game.ownerDisplayName || "No owner"),
129-
+ );
130-
+
131-
+ const actions = document.createElement("td");
132-
+ actions.append(createGameButton(game, activeGame?.id === game.id));
133-
+ row.append(actions);
134-
+ tbody.append(row);
135-
+
136-
+ if (expanded) {
137-
+ renderExpandedGameRow(tbody, game);
138-
+ }
139-
+}
140-
+
141-
function renderProjectInformation(activeGame, currentMember, progress) {
142-
if (!elements.projectRecordsTable) {
143-
return;
144-
@@ -276,25 +364,18 @@ function renderGameList() {
145-
return;
146-
}
147-
148-
- listResult.forEach((game) => {
149-
- const row = document.createElement("article");
150-
- row.className = "callout";
151-
- row.dataset.gameRow = game.id;
152-
-
153-
- const title = document.createElement("h4");
154-
- title.textContent = game.name;
155-
-
156-
- const meta = document.createElement("p");
157-
- meta.className = "eyebrow";
158-
- meta.textContent = `${game.purpose} | ${game.status} | ${game.ownerDisplayName}`;
159-
-
160-
- const isActive = activeGame?.id === game.id;
161-
- const action = createGameButton(game, isActive);
162-
-
163-
- row.append(title, meta, action);
164-
-
165-
- elements.gameList.append(row);
166-
- });
167-
+ const wrapper = document.createElement("div");
168-
+ wrapper.className = "table-wrapper";
169-
+ const table = document.createElement("table");
170-
+ table.className = "data-table data-table--fixed";
171-
+ table.dataset.gameParentTable = "open-games";
172-
+ table.setAttribute("aria-label", "Open Games");
173-
+ table.innerHTML = "<caption>Open Games</caption><thead><tr><th scope=\"col\">Game</th><th scope=\"col\">Purpose</th><th scope=\"col\">Status</th><th scope=\"col\">Owner</th><th scope=\"col\">Actions</th></tr></thead>";
174-
+ const body = document.createElement("tbody");
175-
+ listResult.forEach((game) => renderGameParentRow(body, game, activeGame));
176-
+ table.append(body);
177-
+ wrapper.append(table);
178-
+ elements.gameList.append(wrapper);
80+
function renderExpandedGameRow(tbody, game) {
81+
const row = document.createElement("tr");
82+
row.dataset.gameExpandedRow = game.id;
83+
@@ -280,6 +328,7 @@ function renderExpandedGameRow(tbody, game) {
84+
const stack = document.createElement("div");
85+
stack.className = "content-stack content-stack--compact";
86+
renderGameSummaryChildTable(stack, game);
87+
+ renderSourceIdeaChildTable(stack, game);
88+
content.append(stack);
89+
row.append(content);
90+
tbody.append(row);
91+
@@ -443,20 +492,15 @@ function renderTableCounts() {
17992
}
18093

181-
function renderMembersTable(activeGame) {
182-
@@ -471,6 +552,13 @@ elements.form?.addEventListener("submit", (event) => {
183-
});
94+
function renderSourceIdea(activeGame) {
95+
- const sourceIdea = isRecord(activeGame?.sourceIdea) ? activeGame.sourceIdea : null;
96+
- const name = String(sourceIdea?.idea || "").trim();
97+
- const pitch = String(sourceIdea?.pitch || "").trim();
98+
- const notes = Array.isArray(sourceIdea?.notes)
99+
- ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean)
100+
- : [];
101+
+ const sourceIdea = gameSourceIdeaDetails(activeGame);
184102

185-
elements.gameList?.addEventListener("click", (event) => {
186-
+ const toggle = event.target.closest("[data-game-toggle]");
187-
+ if (toggle) {
188-
+ state.expandedGameId = state.expandedGameId === toggle.dataset.gameToggle ? "" : toggle.dataset.gameToggle;
189-
+ renderWorkspace();
190-
+ return;
191-
+ }
192-
+
193-
const button = event.target.closest("[data-game-open]");
103+
- setText(elements.sourceIdeaName, name || "No source idea yet");
104+
- setText(elements.sourceIdeaDisplay, name || "No source idea yet");
105+
- setText(elements.sourceIdeaPitch, pitch || "Create a project from Idea Board to see source details.");
106+
+ setText(elements.sourceIdeaName, sourceIdea.name || "No source idea yet");
107+
+ setText(elements.sourceIdeaDisplay, sourceIdea.name || "No source idea yet");
108+
+ setText(elements.sourceIdeaPitch, sourceIdea.pitch || "Create a project from Idea Board to see source details.");
194109

195-
if (!button) {
110+
if (elements.sourceIdeaNotes) {
111+
elements.sourceIdeaNotes.replaceChildren();
112+
- const visibleNotes = notes.length ? notes : ["No source notes."];
113+
+ const visibleNotes = sourceIdea.notes.length ? sourceIdea.notes : ["No source notes."];
114+
visibleNotes.forEach((note) => {
115+
const item = document.createElement("li");
116+
item.textContent = note;

tests/playwright/tools/IdeaBoardTableNotes.spec.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,18 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
392392
await expect(page.locator("[data-source-idea-notes]")).toContainText("Use dusk tide changes as the first Game Hub planning note.");
393393
await expect(page.locator("[data-source-idea-section] :is(input, textarea, select, button)")).toHaveCount(0);
394394
await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0);
395+
const activeGameRow = page.locator("[data-game-row][data-game-active='true']");
396+
await expect(activeGameRow).toContainText("Lantern Reef");
397+
await activeGameRow.locator("[data-game-toggle]").click();
398+
const sourceIdeaChildTable = page.locator("[data-game-expanded-row] [data-game-child-table='source-idea']");
399+
await expect(sourceIdeaChildTable.locator("caption")).toHaveText("Source Idea");
400+
await expect(sourceIdeaChildTable.locator("thead th")).toHaveText(["Context", "Details"]);
401+
await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([
402+
"IdeaLantern Reef",
403+
"PitchGuide light through a reef that rearranges at dusk.",
404+
"Note 1Use dusk tide changes as the first Game Hub planning note.",
405+
]);
406+
await expect(sourceIdeaChildTable.locator(":is(input, textarea, select, button)")).toHaveCount(0);
395407
await page.reload({ waitUntil: "networkidle" });
396408
await expect(page.locator("[data-active-game-name]")).toHaveText("Lantern Reef");
397409
await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef");

0 commit comments

Comments
 (0)