feat: route mobile My Work sheets (#205)
This commit is contained in:
1 parent
5e826bcc7e
commit
9e697dc63f
3 files changed
+361
-34
No files matched your search
+108
-33
@@ -71,7 +71,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.my-work-card:hover { border-color:var(--accent); }
|
||||
.my-work-card-title { display:block; margin:5px 0; font-weight:650; }
|
||||
.mark-update-read { min-height:44px; width:100%; }
|
||||
.read-update { min-height:44px; width:100%; }
|
||||
.read-update { min-height:44px; width:100%; display:flex; align-items:center; justify-content:center; }
|
||||
.load-more-notifications { min-height:44px; width:100%; margin-top:10px; }
|
||||
.load-more-notifications[hidden] { display:none; }
|
||||
.load-more-work { min-height:44px; width:100%; margin-top:10px; }
|
||||
@@ -287,6 +287,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<div class="small" id="notification-page-status" aria-live="polite"></div>
|
||||
<button class="load-more-notifications" id="load-more-notifications" type="button" hidden>Load older updates</button>
|
||||
<div class="small" id="my-work-action-status" aria-live="assertive"></div>
|
||||
<div class="small" id="work-route-share-status" aria-live="polite"></div>
|
||||
<div class="my-work-bulk" id="bulk-mark-read-bar" hidden>
|
||||
<button id="bulk-mark-read" type="button"></button>
|
||||
</div>
|
||||
@@ -454,6 +455,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<div id="issue-comment-status" class="small" aria-live="assertive"></div>
|
||||
</section>
|
||||
<div class="issue-sheet-actions">
|
||||
<button class="share-work-route" type="button">Share</button>
|
||||
<button id="release-issue" type="button">Release assignment</button>
|
||||
<button id="close-issue" type="button">Close issue</button>
|
||||
<a id="open-issue-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||
@@ -539,6 +541,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<div id="update-reply-status" class="small" aria-live="assertive"></div>
|
||||
</section>
|
||||
<div class="update-sheet-actions">
|
||||
<button class="share-work-route" type="button">Share</button>
|
||||
<button id="mark-update-read-next" type="button">Mark read & next</button>
|
||||
<a id="open-update-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||
</div>
|
||||
@@ -571,6 +574,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<div id="pull-comment-status" class="small" aria-live="assertive"></div>
|
||||
</section>
|
||||
<div class="pull-sheet-actions">
|
||||
<button class="share-work-route" type="button">Share</button>
|
||||
<button id="merge-pull" type="button" disabled>Merge</button>
|
||||
<a id="open-pull-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a>
|
||||
</div>
|
||||
@@ -594,7 +598,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<div id="review-sheet-status" class="small" aria-live="polite">Choose a review request.</div>
|
||||
<button class="review-retry" id="retry-review-load" hidden>Retry loading review</button>
|
||||
<p class="review-sheet-body" id="review-sheet-body"></p>
|
||||
<div class="row"><span class="pill" id="review-ci-state">CI unknown</span><a id="open-review-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a></div>
|
||||
<div class="row"><span class="pill" id="review-ci-state">CI unknown</span><button class="share-work-route" type="button">Share</button><a id="open-review-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a></div>
|
||||
<h2>Changed files</h2>
|
||||
<div id="review-files" class="muted"></div>
|
||||
<section class="review-inline-composer" id="review-inline-composer" aria-labelledby="review-inline-title" hidden>
|
||||
@@ -654,6 +658,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
<script src="static/create-issue-sheet.js"></script>
|
||||
<script src="static/pull-sheet.js"></script>
|
||||
<script src="static/review-sheet.js"></script>
|
||||
<script src="static/work-route.js"></script>
|
||||
<script src="static/context-poller.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
@@ -917,6 +922,48 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
onClose: () => closeUpdateSheet(false),
|
||||
});
|
||||
|
||||
function routedWorkItem(item) {
|
||||
if (item?.has_update && item.kind === 'update') {
|
||||
return { ...item, kind:'update' };
|
||||
}
|
||||
return { ...item, kind:item?.is_review ? 'review' : item?.kind };
|
||||
}
|
||||
|
||||
function openRoutedWork(item, trigger, options = {}) {
|
||||
if (!item) return;
|
||||
if (item.has_update && item.kind === 'update') updateTrigger = trigger;
|
||||
else if (item.is_review) reviewTrigger = trigger;
|
||||
else if (item.kind === 'issue') issueTrigger = trigger;
|
||||
else if (item.kind === 'pull') pullTrigger = trigger;
|
||||
workRoute.open(routedWorkItem(item), options);
|
||||
}
|
||||
|
||||
const workRoute = createWorkRoute.createController({
|
||||
location: window.location,
|
||||
history: window.history,
|
||||
eventTarget: window,
|
||||
onOpen: item => {
|
||||
closeOpenWorkSheets();
|
||||
if (item.kind === 'update') notificationReader.open(item, lastMyWork);
|
||||
else if (item.kind === 'review') openReviewSheet(item, reviewTrigger);
|
||||
else if (item.kind === 'issue') openIssueSheet(item, issueTrigger);
|
||||
else if (item.kind === 'pull') openPullSheet(item, pullTrigger);
|
||||
},
|
||||
onClose: () => {
|
||||
if (selectedIssue) closeIssueSheet(false);
|
||||
if (selectedPull) closePullSheet(false);
|
||||
if (selectedReview) closeReviewSheet(false);
|
||||
if (selectedUpdate) closeUpdateSheet(true, false);
|
||||
},
|
||||
onInvalid: () => {
|
||||
window.history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
closeOpenWorkSheets();
|
||||
qs('#my-work-action-status').textContent = 'Route unavailable · this item is no longer in My Work.';
|
||||
qs('#my-work').scrollIntoView({ block:'start' });
|
||||
},
|
||||
});
|
||||
workRoute.start();
|
||||
|
||||
function closeOpenWorkSheets() {
|
||||
['#issue-sheet', '#pull-sheet', '#review-sheet', '#update-sheet'].forEach(selector =>
|
||||
qs(selector).classList.remove('open')
|
||||
@@ -930,20 +977,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
|
||||
function openWorkSessionItem(item) {
|
||||
closeOpenWorkSheets();
|
||||
if (selectedWorkFilter === 'update' && item.has_update && Number.isInteger(item.notification_id)) {
|
||||
updateTrigger = null;
|
||||
notificationReader.open(item, lastMyWork);
|
||||
} else if (item.is_review) {
|
||||
openReviewSheet(item, null);
|
||||
} else if (item.kind === 'issue') {
|
||||
openIssueSheet(item, null);
|
||||
} else if (item.kind === 'pull') {
|
||||
openPullSheet(item, null);
|
||||
} else if (item.has_update && Number.isInteger(item.notification_id)) {
|
||||
updateTrigger = null;
|
||||
notificationReader.open(item, lastMyWork);
|
||||
}
|
||||
openRoutedWork(item, null, { replace:true });
|
||||
}
|
||||
|
||||
const workSession = createWorkSession({
|
||||
@@ -1093,6 +1127,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
const incomplete = activeWorkStreams().some(stream => workPagination[stream]?.has_more);
|
||||
qs('#my-work-list').innerHTML = visible.length ? visible.map(item => {
|
||||
const index = lastMyWork.findIndex(candidate => candidate.key === item.key && candidate.kind === item.kind);
|
||||
const routeItem = routedWorkItem(item);
|
||||
const routeHref = createWorkRoute.serialize(routeItem);
|
||||
const contents =
|
||||
'<span class="small">' + escapeHtml(item.key) + ' · ' + escapeHtml(item.kind === 'pull' ? 'PR' : (item.kind === 'update' ? 'Update' : 'Issue')) + '</span>' +
|
||||
'<span class="my-work-card-title">' + escapeHtml(item.title) + '</span>' +
|
||||
@@ -1104,35 +1140,36 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
const markRead = item.has_update && Number.isInteger(item.notification_id) ?
|
||||
'<button class="mark-update-read" data-notification-id="' + item.notification_id + '">Mark read</button>' : '';
|
||||
const readUpdate = item.has_update && Number.isInteger(item.notification_id) ?
|
||||
'<button class="read-update" data-update-index="' + index + '">Read update</button>' : '';
|
||||
'<a class="read-update" href="' + escAttr(createWorkRoute.serialize({ kind:'update', notification_id:item.notification_id })) + '" data-update-index="' + index + '">Read update</a>' : '';
|
||||
if (item.is_review) {
|
||||
return '<article class="my-work-card"><button class="my-work-card-main review-trigger" data-review-index="' + index + '">' + contents + '</button>' + readUpdate + markRead + '</article>';
|
||||
return '<article class="my-work-card"><a class="my-work-card-main review-trigger" href="' + escAttr(routeHref) + '" data-review-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + '</article>';
|
||||
}
|
||||
if (item.kind === 'issue') {
|
||||
return '<article class="my-work-card"><button class="my-work-card-main issue-trigger" data-issue-index="' + index + '">' + contents + '</button>' + readUpdate + markRead + '</article>';
|
||||
return '<article class="my-work-card"><a class="my-work-card-main issue-trigger" href="' + escAttr(routeHref) + '" data-issue-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + '</article>';
|
||||
}
|
||||
if (item.kind === 'pull') {
|
||||
return '<article class="my-work-card"><button class="my-work-card-main pull-trigger" data-pull-index="' + index + '">' + contents + '</button>' + readUpdate + markRead + '</article>';
|
||||
return '<article class="my-work-card"><a class="my-work-card-main pull-trigger" href="' + escAttr(routeHref) + '" data-pull-index="' + index + '">' + contents + '</a>' + readUpdate + markRead + '</article>';
|
||||
}
|
||||
return '<article class="my-work-card"><a class="my-work-card-main" href="' + escAttr(item.url) + '" target="_blank" rel="noopener noreferrer">' + contents + '</a>' + readUpdate + markRead + '</article>';
|
||||
return '<article class="my-work-card"><a class="my-work-card-main update-trigger" href="' + escAttr(routeHref) + '" data-update-index="' + index + '">' + contents + '</a>' + markRead + '</article>';
|
||||
}).join('') : '<div class="muted">' + (incomplete ?
|
||||
'More work is available. Load the next page.' :
|
||||
'No ' + (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))) + '.') + '</div>';
|
||||
document.querySelectorAll('[data-review-index]').forEach(button => {
|
||||
button.addEventListener('click', () => openReviewSheet(lastMyWork[Number(button.dataset.reviewIndex)], button));
|
||||
button.addEventListener('click', event => { event.preventDefault(); openRoutedWork(lastMyWork[Number(button.dataset.reviewIndex)], button); });
|
||||
});
|
||||
document.querySelectorAll('[data-issue-index]').forEach(button => {
|
||||
button.addEventListener('click', () => openIssueSheet(lastMyWork[Number(button.dataset.issueIndex)], button));
|
||||
button.addEventListener('click', event => { event.preventDefault(); openRoutedWork(lastMyWork[Number(button.dataset.issueIndex)], button); });
|
||||
});
|
||||
document.querySelectorAll('[data-pull-index]').forEach(button => {
|
||||
button.addEventListener('click', () => openPullSheet(lastMyWork[Number(button.dataset.pullIndex)], button));
|
||||
button.addEventListener('click', event => { event.preventDefault(); openRoutedWork(lastMyWork[Number(button.dataset.pullIndex)], button); });
|
||||
});
|
||||
document.querySelectorAll('[data-update-index]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
button.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
const item = lastMyWork[Number(button.dataset.updateIndex)];
|
||||
if (!item) return;
|
||||
updateTrigger = button;
|
||||
notificationReader.open(item, lastMyWork);
|
||||
workRoute.open({ ...item, kind:'update' });
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-notification-id]').forEach(button => {
|
||||
@@ -1162,6 +1199,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
bulkButton.textContent = bulkConfirmationPending ?
|
||||
'Confirm marking ' + bulkLabel + ' read' :
|
||||
'Mark ' + bulkLabel + ' read';
|
||||
workRoute.setItems(lastMyWork);
|
||||
}
|
||||
|
||||
function reviewFileElement(filename) {
|
||||
@@ -1327,7 +1365,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
}
|
||||
|
||||
function closeIssueSheet() {
|
||||
function closeIssueSheet(navigate = true) {
|
||||
if (navigate && createWorkRoute.parse(window.location.hash)) {
|
||||
workRoute.close();
|
||||
return;
|
||||
}
|
||||
qs('#issue-sheet').classList.remove('open');
|
||||
selectedIssue = null;
|
||||
selectedIssueDetail = null;
|
||||
@@ -1425,7 +1467,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
}
|
||||
|
||||
function closePullSheet() {
|
||||
function closePullSheet(navigate = true) {
|
||||
if (navigate && createWorkRoute.parse(window.location.hash)) {
|
||||
workRoute.close();
|
||||
return;
|
||||
}
|
||||
qs('#pull-sheet').classList.remove('open');
|
||||
selectedPull = null;
|
||||
selectedPullDetail = null;
|
||||
@@ -1481,7 +1527,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
closeFindWorkSheet();
|
||||
refreshMyWorkView();
|
||||
qs('#my-work-action-status').textContent = confirmed.repository + '#' + confirmed.number + ' assigned to you.';
|
||||
openIssueSheet(claimed, qs('#find-work'));
|
||||
openRoutedWork(claimed, qs('#find-work'));
|
||||
} catch (error) {
|
||||
qs('#find-work-status').textContent = error.message + ' Refresh and retry.';
|
||||
button.disabled = false;
|
||||
@@ -1699,7 +1745,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
}
|
||||
|
||||
function closeReviewSheet() {
|
||||
function closeReviewSheet(navigate = true) {
|
||||
if (navigate && createWorkRoute.parse(window.location.hash)) {
|
||||
workRoute.close();
|
||||
return;
|
||||
}
|
||||
qs('#review-sheet').classList.remove('open');
|
||||
selectedReview = null;
|
||||
progress = null;
|
||||
@@ -1710,7 +1760,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
if (reviewTrigger?.isConnected) reviewTrigger.focus();
|
||||
}
|
||||
|
||||
function closeUpdateSheet(restoreTrigger = true) {
|
||||
function closeUpdateSheet(restoreTrigger = true, navigate = true) {
|
||||
if (navigate && createWorkRoute.parse(window.location.hash)) {
|
||||
workRoute.close();
|
||||
return;
|
||||
}
|
||||
qs('#update-sheet').classList.remove('open');
|
||||
selectedUpdate = null;
|
||||
if (restoreTrigger && updateTrigger?.isConnected) updateTrigger.focus();
|
||||
@@ -1961,7 +2015,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
);
|
||||
if (!item) return false;
|
||||
searchPreview.close();
|
||||
await openIssueSheet(item, qs('#find-work'));
|
||||
openRoutedWork(item, qs('#find-work'));
|
||||
return true;
|
||||
}
|
||||
function runCommandItem(item) {
|
||||
@@ -2056,6 +2110,16 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
closePullSheet();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape' && selectedReview) {
|
||||
e.preventDefault();
|
||||
closeReviewSheet();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape' && selectedUpdate) {
|
||||
e.preventDefault();
|
||||
closeUpdateSheet();
|
||||
return;
|
||||
}
|
||||
if ((e.metaKey||e.ctrlKey) && e.key==='k') {
|
||||
e.preventDefault();
|
||||
if (qs('#cmd-palette').classList.contains('open')) {
|
||||
@@ -2065,6 +2129,17 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
});
|
||||
qs('#close-search-preview').addEventListener('click', closeSearchPreview);
|
||||
document.querySelectorAll('.share-work-route').forEach(button => {
|
||||
button.addEventListener('click', async () => {
|
||||
const status = qs('#work-route-share-status');
|
||||
try {
|
||||
const result = await createWorkRoute.share(window.location.href, navigator, navigator.clipboard);
|
||||
status.textContent = result === 'shared' ? 'Work link shared.' : 'Work link copied.';
|
||||
} catch (error) {
|
||||
status.textContent = error?.name === 'AbortError' ? 'Share canceled.' : 'Could not share this work link.';
|
||||
}
|
||||
});
|
||||
});
|
||||
qs('#claim-search-result').addEventListener('click', async () => {
|
||||
if (!searchPreviewDetail || (!searchPreviewDetail.claimable && !searchPreviewDetail.assigned_to_me)) return;
|
||||
try {
|
||||
@@ -2134,7 +2209,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
closeCreateIssueSheet();
|
||||
refreshMyWorkView();
|
||||
qs('#my-work-action-status').textContent = created.key + ' created and assigned to you.';
|
||||
openIssueSheet(created, qs('#new-issue'));
|
||||
openRoutedWork(created, qs('#new-issue'));
|
||||
} catch (error) {
|
||||
qs('#create-issue-status').textContent = error.message + ' Your draft is safe; retry.';
|
||||
button.disabled = false;
|
||||
|
||||
Reference in new issue
Block a user