Mobile Phase 8h: Grid detail stage/reminder cards + Open-in-Grid deep-link
Grid full-screen investor detail, conformed to the dc anatomy: - G4: pipeline stage as a single tappable .detail-tap-card (chip + Change/Add) - G5: dedicated Reminder card fed by the soonest active reminder; tri-state (loading → disabled "Checking…" so a pre-load tap can't POST a duplicate; none → "No reminder set"; object → edit). Edits PATCH in place, else POST. - G6 (notes timeline) was already in place. Open-in-Grid deep-link, now on all three mobile detail surfaces (Contacts, Pipeline, Reminders): a shared shell openInvestorInGrid(rowId) sets a one-shot gridUiAction object the mobile grid consumes on mount to open that investor's detail; the desktop grid drains the unrecognized object so it can't linger. Each surface gets its grid row id from a server-injected source_row_id: contacts via contact_grid_signals, opportunities via the durable fundraising_investor_id join, reminders via the investor_id join. All are read-only/GET-only or field-allowlist writes, so none need a strip point. Tests: source_row_id injection assertions for contacts, opportunities, and reminders; full suite 40/40. Client surfaces jsdom-verified.
This commit is contained in:
@@ -135,6 +135,7 @@ def main():
|
||||
check(bool(fr_id), f"opportunity carries fundraising_investor_id (got {fr_id})")
|
||||
check(_opp_count_live(fr_id) == 1, "exactly one live opp linked to the investor")
|
||||
opp_id = opp.get("id")
|
||||
jane_contact_id = opp.get("contact_id")
|
||||
|
||||
# ── idempotent re-link: returns existing, board-owned stage NOT reseeded ──
|
||||
print("\n[idempotent: re-link returns existing opp without reseeding funnel fields]")
|
||||
@@ -222,6 +223,11 @@ def main():
|
||||
st, d = _req(port, "GET", "/api/opportunities?limit=1000", token)
|
||||
ids = [o["id"] for o in (d or {}).get("data", [])]
|
||||
check(opp_id in ids, "linked opp appears on the board")
|
||||
# 8h deep-link: the opp list injects source_row_id from the durable fundraising_investor_id
|
||||
# (the mobile Pipeline detail's "Open investor in Grid" target).
|
||||
acme_opp = next((o for o in (d or {}).get("data", []) if o["id"] == opp_id), {})
|
||||
check(acme_opp.get("source_row_id") == "rowAcme",
|
||||
f"opp list injects source_row_id == 'rowAcme' (got {acme_opp.get('source_row_id')!r})")
|
||||
st, d = _req(port, "GET", "/api/reports/dashboard", token)
|
||||
active = (d or {}).get("data", {}).get("metrics", {}).get("active_opportunities")
|
||||
check(active == 1, f"dashboard active_opportunities == 1 (got {active})")
|
||||
@@ -281,6 +287,18 @@ def main():
|
||||
check(_opp_count_live(beta_fr) == 0, "beta's orphaned opp archived by the reconciler")
|
||||
st, d = _req(port, "GET", "/api/opportunities?limit=1000", token)
|
||||
check(beta_opp_id not in [o["id"] for o in (d or {}).get("data", [])], "orphaned opp left the board")
|
||||
|
||||
# ── 8h: a manually-created deal (no fundraising_investor_id) has a null source_row_id, so
|
||||
# the mobile Pipeline detail hides "Open investor in Grid" for it ──
|
||||
print("\n[8h: opp source_row_id is null for a deal with no grid link]")
|
||||
st, d = _req(port, "POST", "/api/opportunities", token,
|
||||
{"name": "Manual deal", "contact_id": jane_contact_id, "stage": "lead"})
|
||||
manual_id = (d or {}).get("data", {}).get("id")
|
||||
check(st in (200, 201) and bool(manual_id), f"create a manual (non-grid) opp (got {st})")
|
||||
st, d = _req(port, "GET", "/api/opportunities?limit=1000", token)
|
||||
manual = next((o for o in (d or {}).get("data", []) if o["id"] == manual_id), {})
|
||||
check(manual.get("source_row_id") in (None, ""),
|
||||
f"manual opp has null source_row_id (got {manual.get('source_row_id')!r})")
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user