214 lines
8.2 KiB
JavaScript
214 lines
8.2 KiB
JavaScript
function createFindWork({ fetchJson, onItems, onPagination, onStatus, onSelection = () => {}, onFacets = () => {} }) {
|
|
let available = [];
|
|
let pagination = { page: 1, total: 0, has_more: false };
|
|
let loadRequest = null;
|
|
let loadGeneration = 0;
|
|
let query = '';
|
|
let filters = { repositories: [], labels: [] };
|
|
let facetView;
|
|
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 });
|
|
if (result?.facets) onFacets({
|
|
repositories: Array.isArray(result.facets.repositories) ? result.facets.repositories.slice() : [],
|
|
labels: Array.isArray(result.facets.labels) ? result.facets.labels.slice() : [],
|
|
});
|
|
if (result?.facets && typeof document !== 'undefined') {
|
|
facetView ||= attachFindWorkFacets(document.querySelector('#find-work-filters'), value =>
|
|
String(value).replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[character])),
|
|
next => { filters = next; loadPage(1, false); });
|
|
facetView(result.facets);
|
|
}
|
|
}
|
|
|
|
function loadPage(page, append, requestedQuery = query) {
|
|
if (loadRequest && requestedQuery === query && append) return loadRequest;
|
|
const generation = ++loadGeneration;
|
|
const params = new URLSearchParams();
|
|
params.set('facets', 'true');
|
|
if (requestedQuery) params.set('q', requestedQuery);
|
|
filters.repositories.forEach(value => params.append('repository', value));
|
|
filters.labels.forEach(value => params.append('label', value));
|
|
const queryPart = params.toString() ? '&' + params.toString() : '';
|
|
const request = fetchJson('api/v1/available-issues?page=' + page + queryPart, {
|
|
headers: { Accept: 'application/json' },
|
|
}).then(result => {
|
|
if (generation !== loadGeneration) return 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(() => { if (loadRequest === request) loadRequest = null; });
|
|
loadRequest = request;
|
|
return request;
|
|
}
|
|
|
|
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);
|
|
},
|
|
search(value) {
|
|
query = String(value || '').trim();
|
|
return loadPage(1, false, query);
|
|
},
|
|
query() {
|
|
return query;
|
|
},
|
|
setFilters(next) {
|
|
filters = {
|
|
repositories: Array.from(new Set((next?.repositories || []).map(String))).slice(0, 10),
|
|
labels: Array.from(new Set((next?.labels || []).map(String))).slice(0, 10),
|
|
};
|
|
return loadPage(1, false);
|
|
},
|
|
filters() {
|
|
return { repositories: filters.repositories.slice(), labels: filters.labels.slice() };
|
|
},
|
|
|
|
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();
|
|
},
|
|
fillSelection(limit) {
|
|
limit = Math.max(0, Math.floor(Number(limit) || 0));
|
|
if (!limit) return { selected: selected.size, added: 0, limit };
|
|
selecting = true;
|
|
const before = selected.size;
|
|
available.some(item => {
|
|
if (selected.size >= limit) return true;
|
|
selected.set(itemKey(item), item);
|
|
return false;
|
|
});
|
|
emitSelection();
|
|
return { selected: selected.size, added: selected.size - before, limit };
|
|
},
|
|
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;
|
|
},
|
|
};
|
|
}
|
|
|
|
function attachFindWorkFacets(root, escapeHtml, onChange) {
|
|
const values = name => Array.from(root.querySelectorAll('input[name="' + name + '"]:checked'), input => input.value);
|
|
root.onchange = event => event.target.type === 'checkbox' && onChange({ repositories:values('find-work-repository'), labels:values('find-work-label') });
|
|
root.querySelector('#clear-find-work-filters').onclick = () => {
|
|
root.querySelectorAll('input').forEach(input => { input.checked = false; });
|
|
onChange({ repositories:[], labels:[] });
|
|
};
|
|
return facets => {
|
|
const active = { repositories:values('find-work-repository'), labels:values('find-work-label') };
|
|
const render = (id, choices, selected, name) => {
|
|
root.querySelector(id).innerHTML = choices.map(value => '<label class="find-work-filter-option"><input type="checkbox" name="' + name + '" value="' + escapeHtml(value) + '"' + (selected.includes(value) ? ' checked' : '') + ' />' + escapeHtml(value) + '</label>').join('');
|
|
};
|
|
render('#find-work-repository-filters', facets.repositories || [], active.repositories, 'find-work-repository');
|
|
render('#find-work-label-filters', facets.labels || [], active.labels, 'find-work-label');
|
|
const count = active.repositories.length + active.labels.length;
|
|
root.querySelector('#find-work-filter-count').textContent = count ? '(' + count + ' active)' : '';
|
|
};
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = createFindWork;
|