function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelection = () => {} }) { let available = []; let pagination = { page: 1, total: 0, has_more: false }; let loadRequest = null; let claimRequest = null; const previewed = new Set(); let selecting = false; const selected = new Map(); function itemKey(item) { return String(item?.repository || '') + '#' + String(item?.number || ''); } function emitSelection() { const state = { active: selecting, count: selected.size, ids: Array.from(selected.keys()) }; onSelection(state); return state; } function apply(result, append) { const incoming = Array.isArray(result?.items) ? result.items : []; if (append) { const byId = new Map(available.map(item => [item.id, item])); incoming.forEach(item => { if (item && !byId.has(item.id)) byId.set(item.id, { ...item }); }); available = Array.from(byId.values()); } else { available = incoming.map(item => ({ ...item })); } const availableKeys = new Set(available.map(itemKey)); previewed.forEach(key => { if (!availableKeys.has(key)) previewed.delete(key); }); pagination = { page: Number(result?.page) || 1, total: Number(result?.total) || available.length, has_more: result?.has_more === true, }; onItems(available.slice()); onPagination({ ...pagination }); } function loadPage(page, append) { if (loadRequest) return loadRequest; loadRequest = fetchJson('api/v1/available-issues?page=' + page, { headers: { Accept: 'application/json' }, }).then(result => { apply(result, append); if (result?.refresh_failed === true) { onStatus('Showing saved available work. Catalog refresh failed; retrying shortly.'); } else if (result?.revalidating === true) { onStatus('Showing saved available work while the catalog refreshes…'); } return result; }).finally(() => { loadRequest = null; }); return loadRequest; } return { reset(result) { apply(result, false); }, load() { return loadPage(1, false); }, loadMore() { if (!pagination.has_more) return Promise.resolve(false); return loadPage(pagination.page + 1, true); }, items() { return available.slice(); }, startSelection() { selecting = true; selected.clear(); return emitSelection(); }, cancelSelection() { selecting = false; selected.clear(); return emitSelection(); }, toggleSelection(item) { if (!selecting || !item) return emitSelection(); const key = itemKey(item); if (selected.has(key)) selected.delete(key); else selected.set(key, item); return emitSelection(); }, isSelected(item) { return selected.has(itemKey(item)); }, selectedItems() { return Array.from(selected.values()); }, selection() { return { active: selecting, count: selected.size, ids: Array.from(selected.keys()) }; }, togglePreview(item) { const key = itemKey(item); if (previewed.has(key)) previewed.delete(key); else previewed.add(key); return previewed.has(key); }, isPreviewed(item) { return previewed.has(itemKey(item)); }, claim(item) { if (claimRequest) return claimRequest; const key = item.repository + '#' + item.number; onStatus('Assigning ' + key + '…'); const path = 'api/v1/repos/' + item.repository.split('/').map(encodeURIComponent).join('/') + '/issues/' + encodeURIComponent(item.number) + '/claim'; claimRequest = fetchJson(path, { method: 'PATCH', headers: { Accept: 'application/json' }, }).then(confirmed => { if (!Array.isArray(confirmed?.assignees) || !confirmed.assignees.length) { throw new Error('Issue assignment was not confirmed.'); } available = available.filter(candidate => candidate.repository !== item.repository || candidate.number !== item.number ); pagination.total = Math.max(available.length, pagination.total - 1); pagination.has_more = available.length < pagination.total; previewed.delete(itemKey(item)); selected.delete(itemKey(item)); emitSelection(); onItems(available.slice()); onPagination({ ...pagination }); onStatus('Assigned ' + key + ' to you.'); return confirmed; }).finally(() => { claimRequest = null; }); return claimRequest; }, }; } if (typeof module !== 'undefined' && module.exports) module.exports = createFindWork;