bud panels duplicate-add guard: server-side already_present flag + client-side error Brief w. FYI flash highlight on the existing entry — for each of the three #id_bud_btn panels (My Buds / post-share / gatekeeper-invite), the JSON response from add_bud / share_post / invite_gamer now carries {already_present, recipient_display, recipient_user_id}; bud-btn.js branches on already_present → calls new Brief.showDuplicateBanner({display_name, target_selector}) instead of the normal onSuccess append; banner title reads @<username> is already present, NVM dismisses, FYI dismisses AND eases in the .bud-duplicate-flash class (color: var(--terUser); text-shadow: 0 0 .5em var(--ninUser); transition: 600ms) onto the existing element (.bud-entry .bud-name / .post-recipient[data-user-id=…] / .gate-slot.filled[data-user-id=…]); gatekeeper "already present" = recipient is either GateSlot.FILLED + gamer OR has TableSeat OR has a pending RoomInvite (highlight target only set when seated — pending invites have no visible slot); .post-recipient chips + .gate-slot.filled cells gain data-user-id so the FYI selector can find them; my_buds.html now loads note.js via the {% block scripts %} pattern (Brief module is required by the duplicate banner path); bonus: latent test_jasmine.py bug fixed — "0 failures" in result.text matched "10 failures" / "20 failures" / etc, silently passing up to 99 failed specs; replaced w. re.search(r"(?<!\d)0 failures\b", …) (caught my new red specs, would've caught any prior Jasmine regression); 18 new ITs + 10 new Jasmine specs + 3 new FTs (one per panel) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,9 @@
|
||||
onSuccess: function (data) {
|
||||
if (data.bud) _appendBudEntry(data.bud);
|
||||
},
|
||||
duplicateTargetSelector: function (data) {
|
||||
return '.bud-entry[data-bud-id="' + data.recipient_user_id + '"] .bud-name';
|
||||
},
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
|
||||
@@ -35,5 +35,12 @@ bindBudBtn({
|
||||
onSuccess: function (data) {
|
||||
if (window.Brief && data.brief) Brief.showBanner(data.brief);
|
||||
},
|
||||
duplicateTargetSelector: function (data) {
|
||||
// Pending RoomInvite duplicates have no recipient_user_id (no
|
||||
// visible slot to highlight); selector returning null is fine —
|
||||
// showDuplicateBanner's FYI just dismisses without easing.
|
||||
if (!data.recipient_user_id) return null;
|
||||
return '.gate-slot.filled[data-user-id="' + data.recipient_user_id + '"]';
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -68,7 +68,9 @@
|
||||
// under .post-header. State transitions:
|
||||
// 0 → 1+ recipients : "just me, X" → "shared between {chip}" + "& me, X"
|
||||
// ≥1 → +1 recipients: append chip + ", " separator before existing.
|
||||
function _appendRecipientChip(displayName) {
|
||||
// `userId` is stamped onto the chip as data-user-id so a later duplicate-
|
||||
// share attempt can highlight this same element via .bud-duplicate-flash.
|
||||
function _appendRecipientChip(displayName, userId) {
|
||||
if (!displayName) return;
|
||||
var header = document.querySelector('.post-page .post-header');
|
||||
if (!header) return;
|
||||
@@ -78,6 +80,7 @@
|
||||
var chip = document.createElement('span');
|
||||
chip.className = 'post-recipient';
|
||||
chip.textContent = displayName;
|
||||
if (userId) chip.dataset.userId = userId;
|
||||
|
||||
if (existingRecipients) {
|
||||
existingRecipients.appendChild(document.createTextNode(', '));
|
||||
@@ -103,7 +106,12 @@
|
||||
onSuccess: function (data) {
|
||||
if (data.line_text) _appendLine(data.line_text);
|
||||
if (window.Brief && data.brief) Brief.showBanner(data.brief);
|
||||
if (data.recipient_display) _appendRecipientChip(data.recipient_display);
|
||||
if (data.recipient_display) {
|
||||
_appendRecipientChip(data.recipient_display, data.recipient_user_id);
|
||||
}
|
||||
},
|
||||
duplicateTargetSelector: function (data) {
|
||||
return '.post-recipient[data-user-id="' + data.recipient_user_id + '"]';
|
||||
},
|
||||
});
|
||||
}());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load static %}
|
||||
{% load lyric_extras %}
|
||||
|
||||
{% block title_text %}Billbuds{% endblock title_text %}
|
||||
@@ -13,3 +14,9 @@
|
||||
{# Bud btn (bottom-left) + slide-out add-bud panel — async POST to add_bud. #}
|
||||
{% include "apps/billboard/_partials/_bud_add_panel.html" %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
{# Brief module — needed by _bud_add_panel's OK handler so the #}
|
||||
{# duplicate-add error banner can render via Brief.showDuplicateBanner.#}
|
||||
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
||||
{% endblock scripts %}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{# Owner viewing — owner-centric prose. "shared between" lists #}
|
||||
{# every recipient; the self line is the owner's own handle. #}
|
||||
{% if other_recipients %}
|
||||
<p class="post-shared-recipients">shared between {% for r in other_recipients %}<span class="post-recipient post-attribution">{{ r|at_handle }}</span>{% if not forloop.last %}, {% endif %}{% endfor %}</p>
|
||||
<p class="post-shared-recipients">shared between {% for r in other_recipients %}<span class="post-recipient post-attribution" data-user-id="{{ r.id }}">{{ r|at_handle }}</span>{% if not forloop.last %}, {% endif %}{% endfor %}</p>
|
||||
<p class="post-shared-self">& me, <span class="post-attribution">{{ post.owner|at_handle }} the {{ post.owner.active_title_display }}</span></p>
|
||||
{% else %}
|
||||
<p class="post-shared-self">just me, <span class="post-attribution">{{ post.owner|at_handle }} the {{ post.owner.active_title_display }}</span></p>
|
||||
@@ -28,7 +28,7 @@
|
||||
{# (request.user). Sole invitee collapses to a single line; the #}
|
||||
{# "created by …" line attributes the post to its founder. #}
|
||||
{% if other_recipients %}
|
||||
<p class="post-shared-recipients">shared with {% for r in other_recipients %}<span class="post-recipient post-attribution">{{ r|at_handle }}</span>{% if not forloop.last %}, {% endif %}{% endfor %}</p>
|
||||
<p class="post-shared-recipients">shared with {% for r in other_recipients %}<span class="post-recipient post-attribution" data-user-id="{{ r.id }}">{{ r|at_handle }}</span>{% if not forloop.last %}, {% endif %}{% endfor %}</p>
|
||||
<p class="post-shared-self">& me, <span class="post-attribution">{{ request.user|at_handle }} the {{ request.user.active_title_display }}</span></p>
|
||||
{% else %}
|
||||
<p class="post-shared-self">shared with me, <span class="post-attribution">{{ request.user|at_handle }} the {{ request.user.active_title_display }}</span></p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="position-strip">
|
||||
{% for pos in gate_positions %}
|
||||
<div class="gate-slot{% if pos.slot.status == 'EMPTY' %} empty{% elif pos.slot.status == 'FILLED' %} filled{% elif pos.slot.status == 'RESERVED' %} reserved{% endif %}{% if pos.role_assigned %} role-assigned{% endif %}"
|
||||
data-slot="{{ pos.slot.slot_number }}">
|
||||
data-slot="{{ pos.slot.slot_number }}"{% if pos.slot.gamer %} data-user-id="{{ pos.slot.gamer.id }}"{% endif %}>
|
||||
<span class="slot-number">{{ pos.slot.slot_number }}</span>
|
||||
<span class="slot-gamer">{% if pos.slot.gamer %}{{ pos.slot.gamer.email }}{% else %}empty{% endif %}</span>
|
||||
{% if pos.slot.status == 'RESERVED' and pos.slot.gamer == request.user %}
|
||||
|
||||
Reference in New Issue
Block a user