feat: preserve mobile work section history (Closes #1006)
CI / lint (pull_request) Successful in 2m58s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 2m1s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-17 06:59:20 +00:00
1 parent 484dcc6b9c
commit 09dbdb9961
9 files changed
+282 -24

No files matched your search

+31 -5
View File
@@ -27,6 +27,7 @@
actions:qs('#issue-planning'),
},
planning:qs('#issue-planning'),
onSectionChange:(section, options) => workRoute.section(section, options),
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
});
mobileIssueDetailNavigation.start();
@@ -41,6 +42,7 @@
review:qs('#pull-review'),
},
beforeNavigate:{review(target) { target.open = true; }},
onSectionChange:(section, options) => workRoute.section(section, options),
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
});
mobilePullDetailNavigation.start();
@@ -1409,6 +1411,7 @@
},
replyComposer:qs('#update-reply'),
jumpToNewActivity:() => updateReadPosition.jump(),
onSectionChange:(section, options) => workRoute.section(section, options),
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
});
mobileUpdateDetailNavigation.start();
@@ -1422,6 +1425,7 @@
history:qs('#review-history-workspace'),
},
summaryComposer:qs('#review-summary'),
onSectionChange:(section, options) => workRoute.section(section, options),
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
});
mobileReviewDetailNavigation.start();
@@ -1665,6 +1669,25 @@
workRoute.open(routedWorkItem(item), options);
}
function navigateWorkSection(kind, section) {
if (['issue', 'filed'].includes(kind)) {
mobileIssueDetailNavigation.navigate(section, { focus:false });
} else if (kind === 'pull') {
mobilePullDetailNavigation.navigate(section, { focus:false });
} else if (kind === 'update') {
mobileUpdateDetailNavigation.navigate(section, { focus:false });
} else if (kind === 'review') {
mobileReviewDetailNavigation.navigate(section, { focus:false });
}
}
async function openRoutedWorkSection(item) {
if (item.kind === 'update') await notificationReader.open(item, lastMyWork);
else if (item.kind === 'review') await openReviewSheet(item, reviewTrigger);
else if (item.kind === 'issue' || item.kind === 'filed') await openIssueSheet(item, issueTrigger);
else if (item.kind === 'pull') await openPullSheet(item, pullTrigger);
}
const workRoute = createWorkRoute.createController({
location: window.location,
history: window.history,
@@ -1682,14 +1705,17 @@
qs('#retry-work-route').hidden = true;
qs('#my-work-action-status').textContent = 'Loading shared work item…';
},
onOpen: item => {
onOpen: async item => {
qs('#retry-work-route').hidden = true;
qs('#my-work-action-status').textContent = '';
closeOpenWorkSheets();
if (item.kind === 'update') notificationReader.open(item, lastMyWork);
else if (item.kind === 'review') openReviewSheet(item, reviewTrigger);
else if (item.kind === 'issue' || item.kind === 'filed') openIssueSheet(item, issueTrigger);
else if (item.kind === 'pull') openPullSheet(item, pullTrigger);
await openRoutedWorkSection(item);
const route = createWorkRoute.parse(window.location.hash);
if (route?.section === item.section) navigateWorkSection(item.kind, item.section);
},
onSection: (section, options) => {
const route = createWorkRoute.parse(window.location.hash);
if (options.restore) navigateWorkSection(route?.kind, section);
},
onQueue: openWorkQueueRoute,
onClose: () => {
+8 -3
View File
@@ -14,7 +14,7 @@ function createMobileIssueDetailNavigation(options) {
});
}
function navigate(name) {
function navigate(name, navigationOptions = {}) {
const target = targets[name];
if (!target) return false;
const prepare = options.beforeNavigate && options.beforeNavigate[name];
@@ -24,7 +24,7 @@ function createMobileIssueDetailNavigation(options) {
block: 'start',
behavior: prefersReducedMotion() ? 'auto' : 'smooth',
});
if (name === 'reply') target.focus();
if (name === 'reply' && navigationOptions.focus !== false) target.focus();
select(name);
return true;
}
@@ -36,6 +36,7 @@ function createMobileIssueDetailNavigation(options) {
const listener = event => {
event.preventDefault();
navigate(name);
if (options.onSectionChange) options.onSectionChange(name, { replace:false });
};
listeners.set(button, listener);
button.addEventListener('click', listener);
@@ -55,7 +56,11 @@ function createMobileIssueDetailNavigation(options) {
const visible = entries
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
if (visible) select(targetNames.get(visible.target));
if (visible) {
const name = targetNames.get(visible.target);
select(name);
if (options.onSectionChange) options.onSectionChange(name, { replace:true });
}
}, Array.from(targetNames.keys()).filter(Boolean));
},
stop() {
+8 -3
View File
@@ -14,14 +14,14 @@ function createMobileReviewDetailNavigation(options) {
});
}
function navigate(name) {
function navigate(name, navigationOptions = {}) {
const target = targets[name];
if (!target) return false;
target.scrollIntoView({
block:'start',
behavior:prefersReducedMotion() ? 'auto' : 'smooth',
});
if (name === 'feedback' && options.summaryComposer) {
if (name === 'feedback' && options.summaryComposer && navigationOptions.focus !== false) {
options.summaryComposer.focus({ preventScroll:true });
}
select(name);
@@ -39,6 +39,7 @@ function createMobileReviewDetailNavigation(options) {
const listener = event => {
event.preventDefault();
navigate(name);
if (options.onSectionChange) options.onSectionChange(name, { replace:false });
};
listeners.set(button, listener);
button.addEventListener('click', listener);
@@ -58,7 +59,11 @@ function createMobileReviewDetailNavigation(options) {
const visible = entries
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
if (visible) select(targetNames.get(visible.target));
if (visible) {
const name = targetNames.get(visible.target);
select(name);
if (options.onSectionChange) options.onSectionChange(name, { replace:true });
}
}, Array.from(targetNames.keys()).filter(Boolean), options.root || null);
},
stop() {
+8 -3
View File
@@ -14,7 +14,7 @@ function createMobileUpdateDetailNavigation(options) {
});
}
function navigate(name) {
function navigate(name, navigationOptions = {}) {
if (name === 'activity') {
if (options.jumpToNewActivity) options.jumpToNewActivity();
select(name);
@@ -27,7 +27,7 @@ function createMobileUpdateDetailNavigation(options) {
block:'start',
behavior:prefersReducedMotion() ? 'auto' : 'smooth',
});
if (name === 'reply' && options.replyComposer) {
if (name === 'reply' && options.replyComposer && navigationOptions.focus !== false) {
options.replyComposer.focus({ preventScroll:true });
}
select(name);
@@ -46,6 +46,7 @@ function createMobileUpdateDetailNavigation(options) {
const listener = event => {
event.preventDefault();
navigate(name);
if (options.onSectionChange) options.onSectionChange(name, { replace:false });
};
listeners.set(button, listener);
button.addEventListener('click', listener);
@@ -65,7 +66,11 @@ function createMobileUpdateDetailNavigation(options) {
const visible = entries
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
if (visible) select(targetNames.get(visible.target));
if (visible) {
const name = targetNames.get(visible.target);
select(name);
if (options.onSectionChange) options.onSectionChange(name, { replace:true });
}
}, Array.from(targetNames.keys()).filter(Boolean));
},
stop() {
+43 -9
View File
@@ -7,6 +7,13 @@
const repositoryPart = /^[A-Za-z0-9_.-]+$/;
const queueFilters = ['today', 'agenda', 'attention', 'filed', 'update', 'later', 'draft'];
const sections = {
issue: ['overview', 'conversation', 'reply', 'actions'],
filed: ['overview', 'conversation', 'reply', 'actions'],
pull: ['overview', 'conversation', 'reply', 'review'],
review: ['overview', 'files', 'feedback', 'history'],
update: ['activity', 'conversation', 'context', 'reply'],
};
function positiveInteger(value) {
const number = Number(value);
@@ -23,27 +30,37 @@
if (queue === 'agenda' && parts[3] === 'protect-today' && parts.length === 4) {
return { kind: 'queue', filter: 'agenda', action: 'protect-today' };
}
if (parts[2] === 'update' && parts.length === 4) {
if (parts[2] === 'update' && [4, 5].includes(parts.length)) {
const notificationId = positiveInteger(parts[3]);
return notificationId ? { kind: 'update', notification_id: notificationId } : null;
const section = parts[4];
if (!notificationId || (section && !sections.update.includes(section))) return null;
return { kind: 'update', notification_id: notificationId, ...(section ? { section } : {}) };
}
if (!['issue', 'filed', 'pull', 'review'].includes(parts[2]) || parts.length !== 6) return null;
if (!['issue', 'filed', 'pull', 'review'].includes(parts[2]) || ![6, 7].includes(parts.length)) return null;
if (!repositoryPart.test(parts[3]) || !repositoryPart.test(parts[4])) return null;
const number = positiveInteger(parts[5]);
return number ? { kind: parts[2], repository: parts[3] + '/' + parts[4], number } : null;
const section = parts[6];
if (!number || (section && !sections[parts[2]].includes(section))) return null;
return {
kind: parts[2], repository: parts[3] + '/' + parts[4], number,
...(section ? { section } : {}),
};
}
function serialize(item) {
if (item?.kind === 'update') {
const notificationId = positiveInteger(item.notification_id);
return notificationId ? '#/my-work/update/' + notificationId : '';
if (!notificationId || (item.section && !sections.update.includes(item.section))) return '';
return '#/my-work/update/' + notificationId + (item.section ? '/' + item.section : '');
}
const kind = item?.is_filed && !item?.is_assigned ? 'filed' : item?.kind;
if (!['issue', 'filed', 'pull', 'review'].includes(kind)) return '';
const repository = String(item.repository || '').split('/');
const number = positiveInteger(item.number);
if (repository.length !== 2 || !repository.every(part => repositoryPart.test(part)) || !number) return '';
return '#/my-work/' + kind + '/' + repository.join('/') + '/' + number;
if (item.section && !sections[kind].includes(item.section)) return '';
return '#/my-work/' + kind + '/' + repository.join('/') + '/' + number +
(item.section ? '/' + item.section : '');
}
function sameRoute(item, route) {
@@ -59,7 +76,7 @@
function createController({
location, history, eventTarget, onOpen, onClose, onInvalid,
onQueue = function () {},
onQueue = function () {}, onSection = function () {},
resolve, onResolving = function () {}, onError = function () {},
}) {
let items = [];
@@ -86,7 +103,7 @@
return;
}
active = fragment;
onOpen({ ...item, kind: route.kind });
onOpen({ ...item, kind: route.kind, ...(route.section ? { section: route.section } : {}) });
}).catch(error => {
if (request !== resolution || String(location.hash || '') !== fragment) return;
resolving = '';
@@ -128,10 +145,16 @@
return;
}
if (active === fragment) return;
const activeRoute = parse(active);
if (activeRoute && sameRoute(item, activeRoute)) {
active = fragment;
onSection(route.section || sections[route.kind][0], { restore: true });
return;
}
resolution += 1;
resolving = '';
active = fragment;
onOpen({ ...item, kind: route.kind });
onOpen({ ...item, kind: route.kind, ...(route.section ? { section: route.section } : {}) });
}
return {
@@ -156,6 +179,17 @@
onOpen(item);
return true;
},
section(name, options = {}) {
const route = parse(location.hash);
if (!route || route.kind === 'queue' || !sections[route.kind].includes(name)) return false;
if ((route.section || sections[route.kind][0]) === name) return true;
const fragment = serialize({ ...route, section: name });
const method = options.replace ? 'replaceState' : 'pushState';
history[method]({ workRoute: fragment }, '', fragment);
active = fragment;
onSection(name, { restore: false });
return true;
},
queue(filter) {
if (!queueFilters.includes(filter)) return false;
const name = filter + (['update', 'draft'].includes(filter) ? 's' : '');