made installer and seperated stuff into diferent files
This commit is contained in:
623
ProjectKiln/app/js/home/events.js
Normal file
623
ProjectKiln/app/js/home/events.js
Normal file
@@ -0,0 +1,623 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
projectTreePromise = loadProjectTree();
|
||||
populateTaskOptionSelects();
|
||||
applyPermissionVisibility();
|
||||
initTaskInlineEditing();
|
||||
initVersionInlineEditing();
|
||||
initProjectInlineEditing();
|
||||
|
||||
loadRouteFromUrl();
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
loadRouteFromUrl();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-view="dashboard"]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
loadDashboard();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('adminMenuButton')?.addEventListener('click', () => {
|
||||
loadAdmin('workflows');
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-admin-section]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
loadAdmin(button.dataset.adminSection);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('workflowRefreshButton')?.addEventListener('click', () => {
|
||||
loadWorkflowEditor();
|
||||
});
|
||||
|
||||
document.getElementById('workflowTaskTypeSelect')?.addEventListener('change', (event) => {
|
||||
selectedWorkflowTaskType = event.target.value || null;
|
||||
renderWorkflowEditor();
|
||||
});
|
||||
|
||||
document.getElementById('workflowDefaultStateSelect')?.addEventListener('change', (event) => {
|
||||
if (!selectedWorkflowTaskType) return;
|
||||
|
||||
saveWorkflowAction('SetDefaultState', {
|
||||
task_type: selectedWorkflowTaskType,
|
||||
state: event.target.value
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('workflowStateForm')?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
const saved = await saveWorkflowAction('CreateState', data);
|
||||
|
||||
if (saved) form.reset();
|
||||
});
|
||||
|
||||
document.getElementById('workflowTransitionForm')?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
const saved = await saveWorkflowAction('CreateTransition', data);
|
||||
|
||||
if (saved) form.reset();
|
||||
});
|
||||
|
||||
document.getElementById('workflowAssignmentForm')?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
data.task_type = selectedWorkflowTaskType;
|
||||
|
||||
if (!data.task_type) {
|
||||
alert('Select a task type first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = await saveWorkflowAction('AssignState', data);
|
||||
|
||||
if (saved) form.reset();
|
||||
});
|
||||
|
||||
document.getElementById('adminWorkflowsPanel')?.addEventListener('click', (event) => {
|
||||
const stateButton = event.target.closest('[data-workflow-delete-state]');
|
||||
const transitionButton = event.target.closest('[data-workflow-delete-transition]');
|
||||
const assignmentButton = event.target.closest('[data-workflow-remove-assignment]');
|
||||
|
||||
if (stateButton) {
|
||||
if (!window.confirm('Delete this state and related transitions/assignments?')) return;
|
||||
|
||||
saveWorkflowAction('DeleteState', {}, {
|
||||
state_id: stateButton.dataset.workflowDeleteState
|
||||
});
|
||||
}
|
||||
|
||||
if (transitionButton) {
|
||||
saveWorkflowAction('DeleteTransition', {}, {
|
||||
transition_id: transitionButton.dataset.workflowDeleteTransition
|
||||
});
|
||||
}
|
||||
|
||||
if (assignmentButton) {
|
||||
saveWorkflowAction('RemoveAssignedState', {}, {
|
||||
assignment_id: assignmentButton.dataset.workflowRemoveAssignment
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-admin-option-form]').forEach((form) => {
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const saved = await saveAdminOptionForm(form.dataset.adminOptionForm, form);
|
||||
|
||||
if (saved) form.reset();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('adminTaskTypeSelect')?.addEventListener('change', (event) => {
|
||||
selectedAdminTaskType = event.target.value || null;
|
||||
renderAdminOptions();
|
||||
});
|
||||
|
||||
document.getElementById('adminUserSelect')?.addEventListener('change', (event) => {
|
||||
selectedAdminUser = event.target.value || null;
|
||||
renderAdminOptions();
|
||||
});
|
||||
|
||||
document.getElementById('adminCustomFieldForm')?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const saved = await saveAdminCustomFieldForm(event.currentTarget);
|
||||
|
||||
if (saved) event.currentTarget.reset();
|
||||
});
|
||||
|
||||
document.getElementById('adminOptionsPanel')?.addEventListener('submit', async (event) => {
|
||||
const form = event.target.closest('[data-admin-option-edit]');
|
||||
|
||||
if (!form) return;
|
||||
|
||||
event.preventDefault();
|
||||
await saveAdminOptionForm(form.dataset.adminOptionEdit, form, form.dataset.optionId);
|
||||
});
|
||||
|
||||
document.getElementById('adminOptionsPanel')?.addEventListener('submit', async (event) => {
|
||||
const form = event.target.closest('[data-admin-custom-field-edit]');
|
||||
|
||||
if (!form) return;
|
||||
|
||||
event.preventDefault();
|
||||
await saveAdminCustomFieldForm(form, form.dataset.adminCustomFieldEdit);
|
||||
});
|
||||
|
||||
document.getElementById('adminOptionsPanel')?.addEventListener('click', (event) => {
|
||||
const deleteButton = event.target.closest('[data-admin-option-delete]');
|
||||
|
||||
if (!deleteButton) return;
|
||||
|
||||
if (!window.confirm('Delete this option?')) return;
|
||||
|
||||
deleteAdminOption(deleteButton.dataset.adminOptionDelete, deleteButton.dataset.optionId);
|
||||
});
|
||||
|
||||
document.getElementById('adminOptionsPanel')?.addEventListener('click', (event) => {
|
||||
const deleteButton = event.target.closest('[data-admin-custom-field-delete]');
|
||||
|
||||
if (!deleteButton) return;
|
||||
|
||||
if (!window.confirm('Delete this custom field?')) return;
|
||||
|
||||
deleteAdminCustomField(deleteButton.dataset.adminCustomFieldDelete);
|
||||
});
|
||||
|
||||
document.getElementById('adminUsersPanel')?.addEventListener('change', async (event) => {
|
||||
const checkbox = event.target.closest('[data-admin-user-right]');
|
||||
|
||||
if (!checkbox) return;
|
||||
|
||||
checkbox.disabled = true;
|
||||
const saved = await setAdminUserRight(
|
||||
checkbox.dataset.userId,
|
||||
checkbox.dataset.rightId,
|
||||
checkbox.checked
|
||||
);
|
||||
|
||||
if (!saved) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
checkbox.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('adminUsersPanel')?.addEventListener('submit', async (event) => {
|
||||
const form = event.target.closest('#adminProjectAccessForm');
|
||||
|
||||
if (!form) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (!selectedAdminUser) return;
|
||||
|
||||
const data = Object.fromEntries(new FormData(form));
|
||||
if (!data.project_id) return;
|
||||
|
||||
await grantAdminProjectAccess(selectedAdminUser, data.project_id);
|
||||
});
|
||||
|
||||
document.getElementById('adminUsersPanel')?.addEventListener('click', async (event) => {
|
||||
const deleteButton = event.target.closest('[data-admin-project-access-delete]');
|
||||
|
||||
if (!deleteButton) return;
|
||||
|
||||
await revokeAdminProjectAccess(deleteButton.dataset.adminProjectAccessDelete);
|
||||
});
|
||||
|
||||
document.getElementById('profileMenuButton')?.addEventListener('click', () => {
|
||||
loadProfile();
|
||||
});
|
||||
|
||||
document.getElementById('taskEditButton')?.addEventListener('click', () => {
|
||||
if (!canEditTasks()) return;
|
||||
openTaskEditPopup();
|
||||
});
|
||||
|
||||
document.getElementById('taskView')?.addEventListener('click', (event) => {
|
||||
const customField = event.target.closest('[data-custom-field-id]');
|
||||
|
||||
if (customField) {
|
||||
if (!canEditTasks()) return;
|
||||
openCustomFieldInlineEditor(customField);
|
||||
return;
|
||||
}
|
||||
|
||||
const statusButton = event.target.closest('#taskStatusButton');
|
||||
|
||||
if (statusButton) {
|
||||
const menu = document.getElementById('taskStatusMenu');
|
||||
if (menu) menu.hidden = !menu.hidden;
|
||||
return;
|
||||
}
|
||||
|
||||
const transitionButton = event.target.closest('[data-task-transition]');
|
||||
|
||||
if (transitionButton) {
|
||||
transitionCurrentTask(transitionButton.dataset.taskTransition);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('taskView')?.addEventListener('keydown', (event) => {
|
||||
if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(event.target.tagName)) return;
|
||||
|
||||
const customField = event.target.closest('[data-custom-field-id]');
|
||||
|
||||
if (!customField || !['Enter', ' '].includes(event.key)) return;
|
||||
|
||||
event.preventDefault();
|
||||
if (!canEditTasks()) return;
|
||||
openCustomFieldInlineEditor(customField);
|
||||
});
|
||||
|
||||
document.getElementById('taskCommentButton')?.addEventListener('click', () => {
|
||||
document.getElementById('taskCommentInput')?.focus();
|
||||
});
|
||||
|
||||
document.getElementById('versionEditButton')?.addEventListener('click', () => {
|
||||
if (!canEditVersions()) return;
|
||||
openVersionEditPopup();
|
||||
});
|
||||
|
||||
document.getElementById('projectEditButton')?.addEventListener('click', () => {
|
||||
if (!canEditProjects()) return;
|
||||
openProjectEditPopup();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-version-task-sort]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const field = button.dataset.versionTaskSort;
|
||||
|
||||
if (versionTaskSort.field === field) {
|
||||
versionTaskSort.direction = versionTaskSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
versionTaskSort.field = field;
|
||||
versionTaskSort.direction = 'asc';
|
||||
}
|
||||
|
||||
if (currentVersion) {
|
||||
loadVersionTasks(currentVersion.id, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-project-task-sort]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const field = button.dataset.projectTaskSort;
|
||||
|
||||
if (projectTaskSort.field === field) {
|
||||
projectTaskSort.direction = projectTaskSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
projectTaskSort.field = field;
|
||||
projectTaskSort.direction = 'asc';
|
||||
}
|
||||
|
||||
if (currentProjectTaskProject) {
|
||||
loadProjectTasks(currentProjectTaskProject, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const pageButton = event.target.closest('[data-task-page]');
|
||||
|
||||
if (!pageButton || pageButton.disabled) return;
|
||||
|
||||
const page = Number(pageButton.dataset.page);
|
||||
|
||||
if (!Number.isFinite(page) || page < 1) return;
|
||||
|
||||
if (pageButton.dataset.taskPage === 'version' && currentVersion) {
|
||||
loadVersionTasks(currentVersion.id, page);
|
||||
}
|
||||
|
||||
if (pageButton.dataset.taskPage === 'project' && currentProjectTaskProject) {
|
||||
loadProjectTasks(currentProjectTaskProject, page);
|
||||
}
|
||||
});
|
||||
|
||||
const createProjectForm = document.getElementById('createProjectForm');
|
||||
|
||||
if (createProjectForm) {
|
||||
createProjectForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(createProjectForm));
|
||||
const mode = createProjectForm.dataset.mode ?? 'create';
|
||||
const projectId = document.getElementById('projectFormId').value;
|
||||
|
||||
const result = await apiPost('/api/project.php', {
|
||||
api: mode === 'edit' ? 'Edit' : 'Create',
|
||||
project_id: mode === 'edit' ? projectId : undefined
|
||||
}, data);
|
||||
|
||||
if (result.success) {
|
||||
taskLookupCache.projectsById.delete(result.project_id);
|
||||
closePopups();
|
||||
createProjectForm.reset();
|
||||
projectTreePromise = loadProjectTree();
|
||||
|
||||
if (mode === 'edit') {
|
||||
await loadProject(projectId, false);
|
||||
}
|
||||
} else {
|
||||
alert(result.error || 'Could not create project.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const createVersionForm = document.getElementById('createVersionForm');
|
||||
|
||||
if (createVersionForm) {
|
||||
createVersionForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(createVersionForm));
|
||||
const mode = createVersionForm.dataset.mode ?? 'create';
|
||||
const versionId = data.version_id;
|
||||
const projectId = data.project_id;
|
||||
delete data.version_id;
|
||||
|
||||
if (mode === 'edit') {
|
||||
delete data.project_id;
|
||||
}
|
||||
|
||||
const result = await apiPost('/api/version.php', {
|
||||
api: mode === 'edit' ? 'Edit' : 'Create',
|
||||
version_id: mode === 'edit' ? versionId : undefined
|
||||
}, data);
|
||||
|
||||
if (result.success) {
|
||||
taskLookupCache.versionsByProject.delete(projectId);
|
||||
closePopups();
|
||||
projectTreePromise = loadProjectTree();
|
||||
|
||||
if (mode === 'edit') {
|
||||
await loadVersion(versionId, false);
|
||||
}
|
||||
} else {
|
||||
alert(result.error || 'Could not save version.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const profileForm = document.getElementById('profileForm');
|
||||
|
||||
if (profileForm) {
|
||||
document.getElementById('profilePictureInput')?.addEventListener('change', (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
if (!file) {
|
||||
renderProfileAvatar(currentProfileUser?.picture ?? null);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('profileRemovePicture').checked = false;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => renderProfileAvatar(reader.result));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
document.getElementById('profileRemovePicture')?.addEventListener('change', (event) => {
|
||||
if (event.target.checked) {
|
||||
document.getElementById('profilePictureInput').value = '';
|
||||
renderProfileAvatar(null);
|
||||
} else {
|
||||
renderProfileAvatar(currentProfileUser?.picture ?? null);
|
||||
}
|
||||
});
|
||||
|
||||
profileForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const password = document.getElementById('profilePassword').value;
|
||||
const passwordConfirm = document.getElementById('profilePasswordConfirm').value;
|
||||
const status = document.getElementById('profileStatus');
|
||||
const submitButton = document.getElementById('profileSubmitButton');
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
status.textContent = 'Passwords do not match.';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new FormData(profileForm);
|
||||
|
||||
if (!password) {
|
||||
data.delete('password');
|
||||
}
|
||||
|
||||
submitButton.disabled = true;
|
||||
status.textContent = 'Saving...';
|
||||
|
||||
const result = await apiPostForm('/api/user.php', {
|
||||
api: 'Edit',
|
||||
user_id: getCurrentUserId()
|
||||
}, data);
|
||||
|
||||
submitButton.disabled = false;
|
||||
|
||||
if (!result.success) {
|
||||
status.textContent = result.error || 'Could not save profile.';
|
||||
return;
|
||||
}
|
||||
|
||||
const userResult = await apiGet('/api/user.php', {
|
||||
api: 'UserInfo',
|
||||
user_id: getCurrentUserId(),
|
||||
_: Date.now()
|
||||
});
|
||||
|
||||
if (userResult.success) {
|
||||
taskLookupCache.users = null;
|
||||
updateAccountProfile(userResult.user);
|
||||
applyTheme(userResult.user.settings?.theme ?? 'dark');
|
||||
renderProfile(userResult.user);
|
||||
status.textContent = 'Saved.';
|
||||
} else {
|
||||
status.textContent = 'Saved. Refresh to see all changes.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const createTaskForm = document.getElementById('createTaskForm');
|
||||
|
||||
if (createTaskForm) {
|
||||
document.getElementById('taskFormProjectId')?.addEventListener('change', (event) => {
|
||||
populateTaskRelationSelects(event.target.value.trim());
|
||||
});
|
||||
|
||||
createTaskForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const data = Object.fromEntries(new FormData(createTaskForm));
|
||||
const mode = createTaskForm.dataset.mode ?? 'create';
|
||||
const taskId = data.task_id;
|
||||
delete data.task_id;
|
||||
|
||||
if (mode === 'edit') {
|
||||
delete data.project_id;
|
||||
}
|
||||
|
||||
const result = await apiPost('/api/task.php', {
|
||||
api: mode === 'edit' ? 'Edit' : 'Create',
|
||||
task_id: mode === 'edit' ? taskId : undefined
|
||||
}, data);
|
||||
|
||||
if (result.success) {
|
||||
closePopups();
|
||||
projectTreePromise = loadProjectTree();
|
||||
loadTask(mode === 'edit' ? taskId : result.task_id, mode !== 'edit');
|
||||
} else {
|
||||
alert(result.error || 'Could not save task.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const taskCommentForm = document.getElementById('taskCommentForm');
|
||||
|
||||
if (taskCommentForm) {
|
||||
taskCommentForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const input = document.getElementById('taskCommentInput');
|
||||
const status = document.getElementById('taskCommentStatus');
|
||||
const comment = input.value.trim();
|
||||
|
||||
if (!comment) {
|
||||
status.textContent = 'Comment cannot be empty.';
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
await submitTaskComment(comment);
|
||||
input.value = '';
|
||||
status.textContent = 'Saved.';
|
||||
} catch (error) {
|
||||
status.textContent = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('taskCommentList')?.addEventListener('click', (event) => {
|
||||
const replyButton = event.target.closest('[data-comment-reply]');
|
||||
const editButton = event.target.closest('[data-comment-edit]');
|
||||
const deleteButton = event.target.closest('[data-comment-delete]');
|
||||
const cancelButton = event.target.closest('[data-comment-reply-cancel]');
|
||||
const editCancelButton = event.target.closest('[data-comment-edit-cancel]');
|
||||
|
||||
if (replyButton) {
|
||||
const commentCard = replyButton.closest('.task-comment');
|
||||
const slot = commentCard?.querySelector('.task-comment-reply-slot');
|
||||
|
||||
if (!slot || slot.querySelector('form')) return;
|
||||
|
||||
slot.innerHTML = `
|
||||
<form class="task-comment-reply-form" data-comment-reply-form="${replyButton.dataset.commentReply}">
|
||||
<textarea class="form-control" name="comment" rows="2" placeholder="Write a reply..."></textarea>
|
||||
<div class="task-comment-reply-actions">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Reply</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-comment-reply-cancel>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
slot.querySelector('textarea')?.focus();
|
||||
}
|
||||
|
||||
if (editButton) {
|
||||
const commentCard = editButton.closest('.task-comment');
|
||||
const slot = commentCard?.querySelector('.task-comment-edit-slot');
|
||||
|
||||
if (!slot || slot.querySelector('form')) return;
|
||||
|
||||
slot.innerHTML = `
|
||||
<form class="task-comment-edit-form" data-comment-edit-form="${editButton.dataset.commentEdit}">
|
||||
<textarea class="form-control" name="comment" rows="3">${escapeHtml(commentCard.dataset.commentText ?? '')}</textarea>
|
||||
<div class="task-comment-reply-actions">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Save</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-comment-edit-cancel>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
slot.querySelector('textarea')?.focus();
|
||||
}
|
||||
|
||||
if (deleteButton) {
|
||||
if (!window.confirm('Delete this comment?')) return;
|
||||
|
||||
deleteTaskComment(deleteButton.dataset.commentDelete).catch((error) => {
|
||||
alert(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelButton) {
|
||||
cancelButton.closest('.task-comment-reply-slot').innerHTML = '';
|
||||
}
|
||||
|
||||
if (editCancelButton) {
|
||||
editCancelButton.closest('.task-comment-edit-slot').innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('taskCommentList')?.addEventListener('submit', async (event) => {
|
||||
const form = event.target.closest('[data-comment-reply-form], [data-comment-edit-form]');
|
||||
|
||||
if (!form) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const textarea = form.querySelector('textarea');
|
||||
const button = form.querySelector('button[type="submit"]');
|
||||
const comment = textarea.value.trim();
|
||||
const responseTo = form.dataset.commentReplyForm;
|
||||
const editCommentId = form.dataset.commentEditForm;
|
||||
|
||||
if (!comment) return;
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
if (editCommentId) {
|
||||
await updateTaskComment(editCommentId, comment);
|
||||
} else {
|
||||
await submitTaskComment(comment, responseTo);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user