feat: report Today blockers and move on (Closes #1066)
This commit is contained in:
1 parent
339baab7ee
commit
4009a4a4c1
9 files changed
+265
-5
No files matched your search
@@ -288,6 +288,9 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.today-progress-evidence { display:grid; gap:8px; min-width:0; margin-top:12px; }
|
||||
.today-progress-evidence .issue-attachment-preview { width:100%; box-sizing:border-box; }
|
||||
.today-progress-evidence .issue-evidence-note textarea { min-height:72px; }
|
||||
.today-progress-blocker { display:grid; gap:8px; margin-top:14px; padding:12px; border:1px solid #875f2a; border-radius:12px; background:#21180d; }
|
||||
.today-progress-blocker input, .today-progress-blocker button { box-sizing:border-box; width:100%; min-height:44px; }
|
||||
.today-progress-blocker p { margin:0; }
|
||||
.today-progress-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; }
|
||||
.today-progress-actions button { width:100%; }
|
||||
.today-progress-panel .voice-conversation { margin-top:10px; }
|
||||
|
||||
@@ -1959,7 +1959,7 @@
|
||||
return {
|
||||
identity:todayWork.identity(item), kind:item.kind, repository:item.repository,
|
||||
number:item.number, label:item.key || item.repository + '#' + item.number,
|
||||
title:item.title || '',
|
||||
title:item.title || '', item,
|
||||
};
|
||||
}
|
||||
const todayProgressView = createTodayProgressView({
|
||||
@@ -1968,6 +1968,7 @@
|
||||
activity:mountTodayProgressActivity(qs,fetchReviewJson,actionHydrator,renderMarkdown),
|
||||
announce:message => { qs('#my-work-action-status').textContent = message; },
|
||||
onAdmitted:() => refreshMyWorkView(),
|
||||
moveOn:(target,until) => detailDefer.deferUntil(target.item,until),
|
||||
});
|
||||
function selectTodayWork() {
|
||||
qs('[data-work-filter="today"]').click();
|
||||
|
||||
@@ -1601,6 +1601,13 @@
|
||||
</div>
|
||||
</section>
|
||||
<p class="small muted">Save privately on this device, or post to the exact Gitea item. Today keeps running either way.</p>
|
||||
<section class="today-progress-blocker" aria-labelledby="today-progress-blocker-title">
|
||||
<strong id="today-progress-blocker-title">Blocked?</strong>
|
||||
<label for="today-blocker-return-at">Return this item to Later at</label>
|
||||
<input id="today-blocker-return-at" type="datetime-local" />
|
||||
<button id="post-today-blocker" type="button">Post blocker & move on</button>
|
||||
<p class="small muted">Posts this update first, then defers the item and continues Today.</p>
|
||||
</section>
|
||||
<p id="today-progress-status" class="small" role="status" aria-live="polite"></p>
|
||||
<div class="today-progress-actions">
|
||||
<button id="save-today-progress" type="button">Save to this item</button>
|
||||
|
||||
@@ -129,7 +129,54 @@ maxLength = 2000, maxItems = 20 }) {
|
||||
return admission;
|
||||
}
|
||||
|
||||
return { load, save, discard:identity => save(identity, ''), post };
|
||||
async function postBlocker(target, value, attachments = [], completeEvidence, until, transition) {
|
||||
if (!validTarget(target)) throw new Error('An active Today issue or pull request is required.');
|
||||
if (typeof transition !== 'function') throw new Error('Today planning is unavailable.');
|
||||
let record = read()[target.identity];
|
||||
let admission = { background:true, alreadyAdmitted:true };
|
||||
if (record?.blocker_pending !== true) {
|
||||
const body = String(value ?? record?.body ?? '').trim();
|
||||
if (!body) throw new Error('Describe what is blocking this Today item.');
|
||||
const evidence = (Array.isArray(attachments) ? attachments : [attachments]).filter(Boolean).slice(0, 5);
|
||||
if (!save(target.identity, body, evidence)) {
|
||||
throw new Error('Blocker update could not be saved on this device.');
|
||||
}
|
||||
record = read()[target.identity];
|
||||
if (typeof admit !== 'function') throw new Error('Progress update delivery is unavailable.');
|
||||
admission = await admit({
|
||||
kind:target.kind + '-comment', repository:target.repository, number:target.number,
|
||||
body:record.body, operationId:record.operation_id,
|
||||
...(evidence.length ? {attachments:evidence} : {}),
|
||||
});
|
||||
const drafts = read();
|
||||
if (drafts[target.identity]?.operation_id === record.operation_id) {
|
||||
drafts[target.identity] = { ...drafts[target.identity], blocker_pending:true, blocker_until:String(until || '') };
|
||||
write(drafts);
|
||||
}
|
||||
} else {
|
||||
until = record.blocker_until;
|
||||
}
|
||||
try {
|
||||
if (typeof completeEvidence === 'function') await completeEvidence();
|
||||
} catch (_error) {
|
||||
const error = new Error('Blocker is already queued; photo cleanup is pending.');
|
||||
error.deliveryAdmitted = true;
|
||||
throw error;
|
||||
}
|
||||
if (await transition(target, until) !== true) {
|
||||
const error = new Error('Blocker was posted, but Today could not move on. Retry the planning step.');
|
||||
error.deliveryAdmitted = true;
|
||||
throw error;
|
||||
}
|
||||
const drafts = read();
|
||||
if (drafts[target.identity]?.operation_id === record.operation_id) {
|
||||
delete drafts[target.identity];
|
||||
write(drafts);
|
||||
}
|
||||
return admission;
|
||||
}
|
||||
|
||||
return { load, save, discard:identity => save(identity, ''), post, postBlocker };
|
||||
}
|
||||
|
||||
function createTodayProgressActivity({ fetchJson, createPager, getActions, onActions, surfaceStatus, paint = () => {}, setStatus = () => {} }) {
|
||||
@@ -247,7 +294,7 @@ function mountTodayProgressActivity(qs, fetchJson, actionSource = {}, render) {
|
||||
return activity;
|
||||
}
|
||||
|
||||
function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {} }) {
|
||||
function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {}, moveOn = null }) {
|
||||
const sheet = qs('#today-progress-sheet');
|
||||
const body = qs('#today-progress-body');
|
||||
const status = qs('#today-progress-status');
|
||||
@@ -310,6 +357,43 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, voice, a
|
||||
announce('Progress update saved privately to this Today item.');
|
||||
close();
|
||||
});
|
||||
const blockerButton = qs('#post-today-blocker');
|
||||
blockerButton?.addEventListener('click', async () => {
|
||||
const target = currentTarget();
|
||||
if (!target || target.identity !== openedTarget?.identity) {
|
||||
status.textContent = 'The active Today item changed. Close and open its update again.';
|
||||
return;
|
||||
}
|
||||
const returnInput = qs('#today-blocker-return-at');
|
||||
const wakeAt = new Date(returnInput?.value || '');
|
||||
if (!body.value.trim()) {
|
||||
status.textContent = 'Describe what is blocking this Today item.';
|
||||
body.focus();
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(wakeAt.getTime()) || wakeAt.getTime() <= Date.now()) {
|
||||
status.textContent = 'Choose a valid future return time.';
|
||||
returnInput?.focus?.();
|
||||
return;
|
||||
}
|
||||
blockerButton.disabled = true;
|
||||
status.textContent = 'Posting blocker before moving Today…';
|
||||
try {
|
||||
await photos?.checkpoint?.();
|
||||
const attachments = await photos?.serialize?.() || [];
|
||||
const admission = await progress.postBlocker(
|
||||
target, body.value, attachments, () => photos?.complete?.(), wakeAt.toISOString(), moveOn
|
||||
);
|
||||
onAdmitted(admission);
|
||||
announce('Blocker queued, deferred to Later, and Today moved on.');
|
||||
close();
|
||||
} catch (error) {
|
||||
status.textContent = error.deliveryAdmitted ? error.message : error.message + ' Your blocker remains here; retry.';
|
||||
body.focus();
|
||||
} finally {
|
||||
blockerButton.disabled = false;
|
||||
}
|
||||
});
|
||||
qs('#post-today-progress').addEventListener('click', async () => {
|
||||
const target = currentTarget();
|
||||
if (!target || target.identity !== openedTarget?.identity) {
|
||||
|
||||
Reference in new issue
Block a user