398 lines
12 KiB
JavaScript
398 lines
12 KiB
JavaScript
async function loadProjectTree() {
|
|
const tree = document.getElementById('projectTree');
|
|
|
|
if (!tree) return;
|
|
|
|
tree.innerHTML = '<div class="kiln-tree-muted">Loading projects...</div>';
|
|
|
|
const result = await apiGet('/api/project.php', {
|
|
api: 'ListProjects'
|
|
});
|
|
|
|
if (!result.success) {
|
|
tree.innerHTML = '<div class="text-danger">Could not load projects.</div>';
|
|
return;
|
|
}
|
|
|
|
if (!result.projects.length) {
|
|
tree.innerHTML = '<div class="kiln-tree-muted">No projects yet.</div>';
|
|
return;
|
|
}
|
|
|
|
tree.innerHTML = '';
|
|
taskLookupCache.projectBlocksById.clear();
|
|
|
|
for (const projectId of result.projects) {
|
|
const project = await getProjectInfo(projectId);
|
|
const projectBlock = document.createElement('div');
|
|
projectBlock.dataset.projectId = projectId;
|
|
|
|
const projectButton = document.createElement('button');
|
|
projectButton.className = 'kiln-tree-project';
|
|
projectButton.innerHTML = `
|
|
<i class="fa-solid fa-caret-right me-1" data-project-caret></i>
|
|
<span>${escapeHtml(project?.name ?? projectId)}</span>
|
|
`;
|
|
projectButton.addEventListener('click', (event) => {
|
|
if (event.target.closest('[data-project-caret]')) {
|
|
toggleProject(projectId, projectBlock);
|
|
return;
|
|
}
|
|
|
|
loadProject(projectId);
|
|
|
|
if (!projectBlock.querySelector('.kiln-tree-group')) {
|
|
toggleProject(projectId, projectBlock, true);
|
|
}
|
|
});
|
|
|
|
projectBlock.appendChild(projectButton);
|
|
tree.appendChild(projectBlock);
|
|
taskLookupCache.projectBlocksById.set(projectId, projectBlock);
|
|
}
|
|
|
|
if (currentTask?.project) {
|
|
const projectBlock = taskLookupCache.projectBlocksById.get(currentTask.project);
|
|
|
|
if (projectBlock && !projectBlock.querySelector('.kiln-tree-group')) {
|
|
await toggleProject(currentTask.project, projectBlock, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getProjectInfo(projectId) {
|
|
if (taskLookupCache.projectsById.has(projectId)) {
|
|
return taskLookupCache.projectsById.get(projectId);
|
|
}
|
|
|
|
const result = await apiGet('/api/project.php', {
|
|
api: 'ProjectInfo',
|
|
project_id: projectId
|
|
});
|
|
|
|
const project = result.success ? result.project : null;
|
|
taskLookupCache.projectsById.set(projectId, project);
|
|
return project;
|
|
}
|
|
|
|
async function ensureProjectOpen(projectId) {
|
|
await projectTreePromise;
|
|
|
|
const projectBlock = taskLookupCache.projectBlocksById.get(projectId);
|
|
|
|
if (!projectBlock || projectBlock.querySelector('.kiln-tree-group')) {
|
|
return;
|
|
}
|
|
|
|
await toggleProject(projectId, projectBlock, true);
|
|
}
|
|
|
|
async function toggleProject(projectId, projectBlock, forceOpen = false) {
|
|
let group = projectBlock.querySelector('.kiln-tree-group');
|
|
|
|
if (group) {
|
|
if (forceOpen) return;
|
|
|
|
group.remove();
|
|
setProjectCaret(projectBlock, false);
|
|
return;
|
|
}
|
|
|
|
setProjectCaret(projectBlock, true);
|
|
|
|
group = document.createElement('div');
|
|
group.className = 'kiln-tree-group';
|
|
group.innerHTML = '<div class="kiln-tree-muted">Loading...</div>';
|
|
projectBlock.appendChild(group);
|
|
|
|
const versions = await apiGet('/api/version.php', {
|
|
api: 'ListVersions',
|
|
project_id: projectId
|
|
});
|
|
|
|
group.innerHTML = '';
|
|
|
|
const versionGroup = document.createElement('div');
|
|
versionGroup.className = 'kiln-tree-version-group';
|
|
group.appendChild(versionGroup);
|
|
|
|
const versionLabel = document.createElement('div');
|
|
versionLabel.className = 'kiln-tree-label';
|
|
versionLabel.textContent = 'Versions';
|
|
versionGroup.appendChild(versionLabel);
|
|
|
|
if (document.querySelector('.kiln-app')?.dataset.canCreateVersions === '1') {
|
|
const createVersion = document.createElement('button');
|
|
createVersion.className = 'kiln-tree-version';
|
|
createVersion.innerHTML = '<i class="fa-solid fa-plus me-1"></i>Create new version';
|
|
createVersion.addEventListener('click', () => openVersionCreatePopup(projectId));
|
|
versionGroup.appendChild(createVersion);
|
|
}
|
|
|
|
if (versions.success && versions.versions.length) {
|
|
for (const versionId of versions.versions) {
|
|
const versionInfo = await apiGet('/api/version.php', {
|
|
api: 'VersionInfo',
|
|
version_id: versionId
|
|
});
|
|
|
|
const versionButton = document.createElement('button');
|
|
versionButton.className = 'kiln-tree-version';
|
|
|
|
if (versionInfo.success && versionInfo.version) {
|
|
versionButton.textContent = versionInfo.version.name;
|
|
} else {
|
|
versionButton.textContent = `Version ${versionId}`;
|
|
}
|
|
|
|
versionButton.addEventListener('click', () => loadVersion(versionId));
|
|
versionGroup.appendChild(versionButton);
|
|
}
|
|
}
|
|
|
|
const showTasksButton = document.createElement('button');
|
|
showTasksButton.className = 'kiln-tree-version';
|
|
showTasksButton.innerHTML = '<i class="fa-solid fa-list-check me-1"></i>Show all tasks';
|
|
showTasksButton.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
loadProjectTasks(projectId);
|
|
});
|
|
|
|
group.appendChild(showTasksButton);
|
|
}
|
|
|
|
function setProjectCaret(projectBlock, isOpen) {
|
|
const caret = projectBlock.querySelector('[data-project-caret]');
|
|
|
|
if (!caret) return;
|
|
|
|
caret.classList.toggle('fa-caret-right', !isOpen);
|
|
caret.classList.toggle('fa-caret-down', isOpen);
|
|
}
|
|
|
|
async function loadProject(projectId, pushHistory = true) {
|
|
const view = showView('projectView');
|
|
|
|
if (!view) return;
|
|
|
|
if (pushHistory) {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set('page', 'home');
|
|
url.searchParams.set('project', projectId);
|
|
url.searchParams.delete('tasks');
|
|
url.searchParams.delete('task');
|
|
url.searchParams.delete('version');
|
|
url.searchParams.delete('profile');
|
|
url.searchParams.delete('admin');
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
|
|
const result = await apiGet('/api/project.php', {
|
|
api: 'ProjectInfo',
|
|
project_id: projectId,
|
|
_: Date.now()
|
|
});
|
|
|
|
if (!result.success) {
|
|
view.innerHTML = '<div class="alert alert-danger">Project not found.</div>';
|
|
return;
|
|
}
|
|
|
|
await renderProject(result.project);
|
|
await ensureProjectOpen(result.project.id);
|
|
}
|
|
|
|
async function renderProject(project) {
|
|
currentProject = project;
|
|
taskLookupCache.projectsById.set(project.id, project);
|
|
|
|
const owner = project.owner ? await getUserInfo(project.owner) : null;
|
|
|
|
setVersionEditableText('projectKey', project.id);
|
|
setVersionEditableText('projectName', project.name, project.name);
|
|
setVersionEditableText('projectInlineName', project.name, project.name);
|
|
setText('projectId', project.id);
|
|
setEditableHtml('projectOwner', renderUser(owner), project.owner ?? '', 'No owner');
|
|
setText('projectCreated', project.created_date || '-');
|
|
}
|
|
|
|
async function populateProjectOwnerSelect(selectedOwner = '') {
|
|
populateSelect(document.getElementById('projectFormOwner'), await getUsers(), {
|
|
includeEmpty: false,
|
|
selectedValue: selectedOwner
|
|
});
|
|
}
|
|
|
|
function resetProjectPopup() {
|
|
const form = document.getElementById('createProjectForm');
|
|
|
|
if (!form) return;
|
|
|
|
form.reset();
|
|
form.dataset.mode = 'create';
|
|
document.getElementById('projectPopupTitle').textContent = 'Create Project';
|
|
document.getElementById('projectPopupSubmit').textContent = 'Create Project';
|
|
document.getElementById('projectFormId').disabled = false;
|
|
document.getElementById('projectFormId').value = '';
|
|
document.getElementById('projectFormName').value = '';
|
|
}
|
|
|
|
async function openProjectCreatePopup() {
|
|
resetProjectPopup();
|
|
await populateProjectOwnerSelect(getCurrentUserId());
|
|
openPopup('createProject');
|
|
}
|
|
|
|
async function openProjectEditPopup() {
|
|
if (!currentProject) return;
|
|
|
|
resetProjectPopup();
|
|
|
|
const form = document.getElementById('createProjectForm');
|
|
|
|
if (!form) return;
|
|
|
|
form.dataset.mode = 'edit';
|
|
document.getElementById('projectPopupTitle').textContent = `Edit ${currentProject.id}`;
|
|
document.getElementById('projectPopupSubmit').textContent = 'Update Project';
|
|
document.getElementById('projectFormId').value = currentProject.id;
|
|
document.getElementById('projectFormId').disabled = true;
|
|
document.getElementById('projectFormName').value = currentProject.name ?? '';
|
|
await populateProjectOwnerSelect(currentProject.owner ?? '');
|
|
|
|
openPopup('createProject');
|
|
}
|
|
|
|
function initProjectInlineEditing() {
|
|
if (!canEditProjects()) return;
|
|
|
|
document.querySelectorAll('.project-editable').forEach((element) => {
|
|
if (element.dataset.inlineReady === 'true') return;
|
|
|
|
element.dataset.inlineReady = 'true';
|
|
element.title = 'Click to edit';
|
|
|
|
element.addEventListener('click', () => openProjectInlineEditor(element));
|
|
element.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
openProjectInlineEditor(element);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function openProjectInlineEditor(element) {
|
|
if (!canEditProjects() || !currentProject || element.querySelector('.version-inline-form')) return;
|
|
|
|
const field = element.dataset.projectField;
|
|
const currentValue = element.dataset.currentValue ?? '';
|
|
const originalHtml = element.innerHTML;
|
|
const originalClasses = Array.from(element.classList);
|
|
|
|
if (!['name', 'owner'].includes(field)) return;
|
|
|
|
element.classList.remove('is-empty');
|
|
element.innerHTML = '';
|
|
|
|
const form = document.createElement('form');
|
|
form.className = 'version-inline-form';
|
|
|
|
let input;
|
|
|
|
if (field === 'owner') {
|
|
input = document.createElement('select');
|
|
input.className = 'form-select version-inline-input';
|
|
populateSelect(input, await getUsers(), {
|
|
includeEmpty: false,
|
|
selectedValue: currentValue
|
|
});
|
|
} else {
|
|
input = document.createElement('input');
|
|
input.className = 'form-control version-inline-input';
|
|
input.type = 'text';
|
|
input.required = true;
|
|
input.maxLength = 128;
|
|
input.value = currentValue;
|
|
}
|
|
|
|
form.appendChild(input);
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'version-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-project-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 = field === 'name' ? input.value.trim() : input.value;
|
|
|
|
if (nextValue === currentValue) {
|
|
restore();
|
|
return;
|
|
}
|
|
|
|
const result = await apiPost('/api/project.php', {
|
|
api: 'Edit',
|
|
project_id: currentProject.id
|
|
}, {
|
|
[field]: nextValue
|
|
});
|
|
|
|
if (!result.success) {
|
|
alert(result.error || 'Could not update project.');
|
|
restore();
|
|
return;
|
|
}
|
|
|
|
taskLookupCache.projectsById.delete(currentProject.id);
|
|
closed = true;
|
|
await loadProject(currentProject.id, false);
|
|
projectTreePromise = loadProjectTree();
|
|
};
|
|
|
|
form.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
await save();
|
|
});
|
|
|
|
if (input.tagName === 'SELECT') {
|
|
input.addEventListener('change', save);
|
|
}
|
|
|
|
form.querySelector('[data-project-inline-cancel]')?.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
restore();
|
|
});
|
|
|
|
input.addEventListener('keydown', async (event) => {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
restore();
|
|
}
|
|
});
|
|
}
|
|
|