made installer and seperated stuff into diferent files
This commit is contained in:
685
ProjectKiln/app/js/home/task.js
Normal file
685
ProjectKiln/app/js/home/task.js
Normal file
@@ -0,0 +1,685 @@
|
||||
async function loadTask(taskId, pushHistory = true) {
|
||||
const view = showView('taskView');
|
||||
|
||||
if (!view) return;
|
||||
|
||||
if (pushHistory) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('page', 'home');
|
||||
url.searchParams.set('task', taskId);
|
||||
url.searchParams.delete('project');
|
||||
url.searchParams.delete('tasks');
|
||||
url.searchParams.delete('version');
|
||||
url.searchParams.delete('profile');
|
||||
url.searchParams.delete('admin');
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
const result = await apiGet('/api/task.php', {
|
||||
api: 'TaskInfo',
|
||||
task_id: taskId,
|
||||
_: Date.now()
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
view.innerHTML = '<div class="alert alert-danger">Task not found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
await renderTask(result.task);
|
||||
await loadTaskComments(result.task.id);
|
||||
await ensureProjectOpen(result.task.project);
|
||||
}
|
||||
|
||||
async function renderTask(task) {
|
||||
currentTask = task;
|
||||
|
||||
const [types, priorities, versions, reporter, assignee] = await Promise.all([
|
||||
getTaskTypes(),
|
||||
getTaskPriorities(),
|
||||
getProjectVersions(task.project),
|
||||
getUserInfo(task.reporter),
|
||||
task.assignee ? getUserInfo(task.assignee) : null
|
||||
]);
|
||||
|
||||
const type = types.find((item) => String(item.id) === String(task.type)) ?? null;
|
||||
const priority = priorities.find((item) => String(item.id) === String(task.priority)) ?? null;
|
||||
const fixVersion = versions.find((item) => String(item.id) === String(task.fix_version)) ?? null;
|
||||
|
||||
setEditableText('taskKey', task.id);
|
||||
setEditableText('taskTitle', task.title, task.title);
|
||||
setEditableText('taskDescription', task.description, task.description, 'No description provided.');
|
||||
setText('taskProject', task.project);
|
||||
document.getElementById('taskReporter').innerHTML = renderUser(reporter);
|
||||
setEditableHtml('taskAssignee', renderUser(assignee), task.assignee ?? '', 'Unassigned');
|
||||
setEditableText('taskFixVersion', fixVersion ? fixVersion.name : null, task.fix_version ?? '', 'No fix version');
|
||||
setText('taskCreated', task.created_date);
|
||||
setText('taskUpdated', task.last_changed);
|
||||
|
||||
setEditableHtml('taskType', renderMetaBadge(type), task.type);
|
||||
setEditableHtml('taskPriority', renderMetaBadge(priority), task.priority);
|
||||
renderTaskStatus(task);
|
||||
renderTaskCustomFields(task.custom_fields ?? []);
|
||||
}
|
||||
|
||||
function renderTaskStatus(task) {
|
||||
const slot = document.getElementById('taskStatusSlot');
|
||||
|
||||
if (!slot) return;
|
||||
|
||||
slot.innerHTML = '';
|
||||
|
||||
if (!task.status_state) return;
|
||||
|
||||
const isCurrentAssignee = String(task.assignee ?? '') === String(getCurrentUserId());
|
||||
const canTransition = (canEditTasks() || appFlag('isAdmin') || isCurrentAssignee)
|
||||
&& (task.status_transitions ?? []).length > 0;
|
||||
const statusStyle = `--task-status-color: ${escapeHtml(task.status_state.color ?? '#6c757d')}`;
|
||||
|
||||
if (!canTransition) {
|
||||
slot.innerHTML = `
|
||||
<span class="task-status-badge" style="${statusStyle}">
|
||||
${escapeHtml(task.status_state.name)}
|
||||
</span>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
slot.innerHTML = `
|
||||
<div class="task-status-control">
|
||||
<button class="task-status-badge task-status-button" type="button" style="${statusStyle}" id="taskStatusButton">
|
||||
<span>${escapeHtml(task.status_state.name)}</span>
|
||||
<i class="fa-solid fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="task-status-menu" id="taskStatusMenu" hidden>
|
||||
${(task.status_transitions ?? []).map((transition) => `
|
||||
<button type="button" data-task-transition="${escapeHtml(transition.id)}">
|
||||
<span>${escapeHtml(transition.action_name)}</span>
|
||||
<small>${escapeHtml(transition.to_state?.name ?? '')}</small>
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTaskCustomFields(fields) {
|
||||
const panel = document.getElementById('taskCustomFieldsPanel');
|
||||
const list = document.getElementById('taskCustomFieldList');
|
||||
|
||||
if (!panel || !list) return;
|
||||
|
||||
const canEdit = canEditTasks();
|
||||
|
||||
panel.hidden = fields.length === 0;
|
||||
list.innerHTML = fields.map((field) => `
|
||||
<div class="task-custom-field-row">
|
||||
<span>${escapeHtml(field.name)}</span>
|
||||
<strong
|
||||
class="task-custom-field-value ${field.raw_value ? '' : 'is-empty'} ${canEdit ? '' : 'is-readonly'}"
|
||||
${canEdit ? 'tabindex="0"' : 'aria-disabled="true"'}
|
||||
data-custom-field-id="${escapeHtml(field.id)}"
|
||||
data-custom-field-type="${escapeHtml(field.type)}"
|
||||
data-current-value="${escapeHtml(field.raw_value ?? '')}"
|
||||
>${field.raw_value ? escapeHtml(field.raw_value) : 'No value'}</strong>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadTaskComments(taskId) {
|
||||
const list = document.getElementById('taskCommentList');
|
||||
const empty = document.getElementById('taskCommentEmpty');
|
||||
|
||||
if (!list || !empty) return;
|
||||
|
||||
list.innerHTML = '';
|
||||
empty.hidden = true;
|
||||
currentTaskComments = [];
|
||||
setText('taskCommentStatus', '');
|
||||
|
||||
const result = await apiGet('/api/task.php', {
|
||||
api: 'ListComments',
|
||||
task_id: taskId,
|
||||
_: Date.now()
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
list.innerHTML = '<div class="alert alert-danger mb-0">Could not load comments.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
currentTaskComments = result.comments ?? [];
|
||||
renderTaskComments();
|
||||
}
|
||||
|
||||
function renderTaskComments() {
|
||||
const list = document.getElementById('taskCommentList');
|
||||
const empty = document.getElementById('taskCommentEmpty');
|
||||
|
||||
if (!list || !empty) return;
|
||||
|
||||
list.innerHTML = '';
|
||||
empty.hidden = currentTaskComments.length > 0;
|
||||
|
||||
const commentsByParent = groupCommentsByParent(currentTaskComments);
|
||||
const renderComment = (comment, depth = 0) => {
|
||||
const user = {
|
||||
id: comment.commenter,
|
||||
name: comment.commenter_name,
|
||||
email: comment.commenter_email,
|
||||
picture: comment.commenter_picture
|
||||
};
|
||||
const article = document.createElement('article');
|
||||
const canManageComment = String(comment.commenter) === String(getCurrentUserId());
|
||||
const manageActions = canManageComment
|
||||
? `
|
||||
<button class="task-comment-action" type="button" data-comment-edit="${escapeHtml(comment.id)}">Edit</button>
|
||||
<button class="task-comment-action text-danger" type="button" data-comment-delete="${escapeHtml(comment.id)}">Delete</button>
|
||||
`
|
||||
: '';
|
||||
article.className = `task-comment${comment.response_to ? ' is-reply' : ''}`;
|
||||
article.style.setProperty('--comment-depth', String(Math.min(depth, 8)));
|
||||
article.dataset.commentId = comment.id;
|
||||
article.dataset.commentText = comment.comment;
|
||||
article.innerHTML = `
|
||||
<div class="task-comment-header">
|
||||
${renderUser(user)}
|
||||
<span class="task-comment-id">#${escapeHtml(comment.id)}</span>
|
||||
</div>
|
||||
<div class="task-comment-body">${escapeHtml(comment.comment)}</div>
|
||||
<div class="task-comment-action-row">
|
||||
<button class="task-comment-action" type="button" data-comment-reply="${escapeHtml(comment.id)}">Reply</button>
|
||||
${manageActions}
|
||||
</div>
|
||||
<div class="task-comment-reply-slot"></div>
|
||||
<div class="task-comment-edit-slot"></div>
|
||||
`;
|
||||
|
||||
list.appendChild(article);
|
||||
|
||||
const children = commentsByParent.get(Number(comment.id)) ?? [];
|
||||
children.forEach((child) => renderComment(child, depth + 1));
|
||||
};
|
||||
|
||||
const rootComments = commentsByParent.get(null) ?? [];
|
||||
rootComments.forEach((comment) => renderComment(comment));
|
||||
}
|
||||
|
||||
function groupCommentsByParent(comments) {
|
||||
const knownIds = new Set(comments.map((comment) => Number(comment.id)));
|
||||
const groups = new Map([[null, []]]);
|
||||
|
||||
[...comments]
|
||||
.sort((first, second) => Number(first.id) - Number(second.id))
|
||||
.forEach((comment) => {
|
||||
const parentId = comment.response_to && knownIds.has(Number(comment.response_to))
|
||||
? Number(comment.response_to)
|
||||
: null;
|
||||
|
||||
if (!groups.has(parentId)) {
|
||||
groups.set(parentId, []);
|
||||
}
|
||||
|
||||
groups.get(parentId).push(comment);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
async function submitTaskComment(comment, responseTo = null) {
|
||||
if (!currentTask) return;
|
||||
|
||||
const result = await apiPost('/api/task.php', {
|
||||
api: 'CreateComment',
|
||||
task_id: currentTask.id
|
||||
}, {
|
||||
comment,
|
||||
response_to: responseTo
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Could not save comment.');
|
||||
}
|
||||
|
||||
await loadTaskComments(currentTask.id);
|
||||
}
|
||||
|
||||
async function updateTaskComment(commentId, comment) {
|
||||
if (!currentTask) return;
|
||||
|
||||
const result = await apiPost('/api/task.php', {
|
||||
api: 'EditComment',
|
||||
task_id: currentTask.id,
|
||||
comment_id: commentId
|
||||
}, {
|
||||
comment
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Could not update comment.');
|
||||
}
|
||||
|
||||
await loadTaskComments(currentTask.id);
|
||||
}
|
||||
|
||||
async function deleteTaskComment(commentId) {
|
||||
if (!currentTask) return;
|
||||
|
||||
const result = await apiPost('/api/task.php', {
|
||||
api: 'DeleteComment',
|
||||
task_id: currentTask.id,
|
||||
comment_id: commentId
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Could not delete comment.');
|
||||
}
|
||||
|
||||
await loadTaskComments(currentTask.id);
|
||||
}
|
||||
|
||||
async function openTaskCreatePopup() {
|
||||
const form = document.getElementById('createTaskForm');
|
||||
|
||||
if (!form) return;
|
||||
|
||||
await populateTaskOptionSelects();
|
||||
await populateTaskProjectSelect();
|
||||
await populateTaskRelationSelects();
|
||||
|
||||
resetTaskPopup();
|
||||
|
||||
openPopup('createTask');
|
||||
}
|
||||
|
||||
async function openTaskEditPopup() {
|
||||
if (!currentTask) return;
|
||||
|
||||
const form = document.getElementById('createTaskForm');
|
||||
|
||||
if (!form) return;
|
||||
|
||||
await populateTaskOptionSelects();
|
||||
await populateTaskProjectSelect(currentTask.project);
|
||||
await populateTaskRelationSelects(currentTask.project, {
|
||||
assignee: currentTask.assignee ?? '',
|
||||
fix_version: currentTask.fix_version ?? ''
|
||||
});
|
||||
|
||||
form.reset();
|
||||
form.dataset.mode = 'edit';
|
||||
document.getElementById('taskPopupTitle').textContent = `Edit ${currentTask.id}`;
|
||||
document.getElementById('taskPopupSubmit').textContent = 'Update Task';
|
||||
document.getElementById('taskFormTaskId').value = currentTask.id;
|
||||
document.getElementById('taskFormProjectId').value = currentTask.project;
|
||||
document.getElementById('taskFormProjectId').disabled = true;
|
||||
document.getElementById('taskFormTitle').value = currentTask.title ?? '';
|
||||
document.getElementById('taskFormDescription').value = currentTask.description ?? '';
|
||||
document.getElementById('createTaskType').value = currentTask.type ?? '';
|
||||
document.getElementById('createTaskPriority').value = currentTask.priority ?? '';
|
||||
document.getElementById('taskFormFixVersion').value = currentTask.fix_version ?? '';
|
||||
document.getElementById('taskFormAssignee').value = currentTask.assignee ?? '';
|
||||
|
||||
openPopup('createTask');
|
||||
}
|
||||
|
||||
function initTaskInlineEditing() {
|
||||
if (!canEditTasks()) return;
|
||||
|
||||
document.querySelectorAll('.task-editable').forEach((element) => {
|
||||
if (element.dataset.inlineReady === 'true') return;
|
||||
|
||||
element.dataset.inlineReady = 'true';
|
||||
element.title = 'Click to edit';
|
||||
|
||||
element.addEventListener('click', () => openTaskInlineEditor(element));
|
||||
element.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
openTaskInlineEditor(element);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function openTaskInlineEditor(element) {
|
||||
if (!canEditTasks() || !currentTask || element.querySelector('.task-inline-form')) return;
|
||||
|
||||
const field = element.dataset.taskField;
|
||||
const currentValue = element.dataset.currentValue ?? '';
|
||||
const originalHtml = element.innerHTML;
|
||||
const originalClasses = Array.from(element.classList);
|
||||
|
||||
element.classList.remove('is-empty');
|
||||
element.innerHTML = '';
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.className = 'task-inline-form';
|
||||
|
||||
const input = await createTaskInlineInput(field, currentValue);
|
||||
form.appendChild(input);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'task-inline-actions';
|
||||
actions.innerHTML = `
|
||||
<button class="btn btn-sm btn-primary" type="submit">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-inline-cancel>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
if (input.tagName !== 'SELECT') {
|
||||
form.appendChild(actions);
|
||||
}
|
||||
|
||||
element.appendChild(form);
|
||||
input.focus();
|
||||
|
||||
if (input.select && input.tagName !== 'SELECT') {
|
||||
input.select();
|
||||
}
|
||||
|
||||
let editorClosed = false;
|
||||
|
||||
const restore = () => {
|
||||
if (editorClosed) return;
|
||||
|
||||
editorClosed = true;
|
||||
element.innerHTML = originalHtml;
|
||||
element.className = originalClasses.join(' ');
|
||||
};
|
||||
|
||||
let saving = false;
|
||||
|
||||
const save = async () => {
|
||||
if (saving) return;
|
||||
|
||||
const nextValue = input.value;
|
||||
|
||||
if (String(nextValue) === String(currentValue)) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
element.classList.add('is-saving');
|
||||
|
||||
const result = await apiPost('/api/task.php', {
|
||||
api: 'Edit',
|
||||
task_id: currentTask.id
|
||||
}, {
|
||||
[field]: nextValue
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not update task.');
|
||||
saving = false;
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
editorClosed = true;
|
||||
element.classList.remove('is-saving');
|
||||
currentTask = {
|
||||
...currentTask,
|
||||
[field]: nextValue === '' ? null : nextValue
|
||||
};
|
||||
|
||||
if (field === 'type') {
|
||||
await loadTask(currentTask.id, false);
|
||||
return;
|
||||
}
|
||||
|
||||
await renderTask(currentTask);
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
await save();
|
||||
});
|
||||
|
||||
form.querySelector('[data-inline-cancel]')?.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
restore();
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', async (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
restore();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && input.tagName === 'TEXTAREA' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
await save();
|
||||
}
|
||||
});
|
||||
|
||||
if (input.tagName === 'SELECT') {
|
||||
input.addEventListener('change', save);
|
||||
input.addEventListener('blur', () => {
|
||||
window.setTimeout(() => {
|
||||
if (editorClosed) return;
|
||||
if (element.classList.contains('is-saving')) return;
|
||||
if (element.contains(document.activeElement)) return;
|
||||
restore();
|
||||
}, 120);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createTaskInlineInput(field, currentValue) {
|
||||
if (field === 'description') {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'form-control task-inline-input';
|
||||
textarea.rows = 5;
|
||||
textarea.value = currentValue;
|
||||
return textarea;
|
||||
}
|
||||
|
||||
if (field === 'type') {
|
||||
const select = document.createElement('select');
|
||||
select.className = 'form-select task-inline-select';
|
||||
populateSelect(select, await getTaskTypes(), {
|
||||
includeEmpty: false,
|
||||
selectedValue: currentValue
|
||||
});
|
||||
return select;
|
||||
}
|
||||
|
||||
if (field === 'priority') {
|
||||
const select = document.createElement('select');
|
||||
select.className = 'form-select task-inline-select';
|
||||
populateSelect(select, await getTaskPriorities(), {
|
||||
includeEmpty: false,
|
||||
selectedValue: currentValue
|
||||
});
|
||||
return select;
|
||||
}
|
||||
|
||||
if (field === 'fix_version') {
|
||||
const select = document.createElement('select');
|
||||
select.className = 'form-select task-inline-select';
|
||||
populateSelect(select, await getProjectVersions(currentTask.project), {
|
||||
placeholder: 'No fix version',
|
||||
selectedValue: currentValue
|
||||
});
|
||||
return select;
|
||||
}
|
||||
|
||||
if (field === 'assignee') {
|
||||
const select = document.createElement('select');
|
||||
select.className = 'form-select task-inline-select';
|
||||
populateSelect(select, await getUsers(), {
|
||||
placeholder: 'Unassigned',
|
||||
selectedValue: currentValue
|
||||
});
|
||||
return select;
|
||||
}
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = 'form-control task-inline-input';
|
||||
input.type = 'text';
|
||||
input.value = currentValue;
|
||||
input.required = field === 'title';
|
||||
return input;
|
||||
}
|
||||
|
||||
function createCustomFieldInput(type, currentValue) {
|
||||
if (type === 'boolean') {
|
||||
const select = document.createElement('select');
|
||||
select.className = 'form-select task-inline-select';
|
||||
[
|
||||
{ id: '', name: 'No value' },
|
||||
{ id: 'true', name: 'True' },
|
||||
{ id: 'false', name: 'False' }
|
||||
].forEach((item) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = item.name;
|
||||
option.selected = String(item.id) === String(currentValue);
|
||||
select.appendChild(option);
|
||||
});
|
||||
return select;
|
||||
}
|
||||
|
||||
if (type === 'text' || type === 'json') {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'form-control task-inline-input';
|
||||
textarea.rows = type === 'json' ? 4 : 3;
|
||||
textarea.value = currentValue;
|
||||
return textarea;
|
||||
}
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = 'form-control task-inline-input';
|
||||
input.type = type === 'date' ? 'date' : 'text';
|
||||
input.inputMode = ['int', 'float'].includes(type) ? 'decimal' : '';
|
||||
input.value = currentValue;
|
||||
return input;
|
||||
}
|
||||
|
||||
async function openCustomFieldInlineEditor(element) {
|
||||
if (!canEditTasks() || !currentTask || element.querySelector('.task-inline-form')) return;
|
||||
|
||||
const fieldId = element.dataset.customFieldId;
|
||||
const fieldType = element.dataset.customFieldType ?? 'string';
|
||||
const currentValue = element.dataset.currentValue ?? '';
|
||||
const originalHtml = element.innerHTML;
|
||||
const originalClasses = Array.from(element.classList);
|
||||
|
||||
element.classList.remove('is-empty');
|
||||
element.innerHTML = '';
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.className = 'task-inline-form';
|
||||
|
||||
const input = createCustomFieldInput(fieldType, currentValue);
|
||||
form.appendChild(input);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'task-inline-actions';
|
||||
actions.innerHTML = `
|
||||
<button class="btn btn-sm btn-primary" type="submit">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-inline-cancel>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
`;
|
||||
form.appendChild(actions);
|
||||
|
||||
element.appendChild(form);
|
||||
input.focus();
|
||||
|
||||
if (input.select && input.tagName !== 'SELECT') {
|
||||
input.select();
|
||||
}
|
||||
|
||||
let closed = false;
|
||||
const restore = () => {
|
||||
if (closed) return;
|
||||
|
||||
closed = true;
|
||||
element.innerHTML = originalHtml;
|
||||
element.className = originalClasses.join(' ');
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
const nextValue = input.value;
|
||||
|
||||
if (String(nextValue) === String(currentValue)) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
element.classList.add('is-saving');
|
||||
|
||||
const result = await apiPost('/api/task.php', {
|
||||
api: 'SetCustomFieldValue',
|
||||
task_id: currentTask.id
|
||||
}, {
|
||||
field_id: fieldId,
|
||||
value: nextValue
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not update custom field.');
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
closed = true;
|
||||
await loadTask(currentTask.id, false);
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
await save();
|
||||
});
|
||||
|
||||
form.querySelector('[data-inline-cancel]')?.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
restore();
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', async (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
restore();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && input.tagName !== 'TEXTAREA') {
|
||||
event.preventDefault();
|
||||
await save();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && input.tagName === 'TEXTAREA' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
await save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function transitionCurrentTask(transitionId) {
|
||||
if (!currentTask || !transitionId) return;
|
||||
|
||||
const result = await apiPost('/api/task.php', {
|
||||
api: 'TransitionStatus',
|
||||
task_id: currentTask.id
|
||||
}, {
|
||||
transition_id: transitionId
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not transition task.');
|
||||
return;
|
||||
}
|
||||
|
||||
await loadTask(currentTask.id, false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user