Merge pull request 'Complete unread discovery before mobile Updates triage' (#750) from timmy/749-complete-updates-discovery into main
This commit is contained in:
commit
b93e63ab70
|
|
@ -992,9 +992,19 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateTriageLauncher = createUpdateTriageLauncher({
|
||||||
|
selectUpdates: () => selectMobileQueue('update'),
|
||||||
|
discover: () => notificationPager.loadAll(() => lastNotifications),
|
||||||
|
isUpdatesSelected: () => selectedWorkFilter === 'update',
|
||||||
|
hasMore: () => Boolean(notificationPagination.has_more),
|
||||||
|
hasCheckpoint: () => updateTriage.resumable(),
|
||||||
|
resume: () => updateTriage.resume(),
|
||||||
|
start: () => updateTriage.start(),
|
||||||
|
announce: message => { qs('#my-work-action-status').textContent = message; },
|
||||||
|
});
|
||||||
|
|
||||||
function openUpdateTriage() {
|
function openUpdateTriage() {
|
||||||
selectMobileQueue('update');
|
return updateTriageLauncher.open();
|
||||||
return updateTriage.resumable() ? updateTriage.resume() : updateTriage.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function routedWorkItem(item) {
|
function routedWorkItem(item) {
|
||||||
|
|
|
||||||
|
|
@ -1073,6 +1073,7 @@
|
||||||
<script src="static/mobile-work-entry.js"></script>
|
<script src="static/mobile-work-entry.js"></script>
|
||||||
<script src="static/mobile-queue-launcher.js"></script>
|
<script src="static/mobile-queue-launcher.js"></script>
|
||||||
<script src="static/update-triage-session.js"></script>
|
<script src="static/update-triage-session.js"></script>
|
||||||
|
<script src="static/update-triage-launcher.js"></script>
|
||||||
<script src="static/agenda-session-launcher.js"></script>
|
<script src="static/agenda-session-launcher.js"></script>
|
||||||
<script src="static/mobile-launch.js"></script>
|
<script src="static/mobile-launch.js"></script>
|
||||||
<script src="static/mobile-app-shortcuts.js"></script>
|
<script src="static/mobile-app-shortcuts.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,8 @@ function createNotificationSelection({ limit = 50, onChange = () => {} } = {}) {
|
||||||
function createNotificationPager({ load, onNotifications, onPagination, onStatus }) {
|
function createNotificationPager({ load, onNotifications, onPagination, onStatus }) {
|
||||||
let pagination = { page: 1, total: 0, has_more: false };
|
let pagination = { page: 1, total: 0, has_more: false };
|
||||||
let pending = false;
|
let pending = false;
|
||||||
return {
|
let completing = null;
|
||||||
|
const pager = {
|
||||||
reset(next) {
|
reset(next) {
|
||||||
pagination = { ...pagination, ...(next || {}) };
|
pagination = { ...pagination, ...(next || {}) };
|
||||||
onPagination(pagination);
|
onPagination(pagination);
|
||||||
|
|
@ -282,7 +283,19 @@ function createNotificationPager({ load, onNotifications, onPagination, onStatus
|
||||||
pending = false;
|
pending = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
loadAll(getExisting) {
|
||||||
|
if (completing) return completing;
|
||||||
|
completing = (async () => {
|
||||||
|
while (pagination.has_more) {
|
||||||
|
const loaded = await pager.loadMore(getExisting());
|
||||||
|
if (!loaded) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})().finally(() => { completing = null; });
|
||||||
|
return completing;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
return pager;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWorkPager({ load, onItems, onPagination, onStatus }) {
|
function createWorkPager({ load, onItems, onPagination, onStatus }) {
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ const SHELL = [
|
||||||
BASE + 'static/mobile-work-entry.js',
|
BASE + 'static/mobile-work-entry.js',
|
||||||
BASE + 'static/mobile-queue-launcher.js',
|
BASE + 'static/mobile-queue-launcher.js',
|
||||||
BASE + 'static/update-triage-session.js',
|
BASE + 'static/update-triage-session.js',
|
||||||
|
BASE + 'static/update-triage-launcher.js',
|
||||||
BASE + 'static/agenda-session-launcher.js',
|
BASE + 'static/agenda-session-launcher.js',
|
||||||
BASE + 'static/mobile-launch.js',
|
BASE + 'static/mobile-launch.js',
|
||||||
BASE + 'static/mobile-app-shortcuts.js',
|
BASE + 'static/mobile-app-shortcuts.js',
|
||||||
|
|
|
||||||
34
frontend/update-triage-launcher.js
Normal file
34
frontend/update-triage-launcher.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||||
|
else root.createUpdateTriageLauncher = factory;
|
||||||
|
})(typeof self !== 'undefined' ? self : this, function createUpdateTriageLauncher(options) {
|
||||||
|
let pending = null;
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
if (pending) return pending;
|
||||||
|
options.selectUpdates();
|
||||||
|
options.announce('Checking all unread updates…');
|
||||||
|
pending = Promise.resolve()
|
||||||
|
.then(() => options.discover())
|
||||||
|
.then(complete => {
|
||||||
|
if (!options.isUpdatesSelected()) return 'cancelled';
|
||||||
|
if (complete === false || options.hasMore()) {
|
||||||
|
options.announce('Updates check paused. Retry to check older unread updates.');
|
||||||
|
return 'incomplete';
|
||||||
|
}
|
||||||
|
const opened = options.hasCheckpoint() ? options.resume() : options.start();
|
||||||
|
if (!opened) options.announce('No unread updates are ready.');
|
||||||
|
return opened ? 'opened' : 'empty';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (options.isUpdatesSelected()) {
|
||||||
|
options.announce('Updates check paused. Retry to check older unread updates.');
|
||||||
|
}
|
||||||
|
return 'incomplete';
|
||||||
|
})
|
||||||
|
.finally(() => { pending = null; });
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {open};
|
||||||
|
});
|
||||||
|
|
@ -28,7 +28,7 @@ FEATURE_SOURCES = {
|
||||||
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
|
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
|
||||||
"security-center": ("static/security-center.js",),
|
"security-center": ("static/security-center.js",),
|
||||||
"today-timer": (
|
"today-timer": (
|
||||||
"static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
"static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-triage-launcher.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
|
||||||
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
|
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
|
||||||
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
|
||||||
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,109 @@ PICK_WORK = Path(__file__).parents[1] / "frontend" / "pick-work.js"
|
||||||
WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js"
|
WORK_ROUTE = Path(__file__).parents[1] / "frontend" / "work-route.js"
|
||||||
UPDATE_OWNERSHIP = Path(__file__).parents[1] / "frontend" / "update-ownership.js"
|
UPDATE_OWNERSHIP = Path(__file__).parents[1] / "frontend" / "update-ownership.js"
|
||||||
AGENDA_SESSION_LAUNCHER = Path(__file__).parents[1] / "frontend" / "agenda-session-launcher.js"
|
AGENDA_SESSION_LAUNCHER = Path(__file__).parents[1] / "frontend" / "agenda-session-launcher.js"
|
||||||
|
UPDATE_TRIAGE_LAUNCHER = Path(__file__).parents[1] / "frontend" / "update-triage-launcher.js"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_triage_launch_waits_for_complete_single_flight_discovery():
|
||||||
|
script = f"""
|
||||||
|
const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))});
|
||||||
|
let release;
|
||||||
|
let discoveries = 0;
|
||||||
|
let opens = 0;
|
||||||
|
const messages = [];
|
||||||
|
const launcher = createLauncher({{
|
||||||
|
selectUpdates: () => {{}},
|
||||||
|
discover: () => {{ discoveries += 1; return new Promise(resolve => {{ release = resolve; }}); }},
|
||||||
|
isUpdatesSelected: () => true,
|
||||||
|
hasMore: () => false,
|
||||||
|
hasCheckpoint: () => false,
|
||||||
|
resume: () => {{ throw new Error('must start'); }},
|
||||||
|
start: () => {{ opens += 1; return true; }},
|
||||||
|
announce: message => messages.push(message),
|
||||||
|
}});
|
||||||
|
const first = launcher.open();
|
||||||
|
const second = launcher.open();
|
||||||
|
Promise.resolve().then(() => {{
|
||||||
|
const before = {{discoveries, opens, same:first === second}};
|
||||||
|
release(true);
|
||||||
|
return Promise.all([first, second]).then(results =>
|
||||||
|
process.stdout.write(JSON.stringify({{before, discoveries, opens, results, messages}}))
|
||||||
|
);
|
||||||
|
}});
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"before": {"discoveries": 1, "opens": 0, "same": True},
|
||||||
|
"discoveries": 1,
|
||||||
|
"opens": 1,
|
||||||
|
"results": ["opened", "opened"],
|
||||||
|
"messages": ["Checking all unread updates…"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_triage_launch_preserves_checkpoint_when_discovery_fails():
|
||||||
|
script = f"""
|
||||||
|
const createLauncher = require({json.dumps(str(UPDATE_TRIAGE_LAUNCHER))});
|
||||||
|
const messages = [];
|
||||||
|
let resumed = 0;
|
||||||
|
const launcher = createLauncher({{
|
||||||
|
selectUpdates: () => {{}},
|
||||||
|
discover: async () => false,
|
||||||
|
isUpdatesSelected: () => true,
|
||||||
|
hasMore: () => true,
|
||||||
|
hasCheckpoint: () => true,
|
||||||
|
resume: () => {{ resumed += 1; return true; }},
|
||||||
|
start: () => true,
|
||||||
|
announce: message => messages.push(message),
|
||||||
|
}});
|
||||||
|
launcher.open().then(result => process.stdout.write(JSON.stringify({{result, resumed, messages}})));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"result": "incomplete",
|
||||||
|
"resumed": 0,
|
||||||
|
"messages": [
|
||||||
|
"Checking all unread updates…",
|
||||||
|
"Updates check paused. Retry to check older unread updates.",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_notification_pager_load_all_retries_from_failed_page_without_duplicates():
|
||||||
|
script = f"""
|
||||||
|
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||||
|
let items = [{{id:1}}];
|
||||||
|
let fail = true;
|
||||||
|
const requested = [];
|
||||||
|
const pager = buildMyWork.createNotificationPager({{
|
||||||
|
load: async page => {{
|
||||||
|
requested.push(page);
|
||||||
|
if (page === 2 && fail) {{ fail = false; throw new Error('offline'); }}
|
||||||
|
return page === 2 ? {{page:2,total:101,has_more:true,items:[{{id:1}},{{id:51}}]}} :
|
||||||
|
{{page:3,total:101,has_more:false,items:[{{id:101}}]}};
|
||||||
|
}},
|
||||||
|
onNotifications: value => {{ items = value; }},
|
||||||
|
onPagination: () => {{}},
|
||||||
|
onStatus: () => {{}},
|
||||||
|
}});
|
||||||
|
pager.reset({{page:1,total:101,has_more:true}});
|
||||||
|
pager.loadAll(() => items).then(firstResult => pager.loadAll(() => items).then(secondResult =>
|
||||||
|
process.stdout.write(JSON.stringify({{firstResult, secondResult, requested, ids:items.map(item => item.id)}}))
|
||||||
|
));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"firstResult": False,
|
||||||
|
"secondResult": True,
|
||||||
|
"requested": [2, 2, 3],
|
||||||
|
"ids": [1, 51, 101],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_agenda_session_launch_waits_for_complete_single_flight_discovery():
|
def test_agenda_session_launch_waits_for_complete_single_flight_discovery():
|
||||||
|
|
|
||||||
|
|
@ -847,6 +847,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/mobile-work-entry.js",
|
"/dashboard/static/mobile-work-entry.js",
|
||||||
"/dashboard/static/mobile-queue-launcher.js",
|
"/dashboard/static/mobile-queue-launcher.js",
|
||||||
"/dashboard/static/update-triage-session.js",
|
"/dashboard/static/update-triage-session.js",
|
||||||
|
"/dashboard/static/update-triage-launcher.js",
|
||||||
"/dashboard/static/agenda-session-launcher.js",
|
"/dashboard/static/agenda-session-launcher.js",
|
||||||
"/dashboard/static/mobile-launch.js",
|
"/dashboard/static/mobile-launch.js",
|
||||||
"/dashboard/static/mobile-app-shortcuts.js",
|
"/dashboard/static/mobile-app-shortcuts.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user