feat: paginate issue capture repositories (#403)
This commit is contained in:
1 parent
33a6d1bb6d
commit
9ea590a65c
16 files changed
+393
-46
No files matched your search
@@ -26,6 +26,7 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
let duplicateRequest = 0;
|
||||
let duplicateState = {status: 'idle', key: '', candidates: []};
|
||||
let acknowledgedDuplicateKey = '';
|
||||
const repositoryPageRequests = new Map();
|
||||
const safeLabelIds = value => Array.from(new Set(
|
||||
(Array.isArray(value) ? value : []).filter(id => Number.isInteger(id) && id > 0)
|
||||
)).slice(0, 20);
|
||||
@@ -143,6 +144,21 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
);
|
||||
}
|
||||
|
||||
function loadRepositoryPage(page) {
|
||||
const safePage = Math.max(1, Math.floor(Number(page) || 1));
|
||||
if (repositoryPageRequests.has(safePage)) return repositoryPageRequests.get(safePage);
|
||||
const request = fetchJson('api/v1/repositories?page=' + safePage + '&limit=50')
|
||||
.then(payload => ({
|
||||
items: Array.isArray(payload?.items) ? payload.items : [],
|
||||
page: Number(payload?.page) || safePage,
|
||||
total: Math.max(0, Number(payload?.total) || 0),
|
||||
has_more: payload?.has_more === true,
|
||||
}))
|
||||
.finally(() => repositoryPageRequests.delete(safePage));
|
||||
repositoryPageRequests.set(safePage, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
function duplicateKey(draft) {
|
||||
const repository = String(draft?.repository || '').trim();
|
||||
const title = String(draft?.title || '').replace(/\s+/g, ' ').trim();
|
||||
@@ -214,7 +230,7 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
|
||||
}
|
||||
|
||||
return {
|
||||
saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, findDuplicates,
|
||||
saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, loadRepositoryPage, findDuplicates,
|
||||
needsDuplicateAcknowledgement, acknowledgeDuplicates, submit,
|
||||
stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent,
|
||||
};
|
||||
|
||||
@@ -289,6 +289,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.create-issue-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||
.create-issue-header button, .create-issue-actions button { min-height:44px; }
|
||||
.create-issue-form { display:grid; gap:12px; }
|
||||
.create-issue-repository-more { min-height:44px; width:100%; }
|
||||
.create-issue-form label { display:grid; gap:6px; }
|
||||
.create-issue-form select, .create-issue-form input[type="date"] { min-height:44px; padding:8px; border-radius:8px; border:1px solid #1f3a5f; background:#0b1526; color:#e5e7eb; }
|
||||
.create-issue-labels { display:grid; gap:8px; margin:0; padding:0; border:0; }
|
||||
|
||||
+53
-7
@@ -1868,6 +1868,27 @@
|
||||
};
|
||||
}
|
||||
|
||||
let issueCaptureRepositories = [];
|
||||
let nextIssueRepositoryPage = 2;
|
||||
let moreIssueRepositoriesAvailable = false;
|
||||
|
||||
function appendIssueRepositories(items) {
|
||||
const select = qs('#create-issue-repository');
|
||||
const selected = select.value;
|
||||
const known = new Set(issueCaptureRepositories);
|
||||
(Array.isArray(items) ? items : []).forEach(item => {
|
||||
const repository = String(item?.full_name || '').trim();
|
||||
if (!repository || known.has(repository)) return;
|
||||
known.add(repository);
|
||||
issueCaptureRepositories.push(repository);
|
||||
const option = document.createElement('option');
|
||||
option.value = repository;
|
||||
option.textContent = repository;
|
||||
select.appendChild(option);
|
||||
});
|
||||
if (selected) select.value = selected;
|
||||
}
|
||||
|
||||
let duplicateCheckTimer = null;
|
||||
function renderIssueDuplicates(state) {
|
||||
if (state.status === 'stale') return;
|
||||
@@ -1990,13 +2011,18 @@
|
||||
return;
|
||||
}
|
||||
const captureDraft = issueCapture.loadDraft();
|
||||
const repositories = (lastContextSnapshot?.repos || []).map(repository => repository.full_name).filter(Boolean);
|
||||
if (captureDraft.repository && !repositories.includes(captureDraft.repository)) {
|
||||
repositories.unshift(captureDraft.repository);
|
||||
const initialRepositories = (lastContextSnapshot?.repos || []).map(repository => repository.full_name).filter(Boolean);
|
||||
if (!issueCaptureRepositories.length) issueCaptureRepositories = initialRepositories.slice();
|
||||
if (captureDraft.repository && !issueCaptureRepositories.includes(captureDraft.repository)) {
|
||||
issueCaptureRepositories.unshift(captureDraft.repository);
|
||||
}
|
||||
qs('#create-issue-repository').innerHTML = repositories.map(repository =>
|
||||
qs('#create-issue-repository').innerHTML = issueCaptureRepositories.map(repository =>
|
||||
'<option value="' + escAttr(repository) + '">' + escapeHtml(repository) + '</option>'
|
||||
).join('');
|
||||
if (nextIssueRepositoryPage === 2) {
|
||||
moreIssueRepositoriesAvailable = lastContextSnapshot?.repository_pagination?.has_more === true;
|
||||
}
|
||||
qs('#load-more-issue-repositories').hidden = !moreIssueRepositoriesAvailable;
|
||||
if (captureDraft.repository) qs('#create-issue-repository').value = captureDraft.repository;
|
||||
qs('#create-issue-title').value = captureDraft.title;
|
||||
qs('#create-issue-body').value = captureDraft.body;
|
||||
@@ -2004,9 +2030,9 @@
|
||||
loadIssueLabels(qs('#create-issue-repository').value, captureDraft.labelIds);
|
||||
loadIssueMilestones(qs('#create-issue-repository').value, captureDraft.milestoneId);
|
||||
scheduleIssueDuplicateCheck();
|
||||
qs('#create-issue-status').textContent = repositories.length ? '' : 'No accessible repositories are available.';
|
||||
qs('#submit-new-issue').disabled = !repositories.length;
|
||||
qs('#create-and-start-issue').disabled = !repositories.length || !createAndStart.available();
|
||||
qs('#create-issue-status').textContent = issueCaptureRepositories.length ? '' : 'No accessible repositories are available.';
|
||||
qs('#submit-new-issue').disabled = !issueCaptureRepositories.length;
|
||||
qs('#create-and-start-issue').disabled = !issueCaptureRepositories.length || !createAndStart.available();
|
||||
qs('#create-issue-sheet').classList.add('open');
|
||||
creatingIssue = true;
|
||||
qs('#create-issue-title').focus();
|
||||
@@ -2799,6 +2825,26 @@
|
||||
saveIssueCaptureDraft();
|
||||
scheduleIssueDuplicateCheck();
|
||||
});
|
||||
qs('#load-more-issue-repositories').addEventListener('click', async event => {
|
||||
const button = event.currentTarget;
|
||||
const status = qs('#create-issue-repository-status');
|
||||
saveIssueCaptureDraft();
|
||||
button.disabled = true;
|
||||
status.textContent = 'Loading more repositories…';
|
||||
try {
|
||||
const payload = await issueCapture.loadRepositoryPage(nextIssueRepositoryPage);
|
||||
appendIssueRepositories(payload.items);
|
||||
nextIssueRepositoryPage = payload.page + 1;
|
||||
moreIssueRepositoriesAvailable = payload.has_more;
|
||||
button.hidden = !moreIssueRepositoriesAvailable;
|
||||
status.textContent = payload.items.length ?
|
||||
'More repositories are available in the selector.' : 'All accessible repositories are loaded.';
|
||||
} catch (_error) {
|
||||
status.textContent = 'Repositories could not be loaded. Your draft is safe; retry.';
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
qs('#create-issue-label-list').addEventListener('change', saveIssueCaptureDraft);
|
||||
qs('#create-issue-milestone').addEventListener('change', saveIssueCaptureDraft);
|
||||
qs('#create-issue-anyway').addEventListener('click', () => {
|
||||
|
||||
@@ -361,6 +361,8 @@
|
||||
<label for="create-issue-repository">Repository
|
||||
<select id="create-issue-repository" required></select>
|
||||
</label>
|
||||
<button class="create-issue-repository-more" id="load-more-issue-repositories" type="button" hidden>Load more repositories</button>
|
||||
<div id="create-issue-repository-status" class="small" aria-live="polite"></div>
|
||||
<label for="create-issue-title">Title
|
||||
<input id="create-issue-title" type="text" maxlength="255" required autocomplete="off" />
|
||||
</label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v63';
|
||||
const CACHE = 'stackchain-dashboard-shell-v64';
|
||||
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