feat: guide mobile Find Work workflow (Closes #979)
This commit is contained in:
1 parent
611b10b477
commit
53e556ddab
9 files changed
+438
-85
No files matched your search
+13
-1
@@ -617,9 +617,21 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.work-session-nav button { min-height:44px; width:100%; }
|
||||
.find-work-sheet { position:fixed; inset:0; z-index:58; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
|
||||
.find-work-sheet.open { display:flex; }
|
||||
.find-work-panel { width:min(560px,100%); height:100dvh; overflow:auto; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
|
||||
.find-work-panel { width:min(560px,100%); height:100dvh; overflow:auto; overflow-x:hidden; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
|
||||
.find-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||
.find-work-header-actions { display:flex; gap:8px; }
|
||||
.mobile-find-work-nav { position:sticky; top:0; z-index:4; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:6px; padding:6px 0; background:#0b1526; }
|
||||
.mobile-find-work-nav button { min-height:44px; min-width:0; padding:6px; }
|
||||
.mobile-find-work-nav button[aria-current="step"] { border-color:#60a5fa; background:#17355b; color:#fff; }
|
||||
.mobile-find-work-nav button:disabled { opacity:.5; }
|
||||
.find-work-stage { display:grid; gap:12px; min-width:0; outline:none; }
|
||||
.find-work-stage[hidden] { display:none; }
|
||||
.find-work-review { padding:12px; border:1px solid #2a496e; border-radius:12px; background:#101f36; }
|
||||
.find-work-review > div:first-child, .find-work-review-list { display:grid; gap:8px; }
|
||||
.find-work-review-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:center; padding:10px; border:1px solid #2a496e; border-radius:10px; overflow-wrap:anywhere; }
|
||||
.find-work-review-item > div { display:grid; gap:4px; min-width:0; }
|
||||
.find-work-review-item button, .find-work-stage-actions button { min-height:44px; }
|
||||
.find-work-stage-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:10px 0 env(safe-area-inset-bottom); background:#101f36; }
|
||||
.find-work-header button, .find-work-card button, .find-work-card a, .find-work-more { min-height:44px; }
|
||||
.find-work-search { position:sticky; top:0; z-index:2; display:grid; gap:6px; padding:8px 0; background:#0b1526; }
|
||||
.find-work-search > div { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; }
|
||||
|
||||
+35
-46
@@ -1095,6 +1095,7 @@
|
||||
}
|
||||
return offlineToday.warm(confirmedOwnerLogin, todayMyWork);
|
||||
}
|
||||
let findWorkNavigation = null;
|
||||
const findWorkController = createFindWork({
|
||||
fetchJson: fetchReviewJson,
|
||||
onItems: renderAvailableIssues,
|
||||
@@ -1109,9 +1110,20 @@
|
||||
qs('#find-work-selection-status').textContent = state.count ?
|
||||
state.count + ' issue' + (state.count === 1 ? '' : 's') + ' selected.' : 'No work selected.';
|
||||
qs('#claim-selected-work').disabled = state.count === 0;
|
||||
findWorkNavigation?.sync({ selectedCount:state.count });
|
||||
renderAvailableIssues(findWorkController.items());
|
||||
},
|
||||
});
|
||||
|
||||
findWorkNavigation = createMobileFindWorkNavigation({
|
||||
document,
|
||||
history:window.history,
|
||||
eventTarget:window,
|
||||
controller:findWorkController,
|
||||
todayWork,
|
||||
openFit:openFindWorkEstimateReview,
|
||||
});
|
||||
findWorkNavigation.start();
|
||||
let findWorkSearchTimer = null;
|
||||
|
||||
function updateFindWorkMatchStatus() {
|
||||
@@ -2192,26 +2204,22 @@
|
||||
));
|
||||
}
|
||||
|
||||
function closeFindWorkEstimateReview() {
|
||||
qs('#find-work-estimate-review').hidden = true;
|
||||
qs('#batch-find-work-actions').hidden = !findWorkController.selection().active;
|
||||
}
|
||||
|
||||
function openFindWorkEstimateReview(items) {
|
||||
const plan = todayWork.planning();
|
||||
const planned = Object.values(plan.estimates).reduce((sum, minutes) => sum + minutes, 0);
|
||||
qs('#find-work-estimate-summary').textContent = formatPlanMinutes(Math.max(0, plan.capacity_minutes - planned)) +
|
||||
' remaining. Estimate selected work before assigning it.';
|
||||
const estimatesRequired = Number.isInteger(plan.capacity_minutes) && plan.capacity_minutes > 0;
|
||||
qs('#find-work-estimate-summary').textContent = estimatesRequired ?
|
||||
formatPlanMinutes(Math.max(0, plan.capacity_minutes - planned)) +
|
||||
' remaining. Estimate selected work before assigning it.' :
|
||||
items.length + ' selected. Confirm to assign and queue this batch in Today.';
|
||||
qs('#find-work-estimate-list').innerHTML = items.map(item => {
|
||||
const id = String(item.repository || '') + '#' + String(item.number || '');
|
||||
return '<div class="find-work-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" data-find-work-estimate="' +
|
||||
escAttr(id) + '" aria-label="Estimate for ' + escAttr(item.title || id) + ' in minutes" /> min</label></div>';
|
||||
'</span><strong>' + escapeHtml(item.title || 'Untitled work') + '</strong></div>' +
|
||||
(estimatesRequired ? '<label class="small"><input type="number" inputmode="numeric" min="5" max="1440" step="5" data-find-work-estimate="' +
|
||||
escAttr(id) + '" aria-label="Estimate for ' + escAttr(item.title || id) + ' in minutes" /> min</label>' : '') + '</div>';
|
||||
}).join('');
|
||||
qs('#batch-find-work-actions').hidden = true;
|
||||
qs('#find-work-estimate-review').hidden = false;
|
||||
qs('[data-find-work-estimate]')?.focus();
|
||||
if (estimatesRequired) qs('[data-find-work-estimate]')?.focus();
|
||||
}
|
||||
|
||||
function formatCalendarDueDate(value) {
|
||||
@@ -4290,6 +4298,7 @@
|
||||
}
|
||||
findingWork = true;
|
||||
qs('#find-work-sheet').classList.add('open');
|
||||
findWorkNavigation.open(batchFindWork.pending() > 0);
|
||||
const retainedItems = findWorkController.items();
|
||||
if (retainedItems.length) renderAvailableIssues(retainedItems);
|
||||
else qs('#find-work-list').textContent = '';
|
||||
@@ -5635,15 +5644,13 @@
|
||||
});
|
||||
qs('#close-whiteboard').addEventListener('click', () => closeModal('whiteboard-modal'));
|
||||
qs('#find-work').addEventListener('click', openFindWorkSheet);
|
||||
qs('#close-find-work').addEventListener('click', closeFindWorkSheet);
|
||||
qs('#select-find-work').addEventListener('click', () => findWorkController.startSelection());
|
||||
|
||||
qs('#fill-find-work-today').addEventListener('click', () => {
|
||||
const remainingSlots = todayWork.limit - todayWork.read().length;
|
||||
const outcome = findWorkController.fillSelection(remainingSlots);
|
||||
qs('#find-work-status').textContent = remainingSlots < 1 ? 'Today is full.' :
|
||||
outcome.selected ? outcome.selected + ' ranked work selected.' : 'No matching work.';
|
||||
if (outcome.selected && todayWork.planning().capacity_minutes !== null)
|
||||
openFindWorkEstimateReview(findWorkController.selectedItems());
|
||||
if (outcome.selected) findWorkNavigation.go('review');
|
||||
});
|
||||
qs('#find-work-search-form').addEventListener('submit', event => event.preventDefault());
|
||||
qs('#find-work-search').addEventListener('input', event => {
|
||||
@@ -5669,33 +5676,7 @@
|
||||
input.dispatchEvent(new Event('input', { bubbles:true }));
|
||||
input.focus();
|
||||
});
|
||||
qs('#cancel-find-work-selection').addEventListener('click', () => findWorkController.cancelSelection());
|
||||
|
||||
qs('#claim-selected-work').addEventListener('click', async event => {
|
||||
if (todayWork.planning().capacity_minutes !== null) {
|
||||
openFindWorkEstimateReview(findWorkController.selectedItems());
|
||||
return;
|
||||
}
|
||||
event.currentTarget.disabled = true;
|
||||
const outcome = await batchFindWork.run(findWorkController.selectedItems());
|
||||
if (outcome.status === 'complete') {
|
||||
const assignedOnly = outcome.failed.filter(item => item.assigned).length;
|
||||
const unavailable = outcome.failed.length - assignedOnly;
|
||||
qs('#find-work-status').textContent = outcome.queued.length + ' queued' +
|
||||
(unavailable ? ' · ' + unavailable + ' unavailable' : '') +
|
||||
(assignedOnly ? ' · ' + assignedOnly + ' assigned but not queued' : '') + '.';
|
||||
if (outcome.failed.length) {
|
||||
renderAvailableIssues(findWorkController.items());
|
||||
event.currentTarget.disabled = false;
|
||||
} else {
|
||||
findWorkController.cancelSelection();
|
||||
}
|
||||
refreshMyWorkView();
|
||||
} else {
|
||||
event.currentTarget.disabled = false;
|
||||
}
|
||||
});
|
||||
qs('#cancel-find-work-estimates').addEventListener('click', closeFindWorkEstimateReview);
|
||||
qs('#confirm-find-work-estimates').addEventListener('click', async event => {
|
||||
event.currentTarget.disabled = true;
|
||||
const outcome = await batchFindWork.run(findWorkController.selectedItems(), findWorkEstimateValues());
|
||||
@@ -5706,10 +5687,18 @@
|
||||
return;
|
||||
}
|
||||
if (outcome.status !== 'complete') return;
|
||||
const assignedOnly = outcome.failed.filter(item => item.assigned).length;
|
||||
const unavailable = outcome.failed.length - assignedOnly;
|
||||
qs('#find-work-status').textContent = outcome.queued.length + ' queued' +
|
||||
(outcome.failed.length ? ' · ' + outcome.failed.length + ' unavailable.' : '.');
|
||||
closeFindWorkEstimateReview();
|
||||
if (!outcome.failed.length) findWorkController.cancelSelection();
|
||||
(unavailable ? ' · ' + unavailable + ' unavailable' : '') +
|
||||
(assignedOnly ? ' · ' + assignedOnly + ' assigned but not queued' : '') + '.';
|
||||
if (!outcome.failed.length) {
|
||||
findWorkNavigation.complete();
|
||||
findWorkController.cancelSelection();
|
||||
} else {
|
||||
renderAvailableIssues(findWorkController.items());
|
||||
findWorkNavigation.sync({ selectedCount:findWorkController.selection().count, recovery:true });
|
||||
}
|
||||
refreshMyWorkView();
|
||||
});
|
||||
qs('#load-more-available').addEventListener('click', async event => {
|
||||
|
||||
+53
-34
@@ -843,42 +843,60 @@
|
||||
<div><div class="small">Open · unassigned</div><h3 id="find-work-heading">Find work</h3></div>
|
||||
<div class="find-work-header-actions"><button id="select-find-work" type="button">Select work</button><button id="close-find-work" type="button">Close</button></div>
|
||||
</div>
|
||||
<p class="small">Claim an available issue and continue it in My Work.</p>
|
||||
<form class="find-work-search" id="find-work-search-form" role="search">
|
||||
<label for="find-work-search">Search repository, #number, or title</label>
|
||||
<div><input id="find-work-search" type="search" maxlength="100" autocomplete="off" />
|
||||
<button id="clear-find-work-search" type="button" hidden>Clear</button></div>
|
||||
<span id="find-work-match-status" class="small" aria-live="polite"></span>
|
||||
</form>
|
||||
<details class="find-work-filters" id="find-work-filters">
|
||||
<summary>Filter work <span id="find-work-filter-count" class="small"></span></summary>
|
||||
<div class="find-work-filter-panel" aria-label="Filter available work">
|
||||
<fieldset><legend>Repositories</legend><div id="find-work-repository-filters" class="find-work-filter-options"></div></fieldset>
|
||||
<fieldset><legend>Labels</legend><div id="find-work-label-filters" class="find-work-filter-options"></div></fieldset>
|
||||
<button id="clear-find-work-filters" type="button">Clear filters</button>
|
||||
</div>
|
||||
</details>
|
||||
<button class="fill-find-work-today" id="fill-find-work-today" type="button"
|
||||
aria-describedby="find-work-selection-status">Fill remaining Today slots</button>
|
||||
<div id="find-work-status" class="small" aria-live="assertive">Open Find Work to load available issues.</div>
|
||||
<button class="find-work-batch-recovery" id="resume-find-work-batch" type="button"
|
||||
aria-describedby="find-work-status" hidden>Resume assigning & queueing</button>
|
||||
<div class="find-work-list" id="find-work-list"></div>
|
||||
<button class="find-work-more" id="load-more-available" type="button" hidden>Load more available issues</button>
|
||||
<section class="find-work-estimate-review" id="find-work-estimate-review" aria-labelledby="find-work-estimate-heading" hidden>
|
||||
<div><strong id="find-work-estimate-heading">Fit this work into Today</strong>
|
||||
<span class="small" id="find-work-estimate-summary" aria-live="polite"></span></div>
|
||||
<div id="find-work-estimate-list"></div>
|
||||
<div class="find-work-estimate-actions">
|
||||
<button id="cancel-find-work-estimates" type="button">Back</button>
|
||||
<button id="confirm-find-work-estimates" type="button">Confirm & assign</button>
|
||||
<p class="small">Choose available issues, review the batch, then fit it into Today.</p>
|
||||
<nav class="mobile-find-work-nav" aria-label="Find Work steps">
|
||||
<button type="button" data-find-work-stage="discover">Discover</button>
|
||||
<button type="button" data-find-work-stage="review" disabled>Review</button>
|
||||
<button type="button" data-find-work-stage="fit" disabled>Fit & assign</button>
|
||||
</nav>
|
||||
<section class="find-work-stage" id="find-work-discover-stage" tabindex="-1" aria-labelledby="find-work-heading">
|
||||
<form class="find-work-search" id="find-work-search-form" role="search">
|
||||
<label for="find-work-search">Search repository, #number, or title</label>
|
||||
<div><input id="find-work-search" type="search" maxlength="100" autocomplete="off" />
|
||||
<button id="clear-find-work-search" type="button" hidden>Clear</button></div>
|
||||
<span id="find-work-match-status" class="small" aria-live="polite"></span>
|
||||
</form>
|
||||
<details class="find-work-filters" id="find-work-filters">
|
||||
<summary>Filter work <span id="find-work-filter-count" class="small"></span></summary>
|
||||
<div class="find-work-filter-panel" aria-label="Filter available work">
|
||||
<fieldset><legend>Repositories</legend><div id="find-work-repository-filters" class="find-work-filter-options"></div></fieldset>
|
||||
<fieldset><legend>Labels</legend><div id="find-work-label-filters" class="find-work-filter-options"></div></fieldset>
|
||||
<button id="clear-find-work-filters" type="button">Clear filters</button>
|
||||
</div>
|
||||
</details>
|
||||
<button class="fill-find-work-today" id="fill-find-work-today" type="button"
|
||||
aria-describedby="find-work-selection-status">Fill remaining Today slots</button>
|
||||
<div id="find-work-status" class="small" aria-live="assertive">Open Find Work to load available issues.</div>
|
||||
<button class="find-work-batch-recovery" id="resume-find-work-batch" type="button"
|
||||
aria-describedby="find-work-status" hidden>Resume assigning & queueing</button>
|
||||
<div class="find-work-list" id="find-work-list"></div>
|
||||
<button class="find-work-more" id="load-more-available" type="button" hidden>Load more available issues</button>
|
||||
<div class="find-work-batch-actions" id="batch-find-work-actions" hidden>
|
||||
<span id="find-work-selection-status" class="small" aria-live="polite">No work selected.</span>
|
||||
<button id="cancel-find-work-selection" type="button">Cancel</button>
|
||||
<button id="claim-selected-work" type="button" disabled>Review selected</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="find-work-stage find-work-review" id="find-work-review-stage" tabindex="-1" aria-labelledby="find-work-review-heading" hidden>
|
||||
<div><strong id="find-work-review-heading">Review selected work</strong>
|
||||
<span class="small" id="find-work-review-summary" aria-live="polite"></span></div>
|
||||
<div class="find-work-review-list" id="find-work-review-list"></div>
|
||||
<div class="find-work-stage-actions">
|
||||
<button id="back-find-work-discover" type="button">Back to discover</button>
|
||||
<button id="continue-find-work-fit" type="button">Continue</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="find-work-stage" id="find-work-fit-stage" tabindex="-1" aria-labelledby="find-work-estimate-heading" hidden>
|
||||
<div class="find-work-estimate-review" id="find-work-estimate-review">
|
||||
<div><strong id="find-work-estimate-heading">Fit this work into Today</strong>
|
||||
<span class="small" id="find-work-estimate-summary" aria-live="polite"></span></div>
|
||||
<div id="find-work-estimate-list"></div>
|
||||
<div class="find-work-estimate-actions">
|
||||
<button id="cancel-find-work-estimates" type="button">Back to review</button>
|
||||
<button id="confirm-find-work-estimates" type="button">Confirm & assign</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="find-work-batch-actions" id="batch-find-work-actions" hidden>
|
||||
<span id="find-work-selection-status" class="small" aria-live="polite">No work selected.</span>
|
||||
<button id="cancel-find-work-selection" type="button">Cancel</button>
|
||||
<button id="claim-selected-work" type="button" disabled>Assign & queue Today</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1596,6 +1614,7 @@
|
||||
<script src="static/mobile-review-detail-nav.js"></script>
|
||||
<script src="static/mobile-search-preview-nav.js"></script>
|
||||
<script src="static/mobile-plan-today-nav.js"></script>
|
||||
<script src="static/mobile-find-work-nav.js"></script>
|
||||
<script src="static/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,194 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||
else root.createMobileFindWorkNavigation = api;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||
'use strict';
|
||||
|
||||
const stages = ['discover', 'review', 'fit'];
|
||||
|
||||
return function createMobileFindWorkNavigation(options) {
|
||||
const document = options.document;
|
||||
const buttons = options.buttons || Object.fromEntries(stages.map(name =>
|
||||
[name, document?.querySelector('[data-find-work-stage="' + name + '"]')]
|
||||
));
|
||||
const sections = options.sections || Object.fromEntries(stages.map(name =>
|
||||
[name, document?.getElementById('find-work-' + name + '-stage')]
|
||||
));
|
||||
const history = options.history;
|
||||
const eventTarget = options.eventTarget;
|
||||
const onStageChange = options.onStageChange || (() => {});
|
||||
const listeners = new Map();
|
||||
let current = 'discover';
|
||||
let selectedCount = 0;
|
||||
let recovery = false;
|
||||
let started = false;
|
||||
|
||||
function escape(value) {
|
||||
return String(value || '').replace(/[&<>"']/g, character =>
|
||||
({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[character]
|
||||
);
|
||||
}
|
||||
|
||||
function renderReview() {
|
||||
const reviewList = options.reviewList || document?.getElementById('find-work-review-list');
|
||||
const reviewSummary = options.reviewSummary || document?.getElementById('find-work-review-summary');
|
||||
if (!reviewList) return;
|
||||
const items = options.controller?.selectedItems?.() || options.getSelectedItems?.() || [];
|
||||
const planning = options.todayWork?.planning?.();
|
||||
const today = options.todayWork ? {
|
||||
limit:options.todayWork.limit, count:options.todayWork.read().length,
|
||||
capacityMinutes:planning.capacity_minutes, estimates:planning.estimates,
|
||||
} : options.getTodayState?.() || {};
|
||||
const remainingSlots = Math.max(0, (Number(today.limit) || 0) - (Number(today.count) || 0));
|
||||
const planned = Object.values(today.estimates || {}).reduce((sum, minutes) => sum + minutes, 0);
|
||||
const remainingMinutes = today.capacityMinutes === null || today.capacityMinutes === undefined ? null :
|
||||
Math.max(0, today.capacityMinutes - planned);
|
||||
const formatMinutes = minutes => [Math.floor(minutes / 60) ? Math.floor(minutes / 60) + 'h' : '',
|
||||
minutes % 60 ? minutes % 60 + 'm' : ''].filter(Boolean).join(' ') || '0m';
|
||||
if (reviewSummary) reviewSummary.textContent = items.length + ' selected · ' + remainingSlots +
|
||||
' Today slot' + (remainingSlots === 1 ? '' : 's') + ' open' +
|
||||
(remainingMinutes === null ? '' : ' · ' + formatMinutes(remainingMinutes) + ' available') + '.';
|
||||
reviewList.innerHTML = items.map(item => {
|
||||
const id = String(item.repository || '') + '#' + String(item.number || '');
|
||||
return '<article class="find-work-review-item"><div><span class="small">' + escape(id) +
|
||||
'</span><strong>' + escape(item.title || 'Untitled work') + '</strong></div>' +
|
||||
'<button type="button" data-find-work-review-remove="' + escape(id) + '">Remove</button></article>';
|
||||
}).join('');
|
||||
reviewList.querySelectorAll('[data-find-work-review-remove]').forEach(button =>
|
||||
button.addEventListener('click', () => {
|
||||
const item = items.find(candidate => String(candidate.repository || '') + '#' +
|
||||
String(candidate.number || '') === button.dataset.findWorkReviewRemove);
|
||||
if (item) (options.controller?.toggleSelection || options.toggleSelection)?.(item);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function allowed(name) {
|
||||
return stages.includes(name) && (name === 'discover' || selectedCount > 0 || recovery);
|
||||
}
|
||||
|
||||
function paint(name, focus = false, notify = true) {
|
||||
if (!allowed(name)) name = 'discover';
|
||||
current = name;
|
||||
stages.forEach(stage => {
|
||||
const button = buttons[stage];
|
||||
const section = sections[stage];
|
||||
if (button) {
|
||||
button.disabled = stage !== 'discover' && selectedCount === 0 && !recovery;
|
||||
if (stage === name) button.setAttribute('aria-current', 'step');
|
||||
else button.removeAttribute('aria-current');
|
||||
}
|
||||
if (section) section.hidden = stage !== name;
|
||||
});
|
||||
if (name === 'review') renderReview();
|
||||
if (name === 'fit') options.openFit?.(options.controller?.selectedItems?.() || []);
|
||||
if (notify) onStageChange(name);
|
||||
if (focus) sections[name]?.focus?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
function stateFor(name) {
|
||||
const state = { ...(history?.state || {}) };
|
||||
if (name === 'discover') delete state.findWorkStage;
|
||||
else state.findWorkStage = name;
|
||||
return state;
|
||||
}
|
||||
|
||||
function go(name) {
|
||||
if (!allowed(name) || name === current) return false;
|
||||
const currentIndex = stages.indexOf(current);
|
||||
const nextIndex = stages.indexOf(name);
|
||||
if (nextIndex < currentIndex && history?.go) {
|
||||
history.go(nextIndex - currentIndex);
|
||||
return true;
|
||||
}
|
||||
if (nextIndex === currentIndex - 1 && history?.back) {
|
||||
history.back();
|
||||
return true;
|
||||
}
|
||||
history?.pushState?.(stateFor(name), '');
|
||||
paint(name, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function onPopState(event) {
|
||||
if (event.state?.taskOverlay !== 'find') return;
|
||||
paint(event.state?.findWorkStage || 'discover', true);
|
||||
}
|
||||
|
||||
function sync(state = {}) {
|
||||
selectedCount = Math.max(0, Number(state.selectedCount) || 0);
|
||||
recovery = state.recovery === true;
|
||||
if (!allowed(current)) paint('discover');
|
||||
else paint(current, false, false);
|
||||
}
|
||||
|
||||
function open(hasRecovery) {
|
||||
if (options.controller) {
|
||||
selectedCount = options.controller.selection().count;
|
||||
recovery = hasRecovery === true;
|
||||
}
|
||||
const restored = history?.state?.taskOverlay === 'find' ? history.state.findWorkStage : null;
|
||||
return paint(allowed(restored) ? restored : 'discover');
|
||||
}
|
||||
|
||||
function complete() {
|
||||
selectedCount = 0;
|
||||
recovery = false;
|
||||
history?.replaceState?.(stateFor('discover'), '');
|
||||
return paint('discover');
|
||||
}
|
||||
|
||||
function back() {
|
||||
if (current === 'discover') return false;
|
||||
history?.back?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
start() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
stages.forEach(name => {
|
||||
const button = buttons[name];
|
||||
if (!button) return;
|
||||
const listener = event => { event.preventDefault(); go(name); };
|
||||
listeners.set(button, listener);
|
||||
button.addEventListener('click', listener);
|
||||
});
|
||||
if (document) {
|
||||
const bind = (id, action) => {
|
||||
const button = document.getElementById(id);
|
||||
if (!button) return;
|
||||
const listener = event => { event.preventDefault(); action(); };
|
||||
listeners.set(button, listener);
|
||||
button.addEventListener('click', listener);
|
||||
};
|
||||
bind('select-find-work', () => { complete(); options.controller?.startSelection?.(); });
|
||||
bind('cancel-find-work-selection', () => { complete(); options.controller?.cancelSelection?.(); });
|
||||
bind('claim-selected-work', () => go('review'));
|
||||
bind('continue-find-work-fit', () => go('fit'));
|
||||
bind('back-find-work-discover', back);
|
||||
bind('cancel-find-work-estimates', back);
|
||||
bind('close-find-work', () => history?.go?.(-(stages.indexOf(current) + 1)));
|
||||
}
|
||||
eventTarget?.addEventListener?.('popstate', onPopState);
|
||||
paint('discover');
|
||||
},
|
||||
stop() {
|
||||
listeners.forEach((listener, button) => button.removeEventListener('click', listener));
|
||||
listeners.clear();
|
||||
eventTarget?.removeEventListener?.('popstate', onPopState);
|
||||
started = false;
|
||||
},
|
||||
sync,
|
||||
open,
|
||||
go,
|
||||
back,
|
||||
complete,
|
||||
renderReview,
|
||||
stage: () => current,
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -82,6 +82,7 @@ const SHELL = [
|
||||
BASE + 'static/mobile-review-detail-nav.js',
|
||||
BASE + 'static/mobile-search-preview-nav.js',
|
||||
BASE + 'static/mobile-plan-today-nav.js',
|
||||
BASE + 'static/mobile-find-work-nav.js',
|
||||
BASE + 'static/checklist-conflict.js',
|
||||
BASE + 'static/voice-transcript-store.js',
|
||||
BASE + 'static/voice-issue-capture.js',
|
||||
|
||||
Reference in new issue
Block a user