fix(timer): use url_for for task API URLs and improve timer page init

Build task API URLs from url_for in timer page, bulk entry, edit timer, and time entry template edit to support subpath deployment. Wrap timer page script in DOMContentLoaded, load tasks when project is pre-selected, expose selectRecentProject on window, and fix client/project select attachment logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dries Peeters
2026-02-04 22:10:31 +01:00
parent 25d9a8a36f
commit a53f5a6163
4 changed files with 45 additions and 8 deletions
+7 -1
View File
@@ -145,6 +145,12 @@
</div>
<script>
const tasksApiUrlTemplate = {{ url_for('api.get_project_tasks', project_id=0)|tojson }};
function buildTasksUrl(projectId) {
const pid = String(projectId || '').trim();
if (!pid) return tasksApiUrlTemplate;
return String(tasksApiUrlTemplate).replace(/\/0\/tasks$/, '/' + encodeURIComponent(pid) + '/tasks');
}
function loadProjectTasks(projectId) {
const taskSelect = document.getElementById('task_id');
const selectedTaskId = taskSelect.dataset.selected;
@@ -157,7 +163,7 @@ function loadProjectTasks(projectId) {
}
// Fetch tasks for the selected project
fetch(`/api/projects/${projectId}/tasks`)
fetch(buildTasksUrl(projectId))
.then(response => response.json())
.then(data => {
data.tasks.forEach(task => {
+23 -5
View File
@@ -237,12 +237,20 @@
{% block scripts_extra %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load tasks when project is selected and sync with client selection
const projectSelectEl = document.getElementById('project_id');
const clientSelectEl = document.getElementById('client_id');
const taskSelectEl = document.getElementById('task_id');
const noTaskText = {{ _('No task')|tojson }};
const failedToLoadTasksText = {{ _('Failed to load tasks')|tojson }};
const tasksApiUrlTemplate = {{ url_for('api.get_project_tasks', project_id=0)|tojson }};
function buildTasksUrl(projectId) {
const pid = String(projectId || '').trim();
if (!pid) return tasksApiUrlTemplate;
return String(tasksApiUrlTemplate).replace(/\/0\/tasks$/, '/' + encodeURIComponent(pid) + '/tasks');
}
async function loadTasksForProject(projectId) {
if (!taskSelectEl) return;
@@ -253,7 +261,7 @@ async function loadTasksForProject(projectId) {
}
try {
const response = await fetch(`/api/projects/${projectId}/tasks`, { credentials: 'same-origin' });
const response = await fetch(buildTasksUrl(projectId), { credentials: 'same-origin' });
if (!response.ok) throw new Error(failedToLoadTasksText);
const data = await response.json();
const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
@@ -276,7 +284,8 @@ async function loadTasksForProject(projectId) {
const onlyOneClient = {{ 'true' if only_one_client|default(false) else 'false' }};
const singleClientId = {{ ('"' ~ single_client.id ~ '"') if single_client else 'null' }};
if (projectSelectEl && clientSelectEl) {
// Task loading: attach when project and task selects exist (independent of client)
if (projectSelectEl && taskSelectEl) {
projectSelectEl.addEventListener('change', () => {
const pid = projectSelectEl.value;
if (pid && clientSelectEl) {
@@ -286,7 +295,14 @@ if (projectSelectEl && clientSelectEl) {
}
loadTasksForProject(pid);
});
// Initial load when project is pre-selected
if (projectSelectEl.value) {
loadTasksForProject(projectSelectEl.value);
}
}
// Client/project mutual exclusivity (when client select exists)
if (clientSelectEl) {
clientSelectEl.addEventListener('change', () => {
const cid = clientSelectEl.value;
if (cid) {
@@ -341,14 +357,16 @@ if (timerStartForm) {
}, true); // Use capture phase to run before other handlers
}
// Select recent project
function selectRecentProject(projectId, projectName) {
// Select recent project (exposed for onclick handlers)
window.selectRecentProject = function(projectId, projectName) {
const projectSelect = document.getElementById('project_id');
if (projectSelect) {
projectSelect.value = projectId;
projectSelect.dispatchEvent(new Event('change'));
}
}
};
}); // end DOMContentLoaded
// Update timer display every second
{% if active_timer %}
+7 -1
View File
@@ -399,6 +399,12 @@ document.addEventListener('DOMContentLoaded', function() {
entriesFor: i18nTimerBulk.entries_for || 'Entries will be created for'
};
const tasksApiUrlTemplate = {{ url_for('api.list_tasks_for_project', project_id=0)|tojson }};
function buildTasksUrl(projectId) {
const pid = String(projectId || '').trim();
if (!pid) return tasksApiUrlTemplate;
return String(tasksApiUrlTemplate).replace(/project_id=0/, 'project_id=' + encodeURIComponent(pid));
}
async function loadTasksForProject(projectId) {
if (!projectId) {
taskSelect.innerHTML = '<option value="">' + L.noTask + '</option>';
@@ -406,7 +412,7 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
try {
const resp = await fetch(`/api/tasks?project_id=${projectId}`);
const resp = await fetch(buildTasksUrl(projectId));
if (!resp.ok) throw new Error(L.failedLoad);
const data = await resp.json();
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
+8 -1
View File
@@ -14,6 +14,13 @@ document.addEventListener('DOMContentLoaded', function() {
const projectSelect = document.getElementById('project_id');
const taskSelect = document.getElementById('task_id');
const form = document.querySelector('form');
const tasksApiUrlTemplate = {{ url_for('api.get_project_tasks', project_id=0)|tojson }};
function buildTasksUrl(projectId) {
const pid = String(projectId || '').trim();
if (!pid) return tasksApiUrlTemplate;
return String(tasksApiUrlTemplate).replace(/\/0\/tasks$/, '/' + encodeURIComponent(pid) + '/tasks');
}
if (projectSelect && taskSelect) {
projectSelect.addEventListener('change', function() {
@@ -24,7 +31,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (projectId) {
// Fetch tasks for the selected project
fetch(`/api/projects/${projectId}/tasks`, { credentials: 'same-origin' })
fetch(buildTasksUrl(projectId), { credentials: 'same-origin' })
.then(response => response.json())
.then(data => {
if (data.success && data.tasks) {