feat: review conversation photo bundles before sending (Closes #945)
This commit is contained in:
parent
55bd353daf
commit
5da5d7cdc4
|
|
@ -13,17 +13,21 @@ function createAuthoredOutbox({ storage, fetchJson, coordinator, backgroundSync,
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachmentMetadata(value) {
|
function attachmentMetadata(value) {
|
||||||
|
const note = String(value.note || '').replace(/\s+/g, ' ').trim().slice(0, 240);
|
||||||
return {
|
return {
|
||||||
filename: String(value.filename || ''),
|
filename: String(value.filename || ''),
|
||||||
contentType: String(value.contentType || ''),
|
contentType: String(value.contentType || ''),
|
||||||
stored: true,
|
stored: true,
|
||||||
|
...(note ? { note } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function durableAttachment(value) {
|
function durableAttachment(value) {
|
||||||
|
const note = String(value.note || '').replace(/\s+/g, ' ').trim().slice(0, 240);
|
||||||
return {
|
return {
|
||||||
filename: String(value.filename || ''),
|
filename: String(value.filename || ''),
|
||||||
contentType: String(value.contentType || ''),
|
contentType: String(value.contentType || ''),
|
||||||
|
...(note ? { note } : {}),
|
||||||
...(value.blob ? { blob:value.blob } : { data:String(value.data || '') }),
|
...(value.blob ? { blob:value.blob } : { data:String(value.data || '') }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -701,6 +701,14 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.issue-evidence-note textarea:focus-visible { outline:2px solid #60a5fa; outline-offset:2px; }
|
.issue-evidence-note textarea:focus-visible { outline:2px solid #60a5fa; outline-offset:2px; }
|
||||||
.issue-evidence-review-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
|
.issue-evidence-review-actions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
|
||||||
.issue-evidence-review-actions button { min-width:44px; min-height:44px; }
|
.issue-evidence-review-actions button { min-width:44px; min-height:44px; }
|
||||||
|
.conversation-evidence-review { grid-template-columns:1fr; width:100%; min-width:0; box-sizing:border-box; overflow-x:hidden; }
|
||||||
|
.conversation-evidence-review > img { grid-row:auto; width:100%; height:auto; max-height:280px; }
|
||||||
|
.conversation-evidence-review .issue-evidence-tray { overflow-x:auto; }
|
||||||
|
.conversation-evidence-review .issue-evidence-review-actions { grid-template-columns:repeat(3,minmax(0,1fr)); }
|
||||||
|
.conversation-evidence-review .issue-evidence-review-actions button { min-width:44px; min-height:44px; width:100%; }
|
||||||
|
@media (max-width:390px) {
|
||||||
|
.conversation-evidence-review .issue-evidence-review-actions { grid-template-columns:1fr; }
|
||||||
|
}
|
||||||
.issue-evidence-editor { position:fixed; inset:0; z-index:70; display:grid; place-items:end center; overflow-x:hidden; background:rgba(2,6,15,.92); }
|
.issue-evidence-editor { position:fixed; inset:0; z-index:70; display:grid; place-items:end center; overflow-x:hidden; background:rgba(2,6,15,.92); }
|
||||||
.issue-evidence-editor[hidden] { display:none; }
|
.issue-evidence-editor[hidden] { display:none; }
|
||||||
.issue-evidence-editor-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; display:grid; gap:12px; padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); background:#0b1526; border:1px solid #2a496e; border-radius:14px 14px 0 0; }
|
.issue-evidence-editor-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; display:grid; gap:12px; padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); background:#0b1526; border:1px solid #2a496e; border-radius:14px 14px 0 0; }
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,11 @@
|
||||||
image: qs('#issue-attachment-image'),
|
image: qs('#issue-attachment-image'),
|
||||||
meta: qs('#issue-attachment-meta'),
|
meta: qs('#issue-attachment-meta'),
|
||||||
remove: qs('#remove-issue-attachment'),
|
remove: qs('#remove-issue-attachment'),
|
||||||
|
tray: qs('#issue-attachment-tray'),
|
||||||
|
earlier: qs('#move-issue-attachment-earlier'),
|
||||||
|
later: qs('#move-issue-attachment-later'),
|
||||||
|
note: qs('#issue-attachment-note'),
|
||||||
|
noteLabel: qs('#issue-attachment-note-label'),
|
||||||
status: qs('#issue-comment-status'),
|
status: qs('#issue-comment-status'),
|
||||||
createObjectURL: file => URL.createObjectURL(file),
|
createObjectURL: file => URL.createObjectURL(file),
|
||||||
revokeObjectURL: url => URL.revokeObjectURL(url),
|
revokeObjectURL: url => URL.revokeObjectURL(url),
|
||||||
|
|
@ -534,6 +539,11 @@
|
||||||
image: qs('#pull-attachment-image'),
|
image: qs('#pull-attachment-image'),
|
||||||
meta: qs('#pull-attachment-meta'),
|
meta: qs('#pull-attachment-meta'),
|
||||||
remove: qs('#remove-pull-attachment'),
|
remove: qs('#remove-pull-attachment'),
|
||||||
|
tray: qs('#pull-attachment-tray'),
|
||||||
|
earlier: qs('#move-pull-attachment-earlier'),
|
||||||
|
later: qs('#move-pull-attachment-later'),
|
||||||
|
note: qs('#pull-attachment-note'),
|
||||||
|
noteLabel: qs('#pull-attachment-note-label'),
|
||||||
status: qs('#pull-comment-status'),
|
status: qs('#pull-comment-status'),
|
||||||
createObjectURL: file => URL.createObjectURL(file),
|
createObjectURL: file => URL.createObjectURL(file),
|
||||||
revokeObjectURL: url => URL.revokeObjectURL(url),
|
revokeObjectURL: url => URL.revokeObjectURL(url),
|
||||||
|
|
@ -557,6 +567,11 @@
|
||||||
image: qs('#update-reply-attachment-image'),
|
image: qs('#update-reply-attachment-image'),
|
||||||
meta: qs('#update-reply-attachment-meta'),
|
meta: qs('#update-reply-attachment-meta'),
|
||||||
remove: qs('#remove-update-reply-attachment'),
|
remove: qs('#remove-update-reply-attachment'),
|
||||||
|
tray: qs('#update-reply-attachment-tray'),
|
||||||
|
earlier: qs('#move-update-reply-attachment-earlier'),
|
||||||
|
later: qs('#move-update-reply-attachment-later'),
|
||||||
|
note: qs('#update-reply-attachment-note'),
|
||||||
|
noteLabel: qs('#update-reply-attachment-note-label'),
|
||||||
status: qs('#update-reply-status'),
|
status: qs('#update-reply-status'),
|
||||||
readyMessage: 'Screenshot ready to send with this reply.',
|
readyMessage: 'Screenshot ready to send with this reply.',
|
||||||
removedMessage: 'Screenshot removed. Your reply is unchanged.',
|
removedMessage: 'Screenshot removed. Your reply is unchanged.',
|
||||||
|
|
|
||||||
|
|
@ -667,10 +667,19 @@
|
||||||
<label class="issue-attachment-trigger" for="issue-attachment">Choose existing</label>
|
<label class="issue-attachment-trigger" for="issue-attachment">Choose existing</label>
|
||||||
<input class="visually-hidden" id="issue-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple />
|
<input class="visually-hidden" id="issue-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple />
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-attachment-preview" id="issue-attachment-preview" hidden>
|
<div class="issue-attachment-preview conversation-evidence-review" id="issue-attachment-preview" hidden>
|
||||||
<img id="issue-attachment-image" alt="Selected screenshot preview" />
|
<img id="issue-attachment-image" alt="Selected screenshot preview" />
|
||||||
<span class="small" id="issue-attachment-meta"></span>
|
<span class="small" id="issue-attachment-meta"></span>
|
||||||
<button id="remove-issue-attachment" type="button">Remove screenshot</button>
|
<div class="issue-evidence-tray" id="issue-attachment-tray" role="toolbar" aria-label="Comment photos" hidden></div>
|
||||||
|
<label class="issue-evidence-note" for="issue-attachment-note">
|
||||||
|
<span id="issue-attachment-note-label">Photo caption (optional)</span>
|
||||||
|
<textarea id="issue-attachment-note" maxlength="240" rows="2" disabled placeholder="What does this photo show?"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="issue-evidence-review-actions" aria-label="Reorder selected photo">
|
||||||
|
<button id="move-issue-attachment-earlier" type="button">Move earlier</button>
|
||||||
|
<button id="remove-issue-attachment" type="button">Remove selected</button>
|
||||||
|
<button id="move-issue-attachment-later" type="button">Move later</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
<button id="send-issue-comment" type="button">Post comment</button>
|
<button id="send-issue-comment" type="button">Post comment</button>
|
||||||
|
|
@ -1058,10 +1067,19 @@
|
||||||
<label class="issue-attachment-trigger" for="update-reply-attachment">Choose existing</label>
|
<label class="issue-attachment-trigger" for="update-reply-attachment">Choose existing</label>
|
||||||
<input class="visually-hidden" id="update-reply-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple />
|
<input class="visually-hidden" id="update-reply-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple />
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-attachment-preview" id="update-reply-attachment-preview" hidden>
|
<div class="issue-attachment-preview conversation-evidence-review" id="update-reply-attachment-preview" hidden>
|
||||||
<img id="update-reply-attachment-image" alt="Selected screenshot preview" />
|
<img id="update-reply-attachment-image" alt="Selected screenshot preview" />
|
||||||
<span class="small" id="update-reply-attachment-meta"></span>
|
<span class="small" id="update-reply-attachment-meta"></span>
|
||||||
<button id="remove-update-reply-attachment" type="button">Remove screenshot</button>
|
<div class="issue-evidence-tray" id="update-reply-attachment-tray" role="toolbar" aria-label="Reply photos" hidden></div>
|
||||||
|
<label class="issue-evidence-note" for="update-reply-attachment-note">
|
||||||
|
<span id="update-reply-attachment-note-label">Photo caption (optional)</span>
|
||||||
|
<textarea id="update-reply-attachment-note" maxlength="240" rows="2" disabled placeholder="What does this photo show?"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="issue-evidence-review-actions" aria-label="Reorder selected photo">
|
||||||
|
<button id="move-update-reply-attachment-earlier" type="button">Move earlier</button>
|
||||||
|
<button id="remove-update-reply-attachment" type="button">Remove selected</button>
|
||||||
|
<button id="move-update-reply-attachment-later" type="button">Move later</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="update-reply-actions">
|
<div class="update-reply-actions">
|
||||||
<button id="send-update-reply" type="button">Send reply</button>
|
<button id="send-update-reply" type="button">Send reply</button>
|
||||||
|
|
@ -1130,10 +1148,19 @@
|
||||||
<label class="issue-attachment-trigger" for="pull-attachment">Choose existing</label>
|
<label class="issue-attachment-trigger" for="pull-attachment">Choose existing</label>
|
||||||
<input class="visually-hidden" id="pull-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple />
|
<input class="visually-hidden" id="pull-attachment" type="file" accept="image/png,image/jpeg,image/webp" multiple />
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-attachment-preview" id="pull-attachment-preview" hidden>
|
<div class="issue-attachment-preview conversation-evidence-review" id="pull-attachment-preview" hidden>
|
||||||
<img id="pull-attachment-image" alt="Selected screenshot preview" />
|
<img id="pull-attachment-image" alt="Selected screenshot preview" />
|
||||||
<span class="small" id="pull-attachment-meta"></span>
|
<span class="small" id="pull-attachment-meta"></span>
|
||||||
<button id="remove-pull-attachment" type="button">Remove screenshot</button>
|
<div class="issue-evidence-tray" id="pull-attachment-tray" role="toolbar" aria-label="Pull-request comment photos" hidden></div>
|
||||||
|
<label class="issue-evidence-note" for="pull-attachment-note">
|
||||||
|
<span id="pull-attachment-note-label">Photo caption (optional)</span>
|
||||||
|
<textarea id="pull-attachment-note" maxlength="240" rows="2" disabled placeholder="What does this photo show?"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="issue-evidence-review-actions" aria-label="Reorder selected photo">
|
||||||
|
<button id="move-pull-attachment-earlier" type="button">Move earlier</button>
|
||||||
|
<button id="remove-pull-attachment" type="button">Remove selected</button>
|
||||||
|
<button id="move-pull-attachment-later" type="button">Move later</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
<button id="send-pull-comment" type="button">Post comment</button>
|
<button id="send-pull-comment" type="button">Post comment</button>
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,40 @@ outbox.flush('timmy').then(result => process.stdout.write(JSON.stringify({{persi
|
||||||
assert output["remaining"] == []
|
assert output["remaining"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_durable_conversation_photo_captions_stay_paired_without_local_binary_storage():
|
||||||
|
script = f"""
|
||||||
|
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
|
||||||
|
const values = new Map(); const mirrors = [];
|
||||||
|
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v)}};
|
||||||
|
const outbox = createAuthoredOutbox({{
|
||||||
|
storage, getOwnerLogin:()=> 'timmy',
|
||||||
|
backgroundSync:{{reconcile:async items=>mirrors.push(items),requestSync:async()=>{{}}}},
|
||||||
|
}});
|
||||||
|
const photo=(filename,note)=>({{filename,contentType:'image/png',blob:new Blob([filename]),note}});
|
||||||
|
(async()=>{{
|
||||||
|
await outbox.enqueueDurably({{
|
||||||
|
kind:'issue-comment',repository:'o/r',number:9,body:'Before and after',operationId:'bundle-note',
|
||||||
|
attachments:[photo('before.png','Before repair'),photo('after.png','After *repair*')],
|
||||||
|
}});
|
||||||
|
const metadata=JSON.parse(values.get('stackchain.authored-outbox.v1')).items[0].attachments;
|
||||||
|
const durable=mirrors[0][0].attachments;
|
||||||
|
process.stdout.write(JSON.stringify({{
|
||||||
|
metadata,durable:durable.map(x=>({{filename:x.filename,note:x.note,text:x.data||null,hasBlob:x.blob instanceof Blob}})),
|
||||||
|
}}));
|
||||||
|
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||||
|
"""
|
||||||
|
output = run_node(script)
|
||||||
|
|
||||||
|
assert output["metadata"] == [
|
||||||
|
{"filename": "before.png", "contentType": "image/png", "stored": True, "note": "Before repair"},
|
||||||
|
{"filename": "after.png", "contentType": "image/png", "stored": True, "note": "After *repair*"},
|
||||||
|
]
|
||||||
|
assert output["durable"] == [
|
||||||
|
{"filename": "before.png", "note": "Before repair", "text": None, "hasBlob": True},
|
||||||
|
{"filename": "after.png", "note": "After *repair*", "text": None, "hasBlob": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_authored_outbox_persists_and_delivers_revision_checked_issue_content():
|
def test_authored_outbox_persists_and_delivers_revision_checked_issue_content():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
|
const createAuthoredOutbox = require({json.dumps(str(OUTBOX))});
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,38 @@ def test_mobile_conversation_composers_accept_five_ordered_photos():
|
||||||
assert "maxFiles: 5" in mount.group(1), controller
|
assert "maxFiles: 5" in mount.group(1), controller
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_conversation_composers_review_reorder_remove_and_caption_every_photo():
|
||||||
|
html = INDEX.read_text()
|
||||||
|
dashboard = DASHBOARD.read_text()
|
||||||
|
css = CSS.read_text()
|
||||||
|
|
||||||
|
composers = (
|
||||||
|
("issue-attachment", "issueAttachmentController"),
|
||||||
|
("pull-attachment", "pullAttachmentController"),
|
||||||
|
("update-reply-attachment", "updateReplyAttachmentController"),
|
||||||
|
)
|
||||||
|
for attachment_id, controller in composers:
|
||||||
|
assert f'id="{attachment_id}-tray"' in html
|
||||||
|
assert f'id="{attachment_id}-note" maxlength="240"' in html
|
||||||
|
assert f'id="move-{attachment_id}-earlier"' in html
|
||||||
|
assert f'id="remove-{attachment_id}"' in html
|
||||||
|
assert f'id="move-{attachment_id}-later"' in html
|
||||||
|
mount = re.search(
|
||||||
|
rf"const {controller} = issueAttachment\.mount\(\{{(.*?)\n \}}\);",
|
||||||
|
dashboard,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert mount, controller
|
||||||
|
assert f"tray: qs('#{attachment_id}-tray')" in mount.group(1)
|
||||||
|
assert f"earlier: qs('#move-{attachment_id}-earlier')" in mount.group(1)
|
||||||
|
assert f"later: qs('#move-{attachment_id}-later')" in mount.group(1)
|
||||||
|
assert f"note: qs('#{attachment_id}-note')" in mount.group(1)
|
||||||
|
|
||||||
|
assert ".conversation-evidence-review" in css
|
||||||
|
assert ".conversation-evidence-review .issue-evidence-review-actions button" in css
|
||||||
|
assert "min-height:44px" in css
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_evidence_bundle_reorders_selected_image_for_serialization_and_upload():
|
def test_mobile_evidence_bundle_reorders_selected_image_for_serialization_and_upload():
|
||||||
script = f"""
|
script = f"""
|
||||||
const attachment = require({json.dumps(str(ATTACHMENT))});
|
const attachment = require({json.dumps(str(ATTACHMENT))});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user