feat(admin): независимые именованные UTM-наборы
This commit is contained in:
1 parent
941a4f01aa
commit
94619432a3
16 files changed
+220
-114
No files matched your search
@@ -19,3 +19,4 @@
|
||||
.matrix-read-at{font-size:11px;font-weight:400;margin:4px 0 8px;line-height:1.4}.matrix-variant-number{font-size:13px;padding:3px 5px}
|
||||
|
||||
.matrix-add-variant{display:inline-flex;align-items:center;border:0;background:transparent;color:#337ab7;padding:8px 0 0;font-size:12px;cursor:pointer}.matrix-add-variant:hover{color:#23527c;text-decoration:underline}.matrix-add-variant:focus-visible{outline:2px solid #337ab7;outline-offset:3px}.matrix-add-variant:disabled{opacity:.5;cursor:default}.matrix-new-number{width:44px}.indpic-add-form{align-items:end}
|
||||
.domain-preset{display:inline-flex;align-items:center;justify-content:center;padding:3px;border:0;background:transparent;color:#8092a2;border-radius:3px;cursor:pointer;flex-shrink:0}.domain-preset:hover{color:#337ab7;background:#e9f1f7}.domain-preset:disabled{opacity:.5;cursor:default}.preset-select-row{display:flex;gap:8px;align-items:stretch;margin-top:8px}.preset-select-row select{flex:1;min-width:0}.preset-icon{display:inline-flex;align-items:center;justify-content:center;padding:7px 9px;flex-shrink:0}.preset-icon:hover{color:#a94442}.preset-list-tools{margin:8px 0}.preset-dialog{width:460px}.preset-dialog .modal-body{white-space:normal}.preset-dialog label{display:block;font-weight:600;margin-bottom:8px}.preset-help{font-size:12px;margin:12px 0 0}.preset-dialog #preset-error:not(:empty){margin:12px 0 0}.preset-dialog .modal-footer{display:flex;justify-content:flex-end;gap:8px}
|
||||
@@ -140,33 +140,78 @@
|
||||
}catch(error){if(isCurrent()){entry.error=error.message;entry.conflict=entry.conflict||error.status===409;}}
|
||||
finally{if(isCurrent()){entry.saving=false;updateMatrixStatus(entry);renderCopy();}}
|
||||
}
|
||||
const presets={items:[],loaded:false,loading:false,error:'',serial:0,busy:false,snapshot:null};
|
||||
async function loadPresets(){
|
||||
const serial=++presets.serial,generation=state.sessionGeneration;presets.loading=true;presets.error='';renderCopy();
|
||||
try{const result=await api('utm-presets');if(serial!==presets.serial||generation!==state.sessionGeneration||!state.csrf)return;presets.items=result.presets;presets.loaded=true;if(!presets.items.some(p=>p.id===matrix.copySource))matrix.copySource='';}
|
||||
catch(error){if(serial===presets.serial&&generation===state.sessionGeneration)presets.error=error.message;}
|
||||
finally{if(serial===presets.serial&&generation===state.sessionGeneration){presets.loading=false;renderCopy();}}
|
||||
}
|
||||
function currentUtm(entry){
|
||||
if(!entry.data||entry.reading||entry.saving)throw Error('Дождитесь чтения настроек и завершения сохранения.');
|
||||
const invalid=matrixValidation(entry);if(invalid)throw Error(invalid);
|
||||
const utm={};
|
||||
for(const marker of markers){const group=entry.groups.get(marker),raw=groupFor(entry.data.config,marker);if(!raw.key&&!group?.added)continue;
|
||||
if(!group)throw Error(marker+': некорректная группа.');
|
||||
if(!group.touched&&!group.added){utm[marker]={default:structuredClone(raw.value.default),weights:structuredClone(raw.value.weights)};continue;}
|
||||
if(!group.rows.length||!group.defaultRow)throw Error(marker+': заполните варианты и выберите default.');
|
||||
const weights={};for(const row of group.rows){if(!row.isNew&&!Object.hasOwn(group.original.weights,row.variant.value)&&Number(row.weight.value)===0)continue;weights[row.variant.value]=Number(row.weight.value);}
|
||||
utm[marker]={default:group.defaultRow.variant.value,weights};
|
||||
}
|
||||
if(!Object.keys(utm).length)throw Error('Нет UTM для сохранения.');return utm;
|
||||
}
|
||||
function openPreset(id){
|
||||
try{presets.snapshot=currentUtm(matrixEntry(id));$('preset-name').value='';$('preset-name').disabled=false;$('preset-save').disabled=false;$('preset-cancel').disabled=false;$('preset-error').textContent='';$('preset-dialog').showModal();$('preset-name').focus();}
|
||||
catch(error){notice(error.message);}
|
||||
}
|
||||
async function savePreset(event){
|
||||
event.preventDefault();if(presets.busy||!presets.snapshot)return;const generation=state.sessionGeneration;
|
||||
presets.busy=true;$('preset-save').disabled=true;$('preset-cancel').disabled=true;$('preset-name').disabled=true;$('preset-error').textContent='';
|
||||
try{const result=await api('utm-presets',{name:$('preset-name').value,utm:presets.snapshot});if(generation!==state.sessionGeneration||!state.csrf)return;
|
||||
presets.serial++;presets.loading=false;presets.loaded=true;presets.error='';presets.items=presets.items.filter(p=>p.id!==result.preset.id);presets.items.push({id:result.preset.id,name:result.preset.name});matrix.copySource=result.preset.id;
|
||||
$('preset-dialog').close();presets.snapshot=null;notice('UTM-набор «'+result.preset.name+'» сохранён в админке. Настройки сайтов не изменены.','success');renderCopy();
|
||||
}catch(error){if(generation===state.sessionGeneration)$('preset-error').textContent=error.message;}
|
||||
finally{if(generation===state.sessionGeneration){presets.busy=false;$('preset-save').disabled=false;$('preset-cancel').disabled=false;$('preset-name').disabled=false;}}
|
||||
}
|
||||
async function deletePreset(){
|
||||
const selected=presets.items.find(p=>p.id===matrix.copySource);if(!selected||presets.busy||presets.loading||matrix.copyBusy)return;
|
||||
if(!confirm('Удалить набор «'+selected.name+'» из админки? Настройки сайтов и заметки не изменятся.'))return;
|
||||
const generation=state.sessionGeneration;presets.busy=true;renderCopy();
|
||||
try{await api('utm-presets',{id:selected.id},'DELETE');if(generation!==state.sessionGeneration||!state.csrf)return;presets.serial++;presets.items=presets.items.filter(p=>p.id!==selected.id);if(matrix.copySource===selected.id)matrix.copySource='';presets.error='';notice('Набор удалён.','success');}
|
||||
catch(error){if(generation===state.sessionGeneration)presets.error=error.message;}
|
||||
finally{if(generation===state.sessionGeneration){presets.busy=false;renderCopy();}}
|
||||
}
|
||||
function renderCopy(){
|
||||
$('copy-all').disabled=matrix.copyBusy;$('copy-none').disabled=matrix.copyBusy;
|
||||
const resultBox=$('copy-results');resultBox.replaceChildren();if(matrix.copyError)resultBox.append(el('p',{class:'overview-error',text:matrix.copyError}));for(const result of orderedResults(matrix.copyResults)){const id=typeof result.target==='object'?result.target.id:result.target;resultBox.append(el('p',{class:'status-'+result.status,text:targetName(id)+' — '+(statuses[result.status]||result.status)+(result.message?' · '+result.message:'')}));}
|
||||
const source=$('copy-source'),ids=state.targets.map(t=>t.id);if([...source.options].slice(1).map(o=>o.value).join('|')!==ids.join('|'))source.replaceChildren(el('option',{value:'',text:'Выберите домен'}),...state.targets.map(t=>el('option',{value:t.id,text:t.domain})));for(const option of [...source.options]){const target=state.targets.find(t=>t.id===option.value);if(target)option.textContent=target.domain;}source.value=matrix.copySource;source.disabled=matrix.copyBusy;const box=$('copy-targets');box.replaceChildren();for(const target of state.targets){if(target.id===matrix.copySource){matrix.copyTargets.delete(target.id);continue;}const input=el('input',{type:'checkbox',checked:matrix.copyTargets.has(target.id),disabled:matrix.copyBusy||!target.enabled||!target.verified,'aria-label':'Копировать в '+target.domain});input.addEventListener('change',()=>{if(input.checked)matrix.copyTargets.add(target.id);else matrix.copyTargets.delete(target.id);renderCopy();});box.append(el('label',{},input,target.domain));}const from=matrix.entries.get(matrix.copySource),targets=[...matrix.copyTargets].map(id=>matrixEntry(id));let reason=matrix.copyBusy?'Копирование…':!from?'Выберите исходный домен.':!state.targets.find(t=>t.id===from.id)?.enabled||!state.targets.find(t=>t.id===from.id)?.verified?'Источник отключён или соединение не проверено.':!from.data?'Дождитесь чтения исходного домена.':overview.entries.get(from.id)?.status!=='success'?'Обновите настройки источника после ошибки чтения.':matrixValidation(from)?'Источник: '+matrixValidation(from):from.dirty||from.saving?'Сначала сохраните или отмените изменения источника.':from.conflict?'Обновите настройки источника.':!targets.length?'Выберите домены получателей.':targets.some(e=>!state.targets.find(t=>t.id===e.id)?.enabled||!state.targets.find(t=>t.id===e.id)?.verified)?'Получатель отключён или соединение не проверено.':targets.some(e=>e.data&&matrixValidation(e))?'Получатель: '+matrixValidation(targets.find(e=>e.data&&matrixValidation(e))):targets.some(e=>e.dirty||e.saving)?'Сначала сохраните или отмените изменения получателей.':targets.some(e=>!e.data||e.conflict||overview.entries.get(e.id)?.status!=='success')?'Обновите настройки получателей.':'';$('copy-reason').textContent=reason;$('copy-submit').disabled=!!reason;$('copy-submit').textContent='Копировать в '+targets.length+' '+(targets.length%10===1&&targets.length%100!==11?'домен':targets.length%10>=2&&targets.length%10<=4&&(targets.length%100<12||targets.length%100>14)?'домена':'доменов');}
|
||||
const source=$('copy-source');source.replaceChildren(el('option',{value:'',text:'Выберите сохранённый набор'}),...presets.items.map(p=>el('option',{value:p.id,text:p.name})));source.value=matrix.copySource;source.disabled=matrix.copyBusy||presets.busy||presets.loading;
|
||||
$('preset-delete').disabled=!presets.items.some(p=>p.id===matrix.copySource)||matrix.copyBusy||presets.busy||presets.loading;
|
||||
$('preset-reload').disabled=presets.loading||presets.busy||matrix.copyBusy;
|
||||
$('preset-list-status').textContent=presets.loading?'Загрузка наборов…':presets.error?'Не удалось обновить список: '+presets.error:presets.loaded&&!presets.items.length?'Пока нет сохранённых наборов. Нажмите дискету рядом с доменом.':'';
|
||||
const box=$('copy-targets');box.replaceChildren();for(const target of state.targets){const input=el('input',{type:'checkbox',checked:matrix.copyTargets.has(target.id),disabled:matrix.copyBusy||!target.enabled||!target.verified,'aria-label':'Копировать в '+target.domain});input.addEventListener('change',()=>{if(input.checked)matrix.copyTargets.add(target.id);else matrix.copyTargets.delete(target.id);renderCopy();});box.append(el('label',{},input,target.domain));}
|
||||
const targets=[...matrix.copyTargets].map(id=>matrixEntry(id));let reason=matrix.copyBusy?'Копирование…':presets.loading?'Дождитесь загрузки наборов.':presets.error?'Обновите список сохранённых наборов.':!presets.items.some(p=>p.id===matrix.copySource)?'Выберите сохранённый набор.':!targets.length?'Выберите домены получателей.':targets.some(e=>!state.targets.find(t=>t.id===e.id)?.enabled||!state.targets.find(t=>t.id===e.id)?.verified)?'Получатель отключён или соединение не проверено.':targets.some(e=>e.data&&matrixValidation(e))?'Получатель: '+matrixValidation(targets.find(e=>e.data&&matrixValidation(e))):targets.some(e=>e.dirty||e.saving)?'Сначала сохраните или отмените изменения получателей.':targets.some(e=>!e.data||e.conflict||overview.entries.get(e.id)?.status!=='success')?'Обновите настройки получателей.':'';
|
||||
$('copy-reason').textContent=reason;$('copy-submit').disabled=!!reason||presets.busy;$('copy-submit').textContent='Копировать в '+targets.length+' '+(targets.length%10===1&&targets.length%100!==11?'домен':targets.length%10>=2&&targets.length%10<=4&&(targets.length%100<12||targets.length%100>14)?'домена':'доменов');
|
||||
}
|
||||
async function copySettings(){
|
||||
if($('copy-submit').disabled)return;
|
||||
const from=matrixEntry(matrix.copySource),ids=[...matrix.copyTargets],generation=state.sessionGeneration,operation=++matrix.copyEpoch;
|
||||
const entries=new Map([from.id,...ids].map(id=>[id,matrix.entries.get(id)]));
|
||||
const preset=matrix.copySource,ids=[...matrix.copyTargets],generation=state.sessionGeneration,operation=++matrix.copyEpoch;
|
||||
const entries=new Map(ids.map(id=>[id,matrix.entries.get(id)]));
|
||||
const sameOperation=()=>generation===state.sessionGeneration&&!!state.csrf&&matrix.copyEpoch===operation;
|
||||
const isCurrent=()=>sameOperation()&&[...entries].every(([id,entry])=>matrix.entries.get(id)===entry);
|
||||
matrix.copyError='';matrix.copyResults=[];matrix.copyBusy=true;for(const entry of entries.values()){entry.epoch++;entry.saving=true;updateMatrixStatus(entry);}renderCopy();
|
||||
try{
|
||||
const result=await api('copy',{source:{id:from.id,version:from.data.version},targets:ids.map(id=>({id,version:entries.get(id).data.version}))});if(!isCurrent())return;
|
||||
const job=await waitMatrixJob(result.job,isCurrent);if(!isCurrent()||!job)return;
|
||||
matrix.copyResults=job.results||[];
|
||||
for(const outcome of job.results||[]){
|
||||
if(!isCurrent())return;const id=typeof outcome.target==='object'?outcome.target.id:outcome.target,entry=entries.get(id);if(!entry)continue;
|
||||
const result=await api('copy',{preset,targets:ids.map(id=>({id,version:entries.get(id).data.version}))});if(!isCurrent())return;
|
||||
const job=await waitMatrixJob(result.job,isCurrent);if(!isCurrent()||!job)return;matrix.copyResults=job.results||[];
|
||||
for(const outcome of job.results||[]){if(!isCurrent())return;const id=typeof outcome.target==='object'?outcome.target.id:outcome.target,entry=entries.get(id);if(!entry)continue;
|
||||
if(outcome.status==='success'){entry.saving=false;await refreshOverviewTarget(id);if(!isCurrent())return;if(overview.entries.get(id)?.status==='success')entry.written=true;}
|
||||
else{entry.error=outcome.message||'Не удалось скопировать настройки.';entry.conflict=outcome.status==='conflict';updateMatrixStatus(entry);}
|
||||
}
|
||||
else{entry.error=outcome.message||'Не удалось скопировать настройки.';entry.conflict=outcome.status==='conflict';updateMatrixStatus(entry);}}
|
||||
if(!isCurrent())return;notice('Копирование завершено. Результат каждого домена показан в журнале.','info');
|
||||
if(state.source&&ids.includes(state.source)){await fetchRead(state.source);if(!isCurrent())return;await loadSets();if(!isCurrent())return;await loadBackups();}
|
||||
}catch(error){if(isCurrent()){matrix.copyError=error.message;notice(error.message);if(error.status===409){from.conflict=true;from.error=error.message;updateMatrixStatus(from);}}}
|
||||
}catch(error){if(isCurrent()){matrix.copyError=error.message;notice(error.message);if(error.status===404)run(loadPresets);}}
|
||||
finally{if(sameOperation()){matrix.copyBusy=false;for(const [id,entry]of entries)if(matrix.entries.get(id)===entry){entry.saving=false;updateMatrixStatus(entry);}renderCopy();}}
|
||||
}
|
||||
function syncOverview(refresh=true){
|
||||
const ids=new Set(state.targets.map(target=>target.id));for(const id of matrix.entries.keys())if(!ids.has(id)){matrix.entries.get(id).epoch++;matrix.entries.delete(id);matrix.copyTargets.delete(id);matrix.operationTargets.delete(id);}if(!ids.has(matrix.copySource))matrix.copySource='';for(const id of overview.entries.keys())if(!ids.has(id)){overview.entries.delete(id);overview.pending.delete(id);builder.entries.delete(id);}
|
||||
const ids=new Set(state.targets.map(target=>target.id));for(const id of matrix.entries.keys())if(!ids.has(id)){matrix.entries.get(id).epoch++;matrix.entries.delete(id);matrix.copyTargets.delete(id);matrix.operationTargets.delete(id);}for(const id of overview.entries.keys())if(!ids.has(id)){overview.entries.delete(id);overview.pending.delete(id);builder.entries.delete(id);}
|
||||
if(builder.target&&!ids.has(builder.target))builder.target='';
|
||||
const select=$('builder-target');select.replaceChildren(el('option',{value:'',text:'Выберите домен'}),...state.targets.map(t=>el('option',{value:t.id,text:targetName(t.id)})));select.value=builder.target;renderOverview();if(builder.target&&builder.entries.get(builder.target)?.domain!==state.targets.find(t=>t.id===builder.target)?.domain)run(()=>openBuilder(builder.target,true));else renderBuilder();if(refresh)run(refreshOverview);
|
||||
}
|
||||
@@ -231,7 +276,7 @@
|
||||
if(generation!==state.sessionGeneration){const error=new Error('Ответ относится к завершённому сеансу.');error.staleSession=true;throw error;}if(!response.ok||!body.ok){if(response.status===401&&generation===state.sessionGeneration)showLogin();const error=new Error(body.error||'Не удалось выполнить операцию (HTTP '+response.status+').');error.status=response.status;throw error;}return body;
|
||||
}
|
||||
async function run(action){try{await action();}catch(error){if(!error.staleSession)notice(error.message);}}
|
||||
function showLogin(){state.sessionGeneration++;matrix.copyEpoch++;matrix.copyBusy=false;for(const entry of matrix.entries.values())entry.epoch++;matrix.entries.clear();matrix.structure='';matrix.copySource='';matrix.copyError='';matrix.copyResults=[];matrix.copyTargets.clear();matrix.operationTargets.clear();utmNotes.serial++;utmNotes.entries.clear();utmNotes.loading=false;utmNotes.error='';overview.entries.clear();overview.pending.clear();builder.entries.clear();builder.target='';for(const timer of pollTimers.values())clearTimeout(timer);pollTimers.clear();state.csrf='';state.targetsEpoch++;state.targetsBusy=false;state.targets=[];state.allTargets=[];state.source='';state.reads.clear();$('workspace').hidden=true;$('logout').hidden=true;$('login-panel').hidden=false;}
|
||||
function showLogin(){state.sessionGeneration++;presets.serial++;presets.items=[];presets.loaded=false;presets.loading=false;presets.busy=false;presets.snapshot=null;presets.error='';$('preset-dialog').close();matrix.copyEpoch++;matrix.copyBusy=false;for(const entry of matrix.entries.values())entry.epoch++;matrix.entries.clear();matrix.structure='';matrix.copySource='';matrix.copyError='';matrix.copyResults=[];matrix.copyTargets.clear();matrix.operationTargets.clear();utmNotes.serial++;utmNotes.entries.clear();utmNotes.loading=false;utmNotes.error='';overview.entries.clear();overview.pending.clear();builder.entries.clear();builder.target='';for(const timer of pollTimers.values())clearTimeout(timer);pollTimers.clear();state.csrf='';state.targetsEpoch++;state.targetsBusy=false;state.targets=[];state.allTargets=[];state.source='';state.reads.clear();$('workspace').hidden=true;$('logout').hidden=true;$('login-panel').hidden=false;}
|
||||
function selected(){return [...$('targets').querySelectorAll('input:checked')].map(n=>n.value);}
|
||||
function targetName(id){const t=state.targets.find(t=>t.id===id);return t? t.domain+' ('+t.environment+')':id;}
|
||||
function equivalent(a,b){return JSON.stringify(a)===JSON.stringify(b);}
|
||||
@@ -242,7 +287,7 @@
|
||||
|
||||
async function fetchRead(id){const generation=state.sessionGeneration;const data=await api('config?target='+encodeURIComponent(id));if(generation===state.sessionGeneration&&state.targets.some(t=>t.id===id))state.reads.set(id,data);return data;}
|
||||
async function openTarget(id){if(id&&!state.targets.some(t=>t.id===id))return;if(id!==state.source)matrix.operationTargets=new Set(id?[id]:[]);state.source=id;$('sets-body').replaceChildren();$('backups').replaceChildren();renderTargetOptions();if(!id){$('read-state').textContent='Выберите домен.';return;}state.loading++;$('read-state').textContent='Чтение конфигурации для наборов и резерва…';try{await fetchRead(id);await Promise.all([loadSets(),loadBackups()]);if(state.source===id)$('read-state').textContent='Домен прочитан.';}finally{state.loading--;}} function renderTargetOptions(){const box=$('targets');box.replaceChildren();for(const target of state.targets){const input=el('input',{type:'checkbox',value:target.id,checked:matrix.operationTargets.has(target.id)&&target.enabled,disabled:!target.enabled});const status=el('span',{class:'text-muted'});const label=el('label',{},input,targetName(target.id),status);box.append(label);input.addEventListener('change',()=>run(async()=>{if(input.checked)matrix.operationTargets.add(target.id);else matrix.operationTargets.delete(target.id);if(input.checked&&!state.reads.has(target.id)){status.textContent=' · чтение версии…';state.loading++;try{await fetchRead(target.id);status.textContent=' · прочитано';}catch(e){input.checked=false;status.textContent=' · недоступен';throw e;}finally{state.loading--;}}}));}}
|
||||
async function loadState(){if(state.targetsBusy)return;const epoch=state.targetsEpoch,generation=state.sessionGeneration;const data=await api('state');if(generation!==state.sessionGeneration||epoch!==state.targetsEpoch||state.targetsBusy)return;state.csrf=data.csrf;$('login-panel').hidden=true;$('workspace').hidden=false;$('logout').hidden=false;acceptTargets(data.targets||[]);await loadJobs();}
|
||||
async function loadState(){if(state.targetsBusy)return;const epoch=state.targetsEpoch,generation=state.sessionGeneration;const data=await api('state');if(generation!==state.sessionGeneration||epoch!==state.targetsEpoch||state.targetsBusy)return;state.csrf=data.csrf;$('login-panel').hidden=true;$('workspace').hidden=false;$('logout').hidden=false;acceptTargets(data.targets||[]);run(loadPresets);await loadJobs();}
|
||||
function orderedResults(results){return (results||[]).filter(visibleResult).slice().sort((a,b)=>{const id=r=>typeof r.target==='object'?r.target.id:r.target;return state.targets.findIndex(t=>t.id===id(a))-state.targets.findIndex(t=>t.id===id(b));});}
|
||||
function visibleResult(result){const id=typeof result.target==='object'?result.target.id:result.target;return state.targets.some(t=>t.id===id);}
|
||||
function acceptTargets(targets,refresh=true){
|
||||
@@ -268,7 +313,9 @@
|
||||
grip.addEventListener('pointerdown',event=>startDomainDrag(event,target.id));
|
||||
const hide=el('button',{type:'button',class:'domain-hide',title:'Скрыть домен','aria-label':'Скрыть домен '+target.domain,onclick:()=>run(()=>hideTarget(target.id))});
|
||||
hide.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m3 3 18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.5 5.3A11 11 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-3.4 4.1M6.2 6.2C3.6 8 2 12 2 12s4 7 10 7a11 11 0 0 0 5.1-1.4"/></svg>';
|
||||
return el('div',{class:'domain-heading'},grip,el('span',{class:'overview-domain',text:target.domain}),hide);
|
||||
const save=el('button',{type:'button',class:'domain-preset',title:'Сохранить UTM-настройки','aria-label':'Сохранить UTM-набор '+target.domain,onclick:()=>openPreset(target.id)});
|
||||
save.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M5 3h12l4 4v14H3V3h2Zm2 0v6h10V3M7 21v-8h10v8"/></svg>';
|
||||
return el('div',{class:'domain-heading'},grip,el('span',{class:'overview-domain',text:target.domain}),save,hide);
|
||||
}
|
||||
function startDomainDrag(event,id){
|
||||
if(event.button!==0||state.targetsBusy)return;event.preventDefault();const grip=event.currentTarget,wrapper=$('overview-table').closest('.overview-wrapper'),header=grip.closest('th'),preview=header.cloneNode(true),marker=el('div',{class:'domain-insertion'});preview.className='domain-drag-preview';preview.style.width=header.offsetWidth+'px';document.body.append(preview,marker);let before=id,x=event.clientX,y=event.clientY,frame;
|
||||
@@ -302,9 +349,14 @@
|
||||
$('logout').addEventListener('click',()=>run(async()=>{if(matrixDirty()&&!confirm('Есть несохранённые изменения настроек или заметок. Выйти?'))return;await api('logout',{});for(const timer of pollTimers.values())clearTimeout(timer);pollTimers.clear();showLogin();notice('Сеанс завершён.','info');}));
|
||||
$('source-target').addEventListener('change',event=>run(()=>openTarget(event.target.value)));
|
||||
$('reload').addEventListener('click',()=>run(()=>openTarget(state.source)));
|
||||
$('copy-source').addEventListener('change',e=>{matrix.copySource=e.target.value;matrix.copyError='';matrix.copyResults=[];matrix.copyTargets.delete(e.target.value);renderCopy();});
|
||||
$('copy-all').addEventListener('click',()=>{matrix.copyTargets=new Set(state.targets.filter(t=>t.id!==matrix.copySource&&t.enabled&&t.verified).map(t=>t.id));renderCopy();});
|
||||
$('copy-source').addEventListener('change',e=>{matrix.copySource=e.target.value;matrix.copyError='';matrix.copyResults=[];renderCopy();});
|
||||
$('copy-all').addEventListener('click',()=>{matrix.copyTargets=new Set(state.targets.filter(t=>t.enabled&&t.verified).map(t=>t.id));renderCopy();});
|
||||
$('copy-none').addEventListener('click',()=>{matrix.copyTargets.clear();renderCopy();});
|
||||
$('preset-form').addEventListener('submit',event=>run(()=>savePreset(event)));
|
||||
$('preset-cancel').addEventListener('click',()=>{$('preset-dialog').close();presets.snapshot=null;});
|
||||
$('preset-dialog').addEventListener('cancel',event=>{if(presets.busy)event.preventDefault();else presets.snapshot=null;});
|
||||
$('preset-delete').addEventListener('click',()=>run(deletePreset));
|
||||
$('preset-reload').addEventListener('click',()=>run(loadPresets));
|
||||
$('copy-submit').addEventListener('click',()=>run(copySettings)); $('set-add').elements.sftp.addEventListener('change',event=>{const form=$('set-add');form.querySelector('.zip-field').hidden=event.target.checked;form.elements.file.required=!event.target.checked;form.querySelector('button').textContent=event.target.checked?'Проверить и добавить':'Загрузить сет';});
|
||||
$('set-add').addEventListener('submit',event=>{event.preventDefault();run(async()=>{const form=event.currentTarget;const number=Number(form.elements.number.value);if(form.elements.sftp.checked){const r=await api('sets/register',{targets:safeTargets(),number,name:form.elements.name.value});await showJob(r.job,true);}else await uploadSet(form.elements.file.files[0],number,form.elements.name.value,false);});});
|
||||
$('connection-new').addEventListener('click',()=>{$('connection-form').reset();$('connection-form').elements.id.readOnly=false;$('connection-title').textContent='Добавить подключение';});
|
||||
|
||||
Reference in new issue
Block a user