169 lines
12 KiB
JavaScript
169 lines
12 KiB
JavaScript
|
|
/* Creation uses the existing command parser and case command endpoints. */
|
||
|
|
(function(root,factory){
|
||
|
|
const api=factory();
|
||
|
|
if(typeof module==='object'&&module.exports)module.exports=api;
|
||
|
|
if(root && root.document) api.init(root);
|
||
|
|
})(typeof window!=='undefined'?window:null,function(){
|
||
|
|
'use strict';
|
||
|
|
class CommandRun {
|
||
|
|
constructor(request,uuid){this.request=request;this.uuid=uuid;this.pending=null;}
|
||
|
|
attach(caseId,commands){
|
||
|
|
if(!commands.length)return;
|
||
|
|
this.pending={caseId,body:{request_id:this.uuid(),comment:'',internal:true,billing_mode:'automatic',commands}};
|
||
|
|
}
|
||
|
|
async execute(){
|
||
|
|
if(!this.pending)return;
|
||
|
|
const {caseId,body}=this.pending;
|
||
|
|
await this.request(`/api/v1/sag/${caseId}/command-execute`,body);
|
||
|
|
this.pending=null;
|
||
|
|
return caseId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function init(root){
|
||
|
|
const doc=root.document, input=doc.getElementById('beskrivelse');
|
||
|
|
if(!input || !root.CaseCommands || !root.CaseCommandSuggestions)return;
|
||
|
|
const config=JSON.parse(doc.getElementById('caseCreateCommandData').textContent);
|
||
|
|
const values={users:(config.users||[]).map(user=>({...user,full_name:user.full_name,username:user.username||user.display_name})),groups:config.groups||[]};
|
||
|
|
const panel=doc.createElement('section');panel.className='cc-command-tools';
|
||
|
|
const toolbar=doc.createElement('div');toolbar.className='cc-command-toolbar';
|
||
|
|
const hint=doc.createElement('small');hint.textContent='Skriv / på en ny linje for handlinger.';
|
||
|
|
const help=doc.createElement('details'),summary=doc.createElement('summary');summary.textContent='/ Hjælp';
|
||
|
|
const helpList=doc.createElement('div');helpList.className='cc-command-help';help.append(summary,helpList);
|
||
|
|
const suggestions=doc.createElement('div');suggestions.className='case-command-suggestions';
|
||
|
|
const preview=doc.createElement('div');preview.className='cc-command-preview';preview.setAttribute('aria-live','polite');
|
||
|
|
const recovery=doc.createElement('div');recovery.className='cc-command-recovery';recovery.hidden=true;
|
||
|
|
toolbar.append(hint,help);panel.append(toolbar,suggestions,preview,recovery);input.after(panel);
|
||
|
|
let active=-1, options=[],storageKey=null,prepared=null;
|
||
|
|
const request=async(url,body)=>{
|
||
|
|
const response=await root.fetch(url,{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||
|
|
const data=await response.json();
|
||
|
|
if(!response.ok){
|
||
|
|
const error=new Error(typeof data.detail==='string'?data.detail:'Kommandoerne kunne ikke udføres.');
|
||
|
|
error.definite=[400,403,404,409,422].includes(response.status);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
return data;
|
||
|
|
};
|
||
|
|
const run=new CommandRun(request,()=>root.crypto.randomUUID());
|
||
|
|
const ready=(async()=>{
|
||
|
|
try {
|
||
|
|
const response=await root.fetch('/api/v1/auth/me',{credentials:'include'});
|
||
|
|
if(!response.ok)return;
|
||
|
|
const actor=await response.json();
|
||
|
|
if(!actor.id && !actor.user_id)return;
|
||
|
|
storageKey=`bmc:create-command-run:${actor.id||actor.user_id}`;
|
||
|
|
const stored=root.sessionStorage.getItem(storageKey);
|
||
|
|
if(stored){run.pending=JSON.parse(stored);if(run.pending.draftText)input.value=run.pending.draftText;}
|
||
|
|
} catch(_) { /* The current page still supports an in-memory retry. */ }
|
||
|
|
showRecovery();
|
||
|
|
})();
|
||
|
|
function persist(){try{if(storageKey){if(run.pending)root.sessionStorage.setItem(storageKey,JSON.stringify(run.pending));else root.sessionStorage.removeItem(storageKey);}}catch(_){}}
|
||
|
|
function showRecovery(){
|
||
|
|
recovery.replaceChildren();recovery.hidden=!run.pending;
|
||
|
|
if(!run.pending)return;
|
||
|
|
input.readOnly=!run.pending.canEdit;
|
||
|
|
const link=doc.createElement('a');link.href=`/sag/${run.pending.caseId}/v5`;link.textContent=`Åbn den oprettede sag #${run.pending.caseId}`;
|
||
|
|
recovery.append(doc.createTextNode('Sagen er oprettet. Kommandoerne afventer. '),link);
|
||
|
|
doc.getElementById('submitBtn').textContent=run.pending.canEdit?'Gem rettede kommandoer':'Prøv kommandoerne igen';
|
||
|
|
}
|
||
|
|
function line(){const start=input.value.lastIndexOf('\n',input.selectionStart-1)+1;const end=input.value.indexOf('\n',input.selectionStart);return {start,text:input.value.slice(start,end<0?input.value.length:end)};}
|
||
|
|
function choose(option){
|
||
|
|
if(input.readOnly)return;
|
||
|
|
const current=line();
|
||
|
|
input.setRangeText(option.value,current.start+option.from,current.start+option.to,'end');
|
||
|
|
if(option.caret!==undefined){const caret=current.start+option.from+option.caret;input.setSelectionRange(caret,caret);}
|
||
|
|
input.focus();update();input.dispatchEvent(new root.Event('input',{bubbles:true}));
|
||
|
|
}
|
||
|
|
function renderSuggestions(){
|
||
|
|
suggestions.replaceChildren();active=-1;
|
||
|
|
const current=line();options=root.CaseCommandSuggestions.suggest(current.text,input.selectionStart-current.start,values);
|
||
|
|
options.forEach(option=>{
|
||
|
|
const button=doc.createElement('button');button.type='button';
|
||
|
|
const title=doc.createElement('strong');title.textContent=option.label;
|
||
|
|
const detail=doc.createElement('small');detail.textContent=option.hint;
|
||
|
|
button.append(title,detail);button.addEventListener('click',()=>choose(option));suggestions.append(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
function update(){
|
||
|
|
if(run.pending){showRecovery();if(!run.pending.canEdit)return;}
|
||
|
|
const current=line(),cursor=input.selectionStart-current.start;
|
||
|
|
if(cursor===current.text.length && /\s$/.test(current.text)){
|
||
|
|
const option=root.CaseCommandSuggestions.suggest(current.text,cursor,values).find(row=>row.value==='"" '&&row.from===cursor);
|
||
|
|
if(option){input.setRangeText('""',input.selectionStart,input.selectionStart,'end');const caret=current.start+cursor+1;input.setSelectionRange(caret,caret);}
|
||
|
|
}
|
||
|
|
const parsed=root.CaseCommands.parse(input.value);
|
||
|
|
if(parsed.commands.some(command=>command.type==='help'))help.open=true;
|
||
|
|
preview.replaceChildren();
|
||
|
|
parsed.commands.forEach(command=>{
|
||
|
|
const chip=doc.createElement('span');chip.className='cc-command-chip';
|
||
|
|
const target=command.scope==='main'?'Ny sag':`Undersag ${command.scope+1}`;
|
||
|
|
const name=root.CaseCommands.names.find(name=>({tid:'time',tildel:'assign',forventet:'estimate',undersag:'subcase',mig:'self',afventer:'waiting',løsning:'solution',udsat:'defer',fil:'file',hjælp:'help',prioritet:'priority',gruppe:'group'}[name]||name)===command.type)||command.type;
|
||
|
|
chip.textContent=`/${name} · ${command.title||command.assignee||command.minutes&&command.minutes+' min'||command.date||command.action||target} · ${target}`;
|
||
|
|
preview.append(chip);
|
||
|
|
});
|
||
|
|
if(parsed.errors.length){const error=doc.createElement('small');error.className='text-danger';error.textContent=parsed.errors.map(error=>`Linje ${error.line}: ${error.message}`).join(' · ');preview.append(error);}
|
||
|
|
renderSuggestions();
|
||
|
|
}
|
||
|
|
root.CaseCommands.names.forEach(name=>{
|
||
|
|
const button=doc.createElement('button');button.type='button';button.textContent='/'+name;
|
||
|
|
button.addEventListener('click',()=>{
|
||
|
|
if(input.readOnly)return;
|
||
|
|
const prefix=input.value&&!input.value.endsWith('\n')?'\n':'';
|
||
|
|
input.value+=prefix+'/'+name+' ';input.setSelectionRange(input.value.length,input.value.length);input.focus();update();input.dispatchEvent(new root.Event('input',{bubbles:true}));
|
||
|
|
});helpList.append(button);
|
||
|
|
});
|
||
|
|
input.addEventListener('input',update);input.addEventListener('click',renderSuggestions);
|
||
|
|
input.addEventListener('keydown',event=>{
|
||
|
|
if(event.isComposing||event.ctrlKey||event.metaKey||event.altKey||event.shiftKey)return;
|
||
|
|
if(event.key==='Escape'){suggestions.replaceChildren();options=[];active=-1;return;}
|
||
|
|
if(event.key==='ArrowDown'||event.key==='ArrowUp'){
|
||
|
|
if(!options.length)return;event.preventDefault();active=(active+(event.key==='ArrowDown'?1:-1)+options.length)%options.length;
|
||
|
|
[...suggestions.children].forEach((button,index)=>button.classList.toggle('is-active',index===active));return;
|
||
|
|
}
|
||
|
|
if(event.key!=='Enter'||input.selectionStart!==input.selectionEnd)return;
|
||
|
|
const current=line(),cursor=input.selectionStart-current.start;
|
||
|
|
if(active<0&&/^\s*\/\/?\S+/.test(current.text)){
|
||
|
|
let opening=-1;
|
||
|
|
for(let index=0;index<current.text.length;index++){
|
||
|
|
if(current.text[index]==='\\'&&opening>=0&&['"','\\'].includes(current.text[index+1])){index++;continue;}
|
||
|
|
if(current.text[index]!=='"')continue;
|
||
|
|
if(opening<0)opening=index;
|
||
|
|
else {if(cursor>opening&&cursor<=index){event.preventDefault();const end=current.start+current.text.length;input.setRangeText('\n',end,end,'end');update();input.dispatchEvent(new root.Event('input',{bubbles:true}));return;}opening=-1;}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if(options.length){event.preventDefault();choose(options[active>=0?active:0]);}
|
||
|
|
});
|
||
|
|
root.caseCreateCommands={
|
||
|
|
ready,
|
||
|
|
prepare(){
|
||
|
|
const parsed=root.CaseCommands.parse(input.value);
|
||
|
|
if(parsed.errors.length)throw new Error(parsed.errors.map(error=>`Linje ${error.line}: ${error.message}`).join(' · '));
|
||
|
|
if(!parsed.comment)throw new Error('Skriv også en beskrivelse af sagen over kommandoerne.');
|
||
|
|
if(parsed.commands.some(command=>command.type==='file'))throw new Error('Vedhæft filer på sagen efter oprettelse. Fjern /fil før du opretter.');
|
||
|
|
prepared=parsed;return parsed;
|
||
|
|
},
|
||
|
|
hasPending:()=>Boolean(run.pending),
|
||
|
|
async created(result){run.attach(result.id,prepared?.commands||[]);if(run.pending)run.pending.draftText=input.value;persist();showRecovery();await this.retry();},
|
||
|
|
async retry(){
|
||
|
|
if(!run.pending)return;
|
||
|
|
if(run.pending.canEdit){
|
||
|
|
const caseId=run.pending.caseId;
|
||
|
|
const parsed=this.prepare();
|
||
|
|
run.attach(caseId,parsed.commands);
|
||
|
|
// An empty command list explicitly finishes the remaining step.
|
||
|
|
if(!parsed.commands.length){run.pending=null;persist();return;}
|
||
|
|
run.pending.draftText=input.value;persist();
|
||
|
|
}
|
||
|
|
input.readOnly=true;
|
||
|
|
try {await run.execute();persist();showRecovery();}
|
||
|
|
catch(error){run.pending.canEdit=Boolean(error.definite);persist();showRecovery();throw new Error(`Sag #${run.pending.caseId} er oprettet, men kommandoerne afventer: ${error.message}`);}
|
||
|
|
finally {input.readOnly=Boolean(run.pending&&!run.pending.canEdit);}
|
||
|
|
},
|
||
|
|
setCreating:busy=>{input.readOnly=busy||Boolean(run.pending&&!run.pending.canEdit);},
|
||
|
|
pendingCaseId:()=>run.pending?.caseId
|
||
|
|
};
|
||
|
|
update();
|
||
|
|
}
|
||
|
|
return {CommandRun,init};
|
||
|
|
});
|