499 lines
25 KiB
JavaScript
499 lines
25 KiB
JavaScript
(function (root, factory) {
|
|
const createSearchBatchPlan = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = createSearchBatchPlan;
|
|
if (root) {
|
|
root.createSearchBatchPlan = createSearchBatchPlan;
|
|
root.mountSearchBatchPlanning = createSearchBatchPlan.mount;
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
function createSearchBatchPlan({
|
|
limit = 50,
|
|
resolve = item => Promise.resolve(item),
|
|
claim = item => Promise.resolve(item),
|
|
onChange = () => {},
|
|
} = {}) {
|
|
const maximum = Number.isInteger(limit) && limit > 0 ? limit : 50;
|
|
const selected = new Map();
|
|
let active = false;
|
|
|
|
const identity = item => [item?.kind || '', item?.repository || '', item?.number || ''].join(':');
|
|
const eligible = item => item?.kind === 'issue' && item?.state === 'open' &&
|
|
Boolean(item.repository) && Number.isInteger(item.number);
|
|
function snapshot() {
|
|
return { active, count:selected.size, items:Array.from(selected.values()) };
|
|
}
|
|
function changed() {
|
|
const state = snapshot();
|
|
onChange(state);
|
|
return state;
|
|
}
|
|
function start() {
|
|
active = true;
|
|
selected.clear();
|
|
return changed();
|
|
}
|
|
function cancel() {
|
|
active = false;
|
|
selected.clear();
|
|
return changed();
|
|
}
|
|
function toggle(item) {
|
|
if (!active) return 'inactive';
|
|
if (!eligible(item)) return 'ineligible';
|
|
const key = identity(item);
|
|
if (selected.has(key)) {
|
|
selected.delete(key);
|
|
changed();
|
|
return 'removed';
|
|
}
|
|
if (selected.size >= maximum) return 'limit';
|
|
selected.set(key, { ...item });
|
|
changed();
|
|
return 'selected';
|
|
}
|
|
async function prepare(item) {
|
|
const detail = await resolve(item);
|
|
if (detail?.assigned_to_me) return detail;
|
|
if (detail?.claimable) return claim(detail);
|
|
throw new Error('This issue is no longer available to assign.');
|
|
}
|
|
function releaseRepository() {
|
|
const repositories = new Set(Array.from(selected.values()).map(item => item.repository));
|
|
return repositories.size === 1 ? repositories.values().next().value : null;
|
|
}
|
|
return { identity, eligible, start, cancel, toggle, snapshot, prepare, releaseRepository, limit:maximum };
|
|
}
|
|
|
|
function createWeekBatchPlan({
|
|
week,
|
|
prepare = item => Promise.resolve(item),
|
|
accept = item => item,
|
|
identity = item => String(item?.repository || '') + '#' + String(item?.number || ''),
|
|
maxItems = 5,
|
|
onProgress = () => {},
|
|
} = {}) {
|
|
let running = null;
|
|
const load = async () => {
|
|
await week.load();
|
|
return week.dates().map(day => ({ ...day, ...week.day(day.date) }));
|
|
};
|
|
function validate(items, assignments, days, confirmOverload = false) {
|
|
const allowed = new Set(days.map(day => day.date));
|
|
const errors = [];
|
|
const simulated = new Map(days.map(day => [day.date, {
|
|
ids:[...(day.ids || [])], estimates:{...(day.estimates || {})},
|
|
capacity_minutes:Number(day.capacity_minutes) || 0,
|
|
}]));
|
|
items.forEach(item => {
|
|
const id = identity(item), choice = assignments?.[id] || {};
|
|
const estimate = Number(choice.estimate);
|
|
if (!allowed.has(choice.date)) errors.push({ id, reason:'date-required' });
|
|
if (!Number.isFinite(estimate) || estimate <= 0) errors.push({ id, reason:'estimate-required' });
|
|
if (!allowed.has(choice.date) || !Number.isFinite(estimate) || estimate <= 0) return;
|
|
simulated.forEach((day, date) => {
|
|
if (day.ids.includes(id) && choice.date !== date) {
|
|
day.ids = day.ids.filter(value => value !== id); delete day.estimates[id];
|
|
}
|
|
});
|
|
const destination = simulated.get(choice.date);
|
|
if (!destination.ids.includes(id) && destination.ids.length >= maxItems) {
|
|
errors.push({ id, reason:'day-full', date:choice.date, limit:maxItems }); return;
|
|
}
|
|
if (!destination.ids.includes(id)) destination.ids.push(id);
|
|
destination.estimates[id] = estimate;
|
|
});
|
|
const overloads = [];
|
|
simulated.forEach((day, date) => {
|
|
const planned = day.ids.reduce((sum, id) => sum + (Number(day.estimates[id]) || 0), 0);
|
|
if (day.capacity_minutes > 0 && planned > day.capacity_minutes) {
|
|
overloads.push({ date, planned_minutes:planned, capacity_minutes:day.capacity_minutes });
|
|
}
|
|
});
|
|
if (errors.length) return { status:'invalid', errors, overloads:[] };
|
|
if (overloads.length && !confirmOverload) return { status:'overload-confirmation-required', errors:[], overloads };
|
|
return { status:'ready', errors:[], overloads };
|
|
}
|
|
async function preview(items) {
|
|
const days = await load();
|
|
return { days, placements:Object.fromEntries(items.map(item => [identity(item), week.placement(identity(item))])) };
|
|
}
|
|
function run(items, assignments, { confirmOverload = false } = {}) {
|
|
if (running) return running;
|
|
running = (async () => {
|
|
const selected = Array.isArray(items) ? items.slice() : [];
|
|
const days = await load();
|
|
const checked = validate(selected, assignments, days, confirmOverload);
|
|
if (checked.status !== 'ready') return checked;
|
|
const failed = [], planned = [];
|
|
for (let index = 0; index < selected.length; index += 1) {
|
|
const source = selected[index], originalId = identity(source), choice = assignments[originalId];
|
|
try {
|
|
const confirmed = accept(await prepare(source)) || source;
|
|
const id = identity(confirmed);
|
|
const existing = week.placement(id);
|
|
if (!week.place(id, choice.date, Number(choice.estimate), {move:Boolean(existing && existing.date !== choice.date)})) {
|
|
failed.push({id:originalId, reason:'assigned-not-planned'});
|
|
} else {
|
|
week.rememberPendingItem?.(id, confirmed);
|
|
planned.push(id);
|
|
}
|
|
} catch (error) {
|
|
failed.push({id:originalId, reason:error?.message || 'assignment failed'});
|
|
}
|
|
onProgress({status:'running',processed:index + 1,selected:selected.length});
|
|
}
|
|
if (planned.length) {
|
|
try { await week.flush(); }
|
|
catch (_error) { return {status:'planned-pending',planned:planned.length,failed}; }
|
|
}
|
|
return {status:failed.length?'partial':'planned',planned:planned.length,failed};
|
|
})().finally(() => { running = null; });
|
|
return running;
|
|
}
|
|
return { preview, validate, run, pending:() => Boolean(running) };
|
|
}
|
|
createSearchBatchPlan.createWeekBatchPlan = createWeekBatchPlan;
|
|
|
|
createSearchBatchPlan.mount = function mountSearchBatchPlanning(
|
|
document, batchFactory, todayWork, getOwner, fetchJson, previewPath, queueToday,
|
|
todaySync, acceptIssue, lookup, render, escapeHtml, escapeAttribute,
|
|
laterWork = null, laterPicker = null, weekPlan = null, storage = null
|
|
) {
|
|
const get = selector => document.querySelector(selector);
|
|
let processor;
|
|
let laterProcessor;
|
|
let releaseProcessor;
|
|
let releaseContext = null;
|
|
const plan = createSearchBatchPlan({
|
|
resolve:item => fetchJson(previewPath(item), {headers:{Accept:'application/json'}}),
|
|
claim:detail => fetchJson(
|
|
'api/v1/repos/' + detail.repository.split('/').map(encodeURIComponent).join('/') +
|
|
'/issues/' + encodeURIComponent(detail.number) + '/claim',
|
|
{method:'PATCH', headers:{Accept:'application/json'}}
|
|
),
|
|
onChange:state => {
|
|
get('#search-batch-actions').hidden = !state.active;
|
|
get('#select-search-results').hidden = state.active;
|
|
get('#queue-selected-search-results').disabled = state.count === 0;
|
|
get('#defer-selected-search-results').disabled = state.count === 0;
|
|
get('#plan-selected-search-results').disabled = state.count === 0;
|
|
if (get('#week-selected-search-results')) get('#week-selected-search-results').disabled = state.count === 0;
|
|
get('#search-selection-status').textContent = state.count ?
|
|
state.count + ' issue' + (state.count === 1 ? '' : 's') + ' selected.' : 'No issues selected.';
|
|
render();
|
|
},
|
|
});
|
|
processor = batchFactory({
|
|
capacity:() => Math.max(0, todayWork.limit - todayWork.read().length),
|
|
owner:getOwner,
|
|
timeBudget:() => {
|
|
const planning = todayWork.planning();
|
|
return {
|
|
capacity_minutes:planning.capacity_minutes,
|
|
planned_minutes:Object.values(planning.estimates || {}).reduce((sum, minutes) => sum + minutes, 0),
|
|
};
|
|
},
|
|
journalName:'search-today-batch',
|
|
autoMount:false,
|
|
claim:item => plan.prepare(item),
|
|
queue:confirmed => queueToday(acceptIssue(confirmed)),
|
|
persistEstimate:(confirmed, minutes) => {
|
|
const planning = todayWork.planning();
|
|
planning.estimates[todayWork.identity(confirmed)] = minutes;
|
|
todayWork.replacePlanning(planning);
|
|
todaySync.enqueueConfiguration(planning.capacity_minutes, planning.estimates);
|
|
todaySync.flush();
|
|
},
|
|
onProgress:progress => {
|
|
get('#queue-selected-search-results').disabled = progress.status === 'running';
|
|
if (progress.status === 'running') {
|
|
get('#search-selection-status').textContent = 'Planning ' + progress.processed + ' of ' + progress.selected + '…';
|
|
} else if (progress.status === 'full') {
|
|
get('#search-selection-status').textContent = 'Today has ' + progress.available + ' remaining slot' +
|
|
(progress.available === 1 ? '.' : 's.');
|
|
} else if (progress.status === 'estimates-required') {
|
|
get('#search-batch-estimate-summary').textContent = 'Estimate every selected issue.';
|
|
} else if (progress.status === 'over-budget') {
|
|
get('#search-batch-estimate-summary').textContent = progress.requested_minutes +
|
|
' min selected · ' + progress.remaining_minutes + ' min remain.';
|
|
} else if (progress.status === 'complete') {
|
|
get('#cmd-search-action-status').textContent = progress.failed.length ?
|
|
progress.queued.length + ' queued · ' + progress.failed.length + ' need retry.' :
|
|
progress.queued.length + ' added to Today.';
|
|
get('#resume-search-batch').hidden = processor.pending() === 0;
|
|
closeEstimateReview();
|
|
if (!progress.failed.length) plan.cancel();
|
|
}
|
|
},
|
|
});
|
|
laterProcessor = batchFactory({
|
|
capacity:() => plan.limit,
|
|
owner:getOwner,
|
|
journalName:'search-later-batch',
|
|
queueFailureReason:'assigned but Later could not be saved',
|
|
autoMount:false,
|
|
claim:item => plan.prepare(item),
|
|
queue:(confirmed, context) => {
|
|
const outcome = laterWork?.defer(acceptIssue(confirmed), context?.until);
|
|
return outcome === 'deferred' ? 'queued' : outcome;
|
|
},
|
|
onProgress:progress => {
|
|
get('#defer-selected-search-results').disabled = progress.status === 'running';
|
|
if (progress.status === 'running') {
|
|
get('#search-selection-status').textContent = 'Deferring ' + progress.processed + ' of ' + progress.selected + '…';
|
|
} else if (progress.status === 'complete') {
|
|
get('#cmd-search-action-status').textContent = progress.failed.length ?
|
|
progress.queued.length + ' deferred · ' + progress.failed.length + ' need retry.' :
|
|
progress.queued.length + ' added to Later.';
|
|
restore();
|
|
if (!progress.failed.length) plan.cancel();
|
|
}
|
|
},
|
|
});
|
|
releaseProcessor = batchFactory({
|
|
capacity:() => plan.limit,
|
|
owner:getOwner,
|
|
journalName:'search-release-batch',
|
|
queueFailureReason:'assigned but release planning could not be confirmed',
|
|
autoMount:false,
|
|
claim:item => plan.prepare(item),
|
|
queue:async (confirmed, context) => {
|
|
const issuePath = 'api/v1/repos/' + confirmed.repository.split('/').map(encodeURIComponent).join('/') +
|
|
'/issues/' + encodeURIComponent(confirmed.number);
|
|
const releasePlan = await fetchJson(issuePath + '/release-plan', {
|
|
method:'PATCH', headers:{Accept:'application/json','Content-Type':'application/json'},
|
|
body:JSON.stringify({milestone_id:context.milestone_id, due_date:context.due_date || null}),
|
|
});
|
|
if (releasePlan?.number !== confirmed.number ||
|
|
releasePlan?.milestone?.id !== context.milestone_id ||
|
|
(context.due_date && releasePlan?.due_date !== context.due_date)) {
|
|
throw new Error('Issue release plan was not confirmed.');
|
|
}
|
|
return 'queued';
|
|
},
|
|
onProgress:progress => {
|
|
get('#confirm-search-release').disabled = progress.status === 'running';
|
|
if (progress.status === 'running') {
|
|
get('#search-release-summary').textContent = 'Planning ' + progress.processed + ' of ' + progress.selected + '…';
|
|
} else if (progress.status === 'complete') {
|
|
get('#cmd-search-action-status').textContent = progress.failed.length ?
|
|
progress.queued.length + ' planned · ' + progress.failed.length + ' need retry.' :
|
|
progress.queued.length + ' planned for ' + (releaseContext?.milestone_title || 'release') + '.';
|
|
get('#resume-search-batch').hidden = releaseProcessor.pending() === 0;
|
|
closeReleaseReview();
|
|
if (!progress.failed.length) plan.cancel();
|
|
}
|
|
},
|
|
});
|
|
const weekProcessor = weekPlan ? createWeekBatchPlan({
|
|
week:weekPlan,prepare:item=>plan.prepare(item),accept:acceptIssue,
|
|
identity:item=>todayWork.identity(item),
|
|
onProgress:progress=>{
|
|
if (progress.status === 'running') get('#search-week-batch-summary').textContent =
|
|
'Planning ' + progress.processed + ' of ' + progress.selected + '…';
|
|
},
|
|
storage,
|
|
}) : null;
|
|
let weekOverload = false;
|
|
function weekAssignments() {
|
|
const values = {};
|
|
document.querySelectorAll('[data-search-week-batch-date]').forEach(select => {
|
|
const id = select.dataset.searchWeekBatchDate;
|
|
const estimate = document.querySelector('[data-search-week-batch-estimate="' + id + '"]');
|
|
values[id] = {date:select.value,estimate:Number(estimate?.value)};
|
|
});
|
|
return values;
|
|
}
|
|
function closeWeekReview() {
|
|
if (!get('#search-week-batch-review')) return;
|
|
get('#search-week-batch-review').hidden = true;
|
|
get('#search-batch-actions').hidden = !plan.snapshot().active;
|
|
weekOverload = false;
|
|
get('#confirm-search-week-batch').textContent = 'Assign & plan';
|
|
}
|
|
async function openWeekReview() {
|
|
const items = plan.snapshot().items;
|
|
if (!weekProcessor || !items.length) return;
|
|
const button = get('#week-selected-search-results');
|
|
button.disabled = true;
|
|
get('#search-selection-status').textContent = 'Loading Week Ahead…';
|
|
try {
|
|
const preview = await weekProcessor.preview(items);
|
|
const options = selected => preview.days.map(day => '<option value="' + escapeAttribute(day.date) + '"' +
|
|
(day.date === selected ? ' selected' : '') + '>' + escapeHtml(day.label + ' · ' + (day.ids || []).length + ' planned') + '</option>').join('');
|
|
get('#search-week-batch-list').innerHTML = items.map(item => {
|
|
const id = todayWork.identity(item), existing = preview.placements[id];
|
|
return '<div class="search-week-batch-row"><div><span class="small">' + escapeHtml(id) + '</span><strong>' +
|
|
escapeHtml(item.title || 'Untitled work') + '</strong></div><div class="search-week-batch-row-controls"><label>Day<select data-search-week-batch-date="' +
|
|
escapeAttribute(id) + '">' + options(existing?.date || preview.days[0]?.date) + '</select></label><label>Minutes<input type="number" inputmode="numeric" min="5" max="1440" step="5" value="' +
|
|
escapeAttribute(existing?.estimate || '') + '" data-search-week-batch-estimate="' + escapeAttribute(id) +
|
|
'" aria-label="Estimate for ' + escapeAttribute(item.title || id) + ' in minutes"></label></div></div>';
|
|
}).join('');
|
|
get('#search-week-batch-summary').textContent = items.length + ' selected · review every day and estimate before assignment.';
|
|
get('#search-batch-actions').hidden = true;
|
|
get('#search-week-batch-review').hidden = false;
|
|
document.querySelector('[data-search-week-batch-date]')?.focus();
|
|
} catch (error) {
|
|
get('#search-selection-status').textContent = error?.message || 'Week Ahead is unavailable. Retry when connected.';
|
|
} finally { button.disabled = false; }
|
|
}
|
|
async function confirmWeekReview() {
|
|
if (!weekProcessor || weekProcessor.pending()) return;
|
|
const button = get('#confirm-search-week-batch');
|
|
button.disabled = true;
|
|
const outcome = await weekProcessor.run(plan.snapshot().items, weekAssignments(), {confirmOverload:weekOverload});
|
|
if (outcome.status === 'invalid') {
|
|
const reasons = [...new Set(outcome.errors.map(error => error.reason))];
|
|
get('#search-week-batch-summary').textContent = reasons.includes('day-full') ?
|
|
'A day already has five items. Choose another day.' : 'Choose a valid day and estimate for every issue.';
|
|
} else if (outcome.status === 'overload-confirmation-required') {
|
|
weekOverload = true;
|
|
button.textContent = 'Confirm over capacity';
|
|
get('#search-week-batch-summary').textContent = outcome.overloads.map(day =>
|
|
day.planned_minutes + ' of ' + day.capacity_minutes + ' min on ' + day.date).join(' · ') + '. Confirm to plan anyway.';
|
|
} else {
|
|
const pending = outcome.status === 'planned-pending';
|
|
get('#cmd-search-action-status').textContent = outcome.planned + ' planned' +
|
|
(pending ? ' · sync pending' : '') + (outcome.failed.length ? ' · ' + outcome.failed.length + ' need attention.' : '.');
|
|
closeWeekReview();
|
|
if (!outcome.failed.length) plan.cancel();
|
|
}
|
|
button.disabled = false;
|
|
}
|
|
function estimateValues() {
|
|
return Object.fromEntries(Array.from(document.querySelectorAll('[data-search-batch-estimate]')).map(input =>
|
|
[input.dataset.searchBatchEstimate, Number(input.value)]
|
|
));
|
|
}
|
|
function closeEstimateReview() {
|
|
get('#search-batch-estimate-review').hidden = true;
|
|
get('#search-batch-actions').hidden = !plan.snapshot().active;
|
|
}
|
|
function openEstimateReview(items) {
|
|
const planning = todayWork.planning();
|
|
const planned = Object.values(planning.estimates || {}).reduce((sum, minutes) => sum + minutes, 0);
|
|
const remaining = Math.max(0, planning.capacity_minutes - planned);
|
|
get('#search-batch-estimate-summary').textContent = remaining +
|
|
' min remain. Estimate before assigning.';
|
|
get('#search-batch-estimate-list').innerHTML = items.map(item => {
|
|
const id = String(item.repository || '') + '#' + String(item.number || '');
|
|
const estimate = planning.estimates?.[id] || '';
|
|
return '<div class="search-batch-estimate-row"><div><span class="small">' + escapeHtml(id) +
|
|
'</span><strong>' + escapeHtml(item.title || 'Untitled work') + '</strong></div><label class="small">' +
|
|
'<input type="number" inputmode="numeric" min="5" max="1440" step="5" value="' +
|
|
escapeAttribute(estimate) + '" data-search-batch-estimate="' + escapeAttribute(id) +
|
|
'" aria-label="Estimate for ' + escapeAttribute(item.title || id) + ' in minutes" /> min</label></div>';
|
|
}).join('');
|
|
get('#search-batch-actions').hidden = true;
|
|
get('#search-batch-estimate-review').hidden = false;
|
|
document.querySelector('[data-search-batch-estimate]')?.focus();
|
|
}
|
|
function closeReleaseReview() {
|
|
get('#search-release-review').hidden = true;
|
|
get('#search-batch-actions').hidden = !plan.snapshot().active;
|
|
}
|
|
async function openReleaseReview() {
|
|
const items = plan.snapshot().items;
|
|
const repository = plan.releaseRepository();
|
|
if (!items.length) return;
|
|
if (!repository) {
|
|
get('#search-selection-status').textContent = 'Select issues from one repository to plan a release.';
|
|
return;
|
|
}
|
|
get('#plan-selected-search-results').disabled = true;
|
|
get('#search-selection-status').textContent = 'Loading release milestones…';
|
|
try {
|
|
const milestones = await fetchJson(
|
|
'api/v1/repos/' + repository.split('/').map(encodeURIComponent).join('/') + '/milestones',
|
|
{headers:{Accept:'application/json'}}
|
|
);
|
|
const open = (Array.isArray(milestones) ? milestones : []).filter(item => item?.state !== 'closed');
|
|
get('#search-release-milestone').innerHTML = '<option value="">Choose milestone</option>' + open.map(item =>
|
|
'<option value="' + Number(item.id) + '">' + escapeHtml(item.title) + '</option>'
|
|
).join('');
|
|
get('#search-release-list').innerHTML = items.map(item => '<div class="search-release-row"><span>' +
|
|
escapeHtml(item.repository + ' #' + item.number) + '</span><strong>' +
|
|
escapeHtml(item.title || 'Untitled work') + '</strong></div>').join('');
|
|
get('#search-release-summary').textContent = items.length + ' issues in ' + repository + '. Assignment starts only after confirmation.';
|
|
get('#search-batch-actions').hidden = true;
|
|
get('#search-release-review').hidden = false;
|
|
get('#search-release-milestone').focus();
|
|
} catch (error) {
|
|
get('#search-selection-status').textContent = error?.message || 'Milestones could not be loaded. Try again.';
|
|
} finally {
|
|
get('#plan-selected-search-results').disabled = false;
|
|
}
|
|
}
|
|
get('#select-search-results').addEventListener('click', () => plan.start());
|
|
get('#cancel-search-selection').addEventListener('click', () => plan.cancel());
|
|
get('#queue-selected-search-results').addEventListener('click', () => {
|
|
const items = plan.snapshot().items;
|
|
const capacity = todayWork.planning().capacity_minutes;
|
|
if (Number.isInteger(capacity) && capacity > 0) openEstimateReview(items);
|
|
else processor.run(items);
|
|
});
|
|
get('#defer-selected-search-results').addEventListener('click', event => {
|
|
const items = plan.snapshot().items;
|
|
if (!items.length) return;
|
|
laterPicker?.open({ items, confirm:until => () => laterProcessor.run(
|
|
items, {}, { until:new Date(until).toISOString() }
|
|
) }, event.currentTarget, 'search-batch');
|
|
});
|
|
get('#plan-selected-search-results').addEventListener('click', openReleaseReview);
|
|
get('#week-selected-search-results')?.addEventListener('click', openWeekReview);
|
|
get('#cancel-search-week-batch')?.addEventListener('click', closeWeekReview);
|
|
get('#confirm-search-week-batch')?.addEventListener('click', confirmWeekReview);
|
|
get('#cancel-search-release').addEventListener('click', closeReleaseReview);
|
|
get('#confirm-search-release').addEventListener('click', () => {
|
|
const milestoneSelect = get('#search-release-milestone');
|
|
const milestoneId = Number(milestoneSelect.value);
|
|
if (!Number.isInteger(milestoneId) || milestoneId < 1) {
|
|
get('#search-release-summary').textContent = 'Choose an open milestone.';
|
|
return;
|
|
}
|
|
const selectedOption = milestoneSelect.options?.[milestoneSelect.selectedIndex];
|
|
const milestoneTitle = selectedOption?.textContent || selectedOption?.text || 'release';
|
|
const dueDay = get('#search-release-due-date').value;
|
|
releaseContext = {
|
|
milestone_id:milestoneId, milestone_title:milestoneTitle,
|
|
due_date:dueDay ? dueDay + 'T23:59:59Z' : null,
|
|
};
|
|
releaseProcessor.run(plan.snapshot().items, {}, releaseContext);
|
|
});
|
|
get('#cancel-search-batch-estimates').addEventListener('click', closeEstimateReview);
|
|
get('#confirm-search-batch-estimates').addEventListener('click', () =>
|
|
processor.run(plan.snapshot().items, estimateValues())
|
|
);
|
|
get('#resume-search-batch').addEventListener('click', () => {
|
|
if (processor.pending()) return processor.resume();
|
|
if (laterProcessor.pending()) return laterProcessor.resume();
|
|
return releaseProcessor.resume();
|
|
});
|
|
get('#open-palette').addEventListener('click', () => restore());
|
|
get('#cmd-results').addEventListener('change', event => {
|
|
const index = event.target?.dataset?.searchSelect;
|
|
if (index === undefined) return;
|
|
const result = lookup(Number(index));
|
|
if (result) plan.toggle(result);
|
|
});
|
|
function restore() {
|
|
const pending = processor.pending() + laterProcessor.pending() + releaseProcessor.pending();
|
|
get('#resume-search-batch').hidden = pending === 0;
|
|
get('#resume-search-batch').textContent = 'Resume ' + pending + ' interrupted';
|
|
}
|
|
function resultHtml(result, index) {
|
|
const allowed = result.kind === 'issue' && result.state === 'open';
|
|
const selected = plan.snapshot().items.some(item => plan.identity(item) === plan.identity(result));
|
|
return '<label class="cmd-select-result"' + (allowed ? '' : ' aria-disabled="true"') + '>' +
|
|
'<input type="checkbox" data-search-select="' + index + '" ' + (selected ? 'checked ' : '') +
|
|
(allowed ? '' : 'disabled ') + 'aria-label="Select ' + escapeAttribute(result.title) + '" />' +
|
|
'<span>' + escapeHtml(result.title) + '</span><span class="cmd-meta">' +
|
|
escapeHtml(result.repository) + ' #' + escapeHtml(result.number) + ' · ' +
|
|
(allowed ? 'Open issue' : 'Not eligible') + '</span></label>';
|
|
}
|
|
return { plan, processor, laterProcessor, releaseProcessor, weekProcessor, restore, resultHtml };
|
|
};
|
|
|
|
return createSearchBatchPlan;
|
|
});
|