abt-cli 0.0.4 → 0.0.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c8beb7e6080443354e8ddb0d8f00106694731d931283494e1337daee0547c9a
4
- data.tar.gz: df7840006160bbdb850bdd475496202848080ec118753be8df00f99bc5aa7c3f
3
+ metadata.gz: 5044c8e5cb4330dd9db124834ee6f9bffca78a0ffaa08c268ba35eb3b0a21fe9
4
+ data.tar.gz: d9eb98460e021a032f9efdc0d9b2b98a20b57aba6267ef9024ee6f840bcf6858
5
5
  SHA512:
6
- metadata.gz: e6f1c8df5509096dc414acff24952b57d656b064c95777f2741ada1164576c9ef6fe20bd7a13a50c8a0d30784c92d1d56ff000bf853972c5f8ee8334e69c538d
7
- data.tar.gz: 8d3c36f885f175927efb5418547e782fcd864e81646602d8473b435222f3405f89973337b9368b718ff8413b8a8a0e714d90d2ce0e784fe62cbf02bc98bf734c
6
+ metadata.gz: e2536f6b91bebf10d0fe637864045f61d04f90aa1b1ede6bd0a0db62ad54ef54cb541eda71fca2554cd741fe0011cb5586a0bb904ec412a6a07d90fc6c477208
7
+ data.tar.gz: 10c5fcf70d15dcfff152cf62a3d39109db499768c5fbb96d0eadecc107c85150e9277f647da482ea72d6d2cab68974b950500c2e433a0a3f8c1547c6c6fe74a0
data/bin/abt CHANGED
@@ -5,6 +5,7 @@ require 'dry-inflector'
5
5
  require 'faraday'
6
6
  require 'oj'
7
7
  require 'open3'
8
+ require 'stringio'
8
9
 
9
10
  require_relative '../lib/abt.rb'
10
11
 
@@ -60,7 +60,7 @@ module Abt
60
60
  def args_from_stdin
61
61
  input = STDIN.read
62
62
 
63
- return [] if input.nil?
63
+ abort 'No input from pipe' if input.nil? || input.empty?
64
64
 
65
65
  # Exclude comment part of piped input lines
66
66
  lines_without_comments = input.lines.map do |line|
@@ -25,13 +25,15 @@ module Abt
25
25
  end
26
26
 
27
27
  def prompt_choice(text, options, allow_back_option = false)
28
- if options.one?
29
- warn "Selected: #{options.first['name']}"
30
- return options.first
31
- end
32
-
33
28
  warn "#{text}:"
34
29
 
30
+ if options.length.zero?
31
+ abort 'No available options' unless allow_back_option
32
+
33
+ warn 'No available options'
34
+ return nil
35
+ end
36
+
35
37
  print_options(options)
36
38
  select_options(options, allow_back_option)
37
39
  end
@@ -45,11 +47,12 @@ module Abt
45
47
  end
46
48
 
47
49
  def select_options(options, allow_back_option)
48
- while (number = read_option_number(options.length, allow_back_option))
50
+ loop do
51
+ number = read_option_number(options.length, allow_back_option)
49
52
  if number.nil?
50
53
  return nil if allow_back_option
51
54
 
52
- abort
55
+ next
53
56
  end
54
57
 
55
58
  option = options[number - 1]
@@ -17,6 +17,10 @@ module Abt
17
17
  'abt start asana harvest' => 'Continue working, e.g. after a break',
18
18
  'abt finalize asana' => 'Finalize the selected asana task'
19
19
  },
