Make mobile issue sheets read-first with on-demand planning #216
|
|
@ -142,6 +142,9 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
|
||||||
.issue-sheet-header button { min-height:44px; }
|
.issue-sheet-header button { min-height:44px; }
|
||||||
.issue-sheet-content { overflow-wrap:anywhere; white-space:pre-wrap; }
|
.issue-sheet-content { overflow-wrap:anywhere; white-space:pre-wrap; }
|
||||||
|
.issue-planning { max-width:100%; margin-top:16px; border:1px solid #2a496e; border-radius:12px; padding:0 12px 12px; overflow-x:hidden; }
|
||||||
|
.issue-planning > summary { min-height:44px; display:flex; align-items:center; cursor:pointer; font-weight:700; }
|
||||||
|
.issue-planning-retry { min-height:44px; width:100%; }
|
||||||
#edit-issue-content { min-height:44px; width:100%; }
|
#edit-issue-content { min-height:44px; width:100%; }
|
||||||
.issue-edit-form { display:grid; gap:8px; max-width:100%; margin:12px 0; }
|
.issue-edit-form { display:grid; gap:8px; max-width:100%; margin:12px 0; }
|
||||||
.issue-edit-form label { display:grid; gap:6px; min-width:0; }
|
.issue-edit-form label { display:grid; gap:6px; min-width:0; }
|
||||||
|
|
@ -417,10 +420,38 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<div id="issue-sheet-status" class="small" aria-live="polite">Choose an issue.</div>
|
<div id="issue-sheet-status" class="small" aria-live="polite">Choose an issue.</div>
|
||||||
<button class="issue-retry" id="retry-issue-load" type="button" hidden>Reload latest issue</button>
|
<button class="issue-retry" id="retry-issue-load" type="button" hidden>Reload latest issue</button>
|
||||||
<div class="row"><span id="issue-labels"></span><span class="small" id="issue-assignees"></span></div>
|
<div class="row"><span id="issue-labels"></span><span class="small" id="issue-assignees"></span></div>
|
||||||
|
<p class="issue-sheet-content" id="issue-sheet-body"></p>
|
||||||
|
<h2>Full conversation</h2>
|
||||||
|
<div id="issue-comments"></div>
|
||||||
|
<button class="conversation-more" id="load-older-issue-comments" type="button" hidden>Load older messages</button>
|
||||||
|
<div id="issue-conversation-status" class="small" aria-live="assertive"></div>
|
||||||
|
<section class="issue-comment-composer" aria-labelledby="issue-comment-title">
|
||||||
|
<h2 id="issue-comment-title">Add comment</h2>
|
||||||
|
<textarea id="issue-comment" maxlength="10000" placeholder="Write a comment"></textarea>
|
||||||
|
<button id="send-issue-comment" type="button">Post comment</button>
|
||||||
|
<div id="issue-comment-status" class="small" aria-live="assertive"></div>
|
||||||
|
</section>
|
||||||
|
<details class="issue-planning" id="issue-planning">
|
||||||
|
<summary>Plan & edit</summary>
|
||||||
|
<button class="issue-planning-retry" id="retry-issue-planning" type="button" hidden>Retry planning controls</button>
|
||||||
|
<button id="edit-issue-content" type="button" disabled>Edit issue</button>
|
||||||
|
<form class="issue-edit-form" id="issue-edit-form" hidden>
|
||||||
|
<label for="issue-edit-title">Title
|
||||||
|
<input id="issue-edit-title" type="text" maxlength="255" required autocomplete="off" />
|
||||||
|
</label>
|
||||||
|
<label for="issue-edit-body">Description
|
||||||
|
<textarea id="issue-edit-body" maxlength="10000"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="issue-edit-actions">
|
||||||
|
<button id="save-issue-content" type="submit">Save issue</button>
|
||||||
|
<button id="cancel-issue-content" type="button">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div id="issue-edit-status" class="small" aria-live="assertive"></div>
|
||||||
|
</form>
|
||||||
<fieldset class="issue-label-editor" id="issue-label-editor" aria-describedby="issue-label-status">
|
<fieldset class="issue-label-editor" id="issue-label-editor" aria-describedby="issue-label-status">
|
||||||
<legend>Labels</legend>
|
<legend>Labels</legend>
|
||||||
<div class="issue-label-list" id="issue-label-list"></div>
|
<div class="issue-label-list" id="issue-label-list"></div>
|
||||||
<div class="small" id="issue-label-status" aria-live="polite">Load an issue to edit labels.</div>
|
<div class="small" id="issue-label-status" aria-live="polite">Expand planning controls to load labels.</div>
|
||||||
<button id="save-issue-labels" type="button" disabled>Save labels</button>
|
<button id="save-issue-labels" type="button" disabled>Save labels</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="issue-due-editor" aria-describedby="issue-due-status">
|
<fieldset class="issue-due-editor" aria-describedby="issue-due-status">
|
||||||
|
|
@ -438,33 +469,9 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
<label for="issue-milestone" class="small">Plan this issue into a release</label>
|
<label for="issue-milestone" class="small">Plan this issue into a release</label>
|
||||||
<select id="issue-milestone" disabled><option value="">No milestone</option></select>
|
<select id="issue-milestone" disabled><option value="">No milestone</option></select>
|
||||||
<button id="save-issue-milestone" type="button" disabled>Save milestone</button>
|
<button id="save-issue-milestone" type="button" disabled>Save milestone</button>
|
||||||
<div id="issue-milestone-status" class="small" aria-live="assertive">Load an issue to plan it.</div>
|
<div id="issue-milestone-status" class="small" aria-live="assertive">Expand planning controls to load milestones.</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<p class="issue-sheet-content" id="issue-sheet-body"></p>
|
</details>
|
||||||
<button id="edit-issue-content" type="button" disabled>Edit issue</button>
|
|
||||||
<form class="issue-edit-form" id="issue-edit-form" hidden>
|
|
||||||
<label for="issue-edit-title">Title
|
|
||||||
<input id="issue-edit-title" type="text" maxlength="255" required autocomplete="off" />
|
|
||||||
</label>
|
|
||||||
<label for="issue-edit-body">Description
|
|
||||||
<textarea id="issue-edit-body" maxlength="10000"></textarea>
|
|
||||||
</label>
|
|
||||||
<div class="issue-edit-actions">
|
|
||||||
<button id="save-issue-content" type="submit">Save issue</button>
|
|
||||||
<button id="cancel-issue-content" type="button">Cancel</button>
|
|
||||||
</div>
|
|
||||||
<div id="issue-edit-status" class="small" aria-live="assertive"></div>
|
|
||||||
</form>
|
|
||||||
<h2>Full conversation</h2>
|
|
||||||
<div id="issue-comments"></div>
|
|
||||||
<button class="conversation-more" id="load-older-issue-comments" type="button" hidden>Load older messages</button>
|
|
||||||
<div id="issue-conversation-status" class="small" aria-live="assertive"></div>
|
|
||||||
<section class="issue-comment-composer" aria-labelledby="issue-comment-title">
|
|
||||||
<h2 id="issue-comment-title">Add comment</h2>
|
|
||||||
<textarea id="issue-comment" maxlength="10000" placeholder="Write a comment"></textarea>
|
|
||||||
<button id="send-issue-comment" type="button">Post comment</button>
|
|
||||||
<div id="issue-comment-status" class="small" aria-live="assertive"></div>
|
|
||||||
</section>
|
|
||||||
<div class="issue-sheet-actions">
|
<div class="issue-sheet-actions">
|
||||||
<button class="share-work-route" type="button">Share</button>
|
<button class="share-work-route" type="button">Share</button>
|
||||||
<button id="release-issue" type="button">Release assignment</button>
|
<button id="release-issue" type="button">Release assignment</button>
|
||||||
|
|
@ -764,6 +771,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
}
|
}
|
||||||
const reviewController = createReviewController({ fetchJson: fetchReviewJson, storage: localStorage });
|
const reviewController = createReviewController({ fetchJson: fetchReviewJson, storage: localStorage });
|
||||||
const issueController = createIssueSheet({ fetchJson: fetchReviewJson, storage: localStorage });
|
const issueController = createIssueSheet({ fetchJson: fetchReviewJson, storage: localStorage });
|
||||||
|
const planningLoader = createIssueSheet.createPlanningLoader({
|
||||||
|
loadLabels: item => issueController.loadLabels(item),
|
||||||
|
loadMilestones: item => issueController.loadMilestones(item),
|
||||||
|
});
|
||||||
const issueCapture = createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage });
|
const issueCapture = createIssueCapture({ fetchJson: fetchReviewJson, storage: localStorage });
|
||||||
const pullController = createPullSheet({ fetchJson: fetchReviewJson, storage: localStorage });
|
const pullController = createPullSheet({ fetchJson: fetchReviewJson, storage: localStorage });
|
||||||
const draftInbox = createDraftInbox({ storage: localStorage });
|
const draftInbox = createDraftInbox({ storage: localStorage });
|
||||||
|
|
@ -1356,15 +1367,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
comments.length + ' of ' + Math.max(state.total || 0, comments.length) + ' messages loaded.' : 'No comments yet.';
|
comments.length + ' of ' + Math.max(state.total || 0, comments.length) + ' messages loaded.' : 'No comments yet.';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadIssueLabelEditor(item, confirmedNames) {
|
function renderIssueLabelEditor(item, confirmedNames, labels) {
|
||||||
const list = qs('#issue-label-list');
|
const list = qs('#issue-label-list');
|
||||||
const status = qs('#issue-label-status');
|
const status = qs('#issue-label-status');
|
||||||
list.textContent = '';
|
list.textContent = '';
|
||||||
qs('#save-issue-labels').disabled = true;
|
|
||||||
status.textContent = 'Loading labels…';
|
|
||||||
try {
|
|
||||||
const labels = await issueController.loadLabels(item);
|
|
||||||
if (selectedIssue !== item) return;
|
|
||||||
const draftIds = issueController.loadLabelDraft(item);
|
const draftIds = issueController.loadLabelDraft(item);
|
||||||
const selectedIds = draftIds.length ? new Set(draftIds) : new Set(
|
const selectedIds = draftIds.length ? new Set(draftIds) : new Set(
|
||||||
labels.filter(label => confirmedNames.includes(label.name)).map(label => Number(label.id))
|
labels.filter(label => confirmedNames.includes(label.name)).map(label => Number(label.id))
|
||||||
|
|
@ -1376,9 +1382,6 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
).join('');
|
).join('');
|
||||||
status.textContent = labels.length ? 'Choose labels, then save.' : 'This repository has no labels.';
|
status.textContent = labels.length ? 'Choose labels, then save.' : 'This repository has no labels.';
|
||||||
qs('#save-issue-labels').disabled = false;
|
qs('#save-issue-labels').disabled = false;
|
||||||
} catch (_error) {
|
|
||||||
status.textContent = 'Labels could not be loaded. Retry by reopening this issue.';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedEditIssueLabelIds() {
|
function selectedEditIssueLabelIds() {
|
||||||
|
|
@ -1386,16 +1389,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.map(input => Number(input.value)).filter(Number.isInteger);
|
.map(input => Number(input.value)).filter(Number.isInteger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadIssueMilestoneEditor(item, confirmedMilestone) {
|
function renderIssueMilestoneEditor(item, confirmedMilestone, milestones) {
|
||||||
const select = qs('#issue-milestone');
|
const select = qs('#issue-milestone');
|
||||||
const status = qs('#issue-milestone-status');
|
const status = qs('#issue-milestone-status');
|
||||||
select.innerHTML = '<option value="">No milestone</option>';
|
select.innerHTML = '<option value="">No milestone</option>';
|
||||||
select.disabled = true;
|
|
||||||
qs('#save-issue-milestone').disabled = true;
|
|
||||||
status.textContent = 'Loading milestones…';
|
|
||||||
try {
|
|
||||||
const milestones = await issueController.loadMilestones(item);
|
|
||||||
if (selectedIssue !== item) return;
|
|
||||||
select.innerHTML += milestones.map(milestone =>
|
select.innerHTML += milestones.map(milestone =>
|
||||||
'<option value="' + Number(milestone.id) + '">' + escapeHtml(milestone.title) + '</option>'
|
'<option value="' + Number(milestone.id) + '">' + escapeHtml(milestone.title) + '</option>'
|
||||||
).join('');
|
).join('');
|
||||||
|
|
@ -1405,8 +1402,25 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#save-issue-milestone').disabled = false;
|
qs('#save-issue-milestone').disabled = false;
|
||||||
status.textContent = confirmedMilestone ?
|
status.textContent = confirmedMilestone ?
|
||||||
'Planned for ' + confirmedMilestone.title + '.' : 'No milestone set.';
|
'Planned for ' + confirmedMilestone.title + '.' : 'No milestone set.';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadIssuePlanning() {
|
||||||
|
const item = selectedIssue;
|
||||||
|
const detail = selectedIssueDetail;
|
||||||
|
if (!item || !detail) return;
|
||||||
|
qs('#retry-issue-planning').hidden = true;
|
||||||
|
qs('#issue-label-status').textContent = 'Loading labels…';
|
||||||
|
qs('#issue-milestone-status').textContent = 'Loading milestones…';
|
||||||
|
try {
|
||||||
|
const planning = await planningLoader.open(selectedIssue);
|
||||||
|
if (selectedIssue !== item) return;
|
||||||
|
renderIssueLabelEditor(item, detail.labels || [], planning.labels);
|
||||||
|
renderIssueMilestoneEditor(item, detail.milestone, planning.milestones);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
status.textContent = 'Milestones could not be loaded. Retry by reopening this issue.';
|
if (selectedIssue !== item) return;
|
||||||
|
qs('#issue-label-status').textContent = 'Labels could not be loaded.';
|
||||||
|
qs('#issue-milestone-status').textContent = 'Milestones could not be loaded.';
|
||||||
|
qs('#retry-issue-planning').hidden = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1428,8 +1442,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#issue-conversation-status').textContent = 'Loading newest messages…';
|
qs('#issue-conversation-status').textContent = 'Loading newest messages…';
|
||||||
qs('#issue-comment').value = issueController.loadDraft(item);
|
qs('#issue-comment').value = issueController.loadDraft(item);
|
||||||
qs('#issue-comment-status').textContent = '';
|
qs('#issue-comment-status').textContent = '';
|
||||||
|
qs('#issue-planning').open = false;
|
||||||
|
qs('#retry-issue-planning').hidden = true;
|
||||||
qs('#issue-label-list').textContent = '';
|
qs('#issue-label-list').textContent = '';
|
||||||
qs('#issue-label-status').textContent = 'Loading labels…';
|
qs('#issue-label-status').textContent = 'Expand planning controls to load labels.';
|
||||||
qs('#save-issue-labels').disabled = true;
|
qs('#save-issue-labels').disabled = true;
|
||||||
qs('#issue-due-date').value = '';
|
qs('#issue-due-date').value = '';
|
||||||
qs('#issue-due-date').disabled = true;
|
qs('#issue-due-date').disabled = true;
|
||||||
|
|
@ -1439,7 +1455,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#issue-milestone').innerHTML = '<option value="">No milestone</option>';
|
qs('#issue-milestone').innerHTML = '<option value="">No milestone</option>';
|
||||||
qs('#issue-milestone').disabled = true;
|
qs('#issue-milestone').disabled = true;
|
||||||
qs('#save-issue-milestone').disabled = true;
|
qs('#save-issue-milestone').disabled = true;
|
||||||
qs('#issue-milestone-status').textContent = 'Loading milestones…';
|
qs('#issue-milestone-status').textContent = 'Expand planning controls to load milestones.';
|
||||||
qs('#retry-issue-load').hidden = true;
|
qs('#retry-issue-load').hidden = true;
|
||||||
qs('#open-issue-gitea').href = item.url || '#';
|
qs('#open-issue-gitea').href = item.url || '#';
|
||||||
qs('#send-issue-comment').disabled = false;
|
qs('#send-issue-comment').disabled = false;
|
||||||
|
|
@ -1458,8 +1474,6 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#issue-labels').innerHTML = (detail.labels || []).map(label =>
|
qs('#issue-labels').innerHTML = (detail.labels || []).map(label =>
|
||||||
'<span class="pill">' + escapeHtml(label) + '</span>'
|
'<span class="pill">' + escapeHtml(label) + '</span>'
|
||||||
).join(' ');
|
).join(' ');
|
||||||
loadIssueLabelEditor(item, detail.labels || []);
|
|
||||||
loadIssueMilestoneEditor(item, detail.milestone);
|
|
||||||
qs('#issue-assignees').textContent = (detail.assignees || []).length ?
|
qs('#issue-assignees').textContent = (detail.assignees || []).length ?
|
||||||
'Assigned to ' + detail.assignees.join(', ') : 'No assignee reported';
|
'Assigned to ' + detail.assignees.join(', ') : 'No assignee reported';
|
||||||
renderIssueConversation(issueConversation.snapshot());
|
renderIssueConversation(issueConversation.snapshot());
|
||||||
|
|
@ -1473,6 +1487,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#clear-issue-due-date').disabled = !detail.due_date;
|
qs('#clear-issue-due-date').disabled = !detail.due_date;
|
||||||
qs('#issue-due-status').textContent = detail.due_date ?
|
qs('#issue-due-status').textContent = detail.due_date ?
|
||||||
'Due ' + new Date(detail.due_date).toLocaleDateString() : 'No due date set.';
|
'Due ' + new Date(detail.due_date).toLocaleDateString() : 'No due date set.';
|
||||||
|
if (qs('#issue-planning').open) loadIssuePlanning();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (selectedIssue !== item) return;
|
if (selectedIssue !== item) return;
|
||||||
qs('#issue-sheet-status').textContent = error.message + ' Retry here or open it in Gitea.';
|
qs('#issue-sheet-status').textContent = error.message + ' Retry here or open it in Gitea.';
|
||||||
|
|
@ -2346,6 +2361,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
qs('#retry-issue-load').addEventListener('click', () => {
|
qs('#retry-issue-load').addEventListener('click', () => {
|
||||||
if (selectedIssue) openIssueSheet(selectedIssue, issueTrigger);
|
if (selectedIssue) openIssueSheet(selectedIssue, issueTrigger);
|
||||||
});
|
});
|
||||||
|
qs('#issue-planning').addEventListener('toggle', event => {
|
||||||
|
if (event.currentTarget.open) loadIssuePlanning();
|
||||||
|
});
|
||||||
|
qs('#retry-issue-planning').addEventListener('click', loadIssuePlanning);
|
||||||
qs('#edit-issue-content').addEventListener('click', () => {
|
qs('#edit-issue-content').addEventListener('click', () => {
|
||||||
if (!selectedIssue || !selectedIssueDetail?.updated_at) return;
|
if (!selectedIssue || !selectedIssueDetail?.updated_at) return;
|
||||||
const draft = issueController.loadEditDraft(selectedIssue) || {
|
const draft = issueController.loadEditDraft(selectedIssue) || {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,23 @@
|
||||||
|
function createPlanningLoader({ loadLabels, loadMilestones }) {
|
||||||
|
const requests = new Map();
|
||||||
|
const keyFor = item => item.repository + '#' + item.number;
|
||||||
|
|
||||||
|
return {
|
||||||
|
open(item) {
|
||||||
|
const key = keyFor(item);
|
||||||
|
if (requests.has(key)) return requests.get(key);
|
||||||
|
const request = Promise.all([loadLabels(item), loadMilestones(item)])
|
||||||
|
.then(([labels, milestones]) => ({ labels, milestones }))
|
||||||
|
.catch(error => {
|
||||||
|
if (requests.get(key) === request) requests.delete(key);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
requests.set(key, request);
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createIssueSheet({ fetchJson, storage, createConversationPager = globalThis.createConversationPager, createOperationId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random() }) {
|
function createIssueSheet({ fetchJson, storage, createConversationPager = globalThis.createConversationPager, createOperationId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random() }) {
|
||||||
let commentRequest = null;
|
let commentRequest = null;
|
||||||
let closeRequest = null;
|
let closeRequest = null;
|
||||||
|
|
@ -213,4 +233,6 @@ function createIssueSheet({ fetchJson, storage, createConversationPager = global
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createIssueSheet.createPlanningLoader = createPlanningLoader;
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) module.exports = createIssueSheet;
|
if (typeof module !== 'undefined' && module.exports) module.exports = createIssueSheet;
|
||||||
|
|
|
||||||
|
|
@ -535,6 +535,71 @@ async def test_mobile_my_work_exposes_touch_safe_milestone_lane_and_issue_editor
|
||||||
assert 'buildMyWork.replaceIssueMilestone(' in html
|
assert 'buildMyWork.replaceIssueMilestone(' in html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_mobile_issue_sheet_puts_reading_before_collapsed_planning_controls():
|
||||||
|
html = await dashboard()
|
||||||
|
|
||||||
|
body = html.index('id="issue-sheet-body"')
|
||||||
|
conversation = html.index('<h2>Full conversation</h2>', body)
|
||||||
|
planning = html.index('id="issue-planning"', conversation)
|
||||||
|
labels = html.index('id="issue-label-editor"', planning)
|
||||||
|
milestone = html.index('id="issue-milestone"', planning)
|
||||||
|
|
||||||
|
assert '<summary>Plan & edit</summary>' in html[planning:labels]
|
||||||
|
assert '<details class="issue-planning" id="issue-planning">' in html
|
||||||
|
assert body < conversation < planning < labels < milestone
|
||||||
|
|
||||||
|
|
||||||
|
def test_issue_planning_metadata_is_lazy_cached_and_retryable_after_failure():
|
||||||
|
script = f"""
|
||||||
|
const createIssueSheet = require({json.dumps(str(ISSUE_SHEET))});
|
||||||
|
const calls = [];
|
||||||
|
let fail = true;
|
||||||
|
const planning = createIssueSheet.createPlanningLoader({{
|
||||||
|
loadLabels: async item => {{ calls.push('labels:' + item.number); return [{{id:1,name:'P0'}}]; }},
|
||||||
|
loadMilestones: async item => {{
|
||||||
|
calls.push('milestones:' + item.number);
|
||||||
|
if (fail) throw new Error('offline');
|
||||||
|
return [{{id:9,title:'RC'}}];
|
||||||
|
}},
|
||||||
|
}});
|
||||||
|
const item = {{repository:'stackchain/api', number:17}};
|
||||||
|
(async () => {{
|
||||||
|
const before = calls.slice();
|
||||||
|
let failed = false;
|
||||||
|
try {{ await planning.open(item); }} catch (_error) {{ failed = true; }}
|
||||||
|
fail = false;
|
||||||
|
const first = await planning.open(item);
|
||||||
|
const second = await planning.open(item);
|
||||||
|
process.stdout.write(JSON.stringify({{before, failed, calls, same:first === second, first}}));
|
||||||
|
}})();
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"before": [],
|
||||||
|
"failed": True,
|
||||||
|
"calls": ["labels:17", "milestones:17", "labels:17", "milestones:17"],
|
||||||
|
"same": True,
|
||||||
|
"first": {"labels": [{"id": 1, "name": "P0"}], "milestones": [{"id": 9, "title": "RC"}]},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_opening_issue_defers_planning_requests_until_disclosure_expands():
|
||||||
|
html = await dashboard()
|
||||||
|
open_handler = html[html.index('async function openIssueSheet'):html.index('function closeIssueSheet')]
|
||||||
|
|
||||||
|
assert 'loadIssueLabelEditor(' not in open_handler
|
||||||
|
assert 'loadIssueMilestoneEditor(' not in open_handler
|
||||||
|
assert "if (qs('#issue-planning').open) loadIssuePlanning();" in open_handler
|
||||||
|
assert "qs('#issue-planning').addEventListener('toggle'" in html
|
||||||
|
assert 'planningLoader.open(selectedIssue)' in html
|
||||||
|
assert 'id="retry-issue-planning"' in html
|
||||||
|
|
||||||
|
|
||||||
def test_my_work_reviews_filter_and_summary_are_actionable():
|
def test_my_work_reviews_filter_and_summary_are_actionable():
|
||||||
items = [
|
items = [
|
||||||
{"title": "Issue", "kind": "issue", "is_review": False, "is_assigned": True},
|
{"title": "Issue", "kind": "issue", "is_review": False, "is_assigned": True},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user