Compare commits
No commits in common. "1822004f3dcffc919d1721eafa1c8095afb14d7c" and "024087d05179a0376726c5ff0442bbc66fc71ccb" have entirely different histories.
1822004f3d
...
024087d051
|
|
@ -1,7 +1,6 @@
|
||||||
(async function(){
|
(async function(){
|
||||||
const workspaceLifecycle = await (window.stackchainWorkspaceLifecycle || loadWorkspace({ document, window }));
|
const workspaceLifecycle = await (window.stackchainWorkspaceLifecycle || loadWorkspace({ document, window }));
|
||||||
await workspaceLifecycle.optionalReady;
|
await workspaceLifecycle.optionalReady;
|
||||||
const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();
|
|
||||||
window.stackchainProgressiveMyWork?.stop();
|
window.stackchainProgressiveMyWork?.stop();
|
||||||
const qs = (s, el=document) => el.querySelector(s);
|
const qs = (s, el=document) => el.querySelector(s);
|
||||||
const announceWork = message => qs('#my-work-action-status').textContent = message;
|
const announceWork = message => qs('#my-work-action-status').textContent = message;
|
||||||
|
|
@ -323,17 +322,15 @@
|
||||||
let offlineWorkMode = false;
|
let offlineWorkMode = false;
|
||||||
const WORK_FILTER_KEY = 'stackchain.my-work-filter.v1';
|
const WORK_FILTER_KEY = 'stackchain.my-work-filter.v1';
|
||||||
const WORK_MILESTONE_KEY = 'stackchain.my-work-milestone.v1';
|
const WORK_MILESTONE_KEY = 'stackchain.my-work-milestone.v1';
|
||||||
const WORK_FILTERS = ['all', 'today', 'agenda', 'attention', 'filed', 'authored', 'issue', 'pull', 'review', 'update', 'later', 'draft'];
|
let selectedWorkFilter = 'all';
|
||||||
const hasProgressiveWorkFilter = WORK_FILTERS.includes(progressiveWorkHandoff?.selectedFilter);
|
|
||||||
let selectedWorkFilter = hasProgressiveWorkFilter ? progressiveWorkHandoff.selectedFilter : 'all';
|
|
||||||
let selectedFiledView = 'needs-review';
|
let selectedFiledView = 'needs-review';
|
||||||
let selectedWorkMilestone = 'all';
|
let selectedWorkMilestone = 'all';
|
||||||
let queueFindQuery = '';
|
let queueFindQuery = '';
|
||||||
let savedWorkFilter = null;
|
let savedWorkFilter = null;
|
||||||
let launchFilterResolved = hasProgressiveWorkFilter;
|
let launchFilterResolved = false;
|
||||||
try {
|
try {
|
||||||
const savedFilter = sessionStorage.getItem(WORK_FILTER_KEY);
|
const savedFilter = sessionStorage.getItem(WORK_FILTER_KEY);
|
||||||
if (!hasProgressiveWorkFilter && ['all', 'today', 'agenda', 'attention', 'filed', 'authored', 'issue', 'pull', 'review', 'update', 'later', 'draft'].includes(savedFilter)) {
|
if (['all', 'today', 'agenda', 'attention', 'filed', 'authored', 'issue', 'pull', 'review', 'update', 'later', 'draft'].includes(savedFilter)) {
|
||||||
selectedWorkFilter = savedFilter;
|
selectedWorkFilter = savedFilter;
|
||||||
savedWorkFilter = savedFilter;
|
savedWorkFilter = savedFilter;
|
||||||
launchFilterResolved = true;
|
launchFilterResolved = true;
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,6 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
let items = [];
|
let items = [];
|
||||||
let active = 'all';
|
let active = 'all';
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
let selectedByUser = false;
|
|
||||||
const deferredQueues = {
|
|
||||||
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
|
||||||
};
|
|
||||||
|
|
||||||
const escapeHtml = value => String(value || '').replace(/[&<>"']/g, character => ({
|
const escapeHtml = value => String(value || '').replace(/[&<>"']/g, character => ({
|
||||||
'&':'&', '<':'<', '>':'>', '"':'"', "'":''',
|
'&':'&', '<':'<', '>':'>', '"':'"', "'":''',
|
||||||
|
|
@ -20,26 +16,15 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
|
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
|
||||||
} catch (_error) { return ''; }
|
} catch (_error) { return ''; }
|
||||||
};
|
};
|
||||||
const matches = (item, filter) =>
|
const visibleItems = () => active === 'all' ? items : items.filter(item =>
|
||||||
filter === 'review' ? item.is_review :
|
active === 'review' ? item.is_review :
|
||||||
filter === 'update' ? item.has_update :
|
active === 'update' ? item.has_update :
|
||||||
filter === 'attention' ? item.needs_attention :
|
active === 'attention' ? item.needs_attention : item.kind === active
|
||||||
filter === 'filed' ? item.is_filed :
|
);
|
||||||
filter === 'authored' ? item.is_authored :
|
|
||||||
filter === 'pull' ? item.kind === 'pull' && !item.is_review : item.kind === filter;
|
|
||||||
const visibleItems = () => active === 'all' ? items : items.filter(item => matches(item, active));
|
|
||||||
const updateCounts = () => {
|
|
||||||
['all','attention','filed','authored','issue','pull','review','update'].forEach(filter => {
|
|
||||||
const count = filter === 'all' ? items.length : items.filter(item => matches(item, filter)).length;
|
|
||||||
const element = document.querySelector('[data-work-count="' + filter + '"]');
|
|
||||||
if (element) element.textContent = String(count);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
if (stopped || !list) return;
|
if (stopped || !list) return;
|
||||||
const visible = visibleItems();
|
const visible = visibleItems();
|
||||||
list.innerHTML = deferredQueues[active] ?
|
list.innerHTML = visible.length ? visible.map(item => {
|
||||||
'<div class="muted">' + deferredQueues[active] + ' is still loading…</div>' : visible.length ? visible.map(item => {
|
|
||||||
const href = safeUrl(item.url);
|
const href = safeUrl(item.url);
|
||||||
const title = escapeHtml(item.title || item.key || 'Untitled work');
|
const title = escapeHtml(item.title || item.key || 'Untitled work');
|
||||||
const context = escapeHtml(item.key || '');
|
const context = escapeHtml(item.key || '');
|
||||||
|
|
@ -52,15 +37,12 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
filters.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.workFilter === active)));
|
filters.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.workFilter === active)));
|
||||||
};
|
};
|
||||||
filters.forEach(button => {
|
filters.forEach(button => {
|
||||||
const listener = () => { active = button.dataset.workFilter || 'all'; selectedByUser = true; render(); };
|
const listener = () => { active = button.dataset.workFilter || 'all'; render(); };
|
||||||
button.addEventListener('click', listener);
|
button.addEventListener('click', listener);
|
||||||
listeners.push([button, listener]);
|
listeners.push([button, listener]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handoff() {
|
|
||||||
return { selectedFilter:selectedByUser ? active : null };
|
|
||||||
},
|
|
||||||
async start() {
|
async start() {
|
||||||
if (status) status.textContent = 'Loading assigned work…';
|
if (status) status.textContent = 'Loading assigned work…';
|
||||||
try {
|
try {
|
||||||
|
|
@ -68,7 +50,6 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
if (stopped) return false;
|
if (stopped) return false;
|
||||||
const context = snapshot?.context || snapshot || {};
|
const context = snapshot?.context || snapshot || {};
|
||||||
items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] });
|
items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] });
|
||||||
updateCounts();
|
|
||||||
render();
|
render();
|
||||||
const assigned = items.filter(item => item.is_assigned).length;
|
const assigned = items.filter(item => item.is_assigned).length;
|
||||||
if (status) status.textContent = assigned + ' assigned work item' + (assigned === 1 ? '' : 's') + ' ready.';
|
if (status) status.textContent = assigned + ' assigned work item' + (assigned === 1 ? '' : 's') + ' ready.';
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ from pathlib import Path
|
||||||
|
|
||||||
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
MODULE = Path(__file__).parents[1] / "frontend" / "progressive-my-work.js"
|
||||||
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
||||||
DASHBOARD = Path(__file__).parents[1] / "frontend" / "dashboard.js"
|
|
||||||
|
|
||||||
|
|
||||||
def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace():
|
def test_progressive_my_work_renders_and_filters_assigned_work_before_full_workspace():
|
||||||
|
|
@ -37,100 +36,3 @@ const flow=createProgressiveMyWork({{document,fetchSnapshot:async()=>({{
|
||||||
assert "Fix mobile queue" in state["rendered"]
|
assert "Fix mobile queue" in state["rendered"]
|
||||||
assert 'href="https://forge.example/issues/7"' in state["rendered"]
|
assert 'href="https://forge.example/issues/7"' in state["rendered"]
|
||||||
assert state["pressed"] == ["false", "true"]
|
assert state["pressed"] == ["false", "true"]
|
||||||
|
|
||||||
|
|
||||||
def test_progressive_my_work_keeps_supported_queues_truthful_while_planning_loads():
|
|
||||||
harness = f"""
|
|
||||||
const fs=require('fs'); const vm=require('vm');
|
|
||||||
const names=['all','today','agenda','attention','filed','authored','issue','pull','review','update','later','draft'];
|
|
||||||
const counts=Object.fromEntries(names.map(name=>[name,{{textContent:'0'}}]));
|
|
||||||
const buttons=names.map(name=>({{
|
|
||||||
dataset:{{workFilter:name}},attrs:{{}},
|
|
||||||
addEventListener(_name,cb){{this.cb=cb;}},
|
|
||||||
removeEventListener(){{}},
|
|
||||||
setAttribute(key,value){{this.attrs[key]=value;}},
|
|
||||||
}}));
|
|
||||||
const list={{innerHTML:''}}; const status={{textContent:''}};
|
|
||||||
const document={{
|
|
||||||
querySelector(selector){{
|
|
||||||
if(selector==='#my-work-list')return list;
|
|
||||||
if(selector==='#my-work-status')return status;
|
|
||||||
const match=selector.match(/^\\[data-work-count="(.+)"\\]$/);
|
|
||||||
return match?counts[match[1]]:null;
|
|
||||||
}},
|
|
||||||
querySelectorAll:()=>buttons,
|
|
||||||
}};
|
|
||||||
const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context);
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context);
|
|
||||||
context.buildMyWork=context.module.exports; context.module={{exports:{{}}}};
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context);
|
|
||||||
const flow=context.module.exports({{document,fetchSnapshot:async()=>({{
|
|
||||||
user:{{login:'timmy'}},
|
|
||||||
issues:[{{number:7,title:'Filed from phone',repository:'stackchain/dashboard',assignees:[],work_reasons:['created_by_me'],url:'https://forge.example/issues/7'}}],
|
|
||||||
pull_requests:[{{number:8,title:'Authored release',repository:'stackchain/dashboard',assignees:[],work_reasons:['authored_by_me'],url:'https://forge.example/pulls/8'}}],
|
|
||||||
}})}});
|
|
||||||
(async()=>{{
|
|
||||||
await flow.start();
|
|
||||||
buttons[names.indexOf('filed')].cb(); const filed=list.innerHTML;
|
|
||||||
buttons[names.indexOf('authored')].cb(); const authored=list.innerHTML;
|
|
||||||
buttons[names.indexOf('today')].cb(); const today=list.innerHTML;
|
|
||||||
console.log(JSON.stringify({{filed,authored,today,counts:Object.fromEntries(names.map(name=>[name,counts[name].textContent]))}}));
|
|
||||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
||||||
state = json.loads(result.stdout)
|
|
||||||
assert "Filed from phone" in state["filed"]
|
|
||||||
assert "Authored release" in state["authored"]
|
|
||||||
assert "Today is still loading" in state["today"]
|
|
||||||
assert "No work" not in state["today"]
|
|
||||||
assert state["counts"] == {
|
|
||||||
"all": "2",
|
|
||||||
"today": "0",
|
|
||||||
"agenda": "0",
|
|
||||||
"attention": "0",
|
|
||||||
"filed": "1",
|
|
||||||
"authored": "1",
|
|
||||||
"issue": "1",
|
|
||||||
"pull": "1",
|
|
||||||
"review": "0",
|
|
||||||
"update": "0",
|
|
||||||
"later": "0",
|
|
||||||
"draft": "0",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_progressive_queue_selection_is_handed_to_the_hydrated_workspace():
|
|
||||||
harness = f"""
|
|
||||||
const fs=require('fs'); const vm=require('vm');
|
|
||||||
const buttons=['all','filed'].map(name=>({{
|
|
||||||
dataset:{{workFilter:name}},attrs:{{}},removed:false,
|
|
||||||
addEventListener(_name,cb){{this.cb=cb;}},
|
|
||||||
removeEventListener(_name,cb){{if(cb===this.cb)this.removed=true;}},
|
|
||||||
setAttribute(key,value){{this.attrs[key]=value;}},
|
|
||||||
}}));
|
|
||||||
const document={{
|
|
||||||
querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null,
|
|
||||||
querySelectorAll:()=>buttons,
|
|
||||||
}};
|
|
||||||
const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context);
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context);
|
|
||||||
context.buildMyWork=context.module.exports; context.module={{exports:{{}}}};
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context);
|
|
||||||
const flow=context.module.exports({{document,fetchSnapshot:async()=>({{user:{{login:'timmy'}},issues:[],pull_requests:[]}})}});
|
|
||||||
(async()=>{{
|
|
||||||
await flow.start(); buttons[1].cb();
|
|
||||||
const before=flow.handoff(); flow.stop(); const after=flow.handoff();
|
|
||||||
console.log(JSON.stringify({{before,after,removed:buttons.map(button=>button.removed)}}));
|
|
||||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
||||||
state = json.loads(result.stdout)
|
|
||||||
assert state == {
|
|
||||||
"before": {"selectedFilter": "filed"},
|
|
||||||
"after": {"selectedFilter": "filed"},
|
|
||||||
"removed": [True, True],
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboard = DASHBOARD.read_text()
|
|
||||||
assert "const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();" in dashboard
|
|
||||||
assert "progressiveWorkHandoff?.selectedFilter" in dashboard
|
|
||||||
Loading…
Reference in New Issue
Block a user