Merge pull request 'Open Following alerts directly in changed-first review' (#1318) from timmy/1317-open-following-alert-review into main
This commit is contained in:
commit
e572b3410e
|
|
@ -8233,6 +8233,7 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async function openWorkQueueRoute(filter, action = null) {
|
async function openWorkQueueRoute(filter, action = null) {
|
||||||
|
if (filter === 'following') return followingQueue.route();
|
||||||
if (!selectWorkQueue(filter, { preserveRoute:true })) return;
|
if (!selectWorkQueue(filter, { preserveRoute:true })) return;
|
||||||
qs('#my-work').scrollIntoView({block:'start'});
|
qs('#my-work').scrollIntoView({block:'start'});
|
||||||
qs('#my-work').focus();
|
qs('#my-work').focus();
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@
|
||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
let feature;
|
let feature;
|
||||||
|
const show = () => query('#following-sheet').open || query('#following-sheet').showModal();
|
||||||
function render(state) {
|
function render(state) {
|
||||||
hooks.onStatus?.(state.status);
|
hooks.onStatus?.(state.status);
|
||||||
const list = query('#following-list');
|
const list = query('#following-list');
|
||||||
|
|
@ -191,16 +192,18 @@
|
||||||
query('#review-following').addEventListener('click', () => {
|
query('#review-following').addEventListener('click', () => {
|
||||||
query('#following-sheet').close();
|
query('#following-sheet').close();
|
||||||
feature.startReview().catch(() => {
|
feature.startReview().catch(() => {
|
||||||
if (!query('#following-sheet').open) query('#following-sheet').showModal();
|
show();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
query('#retry-following').addEventListener('click', () => feature.load().catch(() => {}));
|
query('#retry-following').addEventListener('click', () => feature.load().catch(() => {}));
|
||||||
return {
|
return {
|
||||||
load:feature.load,
|
load:feature.load,
|
||||||
review:feature.startReview,
|
review:feature.startReview,
|
||||||
|
async route() {
|
||||||
|
await feature.load().then(feature.startReview).catch(() => false) || show();
|
||||||
|
},
|
||||||
open() {
|
open() {
|
||||||
const sheet = query('#following-sheet');
|
show();
|
||||||
if (!sheet.open) sheet.showModal();
|
|
||||||
feature.load().catch(() => {});
|
feature.load().catch(() => {});
|
||||||
return 'opened-following';
|
return 'opened-following';
|
||||||
},
|
},
|
||||||
|
|
@ -210,8 +213,7 @@
|
||||||
returnToFollowing() {
|
returnToFollowing() {
|
||||||
const completed = feature.finishReview();
|
const completed = feature.finishReview();
|
||||||
if (completed) return 'completed-following';
|
if (completed) return 'completed-following';
|
||||||
const sheet = query('#following-sheet');
|
show();
|
||||||
if (!sheet.open) sheet.showModal();
|
|
||||||
globalThis.requestAnimationFrame?.(() => {
|
globalThis.requestAnimationFrame?.(() => {
|
||||||
const target = query('#review-following:not([hidden])') || query('.following-card') || query('#close-following');
|
const target = query('#review-following:not([hidden])') || query('.following-card') || query('#close-following');
|
||||||
target?.focus();
|
target?.focus();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const repositoryPart = /^[A-Za-z0-9_.-]+$/;
|
const repositoryPart = /^[A-Za-z0-9_.-]+$/;
|
||||||
const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft'];
|
const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft', 'following'];
|
||||||
const sections = {
|
const sections = {
|
||||||
issue: ['overview', 'conversation', 'reply', 'actions'],
|
issue: ['overview', 'conversation', 'reply', 'actions'],
|
||||||
filed: ['overview', 'conversation', 'reply', 'actions'],
|
filed: ['overview', 'conversation', 'reply', 'actions'],
|
||||||
|
|
|
||||||
|
|
@ -80,3 +80,34 @@ def test_following_queue_is_phone_usable_at_narrow_viewport(viewport):
|
||||||
overflow = page.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth")
|
overflow = page.evaluate("document.documentElement.scrollWidth > document.documentElement.clientWidth")
|
||||||
assert overflow is False
|
assert overflow is False
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_alert_route_opens_truthful_empty_state_with_one_refresh():
|
||||||
|
with sync_playwright() as playwright:
|
||||||
|
browser = playwright.chromium.launch(headless=True)
|
||||||
|
page = browser.new_page(viewport={"width": 390, "height": 844})
|
||||||
|
page.set_content((FRONTEND / "index.html").read_text())
|
||||||
|
page.add_style_tag(path=FRONTEND / "dashboard.css")
|
||||||
|
page.add_script_tag(path=FRONTEND / "following.js")
|
||||||
|
|
||||||
|
result = page.evaluate("""async () => {
|
||||||
|
let requests=0;
|
||||||
|
globalThis.fetch=async () => {
|
||||||
|
requests += 1;
|
||||||
|
return {ok:true,json:async () => ({revision:2,items:[]})};
|
||||||
|
};
|
||||||
|
const queue=attachFollowing(() => {});
|
||||||
|
await queue.route();
|
||||||
|
return {requests,open:document.querySelector('#following-sheet').open,
|
||||||
|
status:document.querySelector('#following-status').textContent};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"requests": 1,
|
||||||
|
|
||||||
|
"open": True,
|
||||||
|
"status": "No watched items yet. Watch an issue or pull request from Search to keep it here.",
|
||||||
|
}
|
||||||
|
expect(page.locator("#following-sheet")).to_be_visible()
|
||||||
|
assert page.locator("#close-following").bounding_box()["height"] >= 44
|
||||||
|
browser.close()
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,26 @@ const feature=createFollowing({{
|
||||||
assert result["session"]["items"][0]["kind"] == "pull"
|
assert result["session"]["items"][0]["kind"] == "pull"
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_route_loads_authoritative_state_before_starting_review():
|
||||||
|
following = MODULE.read_text()
|
||||||
|
start = following.index("async route()")
|
||||||
|
route = following[start:following.index("open()", start)]
|
||||||
|
|
||||||
|
assert "await feature.load().then(feature.startReview)" in route
|
||||||
|
assert ".catch(() => false)" in route
|
||||||
|
assert "show();" in route
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_dispatches_following_route_to_changed_first_review():
|
||||||
|
dashboard = (ROOT / "frontend" / "dashboard.js").read_text()
|
||||||
|
|
||||||
|
start = dashboard.index("async function openWorkQueueRoute")
|
||||||
|
route_handler = dashboard[start:dashboard.index("qs('#work-milestone-filter')", start)]
|
||||||
|
assert "if (filter === 'following')" in route_handler
|
||||||
|
assert "return followingQueue.route();" in route_handler
|
||||||
|
assert route_handler.index("followingQueue.route") < route_handler.index("selectWorkQueue")
|
||||||
|
|
||||||
|
|
||||||
def test_following_typed_identity_prevents_issue_pull_collisions():
|
def test_following_typed_identity_prevents_issue_pull_collisions():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createFollowing = require({json.dumps(str(MODULE))});
|
const createFollowing = require({json.dumps(str(MODULE))});
|
||||||
|
|
|
||||||
|
|
@ -676,6 +676,34 @@ process.stdout.write(JSON.stringify(routes.parse('#/my-work/agenda/protect-today
|
||||||
assert json.loads(result.stdout) == {
|
assert json.loads(result.stdout) == {
|
||||||
"kind": "queue", "filter": "agenda", "action": "protect-today"
|
"kind": "queue", "filter": "agenda", "action": "protect-today"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_following_route_waits_for_hydration_then_opens_once():
|
||||||
|
script = f"""
|
||||||
|
const routes = require({json.dumps(str(WORK_ROUTE))});
|
||||||
|
const listeners = {{}};
|
||||||
|
const location = {{hash:'#/my-work/following'}};
|
||||||
|
const calls = [];
|
||||||
|
const controller = routes.createController({{
|
||||||
|
location,
|
||||||
|
history: {{pushState() {{}}, replaceState() {{}}, back() {{}}}},
|
||||||
|
eventTarget: {{addEventListener(name, fn) {{ listeners[name] = fn; }}}},
|
||||||
|
onQueue: (queue, action) => calls.push([queue, action]),
|
||||||
|
onInvalid: () => calls.push(['invalid']),
|
||||||
|
}});
|
||||||
|
controller.start();
|
||||||
|
controller.setItems([]);
|
||||||
|
controller.sync();
|
||||||
|
process.stdout.write(JSON.stringify({{route:routes.parse(location.hash), calls}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], check=True, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"route": {"kind": "queue", "filter": "following"},
|
||||||
|
"calls": [["following", None]],
|
||||||
|
}
|
||||||
CARD_PLANNING = Path(__file__).parents[1] / "frontend" / "card-planning.js"
|
CARD_PLANNING = Path(__file__).parents[1] / "frontend" / "card-planning.js"
|
||||||
TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js"
|
TODAY_WORK = Path(__file__).parents[1] / "frontend" / "today-work.js"
|
||||||
WORK_SELECTION = Path(__file__).parents[1] / "frontend" / "work-selection.js"
|
WORK_SELECTION = Path(__file__).parents[1] / "frontend" / "work-selection.js"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user