superthread 0.7.2
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 +7 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE +21 -0
- data/README.md +492 -0
- data/exe/suth +19 -0
- data/lib/superthread/cli/accounts.rb +240 -0
- data/lib/superthread/cli/activity.rb +210 -0
- data/lib/superthread/cli/base.rb +355 -0
- data/lib/superthread/cli/boards.rb +131 -0
- data/lib/superthread/cli/cards.rb +530 -0
- data/lib/superthread/cli/checklists.rb +223 -0
- data/lib/superthread/cli/comments.rb +86 -0
- data/lib/superthread/cli/completion.rb +306 -0
- data/lib/superthread/cli/concerns/board_resolvable.rb +70 -0
- data/lib/superthread/cli/concerns/confirmable.rb +55 -0
- data/lib/superthread/cli/concerns/date_parsable.rb +196 -0
- data/lib/superthread/cli/concerns/list_resolvable.rb +53 -0
- data/lib/superthread/cli/concerns/space_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/sprint_resolvable.rb +55 -0
- data/lib/superthread/cli/concerns/tag_resolvable.rb +49 -0
- data/lib/superthread/cli/concerns/user_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/workspace_resolvable.rb +83 -0
- data/lib/superthread/cli/config.rb +129 -0
- data/lib/superthread/cli/formatter.rb +388 -0
- data/lib/superthread/cli/lists.rb +85 -0
- data/lib/superthread/cli/main.rb +121 -0
- data/lib/superthread/cli/members.rb +19 -0
- data/lib/superthread/cli/notes.rb +64 -0
- data/lib/superthread/cli/pages.rb +128 -0
- data/lib/superthread/cli/projects.rb +124 -0
- data/lib/superthread/cli/replies.rb +94 -0
- data/lib/superthread/cli/search.rb +34 -0
- data/lib/superthread/cli/setup.rb +253 -0
- data/lib/superthread/cli/spaces.rb +141 -0
- data/lib/superthread/cli/sprints.rb +32 -0
- data/lib/superthread/cli/tags.rb +86 -0
- data/lib/superthread/cli/ui/gum_prompt.rb +58 -0
- data/lib/superthread/cli/ui/plain_prompt.rb +73 -0
- data/lib/superthread/cli/ui.rb +263 -0
- data/lib/superthread/cli/workspaces.rb +105 -0
- data/lib/superthread/cli.rb +12 -0
- data/lib/superthread/client.rb +207 -0
- data/lib/superthread/configuration.rb +354 -0
- data/lib/superthread/connection.rb +57 -0
- data/lib/superthread/error.rb +164 -0
- data/lib/superthread/mention_formatter.rb +96 -0
- data/lib/superthread/model.rb +178 -0
- data/lib/superthread/models/board.rb +59 -0
- data/lib/superthread/models/card.rb +321 -0
- data/lib/superthread/models/checklist.rb +91 -0
- data/lib/superthread/models/checklist_item.rb +69 -0
- data/lib/superthread/models/comment.rb +71 -0
- data/lib/superthread/models/concerns/archivable.rb +32 -0
- data/lib/superthread/models/concerns/presentable.rb +113 -0
- data/lib/superthread/models/concerns/timestampable.rb +91 -0
- data/lib/superthread/models/list.rb +67 -0
- data/lib/superthread/models/member.rb +40 -0
- data/lib/superthread/models/note.rb +56 -0
- data/lib/superthread/models/page.rb +70 -0
- data/lib/superthread/models/project.rb +83 -0
- data/lib/superthread/models/space.rb +71 -0
- data/lib/superthread/models/sprint.rb +53 -0
- data/lib/superthread/models/tag.rb +52 -0
- data/lib/superthread/models/team.rb +68 -0
- data/lib/superthread/models/user.rb +76 -0
- data/lib/superthread/models.rb +12 -0
- data/lib/superthread/object.rb +285 -0
- data/lib/superthread/objects/collection.rb +179 -0
- data/lib/superthread/resources/base.rb +204 -0
- data/lib/superthread/resources/boards.rb +150 -0
- data/lib/superthread/resources/cards.rb +363 -0
- data/lib/superthread/resources/comments.rb +163 -0
- data/lib/superthread/resources/notes.rb +61 -0
- data/lib/superthread/resources/pages.rb +110 -0
- data/lib/superthread/resources/projects.rb +117 -0
- data/lib/superthread/resources/search.rb +46 -0
- data/lib/superthread/resources/spaces.rb +104 -0
- data/lib/superthread/resources/sprints.rb +37 -0
- data/lib/superthread/resources/tags.rb +52 -0
- data/lib/superthread/resources/users.rb +29 -0
- data/lib/superthread/version.rb +6 -0
- data/lib/superthread/version_checker.rb +174 -0
- data/lib/superthread.rb +30 -0
- metadata +259 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for managing Superthread accounts.
|
|
6
|
+
#
|
|
7
|
+
# Accounts store API credentials and workspace associations. Multiple
|
|
8
|
+
# accounts can be configured and switched between using these commands.
|
|
9
|
+
class Accounts < Base
|
|
10
|
+
desc "list", "List all configured accounts"
|
|
11
|
+
# Lists all configured accounts with their workspace associations.
|
|
12
|
+
#
|
|
13
|
+
# @return [void]
|
|
14
|
+
def list
|
|
15
|
+
accounts = app_config.accounts
|
|
16
|
+
|
|
17
|
+
if accounts.empty?
|
|
18
|
+
Ui.warning "No accounts configured"
|
|
19
|
+
Ui.muted "Run 'suth setup' to add an account"
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
current = app_config.current_account
|
|
24
|
+
|
|
25
|
+
Ui.section "Accounts"
|
|
26
|
+
accounts.each do |name, _data|
|
|
27
|
+
state = app_config.account_state(name.to_s)
|
|
28
|
+
workspace_name = state&.dig(:workspace_name) || state&.dig(:workspace_id) || "(no workspace)"
|
|
29
|
+
marker = (name.to_s == current) ? "*" : " "
|
|
30
|
+
puts " #{marker} #{name.to_s.ljust(15)} #{workspace_name}"
|
|
31
|
+
end
|
|
32
|
+
puts ""
|
|
33
|
+
Ui.muted "Use 'suth account use <name>' to switch accounts"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "show [NAME]", "Show account details (defaults to current account)"
|
|
37
|
+
# Displays details for a specific account or the current account.
|
|
38
|
+
#
|
|
39
|
+
# @param name [String, nil] account name to show, or nil for current account
|
|
40
|
+
# @return [void]
|
|
41
|
+
def show(name = nil)
|
|
42
|
+
name ||= app_config.current_account
|
|
43
|
+
|
|
44
|
+
unless name
|
|
45
|
+
Ui.warning "No account selected"
|
|
46
|
+
Ui.muted "Run 'suth setup' to configure an account"
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless app_config.accounts.key?(name.to_sym)
|
|
51
|
+
Ui.error "Account '#{name}' not found"
|
|
52
|
+
Ui.muted "Available accounts: #{app_config.accounts.keys.join(", ")}"
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
account_config = app_config.accounts[name.to_sym]
|
|
57
|
+
state = app_config.account_state(name)
|
|
58
|
+
is_current = name == app_config.current_account
|
|
59
|
+
|
|
60
|
+
label = is_current ? "Account: #{name} (current)" : "Account: #{name}"
|
|
61
|
+
Ui.section label
|
|
62
|
+
puts " API Key: #{mask_api_key(account_config&.dig(:api_key))}"
|
|
63
|
+
puts " Workspace ID: #{state&.dig(:workspace_id) || "(not set)"}"
|
|
64
|
+
puts " Workspace Name: #{state&.dig(:workspace_name) || "(not set)"}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "use NAME", "Switch to a different account"
|
|
68
|
+
# Switches the active account to the specified account.
|
|
69
|
+
#
|
|
70
|
+
# @param name [String] account name to switch to
|
|
71
|
+
# @return [void]
|
|
72
|
+
def use(name)
|
|
73
|
+
unless app_config.accounts.key?(name.to_sym)
|
|
74
|
+
Ui.error "Account '#{name}' not found"
|
|
75
|
+
Ui.muted "Available accounts: #{app_config.accounts.keys.join(", ")}"
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
app_config.current_account = name
|
|
80
|
+
state = app_config.account_state(name)
|
|
81
|
+
workspace_name = state&.dig(:workspace_name) || name
|
|
82
|
+
|
|
83
|
+
output_success "Switched to account '#{name}' (#{workspace_name})"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc "add NAME", "Add a new account"
|
|
87
|
+
method_option :with_token, type: :boolean,
|
|
88
|
+
desc: "Read API key from standard input (for scripts and agents)"
|
|
89
|
+
method_option :workspace_name, type: :string,
|
|
90
|
+
desc: "Select workspace by name (non-interactive)"
|
|
91
|
+
# Adds a new account with API key and workspace selection.
|
|
92
|
+
#
|
|
93
|
+
# By default, prompts interactively for the API key. Use --with-token
|
|
94
|
+
# to read the key from standard input instead (like gh auth login --with-token).
|
|
95
|
+
#
|
|
96
|
+
# @param name [String] unique name for the new account
|
|
97
|
+
# @return [void]
|
|
98
|
+
def add(name)
|
|
99
|
+
if app_config.accounts.key?(name.to_sym)
|
|
100
|
+
Ui.error "Account '#{name}' already exists"
|
|
101
|
+
Ui.muted "Use 'suth account remove #{name}' first to replace it"
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
api_key = read_api_key(name)
|
|
106
|
+
return unless api_key
|
|
107
|
+
|
|
108
|
+
# Validate and fetch workspaces
|
|
109
|
+
begin
|
|
110
|
+
Ui.spin("Validating API key") do
|
|
111
|
+
temp_client = Superthread::Client.new(api_key: api_key)
|
|
112
|
+
@user = temp_client.users.me
|
|
113
|
+
end
|
|
114
|
+
rescue Superthread::AuthenticationError
|
|
115
|
+
Ui.error "Invalid API key"
|
|
116
|
+
return
|
|
117
|
+
rescue Superthread::ApiError => e
|
|
118
|
+
Ui.error "API error: #{e.message}"
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
teams = @user.teams || []
|
|
123
|
+
if teams.empty?
|
|
124
|
+
Ui.error "No workspaces found for this API key"
|
|
125
|
+
return
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
workspace = select_workspace(teams)
|
|
129
|
+
return unless workspace
|
|
130
|
+
|
|
131
|
+
# Save account
|
|
132
|
+
app_config.add_account(name, api_key: api_key)
|
|
133
|
+
app_config.save_account_state(name,
|
|
134
|
+
workspace_id: workspace.id,
|
|
135
|
+
workspace_name: workspace.team_name)
|
|
136
|
+
app_config.set_current_account(name)
|
|
137
|
+
|
|
138
|
+
output_success "Account '#{name}' configured (#{workspace.team_name || workspace.id})"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
desc "remove NAME", "Remove an account"
|
|
142
|
+
# Removes a configured account after confirmation.
|
|
143
|
+
#
|
|
144
|
+
# @param name [String] account name to remove
|
|
145
|
+
# @return [void]
|
|
146
|
+
def remove(name)
|
|
147
|
+
unless app_config.accounts.key?(name.to_sym)
|
|
148
|
+
Ui.error "Account '#{name}' not found"
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
confirming("Remove account '#{name}'?") do
|
|
153
|
+
app_config.remove_account(name)
|
|
154
|
+
output_success "Account '#{name}' removed"
|
|
155
|
+
|
|
156
|
+
if app_config.accounts.empty?
|
|
157
|
+
Ui.muted "No accounts remaining. Run 'suth setup' to add one."
|
|
158
|
+
elsif app_config.current_account.nil?
|
|
159
|
+
first_account = app_config.accounts.keys.first
|
|
160
|
+
Ui.muted "Tip: Run 'suth account use #{first_account}' to select an account"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# Reads the API key from stdin (--with-token) or interactive prompt.
|
|
168
|
+
#
|
|
169
|
+
# @param account_name [String] the account name (used in interactive prompt)
|
|
170
|
+
# @return [String, nil] the API key, or nil if empty/missing
|
|
171
|
+
def read_api_key(account_name)
|
|
172
|
+
api_key = if options[:with_token]
|
|
173
|
+
$stdin.gets&.chomp
|
|
174
|
+
else
|
|
175
|
+
Ui.password("API key for '#{account_name}'")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if api_key.nil? || api_key.empty?
|
|
179
|
+
if options[:with_token]
|
|
180
|
+
Ui.error "No API key provided on standard input"
|
|
181
|
+
Ui.muted "Usage: echo $SUPERTHREAD_API_KEY | suth accounts add NAME --with-token"
|
|
182
|
+
else
|
|
183
|
+
Ui.error "API key is required"
|
|
184
|
+
end
|
|
185
|
+
return nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
api_key
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Selects a workspace from the list of teams.
|
|
192
|
+
#
|
|
193
|
+
# Uses --workspace-name if provided, auto-selects if only one,
|
|
194
|
+
# or prompts interactively.
|
|
195
|
+
#
|
|
196
|
+
# @param teams [Array] available workspaces
|
|
197
|
+
# @return [Object, nil] the selected workspace, or nil on error
|
|
198
|
+
def select_workspace(teams)
|
|
199
|
+
if teams.length == 1
|
|
200
|
+
return teams.first
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if options[:workspace_name]
|
|
204
|
+
match = teams.find { |t| t.team_name&.downcase == options[:workspace_name].downcase }
|
|
205
|
+
unless match
|
|
206
|
+
names = teams.map { |t| t.team_name || t.id }.join(", ")
|
|
207
|
+
Ui.error "Workspace '#{options[:workspace_name]}' not found"
|
|
208
|
+
Ui.muted "Available: #{names}"
|
|
209
|
+
return nil
|
|
210
|
+
end
|
|
211
|
+
return match
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if options[:with_token]
|
|
215
|
+
Ui.muted "Multiple workspaces found, using '#{teams.first.team_name || teams.first.id}'"
|
|
216
|
+
Ui.muted "Use --workspace-name to select a different workspace"
|
|
217
|
+
return teams.first
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
Ui.muted "Found #{teams.length} workspaces:"
|
|
221
|
+
teams.each_with_index do |team, i|
|
|
222
|
+
puts " #{i + 1}. #{team.team_name || team.id}"
|
|
223
|
+
end
|
|
224
|
+
choice = Ui.input("Select workspace (1-#{teams.length})")&.to_i || 1
|
|
225
|
+
teams[choice - 1] || teams.first
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Masks an API key for display, showing only the first 4 and last 4 characters.
|
|
229
|
+
#
|
|
230
|
+
# @param key [String, nil] the API key to mask
|
|
231
|
+
# @return [String] the masked key or "(not set)" if nil
|
|
232
|
+
def mask_api_key(key)
|
|
233
|
+
return "(not set)" unless key
|
|
234
|
+
return "***" if key.length < 10
|
|
235
|
+
|
|
236
|
+
"#{key[0..3]}...#{key[-4..]}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Cli
|
|
5
|
+
# CLI commands for viewing recent activity across the workspace.
|
|
6
|
+
class Activity < Base
|
|
7
|
+
include Concerns::DateParsable
|
|
8
|
+
|
|
9
|
+
desc "show", "Show recent activity across workspace"
|
|
10
|
+
option :since, type: :string, default: "today", desc: "Filter by date (e.g., 'friday', '3 days ago')"
|
|
11
|
+
option :user, type: :string, desc: "Filter by user (ID, name, or 'me')"
|
|
12
|
+
option :board, type: :string, aliases: "-b", desc: "Filter by board (ID or name)"
|
|
13
|
+
option :space, type: :string, aliases: "-s", desc: "Space (ID or name) - helps resolve board by name"
|
|
14
|
+
# Display recent activity across the workspace.
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
def show
|
|
18
|
+
handle_error do
|
|
19
|
+
since_ts = parse_date(options[:since])
|
|
20
|
+
activity = fetch_activity(since_ts)
|
|
21
|
+
|
|
22
|
+
if json_output?
|
|
23
|
+
output_activity_json(activity)
|
|
24
|
+
else
|
|
25
|
+
output_activity_summary(activity, options[:since])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
default_task :show
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Fetch all activity data from the workspace.
|
|
34
|
+
# Returns categorized cards: created, updated, completed.
|
|
35
|
+
#
|
|
36
|
+
# @param since_ts [Integer] Unix timestamp for filtering
|
|
37
|
+
# @return [Hash] Activity data grouped by category
|
|
38
|
+
def fetch_activity(since_ts)
|
|
39
|
+
# If user filter specified, use assigned endpoint
|
|
40
|
+
# Otherwise, we need to get cards differently
|
|
41
|
+
cards = if options[:user]
|
|
42
|
+
user_id = resolve_user(options[:user])
|
|
43
|
+
opts = {user_id: user_id}
|
|
44
|
+
opts[:board_id] = board_id if options[:board]
|
|
45
|
+
client.cards.assigned(workspace_id, **opts).to_a
|
|
46
|
+
elsif options[:board]
|
|
47
|
+
client.cards.list(workspace_id, board_id: board_id).to_a
|
|
48
|
+
else
|
|
49
|
+
# Fetch cards from all boards in the workspace
|
|
50
|
+
fetch_all_cards
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
categorize_activity(cards, since_ts)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Fetch cards from all boards in the workspace.
|
|
57
|
+
# This is slower but necessary when no board filter is provided.
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Card>] All cards
|
|
60
|
+
def fetch_all_cards
|
|
61
|
+
spaces = client.spaces.list(workspace_id)
|
|
62
|
+
all_cards = []
|
|
63
|
+
|
|
64
|
+
spaces.each do |space|
|
|
65
|
+
boards = client.boards.list(workspace_id, space_id: space.id)
|
|
66
|
+
boards.each do |board|
|
|
67
|
+
cards = client.cards.list(workspace_id, board_id: board.id)
|
|
68
|
+
all_cards.concat(cards.to_a)
|
|
69
|
+
rescue Superthread::ApiError
|
|
70
|
+
# Skip boards we can't access
|
|
71
|
+
end
|
|
72
|
+
rescue Superthread::ApiError
|
|
73
|
+
# Skip spaces we can't access
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
all_cards
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Categorize cards into activity buckets based on timestamps.
|
|
80
|
+
#
|
|
81
|
+
# @param cards [Array<Card>] the cards to sort into created, updated, and completed buckets
|
|
82
|
+
# @param since_ts [Integer] Filter timestamp
|
|
83
|
+
# @return [Hash] Categorized activity
|
|
84
|
+
def categorize_activity(cards, since_ts)
|
|
85
|
+
activity = {
|
|
86
|
+
created: [],
|
|
87
|
+
updated: [],
|
|
88
|
+
completed: []
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cards.each do |card|
|
|
92
|
+
created_ts = normalize_timestamp(card.time_created)
|
|
93
|
+
updated_ts = normalize_timestamp(card.time_updated)
|
|
94
|
+
completed_ts = normalize_timestamp(card.completed_date)
|
|
95
|
+
|
|
96
|
+
# Check for completion (takes precedence)
|
|
97
|
+
if completed_ts && completed_ts >= since_ts
|
|
98
|
+
activity[:completed] << card
|
|
99
|
+
# Check for creation
|
|
100
|
+
elsif created_ts && created_ts >= since_ts
|
|
101
|
+
activity[:created] << card
|
|
102
|
+
# Check for update (but not if just created)
|
|
103
|
+
elsif updated_ts && updated_ts >= since_ts && updated_ts != created_ts
|
|
104
|
+
activity[:updated] << card
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Sort each category by most recent first
|
|
109
|
+
activity[:created].sort_by! { |c| -(c.time_created || 0) }
|
|
110
|
+
activity[:updated].sort_by! { |c| -(c.time_updated || 0) }
|
|
111
|
+
activity[:completed].sort_by! { |c| -(c.completed_date || 0) }
|
|
112
|
+
|
|
113
|
+
activity
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Normalize timestamp to seconds (API sometimes returns milliseconds).
|
|
117
|
+
#
|
|
118
|
+
# @param ts [Integer, nil] Timestamp
|
|
119
|
+
# @return [Integer, nil] Normalized timestamp in seconds
|
|
120
|
+
def normalize_timestamp(ts)
|
|
121
|
+
Formatter.normalize_timestamp(ts)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Output activity as JSON.
|
|
125
|
+
#
|
|
126
|
+
# @param activity [Hash] the categorized activity data with :created, :updated, :completed keys
|
|
127
|
+
# @return [void]
|
|
128
|
+
def output_activity_json(activity)
|
|
129
|
+
json_data = {
|
|
130
|
+
created: activity[:created].map { |c| card_to_hash(c) },
|
|
131
|
+
updated: activity[:updated].map { |c| card_to_hash(c) },
|
|
132
|
+
completed: activity[:completed].map { |c| card_to_hash(c) },
|
|
133
|
+
summary: {
|
|
134
|
+
total_created: activity[:created].size,
|
|
135
|
+
total_updated: activity[:updated].size,
|
|
136
|
+
total_completed: activity[:completed].size
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
puts Formatter.json(json_data)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Convert card to hash for JSON output.
|
|
143
|
+
#
|
|
144
|
+
# @param card [Card] the card to convert
|
|
145
|
+
# @return [Hash] the card data as a hash
|
|
146
|
+
def card_to_hash(card)
|
|
147
|
+
{
|
|
148
|
+
id: card.id,
|
|
149
|
+
title: card.title,
|
|
150
|
+
status: card.status,
|
|
151
|
+
priority: card.priority,
|
|
152
|
+
list_title: card.list_title,
|
|
153
|
+
board_title: card.board_title,
|
|
154
|
+
time_created: card.time_created,
|
|
155
|
+
time_updated: card.time_updated,
|
|
156
|
+
completed_date: card.completed_date
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Output activity as a formatted summary.
|
|
161
|
+
#
|
|
162
|
+
# @param activity [Hash] the categorized activity data
|
|
163
|
+
# @param since_label [String] the label describing the time period
|
|
164
|
+
# @return [void]
|
|
165
|
+
def output_activity_summary(activity, since_label)
|
|
166
|
+
total = activity[:created].size + activity[:updated].size + activity[:completed].size
|
|
167
|
+
|
|
168
|
+
if total == 0
|
|
169
|
+
say "No activity since #{since_label}", :yellow
|
|
170
|
+
return
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
Ui.section "Activity since #{since_label}"
|
|
174
|
+
puts ""
|
|
175
|
+
|
|
176
|
+
if activity[:completed].any?
|
|
177
|
+
output_activity_section("Completed", activity[:completed], :green)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if activity[:created].any?
|
|
181
|
+
output_activity_section("Created", activity[:created], :cyan)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if activity[:updated].any?
|
|
185
|
+
output_activity_section("Updated", activity[:updated], :yellow)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
puts ""
|
|
189
|
+
say "Total: #{total} card(s) (#{activity[:completed].size} completed, " \
|
|
190
|
+
"#{activity[:created].size} created, #{activity[:updated].size} updated)", :white
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Output a section of activity.
|
|
194
|
+
#
|
|
195
|
+
# @param label [String] the section header label
|
|
196
|
+
# @param cards [Array<Card>] the cards to display in this section
|
|
197
|
+
# @param color [Symbol] the color for the section header
|
|
198
|
+
# @return [void]
|
|
199
|
+
def output_activity_section(label, cards, color)
|
|
200
|
+
say "#{label} (#{cards.size}):", color
|
|
201
|
+
cards.each do |card|
|
|
202
|
+
board_info = card.board_title ? " [#{card.board_title}]" : ""
|
|
203
|
+
list_info = card.list_title ? " -> #{card.list_title}" : ""
|
|
204
|
+
say " #{card.id} #{card.title}#{board_info}#{list_info}"
|
|
205
|
+
end
|
|
206
|
+
puts ""
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|