Create separate endpoints for add-label and remove-label operations

Co-authored-by: Mord0reK <135718526+Mord0reK@users.noreply.github.com>
pull/878/head
copilot-swe-agent[bot] 2025-11-16 20:33:10 +07:00
parent 5dd016ad27
commit 9b900b640b
3 changed files with 169 additions and 53 deletions

@ -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

@ -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);

@ -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 {