feat: paginate issue capture repositories (#403)
CI / lint (pull_request) Successful in 45s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-09 13:39:27 +00:00
1 parent 33a6d1bb6d
commit 9ea590a65c
16 files changed
+393 -46

No files matched your search

+17 -1
View File
@@ -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,
};
+1
View File
@@ -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
View File
@@ -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', () => {
+2
View File
@@ -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 -1
View File
@@ -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;