20
+ 'Tracking meetings (without changing the config):' => {
21
+ 'abt tasks asana | grep -i standup | abt track harvest' => 'Track on asana meeting task without changing any configuration',
22
+ 'abt tasks harvest | grep -i comment | abt track harvest' => 'Track on harvest "Comment"-task (will prompt for a comment)'
23
+ },
20
24
  'Command output can be piped, e.g.:' => {
21
25
  'abt tasks asana | grep -i <name of task>' => nil,
22
26
  'abt tasks asana | grep -i <name of task> | abt start' => nil
@@ -27,6 +27,8 @@ module Abt
27
27
  private
28
28
 
29
29
  def find_search_result
30
+ cli.warn 'Select a project'
31
+
30
32
  loop do
31
33
  matches = matches_for_string cli.prompt('Enter search')
32
34
  if matches.empty?
@@ -18,7 +18,7 @@ module Abt
18
18
 
19
19
  cli.warn project['name']
20
20
 
21
- task = cli.prompt_choice 'Select a task', tasks
21
+ task = select_task
22
22
 
23
23
  config.project_gid = project_gid # We might have gotten the project ID as an argument
24
24
  config.task_gid = task['gid']
@@ -32,14 +32,26 @@ module Abt
32
32
  @project ||= api.get("projects/#{project_gid}")
33
33
  end
34
34
 
35
- def tasks
36
- @tasks ||= begin
35
+ def select_task
36
+ loop do
37
37
  section = cli.prompt_choice 'Which section?', sections
38
38
  cli.warn 'Fetching tasks...'
39
- api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
39
+ tasks = tasks_in_section(section)
40
+
41
+ if tasks.length.zero?
42
+ cli.warn 'Section is empty'
43
+ next
44
+ end
45
+
46
+ task = cli.prompt_choice 'Select a task', tasks, true
47
+ return task if task
40
48
  end
41
49
  end
42
50
 
51
+ def tasks_in_section(section)
52
+ api.get_paged('tasks', section: section['gid'], opt_fields: 'name,permalink_url')
53
+ end
54
+
43
55
  def sections
44
56
  @sections ||= begin
45
57
  cli.warn 'Fetching sections...'
@@ -109,9 +109,13 @@ module Abt
109
109
  workspaces = api.get_paged('workspaces')
110
110
  if workspaces.empty?
111
111
  cli.abort 'Your asana access token does not have access to any workspaces'
112
+ elsif workspaces.one?
113
+ workspace = workspaces.first
114
+ cli.warn "Selected Asana workspace #{workspace['name']}"
115
+ else
116
+ workspace = cli.prompt_choice('Select Asana workspace', workspaces)
112
117
  end
113
118
 
114
- workspace = cli.prompt_choice('Select Asana workspace', workspaces)
115
119
  git.global['workspaceGid'] = workspace['gid']
116
120
  workspace
117
121
  end
@@ -13,11 +13,9 @@ module Abt
13
13
  'Clear project/task for current git repository'
14
14
  end
15
15
 
16
- def initialize(**); end
17
-
18
16
  def call
19
17
  cli.warn 'Clearing Harvest project configuration'
20
- config.clear
18
+ config.clear_local
21
19
  end
22
20
  end
23
21
  end
@@ -58,22 +58,24 @@ module Abt
58
58
  end
59
59
 
60
60
  def project
61
- @project ||= api.get("projects/#{project_id}")
61
+ @project ||= project_assignment['project'].merge('client' => project_assignment['client'])
62
62
  end
63
63
 
64
64
  def task
65
- project_task_assignments
66
- .map { |assignment| assignment['task'] }
67
- .find { |task| task['id'].to_s == task_id }
65
+ @task ||= project_assignment['task_assignments'].map { |ta| ta['task'] }.find do |task|
66
+ task['id'].to_s == task_id
67
+ end
68
68
  end
69
69
 
70
- def project_task_assignments
71
- @project_task_assignments ||= begin
72
- api.get_paged("projects/#{project_id}/task_assignments", is_active: true)
73
- rescue Abt::HttpError::HttpError # rubocop:disable Layout/RescueEnsureAlignment
74
- []
70
+ def project_assignment
71
+ @project_assignment ||= begin
72
+ project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
75
73
  end
76
74
  end
75
+
76
+ def project_assignments
77
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
78
+ end
77
79
  end
78
80
  end
79
81
  end
@@ -28,6 +28,8 @@ module Abt
28
28
  private
29
29
 
30
30
  def find_search_result
31
+ cli.warn 'Select a project'
32
+
31
33
  loop do
32
34
  matches = matches_for_string cli.prompt('Enter search')
33
35
  if matches.empty?
@@ -37,14 +39,14 @@ module Abt
37
39
 
38
40
  cli.warn 'Showing the 10 first matches' if matches.size > 10
39
41
  choice = cli.prompt_choice 'Select a project', matches[0...10], true
40
- break choice unless choice.nil?
42
+ break choice['project'] unless choice.nil?
41
43
  end
42
44
  end
43
45
 
44
46
  def matches_for_string(string)
45
47
  search_string = sanitize_string(string)
46
48
 
47
- projects.select do |project|
49
+ searchable_projects.select do |project|
48
50
  sanitize_string(project['name']).include?(search_string)
49
51
  end
50
52
  end
@@ -53,12 +55,27 @@ module Abt
53
55
  string.downcase.gsub(/[^\w]/, '')
54
56
  end
55
57
 
58
+ def searchable_projects
59
+ @searchable_projects ||= projects.map do |project|
60
+ {
61
+ 'name' => "#{project['client']['name']} > #{project['name']}",
62
+ 'project' => project
63
+ }
64
+ end
65
+ end
66
+
56
67
  def projects
57
68
  @projects ||= begin
58
69
  cli.warn 'Fetching projects...'
59
- api.get_paged('projects')
70
+ project_assignments.map do |project_assignment|
71
+ project_assignment['project'].merge('client' => project_assignment['client'])
72
+ end
60
73
  end
61
74
  end
75
+
76
+ def project_assignments
77
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
78
+ end
62
79
  end
63
80
  end
64
81
  end
@@ -28,20 +28,22 @@ module Abt
28
28
  private
29
29
 
30
30
  def project
31
- @project ||= api.get("projects/#{project_id}")
31
+ project_assignment['project']
32
32
  end
33
33
 
34
34
  def tasks
35
- project_task_assignments.map { |assignment| assignment['task'] }
35
+ @tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
36
36
  end
37
37
 
38
- def project_task_assignments
39
- @project_task_assignments ||= begin
40
- api.get_paged("projects/#{project_id}/task_assignments", is_active: true)
41
- rescue Abt::HttpError::HttpError # rubocop:disable Layout/RescueEnsureAlignment
42
- []
38
+ def project_assignment
39
+ @project_assignment ||= begin
40
+ project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
43
41
  end
44
42
  end
43
+
44
+ def project_assignments
45
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
46
+ end
45
47
  end
46
48
  end
47
49
  end
@@ -24,9 +24,15 @@ module Abt
24
24
  def projects
25
25
  @projects ||= begin
26
26
  cli.warn 'Fetching projects...'
27
- api.get_paged('projects', is_active: true)
27
+ project_assignments.map do |project_assignment|
28
+ project_assignment['project'].merge('client' => project_assignment['client'])
29
+ end
28
30
  end
29
31
  end
32
+
33
+ def project_assignments
34
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
35
+ end
30
36
  end
31
37
  end
32
38
  end
@@ -10,21 +10,16 @@ module Abt
10
10
  end
11
11
 
12
12
  def self.description
13
- 'Start tracker for current or specified task. Add a relevant provider to link the time entry: E.g. `abt start harvest asana`' # rubocop:disable Layout/LineLength
13
+ 'As track, but also lets the user override the current task and triggers `start` commands for other providers ' # rubocop:disable Layout/LineLength
14
14
  end
15
15
 
16
16
  def call
17
- abort 'No current/provided task' if task_id.nil?
17
+ start_output = call_start
18
+ puts start_output
18
19
 
19
- maybe_override_current_task
20
-
21
- print_task(project, task)
22
-
23
- cli.abort('No task selected') if task_id.nil?
20
+ use_arg_str(arg_str_from_start_output(start_output))
24
21
 
25
- create_time_entry
26
-
27
- cli.warn 'Tracker successfully started'
22
+ maybe_override_current_task
28
23
  rescue Abt::HttpError::HttpError => e
29
24
  cli.warn e
30
25
  cli.abort 'Unable to start tracker'
@@ -32,47 +27,29 @@ module Abt
32
27
 
33
28
  private
34
29
 
35
- def maybe_override_current_task
36
- return if arg_str.nil?
37
- return if same_args_as_config?
38
- return unless config.local_available?
39
-
40
- should_override = cli.prompt_boolean 'Set selected task as current?'
41
- Current.new(arg_str: arg_str, cli: cli).call if should_override
30
+ def arg_str_from_start_output(output)
31
+ output = output.split(' # ').first
32
+ output.split(':')[1]
42
33
  end
43
34
 
44
- def create_time_entry
45
- body = Oj.dump({
46
- project_id: project_id,
47
- task_id: task_id,
48
- user_id: config.user_id,
49
- spent_date: Date.today.iso8601
50
- }.merge(external_link_data), mode: :json)
51
- api.post('time_entries', body)
52
- end
53
-
54
- def project
55
- @project ||= api.get("projects/#{project_id}")
56
- end
35
+ def call_start
36
+ output = StringIO.new
37
+ Abt::Cli.new(argv: ['track', *cli.args], output: output).perform
57
38
 
58
- def task
59
- @task ||= api.get("tasks/#{task_id}")
39
+ output_str = output.string.strip
40
+ cli.abort 'No task provided' if output_str.empty?
41
+ output_str
60
42
  end
61
43
 
62
- def external_link_data
63
- @external_link_data ||= begin
64
- arg_strs = cli.args.join(' ')
65
- lines = `#{$PROGRAM_NAME} harvest-time-entry-data #{arg_strs}`.split("\n")
66
-
67
- return {} if lines.empty?
68
-
69
- # TODO: Make user choose which reference to use by printing the urls
70
- if lines.length > 1
71
- cli.abort('Multiple providers had harvest reference data, only one is supported at a time') # rubocop:disable Layout/LineLength
72
- end
44
+ def maybe_override_current_task
45
+ return if arg_str.nil?
46
+ return if same_args_as_config?
47
+ return unless config.local_available?
48
+ return unless cli.prompt_boolean 'Set selected task as current?'
73
49
 
74
- Oj.load(lines.first)
75
- end
50
+ output = StringIO.new
51
+ Abt::Cli.new(argv: ['current', "harvest:#{project_id}/#{task_id}"],
52
+ output: output).perform
76
53
  end
77
54
  end
78
55
  end
@@ -44,7 +44,7 @@ module Abt
44
44
  api.get_paged(
45
45
  'time_entries',
46
46
  is_running: true,
47
- user_id: Harvest.user_id
47
+ user_id: config.user_id
48
48
  ).first
49
49
  rescue Abt::HttpError::HttpError => e # rubocop:disable Layout/RescueEnsureAlignment
50
50
  cli.warn e
@@ -14,23 +14,30 @@ module Abt
14
14
  end
15
15
 
16
16
  def call
17
- project_task_assignments.each do |a|
18
- project = a['project']
19
- task = a['task']
20
-
17
+ tasks.each do |task|
21
18
  print_task(project, task)
22
19
  end
23
20
  end
24
21
 
25
22
  private
26
23
 
27
- def project_task_assignments
28
- @project_task_assignments ||= begin
29
- api.get_paged("projects/#{project_id}/task_assignments", is_active: true)
30
- rescue Abt::HttpError::HttpError # rubocop:disable Layout/RescueEnsureAlignment
31
- []
24
+ def project
25
+ project_assignment['project']
26
+ end
27
+
28
+ def tasks
29
+ @tasks ||= project_assignment['task_assignments'].map { |ta| ta['task'] }
30
+ end
31
+
32
+ def project_assignment
33
+ @project_assignment ||= begin
34
+ project_assignments.find { |pa| pa['project']['id'].to_s == project_id }
32
35
  end
33
36
  end
37
+
38
+ def project_assignments
39
+ @project_assignments ||= api.get_paged('users/me/project_assignments')
40
+ end
34
41
  end
35
42
  end
36
43
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abt
4
+ module Providers
5
+ module Harvest
6
+ module Commands
7
+ class Track < BaseCommand
8
+ def self.command
9
+ 'track harvest[:<project-id>/<task-id>]'
10
+ end
11
+
12
+ def self.description
13
+ 'Start tracker for current or specified task. Add a relevant provider to link the time entry: E.g. `abt start harvest asana`' # rubocop:disable Layout/LineLength
14
+ end
15
+
16
+ def call
17
+ abort 'No current/provided task' if task_id.nil?
18
+ cli.abort('No task selected') if task_id.nil?
19
+
20
+ print_task(created_time_entry['project'], created_time_entry['task'])
21
+
22
+ cli.warn 'Tracker successfully started'
23
+ rescue Abt::HttpError::HttpError => e
24
+ cli.abort 'Invalid task'
25
+ end
26
+
27
+ private
28
+
29
+ def created_time_entry
30
+ @created_time_entry ||= create_time_entry
31
+ end
32
+
33
+ def create_time_entry
34
+ body = {
35
+ project_id: project_id,
36
+ task_id: task_id,
37
+ user_id: config.user_id,
38
+ spent_date: Date.today.iso8601
39
+ }
40
+
41
+ if external_link_data
42
+ body.merge! external_link_data
43
+ else
44
+ cli.warn 'No external link provided'
45
+ body[:notes] ||= cli.prompt('Fill in comment (optional)')
46
+ end
47
+
48
+ api.post('time_entries', Oj.dump(body, mode: :json))
49
+ end
50
+
51
+ def external_link_data
52
+ @external_link_data ||= begin
53
+ arg_strs = cli.args.join(' ')
54
+ lines = `#{$PROGRAM_NAME} harvest-time-entry-data #{arg_strs}`.split("\n")
55
+
56
+ return if lines.empty?
57
+
58
+ # TODO: Make user choose which reference to use by printing the urls
59
+ if lines.length > 1
60
+ cli.abort('Multiple providers had harvest reference data, only one is supported at a time') # rubocop:disable Layout/LineLength
61
+ end
62
+
63
+ Oj.load(lines.first)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -24,13 +24,15 @@ module Abt
24
24
  end
25
25
 
26
26
  def project_id=(value)
27
+ value = value.to_s unless value.nil?
27
28
  return if project_id == value
28
29
 
29
30
  clear_local
30
- git['projectId'] = value unless value.nil?
31
+ git['projectId'] = value
31
32
  end
32
33
 
33
34
  def task_id=(value)
35
+ value = value.to_s unless value.nil?
34
36
  git['taskId'] = value
35
37
  end
36
38
 
@@ -72,18 +74,17 @@ module Abt
72
74
  def user_id
73
75
  return git.global['userId'] unless git.global['userId'].nil?
74
76
 
75
- git.global['userId'] = cli.prompt([
76
- 'Please provide your harvest User ID.',
77
- 'To find it open "My profile" inside the harvest web UI.',
78
- 'The ID is the number part of the URL for that page.',
79
- '',
80
- 'Enter user id'
81
- ].join("\n"))
77
+ git.global['userId'] = api.get('users/me')['id'].to_s
82
78
  end
83
79
 
84
80
  private
85
81
 
86
82
  attr_reader :git
83
+
84
+ def api
85
+ @api ||=
86
+ Abt::Providers::Harvest::Api.new(access_token: access_token, account_id: account_id)
87
+ end
87
88
  end
88
89
  end
89
90
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Abt
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.9'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abt-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesper Sørensen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-17 00:00:00.000000000 Z
11
+ date: 2021-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-inflector
@@ -113,6 +113,7 @@ files:
113
113
  - "./lib/abt/providers/harvest/commands/start.rb"
114
114
  - "./lib/abt/providers/harvest/commands/stop.rb"
115
115
  - "./lib/abt/providers/harvest/commands/tasks.rb"
116
+ - "./lib/abt/providers/harvest/commands/track.rb"
116
117
  - "./lib/abt/providers/harvest/configuration.rb"
117
118
  - "./lib/abt/version.rb"
118
119
  - bin/abt