feat: follow pull requests from mobile Search (Closes #1307)
This commit is contained in:
1 parent
41c28cc70a
commit
7131792ded
23 files changed
+271
-83
No files matched your search
+21
-16
@@ -13,6 +13,11 @@
|
||||
let snapshot = {revision:0, items:[]};
|
||||
let review = null;
|
||||
|
||||
function sameItem(left, right) {
|
||||
return (left?.kind || 'issue') === (right?.kind || 'issue') &&
|
||||
left?.repository === right?.repository && Number(left?.number) === Number(right?.number);
|
||||
}
|
||||
|
||||
function publish(status, error) {
|
||||
const state = {status, revision:snapshot.revision, items:[...snapshot.items]};
|
||||
state.degraded = snapshot.degraded === true;
|
||||
@@ -36,7 +41,8 @@
|
||||
if (requestGeneration !== generation) return snapshot;
|
||||
snapshot = {
|
||||
revision:Number(result?.revision) || 0,
|
||||
items:Array.isArray(result?.items) ? result.items.slice(0, 50) : [],
|
||||
items:Array.isArray(result?.items) ? result.items.slice(0, 50)
|
||||
.map(item => ({...item, kind:item.kind === 'pull' ? 'pull' : 'issue'})) : [],
|
||||
degraded:result?.degraded === true,
|
||||
refreshFailures:Number(result?.refresh_failures) || 0,
|
||||
};
|
||||
@@ -49,13 +55,12 @@
|
||||
}
|
||||
|
||||
async function acknowledge(item) {
|
||||
const current = snapshot.items.find(candidate =>
|
||||
candidate.repository === item?.repository && Number(candidate.number) === Number(item?.number) &&
|
||||
const current = snapshot.items.find(candidate => sameItem(candidate, item) &&
|
||||
candidate.updated_at === item?.updated_at);
|
||||
if (!current || current.has_unseen_change !== true || typeof options.onAcknowledge !== 'function') return false;
|
||||
await options.onAcknowledge(current);
|
||||
current.has_unseen_change = false;
|
||||
review?.acknowledged.add(current.repository + '#' + current.number + '@' + current.updated_at);
|
||||
review?.acknowledged.add(current.kind + ':' + current.repository + '#' + current.number + '@' + current.updated_at);
|
||||
publish('ready');
|
||||
return true;
|
||||
}
|
||||
@@ -63,7 +68,7 @@
|
||||
async function open(index) {
|
||||
const item = snapshot.items[Number(index)];
|
||||
if (!item) return false;
|
||||
await options.onOpen?.({...item, kind:'issue', following:true});
|
||||
await options.onOpen?.({...item, following:true});
|
||||
await acknowledge(item);
|
||||
return true;
|
||||
}
|
||||
@@ -71,11 +76,10 @@
|
||||
async function startReview() {
|
||||
const items = snapshot.items
|
||||
.filter(item => item.has_unseen_change === true)
|
||||
.map(item => ({...item, kind:'issue', following:true}));
|
||||
.map(item => ({...item, following:true}));
|
||||
if (!items.length) return false;
|
||||
review = {items, more:false, active:true, acknowledged:new Set()};
|
||||
await open(snapshot.items.indexOf(snapshot.items.find(item =>
|
||||
item.repository === items[0].repository && Number(item.number) === Number(items[0].number))));
|
||||
await open(snapshot.items.indexOf(snapshot.items.find(item => sameItem(item, items[0]))));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -92,8 +96,7 @@
|
||||
}
|
||||
|
||||
function retire(item) {
|
||||
const same = candidate => candidate.repository === item?.repository &&
|
||||
Number(candidate.number) === Number(item?.number);
|
||||
const same = candidate => sameItem(candidate, item);
|
||||
const index = review?.active ? review.items.findIndex(same) : -1;
|
||||
snapshot.items = snapshot.items.filter(candidate => !same(candidate));
|
||||
if (index < 0) {
|
||||
@@ -140,21 +143,22 @@
|
||||
const status = query('#following-status');
|
||||
const reviewButton = query('#review-following');
|
||||
query('#retry-following').hidden = state.status !== 'error';
|
||||
if (state.status === 'loading') return void (status.textContent = 'Loading watched issues…');
|
||||
if (state.status === 'loading') return void (status.textContent = 'Loading watched items…');
|
||||
if (state.status === 'error') return void (status.textContent = state.error?.message || 'Following is temporarily unavailable.');
|
||||
const unseen = state.items.filter(item => item.has_unseen_change === true).length;
|
||||
reviewButton.hidden = unseen === 0;
|
||||
reviewButton.textContent = unseen === 1 ? 'Review new activity' : 'Review ' + unseen + ' new changes';
|
||||
status.textContent = (state.reviewSummary
|
||||
? 'Reviewed ' + state.reviewSummary.reviewed + ' changes · ' + state.reviewSummary.remaining + ' still need review. '
|
||||
: '') + (state.degraded ? 'Some watched issues could not be refreshed. Showing last known details. ' : '') + (state.items.length
|
||||
? state.items.length + (state.items.length === 1 ? ' watched issue.' : ' watched issues.')
|
||||
: 'No watched issues yet. Watch one from Search to keep it here.');
|
||||
: '') + (state.degraded ? 'Some watched items could not be refreshed. Showing last known details. ' : '') + (state.items.length
|
||||
? state.items.length + (state.items.length === 1 ? ' watched item.' : ' watched items.')
|
||||
: 'No watched items yet. Watch an issue or pull request from Search to keep it here.');
|
||||
list.innerHTML = state.items.map((item, index) =>
|
||||
'<button class="following-card' + (item.has_unseen_change ? ' has-unseen-change' : '') +
|
||||
'" type="button" data-following-index="' + index + '"><span>' +
|
||||
(item.has_unseen_change ? '<em>New activity</em>' : '') + '<strong>' +
|
||||
escapeHtml(item.title) + '</strong><small>' + escapeHtml(item.repository + ' #' + item.number +
|
||||
escapeHtml(item.title) + '</strong><small>' + escapeHtml(
|
||||
(item.kind === 'pull' ? 'Pull request' : 'Issue') + ' · ' + item.repository + ' #' + item.number +
|
||||
' · ' + item.state + ' · ' + formatTime(item.updated_at)) +
|
||||
'</small></span><span aria-hidden="true">›</span></button>').join('');
|
||||
list.querySelectorAll('[data-following-index]').forEach(button => button.addEventListener('click', () => {
|
||||
@@ -176,7 +180,8 @@
|
||||
onAcknowledge:item => {
|
||||
const [owner, repo] = item.repository.split('/');
|
||||
return fetchJson('api/v1/following/' + encodeURIComponent(owner) + '/' +
|
||||
encodeURIComponent(repo) + '/issues/' + item.number + '/seen', {
|
||||
encodeURIComponent(repo) + '/issues/' + item.number +
|
||||
'/seen?kind=' + encodeURIComponent(item.kind), {
|
||||
method:'PUT', headers:{'Content-Type':'application/json', Accept:'application/json'},
|
||||
body:JSON.stringify({updated_at:item.updated_at}),
|
||||
});
|
||||
|
||||
+1
-1
@@ -2016,7 +2016,7 @@
|
||||
<button data-mobile-queue="delivery" type="button"><span><strong>Delivery</strong><small>Needs recovery</small></span><span data-mobile-queue-count="delivery">0</span></button>
|
||||
<button data-mobile-queue="attention" type="button"><span><strong>Attention</strong><small>Needs a response</small></span><span data-mobile-queue-count="attention">0</span></button>
|
||||
<button data-mobile-queue="update" type="button" aria-label="Updates, 0 unread conversations"><span><strong>Updates</strong><small>Unread conversations</small></span><span data-mobile-queue-count="update">0</span></button>
|
||||
<button data-mobile-queue="following" type="button" aria-label="Following, 0 watched issues"><span><strong>Following</strong><small>Issues you watch</small></span><span data-mobile-queue-count="following">0</span></button>
|
||||
<button data-mobile-queue="following" type="button" aria-label="Following, 0 unseen changes"><span><strong>Following</strong><small>Issues and pull requests you watch</small></span><span data-mobile-queue-count="following">0</span></button>
|
||||
<button data-mobile-queue="filed" type="button"><span><strong>Filed</strong><small>Issues you delegated</small></span><span data-mobile-queue-count="filed">0</span></button>
|
||||
<button data-mobile-queue="later" type="button"><span><strong>Later</strong><small>Deferred work</small></span><span data-mobile-queue-count="later">0</span></button>
|
||||
<button data-mobile-queue="draft" type="button"><span><strong>Drafts</strong><small>Unfiled captures</small></span><span data-mobile-queue-count="draft">0</span></button>
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
root.searchPreviewSubscriptionOptions = fetchJson => {
|
||||
const options = {
|
||||
load:async detail => {
|
||||
if (detail.kind === 'issue' && detail.state === 'closed' && detail.following === true) {
|
||||
if (['issue', 'pull'].includes(detail.kind) && detail.state === 'closed' && detail.following === true) {
|
||||
return {...detail, watching:true};
|
||||
}
|
||||
if (!(detail.kind === 'issue' && detail.state === 'open')) return detail;
|
||||
if (!(['issue', 'pull'].includes(detail.kind) && detail.state === 'open')) return detail;
|
||||
const result = await fetchJson(root.searchPreviewSubscriptionPath(detail), {headers:{Accept:'application/json'}});
|
||||
return {...detail, watching:result.watching === true};
|
||||
},
|
||||
@@ -57,15 +57,17 @@
|
||||
watching:'Starting watch…', unwatching:'Stopping watch…',
|
||||
watched:'Watching · available in Following. Future activity will appear in Updates.',
|
||||
unwatched:'Stopped watching · removed from Following. Assignment and planning are unchanged.',
|
||||
'watch-partial':'Watching in Gitea, but Following could not sync. Tap Stop watching, then Watch issue to repair.',
|
||||
'watch-partial':'Watching in Gitea, but Following could not sync. Tap Stop watching, then Watch ' +
|
||||
(state.detail?.kind === 'pull' ? 'pull request' : 'issue') + ' to repair.',
|
||||
'watch-error':(state.error?.message || 'Watch status was not changed.') + ' Retry.',
|
||||
})[state.status] || '';
|
||||
root.renderSearchPreviewWatch = (detail, state, button) => {
|
||||
const retiring = detail.kind === 'issue' && detail.state === 'closed' &&
|
||||
const watchableKind = ['issue', 'pull'].includes(detail.kind);
|
||||
const retiring = watchableKind && detail.state === 'closed' &&
|
||||
detail.following === true && detail.watching === true;
|
||||
button.hidden = !(detail.kind === 'issue' && detail.state === 'open') && !retiring;
|
||||
button.hidden = !(watchableKind && detail.state === 'open') && !retiring;
|
||||
button.textContent = retiring ? 'Stop watching & next' :
|
||||
(detail.watching ? 'Stop watching' : 'Watch issue');
|
||||
(detail.watching ? 'Stop watching' : 'Watch ' + (detail.kind === 'pull' ? 'pull request' : 'issue'));
|
||||
button.disabled = state.status === 'watching' || state.status === 'unwatching';
|
||||
};
|
||||
root.wireSearchPreviewWatch = (button, preview, getDetail) => button.addEventListener('click', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/private-data-registry.js');
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v134';
|
||||
const CACHE = 'stackchain-dashboard-shell-v135';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
||||
Reference in new issue
Block a user