From 9b900b640b3f26b93b7c2d380d2e17f90a82787a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:33:10 +0000 Subject: [PATCH] Create separate endpoints for add-label and remove-label operations Co-authored-by: Mord0reK <135718526+Mord0reK@users.noreply.github.com> --- internal/glance/glance.go | 99 ++++++++++++++++++++++-- internal/glance/static/js/vikunja.js | 109 ++++++++++++++++++--------- internal/glance/widget-vikunja.go | 14 +--- 3 files changed, 169 insertions(+), 53 deletions(-) diff --git a/internal/glance/glance.go b/internal/glance/glance.go index a751ff4..559b486 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -453,6 +453,8 @@ func (a *application) server() (func() error, func() error) { mux.HandleFunc("GET /api/audio-proxy", a.handleAudioProxyRequest) mux.HandleFunc("POST /api/vikunja/{widgetID}/complete-task", a.handleVikunjaCompleteTask) mux.HandleFunc("POST /api/vikunja/{widgetID}/update-task", a.handleVikunjaUpdateTask) + mux.HandleFunc("POST /api/vikunja/{widgetID}/add-label", a.handleVikunjaAddLabel) + mux.HandleFunc("POST /api/vikunja/{widgetID}/remove-label", a.handleVikunjaRemoveLabel) mux.HandleFunc("GET /api/vikunja/{widgetID}/labels", a.handleVikunjaGetLabels) if a.RequiresAuth { @@ -587,10 +589,9 @@ func (a *application) handleVikunjaUpdateTask(w http.ResponseWriter, r *http.Req } var request struct { - TaskID int `json:"task_id"` - Title string `json:"title"` - DueDate string `json:"due_date"` - LabelIDs []int `json:"label_ids"` + TaskID int `json:"task_id"` + Title string `json:"title"` + DueDate string `json:"due_date"` } if err := json.NewDecoder(r.Body).Decode(&request); err != nil { @@ -599,7 +600,95 @@ func (a *application) handleVikunjaUpdateTask(w http.ResponseWriter, r *http.Req return } - if err := vikunjaWidget.updateTask(request.TaskID, request.Title, request.DueDate, request.LabelIDs); err != nil { + if err := vikunjaWidget.updateTaskBasic(request.TaskID, request.Title, request.DueDate); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"success": true}`)) +} + +func (a *application) handleVikunjaAddLabel(w http.ResponseWriter, r *http.Request) { + widgetIDStr := r.PathValue("widgetID") + widgetID, err := strconv.ParseUint(widgetIDStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid widget ID")) + return + } + + widget, exists := a.widgetByID[widgetID] + if !exists { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Widget not found")) + return + } + + vikunjaWidget, ok := widget.(*vikunjaWidget) + if !ok { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Widget is not a Vikunja widget")) + return + } + + var request struct { + TaskID int `json:"task_id"` + LabelID int `json:"label_id"` + } + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid request body")) + return + } + + if err := vikunjaWidget.addLabelToTask(request.TaskID, request.LabelID); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"success": true}`)) +} + +func (a *application) handleVikunjaRemoveLabel(w http.ResponseWriter, r *http.Request) { + widgetIDStr := r.PathValue("widgetID") + widgetID, err := strconv.ParseUint(widgetIDStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid widget ID")) + return + } + + widget, exists := a.widgetByID[widgetID] + if !exists { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Widget not found")) + return + } + + vikunjaWidget, ok := widget.(*vikunjaWidget) + if !ok { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Widget is not a Vikunja widget")) + return + } + + var request struct { + TaskID int `json:"task_id"` + LabelID int `json:"label_id"` + } + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid request body")) + return + } + + if err := vikunjaWidget.removeLabelFromTask(request.TaskID, request.LabelID); err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return diff --git a/internal/glance/static/js/vikunja.js b/internal/glance/static/js/vikunja.js index 19368a2..b0a7a08 100644 --- a/internal/glance/static/js/vikunja.js +++ b/internal/glance/static/js/vikunja.js @@ -143,7 +143,7 @@ function openEditModal(widgetID, taskID, title, dueDate, currentLabelIDs, row) { saveBtn.removeEventListener('click', saveTask); } - function saveTask() { + async function saveTask() { const newTitle = titleInput.value.trim(); const newDueDate = dueDateInput.value; @@ -163,31 +163,76 @@ function openEditModal(widgetID, taskID, title, dueDate, currentLabelIDs, row) { formattedDueDate = date.toISOString(); } - // Call API to update task - fetch(`${pageData.baseURL}/api/vikunja/${widgetID}/update-task`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - task_id: taskID, - title: newTitle, - due_date: formattedDueDate, - label_ids: selectedLabels - }) - }) - .then(response => { - if (!response.ok) throw new Error('Failed to update task'); - return response.json(); - }) - .then(data => { - // Update the row with new data + try { + // Step 1: Update title and due date + const updateResponse = await fetch(`${pageData.baseURL}/api/vikunja/${widgetID}/update-task`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + task_id: taskID, + title: newTitle, + due_date: formattedDueDate + }) + }); + + if (!updateResponse.ok) { + throw new Error('Failed to update task'); + } + + // Step 2: Update labels + // Get current label IDs from the row + const currentLabels = Array.from(row.querySelectorAll('.label')).map(label => + parseInt(label.dataset.labelId) + ); + + // Determine which labels to add and remove + const labelsToAdd = selectedLabels.filter(id => !currentLabels.includes(id)); + const labelsToRemove = currentLabels.filter(id => !selectedLabels.includes(id)); + + // Add new labels + for (const labelID of labelsToAdd) { + const addResponse = await fetch(`${pageData.baseURL}/api/vikunja/${widgetID}/add-label`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + task_id: taskID, + label_id: labelID + }) + }); + + if (!addResponse.ok) { + throw new Error(`Failed to add label ${labelID}`); + } + } + + // Remove old labels + for (const labelID of labelsToRemove) { + const removeResponse = await fetch(`${pageData.baseURL}/api/vikunja/${widgetID}/remove-label`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + task_id: taskID, + label_id: labelID + }) + }); + + if (!removeResponse.ok) { + throw new Error(`Failed to remove label ${labelID}`); + } + } + + // Update the UI const titleCell = row.querySelector('.vikunja-title'); if (titleCell) { titleCell.textContent = newTitle; } - // Update the edit button's data attributes const editBtn = row.querySelector('.vikunja-edit-btn'); if (editBtn) { editBtn.dataset.taskTitle = newTitle; @@ -196,23 +241,13 @@ function openEditModal(widgetID, taskID, title, dueDate, currentLabelIDs, row) { } } - // Update labels (simplified - would need full refresh for accurate display) - const labelsCell = row.querySelector('.vikunja-labels'); - if (labelsCell && selectedLabels.length > 0) { - const labelContainer = labelsCell.querySelector('.label-container'); - if (labelContainer) { - // For now, just show that labels were updated - // A full page refresh would show the actual labels - alert('Zadanie zostało zaktualizowane. Odśwież stronę, aby zobaczyć wszystkie zmiany.'); - } - } - - closeModal(); - }) - .catch(error => { + // Refresh page to show updated labels + alert('Zadanie zostało zaktualizowane. Strona zostanie odświeżona.'); + location.reload(); + } catch (error) { console.error('Error updating task:', error); - alert('Nie udało się zaktualizować zadania'); - }); + alert('Nie udało się zaaktualizować zadania: ' + error.message); + } } closeBtn.addEventListener('click', closeModal); diff --git a/internal/glance/widget-vikunja.go b/internal/glance/widget-vikunja.go index 8acfa00..f3c885f 100644 --- a/internal/glance/widget-vikunja.go +++ b/internal/glance/widget-vikunja.go @@ -226,8 +226,7 @@ func (widget *vikunjaWidget) completeTask(taskID int) error { return err } -func (widget *vikunjaWidget) updateTask(taskID int, title string, dueDate string, labelIDs []int) error { - // First, update the task basic properties (title, due_date) +func (widget *vikunjaWidget) updateTaskBasic(taskID int, title string, dueDate string) error { url := fmt.Sprintf("%s/api/v1/tasks/%d", widget.URL, taskID) payload := map[string]interface{}{ @@ -252,15 +251,8 @@ func (widget *vikunjaWidget) updateTask(taskID int, title string, dueDate string request.Header.Set("Authorization", "Bearer "+widget.Token) request.Header.Set("Content-Type", "application/json") - task, err := decodeJsonFromRequest[vikunjaAPITask](defaultHTTPClient, request) - if err != nil { - return err - } - - // Second, handle labels separately using the dedicated labels endpoint - // Vikunja requires individual PUT requests to add labels - // and DELETE requests to remove labels - return widget.updateTaskLabels(taskID, task.Labels, labelIDs) + _, err = decodeJsonFromRequest[vikunjaAPITask](defaultHTTPClient, request) + return err } func (widget *vikunjaWidget) updateTaskLabels(taskID int, currentLabels []vikunjaAPILabel, desiredLabelIDs []int) error {