Enterprise UI redesign — Phases 14–22 (Docusign-branded migration console) #1

Merged
paulh merged 24 commits from ui-redesign into master 2026-04-21 15:30:44 -05:00
2 changed files with 75 additions and 2 deletions
Showing only changes of commit 53eb206d89 - Show all commits

View File

@ -325,6 +325,46 @@ def merge_tabs(acc: dict, new: dict) -> dict:
return acc
# Tab types DocuSign forbids as conditional parents (auto-filled or action tabs)
_INVALID_PARENT_TAB_TYPES = {
"signHereTabs", "initialHereTabs", "dateSignedTabs",
"fullNameTabs", "emailTabs", "titleTabs", "signerAttachmentTabs",
}
def _strip_invalid_conditionals(signers: list, warnings: list) -> None:
"""
Remove conditionalParentLabel/Value from any tab whose parent label either
doesn't exist in the template or points to a tab type DocuSign forbids as a
parent (signature, initial, auto-filled). Mutates signers in place.
"""
for signer in signers:
tabs = signer.get("tabs", {})
# Collect valid parent labels: only tab types allowed as parents
valid_labels: set[str] = set()
for tab_type, tab_list in tabs.items():
if tab_type in _INVALID_PARENT_TAB_TYPES:
continue
for tab in tab_list:
label = tab.get("tabLabel") or tab.get("groupName")
if label:
valid_labels.add(label)
# Strip references to invalid/missing parents
for tab_type, tab_list in tabs.items():
for tab in tab_list:
parent = tab.get("conditionalParentLabel")
if parent and parent not in valid_labels:
warnings.append(
f"Conditional parent '{parent}' not found or not a valid "
f"parent tab type — conditional stripped from tab "
f"'{tab.get('tabLabel', '?')}'"
)
tab.pop("conditionalParentLabel", None)
tab.pop("conditionalParentValue", None)
# ---------------------------------------------------------------------------
# Main compose function
# ---------------------------------------------------------------------------
@ -386,6 +426,10 @@ def compose_template(template_dir: str, output_path: str) -> tuple[dict, list[st
tabs = _apply_conditional_to_tabs(tabs, field, warnings)
signers[idx]["tabs"] = merge_tabs(signers[idx]["tabs"], tabs)
# Post-process: strip conditionalParentLabel references that point to
# non-existent or invalid parents (signature/initial tabs can't be parents).
_strip_invalid_conditionals(signers, warnings)
template = {
"name": metadata.get("name", template_dir.name),
"description": f"Migrated from Adobe Sign — original owner: {metadata.get('ownerEmail', '')}",

View File

@ -51,7 +51,14 @@ class TestVerifySend:
@respx.mock
def test_send_returns_envelope_id(self):
"""Authenticated + valid template → envelope_id returned."""
"""Authenticated + valid template → role names fetched, envelope_id returned."""
respx.get(
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/{TEMPLATE_ID}"
).mock(return_value=httpx.Response(200, json={
"recipients": {
"signers": [{"roleName": "Customer", "recipientId": "1"}],
}
}))
respx.post(
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes"
).mock(return_value=httpx.Response(201, json={"envelopeId": ENVELOPE_ID}))
@ -67,10 +74,32 @@ class TestVerifySend:
)
assert resp.status_code == 200
assert resp.json()["envelope_id"] == ENVELOPE_ID
assert resp.json()["roles"] == ["Customer"]
@respx.mock
def test_send_falls_back_to_signer_role_on_template_error(self):
"""Template fetch failure → falls back to 'Signer' role name."""
respx.get(
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/bad-id"
).mock(return_value=httpx.Response(404, json={"message": "Not found"}))
respx.post(
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes"
).mock(return_value=httpx.Response(201, json={"envelopeId": ENVELOPE_ID}))
resp = client.post(
"/api/verify/send",
json={"template_id": "bad-id", "recipient_name": "X", "recipient_email": "x@x.com"},
cookies={_COOKIE_NAME: _ds_session()},
)
assert resp.status_code == 200
assert resp.json()["roles"] == ["Signer"]
@respx.mock
def test_send_propagates_docusign_error(self):
"""DocuSign 400 → 502 with error detail."""
"""DocuSign 400 on envelope create → 502 with error detail."""
respx.get(
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/templates/bad-id"
).mock(return_value=httpx.Response(200, json={"recipients": {}}))
respx.post(
f"{DS_BASE}/v2.1/accounts/{DS_ACCOUNT}/envelopes"
).mock(return_value=httpx.Response(400, json={"message": "Invalid templateId"}))