Files

525 lines
14 KiB
JavaScript

async function apiGet(path, params = {}) {
const url = new URL(path, window.location.origin);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
url.searchParams.set(key, value);
}
});
const response = await fetch(url.toString(), {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache'
}
});
return response.json();
}
async function apiPost(path, params = {}, body = {}) {
const url = new URL(path, window.location.origin);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
url.searchParams.set(key, value);
}
});
const response = await fetch(url.toString(), {
method: 'POST',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
},
body: JSON.stringify(body)
});
const text = await response.text();
try {
return JSON.parse(text);
} catch (error) {
console.error('API did not return JSON:', text);
return {
success: false,
error: 'API did not return JSON. Check console for PHP error.'
};
}
}
async function apiPostForm(path, params = {}, formData) {
const url = new URL(path, window.location.origin);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
url.searchParams.set(key, value);
}
});
const response = await fetch(url.toString(), {
method: 'POST',
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache'
},
body: formData
});
const text = await response.text();
try {
return JSON.parse(text);
} catch (error) {
console.error('API did not return JSON:', text);
return {
success: false,
error: 'API did not return JSON. Check console for PHP error.'
};
}
}
let currentTask = null;
let currentVersion = null;
let currentProject = null;
let currentProfileUser = null;
let currentVersionTasks = [];
let currentProjectTasks = [];
let currentProjectTaskProject = null;
let currentTaskComments = [];
let workflowData = null;
let workflowGraph = null;
let selectedWorkflowTaskType = null;
let adminOptionsData = null;
let selectedAdminTaskType = null;
let selectedAdminUser = null;
let projectTreePromise = null;
const versionTaskSort = {
field: 'id',
direction: 'asc'
};
const projectTaskSort = {
field: 'id',
direction: 'asc'
};
const taskTablePageSize = 100;
let versionTaskPagination = null;
let projectTaskPagination = null;
const themeStyles = {
white: 'app/css/white_mode.css',
dark: 'app/css/dark_mode.css',
purple: 'app/css/purple_mode.css',
green: 'app/css/green_mode.css',
beige: 'app/css/beige_mode.css'
};
const customFieldTypes = [
{ id: 'string', name: 'String' },
{ id: 'int', name: 'Int' },
{ id: 'float', name: 'Float' },
{ id: 'date', name: 'Date' },
{ id: 'boolean', name: 'Boolean' },
{ id: 'text', name: 'Text' },
{ id: 'json', name: 'JSON' }
];
const taskLookupCache = {
types: null,
priorities: null,
users: null,
versionsByProject: new Map(),
projectsById: new Map(),
projectBlocksById: new Map()
};
function appFlag(name) {
return document.querySelector('.kiln-app')?.dataset[name] === '1';
}
function canEditTasks() {
return appFlag('canEditTasks');
}
function canEditVersions() {
return appFlag('canEditVersions');
}
function canEditProjects() {
return appFlag('canEditProjects');
}
function applyPermissionVisibility() {
const visibility = [
['taskEditButton', canEditTasks()],
['versionEditButton', canEditVersions()],
['projectEditButton', canEditProjects()]
];
visibility.forEach(([id, allowed]) => {
const element = document.getElementById(id);
if (element) element.hidden = !allowed;
});
[
['.task-editable', canEditTasks()],
['.version-editable', canEditVersions()],
['.project-editable', canEditProjects()]
].forEach(([selector, allowed]) => {
document.querySelectorAll(selector).forEach((element) => {
element.classList.toggle('is-readonly', !allowed);
if (allowed) {
element.setAttribute('tabindex', '0');
element.removeAttribute('aria-disabled');
return;
}
element.removeAttribute('title');
element.removeAttribute('tabindex');
element.setAttribute('aria-disabled', 'true');
});
});
}
function showView(viewId) {
document.querySelectorAll('#viewer > section').forEach((section) => {
section.hidden = true;
});
const view = document.getElementById(viewId);
if (!view) {
console.error(`Missing view: ${viewId}`);
return null;
}
view.hidden = false;
return view;
}
function setText(id, value) {
const element = document.getElementById(id);
if (element) {
element.textContent = value ?? '';
}
}
function setEditableText(id, displayValue, rawValue = displayValue, emptyText = '-') {
const element = document.getElementById(id);
if (!element) return;
const hasValue = displayValue !== null && displayValue !== undefined && displayValue !== '';
element.textContent = hasValue ? displayValue : emptyText;
element.dataset.currentValue = rawValue ?? '';
element.classList.remove('is-saving');
element.classList.toggle('is-empty', !hasValue);
}
function setEditableHtml(id, displayHtml, rawValue, emptyText = '-') {
const element = document.getElementById(id);
if (!element) return;
const hasValue = displayHtml !== null && displayHtml !== undefined && displayHtml !== '-';
element.innerHTML = hasValue ? displayHtml : emptyText;
element.dataset.currentValue = rawValue ?? '';
element.classList.remove('is-saving');
element.classList.toggle('is-empty', !hasValue || rawValue === '' || rawValue === null);
}
function setVersionEditableText(id, displayValue, rawValue = displayValue, emptyText = '-') {
const element = document.getElementById(id);
if (!element) return;
const hasValue = displayValue !== null && displayValue !== undefined && displayValue !== '';
element.textContent = hasValue ? displayValue : emptyText;
element.dataset.currentValue = rawValue ?? '';
element.classList.remove('is-saving');
element.classList.toggle('is-empty', !hasValue);
}
function renderMetaBadge(item) {
if (!item) {
return '-';
}
const icon = item.icon
? `<img src="${item.icon}" alt="" class="meta-icon">`
: '';
return `
<span class="meta-badge">
${icon}
<span>${escapeHtml(item.name)}</span>
</span>
`;
}
async function getTaskTypes() {
if (taskLookupCache.types) {
return taskLookupCache.types;
}
const result = await apiGet('/api/task.php', {
api: 'ListTypes'
});
taskLookupCache.types = result.success ? result.types : [];
return taskLookupCache.types;
}
async function getTaskPriorities() {
if (taskLookupCache.priorities) {
return taskLookupCache.priorities;
}
const result = await apiGet('/api/task.php', {
api: 'ListPriorities'
});
taskLookupCache.priorities = result.success ? result.priorities : [];
return taskLookupCache.priorities;
}
async function getUsers() {
if (taskLookupCache.users) {
return taskLookupCache.users;
}
const result = await apiGet('/api/user.php', {
api: 'ListUsers'
});
taskLookupCache.users = result.success ? result.users : [];
return taskLookupCache.users;
}
async function getProjectVersions(projectId) {
if (taskLookupCache.versionsByProject.has(projectId)) {
return taskLookupCache.versionsByProject.get(projectId);
}
const result = await apiGet('/api/version.php', {
api: 'ListVersions',
project_id: projectId
});
if (!result.success) {
taskLookupCache.versionsByProject.set(projectId, []);
return [];
}
const versions = [];
for (const versionId of result.versions) {
const versionInfo = await apiGet('/api/version.php', {
api: 'VersionInfo',
version_id: versionId
});
if (versionInfo.success && versionInfo.version) {
versions.push(versionInfo.version);
}
}
taskLookupCache.versionsByProject.set(projectId, versions);
return versions;
}
async function populateTaskOptionSelects() {
const [types, priorities] = await Promise.all([
getTaskTypes(),
getTaskPriorities()
]);
populateSelect(document.getElementById('createTaskType'), types, {
placeholder: 'Select type'
});
populateSelect(document.getElementById('createTaskPriority'), priorities, {
placeholder: 'Select priority'
});
}
async function populateTaskProjectSelect(selectedProjectId = '') {
const result = await apiGet('/api/project.php', {
api: 'ListProjects'
});
const projects = [];
if (result.success) {
for (const projectId of result.projects) {
const project = await getProjectInfo(projectId);
projects.push({
id: projectId,
name: project?.name ?? projectId
});
}
}
populateSelect(document.getElementById('taskFormProjectId'), projects, {
placeholder: 'Select project',
selectedValue: selectedProjectId
});
}
async function populateTaskRelationSelects(projectId = '', selected = {}) {
const users = await getUsers();
populateSelect(document.getElementById('taskFormAssignee'), users, {
placeholder: 'Unassigned',
selectedValue: selected.assignee ?? ''
});
const versions = projectId ? await getProjectVersions(projectId.toUpperCase()) : [];
populateSelect(document.getElementById('taskFormFixVersion'), versions, {
placeholder: 'No fix version',
selectedValue: selected.fix_version ?? ''
});
}
function resetTaskPopup() {
const form = document.getElementById('createTaskForm');
if (!form) return;
form.reset();
form.dataset.mode = 'create';
document.getElementById('taskPopupTitle').textContent = 'Create Task';
document.getElementById('taskPopupSubmit').textContent = 'Create Task';
document.getElementById('taskFormTaskId').value = '';
document.getElementById('taskFormProjectId').disabled = false;
}
function resetVersionPopup() {
const form = document.getElementById('createVersionForm');
if (!form) return;
form.reset();
form.dataset.mode = 'create';
document.getElementById('versionPopupTitle').textContent = 'Create Version';
document.getElementById('versionPopupSubmit').textContent = 'Create Version';
document.getElementById('versionFormVersionId').value = '';
document.getElementById('versionFormProjectId').value = '';
}
function populateSelect(select, options, settings = {}) {
if (!select) return;
const placeholder = settings.placeholder ?? 'Select value';
const includeEmpty = settings.includeEmpty ?? true;
const selectedValue = settings.selectedValue ?? '';
select.innerHTML = '';
if (includeEmpty) {
const option = document.createElement('option');
option.value = '';
option.textContent = placeholder;
select.appendChild(option);
}
options.forEach((item) => {
const option = document.createElement('option');
option.value = item.id;
option.textContent = item.name ?? item.email ?? item.id;
option.selected = String(option.value) === String(selectedValue);
select.appendChild(option);
});
}
async function getUserInfo(userId) {
if (!userId || userId === '-') {
return null;
}
const result = await apiGet('/api/user.php', {
api: 'UserInfo',
user_id: userId
});
return result.success ? result.user : null;
}
function renderUser(user) {
if (!user) {
return '-';
}
const avatar = user.picture
? `<img src="${user.picture}" alt="" class="user-avatar">`
: `
<span class="user-avatar user-avatar-fallback">
<i class="fa-solid fa-user"></i>
</span>
`;
return `
<span class="meta-user">
${avatar}
<span>${escapeHtml(user.name)}</span>
</span>
`;
}
function getCurrentUserId() {
return document.querySelector('.kiln-app')?.dataset.currentUserId ?? '';
}
function updateAccountProfile(user) {
setText('accountName', user.name ?? '');
setText('accountEmail', user.email ?? '');
const accountButton = document.querySelector('.kiln-account-btn');
const accountName = document.getElementById('accountName');
if (!accountButton || !accountName) return;
document.getElementById('accountAvatarImage')?.remove();
document.getElementById('accountAvatarFallback')?.remove();
const avatar = document.createElement(user.picture ? 'img' : 'span');
avatar.id = user.picture ? 'accountAvatarImage' : 'accountAvatarFallback';
avatar.className = user.picture ? 'user-avatar' : 'user-avatar user-avatar-fallback';
if (user.picture) {
avatar.src = user.picture;
avatar.alt = '';
} else {
avatar.innerHTML = '<i class="fa-solid fa-user"></i>';
}
accountButton.insertBefore(avatar, accountName);
}
function applyTheme(theme) {
const stylesheet = document.getElementById('themeStylesheet');
if (!stylesheet || !themeStyles[theme]) return;
stylesheet.href = themeStyles[theme];
stylesheet.dataset.theme = theme;
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}