feat: surface Today issue blockers (#435)
This commit is contained in:
1 parent
e0de45ea90
commit
05f9e06a10
14 files changed
+270
-23
No files matched your search
@@ -213,6 +213,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||
.issue-sheet-header button { min-height:44px; }
|
||||
.issue-sheet-content { overflow-wrap:anywhere; white-space:pre-wrap; }
|
||||
.issue-blockers { max-width:100%; overflow-x:hidden; margin:16px 0; padding:12px; border:1px solid #b45309; border-radius:12px; background:#291b0c; }
|
||||
.issue-blockers h2 { margin-top:0; }
|
||||
.issue-blocker-list { display:grid; gap:8px; }
|
||||
.issue-blocker { min-width:0; overflow-wrap:anywhere; display:grid; gap:4px; padding:10px; border:1px solid #92400e; border-radius:10px; color:#fef3c7; text-decoration:none; }
|
||||
.issue-blocker:hover, .issue-blocker:focus-visible { border-color:#f59e0b; }
|
||||
.issue-planning { max-width:100%; margin-top:16px; border:1px solid #2a496e; border-radius:12px; padding:0 12px 12px; overflow-x:hidden; }
|
||||
.issue-planning > summary { min-height:44px; display:flex; align-items:center; cursor:pointer; font-weight:700; }
|
||||
.issue-planning-retry { min-height:44px; width:100%; }
|
||||
|
||||
+57
-2
@@ -872,7 +872,9 @@
|
||||
const title = escapeHtml(item.title || 'Untitled work');
|
||||
const controls = selected ?
|
||||
'<div class="plan-today-item-actions"><button type="button" data-plan-move="up" data-plan-id="' + escAttr(id) + '"' + (index === 0 ? ' disabled' : '') + '>Up</button><button type="button" data-plan-move="down" data-plan-id="' + escAttr(id) + '"' + (index === planToday.snapshot().count - 1 ? ' disabled' : '') + '>Down</button><button type="button" data-plan-remove="' + escAttr(id) + '">Remove</button></div>' :
|
||||
'<div class="plan-today-candidate-actions"><button type="button" data-plan-preview="' + escAttr(id) + '">Preview</button><button type="button" data-plan-add="' + escAttr(id) + '">Add</button></div>';
|
||||
(item.kind === 'issue' ?
|
||||
'<div class="plan-today-candidate-actions"><button type="button" data-plan-preview="' + escAttr(id) + '">Preview to add</button></div>' :
|
||||
'<div class="plan-today-candidate-actions"><button type="button" data-plan-preview="' + escAttr(id) + '">Preview</button><button type="button" data-plan-add="' + escAttr(id) + '">Add</button></div>');
|
||||
return '<article class="plan-today-item"><div class="plan-today-item-copy"><span class="small">' + key + '</span><strong class="my-work-card-title">' + title + '</strong></div>' + controls + '</article>';
|
||||
}
|
||||
|
||||
@@ -955,6 +957,7 @@
|
||||
function openPlanPreviewDetail(item, trigger) {
|
||||
qs('#plan-today-sheet').hidden = true;
|
||||
qs('#plan-preview-actions').hidden = false;
|
||||
if (item.kind === 'issue') renderPlanIssueDependencies(null, true);
|
||||
if (offlineWorkMode) {
|
||||
openRoutedWork(item, trigger);
|
||||
} else if (item.kind === 'update' && item.has_update) {
|
||||
@@ -981,19 +984,68 @@
|
||||
onClose: (_item, trigger) => {
|
||||
closeOpenWorkSheets();
|
||||
qs('#plan-preview-actions').hidden = true;
|
||||
qs('#issue-blockers').hidden = true;
|
||||
qs('#issue-blocker-list').textContent = '';
|
||||
qs('#add-plan-preview').disabled = false;
|
||||
qs('#add-plan-preview').textContent = 'Add to Today & back';
|
||||
qs('#plan-today-sheet').hidden = false;
|
||||
renderPlanToday();
|
||||
requestAnimationFrame(() => trigger?.focus());
|
||||
},
|
||||
});
|
||||
|
||||
function renderPlanIssueDependencies(detail, loading = false) {
|
||||
const preview = planTodayPreview.snapshot();
|
||||
if (!preview.open || preview.item?.kind !== 'issue') return;
|
||||
const panel = qs('#issue-blockers');
|
||||
const list = qs('#issue-blocker-list');
|
||||
const status = qs('#issue-blocker-status');
|
||||
const addButton = qs('#add-plan-preview');
|
||||
panel.hidden = false;
|
||||
list.innerHTML = '';
|
||||
addButton.disabled = loading;
|
||||
addButton.dataset.planOverride = '';
|
||||
if (loading) {
|
||||
status.textContent = 'Checking unresolved blockers…';
|
||||
addButton.textContent = 'Checking blockers…';
|
||||
return;
|
||||
}
|
||||
const available = detail?.dependencies_available === true;
|
||||
const dependencies = Array.isArray(detail?.dependencies) ? detail.dependencies : [];
|
||||
planTodayPreview.setDependencies({ available, dependencies });
|
||||
if (!available) {
|
||||
status.textContent = 'Blocker status unavailable. Adding requires an explicit override.';
|
||||
addButton.textContent = 'Add without blocker status anyway & back';
|
||||
addButton.dataset.planOverride = 'true';
|
||||
return;
|
||||
}
|
||||
if (dependencies.length) {
|
||||
list.innerHTML = dependencies.map(blocker =>
|
||||
'<a class="issue-blocker" href="' + escAttr(blocker.url || '#') + '" target="_blank" rel="noopener noreferrer"><strong>' +
|
||||
escapeHtml(blocker.repository + '#' + blocker.number) + ' · ' + escapeHtml(blocker.title || 'Untitled blocker') +
|
||||
'</strong><span class="small">State: ' + escapeHtml(blocker.state || 'open') + '</span></a>'
|
||||
).join('');
|
||||
status.textContent = dependencies.length + (dependencies.length === 1 ? ' unresolved blocker.' : ' unresolved blockers.');
|
||||
addButton.textContent = 'Add blocked item anyway & back';
|
||||
addButton.dataset.planOverride = 'true';
|
||||
return;
|
||||
}
|
||||
panel.hidden = true;
|
||||
status.textContent = '';
|
||||
addButton.textContent = 'Add to Today & back';
|
||||
}
|
||||
|
||||
let addPlanPreviewOnReturn = false;
|
||||
let addPlanPreviewOverride = false;
|
||||
|
||||
qs('#back-to-plan').addEventListener('click', () => {
|
||||
addPlanPreviewOnReturn = false;
|
||||
addPlanPreviewOverride = false;
|
||||
taskOverlayHistory.close();
|
||||
});
|
||||
qs('#add-plan-preview').addEventListener('click', () => {
|
||||
addPlanPreviewOnReturn = true;
|
||||
addPlanPreviewOverride = qs('#add-plan-preview').dataset.planOverride === 'true';
|
||||
taskOverlayHistory.close();
|
||||
});
|
||||
|
||||
@@ -1783,6 +1835,7 @@
|
||||
const detail = offlineDetail || await issueController.load(item);
|
||||
if (selectedIssue !== item) return;
|
||||
selectedIssueDetail = detail;
|
||||
renderPlanIssueDependencies(detail);
|
||||
issueConversation = issueController.conversation(item, detail.conversation);
|
||||
qs('#issue-sheet-title').textContent = detail.title || 'Assigned issue';
|
||||
qs('#issue-sheet-body').innerHTML = renderMarkdown(detail.body || 'No description provided.');
|
||||
@@ -2936,9 +2989,11 @@
|
||||
qs('#open-palette').focus();
|
||||
}
|
||||
if (previous === 'plan-today-preview' && kind !== 'plan-today-preview') {
|
||||
if (addPlanPreviewOnReturn) planTodayPreview.close({ add:true });
|
||||
if (addPlanPreviewOnReturn && addPlanPreviewOverride) planTodayPreview.close({ add:true, override:true });
|
||||
else if (addPlanPreviewOnReturn) planTodayPreview.close({ add:true });
|
||||
else planTodayPreview.close();
|
||||
addPlanPreviewOnReturn = false;
|
||||
addPlanPreviewOverride = false;
|
||||
}
|
||||
if (previous === 'plan-today' && kind !== 'plan-today' && kind !== 'plan-today-preview') closePlanToday(false);
|
||||
if (kind === 'new' && previous !== 'new') openCreateIssueSheet(false);
|
||||
|
||||
@@ -262,6 +262,11 @@
|
||||
<button class="issue-retry" id="retry-issue-load" type="button" hidden>Reload latest issue</button>
|
||||
<div class="row"><span id="issue-labels"></span><span class="small" id="issue-assignees"></span></div>
|
||||
<div class="issue-sheet-content markdown-content" id="issue-sheet-body"></div>
|
||||
<section class="issue-blockers" id="issue-blockers" aria-labelledby="issue-blockers-title" hidden>
|
||||
<h2 id="issue-blockers-title">Blocked by</h2>
|
||||
<div class="issue-blocker-list" id="issue-blocker-list"></div>
|
||||
<div id="issue-blocker-status" class="small" aria-live="polite"></div>
|
||||
</section>
|
||||
<h2>Full conversation</h2>
|
||||
<div id="issue-comments"></div>
|
||||
<button class="conversation-more" id="load-older-issue-comments" type="button" hidden>Load older messages</button>
|
||||
|
||||
@@ -22,8 +22,19 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function close({ add = false } = {}) {
|
||||
function setDependencies({ available, dependencies } = {}) {
|
||||
if (!current) return false;
|
||||
current.dependencies_available = available === true;
|
||||
current.dependencies = Array.isArray(dependencies) ? dependencies.filter(item => item?.state === 'open') : [];
|
||||
current.requires_override = !current.dependencies_available || current.dependencies.length > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
function close({ add = false, override = false } = {}) {
|
||||
if (!current) return 'closed';
|
||||
if (add && current.requires_override && !override) {
|
||||
return current.dependencies_available ? 'blocked' : 'dependencies-unavailable';
|
||||
}
|
||||
const { item, trigger, scroll } = current;
|
||||
let result = 'returned';
|
||||
if (add) {
|
||||
@@ -37,9 +48,16 @@
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
return current ? { open:true, item:current.item, trigger:current.trigger, scroll:current.scroll } : { open:false };
|
||||
if (!current) return { open:false };
|
||||
const state = { open:true, item:current.item, trigger:current.trigger, scroll:current.scroll };
|
||||
if (Object.prototype.hasOwnProperty.call(current, 'dependencies_available')) {
|
||||
state.dependencies_available = current.dependencies_available;
|
||||
state.dependencies = current.dependencies;
|
||||
state.requires_override = current.requires_override;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
return { open, close, snapshot };
|
||||
return { open, close, setDependencies, snapshot };
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v72';
|
||||
const CACHE = 'stackchain-dashboard-shell-v73';
|
||||
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