made installer and seperated stuff into diferent files
This commit is contained in:
745
ProjectKiln/app/js/home/admin.js
Normal file
745
ProjectKiln/app/js/home/admin.js
Normal file
@@ -0,0 +1,745 @@
|
||||
async function loadWorkflowEditor() {
|
||||
const result = await apiGet('/api/workflow.php', {
|
||||
api: 'WorkflowData',
|
||||
_: Date.now()
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
document.getElementById('workflowGraph').innerHTML = '<div class="alert alert-danger m-3">Could not load workflow data.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
workflowData = result.workflow;
|
||||
if (!selectedWorkflowTaskType && workflowData.task_types.length) {
|
||||
selectedWorkflowTaskType = String(workflowData.task_types[0].id);
|
||||
}
|
||||
|
||||
if (selectedWorkflowTaskType && !workflowData.task_types.some((type) => String(type.id) === String(selectedWorkflowTaskType))) {
|
||||
selectedWorkflowTaskType = workflowData.task_types.length ? String(workflowData.task_types[0].id) : null;
|
||||
}
|
||||
|
||||
renderWorkflowEditor();
|
||||
}
|
||||
|
||||
function renderWorkflowEditor() {
|
||||
if (!workflowData) return;
|
||||
|
||||
populateWorkflowSelects();
|
||||
renderWorkflowLists();
|
||||
renderWorkflowGraph();
|
||||
}
|
||||
|
||||
function populateWorkflowSelects() {
|
||||
const stateOptions = workflowData.states.map((state) => ({
|
||||
id: state.id,
|
||||
name: state.name
|
||||
}));
|
||||
|
||||
populateSelect(document.getElementById('workflowTransitionFrom'), stateOptions, {
|
||||
placeholder: 'From state'
|
||||
});
|
||||
populateSelect(document.getElementById('workflowTransitionTo'), stateOptions, {
|
||||
placeholder: 'To state'
|
||||
});
|
||||
populateSelect(document.getElementById('workflowAssignmentState'), stateOptions, {
|
||||
placeholder: 'State'
|
||||
});
|
||||
populateSelect(document.getElementById('workflowTaskTypeSelect'), workflowData.task_types, {
|
||||
placeholder: 'Task type',
|
||||
selectedValue: selectedWorkflowTaskType ?? ''
|
||||
});
|
||||
|
||||
const assignedStates = getSelectedWorkflowAssignments()
|
||||
.map((assignment) => workflowData.states.find((state) => Number(state.id) === Number(assignment.state)))
|
||||
.filter(Boolean);
|
||||
const selectedType = getSelectedWorkflowTaskType();
|
||||
|
||||
populateSelect(document.getElementById('workflowDefaultStateSelect'), assignedStates, {
|
||||
placeholder: 'No default state',
|
||||
selectedValue: selectedType?.default_state ?? ''
|
||||
});
|
||||
}
|
||||
|
||||
function renderWorkflowLists() {
|
||||
const statesById = new Map(workflowData.states.map((state) => [Number(state.id), state]));
|
||||
const selectedAssignments = getSelectedWorkflowAssignments();
|
||||
|
||||
const stateList = document.getElementById('workflowStateList');
|
||||
stateList.innerHTML = workflowData.states.map((state) => `
|
||||
<div class="workflow-list-item">
|
||||
<div class="workflow-list-main">
|
||||
<span class="workflow-color-dot" style="background: ${escapeHtml(state.color)}"></span>
|
||||
<span>${escapeHtml(state.name)}</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" data-workflow-delete-state="${escapeHtml(state.id)}">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('') || '<div class="text-secondary">No states yet.</div>';
|
||||
|
||||
const transitionList = document.getElementById('workflowTransitionList');
|
||||
transitionList.innerHTML = workflowData.transitions.map((transition) => `
|
||||
<div class="workflow-list-item">
|
||||
<div class="workflow-list-main">
|
||||
<span>${escapeHtml(statesById.get(Number(transition.from_id))?.name ?? transition.from_id)}</span>
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
<span>${escapeHtml(statesById.get(Number(transition.to_id))?.name ?? transition.to_id)}</span>
|
||||
<span class="text-secondary">${escapeHtml(transition.action_name)}</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" data-workflow-delete-transition="${escapeHtml(transition.id)}">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('') || '<div class="text-secondary">No transitions yet.</div>';
|
||||
|
||||
const assignmentList = document.getElementById('workflowAssignmentList');
|
||||
assignmentList.innerHTML = selectedAssignments.map((assignment) => `
|
||||
<div class="workflow-list-item">
|
||||
<div class="workflow-list-main">
|
||||
<span>${escapeHtml(statesById.get(Number(assignment.state))?.name ?? assignment.state)}</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" data-workflow-remove-assignment="${escapeHtml(assignment.id)}">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('') || '<div class="text-secondary">No assigned states yet.</div>';
|
||||
}
|
||||
|
||||
function getSelectedWorkflowTaskType() {
|
||||
return workflowData?.task_types.find((type) => String(type.id) === String(selectedWorkflowTaskType)) ?? null;
|
||||
}
|
||||
|
||||
function getSelectedWorkflowAssignments() {
|
||||
if (!selectedWorkflowTaskType) return [];
|
||||
|
||||
return workflowData.assignments.filter((assignment) => String(assignment.task_type) === String(selectedWorkflowTaskType));
|
||||
}
|
||||
|
||||
function renderWorkflowGraph() {
|
||||
const graph = document.getElementById('workflowGraph');
|
||||
const empty = document.getElementById('workflowGraphEmpty');
|
||||
|
||||
if (!graph || !empty) return;
|
||||
|
||||
const assignedStateIds = new Set(getSelectedWorkflowAssignments().map((assignment) => Number(assignment.state)));
|
||||
const visibleStates = workflowData.states.filter((state) => assignedStateIds.has(Number(state.id)));
|
||||
const visibleTransitions = workflowData.transitions.filter((transition) => (
|
||||
assignedStateIds.has(Number(transition.from_id))
|
||||
&& assignedStateIds.has(Number(transition.to_id))
|
||||
));
|
||||
|
||||
if (!visibleStates.length) {
|
||||
if (workflowGraph) {
|
||||
workflowGraph.destroy();
|
||||
workflowGraph = null;
|
||||
}
|
||||
|
||||
graph.innerHTML = '';
|
||||
empty.hidden = false;
|
||||
empty.textContent = selectedWorkflowTaskType
|
||||
? 'Assign states to this task type to render its workflow graph.'
|
||||
: 'Select a task type to render its workflow graph.';
|
||||
return;
|
||||
}
|
||||
|
||||
empty.hidden = true;
|
||||
|
||||
if (typeof cytoscape !== 'function') {
|
||||
graph.innerHTML = '<div class="alert alert-warning m-3">Cytoscape.js could not be loaded.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = [
|
||||
...visibleStates.map((state) => ({
|
||||
data: {
|
||||
id: `state-${state.id}`,
|
||||
label: state.name,
|
||||
color: state.color
|
||||
}
|
||||
})),
|
||||
...visibleTransitions.map((transition) => ({
|
||||
data: {
|
||||
id: `transition-${transition.id}`,
|
||||
source: `state-${transition.from_id}`,
|
||||
target: `state-${transition.to_id}`,
|
||||
label: transition.action_name
|
||||
}
|
||||
}))
|
||||
];
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const borderColor = styles.getPropertyValue('--bs-border-color').trim() || '#3a3f46';
|
||||
const bodyColor = styles.getPropertyValue('--bs-body-color').trim() || '#f3f4f6';
|
||||
const secondaryColor = styles.getPropertyValue('--bs-secondary-color').trim() || '#aeb7c4';
|
||||
|
||||
if (workflowGraph) {
|
||||
workflowGraph.destroy();
|
||||
}
|
||||
|
||||
workflowGraph = cytoscape({
|
||||
container: graph,
|
||||
elements,
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': 'data(color)',
|
||||
'border-color': borderColor,
|
||||
'border-width': 1,
|
||||
'color': bodyColor,
|
||||
'font-size': 13,
|
||||
'font-weight': 700,
|
||||
'label': 'data(label)',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': 90,
|
||||
'width': 86,
|
||||
'height': 42,
|
||||
'shape': 'round-rectangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'bezier',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'line-color': secondaryColor,
|
||||
'target-arrow-color': secondaryColor,
|
||||
'color': secondaryColor,
|
||||
'font-size': 11,
|
||||
'label': 'data(label)',
|
||||
'text-rotation': 'autorotate',
|
||||
'text-margin-y': -8,
|
||||
'width': 2
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'breadthfirst',
|
||||
directed: true,
|
||||
padding: 36,
|
||||
spacingFactor: 1.25
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function saveWorkflowAction(api, body = {}, params = {}) {
|
||||
const result = await apiPost('/api/workflow.php', {
|
||||
api,
|
||||
...params
|
||||
}, body);
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not save workflow change.');
|
||||
return false;
|
||||
}
|
||||
|
||||
await loadWorkflowEditor();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadAdminOptions() {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await apiGet('/api/admin_options.php', {
|
||||
api: 'OptionsData',
|
||||
_: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
result = {
|
||||
success: false,
|
||||
error: 'Could not load admin options.'
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
const typeEditor = document.getElementById('adminSelectedTypeEditor');
|
||||
const priorityList = document.getElementById('adminPriorityList');
|
||||
const userRightsList = document.getElementById('adminUserRightsList');
|
||||
|
||||
if (typeEditor) {
|
||||
typeEditor.innerHTML = '<div class="text-danger">Could not load task types.</div>';
|
||||
}
|
||||
|
||||
if (priorityList) {
|
||||
priorityList.innerHTML = '<div class="text-danger">Could not load priorities.</div>';
|
||||
}
|
||||
|
||||
if (userRightsList) {
|
||||
userRightsList.innerHTML = '<div class="text-danger">Could not load users.</div>';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
adminOptionsData = {
|
||||
types: result.options?.types ?? [],
|
||||
priorities: result.options?.priorities ?? [],
|
||||
custom_fields: result.options?.custom_fields ?? [],
|
||||
users: result.options?.users ?? [],
|
||||
rights: result.options?.rights ?? [],
|
||||
user_rights: result.options?.user_rights ?? [],
|
||||
projects: result.options?.projects ?? [],
|
||||
user_access: result.options?.user_access ?? []
|
||||
};
|
||||
if (!selectedAdminTaskType && adminOptionsData.types.length) {
|
||||
selectedAdminTaskType = String(adminOptionsData.types[0].id);
|
||||
}
|
||||
|
||||
if (selectedAdminTaskType && !adminOptionsData.types.some((type) => String(type.id) === String(selectedAdminTaskType))) {
|
||||
selectedAdminTaskType = adminOptionsData.types.length ? String(adminOptionsData.types[0].id) : null;
|
||||
}
|
||||
|
||||
if (!selectedAdminUser && adminOptionsData.users.length) {
|
||||
selectedAdminUser = String(adminOptionsData.users[0].id);
|
||||
}
|
||||
|
||||
if (selectedAdminUser && !adminOptionsData.users.some((user) => String(user.id) === String(selectedAdminUser))) {
|
||||
selectedAdminUser = adminOptionsData.users.length ? String(adminOptionsData.users[0].id) : null;
|
||||
}
|
||||
|
||||
renderAdminOptions();
|
||||
}
|
||||
|
||||
function renderAdminOptions() {
|
||||
if (!adminOptionsData) return;
|
||||
|
||||
const types = adminOptionsData.types ?? [];
|
||||
const priorities = adminOptionsData.priorities ?? [];
|
||||
|
||||
populateSelect(document.getElementById('adminTaskTypeSelect'), types, {
|
||||
placeholder: 'Select task type',
|
||||
selectedValue: selectedAdminTaskType ?? ''
|
||||
});
|
||||
renderSelectedAdminTaskType();
|
||||
renderAdminCustomFields();
|
||||
renderAdminOptionList('priority', priorities, document.getElementById('adminPriorityList'));
|
||||
renderAdminUsers();
|
||||
}
|
||||
|
||||
function getSelectedAdminTaskType() {
|
||||
return adminOptionsData?.types.find((type) => String(type.id) === String(selectedAdminTaskType)) ?? null;
|
||||
}
|
||||
|
||||
function renderSelectedAdminTaskType() {
|
||||
const container = document.getElementById('adminSelectedTypeEditor');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const type = getSelectedAdminTaskType();
|
||||
|
||||
if (!type) {
|
||||
container.innerHTML = '<div class="text-secondary">Create or select a task type first.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<form class="admin-option-item admin-selected-option" data-admin-option-edit="type" data-option-id="${escapeHtml(type.id)}">
|
||||
<div class="admin-option-icon">
|
||||
${type.icon
|
||||
? `<img src="${escapeHtml(type.icon)}" alt="">`
|
||||
: '<i class="fa-solid fa-tag"></i>'}
|
||||
</div>
|
||||
<input class="form-control form-control-sm" name="name" value="${escapeHtml(type.name)}" required maxlength="20">
|
||||
<input class="form-control form-control-sm" type="file" name="logo" accept=".svg,image/svg+xml" aria-label="Logo">
|
||||
<label class="admin-option-remove-logo">
|
||||
<input type="checkbox" name="remove_logo" value="1">
|
||||
<span>Remove logo</span>
|
||||
</label>
|
||||
<div class="admin-option-actions">
|
||||
<button class="btn btn-sm btn-primary" type="submit" title="Save">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" data-admin-option-delete="type" data-option-id="${escapeHtml(type.id)}" title="Delete">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAdminCustomFields() {
|
||||
const container = document.getElementById('adminCustomFieldList');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
if (!selectedAdminTaskType) {
|
||||
container.innerHTML = '<div class="text-secondary">Select a task type first.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = (adminOptionsData.custom_fields ?? [])
|
||||
.filter((field) => String(field.task_type) === String(selectedAdminTaskType));
|
||||
|
||||
container.innerHTML = fields.map((field) => `
|
||||
<form class="admin-custom-field-item" data-admin-custom-field-edit="${escapeHtml(field.id)}">
|
||||
<input class="form-control form-control-sm" name="name" value="${escapeHtml(field.name)}" required maxlength="128">
|
||||
<select class="form-select form-select-sm" name="type" required>
|
||||
${customFieldTypes.map((type) => `
|
||||
<option value="${escapeHtml(type.id)}" ${String(type.id) === String(field.type) ? 'selected' : ''}>${escapeHtml(type.name)}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
<input class="form-control form-control-sm" name="value" value="${escapeHtml(field.raw_value ?? '')}">
|
||||
<div class="admin-option-actions">
|
||||
<button class="btn btn-sm btn-primary" type="submit" title="Save">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" data-admin-custom-field-delete="${escapeHtml(field.id)}" title="Delete">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`).join('') || '<div class="text-secondary">No custom fields yet.</div>';
|
||||
}
|
||||
|
||||
function renderAdminOptionList(kind, options, container) {
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = options.map((option) => `
|
||||
<form class="admin-option-item" data-admin-option-edit="${escapeHtml(kind)}" data-option-id="${escapeHtml(option.id)}">
|
||||
<div class="admin-option-icon">
|
||||
${option.icon
|
||||
? `<img src="${escapeHtml(option.icon)}" alt="">`
|
||||
: '<i class="fa-solid fa-tag"></i>'}
|
||||
</div>
|
||||
<input class="form-control form-control-sm" name="name" value="${escapeHtml(option.name)}" required maxlength="128">
|
||||
<input class="form-control form-control-sm" type="file" name="logo" accept=".svg,image/svg+xml" aria-label="Logo">
|
||||
<label class="admin-option-remove-logo">
|
||||
<input type="checkbox" name="remove_logo" value="1">
|
||||
<span>Remove logo</span>
|
||||
</label>
|
||||
<div class="admin-option-actions">
|
||||
<button class="btn btn-sm btn-primary" type="submit" title="Save">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" data-admin-option-delete="${escapeHtml(kind)}" data-option-id="${escapeHtml(option.id)}" title="Delete">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`).join('') || '<div class="text-secondary">No entries yet.</div>';
|
||||
}
|
||||
|
||||
function getSelectedAdminUser() {
|
||||
return adminOptionsData?.users.find((user) => String(user.id) === String(selectedAdminUser)) ?? null;
|
||||
}
|
||||
|
||||
function userHasAdminRight(userId, rightId) {
|
||||
return (adminOptionsData?.user_rights ?? []).some((entry) => (
|
||||
String(entry.user_id) === String(userId)
|
||||
&& String(entry.right_id) === String(rightId)
|
||||
));
|
||||
}
|
||||
|
||||
function getAdminUserAccess(userId) {
|
||||
return (adminOptionsData?.user_access ?? []).filter((entry) => (
|
||||
String(entry.user_id) === String(userId)
|
||||
));
|
||||
}
|
||||
|
||||
function getAdminProject(projectId) {
|
||||
return (adminOptionsData?.projects ?? []).find((project) => (
|
||||
String(project.id) === String(projectId)
|
||||
)) ?? null;
|
||||
}
|
||||
|
||||
function renderAdminUsers() {
|
||||
const select = document.getElementById('adminUserSelect');
|
||||
const container = document.getElementById('adminUserRightsList');
|
||||
|
||||
if (!select || !container) return;
|
||||
|
||||
const users = adminOptionsData.users ?? [];
|
||||
const rights = adminOptionsData.rights ?? [];
|
||||
const projects = adminOptionsData.projects ?? [];
|
||||
const selectedUser = getSelectedAdminUser();
|
||||
|
||||
populateSelect(select, users, {
|
||||
placeholder: 'Select user',
|
||||
selectedValue: selectedAdminUser ?? ''
|
||||
});
|
||||
|
||||
if (!selectedUser) {
|
||||
container.innerHTML = '<div class="text-secondary">Select a user first.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rights.length) {
|
||||
container.innerHTML = '<div class="text-secondary">No rights configured yet.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const explicitAccess = getAdminUserAccess(selectedUser.id);
|
||||
const explicitProjectIds = new Set(explicitAccess.map((entry) => String(entry.project_id)));
|
||||
const ownerProjects = projects.filter((project) => String(project.owner) === String(selectedUser.id));
|
||||
const ownerProjectIds = new Set(ownerProjects.map((project) => String(project.id)));
|
||||
const addableProjects = projects.filter((project) => (
|
||||
!explicitProjectIds.has(String(project.id))
|
||||
&& !ownerProjectIds.has(String(project.id))
|
||||
));
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="admin-user-summary">
|
||||
${renderUser(selectedUser)}
|
||||
</div>
|
||||
|
||||
<div class="admin-user-section">
|
||||
<h4>Rights</h4>
|
||||
<div class="admin-right-list">
|
||||
${rights.map((right) => {
|
||||
const checked = userHasAdminRight(selectedUser.id, right.id);
|
||||
const isSelfAdminRight = appFlag('isAdmin')
|
||||
&& String(selectedUser.id) === String(getCurrentUserId())
|
||||
&& String(right.name) === 'Admin';
|
||||
|
||||
return `
|
||||
<label class="admin-right-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-admin-user-right
|
||||
data-user-id="${escapeHtml(selectedUser.id)}"
|
||||
data-right-id="${escapeHtml(right.id)}"
|
||||
${checked ? 'checked' : ''}
|
||||
${isSelfAdminRight ? 'disabled' : ''}
|
||||
>
|
||||
<span>${escapeHtml(right.name)}</span>
|
||||
</label>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-user-section">
|
||||
<h4>Project Access</h4>
|
||||
<form class="admin-project-access-form" id="adminProjectAccessForm">
|
||||
<select class="form-select form-select-sm" name="project_id" ${addableProjects.length ? '' : 'disabled'}>
|
||||
${addableProjects.map((project) => `
|
||||
<option value="${escapeHtml(project.id)}">${escapeHtml(project.name)} (${escapeHtml(project.id)})</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
<button class="btn btn-sm btn-primary" type="submit" ${addableProjects.length ? '' : 'disabled'}>
|
||||
<i class="fa-solid fa-plus me-1"></i>
|
||||
Add Access
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="admin-project-access-list">
|
||||
${explicitAccess.map((access) => {
|
||||
const project = getAdminProject(access.project_id);
|
||||
|
||||
return `
|
||||
<div class="admin-project-access-item">
|
||||
<span>${escapeHtml(project?.name ?? access.project_id)} <small>${escapeHtml(access.project_id)}</small></span>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" data-admin-project-access-delete="${escapeHtml(access.id)}" title="Remove access">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('') || '<div class="text-secondary">No explicit project access yet.</div>'}
|
||||
</div>
|
||||
|
||||
${ownerProjects.length ? `
|
||||
<div class="admin-project-owner-list">
|
||||
<strong>Owner access</strong>
|
||||
${ownerProjects.map((project) => `
|
||||
<span>${escapeHtml(project.name)} <small>${escapeHtml(project.id)}</small></span>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function setAdminUserRight(userId, rightId, enabled) {
|
||||
const result = await apiPost('/api/admin_options.php', {
|
||||
api: 'SetUserRight'
|
||||
}, {
|
||||
user_id: userId,
|
||||
right_id: rightId,
|
||||
enabled: enabled ? 1 : 0
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not update user right.');
|
||||
return false;
|
||||
}
|
||||
|
||||
await loadAdminOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function grantAdminProjectAccess(userId, projectId) {
|
||||
const result = await apiPost('/api/admin_options.php', {
|
||||
api: 'GrantProjectAccess'
|
||||
}, {
|
||||
user_id: userId,
|
||||
project_id: projectId
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not add project access.');
|
||||
return false;
|
||||
}
|
||||
|
||||
await loadAdminOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function revokeAdminProjectAccess(accessId) {
|
||||
const result = await apiPost('/api/admin_options.php', {
|
||||
api: 'RevokeProjectAccess',
|
||||
access_id: accessId
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not remove project access.');
|
||||
return false;
|
||||
}
|
||||
|
||||
await loadAdminOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveAdminOptionForm(kind, form, optionId = null) {
|
||||
const formData = new FormData(form);
|
||||
formData.set('kind', kind);
|
||||
|
||||
if (optionId) {
|
||||
formData.set('id', optionId);
|
||||
}
|
||||
|
||||
const result = await apiPostForm('/api/admin_options.php', {
|
||||
api: optionId ? 'UpdateOption' : 'CreateOption'
|
||||
}, formData);
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not save option.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (kind === 'type' && !optionId && result.option_id) {
|
||||
selectedAdminTaskType = String(result.option_id);
|
||||
}
|
||||
|
||||
taskLookupCache.types = null;
|
||||
taskLookupCache.priorities = null;
|
||||
await populateTaskOptionSelects();
|
||||
await loadAdminOptions();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function deleteAdminOption(kind, optionId) {
|
||||
const result = await apiPost('/api/admin_options.php', {
|
||||
api: 'DeleteOption',
|
||||
kind,
|
||||
id: optionId
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not delete option.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (kind === 'type' && String(selectedAdminTaskType) === String(optionId)) {
|
||||
selectedAdminTaskType = null;
|
||||
}
|
||||
|
||||
taskLookupCache.types = null;
|
||||
taskLookupCache.priorities = null;
|
||||
await populateTaskOptionSelects();
|
||||
await loadAdminOptions();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveAdminCustomFieldForm(form, fieldId = null) {
|
||||
if (!selectedAdminTaskType) {
|
||||
alert('Select a task type first.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
data.task_type = selectedAdminTaskType;
|
||||
|
||||
if (fieldId) {
|
||||
data.id = fieldId;
|
||||
}
|
||||
|
||||
const result = await apiPost('/api/admin_options.php', {
|
||||
api: fieldId ? 'UpdateCustomField' : 'CreateCustomField'
|
||||
}, data);
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not save custom field.');
|
||||
return false;
|
||||
}
|
||||
|
||||
await loadAdminOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function deleteAdminCustomField(fieldId) {
|
||||
const result = await apiPost('/api/admin_options.php', {
|
||||
api: 'DeleteCustomField',
|
||||
field_id: fieldId
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Could not delete custom field.');
|
||||
return false;
|
||||
}
|
||||
|
||||
await loadAdminOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
function loadAdmin(section = 'workflows', pushHistory = true) {
|
||||
if (document.querySelector('.kiln-app')?.dataset.isAdmin !== '1') {
|
||||
loadDashboard(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const view = showView('adminView');
|
||||
|
||||
if (!view) return;
|
||||
|
||||
if (!['workflows', 'options', 'users'].includes(section)) {
|
||||
section = 'workflows';
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-admin-section]').forEach((button) => {
|
||||
button.classList.toggle('is-active', button.dataset.adminSection === section);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.admin-panel').forEach((panel) => {
|
||||
panel.hidden = true;
|
||||
});
|
||||
|
||||
const panelBySection = {
|
||||
workflows: 'adminWorkflowsPanel',
|
||||
options: 'adminOptionsPanel',
|
||||
users: 'adminUsersPanel'
|
||||
};
|
||||
const panel = document.getElementById(panelBySection[section]);
|
||||
if (panel) panel.hidden = false;
|
||||
|
||||
if (pushHistory) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('page', 'home');
|
||||
url.searchParams.set('admin', section);
|
||||
url.searchParams.delete('task');
|
||||
url.searchParams.delete('project');
|
||||
url.searchParams.delete('tasks');
|
||||
url.searchParams.delete('version');
|
||||
url.searchParams.delete('profile');
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
if (section === 'workflows') {
|
||||
loadWorkflowEditor();
|
||||
} else if (section === 'options' || section === 'users') {
|
||||
loadAdminOptions();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user