feat: report Today blockers and move on (Closes #1066)
CI / lint (pull_request) Successful in 2m41s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 2m7s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 06:38:55 +00:00
1 parent 339baab7ee
commit 4009a4a4c1
9 files changed
+265 -5

No files matched your search

+3
View File
@@ -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; }
+2 -1
View File
@@ -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();
+7
View File
@@ -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 &amp; 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>
+86 -2
View File
@@ -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) {