1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Add Asana Module (#1052)

* Add Asana Module

* Add Asana Module to the widget_maker

* Asana Module - addressing linting concerns

* Asana Module - ran go mod tidy
This commit is contained in:
Christian Frichot 2021-02-04 16:51:03 +08:00 committed by GitHub
parent 6ecec4f149
commit a8b0fa897a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 618 additions and 3 deletions

View File

@ -3,6 +3,7 @@ package app
import ( import (
"github.com/olebedev/config" "github.com/olebedev/config"
"github.com/rivo/tview" "github.com/rivo/tview"
"github.com/wtfutil/wtf/modules/asana"
"github.com/wtfutil/wtf/modules/azuredevops" "github.com/wtfutil/wtf/modules/azuredevops"
"github.com/wtfutil/wtf/modules/bamboohr" "github.com/wtfutil/wtf/modules/bamboohr"
"github.com/wtfutil/wtf/modules/bargraph" "github.com/wtfutil/wtf/modules/bargraph"
@ -106,6 +107,9 @@ func MakeWidget(
case "arpansagovau": case "arpansagovau":
settings := arpansagovau.NewSettingsFromYAML(moduleName, moduleConfig, config) settings := arpansagovau.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = arpansagovau.NewWidget(tviewApp, settings) widget = arpansagovau.NewWidget(tviewApp, settings)
case "asana":
settings := asana.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = asana.NewWidget(tviewApp, pages, settings)
case "azuredevops": case "azuredevops":
settings := azuredevops.NewSettingsFromYAML(moduleName, moduleConfig, config) settings := azuredevops.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = azuredevops.NewWidget(tviewApp, pages, settings) widget = azuredevops.NewWidget(tviewApp, pages, settings)

2
go.mod
View File

@ -3,6 +3,7 @@ module github.com/wtfutil/wtf
go 1.15 go 1.15
require ( require (
bitbucket.org/mikehouston/asana-go v0.0.0-20201102222432-715318d0343a
code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c code.cloudfoundry.org/bytefmt v0.0.0-20190819182555-854d396b647c
github.com/Azure/go-autorest v11.1.2+incompatible // indirect github.com/Azure/go-autorest v11.1.2+incompatible // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect
@ -31,7 +32,6 @@ require (
github.com/gophercloud/gophercloud v0.5.0 // indirect github.com/gophercloud/gophercloud v0.5.0 // indirect
github.com/hekmon/cunits v2.0.1+incompatible // indirect github.com/hekmon/cunits v2.0.1+incompatible // indirect
github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd
github.com/imdario/mergo v0.3.8 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/jessevdk/go-flags v1.4.0 github.com/jessevdk/go-flags v1.4.0
github.com/lib/pq v1.2.0 // indirect github.com/lib/pq v1.2.0 // indirect

8
go.sum
View File

@ -1,3 +1,5 @@
bitbucket.org/mikehouston/asana-go v0.0.0-20201102222432-715318d0343a h1:qH51iOpTres3x2kNb0f2R3ggMpbYCyCvaRrsvdndhvY=
bitbucket.org/mikehouston/asana-go v0.0.0-20201102222432-715318d0343a/go.mod h1:HcP4iCG6i6uVAyX2X7yKOsjbzLFiTfX0EMT20CYn5Ig=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -388,8 +390,8 @@ github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.0.0-20180119215619-163f41321a19/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.0.0-20180119215619-163f41321a19/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@ -609,6 +611,8 @@ github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= github.com/rubenv/sql-migrate v0.0.0-20160620083229-6f4757563362/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

280
modules/asana/client.go Normal file
View File

@ -0,0 +1,280 @@
package asana
import (
"fmt"
"strings"
"time"
asana "bitbucket.org/mikehouston/asana-go"
)
func fetchTasksFromProject(token, projectId, mode string) ([]*TaskItem, error) {
taskItems := []*TaskItem{}
uidToName := make(map[string]string)
client := asana.NewClientWithAccessToken(token)
uid, err := getCurrentUserId(client, mode)
if err != nil {
return nil, err
}
q := &asana.TaskQuery{
Project: projectId,
}
fetchedTasks, _, err := getTasksFromAsana(client, q)
if err != nil {
return nil, fmt.Errorf("error fetching tasks: %s", err)
}
processFetchedTasks(client, &fetchedTasks, &taskItems, &uidToName, mode, projectId, uid)
return taskItems, nil
}
func fetchTasksFromProjectSections(token, projectId string, sections []string, mode string) ([]*TaskItem, error) {
taskItems := []*TaskItem{}
uidToName := make(map[string]string)
client := asana.NewClientWithAccessToken(token)
uid, err := getCurrentUserId(client, mode)
if err != nil {
return nil, err
}
p := &asana.Project{
ID: projectId,
}
for _, section := range sections {
sectionId, err := findSection(client, p, section)
if err != nil {
return nil, fmt.Errorf("error fetching tasks: %s", err)
}
q := &asana.TaskQuery{
Section: sectionId,
}
fetchedTasks, _, err := getTasksFromAsana(client, q)
if err != nil {
return nil, fmt.Errorf("error fetching tasks: %s", err)
}
if len(fetchedTasks) > 0 {
taskItem := &TaskItem{
name: section,
taskType: TASK_SECTION,
}
taskItems = append(taskItems, taskItem)
}
processFetchedTasks(client, &fetchedTasks, &taskItems, &uidToName, mode, projectId, uid)
}
return taskItems, nil
}
func fetchTasksFromWorkspace(token, workspaceId, mode string) ([]*TaskItem, error) {
taskItems := []*TaskItem{}
uidToName := make(map[string]string)
client := asana.NewClientWithAccessToken(token)
uid, err := getCurrentUserId(client, mode)
if err != nil {
return nil, err
}
q := &asana.TaskQuery{
Workspace: workspaceId,
Assignee: "me",
}
fetchedTasks, _, err := getTasksFromAsana(client, q)
if err != nil {
return nil, fmt.Errorf("error fetching tasks: %s", err)
}
processFetchedTasks(client, &fetchedTasks, &taskItems, &uidToName, mode, workspaceId, uid)
return taskItems, nil
}
func toggleTaskCompletionById(token, taskId string) error {
client := asana.NewClientWithAccessToken(token)
t := &asana.Task{
ID: taskId,
}
err := t.Fetch(client)
if err != nil {
return fmt.Errorf("error fetching task: %s", err)
}
updateReq := &asana.UpdateTaskRequest{}
if *t.Completed {
f := false
updateReq.Completed = &f
} else {
t := true
updateReq.Completed = &t
}
err = t.Update(client, updateReq)
if err != nil {
return fmt.Errorf("error updating task: %s", err)
}
return nil
}
func processFetchedTasks(client *asana.Client, fetchedTasks *[]*asana.Task, taskItems *[]*TaskItem, uidToName *map[string]string, mode, projectId, uid string) {
for _, task := range *fetchedTasks {
switch {
case strings.HasSuffix(mode, "_all"):
if task.Assignee != nil {
// Check if we have already looked up this user
if assigneeName, ok := (*uidToName)[task.Assignee.ID]; ok {
task.Assignee.Name = assigneeName
} else {
// We haven't looked up this user before, perform the lookup now
assigneeName, err := getOtherUserEmail(client, task.Assignee.ID)
if err != nil {
task.Assignee.Name = "Error"
}
(*uidToName)[task.Assignee.ID] = assigneeName
task.Assignee.Name = assigneeName
}
} else {
task.Assignee = &asana.User{
Name: "Unassigned",
}
}
taskItem := buildTaskItem(task, projectId)
(*taskItems) = append((*taskItems), taskItem)
case !strings.HasSuffix(mode, "_all") && task.Assignee != nil && task.Assignee.ID == uid:
taskItem := buildTaskItem(task, projectId)
(*taskItems) = append((*taskItems), taskItem)
}
}
}
func buildTaskItem(task *asana.Task, projectId string) *TaskItem {
dueOnString := ""
if task.DueOn != nil {
dueOn := time.Time(*task.DueOn)
currentYear, _, _ := time.Now().Date()
if currentYear != dueOn.Year() {
dueOnString = dueOn.Format("Jan 2 2006")
} else {
dueOnString = dueOn.Format("Jan 2")
}
}
assignString := ""
if task.Assignee != nil {
assignString = task.Assignee.Name
}
taskItem := &TaskItem{
name: task.Name,
id: task.ID,
numSubtasks: task.NumSubtasks,
dueOn: dueOnString,
url: fmt.Sprintf("https://app.asana.com/0/%s/%s/f", projectId, task.ID),
taskType: TASK_TYPE,
completed: *task.Completed,
assignee: assignString,
}
return taskItem
}
func getOtherUserEmail(client *asana.Client, uid string) (string, error) {
if uid == "" {
return "", fmt.Errorf("missing uid")
}
u := &asana.User{
ID: uid,
}
err := u.Fetch(client, nil)
if err != nil {
return "", fmt.Errorf("error fetching user: %s", err)
}
return u.Email, nil
}
func getCurrentUserId(client *asana.Client, mode string) (string, error) {
if strings.HasSuffix(mode, "_all") {
return "", nil
}
u, err := client.CurrentUser()
if err != nil {
return "", fmt.Errorf("error getting current user: %s", err)
}
return u.ID, nil
}
func findSection(client *asana.Client, project *asana.Project, sectionName string) (string, error) {
sectionId := ""
sections, _, err := project.Sections(client, &asana.Options{
Limit: 100,
})
if err != nil {
return "", fmt.Errorf("error getting sections: %s", err)
}
for _, section := range sections {
if section.Name == sectionName {
sectionId = section.ID
break
}
}
if sectionId == "" {
return "", fmt.Errorf("we didn't find the section %s", sectionName)
}
return sectionId, nil
}
func getTasksFromAsana(client *asana.Client, q *asana.TaskQuery) ([]*asana.Task, bool, error) {
moreTasks := false
tasks, np, err := client.QueryTasks(q, &asana.Options{
Limit: 100,
Fields: []string{
"assignee",
"name",
"num_subtasks",
"due_on",
"completed",
},
})
if err != nil {
return nil, false, fmt.Errorf("error querying tasks: %s", err)
}
if np != nil {
moreTasks = true
}
return tasks, moreTasks, nil
}

20
modules/asana/keyboard.go Normal file
View File

@ -0,0 +1,20 @@
package asana
import "github.com/gdamore/tcell"
func (widget *Widget) initializeKeyboardControls() {
widget.InitializeHelpTextKeyboardControl(widget.ShowHelp)
widget.InitializeRefreshKeyboardControl(widget.Refresh)
widget.SetKeyboardChar("j", widget.Next, "Select next task")
widget.SetKeyboardChar("k", widget.Prev, "Select previous task")
widget.SetKeyboardChar("q", widget.Unselect, "Unselect task")
widget.SetKeyboardChar("o", widget.openTask, "Open task in browser")
widget.SetKeyboardChar("x", widget.toggleTaskCompletion, "Toggles the task's completion state")
widget.SetKeyboardChar("?", widget.ShowHelp, "Shows help")
widget.SetKeyboardKey(tcell.KeyDown, widget.Next, "Select next task")
widget.SetKeyboardKey(tcell.KeyUp, widget.Prev, "Select previous task")
widget.SetKeyboardKey(tcell.KeyEsc, widget.Unselect, "Unselect task")
widget.SetKeyboardKey(tcell.KeyEnter, widget.openTask, "Open task in browser")
}

48
modules/asana/settings.go Normal file
View File

@ -0,0 +1,48 @@
package asana
import (
"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/utils"
)
const (
defaultFocusable = true
defaultTitle = "Asana"
)
type Settings struct {
*cfg.Common
projectId string `help:"The Asana Project ID. If the mode is 'project' or 'project_sections' this is required to known which Asana Project to pull your tasks from" values:"A valid Asana Project ID string" optional:"true"`
workspaceId string `help:"The Asana Workspace ID. If mode is 'workspace' this is required" values:"A valid Asana Workspace ID string" optional:"true"`
sections []string `help:"The Asana Section Labels to fetch from the Project. Required if the mode is 'project_sections'" values:"An array of Asana Section Label strings" optional:"true"`
allUsers bool `help:"Fetch tasks for all users, defaults to false" values:"bool" optional:"true"`
mode string `help:"What mode to query Asana, 'project', 'project_sections', 'workspace'" values:"A string with either 'project', 'project_sections' or 'workspace'"`
hideComplete bool `help:"Hide completed tasks, defaults to false" values:"bool" optional:"true"`
apiKey string `help:"Your Asana Personal Access Token. Leave this blank to use the WTF_ASANA_TOKEN environment variable." values:"Your Asana Personal Access Token as a string" optional:"true"`
token string
}
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
settings := Settings{
Common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig,
globalConfig),
projectId: ymlConfig.UString("projectId", ""),
apiKey: ymlConfig.UString("apiKey", ""),
workspaceId: ymlConfig.UString("workspaceId", ""),
sections: utils.ToStrs(ymlConfig.UList("sections")),
allUsers: ymlConfig.UBool("allUsers", false),
mode: ymlConfig.UString("mode", ""),
hideComplete: ymlConfig.UBool("hideComplete", false),
}
return &settings
}

259
modules/asana/widget.go Normal file
View File

@ -0,0 +1,259 @@
package asana
import (
"fmt"
"os"
"strings"
"sync"
"github.com/rivo/tview"
"github.com/wtfutil/wtf/utils"
"github.com/wtfutil/wtf/view"
)
type TaskType int
const (
TASK_TYPE TaskType = iota
TASK_SECTION
TASK_BREAK
)
type TaskItem struct {
name string
numSubtasks int32
dueOn string
id string
url string
taskType TaskType
completed bool
assignee string
}
type Widget struct {
view.ScrollableWidget
tasks []*TaskItem
mu sync.Mutex
err error
settings *Settings
tviewApp *tview.Application
}
func NewWidget(tviewApp *tview.Application, pages *tview.Pages, settings *Settings) *Widget {
widget := &Widget{
ScrollableWidget: view.NewScrollableWidget(tviewApp, pages, settings.Common),
tviewApp: tviewApp,
settings: settings,
}
widget.SetRenderFunction(widget.Render)
widget.initializeKeyboardControls()
return widget
}
/* -------------------- Exported Functions -------------------- */
func (widget *Widget) Refresh() {
widget.tasks = nil
widget.err = nil
widget.SetItemCount(0)
widget.mu.Lock()
defer widget.mu.Unlock()
tasks, err := widget.Fetch(
widget.settings.workspaceId,
widget.settings.projectId,
widget.settings.mode,
widget.settings.sections,
widget.settings.allUsers,
)
if err != nil {
widget.err = err
} else {
widget.tasks = tasks
widget.SetItemCount(len(tasks))
}
widget.Render()
}
func (widget *Widget) Render() {
widget.Redraw(widget.content)
}
func (widget *Widget) Fetch(workspaceId, projectId, mode string, sections []string, allUsers bool) ([]*TaskItem, error) {
availableModes := map[string]interface{}{
"project": nil,
"project_sections": nil,
"workspace": nil,
}
if _, ok := availableModes[mode]; !ok {
return nil, fmt.Errorf("missing mode, or mode is invalid - please set to project, project_sections or workspace")
}
if widget.settings.apiKey != "" {
widget.settings.token = widget.settings.apiKey
} else {
widget.settings.token = os.Getenv("WTF_ASANA_TOKEN")
}
if widget.settings.token == "" {
return nil, fmt.Errorf("missing environment variable token or apikey config")
}
subMode := mode
if allUsers && mode != "workspace" {
subMode += "_all"
}
if projectId == "" && strings.HasPrefix(subMode, "project") {
return nil, fmt.Errorf("missing project id")
}
if workspaceId == "" && subMode == "workspace" {
return nil, fmt.Errorf("missing workspace id")
}
var tasks []*TaskItem
var err error
switch {
case strings.HasPrefix(subMode, "project_sections"):
tasks, err = fetchTasksFromProjectSections(widget.settings.token, projectId, sections, subMode)
case strings.HasPrefix(subMode, "project"):
tasks, err = fetchTasksFromProject(widget.settings.token, projectId, subMode)
case subMode == "workspace":
tasks, err = fetchTasksFromWorkspace(widget.settings.token, workspaceId, subMode)
default:
err = fmt.Errorf("no mode found")
}
if err != nil {
return nil, err
}
return tasks, nil
}
/* -------------------- Unexported Functions -------------------- */
func (widget *Widget) content() (string, string, bool) {
title := widget.CommonSettings().Title
if widget.err != nil {
return title, widget.err.Error(), true
}
data := widget.tasks
if len(data) == 0 {
return title, "No data", false
}
var str string
for idx, taskItem := range data {
switch {
case taskItem.taskType == TASK_TYPE:
if widget.settings.hideComplete && taskItem.completed {
continue
}
rowColor := widget.RowColor(idx)
completed := "[ []"
if taskItem.completed {
completed = "[x[]"
}
row := ""
if widget.settings.allUsers && taskItem.assignee != "" {
row = fmt.Sprintf(
"[%s] %s %s: %s",
rowColor,
completed,
taskItem.assignee,
taskItem.name,
)
} else {
row = fmt.Sprintf(
"[%s] %s %s",
rowColor,
completed,
taskItem.name,
)
}
if taskItem.numSubtasks > 0 {
row += fmt.Sprintf(" (%d)", taskItem.numSubtasks)
}
if taskItem.dueOn != "" {
row += fmt.Sprintf(" due: %s", taskItem.dueOn)
}
row += " [white]"
str += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))
case taskItem.taskType == TASK_SECTION:
if idx > 1 {
row := "[white] "
str += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))
}
row := fmt.Sprintf(
"[white] %s [white]",
taskItem.name,
)
str += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))
row = "[white] "
str += utils.HighlightableHelper(widget.View, row, idx, len(taskItem.name))
}
}
return title, str, false
}
func (widget *Widget) openTask() {
sel := widget.GetSelected()
if sel >= 0 && widget.tasks != nil && sel < len(widget.tasks) {
task := widget.tasks[sel]
if task.taskType == TASK_TYPE && task.url != "" {
utils.OpenFile(task.url)
}
}
}
func (widget *Widget) toggleTaskCompletion() {
sel := widget.GetSelected()
if sel >= 0 && widget.tasks != nil && sel < len(widget.tasks) {
task := widget.tasks[sel]
if task.taskType == TASK_TYPE {
widget.mu.Lock()
err := toggleTaskCompletionById(widget.settings.token, task.id)
if err != nil {
widget.err = err
}
widget.mu.Unlock()
widget.Refresh()
}
}
}