Legg til dagsplan + kommentar ved statusbytte
Dagsplan: tredje visning ved siden av tavle/tabell. To-kolonne planlegger der man plukker oppgaver fra backlog og bygger en ordnet plan for dagen (drag-sortering, kapasitet i timer, datovelger). Per bruker, per dato, lagret i plans.json; synkes mellom egne faner via plan-WS-melding. Kommentar ved statusbytte: når en oppgave markeres ferdig/blokkert (knapp, kanban-drag eller modal) tilbys en valgfri kommentar som festes til oppgaven (kind:status) og legges i aktivitetsloggen — atomisk i ett update-kall. 20 integrasjonstester (8 nye), alle grønne. README + CLAUDE.md oppdatert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6b9dbf8254
commit
be166034de
8 changed files with 686 additions and 32 deletions
|
|
@ -50,11 +50,14 @@ const state = {
|
|||
editingMap: {}, // taskId -> [brukere med modal åpen]
|
||||
filters: { status: 'open', category: 'all', priority: 'all', owner: 'all', search: '' },
|
||||
sort: { field: 'priority', dir: 'asc' },
|
||||
view: 'board',
|
||||
view: 'board', // 'board' | 'table' | 'plan'
|
||||
editingId: null, // id i åpen modal (null = ny oppgave)
|
||||
editingBaseV: null, // versjonen modalen redigerer fra
|
||||
editingStatus: null, // statusen oppgaven hadde da modalen ble åpnet
|
||||
plan: { date: '', items: [] }, // dagsplan for valgt dato (ordnet liste av task-ids)
|
||||
wsOk: false,
|
||||
};
|
||||
const VIEWS = ['board', 'table', 'plan'];
|
||||
let charts = {};
|
||||
let ws = null;
|
||||
let wsRetry = 0;
|
||||
|
|
@ -69,7 +72,7 @@ function loadPrefs() {
|
|||
const p = JSON.parse(localStorage.getItem('netops.prefs') || '{}');
|
||||
if (p.filters) Object.assign(state.filters, p.filters);
|
||||
if (p.sort) state.sort = p.sort;
|
||||
if (p.view === 'table' || p.view === 'board') state.view = p.view;
|
||||
if (VIEWS.includes(p.view)) state.view = p.view;
|
||||
} catch { /* ignorer korrupt localStorage */ }
|
||||
}
|
||||
function savePrefs() {
|
||||
|
|
@ -229,11 +232,21 @@ function handleWSMessage(msg) {
|
|||
updateModalWarning();
|
||||
break;
|
||||
|
||||
case 'plan':
|
||||
// Dagsplanen synkes mellom samme brukers faner. Bruk bare hvis den
|
||||
// gjelder datoen vi ser på nå.
|
||||
if (msg.date === state.plan.date) {
|
||||
state.plan.items = msg.items || [];
|
||||
if (state.view === 'plan') renderPlan();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task': {
|
||||
const mine = msg.by === state.user;
|
||||
pushActivity(msg.activity);
|
||||
if (msg.op === 'delete') {
|
||||
state.tasks = state.tasks.filter(t => t.id !== msg.id);
|
||||
state.plan.items = state.plan.items.filter(id => id !== msg.id);
|
||||
if (!mine && state.editingId === msg.id) {
|
||||
closeModal();
|
||||
toast(`Oppgaven du redigerte ble slettet av ${msg.by}`, { kind: 'warn', ttl: 6000 });
|
||||
|
|
@ -263,6 +276,8 @@ async function refreshState() {
|
|||
state.activity = data.activity || [];
|
||||
state.knownUsers = data.known_users || [];
|
||||
state.online = data.online || [];
|
||||
if (data.plan) state.plan = data.plan;
|
||||
if (!state.plan.date) state.plan = { date: todayISO(), items: [] };
|
||||
renderAll();
|
||||
} catch (e) {
|
||||
toast('Kunne ikke laste: ' + e.message, { kind: 'err' });
|
||||
|
|
@ -505,10 +520,15 @@ function renderCharts() {
|
|||
function renderView() {
|
||||
$('view-board').className = state.view === 'board' ? 'active-view' : '';
|
||||
$('view-table').className = state.view === 'table' ? 'active-view' : '';
|
||||
$('fg-status').style.display = state.view === 'board' ? 'none' : '';
|
||||
$('view-plan').className = state.view === 'plan' ? 'active-view' : '';
|
||||
// Statusfilteret gir bare mening i tabellvisningen (tavle/plan grupperer selv).
|
||||
$('fg-status').style.display = state.view === 'table' ? '' : 'none';
|
||||
$('board').style.display = state.view === 'board' ? '' : 'none';
|
||||
$('table-panel').style.display = state.view === 'table' ? '' : 'none';
|
||||
if (state.view === 'board') renderBoard(); else renderTable();
|
||||
$('planner').style.display = state.view === 'plan' ? '' : 'none';
|
||||
if (state.view === 'board') renderBoard();
|
||||
else if (state.view === 'table') renderTable();
|
||||
else renderPlan();
|
||||
flashIds.clear();
|
||||
}
|
||||
|
||||
|
|
@ -613,6 +633,12 @@ function bindBoardDnD() {
|
|||
async function moveTask(id, status) {
|
||||
const t = findTask(id);
|
||||
if (!t || t.status === status) return;
|
||||
// Drag til Ferdig/Blokkert → tilby kommentar (ingen optimistisk flytting,
|
||||
// siden brukeren kan avbryte i dialogen).
|
||||
if (status === 'done' || status === 'blocked') {
|
||||
changeStatusWithComment(id, status);
|
||||
return;
|
||||
}
|
||||
const prev = t.status;
|
||||
t.status = status; // optimistisk — broadcast/svar korrigerer
|
||||
renderAll();
|
||||
|
|
@ -673,6 +699,183 @@ function renderTable() {
|
|||
}).join('');
|
||||
}
|
||||
|
||||
// ---------- Dagsplan ----------
|
||||
// Oppgavene i planen, i lagret rekkefølge, med slettede filtrert bort.
|
||||
function planTasks() {
|
||||
return state.plan.items.map(findTask).filter(Boolean);
|
||||
}
|
||||
|
||||
function renderPlan() {
|
||||
$('empty').style.display = 'none';
|
||||
|
||||
const dateInput = $('plan-date');
|
||||
if (document.activeElement !== dateInput) dateInput.value = state.plan.date;
|
||||
|
||||
const planned = planTasks();
|
||||
const planIds = new Set(planned.map(t => t.id));
|
||||
|
||||
// Venstre: åpne oppgaver som ikke alt er i planen (respekterer kat/prio/eier/søk).
|
||||
const avail = sortTasks(
|
||||
applyFilters(state.tasks, { ignoreStatus: true })
|
||||
.filter(t => t.status !== 'done' && !planIds.has(t.id))
|
||||
);
|
||||
$('plan-avail-count').textContent = avail.length ? `(${avail.length})` : '';
|
||||
$('plan-available-body').innerHTML = avail.length
|
||||
? avail.map(renderAvailRow).join('')
|
||||
: '<div class="plan-empty">Ingen åpne oppgaver igjen — alt er planlagt eller filtrert bort.</div>';
|
||||
|
||||
// Høyre: kapasitet + ordnet liste.
|
||||
const remaining = planned.filter(t => t.status !== 'done');
|
||||
const hours = remaining.reduce((s, t) => s + (t.estHours || 0), 0);
|
||||
const doneCount = planned.length - remaining.length;
|
||||
const cap = $('plan-capacity');
|
||||
cap.innerHTML = planned.length
|
||||
? `${planned.length} oppg. · <b>${fmtHours(hours)}</b> planlagt${doneCount ? ` · ${doneCount} ferdig` : ''}`
|
||||
: '';
|
||||
cap.className = 'col-meta' + (hours > 8 ? ' over-capacity' : '');
|
||||
|
||||
$('plan-today-body').innerHTML = planned.length
|
||||
? planned.map(renderPlanItem).join('')
|
||||
: '<div class="plan-empty">Tomt. Legg til oppgaver fra venstre med +, eller dra dem hit. Dra for å sortere.</div>';
|
||||
|
||||
bindPlanDnD();
|
||||
}
|
||||
|
||||
function renderAvailRow(t) {
|
||||
const dl = formatDeadline(t.deadline);
|
||||
return `
|
||||
<div class="avail-row" draggable="true" data-id="${t.id}">
|
||||
<div class="avail-main">
|
||||
<div class="avail-title" onclick="openEdit('${t.id}')">${escapeHtml(t.title)}</div>
|
||||
<div class="avail-meta">
|
||||
<span class="badge prio-${t.priority}">${t.priority}</span>
|
||||
${catBadge(t)}
|
||||
${t.estHours ? `<span>~${t.estHours}t</span>` : ''}
|
||||
${t.deadline ? `<span class="time-cell ${dl.cls}">${dl.text}</span>` : ''}
|
||||
${t.owner ? ownerBadge(t) : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button class="avail-add" onclick="addToPlan('${t.id}')" title="Legg til i dagsplan">+</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPlanItem(t, i) {
|
||||
const dl = formatDeadline(t.deadline);
|
||||
const doneCls = t.status === 'done' ? ' done-item' : '';
|
||||
return `
|
||||
<div class="plan-item${doneCls}" draggable="true" data-id="${t.id}">
|
||||
<span class="plan-handle" title="Dra for å sortere">⠿</span>
|
||||
<span class="plan-num">${i + 1}</span>
|
||||
<div class="plan-item-main">
|
||||
<div class="plan-item-title" onclick="openEdit('${t.id}')">${escapeHtml(t.title)}</div>
|
||||
<div class="plan-item-meta">
|
||||
<span class="badge prio-${t.priority}">${t.priority}</span>
|
||||
${catBadge(t)}
|
||||
<span class="status-badge status-${t.status}">${STATUS_LABELS[t.status] || t.status}</span>
|
||||
${t.estHours ? `<span>~${t.estHours}t</span>` : ''}
|
||||
${t.deadline ? `<span class="time-cell ${dl.cls}">${dl.text}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-item-actions">
|
||||
${t.status !== 'done'
|
||||
? `<button class="icon-btn do" onclick="markDone('${t.id}')" title="Marker ferdig">✓</button>`
|
||||
: `<button class="icon-btn" onclick="reopenTask('${t.id}')" title="Gjenåpne">↺</button>`}
|
||||
<button class="icon-btn del" onclick="removeFromPlan('${t.id}')" title="Fjern fra dagsplan">×</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function getDragAfterElement(container, y) {
|
||||
const els = [...container.querySelectorAll('.plan-item:not(.dragging)')];
|
||||
let closest = { offset: -Infinity, el: null };
|
||||
for (const child of els) {
|
||||
const box = child.getBoundingClientRect();
|
||||
const offset = y - box.top - box.height / 2;
|
||||
if (offset < 0 && offset > closest.offset) closest = { offset, el: child };
|
||||
}
|
||||
return closest.el;
|
||||
}
|
||||
|
||||
// Per-element drag-bindinger. Containeren beholder sine lyttere (satt én gang
|
||||
// i bindUI), så her bindes bare de nygenererte radene/kortene.
|
||||
function bindPlanDnD() {
|
||||
const container = $('plan-today-body');
|
||||
container.querySelectorAll('.plan-item').forEach(item => {
|
||||
item.addEventListener('dragstart', e => {
|
||||
item.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', item.dataset.id);
|
||||
});
|
||||
item.addEventListener('dragend', () => {
|
||||
item.classList.remove('dragging');
|
||||
const ids = [...container.querySelectorAll('.plan-item')].map(el => el.dataset.id);
|
||||
if (ids.join() !== state.plan.items.join()) {
|
||||
state.plan.items = ids;
|
||||
savePlan();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('plan-available-body').querySelectorAll('.avail-row').forEach(row => {
|
||||
row.addEventListener('dragstart', e => {
|
||||
row.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', row.dataset.id);
|
||||
});
|
||||
row.addEventListener('dragend', () => row.classList.remove('dragging'));
|
||||
});
|
||||
}
|
||||
|
||||
// Container-nivå drag-lyttere (settes én gang).
|
||||
function bindPlanContainer() {
|
||||
const container = $('plan-today-body');
|
||||
container.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
const dragging = container.querySelector('.plan-item.dragging');
|
||||
if (!dragging) return; // drar inn et tilgjengelig-kort: håndteres i drop
|
||||
const after = getDragAfterElement(container, e.clientY);
|
||||
if (after == null) container.appendChild(dragging);
|
||||
else container.insertBefore(dragging, after);
|
||||
});
|
||||
container.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
const id = e.dataTransfer.getData('text/plain');
|
||||
if (id && !state.plan.items.includes(id) && findTask(id)) addToPlan(id);
|
||||
});
|
||||
}
|
||||
|
||||
async function savePlan() {
|
||||
renderPlan(); // optimistisk
|
||||
try {
|
||||
const { date, items } = await api('PUT', '/api/plan', { date: state.plan.date, items: state.plan.items });
|
||||
if (date === state.plan.date) { state.plan.items = items; renderPlan(); }
|
||||
} catch (e) {
|
||||
toast('Kunne ikke lagre dagsplan: ' + e.message, { kind: 'err' });
|
||||
refreshPlan();
|
||||
}
|
||||
}
|
||||
|
||||
window.addToPlan = function (id) {
|
||||
if (!state.plan.items.includes(id) && findTask(id)) {
|
||||
state.plan.items.push(id);
|
||||
savePlan();
|
||||
toast('Lagt i dagsplan');
|
||||
}
|
||||
};
|
||||
|
||||
window.removeFromPlan = function (id) {
|
||||
state.plan.items = state.plan.items.filter(x => x !== id);
|
||||
savePlan();
|
||||
};
|
||||
|
||||
async function refreshPlan(date = state.plan.date) {
|
||||
try {
|
||||
const data = await api('GET', `/api/plan?date=${encodeURIComponent(date)}`);
|
||||
state.plan = { date: data.date, items: data.items || [] };
|
||||
if (state.view === 'plan') renderPlan();
|
||||
} catch { /* stille — neste handling prøver igjen */ }
|
||||
}
|
||||
|
||||
function renderActivity() {
|
||||
const body = $('drawer-body');
|
||||
if (!state.activity.length) {
|
||||
|
|
@ -716,17 +919,57 @@ function updateOwnerSelects() {
|
|||
+ sorted.map(u => `<option value="${escapeHtml(u)}">${escapeHtml(u)}</option>`).join('');
|
||||
}
|
||||
|
||||
// ---------- Statuskommentar-dialog ----------
|
||||
// Lover en streng (kommentar, evt. tom = hopp over) eller null (avbrutt).
|
||||
let statusResolve = null;
|
||||
function askStatusComment(task, status) {
|
||||
return new Promise(resolve => {
|
||||
statusResolve = resolve;
|
||||
const label = status === 'done' ? 'Avslutningskommentar (valgfritt)'
|
||||
: status === 'blocked' ? 'Hva blokkerer? (valgfritt)'
|
||||
: 'Kommentar (valgfritt)';
|
||||
$('status-modal-title').textContent = `${STATUS_LABELS[status]}: ${task.title.slice(0, 44)}${task.title.length > 44 ? '…' : ''}`;
|
||||
$('status-modal-label').textContent = label;
|
||||
$('status-comment').value = '';
|
||||
$('status-modal').classList.add('open');
|
||||
setTimeout(() => $('status-comment').focus(), 50);
|
||||
});
|
||||
}
|
||||
function resolveStatus(val) {
|
||||
if (!statusResolve) return;
|
||||
$('status-modal').classList.remove('open');
|
||||
const r = statusResolve;
|
||||
statusResolve = null;
|
||||
r(val);
|
||||
}
|
||||
|
||||
// Statusbytte som tilbyr en kommentar ved fullføring/blokkering.
|
||||
async function changeStatusWithComment(id, status) {
|
||||
const t = findTask(id);
|
||||
if (!t || t.status === status) return;
|
||||
let comment = '';
|
||||
if (status === 'done' || status === 'blocked') {
|
||||
const res = await askStatusComment(t, status);
|
||||
if (res === null) return; // avbrutt — ingen endring
|
||||
comment = res;
|
||||
}
|
||||
await quickStatus(id, status, comment);
|
||||
}
|
||||
|
||||
// ---------- Hurtighandlinger (rad/kort) ----------
|
||||
async function quickStatus(id, status) {
|
||||
async function quickStatus(id, status, comment = '') {
|
||||
const t = findTask(id);
|
||||
if (!t) return;
|
||||
try {
|
||||
const { task } = await api('PUT', `/api/tasks/${id}`, { ...t, status, v: t.v });
|
||||
const body = { ...t, status, v: t.v };
|
||||
if (comment) body.comment = comment;
|
||||
const { task } = await api('PUT', `/api/tasks/${id}`, body);
|
||||
upsertTask(task);
|
||||
renderAll();
|
||||
if (comment) toast('Lagret med kommentar');
|
||||
} catch (e) { handleSaveError(e, id); }
|
||||
}
|
||||
window.markDone = id => quickStatus(id, 'done');
|
||||
window.markDone = id => changeStatusWithComment(id, 'done');
|
||||
window.reopenTask = id => quickStatus(id, 'todo');
|
||||
|
||||
window.claimTask = async function (id) {
|
||||
|
|
@ -785,6 +1028,7 @@ window.openEdit = function (id) {
|
|||
if (!t) return;
|
||||
state.editingId = id;
|
||||
state.editingBaseV = t.v;
|
||||
state.editingStatus = t.status;
|
||||
$('modal-title').textContent = 'Rediger oppgave';
|
||||
$('f-title').value = t.title;
|
||||
$('f-desc').value = t.desc || '';
|
||||
|
|
@ -800,15 +1044,33 @@ window.openEdit = function (id) {
|
|||
$('modal-delete').style.display = 'inline-flex';
|
||||
$('comments-section').style.display = '';
|
||||
$('modal-audit').textContent = `opprettet ${relTime(t.created)} av ${t.createdBy} · sist endret ${relTime(t.updated)} av ${t.updatedBy}`;
|
||||
updateStatusNoteField();
|
||||
renderComments(t);
|
||||
updateModalWarning();
|
||||
$('modal').classList.add('open');
|
||||
sendWS({ type: 'editing', id, active: true });
|
||||
};
|
||||
|
||||
// Viser et kommentarfelt i modalen når status endres til Ferdig/Blokkert.
|
||||
function updateStatusNoteField() {
|
||||
const field = $('f-statusnote-field');
|
||||
if (!state.editingId) { field.style.display = 'none'; $('f-statusnote').value = ''; return; }
|
||||
const val = $('f-stat').value;
|
||||
if (val !== state.editingStatus && (val === 'done' || val === 'blocked')) {
|
||||
$('f-statusnote-label').textContent = val === 'done' ? 'Avslutningskommentar (valgfritt)' : 'Hva blokkerer? (valgfritt)';
|
||||
field.style.display = '';
|
||||
} else {
|
||||
field.style.display = 'none';
|
||||
$('f-statusnote').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function openNew(prefill = {}) {
|
||||
state.editingId = null;
|
||||
state.editingBaseV = null;
|
||||
state.editingStatus = null;
|
||||
$('f-statusnote-field').style.display = 'none';
|
||||
$('f-statusnote').value = '';
|
||||
$('modal-title').textContent = 'Ny oppgave';
|
||||
$('f-title').value = prefill.title || '';
|
||||
$('f-desc').value = '';
|
||||
|
|
@ -870,6 +1132,9 @@ async function saveForm() {
|
|||
let task;
|
||||
if (state.editingId) {
|
||||
data.v = state.editingBaseV;
|
||||
// Statuskommentar (kun synlig når status endres til ferdig/blokkert)
|
||||
const note = $('f-statusnote-field').style.display !== 'none' ? $('f-statusnote').value.trim() : '';
|
||||
if (note) data.comment = note;
|
||||
({ task } = await api('PUT', `/api/tasks/${state.editingId}`, data));
|
||||
toast('Lagret');
|
||||
} else {
|
||||
|
|
@ -897,15 +1162,22 @@ function renderComments(t) {
|
|||
list.innerHTML = '<div class="no-comments">Ingen kommentarer ennå.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = t.comments.map(c => `
|
||||
<div class="comment">
|
||||
list.innerHTML = t.comments.map(c => {
|
||||
const isStatus = c.kind === 'status' && c.status;
|
||||
const kindTag = isStatus
|
||||
? `<span class="comment-kind status-${c.status}">${STATUS_LABELS[c.status] || c.status}</span>`
|
||||
: '';
|
||||
return `
|
||||
<div class="comment${isStatus ? ' comment-status' : ''}">
|
||||
<div class="comment-head">
|
||||
<span class="comment-by ${c.by === state.user ? 'me' : ''}">${escapeHtml(c.by)}</span>
|
||||
${kindTag}
|
||||
<span class="comment-at">${relTime(c.at)}</span>
|
||||
</div>
|
||||
<div class="comment-text">${escapeHtml(c.text)}</div>
|
||||
${c.by === state.user ? `<button class="icon-btn del comment-del" onclick="deleteComment('${t.id}','${c.id}')" title="Slett kommentar">×</button>` : ''}
|
||||
</div>`).join('');
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function sendComment() {
|
||||
|
|
@ -973,6 +1245,12 @@ function bindUI() {
|
|||
// Visning
|
||||
$('view-board').addEventListener('click', () => { state.view = 'board'; savePrefs(); renderView(); });
|
||||
$('view-table').addEventListener('click', () => { state.view = 'table'; savePrefs(); renderView(); });
|
||||
$('view-plan').addEventListener('click', () => { state.view = 'plan'; savePrefs(); renderView(); });
|
||||
$('plan-date').addEventListener('change', e => {
|
||||
const d = e.target.value || todayISO();
|
||||
refreshPlan(d);
|
||||
});
|
||||
bindPlanContainer();
|
||||
|
||||
// Filtre
|
||||
const f = state.filters;
|
||||
|
|
@ -1007,6 +1285,16 @@ function bindUI() {
|
|||
});
|
||||
$('comment-send').addEventListener('click', sendComment);
|
||||
$('comment-input').addEventListener('keydown', e => { if (e.key === 'Enter') sendComment(); });
|
||||
$('f-stat').addEventListener('change', updateStatusNoteField);
|
||||
|
||||
// Statuskommentar-dialog
|
||||
$('status-save').addEventListener('click', () => resolveStatus($('status-comment').value.trim()));
|
||||
$('status-skip').addEventListener('click', () => resolveStatus(''));
|
||||
$('status-modal-close').addEventListener('click', () => resolveStatus(null));
|
||||
$('status-modal').addEventListener('click', e => { if (e.target.id === 'status-modal') resolveStatus(null); });
|
||||
$('status-comment').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); resolveStatus($('status-comment').value.trim()); }
|
||||
});
|
||||
|
||||
// Aktivitets-skuff
|
||||
$('activityBtn').addEventListener('click', () => $('drawer').classList.toggle('open'));
|
||||
|
|
@ -1041,14 +1329,18 @@ function bindUI() {
|
|||
document.addEventListener('keydown', e => {
|
||||
const tag = e.target.tagName;
|
||||
const typing = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
if (e.key === 'Escape') { closeModal(); $('drawer').classList.remove('open'); return; }
|
||||
if (e.key === 'Escape') {
|
||||
if ($('status-modal').classList.contains('open')) { resolveStatus(null); return; }
|
||||
closeModal(); $('drawer').classList.remove('open'); return;
|
||||
}
|
||||
if (typing) return;
|
||||
if (e.key === 'n') { e.preventDefault(); openNew(); }
|
||||
if (e.key === '/') { e.preventDefault(); $('f-search').focus(); }
|
||||
if (e.key === 'v') {
|
||||
state.view = state.view === 'board' ? 'table' : 'board';
|
||||
state.view = VIEWS[(VIEWS.indexOf(state.view) + 1) % VIEWS.length];
|
||||
savePrefs(); renderView();
|
||||
}
|
||||
if (e.key === 'p') { state.view = 'plan'; savePrefs(); renderView(); }
|
||||
if (e.key === 'a') $('drawer').classList.toggle('open');
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue