feat: finish Today plans with inline estimates (Closes #635)
This commit is contained in:
1 parent
528d721cd3
commit
a08a51af02
15 files changed
+98
-23
No files matched your search
@@ -136,6 +136,13 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.plan-today-available input, .plan-today-estimate { box-sizing:border-box; min-height:44px; width:108px; }
|
||||
.plan-today-build { min-height:44px; width:100%; font-weight:700; }
|
||||
.plan-today-build-status { min-height:1.4em; margin-top:6px; color:#bfdbfe; }
|
||||
.plan-today-estimates { margin:12px 0; padding:12px; border:1px solid #31577f; border-radius:12px; background:#10233a; }
|
||||
.plan-today-estimates h3, .plan-today-estimates p { margin-top:0; }
|
||||
.plan-today-estimates input { min-height:44px; width:108px; box-sizing:border-box; }
|
||||
.plan-today-estimate-row { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:10px; padding:8px 0; border-top:1px solid #29496d; }
|
||||
.plan-today-estimate-row label { display:flex; align-items:center; gap:6px; }
|
||||
.plan-today-skipped { display:grid; gap:6px; margin:8px 0; }
|
||||
.plan-today-skipped-item { padding:8px 10px; border-left:3px solid #b45309; background:#261a13; overflow-wrap:anywhere; }
|
||||
.plan-today-estimate-wrap { display:flex; align-items:center; gap:6px; margin-top:8px; }
|
||||
.plan-today-error { min-height:1.4em; color:#fca5a5; }
|
||||
.discard-recap-replan { min-height:44px; margin-bottom:8px; }
|
||||
|
||||
@@ -1399,6 +1399,19 @@
|
||||
('Built ' + build.selected.length + ' ready ' + (build.selected.length === 1 ? 'item' : 'items') +
|
||||
(build.skipped.length ? ' · Skipped ' + build.skipped.length + ' blocked or unverified' : '') +
|
||||
(build.needs_estimate.length ? ' · ' + build.needs_estimate.length + ' need estimates' : '')) : '';
|
||||
const estimateSection = qs('#plan-today-estimates');
|
||||
estimateSection.hidden = !build?.needs_estimate.length;
|
||||
qs('#plan-today-estimate-list').innerHTML = (build?.needs_estimate || []).map(id => {
|
||||
const item = planToday.item(id);
|
||||
const key = escapeHtml(item?.key || ((item?.repository || '') + '#' + (item?.number || '')));
|
||||
return '<div class="plan-today-estimate-row"><div><span class="small">' + key + '</span><strong class="my-work-card-title">' +
|
||||
escapeHtml(item?.title || 'Untitled work') + '</strong></div><label class="small"><input type="number" inputmode="numeric" min="5" max="1440" step="5" data-plan-missing-estimate="' +
|
||||
escAttr(id) + '" aria-label="Estimate for ' + escAttr(item?.title || key) + ' in minutes" /> min</label></div>';
|
||||
}).join('');
|
||||
qs('#plan-today-skipped').innerHTML = (build?.skipped || []).map(entry =>
|
||||
'<div class="small plan-today-skipped-item"><strong>' + escapeHtml(planToday.item(entry.id)?.title || entry.id) +
|
||||
'</strong> · ' + escapeHtml(entry.reason) + '</div>'
|
||||
).join('');
|
||||
qs('#plan-today-list').innerHTML = state.ids.length ? state.ids.map((id, index) =>
|
||||
planTodayItemMarkup(planToday.item(id), true, index)
|
||||
).join('') : '<p class="muted">No work selected yet.</p>';
|
||||
@@ -1434,6 +1447,17 @@
|
||||
planToday.setEstimate(input.dataset.planEstimate, Number(input.value));
|
||||
renderPlanToday();
|
||||
}));
|
||||
document.querySelectorAll('[data-plan-missing-estimate]').forEach(input => input.addEventListener('change', () => {
|
||||
const id = input.dataset.planMissingEstimate;
|
||||
if (!input.checkValidity() || !planToday.setEstimate(id, Number(input.value))) {
|
||||
qs('#plan-today-error').textContent = 'Enter an estimate from 5 minutes to 24 hours.';
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
qs('#plan-today-error').textContent = '';
|
||||
renderPlanToday();
|
||||
document.querySelector('[data-plan-missing-estimate]')?.focus();
|
||||
}));
|
||||
document.querySelectorAll('[data-plan-recommendation]').forEach(button => button.addEventListener('click', () => {
|
||||
planToday.applyRecommendation(button.dataset.planRecommendation);
|
||||
renderPlanToday();
|
||||
|
||||
@@ -256,6 +256,12 @@
|
||||
</label>
|
||||
<button class="plan-today-build" id="build-today-plan" type="button">Build my Today</button>
|
||||
<div class="small plan-today-build-status" id="plan-today-build-status" role="status" aria-live="polite"></div>
|
||||
<section class="plan-today-estimates" id="plan-today-estimates" aria-labelledby="plan-today-estimates-heading" hidden>
|
||||
<h3 id="plan-today-estimates-heading">Estimate to finish your plan</h3>
|
||||
<p class="small muted">Add minutes for ready work and your plan will re-fit immediately.</p>
|
||||
<div id="plan-today-estimate-list"></div>
|
||||
</section>
|
||||
<div class="plan-today-skipped" id="plan-today-skipped"></div>
|
||||
<div class="small plan-today-error" id="plan-today-error" role="alert"></div>
|
||||
<button class="discard-recap-replan" id="discard-recap-replan" type="button" hidden>Discard recap feedback</button>
|
||||
<section aria-labelledby="today-plan-heading">
|
||||
|
||||
@@ -8,6 +8,7 @@ function createPlanToday({ identity, save, start, limit = 5 }) {
|
||||
let recommendationAware = false;
|
||||
let capacityAware = false;
|
||||
let buildState = null;
|
||||
let buildReadiness = null;
|
||||
|
||||
function cleanItems(items) {
|
||||
const unique = new Map();
|
||||
@@ -35,6 +36,7 @@ function createPlanToday({ identity, save, start, limit = 5 }) {
|
||||
recommendations = {};
|
||||
recommendationAware = actualMinutes !== null;
|
||||
buildState = null;
|
||||
buildReadiness = null;
|
||||
for (const [id, minutes] of Object.entries(actualMinutes || {})) {
|
||||
if (draftIds.includes(id) && Number.isInteger(minutes) && minutes >= 5 && minutes <= 1440 &&
|
||||
estimates[id] !== minutes) recommendations[id] = minutes;
|
||||
@@ -77,6 +79,7 @@ function createPlanToday({ identity, save, start, limit = 5 }) {
|
||||
recommendationAware = false;
|
||||
capacityAware = false;
|
||||
buildState = null;
|
||||
buildReadiness = null;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
@@ -91,10 +94,12 @@ function createPlanToday({ identity, save, start, limit = 5 }) {
|
||||
}
|
||||
|
||||
function setEstimate(id, minutes) {
|
||||
if (!openState || !draftIds.includes(id)) return false;
|
||||
const resolvingBuildEstimate = buildState?.needs_estimate?.includes(id);
|
||||
if (!openState || (!draftIds.includes(id) && !resolvingBuildEstimate)) return false;
|
||||
capacityAware = true;
|
||||
if (Number.isInteger(minutes) && minutes > 0) estimates[id] = minutes;
|
||||
else delete estimates[id];
|
||||
if (!Number.isInteger(minutes) || minutes < 5 || minutes > 1440) return false;
|
||||
estimates[id] = minutes;
|
||||
if (resolvingBuildEstimate && buildReadiness) buildRecommendation(buildReadiness);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -109,6 +114,7 @@ function createPlanToday({ identity, save, start, limit = 5 }) {
|
||||
|
||||
function buildRecommendation(readiness = {}) {
|
||||
if (!openState) return null;
|
||||
buildReadiness = { ...readiness };
|
||||
const selected = [];
|
||||
const reasons = {};
|
||||
const skipped = [];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v93';
|
||||
const CACHE = 'stackchain-dashboard-shell-v94';
|
||||
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