Hei,
Minulla on ongelma paginationissa listata Jira issueita. Ja Copilot ja ChatGPT ei osaa auttaa, koska sen näkemys on jäljessä nykyiseen API-dokumenataatioon.
https://stackoverflow.com/questions/79853409/
https://community.developer.atlassian.com/t/how-to-do-pagination-for-jira-issues/98009
Documentaatio:
Eli ei ole startAt vaan on nextPageToken ja limit. Eli requestin yhteydessä luodaan nextPageToken joka on string.
Voin maksaa auttajalle rahaakin jos hinnasta päästään sopimukseen. Ota yheyttä matti.kiviharju@i4ware.fi
Eli vielä.
NodeJS back-end:
export async function getIssues({
project = null,
year,
month,
limit = 50,
issueKeys = null,
pageToken = null,
} = {}) {
if (typeof year !== 'number' || typeof month !== 'number') {
throw new Error('getIssues requires { year: number, month: number }');
}
const safeLimit = Number(limit) && Number(limit) > 0 ? Math.min(Number(limit), 100) : 50;
const dateCondition = 'updated >= -90d OR created >= -90d';
const orderBy = 'ORDER BY created DESC';
let jql = `${dateCondition} ${orderBy}`;
if (issueKeys && Array.isArray(issueKeys) && issueKeys.length) {
const escaped = issueKeys.map(k => `"${String(k).replace(/"/g, '\\"')}"`).join(', ');
jql = `issuekey in (${escaped}) ${orderBy}`;
} else if (project && project !== '-- ALL --') {
jql = `project = "${project}" AND (${dateCondition}) ${orderBy}`;
}
console.log('getIssues JQL:', jql, 'pageToken:', pageToken, 'limit:', safeLimit);
const url = route`/rest/api/3/search/jql`;
let allIssues = [];
let cursor = pageToken || null;
try {
const body = {
expand: '',
fields: ['*all'],
fieldsByKeys: false,
jql: jql,
maxResults: safeLimit,
nextPageToken: cursor || undefined,
properties: []
};
console.log('Fetching issues with body:', JSON.stringify(body));
const res = await api.asUser().requestJira(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (res.status >= 400) {
const text = await res.text();
throw new Error(`Jira API error ${res.status}: ${text}`);
}
const json = await res.json();
const issues = (json.issues || []).map(i => ({
id: i.id,
key: i.key,
summary: i.fields.summary,
avatarUrl: i.fields.issuetype.iconUrl,
issueType: i.fields.issuetype.name,
projectKey: i.fields.project.key
}));
allIssues.push(...issues);
const nextToken = json.nextPageToken || null;
// --- Compute worklogs per issue ---
const daysInMonth = new Date(year, month + 1, 0).getDate();
const fetchForIssue = async (iss) => {
try {
const wurl = route`/rest/api/3/issue/${iss.key}/worklog`;
const wr = await api.asUser().requestJira(wurl, { method: 'GET', headers: { Accept: 'application/json' } });
if (wr.status >= 400) return { iss, hoursByDay: Array(daysInMonth).fill(0), totalHours: 0 };
const wjson = await wr.json();
const hoursByDay = Array(daysInMonth).fill(0);
(wjson.worklogs || []).forEach(w => {
const iso = normalizeIsoTimezone(w.started || '');
const d = new Date(iso);
if (!isNaN(d.getTime()) && d.getFullYear() === year && d.getMonth() === month) {
const dayIndex = d.getDate() - 1;
const secs = Number(w.timeSpentSeconds) || 0;
hoursByDay[dayIndex] += secs / 3600;
}
});
for (let i = 0; i < hoursByDay.length; i++) hoursByDay[i] = +hoursByDay[i].toFixed(2);
const totalHours = +hoursByDay.reduce((s, v) => s + v, 0).toFixed(2);
return { iss, hoursByDay, totalHours };
} catch {
return { iss, hoursByDay: Array(daysInMonth).fill(0), totalHours: 0 };
}
};
const results = await Promise.all(allIssues.map(fetchForIssue));
const issuesWithHours = results.map(r => ({
...r.iss,
hoursByDay: r.hoursByDay,
totalHours: r.totalHours
}));
return {
issues: issuesWithHours,
pageToken: cursor, // current token (used for prev)
nextPageToken: nextToken,
pageSize: safeLimit,
total: issuesWithHours.length
};
} catch (err) {
console.error('getIssues error:', err);
throw err;
}
}Ja ReactJS front-end:
const fetchIssues = useCallback(async (token = null) => {
const y = displayedMonth.getFullYear();
const m = displayedMonth.getMonth(); // 0..11
try {
setLoadingIssues(true);
// When using user's saved timesheet, pass explicit issue keys to backend
// If using my timesheet but no keys saved, return empty list immediately
if (useMyTimesheet && Array.isArray(myTimesheetKeys) && myTimesheetKeys.length === 0) {
setIssues([]);
setTotalIssues(0);
setHours({});
setLoadingIssues(false);
return;
}
const result = await invoke('getIssues', {
project: projectKey || null,
year: y,
month: m,
limit: PAGE_SIZE,
issueKeys: useMyTimesheet ? myTimesheetKeys : undefined,
pageToken: token,
});
// Accept either { issues: [...] } or [...] directly
const payload = Array.isArray(result)
? result
: (result && Array.isArray(result.issues) ? result.issues : null);
if (!payload) {
console.warn('fetchIssues: invalid response', result);
setIssues([]);
setTotalIssues(0);
return;
}
setIssues(payload);
setNextPageToken(result.nextPageToken ?? null);
if (token) {
// push current token to stack for "prev" handling
setPrevTokens(prev => [...prev, token]);
}
// If using saved personal timesheet selection, select all returned issues
if (useMyTimesheet) {
const sel = new Set();
payload.forEach(i => sel.add(i.id));
setSelectedIssues(sel);
}
// track total if provided
setTotalIssues(result?.total ?? payload.length);
// Populate hours map from backend hoursByDay data
const newHours = {};
payload.forEach((iss) => {
newHours[iss.id] = iss.hoursByDay || Array(getDaysInMonth(y, m)).fill('');
});
setHours(newHours);
} catch (error) {
console.error('Error fetching issues:', error);
showAlert('Failed to fetch issues: ' + (error.message || 'Unknown error'), 'danger');
setIssues([]);
setTotalIssues(0);
} finally {
setLoadingIssues(false);
}
}, [displayedMonth, projectKey, showAlert, useMyTimesheet, myTimesheetKeys]);
useEffect(() => {
fetchIssues(null);
}, [fetchIssues]);
const handleNextPage = () => {
if (nextPageToken) {
fetchIssues(nextPageToken);
}
};
const handlePrevPage = () => {
if (prevTokens.length > 1) {
// pop last token
const newPrev = [...prevTokens];
newPrev.pop();
const token = newPrev[newPrev.length - 1];
setPrevTokens(newPrev);
fetchIssues(token);
} else {
// first page, no previous token
fetchIssues(null);
setPrevTokens([]);
}
};
<Button
className="page-btn-prev"
variant="light"
size="sm"
onClick={handlePrevPage}
disabled={page <= 1 || loadingIssues || useMyTimesheet}
>{t('prevPage')}</Button>
<Button
className="page-btn-next"
variant="light"
size="sm"
onClick={handleNextPage}
disabled={page * PAGE_SIZE >= totalIssues || loadingIssues || useMyTimesheet}
>{t('nextPage')}</Button>walkout_ kirjoitti:
Minulla on ongelma paginationissa listata Jira issueita.
[...]
Eli ei ole startAt vaan on nextPageToken ja limit. Eli requestin yhteydessä luodaan nextPageToken joka on string.
Mikä ongelmasi on tarkemmin? Millaiseen kyselyyn saat odottamattoman vastauksen? Koodisi ilmeisesti yrittää käyttää nextPageTokeneita.
Voin auttaa hintaan 500e + 100e/h.