stackchain-dashboard/frontend/mobile-recent-work.js
timmy eb101bf5ce
All checks were successful
CI / lint (pull_request) Successful in 3m39s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 7m53s
CI / release-candidate (pull_request) Has been skipped
feat: reopen recent work from mobile queues (Closes #1471)
2026-08-27 15:40:41 +00:00

103 lines
3.5 KiB
JavaScript

(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 };
});