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:
parent
6ecec4f149
commit
a8b0fa897a
@ -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
2
go.mod
@ -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
8
go.sum
@ -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
280
modules/asana/client.go
Normal 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
20
modules/asana/keyboard.go
Normal 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
48
modules/asana/settings.go
Normal 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
259
modules/asana/widget.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user