abt-cli 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/abt +3 -1
- data/lib/abt.rb +11 -0
- data/lib/abt/cli.rb +25 -29
- data/lib/abt/cli/dialogs.rb +2 -2
- data/lib/abt/cli/io.rb +21 -0
- data/lib/abt/{help.rb → docs.rb} +8 -14
- data/lib/abt/{help → docs}/cli.rb +3 -3
- data/lib/abt/{help → docs}/markdown.rb +3 -3
- data/lib/abt/helpers.rb +16 -0
- data/lib/abt/providers/asana.rb +9 -50
- data/lib/abt/providers/asana/api.rb +57 -0
- data/lib/abt/providers/asana/base_command.rb +5 -12
- data/lib/abt/providers/asana/commands/clear.rb +24 -0
- data/lib/abt/providers/asana/commands/clear_global.rb +24 -0
- data/lib/abt/providers/asana/commands/current.rb +71 -0
- data/lib/abt/providers/asana/commands/harvest_time_entry_data.rb +50 -0
- data/lib/abt/providers/asana/commands/init.rb +63 -0
- data/lib/abt/providers/asana/commands/move.rb +56 -0
- data/lib/abt/providers/asana/commands/pick_task.rb +48 -0
- data/lib/abt/providers/asana/commands/projects.rb +32 -0
- data/lib/abt/providers/asana/commands/start.rb +60 -0
- data/lib/abt/providers/asana/commands/tasks.rb +37 -0
- data/lib/abt/providers/asana/configuration.rb +105 -0
- data/lib/abt/providers/harvest.rb +9 -0
- data/lib/abt/version.rb +1 -1
- metadata +18 -15
- data/lib/abt/asana_client.rb +0 -53
- data/lib/abt/providers/asana/clear.rb +0 -24
- data/lib/abt/providers/asana/clear_global.rb +0 -24
- data/lib/abt/providers/asana/current.rb +0 -69
- data/lib/abt/providers/asana/harvest_link_entry_data.rb +0 -48
- data/lib/abt/providers/asana/init.rb +0 -62
- data/lib/abt/providers/asana/move.rb +0 -54
- data/lib/abt/providers/asana/pick_task.rb +0 -46
- data/lib/abt/providers/asana/projects.rb +0 -30
- data/lib/abt/providers/asana/start.rb +0 -22
- 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
|