Poprawki vikunja:

- Dodanie mozliwosci ustawienia projektu przy dodawaniu zadania
- poprawka pobierania zadan
pull/878/head
Bartosz Nikitiuk 2025-11-19 11:12:48 +07:00
parent 7b95f0b949
commit 34790df449
No known key found for this signature in database
GPG Key ID: 725EADAB8CDD0DB1
4 changed files with 120 additions and 13 deletions

@ -456,6 +456,7 @@ func (a *application) server() (func() error, func() error) {
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)
mux.HandleFunc("GET /api/vikunja/{widgetID}/projects", a.handleVikunjaGetProjects)
mux.HandleFunc("GET /api/vikunja/{widgetID}/refresh", a.handleVikunjaRefresh)
mux.HandleFunc("POST /api/vikunja/{widgetID}/create-task", a.handleVikunjaCreateTask)
@ -734,6 +735,40 @@ func (a *application) handleVikunjaGetLabels(w http.ResponseWriter, r *http.Requ
json.NewEncoder(w).Encode(labels)
}
func (a *application) handleVikunjaGetProjects(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
}
projects, err := vikunjaWidget.fetchProjects()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(projects)
}
func (a *application) handleVikunjaRefresh(w http.ResponseWriter, r *http.Request) {
widgetIDStr := r.PathValue("widgetID")
widgetID, err := strconv.ParseUint(widgetIDStr, 10, 64)
@ -792,9 +827,10 @@ func (a *application) handleVikunjaCreateTask(w http.ResponseWriter, r *http.Req
}
var request struct {
Title string `json:"title"`
DueDate string `json:"due_date"`
LabelIDs []int `json:"label_ids"`
Title string `json:"title"`
DueDate string `json:"due_date"`
LabelIDs []int `json:"label_ids"`
ProjectID int `json:"project_id"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
@ -803,7 +839,7 @@ func (a *application) handleVikunjaCreateTask(w http.ResponseWriter, r *http.Req
return
}
task, err := vikunjaWidget.createTask(request.Title, request.DueDate, request.LabelIDs)
task, err := vikunjaWidget.createTask(request.Title, request.DueDate, request.LabelIDs, request.ProjectID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Failed to create task: %v", err)))

@ -303,11 +303,34 @@ function openCreateModal(widgetID) {
const modal = document.getElementById('vikunja-create-modal');
const titleInput = document.getElementById('vikunja-create-title');
const dueDateInput = document.getElementById('vikunja-create-due-date');
const projectSelect = document.getElementById('vikunja-create-project');
const labelsContainer = document.getElementById('vikunja-create-labels-container');
// Clear the form
titleInput.value = '';
dueDateInput.value = '';
// Fetch and populate projects
projectSelect.innerHTML = '<option value="">Ładowanie...</option>';
fetch(`${pageData.baseURL}/api/vikunja/${widgetID}/projects`)
.then(response => response.json())
.then(projects => {
projectSelect.innerHTML = '<option value="">Domyślny projekt</option>';
if (projects && projects.length > 0) {
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = project.title;
projectSelect.appendChild(option);
});
}
})
.catch(error => {
console.error('Error fetching projects:', error);
projectSelect.innerHTML = '<option value="">Błąd ładowania projektów</option>';
});
// Fetch and display labels
labelsContainer.innerHTML = '<p>Ładowanie etykiet...</p>';
@ -360,6 +383,7 @@ function openCreateModal(widgetID) {
async function createTask() {
const title = titleInput.value.trim();
const dueDate = dueDateInput.value;
const projectID = projectSelect.value ? parseInt(projectSelect.value) : 0;
// Get selected label IDs
const selectedLabels = Array.from(labelsContainer.querySelectorAll('input[type="checkbox"]:checked'))
@ -387,7 +411,8 @@ function openCreateModal(widgetID) {
body: JSON.stringify({
title: title,
due_date: formattedDueDate,
label_ids: selectedLabels
label_ids: selectedLabels,
project_id: projectID
})
});

@ -79,6 +79,12 @@
<label for="vikunja-create-due-date">Termin:</label>
<input type="datetime-local" id="vikunja-create-due-date" class="vikunja-input">
</div>
<div class="vikunja-form-group">
<label for="vikunja-create-project">Projekt:</label>
<select id="vikunja-create-project" class="vikunja-input">
<option value="">Domyślny projekt</option>
</select>
</div>
<div class="vikunja-form-group">
<label>Etykiety:</label>
<div id="vikunja-create-labels-container" class="vikunja-labels-selector">

@ -8,6 +8,7 @@ import (
"html/template"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
@ -56,6 +57,11 @@ type vikunjaAPILabel struct {
HexColor string `json:"hex_color"`
}
type vikunjaProject struct {
ID int `json:"id"`
Title string `json:"title"`
}
func (widget *vikunjaWidget) initialize() error {
widget.withTitle("Vikunja").withCacheDuration(5 * time.Minute)
@ -89,9 +95,20 @@ func (widget *vikunjaWidget) update(ctx context.Context) {
}
func (widget *vikunjaWidget) fetchTasks() ([]vikunjaTask, error) {
url := widget.URL + "/api/v1/tasks/all"
fullURL := widget.URL + "/api/v1/tasks/all"
request, err := http.NewRequest("GET", url, nil)
u, err := url.Parse(fullURL)
if err != nil {
return nil, err
}
q := u.Query()
q.Set("sort_by", "due_date")
q.Set("order_by", "asc")
q.Set("limit", "250")
u.RawQuery = q.Encode()
request, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
@ -331,14 +348,14 @@ func (widget *vikunjaWidget) addLabelToTask(taskID int, labelID int) error {
if response.StatusCode < 200 || response.StatusCode >= 300 {
body, _ := io.ReadAll(response.Body)
bodyStr := string(body)
// Vikunja returns error code 8001 when label already exists
// This is not an error for us - we want the label on the task
if response.StatusCode == 400 && (strings.Contains(bodyStr, "8001") || strings.Contains(bodyStr, "already exists")) {
// Label already exists, which is fine - we wanted it there anyway
return nil
}
return fmt.Errorf("unexpected status code %d: %s", response.StatusCode, bodyStr)
}
@ -390,9 +407,32 @@ func (widget *vikunjaWidget) fetchAllLabels() ([]vikunjaAPILabel, error) {
return labels, nil
}
func (widget *vikunjaWidget) createTask(title string, dueDate string, labelIDs []int) (*vikunjaAPITask, error) {
// Use the configured project ID for creating tasks
url := fmt.Sprintf("%s/api/v1/projects/%d/tasks", widget.URL, widget.ProjectID)
func (widget *vikunjaWidget) fetchProjects() ([]vikunjaProject, error) {
url := widget.URL + "/api/v1/projects"
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+widget.Token)
projects, err := decodeJsonFromRequest[[]vikunjaProject](defaultHTTPClient, request)
if err != nil {
return nil, err
}
return projects, nil
}
func (widget *vikunjaWidget) createTask(title string, dueDate string, labelIDs []int, projectID int) (*vikunjaAPITask, error) {
// Use the configured project ID for creating tasks unless a specific project ID is provided
targetProjectID := widget.ProjectID
if projectID > 0 {
targetProjectID = projectID
}
url := fmt.Sprintf("%s/api/v1/projects/%d/tasks", widget.URL, targetProjectID)
// Build payload matching Vikunja API structure
// Based on Vikunja API documentation and user-provided payload structure
@ -403,7 +443,7 @@ func (widget *vikunjaWidget) createTask(title string, dueDate string, labelIDs [
"done": false,
"priority": 0,
"labels": []interface{}{}, // Empty - labels added separately
"project_id": widget.ProjectID,
"project_id": targetProjectID,
}
// Add due_date if provided