Compare commits
2 Commits
b0dbcb5a25
...
f9d602f85f
| Author | SHA1 | Date | |
|---|---|---|---|
| f9d602f85f | |||
| 2a06cf1daa |
|
|
@ -888,8 +888,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.issue-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
|
.issue-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
|
||||||
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||||
.issue-sheet-header button { min-height:44px; }
|
.issue-sheet-header button { min-height:44px; }
|
||||||
|
.current-work-pin { display:none; }
|
||||||
.mobile-issue-detail-nav, .mobile-pull-detail-nav, .mobile-update-detail-nav, .mobile-review-detail-nav { display:none; }
|
.mobile-issue-detail-nav, .mobile-pull-detail-nav, .mobile-update-detail-nav, .mobile-review-detail-nav { display:none; }
|
||||||
@media (max-width:600px) {
|
@media (max-width:600px) {
|
||||||
|
[data-current-work-pin] { display:inline-flex; align-items:center; justify-content:center; min-width:64px; min-height:44px; padding-inline:12px; }
|
||||||
|
[data-current-work-pin="unpin"] { border-color:#60a5fa; background:#17365a; color:#fff; }
|
||||||
.issue-sheet-panel, .pull-sheet-panel, .update-sheet-panel, .review-sheet-panel { padding-top:max(12px,env(safe-area-inset-top)); }
|
.issue-sheet-panel, .pull-sheet-panel, .update-sheet-panel, .review-sheet-panel { padding-top:max(12px,env(safe-area-inset-top)); }
|
||||||
.mobile-issue-detail-nav, .mobile-pull-detail-nav, .mobile-update-detail-nav, .mobile-review-detail-nav {
|
.mobile-issue-detail-nav, .mobile-pull-detail-nav, .mobile-update-detail-nav, .mobile-review-detail-nav {
|
||||||
position:sticky; top:env(safe-area-inset-top); z-index:6;
|
position:sticky; top:env(safe-area-inset-top); z-index:6;
|
||||||
|
|
|
||||||
|
|
@ -442,6 +442,7 @@
|
||||||
pinnedSection:qs('#mobile-pinned-work'),
|
pinnedSection:qs('#mobile-pinned-work'),
|
||||||
pinnedList:qs('#mobile-pinned-work-list'),
|
pinnedList:qs('#mobile-pinned-work-list'),
|
||||||
pinnedToggle:qs('#mobile-pinned-work-toggle'),
|
pinnedToggle:qs('#mobile-pinned-work-toggle'),
|
||||||
|
detailPins:qsa('[data-current-work-pin]'),
|
||||||
status:qs('#mobile-recent-work-status'),
|
status:qs('#mobile-recent-work-status'),
|
||||||
openRoute:fragment => {
|
openRoute:fragment => {
|
||||||
const sheet = qs('#mobile-queue-sheet');
|
const sheet = qs('#mobile-queue-sheet');
|
||||||
|
|
@ -2082,6 +2083,7 @@
|
||||||
closeOpenWorkSheets();
|
closeOpenWorkSheets();
|
||||||
await openRoutedWorkSection(item);
|
await openRoutedWorkSection(item);
|
||||||
mobileRecentWork.record(item);
|
mobileRecentWork.record(item);
|
||||||
|
mobileRecentWork.setCurrent(item);
|
||||||
const route = createWorkRoute.parse(window.location.hash);
|
const route = createWorkRoute.parse(window.location.hash);
|
||||||
if (route?.section === item.section) navigateWorkSection(item.kind, item.section);
|
if (route?.section === item.section) navigateWorkSection(item.kind, item.section);
|
||||||
},
|
},
|
||||||
|
|
@ -2112,6 +2114,7 @@
|
||||||
qs('#retry-work-route').addEventListener('click', () => workRoute.sync());
|
qs('#retry-work-route').addEventListener('click', () => workRoute.sync());
|
||||||
|
|
||||||
function closeOpenWorkSheets() {
|
function closeOpenWorkSheets() {
|
||||||
|
mobileRecentWork.setCurrent(null);
|
||||||
issueVoiceReply.cancel();
|
issueVoiceReply.cancel();
|
||||||
pullVoiceReply.cancel();
|
pullVoiceReply.cancel();
|
||||||
updateVoiceReply.cancel();
|
updateVoiceReply.cancel();
|
||||||
|
|
|
||||||
|
|
@ -1053,6 +1053,7 @@
|
||||||
<div class="small" id="issue-sheet-key"></div>
|
<div class="small" id="issue-sheet-key"></div>
|
||||||
<h3 id="issue-sheet-title">Assigned issue</h3>
|
<h3 id="issue-sheet-title">Assigned issue</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="current-work-pin" data-current-work-pin type="button" hidden>Pin</button>
|
||||||
<button id="close-issue-sheet" type="button">Close sheet</button>
|
<button id="close-issue-sheet" type="button">Close sheet</button>
|
||||||
</div>
|
</div>
|
||||||
<nav class="mobile-issue-detail-nav" aria-label="Issue sections">
|
<nav class="mobile-issue-detail-nav" aria-label="Issue sections">
|
||||||
|
|
@ -1553,6 +1554,7 @@
|
||||||
<div class="small" id="update-sheet-key"></div>
|
<div class="small" id="update-sheet-key"></div>
|
||||||
<h3 id="update-sheet-title">Unread update</h3>
|
<h3 id="update-sheet-title">Unread update</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="current-work-pin" data-current-work-pin type="button" hidden>Pin</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="update-triage-progress" class="update-triage-progress small" aria-live="polite" hidden></div>
|
<div id="update-triage-progress" class="update-triage-progress small" aria-live="polite" hidden></div>
|
||||||
<nav class="mobile-update-detail-nav" aria-label="Update sections">
|
<nav class="mobile-update-detail-nav" aria-label="Update sections">
|
||||||
|
|
@ -1676,6 +1678,7 @@
|
||||||
<section class="pull-sheet-panel">
|
<section class="pull-sheet-panel">
|
||||||
<div class="pull-sheet-header">
|
<div class="pull-sheet-header">
|
||||||
<div><div class="small" id="pull-sheet-key"></div><h3 id="pull-sheet-title">Assigned pull request</h3></div>
|
<div><div class="small" id="pull-sheet-key"></div><h3 id="pull-sheet-title">Assigned pull request</h3></div>
|
||||||
|
<button class="current-work-pin" data-current-work-pin type="button" hidden>Pin</button>
|
||||||
<button id="close-pull-sheet" type="button">Close</button>
|
<button id="close-pull-sheet" type="button">Close</button>
|
||||||
</div>
|
</div>
|
||||||
<nav class="mobile-detail-nav mobile-pull-detail-nav" aria-label="Pull request sections">
|
<nav class="mobile-detail-nav mobile-pull-detail-nav" aria-label="Pull request sections">
|
||||||
|
|
@ -1912,6 +1915,7 @@
|
||||||
<div class="small" id="review-sheet-key"></div>
|
<div class="small" id="review-sheet-key"></div>
|
||||||
<h3 id="review-sheet-title">Pull request review</h3>
|
<h3 id="review-sheet-title">Pull request review</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="current-work-pin" data-current-work-pin type="button" hidden>Pin</button>
|
||||||
<button class="review-action" id="close-review-sheet">Close</button>
|
<button class="review-action" id="close-review-sheet">Close</button>
|
||||||
</div>
|
</div>
|
||||||
<nav class="mobile-review-detail-nav" aria-label="Review sections">
|
<nav class="mobile-review-detail-nav" aria-label="Review sections">
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,15 @@
|
||||||
let retryAttempt = 0;
|
let retryAttempt = 0;
|
||||||
let operationSequence = 0;
|
let operationSequence = 0;
|
||||||
let pinsExpanded = false;
|
let pinsExpanded = false;
|
||||||
|
let currentItem = null;
|
||||||
|
|
||||||
|
const detailPins = Array.from(options.detailPins || []);
|
||||||
|
detailPins.forEach(button => button.addEventListener?.('click', () => {
|
||||||
|
if (!currentItem) return;
|
||||||
|
const isPinned = pinned().some(item => item.route === currentItem.route);
|
||||||
|
if (isPinned) unpin(currentItem.route);
|
||||||
|
else pin(currentItem);
|
||||||
|
}));
|
||||||
|
|
||||||
options.pinnedToggle?.addEventListener?.('click', () => {
|
options.pinnedToggle?.addEventListener?.('click', () => {
|
||||||
pinsExpanded = !pinsExpanded;
|
pinsExpanded = !pinsExpanded;
|
||||||
|
|
@ -396,6 +405,24 @@
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCurrent() {
|
||||||
|
const isPinned = currentItem && pinned().some(item => item.route === currentItem.route);
|
||||||
|
detailPins.forEach(button => {
|
||||||
|
button.hidden = !currentItem;
|
||||||
|
if (!currentItem) return;
|
||||||
|
button.textContent = isPinned ? 'Pinned' : 'Pin';
|
||||||
|
button.setAttribute('aria-pressed', isPinned ? 'true' : 'false');
|
||||||
|
button.setAttribute('aria-label', (isPinned ? 'Unpin ' : 'Pin ') + currentItem.title);
|
||||||
|
button.setAttribute('data-current-work-pin', isPinned ? 'unpin' : 'pin');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrent(item) {
|
||||||
|
currentItem = normalize(item);
|
||||||
|
renderCurrent();
|
||||||
|
return Boolean(currentItem);
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const recent = items();
|
const recent = items();
|
||||||
const fixed = pinned();
|
const fixed = pinned();
|
||||||
|
|
@ -420,8 +447,9 @@
|
||||||
options.pinnedToggle.setAttribute('aria-expanded', pinsExpanded ? 'true' : 'false');
|
options.pinnedToggle.setAttribute('aria-expanded', pinsExpanded ? 'true' : 'false');
|
||||||
options.pinnedToggle.setAttribute('aria-controls', 'mobile-pinned-work-list');
|
options.pinnedToggle.setAttribute('aria-controls', 'mobile-pinned-work-list');
|
||||||
}
|
}
|
||||||
|
renderCurrent();
|
||||||
return visibleRecent.length + fixed.length;
|
return visibleRecent.length + fixed.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {items, pinned, record, pin, unpin, render, load, sync, startLifecycle, state};
|
return {items, pinned, record, pin, unpin, setCurrent, render, load, sync, startLifecycle, state};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ def test_operator_pins_and_reopens_frequent_work_without_phone_overflow(viewport
|
||||||
pinnedSection:document.querySelector('#mobile-pinned-work'),
|
pinnedSection:document.querySelector('#mobile-pinned-work'),
|
||||||
pinnedList:document.querySelector('#mobile-pinned-work-list'),
|
pinnedList:document.querySelector('#mobile-pinned-work-list'),
|
||||||
pinnedToggle:document.querySelector('#mobile-pinned-work-toggle'),
|
pinnedToggle:document.querySelector('#mobile-pinned-work-toggle'),
|
||||||
|
detailPins:document.querySelectorAll('[data-current-work-pin]'),
|
||||||
status:document.querySelector('#mobile-recent-work-status'),
|
status:document.querySelector('#mobile-recent-work-status'),
|
||||||
openRoute:route => window.opened.push(route),
|
openRoute:route => window.opened.push(route),
|
||||||
});
|
});
|
||||||
|
|
@ -81,4 +82,26 @@ def test_operator_pins_and_reopens_frequent_work_without_phone_overflow(viewport
|
||||||
assert page.evaluate("window.opened") == [
|
assert page.evaluate("window.opened") == [
|
||||||
"#/my-work/issue/stackchain/stackchain-dashboard/20"
|
"#/my-work/issue/stackchain/stackchain-dashboard/20"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
page.evaluate("""() => {
|
||||||
|
document.querySelector('#mobile-queue-sheet').close();
|
||||||
|
document.querySelector('#issue-sheet').classList.add('open');
|
||||||
|
window.recentWork.setCurrent({
|
||||||
|
kind:'issue', repository:'stackchain/stackchain-dashboard', number:1489,
|
||||||
|
title:'Pin the current item from mobile detail',
|
||||||
|
});
|
||||||
|
}""")
|
||||||
|
detail_pin = page.get_by_role(
|
||||||
|
"button", name="Pin Pin the current item from mobile detail"
|
||||||
|
)
|
||||||
|
expect(detail_pin).to_be_visible()
|
||||||
|
bounds = detail_pin.bounding_box()
|
||||||
|
assert bounds and bounds["height"] >= 44
|
||||||
|
assert bounds["x"] >= 0 and bounds["x"] + bounds["width"] <= viewport["width"]
|
||||||
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
|
detail_pin.focus()
|
||||||
|
detail_pin.press("Enter")
|
||||||
|
expect(detail_pin).to_have_text("Pinned")
|
||||||
|
expect(detail_pin).to_have_attribute("aria-pressed", "true")
|
||||||
|
assert page.evaluate("window.recentWork.pinned()[0].number") == 1489
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,69 @@ process.stdout.write(JSON.stringify({{pinnedImmediately,immediate,opened,unpinne
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_detail_pin_follows_route_and_toggles_offline_first():
|
||||||
|
script = f"""
|
||||||
|
const createRecentWork = require({json.dumps(str(RECENT_WORK))});
|
||||||
|
const values=new Map();
|
||||||
|
function button() {{ return {{hidden:true,textContent:'',attributes:{{}},listeners:{{}},
|
||||||
|
setAttribute(name,value){{this.attributes[name]=String(value);}},
|
||||||
|
addEventListener(name,callback){{this.listeners[name]=callback;}},
|
||||||
|
click(){{this.listeners.click?.();}},
|
||||||
|
}}; }}
|
||||||
|
const detailPins=[button(),button(),button(),button()];
|
||||||
|
const recent=createRecentWork({{
|
||||||
|
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value)}},
|
||||||
|
getLogin:()=>'alice',detailPins,debounceMs:99999,
|
||||||
|
}});
|
||||||
|
const issue={{kind:'issue',repository:'stackchain/dashboard',number:1489,title:'Pin current detail'}};
|
||||||
|
const pull={{kind:'pull',repository:'stackchain/api',number:20,title:'Ship API'}};
|
||||||
|
recent.setCurrent(issue);
|
||||||
|
const issueReady=detailPins.map(pin=>({{hidden:pin.hidden,text:pin.textContent,
|
||||||
|
pressed:pin.attributes['aria-pressed'],label:pin.attributes['aria-label'],action:pin.attributes['data-current-work-pin']}}));
|
||||||
|
detailPins[0].click();
|
||||||
|
const pinned={{items:recent.pinned(),state:recent.state(),buttons:detailPins.map(pin=>({{text:pin.textContent,
|
||||||
|
pressed:pin.attributes['aria-pressed'],label:pin.attributes['aria-label'],action:pin.attributes['data-current-work-pin']}}))}};
|
||||||
|
recent.setCurrent(pull);
|
||||||
|
const pullReady={{text:detailPins[1].textContent,label:detailPins[1].attributes['aria-label'],pressed:detailPins[1].attributes['aria-pressed']}};
|
||||||
|
recent.setCurrent(issue);
|
||||||
|
detailPins[2].click();
|
||||||
|
const unpinned={{items:recent.pinned(),recent:recent.items(),state:recent.state(),text:detailPins[0].textContent}};
|
||||||
|
recent.setCurrent(null);
|
||||||
|
process.stdout.write(JSON.stringify({{issueReady,pinned,pullReady,unpinned,hidden:detailPins.map(pin=>pin.hidden)}}));
|
||||||
|
"""
|
||||||
|
payload = run_node(script)
|
||||||
|
|
||||||
|
assert payload["issueReady"] == [
|
||||||
|
{
|
||||||
|
"hidden": False,
|
||||||
|
"text": "Pin",
|
||||||
|
"pressed": "false",
|
||||||
|
"label": "Pin Pin current detail",
|
||||||
|
"action": "pin",
|
||||||
|
}
|
||||||
|
] * 4
|
||||||
|
assert payload["pinned"]["items"][0]["route"] == "#/my-work/issue/stackchain/dashboard/1489"
|
||||||
|
assert payload["pinned"]["state"] == {"pending": True, "pendingCount": 1}
|
||||||
|
assert payload["pinned"]["buttons"] == [
|
||||||
|
{
|
||||||
|
"text": "Pinned",
|
||||||
|
"pressed": "true",
|
||||||
|
"label": "Unpin Pin current detail",
|
||||||
|
"action": "unpin",
|
||||||
|
}
|
||||||
|
] * 4
|
||||||
|
assert payload["pullReady"] == {
|
||||||
|
"text": "Pin",
|
||||||
|
"label": "Pin Ship API",
|
||||||
|
"pressed": "false",
|
||||||
|
}
|
||||||
|
assert payload["unpinned"]["items"] == []
|
||||||
|
assert payload["unpinned"]["recent"] == []
|
||||||
|
assert payload["unpinned"]["state"] == {"pending": True, "pendingCount": 1}
|
||||||
|
assert payload["unpinned"]["text"] == "Pin"
|
||||||
|
assert payload["hidden"] == [True] * 4
|
||||||
|
|
||||||
|
|
||||||
def test_pinned_work_is_compact_deduplicated_and_promotes_on_open():
|
def test_pinned_work_is_compact_deduplicated_and_promotes_on_open():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createRecentWork = require({json.dumps(str(RECENT_WORK))});
|
const createRecentWork = require({json.dumps(str(RECENT_WORK))});
|
||||||
|
|
@ -417,7 +480,12 @@ def test_mobile_queues_integrates_recent_work_with_canonical_detail_routes():
|
||||||
assert "mobileRecentWork.record(item)" in dashboard
|
assert "mobileRecentWork.record(item)" in dashboard
|
||||||
assert "mobileRecentWork.render()" in dashboard
|
assert "mobileRecentWork.render()" in dashboard
|
||||||
assert "workRoute.sync()" in dashboard
|
assert "workRoute.sync()" in dashboard
|
||||||
|
assert html.count('data-current-work-pin') == 4
|
||||||
|
assert "detailPins:qsa('[data-current-work-pin]')" in dashboard
|
||||||
|
assert "mobileRecentWork.setCurrent(item)" in dashboard
|
||||||
|
assert "mobileRecentWork.setCurrent(null)" in dashboard
|
||||||
assert "[data-recent-work-route]" in css
|
assert "[data-recent-work-route]" in css
|
||||||
assert "[data-recent-work-pin]" in css
|
assert "[data-recent-work-pin]" in css
|
||||||
|
assert "[data-current-work-pin]" in css
|
||||||
assert "min-height:44px" in css
|
assert "min-height:44px" in css
|
||||||
assert "min-width:0" in css
|
assert "min-width:0" in css
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user