abt-cli 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/bin/abt +3 -1
  3. data/lib/abt.rb +11 -0
  4. data/lib/abt/cli.rb +25 -29
  5. data/lib/abt/cli/dialogs.rb +2 -2
  6. data/lib/abt/cli/io.rb +21 -0
  7. data/lib/abt/{help.rb → docs.rb} +8 -14
  8. data/lib/abt/{help → docs}/cli.rb +3 -3
  9. data/lib/abt/{help → docs}/markdown.rb +3 -3
  10. data/lib/abt/helpers.rb +16 -0
  11. data/lib/abt/providers/asana.rb +9 -50
  12. data/lib/abt/providers/asana/api.rb +57 -0
  13. data/lib/abt/providers/asana/base_command.rb +5 -12
  14. data/lib/abt/providers/asana/commands/clear.rb +24 -0
  15. data/lib/abt/providers/asana/commands/clear_global.rb +24 -0
  16. data/lib/abt/providers/asana/commands/current.rb +71 -0
  17. data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
  18. data/lib/abt/providers/asana/commands/init.rb +63 -0
  19. data/lib/abt/providers/asana/commands/move.rb +56 -0
  20. data/lib/abt/providers/asana/commands/pick_task.rb +48 -0
  21. data/lib/abt/providers/asana/commands/projects.rb +32 -0
  22. data/lib/abt/providers/asana/commands/start.rb +60 -0
  23. data/lib/abt/providers/asana/commands/tasks.rb +37 -0
  24. data/lib/abt/providers/asana/configuration.rb +105 -0
  25. data/lib/abt/providers/harvest.rb +9 -0
  26. data/lib/abt/version.rb +1 -1
  27. metadata +18 -15
  28. data/lib/abt/asana_client.rb +0 -53
  29. data/lib/abt/providers/asana/clear.rb +0 -24
  30. data/lib/abt/providers/asana/clear_global.rb +0 -24
  31. data/lib/abt/providers/asana/current.rb +0 -69
  32. data/lib/abt/providers/asana/harvest_link_entry_data.rb +0 -48
  33. data/lib/abt/providers/asana/init.rb +0 -62
  34. data/lib/abt/providers/asana/move.rb +0 -54
  35. data/lib/abt/providers/asana/pick_task.rb +0 -46
  36. data/lib/abt/providers/asana/projects.rb +0 -30
  37. data/lib/abt/providers/asana/start.rb +0 -22
  38. data/lib/abt/providers/asana/tasks.rb +0 -35
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Clear < BaseCommand
8
+ def self.command
9
+ 'clear asana'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear project/task for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing Asana project configuration'
18
+ config.clear_local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class ClearGlobal < BaseCommand
8
+ def self.command
9
+ 'clear-global asana'
10
+ end
11
+
12
+ def self.description
13
+ 'Clear all global configuration'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Clearing Asana project configuration'
18
+ config.clear_global
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Current < BaseCommand
8
+ def self.command
9
+ 'current asana[:<project-gid>[/<task-gid>]]'
10
+ end
11
+
12
+ def self.description
13
+ 'Get or set project and or task for current git repository'
14
+ end
15
+
16
+ def call
17
+ if arg_str.nil?
18
+ show_current_configuration
19
+ else
20
+ cli.warn 'Updating configuration'
21
+ update_configuration
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def show_current_configuration
28
+ if project_gid.nil?
29
+ cli.warn 'No project selected'
30
+ elsif task_gid.nil?
31
+ print_project(project)
32
+ else
33
+ print_task(project, task)
34
+ end
35
+ end
36
+
37
+ def update_configuration
38
+ ensure_project_is_valid!
39
+ config.project_gid = project_gid
40
+
41
+ if task_gid.nil?
42
+ print_project(project)
43
+ config.task_gid = nil
44
+ else
45
+ ensure_task_is_valid!
46
+ config.task_gid task_gid
47
+
48
+ print_task(project, task)
49
+ end
50
+ end
51
+
52
+ def ensure_project_is_valid!
53
+ cli.abort "Invalid project: #{project_gid}" if project.nil?
54
+ end
55
+
56
+ def ensure_task_is_valid!
57
+ cli.abort "Invalid task: #{task_gid}" if task.nil?
58
+ end
59
+
60
+ def project
61
+ @project ||= api.get("projects/#{project_gid}")
62
+ end
63
+
64
+ def task
65
+ @task ||= api.get("tasks/#{task_gid}")
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class HarvestTimeEntryData < BaseCommand
8
+ def self.command
9
+ 'harvest-time-entry-data asana[:<project-gid>/<task-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Print Harvest time entry data for Asana task as json. Used by harvest start script.'
14
+ end
15
+
16
+ def call
17
+ ensure_current_is_valid!
18
+
19
+ body = {
20
+ notes: task['name'],
21
+ external_reference: {
22
+ id: task_gid.to_i,
23
+ group_id: project_gid.to_i,
24
+ permalink: task['permalink_url'],
25
+ service: 'app.asana.com',
26
+ service_icon_url: 'https://proxy.harvestfiles.com/production_harvestapp_public/uploads/platform_icons/app.asana.com.png'
27
+ }
28
+ }
29
+
30
+ cli.puts Oj.dump(body, mode: :json)
31
+ end
32
+
33
+ private
34
+
35
+ def ensure_current_is_valid!
36
+ cli.abort "Invalid task gid: #{task_gid}" if task.nil?
37
+
38
+ return if task['memberships'].any? { |m| m.dig('project', 'gid') == project_gid }
39
+
40
+ cli.abort "Invalid project gid: #{project_gid}"
41
+ end
42
+
43
+ def task
44
+ @task ||= api.get("tasks/#{task_gid}")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Init < BaseCommand
8
+ def self.command
9
+ 'init asana'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick Asana project for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn 'Loading projects'
18
+
19
+ projects # Load projects up front to make it obvious that searches are instant
20
+ project = find_search_result
21
+
22
+ config.project_gid = project['gid']
23
+
24
+ print_project(project)
25
+ end
26
+
27
+ private
28
+
29
+ def find_search_result
30
+ loop do
31
+ matches = matches_for_string cli.prompt('Enter search')
32
+ if matches.empty?
33
+ cli.warn 'No matches'
34
+ next
35
+ end
36
+
37
+ cli.warn 'Showing the 10 first matches' if matches.size > 10
38
+ choice = cli.prompt_choice 'Select a project', matches[0...10], true
39
+ break choice unless choice.nil?
40
+ end
41
+ end
42
+
43
+ def matches_for_string(string)
44
+ search_string = sanitize_string(string)
45
+
46
+ projects.select do |project|
47
+ sanitize_string(project['name']).include?(search_string)
48
+ end
49
+ end
50
+
51
+ def sanitize_string(string)
52
+ string.downcase.gsub(/[^\w]/, '')
53
+ end
54
+
55
+ def projects
56
+ @projects ||=
57
+ api.get_paged('projects', workspace: config.workspace_gid, archived: false)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Move < BaseCommand
8
+ def self.command
9
+ 'move asana[:<project-gid>/<task-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Move current or specified task to another section (column)'
14
+ end
15
+
16
+ def call
17
+ print_task(project, task)
18
+
19
+ move_task
20
+
21
+ cli.warn "Asana task moved to #{section['name']}"
22
+ rescue Abt::HttpError::HttpError => e
23
+ cli.warn e
24
+ cli.abort 'Unable to move asana task'
25
+ end
26
+
27
+ private
28
+
29
+ def task
30
+ @task ||= api.get("tasks/#{task_gid}")
31
+ end
32
+
33
+ def move_task
34
+ body = { data: { task: task_gid } }
35
+ body_json = Oj.dump(body, mode: :json)
36
+ api.post("sections/#{section['gid']}/addTask", body_json)
37
+ end
38
+
39
+ def section
40
+ @section ||= cli.prompt_choice 'Move asana task to?', sections
41
+ end
42
+
43
+ def project
44
+ @project ||= api.get("projects/#{project_gid}")
45
+ end
46
+
47
+ def sections
48
+ api.get_paged("projects/#{project_gid}/sections")
49
+ rescue Abt::HttpError::HttpError
50
+ []
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class PickTask < BaseCommand
8
+ def self.command
9
+ 'pick-task asana[:<project-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Pick task for current git repository'
14
+ end
15
+
16
+ def call
17
+ cli.warn project['name']
18
+ task = cli.prompt_choice 'Select a task', tasks
19
+
20
+ config.project_gid = project_gid # We might have gotten the project ID as an argument
21
+ config.task_gid = task['gid']
22
+
23
+ print_task(project, task)
24
+ end
25
+
26
+ private
27
+
28
+ def project
29
+ @project ||= api.get("projects/#{project_gid}")
30
+ end
31
+
32
+ def tasks
33
+ @tasks ||= begin
34
+ section = cli.prompt_choice 'Which section?', sections
35
+ api.get_paged('tasks', section: section['gid'])
36
+ end
37
+ end
38
+
39
+ def sections
40
+ api.get_paged("projects/#{project_gid}/sections")
41
+ rescue Abt::HttpError::HttpError
42
+ []
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Projects < BaseCommand
8
+ def self.command
9
+ 'projects asana'
10
+ end
11
+
12
+ def self.description
13
+ 'List all available projects - useful for piping into grep etc.'
14
+ end
15
+
16
+ def call
17
+ projects.map do |project|
18
+ print_project(project)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def projects
25
+ @projects ||=
26
+ api.get_paged('projects', workspace: config.workspace_gid, archived: false)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Start < BaseCommand
8
+ def self.command
9
+ 'start asana[:<project-gid>/<task-gid>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Set current task and move it to a section (column) of your choice'
14
+ end
15
+
16
+ def call
17
+ Current.new(arg_str: arg_str, cli: cli).call unless arg_str.nil?
18
+
19
+ if task_already_in_wip_section?
20
+ cli.warn "Task already in #{current_task_section['name']}"
21
+ else
22
+ cli.warn "Moving task to #{wip_section['name']}"
23
+ move_task
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def task_already_in_wip_section?
30
+ !task_section_membership.nil?
31
+ end
32
+
33
+ def current_task_section
34
+ task_section_membership&.dig('section')
35
+ end
36
+
37
+ def task_section_membership
38
+ task['memberships'].find do |membership|
39
+ membership.dig('section', 'gid') == config.wip_section_gid
40
+ end
41
+ end
42
+
43
+ def wip_section
44
+ @wip_section ||= api.get("sections/#{config.wip_section_gid}")
45
+ end
46
+
47
+ def move_task
48
+ body = { data: { task: task_gid } }
49
+ body_json = Oj.dump(body, mode: :json)
50
+ api.post("sections/#{config.wip_section_gid}/addTask", body_json)
51
+ end
52
+
53
+ def task
54
+ @task ||= api.get("tasks/#{task_gid}")
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Asana
6
+ module Commands
7
+ class Tasks < BaseCommand
8
+ def self.command
9
+ 'tasks asana'
10
+ end
11
+
12
+ def self.description
13
+ 'List available tasks on project - useful for piping into grep etc.'
14
+ end
15
+
16
+ def call
17
+ tasks.each do |task|
18
+ print_task(project, task)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def project
25
+ @project ||= begin
26
+ api.get("projects/#{project_gid}")
27
+ end
28
+ end
29
+
30
+ def tasks
31
+ @tasks ||= api.get_paged('tasks', project: project['gid'])
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end