game-views ATLAS: mirror SCROLL log styling (strikethrough) + honour the redact filter
The merged ATLAS feed now reads its provenance rows the way SCROLL does: - Struck (retracted/redacted) logs carry the strikethrough — buildAtlasFeed captures the source .drama-event-body.struck state and renderAtlasRow re-applies it as .atlas-row-body.struck (styled to match .drama-event-body.struck). - A log hidden by the SCROLL Frame/Redact gear filter (display:none) is skipped in the ATLAS merge too — buildAtlasFeed checks getComputedStyle(ev).display, so unchecking Redact on SCROLL keeps those rows out of ATLAS. - Also lays the source-toggle seam: atlasSources() reads the (forthcoming) ATLAS gear's view-checkboxes (scroll→provenance, post→post), defaulting to both when absent. Verified: Jasmine renderAtlasRow struck spec + two FTs (struck row shows struck in ATLAS; redact-filtered rows absent from ATLAS) + the existing atlas aggregate FT, all green. [[project-room-game-views-carousel]] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,9 +48,12 @@
|
|||||||
? '<span class="atlas-row-who">' + escapeHtml(r.whoText) + '</span>'
|
? '<span class="atlas-row-who">' + escapeHtml(r.whoText) + '</span>'
|
||||||
: '';
|
: '';
|
||||||
var time = r.timeHtml || '';
|
var time = r.timeHtml || '';
|
||||||
|
// Carry the source row's struck state (a retracted/redacted SCROLL log)
|
||||||
|
// so the merged copy reads the same strikethrough it does in SCROLL.
|
||||||
|
var bodyCls = 'atlas-row-body' + (r.struck ? ' struck' : '');
|
||||||
return '<div class="atlas-row atlas-row--' + r.source + '" data-source="'
|
return '<div class="atlas-row atlas-row--' + r.source + '" data-source="'
|
||||||
+ r.source + '">' + who
|
+ r.source + '">' + who
|
||||||
+ '<span class="atlas-row-body">' + r.bodyHtml + '</span>' + time
|
+ '<span class="' + bodyCls + '">' + r.bodyHtml + '</span>' + time
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,20 +320,44 @@
|
|||||||
return t ? t.outerHTML : '';
|
return t ? t.outerHTML : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which sources the ATLAS gear has enabled (its source checkboxes map
|
||||||
|
// each reelhouse view → a row source: scroll→provenance, post→post).
|
||||||
|
// Defaults to both when the gear form isn't on the page yet.
|
||||||
|
function atlasSources() {
|
||||||
|
var form = document.getElementById('id_atlas_source_form');
|
||||||
|
if (!form) return { provenance: true, post: true };
|
||||||
|
var VIEW_SOURCE = { scroll: 'provenance', post: 'post' };
|
||||||
|
var on = {};
|
||||||
|
form.querySelectorAll('input[name="views"]:checked').forEach(function (cb) {
|
||||||
|
var src = VIEW_SOURCE[cb.value];
|
||||||
|
if (src) on[src] = true;
|
||||||
|
});
|
||||||
|
return on;
|
||||||
|
}
|
||||||
|
|
||||||
function buildAtlasFeed() {
|
function buildAtlasFeed() {
|
||||||
var atlas = document.getElementById('id_room_atlas');
|
var atlas = document.getElementById('id_room_atlas');
|
||||||
if (!atlas) return;
|
if (!atlas) return;
|
||||||
|
var srcOn = atlasSources();
|
||||||
var rows = [];
|
var rows = [];
|
||||||
|
if (srcOn.provenance) {
|
||||||
document.querySelectorAll('.room-view--scroll .drama-event').forEach(function (ev) {
|
document.querySelectorAll('.room-view--scroll .drama-event').forEach(function (ev) {
|
||||||
|
// Honour the SCROLL redact/frame filter: a log hidden there
|
||||||
|
// (display:none) must not surface in the merged ATLAS either.
|
||||||
|
if (getComputedStyle(ev).display === 'none') return;
|
||||||
var body = ev.querySelector('.drama-event-body');
|
var body = ev.querySelector('.drama-event-body');
|
||||||
rows.push({
|
rows.push({
|
||||||
source: 'provenance',
|
source: 'provenance',
|
||||||
ts: nodeTime(ev, '.drama-event-time'),
|
ts: nodeTime(ev, '.drama-event-time'),
|
||||||
whoText: '',
|
whoText: '',
|
||||||
bodyHtml: body ? body.innerHTML : ev.innerHTML,
|
bodyHtml: body ? body.innerHTML : ev.innerHTML,
|
||||||
|
// Struck (retracted/redacted) carries over from SCROLL.
|
||||||
|
struck: !!(body && body.classList.contains('struck')),
|
||||||
timeHtml: nodeTimeHtml(ev, '.drama-event-time'),
|
timeHtml: nodeTimeHtml(ev, '.drama-event-time'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (srcOn.post) {
|
||||||
document.querySelectorAll('.room-view--post #id_post_table .post-line').forEach(function (li) {
|
document.querySelectorAll('.room-view--post #id_post_table .post-line').forEach(function (li) {
|
||||||
var who = li.querySelector('.post-line-author');
|
var who = li.querySelector('.post-line-author');
|
||||||
var txt = li.querySelector('.post-line-text');
|
var txt = li.querySelector('.post-line-text');
|
||||||
@@ -342,6 +369,7 @@
|
|||||||
timeHtml: nodeTimeHtml(li, '.post-line-time'),
|
timeHtml: nodeTimeHtml(li, '.post-line-time'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
atlas.innerHTML =
|
atlas.innerHTML =
|
||||||
'<p class="event-empty"><small>The atlas gathers . . .</small></p>';
|
'<p class="event-empty"><small>The atlas gathers . . .</small></p>';
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ class GameViewsCarouselTest(FunctionalTest):
|
|||||||
data={"token_type": "carte", "token_display": "Carte Blanche",
|
data={"token_type": "carte", "token_display": "Carte Blanche",
|
||||||
"slot_number": 1, "renewal_days": 7},
|
"slot_number": 1, "renewal_days": 7},
|
||||||
)
|
)
|
||||||
|
# A retracted SIG_READY (struck → data-label="redact") so the Atlas can
|
||||||
|
# be checked for SCROLL-style strikethrough + redact-filter coupling.
|
||||||
|
GameEvent.objects.create(
|
||||||
|
room=self.room, actor=self.viewer, verb=GameEvent.SIG_READY,
|
||||||
|
data={"retracted": True, "card_name": "The Nomad"},
|
||||||
|
)
|
||||||
self.room.table_status = Room.ROLE_SELECT
|
self.room.table_status = Room.ROLE_SELECT
|
||||||
self.room.gate_status = Room.OPEN
|
self.room.gate_status = Room.OPEN
|
||||||
self.room.save()
|
self.room.save()
|
||||||
@@ -213,6 +219,45 @@ class GameViewsCarouselTest(FunctionalTest):
|
|||||||
self.assertTrue(atlas.find_elements(
|
self.assertTrue(atlas.find_elements(
|
||||||
By.CSS_SELECTOR, "[data-source='post']"))
|
By.CSS_SELECTOR, "[data-source='post']"))
|
||||||
|
|
||||||
|
def test_atlas_provenance_rows_carry_scroll_strikethrough(self):
|
||||||
|
"""A retracted/redacted SCROLL log reads the same strikethrough in the
|
||||||
|
merged ATLAS as it does in SCROLL (`.atlas-row-body.struck`)."""
|
||||||
|
self._open()
|
||||||
|
self._scroll_to_views()
|
||||||
|
self._click_icon("atlas")
|
||||||
|
self.wait_for(lambda: self.assertTrue(
|
||||||
|
self._in_viewport(".room-view[data-view='atlas']")))
|
||||||
|
atlas = self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".room-view[data-view='atlas']")
|
||||||
|
self.assertTrue(atlas.find_elements(
|
||||||
|
By.CSS_SELECTOR, ".atlas-row-body.struck"))
|
||||||
|
|
||||||
|
def test_atlas_omits_redact_logs_filtered_out_on_scroll(self):
|
||||||
|
"""Unchecking Redact in the SCROLL gear filter hides the struck logs in
|
||||||
|
the feed — and they must not surface in the merged ATLAS either."""
|
||||||
|
self._open()
|
||||||
|
self._scroll_to_views() # lands on SCROLL → gear shows the log filter
|
||||||
|
gear = self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".gear-btn[data-menu-target='id_room_menu']")
|
||||||
|
self.browser.execute_script("arguments[0].click();", gear)
|
||||||
|
self.wait_for(lambda: self.assertTrue(
|
||||||
|
self.browser.find_element(By.ID, "id_scroll_filter_form").is_displayed()))
|
||||||
|
redact = self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_scroll_filter_form input[value='redact']")
|
||||||
|
if redact.is_selected():
|
||||||
|
self.browser.execute_script("arguments[0].click();", redact)
|
||||||
|
self.browser.execute_script("arguments[0].click();", self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_scroll_filter_form button[type='submit']"))
|
||||||
|
|
||||||
|
self._click_icon("atlas")
|
||||||
|
self.wait_for(lambda: self.assertTrue(
|
||||||
|
self._in_viewport(".room-view[data-view='atlas']")))
|
||||||
|
atlas = self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".room-view[data-view='atlas']")
|
||||||
|
# Redact logs (the only struck rows) were filtered out → none in ATLAS.
|
||||||
|
self.assertFalse(atlas.find_elements(
|
||||||
|
By.CSS_SELECTOR, ".atlas-row-body.struck"))
|
||||||
|
|
||||||
def test_yarn_and_pulse_render_as_stubs(self):
|
def test_yarn_and_pulse_render_as_stubs(self):
|
||||||
"""YARN (fa-route) + PULSE (fa-chart-pie) are stub views this sprint —
|
"""YARN (fa-route) + PULSE (fa-chart-pie) are stub views this sprint —
|
||||||
each renders a placeholder, no backing model yet. The watermark icon
|
each renders a placeholder, no backing model yet. The watermark icon
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ describe("RoomViews atlas row rendering", () => {
|
|||||||
expect(row).toContain("<em>safe</em>"); // body trusted (server-escaped)
|
expect(row).toContain("<em>safe</em>"); // body trusted (server-escaped)
|
||||||
expect(row).toContain("<script>"); // who escaped
|
expect(row).toContain("<script>"); // who escaped
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks struck (redacted) provenance rows so they read line-through like SCROLL", () => {
|
||||||
|
const struck = window.RoomViews.renderAtlasRow(
|
||||||
|
{ source: "provenance", bodyHtml: "withdraws yos Carte Blanche", whoText: "", timeHtml: "", struck: true });
|
||||||
|
expect(struck).toContain("struck");
|
||||||
|
const plain = window.RoomViews.renderAtlasRow(
|
||||||
|
{ source: "provenance", bodyHtml: "deposits a Carte Blanche", whoText: "", timeHtml: "" });
|
||||||
|
expect(plain).not.toContain("struck");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// The Text sub-btn swipe machine drives the reelhouse to GAME POST. Its from-
|
// The Text sub-btn swipe machine drives the reelhouse to GAME POST. Its from-
|
||||||
|
|||||||
@@ -247,7 +247,14 @@ html.sea-open #id_aperture_fill {
|
|||||||
padding-inline-start: 0.5rem;
|
padding-inline-start: 0.5rem;
|
||||||
|
|
||||||
.atlas-row-who { font-weight: bold; color: rgba(var(--quaUser), 1); flex-shrink: 0; }
|
.atlas-row-who { font-weight: bold; color: rgba(var(--quaUser), 1); flex-shrink: 0; }
|
||||||
.atlas-row-body { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
.atlas-row-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
// Struck (retracted/redacted) provenance rows read the same
|
||||||
|
// strikethrough they do in SCROLL (.drama-event-body.struck).
|
||||||
|
&.struck { text-decoration: line-through; opacity: 0.5; }
|
||||||
|
}
|
||||||
// The merged rows carry the ORIGINAL <time> from their source row
|
// The merged rows carry the ORIGINAL <time> from their source row
|
||||||
// (.drama-event-time from SCROLL, .post-line-time from POST). Those
|
// (.drama-event-time from SCROLL, .post-line-time from POST). Those
|
||||||
// source rules are scoped to the feed/thread, so restate the shared
|
// source rules are scoped to the feed/thread, so restate the shared
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ describe("RoomViews atlas row rendering", () => {
|
|||||||
expect(row).toContain("<em>safe</em>"); // body trusted (server-escaped)
|
expect(row).toContain("<em>safe</em>"); // body trusted (server-escaped)
|
||||||
expect(row).toContain("<script>"); // who escaped
|
expect(row).toContain("<script>"); // who escaped
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks struck (redacted) provenance rows so they read line-through like SCROLL", () => {
|
||||||
|
const struck = window.RoomViews.renderAtlasRow(
|
||||||
|
{ source: "provenance", bodyHtml: "withdraws yos Carte Blanche", whoText: "", timeHtml: "", struck: true });
|
||||||
|
expect(struck).toContain("struck");
|
||||||
|
const plain = window.RoomViews.renderAtlasRow(
|
||||||
|
{ source: "provenance", bodyHtml: "deposits a Carte Blanche", whoText: "", timeHtml: "" });
|
||||||
|
expect(plain).not.toContain("struck");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// The Text sub-btn swipe machine drives the reelhouse to GAME POST. Its from-
|
// The Text sub-btn swipe machine drives the reelhouse to GAME POST. Its from-
|
||||||
|
|||||||
Reference in New Issue
Block a user