feat: preserve Search work details offline (Closes #1220)
This commit is contained in:
1 parent
ba50784022
commit
2b874d8427
6 files changed
+151
-20
No files matched your search
@@ -133,7 +133,10 @@
|
||||
const existing = week.placement(id);
|
||||
if (!week.place(id, choice.date, Number(choice.estimate), {move:Boolean(existing && existing.date !== choice.date)})) {
|
||||
failed.push({id:originalId, reason:'assigned-not-planned'});
|
||||
} else planned.push(id);
|
||||
} else {
|
||||
week.rememberPendingItem?.(id, confirmed);
|
||||
planned.push(id);
|
||||
}
|
||||
} catch (error) {
|
||||
failed.push({id:originalId, reason:error?.message || 'assignment failed'});
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ function createSearchWeekPlan({week,claim,accept=item=>item,identity,maxItems=5}
|
||||
const id=identity(item);
|
||||
if(!week.place(id,options.date,estimate,{move:Boolean(options.move)}))
|
||||
return {status:assigned?'assigned-not-planned':'not-planned',item};
|
||||
week.rememberPendingItem?.(id,item);
|
||||
try {
|
||||
await week.flush();
|
||||
return {status:'planned',date:options.date,estimate,item};
|
||||
|
||||
+47
-19
@@ -5,12 +5,23 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
let offlineSnapshot=false;
|
||||
let refreshedAt=null;
|
||||
let confirmedItems={};
|
||||
let pendingItems={};
|
||||
const storagePrefix='stackchain.week-sync.v1.';
|
||||
const confirmedPrefix='stackchain.week-confirmed.v1.';
|
||||
const cloneDay=day=>({
|
||||
plan_date:day.plan_date,ids:[...(day.ids||[])],capacity_minutes:day.capacity_minutes??null,
|
||||
estimates:{...(day.estimates||{})},
|
||||
});
|
||||
const sanitizeItem=value=>value?{kind:value.kind==='pull'?'pull':'issue',
|
||||
title:String(value.title||'').slice(0,500),repository:String(value.repository||'').slice(0,255),
|
||||
number:Number.isInteger(value.number)?value.number:null}:null;
|
||||
function sanitizeItems(days,sources={}) {
|
||||
const sanitized={};
|
||||
new Set((days||[]).flatMap(day=>day.ids||[])).forEach(id=>{
|
||||
const value=sanitizeItem(sources[id]);if(value)sanitized[id]=value;
|
||||
});
|
||||
return sanitized;
|
||||
}
|
||||
const state=()=>({revision:week.revision,timezone:week.timezone,days:week.days.map(cloneDay),
|
||||
offline_snapshot:offlineSnapshot,...(refreshedAt?{refreshed_at:refreshedAt}:{}),
|
||||
...(week.sync_pending?{base_revision:week.base_revision,sync_pending:true}:{})});
|
||||
@@ -41,14 +52,7 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
catch(_error){return false;}
|
||||
}
|
||||
function rememberItems(items={}) {
|
||||
const ids=new Set(week.days.flatMap(day=>day.ids||[])),sanitized={};
|
||||
ids.forEach(id=>{
|
||||
const value=items[id];
|
||||
if(!value)return;
|
||||
sanitized[id]={kind:value.kind==='pull'?'pull':'issue',title:String(value.title||'').slice(0,500),
|
||||
repository:String(value.repository||'').slice(0,255),number:Number.isInteger(value.number)?value.number:null};
|
||||
});
|
||||
confirmedItems=sanitized;
|
||||
confirmedItems=sanitizeItems(week.days,{...confirmedItems,...items});
|
||||
return persistConfirmed();
|
||||
}
|
||||
function pending() {
|
||||
@@ -59,6 +63,7 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
return Number.isInteger(value?.base_revision)&&Array.isArray(value?.days)?{
|
||||
revision:value.base_revision,base_revision:value.base_revision,timezone:value.timezone||null,
|
||||
days:value.days.map(cloneDay).sort((left,right)=>left.plan_date.localeCompare(right.plan_date)),sync_pending:true,
|
||||
items:sanitizeItems(value.days,value.items||{}),
|
||||
...(Array.isArray(value.base_days)?{base_days:value.base_days.map(cloneDay)
|
||||
.sort((left,right)=>left.plan_date.localeCompare(right.plan_date))}:{}),
|
||||
...(value.resolutions&&typeof value.resolutions==='object'?{resolutions:{...value.resolutions}}:{}),
|
||||
@@ -70,12 +75,13 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
week={revision:value.revision,timezone:value.timezone||null,days:value.days.map(cloneDay)
|
||||
.sort((left,right)=>left.plan_date.localeCompare(right.plan_date))};
|
||||
offlineSnapshot=false;
|
||||
pendingItems={};
|
||||
lastConflict=null;
|
||||
return state();
|
||||
}
|
||||
function adoptConfirmed(value) {
|
||||
function adoptConfirmed(value,items=confirmedItems) {
|
||||
const adopted=adopt(value);
|
||||
if(adopted)persistConfirmed();
|
||||
if(adopted){confirmedItems=sanitizeItems(week.days,items);persistConfirmed();}
|
||||
return adopted;
|
||||
}
|
||||
function addDays(value, amount) {
|
||||
@@ -101,16 +107,21 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
}
|
||||
async function load() {
|
||||
const queued=pending();
|
||||
if(queued){week=queued;offlineSnapshot=false;return state();}
|
||||
if(queued){
|
||||
week=queued;pendingItems=queued.items||{};
|
||||
confirmedItems=sanitizeItems(week.days,readConfirmed()?.items||{});
|
||||
offlineSnapshot=false;return state();
|
||||
}
|
||||
try {
|
||||
adopt(await fetchJson('api/v1/week'));
|
||||
const cached=readConfirmed();confirmedItems=cached?.items||{};persistConfirmed();
|
||||
const cached=readConfirmed();confirmedItems=sanitizeItems(week.days,cached?.items||{});persistConfirmed();
|
||||
return state();
|
||||
} catch(error) {
|
||||
if(error?.status===401||error?.status===403)throw error;
|
||||
const cached=readConfirmed();
|
||||
if(!cached)throw error;
|
||||
adopt(cached.week);offlineSnapshot=true;refreshedAt=cached.refreshed_at;confirmedItems=cached.items||{};
|
||||
adopt(cached.week);offlineSnapshot=true;refreshedAt=cached.refreshed_at;
|
||||
confirmedItems=sanitizeItems(week.days,cached.items||{});
|
||||
return state();
|
||||
}
|
||||
}
|
||||
@@ -121,12 +132,27 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
const queued={base_revision:Number.isInteger(week.base_revision)?week.base_revision:week.revision,
|
||||
timezone:timeZone(),days:days.map(cloneDay).sort((left,right)=>left.plan_date.localeCompare(right.plan_date)),
|
||||
base_days:(week.base_days||week.days).map(cloneDay)};
|
||||
queued.items=sanitizeItems(queued.days,{...confirmedItems,...pendingItems,...(pending()?.items||{})});
|
||||
try { storage.setItem(key,JSON.stringify(queued)); }
|
||||
catch(_error) { return false; }
|
||||
lastConflict=null;
|
||||
week={revision:queued.base_revision,...queued,sync_pending:true};
|
||||
pendingItems=queued.items;
|
||||
return state();
|
||||
}
|
||||
function rememberPendingItem(id,value) {
|
||||
const key=storageKey(),item=sanitizeItem(value);
|
||||
if(!key||!storage||!item)return false;
|
||||
try {
|
||||
const raw=JSON.parse(storage.getItem(key)||'null');
|
||||
if(!Number.isInteger(raw?.base_revision)||!Array.isArray(raw?.days)||
|
||||
!raw.days.some(day=>(day.ids||[]).includes(id)))return false;
|
||||
raw.items=sanitizeItems(raw.days,{...(raw.items||{}),[id]:item});
|
||||
storage.setItem(key,JSON.stringify(raw));pendingItems=raw.items;
|
||||
if(week.sync_pending)week={...week,items:raw.items};
|
||||
return true;
|
||||
} catch(_error){return false;}
|
||||
}
|
||||
function stageDay(planDate,value) {
|
||||
return stageDays(week.days.filter(item=>item.plan_date!==planDate).concat([{
|
||||
plan_date:planDate,ids:[...(value.ids||[])],capacity_minutes:value.capacity_minutes??null,
|
||||
@@ -244,7 +270,7 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
body:JSON.stringify({base_revision:remote.revision,timezone:local.timezone,days:merged.days})});
|
||||
const current=pending();
|
||||
if(current&&sameBody(current,deliveryBody(local)))storage.removeItem(key);
|
||||
if(!pending())adoptConfirmed(saved);
|
||||
if(!pending())adoptConfirmed(saved,{...confirmedItems,...(local.items||{})});
|
||||
return saved;
|
||||
}
|
||||
lastConflict={key,local,remote:{revision:remote.revision,timezone:remote.timezone||null,
|
||||
@@ -257,7 +283,7 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
}
|
||||
const current=pending();
|
||||
if(current&&sameBody(current,body)){
|
||||
storage.removeItem(key);adoptConfirmed(saved);return saved;
|
||||
storage.removeItem(key);adoptConfirmed(saved,{...confirmedItems,...(current.items||{})});return saved;
|
||||
}
|
||||
if(current){
|
||||
try {
|
||||
@@ -265,10 +291,11 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
raw.base_revision=saved.revision;raw.base_days=saved.days.map(cloneDay);
|
||||
storage.setItem(key,JSON.stringify(raw));
|
||||
week={revision:raw.base_revision,...raw,sync_pending:true};
|
||||
pendingItems=raw.items||{};
|
||||
} catch(_error){return saved;}
|
||||
return deliver();
|
||||
}
|
||||
adoptConfirmed(saved);return saved;
|
||||
adoptConfirmed(saved,{...confirmedItems,...(queued.items||{})});return saved;
|
||||
};
|
||||
flushing=deliver().finally(()=>{flushing=null;});
|
||||
return flushing;
|
||||
@@ -329,7 +356,7 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
const saved=await fetchJson('api/v1/week',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const current=pending();
|
||||
if(current&&JSON.stringify(deliveryBody(current))===JSON.stringify(deliveryBody(local))) storage.removeItem(storageKey());
|
||||
if(!pending()) adoptConfirmed(saved);
|
||||
if(!pending()) adoptConfirmed(saved,{...confirmedItems,...(local.items||{})});
|
||||
lastConflict=null;
|
||||
return saved;
|
||||
} catch(error) {
|
||||
@@ -350,7 +377,7 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
const saved=await fetchJson('api/v1/week',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const current=pending();
|
||||
if(current&&JSON.stringify(deliveryBody(current))===JSON.stringify(deliveryBody(local))) storage.removeItem(storageKey());
|
||||
if(!pending()) adoptConfirmed(saved);
|
||||
if(!pending()) adoptConfirmed(saved,{...confirmedItems,...(local.items||{})});
|
||||
lastConflict=null;
|
||||
return saved;
|
||||
} catch(error) {
|
||||
@@ -398,7 +425,8 @@ function createWeekPlan({fetchJson,localDate,timeZone,storage,getLogin,now=()=>D
|
||||
return label+(pending()?' · sync pending':'');
|
||||
}
|
||||
return {adopt,state,dates,day,pass,load,saveDay,stageDay,stageCapacities,review,move,placement,place,pending,flush,conflict,chooseDay,saveMerged,
|
||||
keepLocal,useRemote,promote,reconcile:reconcilePromotion,summary,rememberItems,item:id=>confirmedItems[id]||null,
|
||||
keepLocal,useRemote,promote,reconcile:reconcilePromotion,summary,rememberItems,rememberPendingItem,
|
||||
item:id=>pendingItems[id]||confirmedItems[id]||null,
|
||||
offline:()=>offlineSnapshot};
|
||||
}
|
||||
function createWeekPlanWorkflow({controller,qs,getLogin,getItem=()=>null,openPlanner,setReviewMode=()=>{},escapeHtml,escapeAttribute,
|
||||
|
||||
Reference in new issue
Block a user