feat: reopen recent work from mobile queues (Closes #1471)
This commit is contained in:
1 parent
46e06c1a37
commit
eb101bf5ce
8 files changed
+268
-2
No files matched your search
@@ -1551,6 +1551,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.mobile-queue-list button { display:flex; align-items:center; justify-content:space-between; gap:12px; min-height:56px; width:100%; padding:10px 14px; text-align:left; }
|
||||
.mobile-queue-list button > span:first-child { display:grid; gap:2px; }
|
||||
.mobile-queue-list small { color:var(--muted); }
|
||||
.mobile-queue-list [data-recent-work-route] { min-height:56px; overflow-wrap:anywhere; }
|
||||
.mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; }
|
||||
.mobile-queue-list [data-mobile-queue="agenda"][data-deadlines="true"] { border-color:#f59e0b; background:#30240f; box-shadow:inset 3px 0 #f59e0b; }
|
||||
.mobile-queue-list [data-recommended="true"] { border-color:#60a5fa; box-shadow:0 0 0 2px #60a5fa; }
|
||||
|
||||
+16
-1
@@ -115,6 +115,7 @@
|
||||
let offlineWorkMode = false;
|
||||
let renderMobileQueuePresentation = () => {};
|
||||
let mobileQueuePriority = null;
|
||||
let mobileRecentWork = { record:() => false, render:() => 0 };
|
||||
const followingQueue = attachFollowing(item => {
|
||||
searchPreviewReturnKind = 'following';
|
||||
return searchPreview.open(item);
|
||||
@@ -313,7 +314,7 @@
|
||||
find: () => qs('#find-work').click(),
|
||||
new: () => qs('#new-issue').click(),
|
||||
search: () => qs('#open-palette').click(),
|
||||
queues: () => refreshTomorrowQueueSummary(),
|
||||
queues: () => { refreshTomorrowQueueSummary(); mobileRecentWork.render(); },
|
||||
},
|
||||
observe(callback, overlays) {
|
||||
const observer = new MutationObserver(callback);
|
||||
@@ -431,6 +432,19 @@
|
||||
let planningOwnerLogin = '';
|
||||
let planningOwnerAccountKey = '';
|
||||
let activeFlushLogin = '';
|
||||
mobileRecentWork = createMobileRecentWork({
|
||||
storage:localStorage,
|
||||
getLogin:() => confirmedOwnerLogin,
|
||||
document,
|
||||
section:qs('#mobile-recent-work'),
|
||||
list:qs('#mobile-recent-work-list'),
|
||||
openRoute:fragment => {
|
||||
const sheet = qs('#mobile-queue-sheet');
|
||||
if (sheet.open) sheet.close();
|
||||
if (window.location.hash !== fragment) window.history.pushState({ workRoute:fragment }, '', fragment);
|
||||
workRoute.sync();
|
||||
},
|
||||
});
|
||||
mobileQueuePriority = createMobileQueuePriority({
|
||||
storage: localStorage,
|
||||
getLogin: () => confirmedOwnerLogin,
|
||||
@@ -2061,6 +2075,7 @@
|
||||
qs('#my-work-action-status').textContent = '';
|
||||
closeOpenWorkSheets();
|
||||
await openRoutedWorkSection(item);
|
||||
mobileRecentWork.record(item);
|
||||
const route = createWorkRoute.parse(window.location.hash);
|
||||
if (route?.section === item.section) navigateWorkSection(item.kind, item.section);
|
||||
},
|
||||
|
||||
@@ -2183,6 +2183,10 @@
|
||||
<p class="small muted" id="mobile-queue-next-heading">Start / Continue</p>
|
||||
<button id="mobile-queue-next-action" type="button">Find Work</button>
|
||||
</section>
|
||||
<section class="mobile-queue-group" id="mobile-recent-work" aria-labelledby="mobile-recent-work-heading" hidden>
|
||||
<h3 id="mobile-recent-work-heading">Recent work</h3>
|
||||
<div class="mobile-queue-list" id="mobile-recent-work-list"></div>
|
||||
</section>
|
||||
<details class="mobile-queue-priority" id="mobile-queue-priority">
|
||||
<summary>Customize routine order</summary>
|
||||
<p class="small muted">Delivery and Human Gates always stay first. Move the routine queues to match how you work.</p>
|
||||
@@ -2416,6 +2420,7 @@
|
||||
<script src="static/mobile-pull-refresh.js"></script>
|
||||
<script src="static/mobile-first-task.js"></script>
|
||||
<script src="static/mobile-work-entry.js"></script>
|
||||
<script src="static/mobile-recent-work.js"></script>
|
||||
<script src="static/mobile-queue-priority.js"></script>
|
||||
<script src="static/mobile-queue-launcher.js"></script>
|
||||
<script src="static/mobile-delivery-recovery.js"></script>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||
else root.createMobileRecentWork = factory;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function createMobileRecentWork(options) {
|
||||
'use strict';
|
||||
|
||||
const storage = options.storage;
|
||||
const getLogin = options.getLogin;
|
||||
const limit = Math.max(1, Number(options.limit) || 5);
|
||||
const prefix = 'stackchain.mobile-recent-work.v1.';
|
||||
const repositoryPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
||||
const kinds = new Set(['issue', 'filed', 'pull', 'review', 'update']);
|
||||
|
||||
function login() {
|
||||
return String(getLogin?.() || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function key() {
|
||||
const owner = login();
|
||||
return owner ? prefix + owner : '';
|
||||
}
|
||||
|
||||
function normalize(item) {
|
||||
const kind = String(item?.kind || '');
|
||||
const number = Number(item?.number ?? item?.notification_id);
|
||||
if (!kinds.has(kind) || !Number.isSafeInteger(number) || number < 1) return null;
|
||||
let route = '';
|
||||
let repository = '';
|
||||
if (kind === 'update') {
|
||||
route = '#/my-work/update/' + number;
|
||||
} else {
|
||||
repository = String(item?.repository || '');
|
||||
if (!repositoryPattern.test(repository)) return null;
|
||||
route = '#/my-work/' + kind + '/' + repository + '/' + number;
|
||||
}
|
||||
const title = String(item?.title || item?.subject?.title || '').trim().slice(0, 180);
|
||||
if (!title) return null;
|
||||
return {
|
||||
kind,
|
||||
...(repository ? { repository } : {}),
|
||||
number,
|
||||
title,
|
||||
route,
|
||||
};
|
||||
}
|
||||
|
||||
function items() {
|
||||
const storageKey = key();
|
||||
if (!storageKey) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(storage.getItem(storageKey) || '[]');
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.map(normalize).filter(Boolean).slice(0, limit);
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function record(item) {
|
||||
const storageKey = key();
|
||||
const normalized = normalize(item);
|
||||
if (!storageKey || !normalized) return false;
|
||||
const next = [normalized, ...items().filter(existing => existing.route !== normalized.route)].slice(0, limit);
|
||||
try {
|
||||
storage.setItem(storageKey, JSON.stringify(next));
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const recent = items();
|
||||
const list = options.list;
|
||||
const section = options.section;
|
||||
if (!list || !section || !options.document) return recent.length;
|
||||
const rows = recent.map(item => {
|
||||
const detail = item.kind === 'update'
|
||||
? 'Update · #' + item.number
|
||||
: item.kind.charAt(0).toUpperCase() + item.kind.slice(1) + ' · ' + item.repository + ' #' + item.number;
|
||||
const button = options.document.createElement('button');
|
||||
const copy = options.document.createElement('span');
|
||||
const primary = options.document.createElement('strong');
|
||||
const secondary = options.document.createElement('small');
|
||||
primary.textContent = item.title;
|
||||
secondary.textContent = detail;
|
||||
copy.appendChild(primary);
|
||||
copy.appendChild(secondary);
|
||||
button.appendChild(copy);
|
||||
button.setAttribute('type', 'button');
|
||||
button.setAttribute('data-recent-work-route', item.route);
|
||||
button.setAttribute('aria-label', 'Open ' + item.title + ', ' + detail.toLowerCase().replace(' · ', ' '));
|
||||
button.addEventListener('click', () => options.openRoute?.(item.route));
|
||||
return button;
|
||||
});
|
||||
list.replaceChildren(...rows);
|
||||
section.hidden = rows.length === 0;
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
return { items, record, render };
|
||||
});
|
||||
@@ -270,6 +270,7 @@ const SHELL = [
|
||||
BASE + 'static/mobile-task-dock.js',
|
||||
BASE + 'static/mobile-first-task.js',
|
||||
BASE + 'static/mobile-work-entry.js',
|
||||
BASE + 'static/mobile-recent-work.js',
|
||||
BASE + 'static/mobile-queue-priority.js',
|
||||
BASE + 'static/mobile-queue-launcher.js',
|
||||
BASE + 'static/mobile-delivery-recovery.js',
|
||||
|
||||
Reference in new issue
Block a user