trello_tool 0.2.0 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +37 -2
- data/lib/trello_tool/configuration.rb +3 -2
- data/lib/trello_tool/health.rb +5 -1
- data/lib/trello_tool/trello_client.rb +52 -0
- data/lib/trello_tool/util.rb +7 -0
- data/lib/trello_tool/version.rb +1 -1
- data/lib/trello_tool_thor.rb +242 -2
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ed0b9ce508c845c11e20e9832e4b0bdce3540b50eb726eb39f2e2b0a92dd1da
|
|
4
|
+
data.tar.gz: 72155610d0070653878eb3f705ece349d23358e97b105fb5d2bf75d5090b5d58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6811ec433a67808fb9b7d457127a59ac39c9e6e2e96717b51df6ef7f6f4669760f19e82c50b57928f3e41a8ee0e6261734765ebfe40ca65d2747df1332ae8942
|
|
7
|
+
data.tar.gz: 8345785d3149a3322c1850cd1ea89d48a2cf38eaf4249714835c047271c4ccd799b16e601a24ac2231e84a01626740ac0ab37ac30629e2185ea1d7e0b70327b7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.4.0] - 2026-10-02
|
|
2
|
+
|
|
3
|
+
* added `search` and `cards` (find cards in a board; markdown or `--json`)
|
|
4
|
+
* added `comment`, `create`, `checklist`, `check` and `move` (change cards), and the
|
|
5
|
+
`default_list_name_for_new_cards` configuration key for `create`
|
|
6
|
+
* commands that fail now exit non-zero
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
* added `summarize_as_md_long`
|
|
2
11
|
|
|
3
12
|
## [0.2.0] - 2022-05-21
|
|
4
13
|
|
data/README.md
CHANGED
|
@@ -56,6 +56,7 @@ You can also configure some defaults
|
|
|
56
56
|
* `divider_template` = "[%s]"
|
|
57
57
|
* `too_many_doing` = 2
|
|
58
58
|
* `too_many_todo` = 10
|
|
59
|
+
* `default_list_name_for_new_cards` = (none) -- the list `create` puts a card in when no list is named, e.g. "Triage"
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
### Trello Authorisation
|
|
@@ -69,8 +70,42 @@ You can also read more at https://github.com/jeremytregunna/ruby-trello
|
|
|
69
70
|
|
|
70
71
|
## Usage
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
`bin/trello_tool help` lists every command and `bin/trello_tool help COMMAND` its options. Boards default to
|
|
74
|
+
`main_board_url`; cards are given by id or url; lists, labels and checklists by name.
|
|
75
|
+
|
|
76
|
+
The board:
|
|
77
|
+
|
|
78
|
+
* `bin/trello_tool health` -- checks whether the main board is "healthy"
|
|
79
|
+
* `bin/trello_tool lists (BOARD_URL)` -- the lists of a board
|
|
80
|
+
* `bin/trello_tool release (VERSION)`, `archive`, `archive_last (N)` -- rename "next version" on release, and
|
|
81
|
+
move old version lists to the archive board
|
|
82
|
+
|
|
83
|
+
Reading cards:
|
|
84
|
+
|
|
85
|
+
* `bin/trello_tool card CARD` -- one card as json (title, description, url, checklists, attachment urls)
|
|
86
|
+
* `bin/trello_tool search QUERY (BOARD_URL) (--json)` -- the unarchived cards matching a trello search, grouped by
|
|
87
|
+
list. The query is trello's own, so `label:bug`, `list:"TO DO"` and the like work, and the last word matches as
|
|
88
|
+
a prefix
|
|
89
|
+
* `bin/trello_tool cards (BOARD_URL) (--json)` -- every unarchived card in the board, grouped by list
|
|
90
|
+
* `bin/trello_tool summarize_as_md`, `summarize_as_md_long`, `summarize_as_urls (LIST_NAME (BOARD_URL))` -- the
|
|
91
|
+
cards of one list
|
|
92
|
+
|
|
93
|
+
Changing cards:
|
|
94
|
+
|
|
95
|
+
* `bin/trello_tool comment CARD "text"` or `--file notes.md` -- adds a comment
|
|
96
|
+
* `bin/trello_tool create (LIST_NAME (BOARD_URL)) --title "…" (--desc "…" | --desc-file card.md) (--label bug …)
|
|
97
|
+
(--top) (--force)` -- creates a card and prints it as json. Without LIST_NAME it goes in
|
|
98
|
+
`default_list_name_for_new_cards`. It first searches for an unarchived card with the same title and stops with
|
|
99
|
+
that card's url if there is one (`--force` creates another; trello's search can lag a few seconds behind a card
|
|
100
|
+
that was only just created)
|
|
101
|
+
* `bin/trello_tool checklist CARD NAME --items "first" "second" (--checked)` -- adds the items to the card's
|
|
102
|
+
checklist of that name, creating the checklist if need be and skipping items already there
|
|
103
|
+
* `bin/trello_tool check CARD "item text" (--checklist NAME) (--uncheck)` -- ticks (or unticks) the item with that
|
|
104
|
+
text, or the only item containing it
|
|
105
|
+
* `bin/trello_tool move CARD (LIST_NAME (BOARD_URL)) (--top | --bottom)` -- to the bottom (or top) of a list; without
|
|
106
|
+
LIST_NAME, to the top or bottom of the list the card is in
|
|
107
|
+
|
|
108
|
+
A command that can't do what it was asked says why and exits non-zero.
|
|
74
109
|
|
|
75
110
|
## Development
|
|
76
111
|
|
|
@@ -18,7 +18,8 @@ module TrelloTool
|
|
|
18
18
|
version_template: "v%s",
|
|
19
19
|
divider_template: "[%s]",
|
|
20
20
|
too_many_doing: 2,
|
|
21
|
-
too_many_todo: 10
|
|
21
|
+
too_many_todo: 10,
|
|
22
|
+
default_list_name_for_new_cards: nil
|
|
22
23
|
}.freeze
|
|
23
24
|
end
|
|
24
25
|
Configuration = Struct.new(*DefaultConfiguration::DEFAULTS.keys, keyword_init: true) do
|
|
@@ -33,7 +34,7 @@ module TrelloTool
|
|
|
33
34
|
# generates a file based on current settings
|
|
34
35
|
def generate
|
|
35
36
|
File.open(config_file, "w") do |f|
|
|
36
|
-
Psych.dump(to_h { |k, v| [k.to_s, v] }, f)
|
|
37
|
+
Psych.dump(to_h { |k, v| [k.to_s, v] }, f)
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
|
data/lib/trello_tool/health.rb
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
module TrelloTool
|
|
4
4
|
# Health analysis of a board
|
|
5
5
|
class Health
|
|
6
|
-
|
|
6
|
+
# @return [Trello::Board]
|
|
7
|
+
attr_reader :board
|
|
8
|
+
# @return [TrelloTool::Configuration]
|
|
9
|
+
attr_reader :configuration
|
|
10
|
+
attr_reader :symbols_and_colours, :unexpected, :expected_lists
|
|
7
11
|
|
|
8
12
|
def initialize(board, configuration)
|
|
9
13
|
@board = board
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require "trello"
|
|
4
5
|
require "trello_tool/util"
|
|
5
6
|
|
|
@@ -7,6 +8,7 @@ module TrelloTool
|
|
|
7
8
|
# Wrapped client for trello adapting it to things we need it to do
|
|
8
9
|
class TrelloClient < SimpleDelegator
|
|
9
10
|
include TrelloTool::Util
|
|
11
|
+
|
|
10
12
|
attr_reader :client, :configuration
|
|
11
13
|
|
|
12
14
|
# @param configuration[TrelloTool::Configuration]
|
|
@@ -54,6 +56,56 @@ module TrelloTool
|
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
|
|
59
|
+
# @param card_id [String]
|
|
60
|
+
# @return [Trello::Card]
|
|
61
|
+
def find_card(card_id)
|
|
62
|
+
client.find(:cards, card_id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Trello::Board]
|
|
66
|
+
def find_board(board_url)
|
|
67
|
+
client.find(:boards, extract_id_from_url(board_url))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# the card fields the listing commands need, as trello names them
|
|
71
|
+
CARD_FIELDS = "name,url,idList,labels,closed"
|
|
72
|
+
|
|
73
|
+
# Unarchived cards of a board matching a trello search (operators such as label: and list: work, and the
|
|
74
|
+
# last word matches as a prefix)
|
|
75
|
+
# @return [Array<Hash>] cards as trello returns them: "name", "url", "idList", "labels"
|
|
76
|
+
def search_cards(board, query, limit: 50)
|
|
77
|
+
response = client.get("/search", "query" => query, "idBoards" => board.id, "modelTypes" => "cards",
|
|
78
|
+
"cards_limit" => limit.to_s, "card_fields" => CARD_FIELDS, "partial" => "true")
|
|
79
|
+
JSON.parse(response.body).fetch("cards", []).reject { |card| card["closed"] }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Every unarchived card in an unarchived list of a board, in one request
|
|
83
|
+
# @return [Array<Hash>] as #search_cards
|
|
84
|
+
def open_cards(board)
|
|
85
|
+
JSON.parse(client.get("/boards/#{board.id}/cards/visible", "fields" => CARD_FIELDS).body)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param list [Trello::List]
|
|
89
|
+
# @return [Hash] the new card as trello returns it ("id", "url"...)
|
|
90
|
+
def create_card(list, title:, description: "", top: false, label_ids: [])
|
|
91
|
+
body = { name: title, desc: description, idList: list.id, pos: top ? "top" : "bottom" }
|
|
92
|
+
body[:idLabels] = label_ids.join(",") if label_ids.any?
|
|
93
|
+
JSON.parse(client.post("/cards", body).body)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param item [Hash] a checklist's check item ("id", "name", "state")
|
|
97
|
+
def set_check_item_state(card, item, complete:)
|
|
98
|
+
client.put("/cards/#{card.id}/checkItem/#{item['id']}", state: complete ? "complete" : "incomplete")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @param position [String] "top" or "bottom"
|
|
102
|
+
# @param list [Trello::List, nil] nil to reposition the card within its list
|
|
103
|
+
def move_card(card, position:, list: nil)
|
|
104
|
+
body = { pos: position }
|
|
105
|
+
body[:idList] = list.id if list
|
|
106
|
+
client.put("/cards/#{card.id}", body)
|
|
107
|
+
end
|
|
108
|
+
|
|
57
109
|
def next_version_list
|
|
58
110
|
@next_version_list ||= find_list_by_list_name(main_board, configuration.next_version_list_name)
|
|
59
111
|
end
|
data/lib/trello_tool/util.rb
CHANGED
|
@@ -14,6 +14,13 @@ module TrelloTool
|
|
|
14
14
|
match_data[1]
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# @param card_id_or_url [String] a card id or a https://trello.com/c/ID/description url
|
|
18
|
+
# @return [String] the card id
|
|
19
|
+
def extract_card_id(card_id_or_url)
|
|
20
|
+
match_data = %r{\Ahttps://trello.com/c/([a-zA-Z0-9]+)/?.*}.match(card_id_or_url)
|
|
21
|
+
match_data ? match_data[1] : card_id_or_url
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
# @param board [Trello::Board]
|
|
18
25
|
# @param list_name [String]
|
|
19
26
|
# @return [Trello::List]
|
data/lib/trello_tool/version.rb
CHANGED
data/lib/trello_tool_thor.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require "thor"
|
|
4
5
|
require "trello_tool/configuration"
|
|
5
6
|
require "trello_tool/health"
|
|
@@ -7,7 +8,7 @@ require "trello_tool/trello_client"
|
|
|
7
8
|
require "trello_tool/util"
|
|
8
9
|
|
|
9
10
|
# The thor class
|
|
10
|
-
# rubocop:disable Metrics/ClassLength
|
|
11
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
11
12
|
class TrelloToolThor < Thor
|
|
12
13
|
include TrelloTool::Util
|
|
13
14
|
|
|
@@ -15,6 +16,11 @@ class TrelloToolThor < Thor
|
|
|
15
16
|
@configuration ||= TrelloTool::Configuration.new
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
# a command that can't do what it was asked (raises Thor::Error) exits non-zero
|
|
20
|
+
def self.exit_on_failure?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
18
24
|
no_commands do
|
|
19
25
|
delegate :configuration, to: :class
|
|
20
26
|
end
|
|
@@ -89,6 +95,14 @@ class TrelloToolThor < Thor
|
|
|
89
95
|
end
|
|
90
96
|
end
|
|
91
97
|
|
|
98
|
+
desc "card CARD_ID_OR_URL",
|
|
99
|
+
"prints out a card as json (title, description, url, checklists, attachment urls)"
|
|
100
|
+
|
|
101
|
+
def card(card_id_or_url)
|
|
102
|
+
card = client.find_card(extract_card_id(card_id_or_url))
|
|
103
|
+
say JSON.pretty_generate(card_as_hash(card))
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
desc "release (VERSION)",
|
|
93
107
|
"rename next_version to VERSION and create next_version. If VERSION isn't specified use whatever is in ./.RELEASE_NEW_VERSION"
|
|
94
108
|
|
|
@@ -162,10 +176,236 @@ class TrelloToolThor < Thor
|
|
|
162
176
|
say "\n"
|
|
163
177
|
end
|
|
164
178
|
|
|
179
|
+
desc "search QUERY (BOARD_URL)",
|
|
180
|
+
"prints the unarchived cards matching a trello search, grouped by list " \
|
|
181
|
+
"(operators like label: and list: work) -- markdown, or --json"
|
|
182
|
+
method_option :json, type: :boolean, default: false, desc: "print json instead of markdown"
|
|
183
|
+
|
|
184
|
+
def search(query, url = configuration.main_board_url)
|
|
185
|
+
board = client.find_board(url)
|
|
186
|
+
print_cards(board, client.search_cards(board, query))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
desc "cards (BOARD_URL)", "prints every unarchived card in a board, grouped by list -- markdown, or --json"
|
|
190
|
+
method_option :json, type: :boolean, default: false, desc: "print json instead of markdown"
|
|
191
|
+
|
|
192
|
+
def cards(url = configuration.main_board_url)
|
|
193
|
+
board = client.find_board(url)
|
|
194
|
+
print_cards(board, client.open_cards(board))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
desc "comment CARD_ID_OR_URL (TEXT)", "adds a comment to a card (the text inline, or from --file)"
|
|
198
|
+
method_option :file, type: :string, desc: "read the comment from this file"
|
|
199
|
+
|
|
200
|
+
def comment(card_id_or_url, text = nil)
|
|
201
|
+
raise Thor::Error, "give the comment either inline or with --file, not both" if text && options[:file]
|
|
202
|
+
|
|
203
|
+
text = File.read(options[:file]) if options[:file]
|
|
204
|
+
raise Thor::Error, "the comment is empty" if text.to_s.strip.empty?
|
|
205
|
+
|
|
206
|
+
card = client.find_card(extract_card_id(card_id_or_url))
|
|
207
|
+
card.add_comment(text)
|
|
208
|
+
say "commented on #{card.url}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
desc "create (LIST_NAME (BOARD_URL)) --title TITLE (--desc TEXT | --desc-file PATH) (--label NAME ...) (--top) (--force)",
|
|
212
|
+
"creates a card at the bottom (or --top) of LIST_NAME (default: default_list_name_for_new_cards) and " \
|
|
213
|
+
"prints it as json; stops if an unarchived card already has that title, unless --force"
|
|
214
|
+
method_option :title, type: :string, required: true
|
|
215
|
+
method_option :desc, type: :string, desc: "the description"
|
|
216
|
+
method_option :desc_file, type: :string, desc: "read the description (markdown) from this file"
|
|
217
|
+
method_option :label, type: :array, default: [], desc: "names of labels of the board"
|
|
218
|
+
method_option :top, type: :boolean, default: false, desc: "at the top of the list rather than the bottom"
|
|
219
|
+
method_option :force, type: :boolean, default: false, desc: "create even if a card with this title exists"
|
|
220
|
+
|
|
221
|
+
def create(list_name = configuration.default_list_name_for_new_cards, url = configuration.main_board_url)
|
|
222
|
+
title = title_option!
|
|
223
|
+
description = description_option!
|
|
224
|
+
no_list = "give a LIST_NAME, or set default_list_name_for_new_cards in #{configuration.config_file}"
|
|
225
|
+
raise Thor::Error, no_list unless list_name
|
|
226
|
+
|
|
227
|
+
board = client.find_board(url)
|
|
228
|
+
list = find_list!(board, list_name)
|
|
229
|
+
label_ids = label_ids!(board, Array(options[:label]))
|
|
230
|
+
refuse_duplicate!(board, title) unless options[:force]
|
|
231
|
+
created = client.create_card(list, title: title, description: description, top: options[:top] ? true : false,
|
|
232
|
+
label_ids: label_ids)
|
|
233
|
+
say "created #{created['url']} in #{list.name.inspect}"
|
|
234
|
+
card(created["id"])
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
desc "checklist CARD_ID_OR_URL NAME --items ITEM ...",
|
|
238
|
+
"adds items to the card's checklist of that name (creating it if need be), skipping items already there; " \
|
|
239
|
+
"prints the checklist as json"
|
|
240
|
+
method_option :items, type: :array, default: [], desc: "the items to add"
|
|
241
|
+
method_option :checked, type: :boolean, default: false, desc: "add the items already ticked"
|
|
242
|
+
|
|
243
|
+
def checklist(card_id_or_url, name)
|
|
244
|
+
card_id = extract_card_id(card_id_or_url)
|
|
245
|
+
list = find_checklist(card_id, name) || create_checklist(card_id, name)
|
|
246
|
+
existing = list.check_items.map { |item| item["name"] }
|
|
247
|
+
(Array(options[:items]).uniq - existing).each { |item| list.add_item(item, options[:checked] ? true : false, "bottom") }
|
|
248
|
+
say JSON.pretty_generate(checklist_as_hash(find_checklist(card_id, name)))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
desc "check CARD_ID_OR_URL ITEM_TEXT (--checklist NAME) (--uncheck)",
|
|
252
|
+
"ticks (or with --uncheck unticks) the checklist item with that text (the whole text, or a part only it has)"
|
|
253
|
+
method_option :checklist, type: :string, desc: "only look in the checklist of this name"
|
|
254
|
+
method_option :uncheck, type: :boolean, default: false, desc: "untick instead"
|
|
255
|
+
|
|
256
|
+
def check(card_id_or_url, item_text)
|
|
257
|
+
card = client.find_card(extract_card_id(card_id_or_url))
|
|
258
|
+
item = find_check_item!(card, item_text)
|
|
259
|
+
client.set_check_item_state(card, item, complete: !options[:uncheck])
|
|
260
|
+
say "#{options[:uncheck] ? 'unticked' : 'ticked'} #{item['name'].inspect} on #{card.url}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
desc "move CARD_ID_OR_URL (LIST_NAME (BOARD_URL)) (--top | --bottom)",
|
|
264
|
+
"moves a card to the bottom (or --top) of a list; without LIST_NAME, to the top or bottom of the list it is in"
|
|
265
|
+
method_option :top, type: :boolean, default: false
|
|
266
|
+
method_option :bottom, type: :boolean, default: false
|
|
267
|
+
|
|
268
|
+
def move(card_id_or_url, list_name = nil, url = configuration.main_board_url)
|
|
269
|
+
position = position_option!(list_name)
|
|
270
|
+
card = client.find_card(extract_card_id(card_id_or_url))
|
|
271
|
+
list = find_list!(client.find_board(url), list_name) if list_name
|
|
272
|
+
client.move_card(card, position: position, list: list)
|
|
273
|
+
say "moved #{card.url} to the #{position} of #{list ? list.name.inspect : 'its list'}"
|
|
274
|
+
end
|
|
275
|
+
|
|
165
276
|
private
|
|
166
277
|
|
|
167
278
|
def client
|
|
168
279
|
TrelloTool::TrelloClient.new(configuration)
|
|
169
280
|
end
|
|
281
|
+
|
|
282
|
+
# @param cards [Array<Hash>] as trello returns them ("name", "url", "idList", "labels"); cards whose list is
|
|
283
|
+
# archived (not among the board's lists) are left out
|
|
284
|
+
def print_cards(board, cards)
|
|
285
|
+
# in the board's list order
|
|
286
|
+
rows = board.lists.flat_map { |list| cards.filter_map { |found| card_row(found, [list]) } }
|
|
287
|
+
return say(JSON.pretty_generate(rows)) if options[:json]
|
|
288
|
+
return say("no cards found") if rows.empty?
|
|
289
|
+
|
|
290
|
+
print_cards_as_markdown(rows)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def print_cards_as_markdown(rows)
|
|
294
|
+
rows.group_by { |row| row[:list] }.each do |list_name, in_list|
|
|
295
|
+
say "\n# #{list_name} (#{in_list.length} cards)\n\n"
|
|
296
|
+
in_list.each { |row| say "* [#{row[:title]}](#{row[:url]})#{row[:labels].map { |label| " [#{label}]" }.join}" }
|
|
297
|
+
end
|
|
298
|
+
say "\n"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def title_option!
|
|
302
|
+
title = options[:title].to_s.strip
|
|
303
|
+
raise Thor::Error, "--title is empty" if title.empty?
|
|
304
|
+
|
|
305
|
+
title
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def description_option!
|
|
309
|
+
both = "give the description either with --desc or with --desc-file, not both"
|
|
310
|
+
raise Thor::Error, both if options[:desc] && options[:desc_file]
|
|
311
|
+
|
|
312
|
+
options[:desc_file] ? File.read(options[:desc_file]) : options[:desc].to_s
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# @return [String] "top" or "bottom" (the default when the card is going to another list)
|
|
316
|
+
def position_option!(list_name)
|
|
317
|
+
raise Thor::Error, "--top or --bottom, not both" if options[:top] && options[:bottom]
|
|
318
|
+
raise Thor::Error, "give a LIST_NAME, --top or --bottom" unless list_name || options[:top] || options[:bottom]
|
|
319
|
+
|
|
320
|
+
options[:top] ? "top" : "bottom"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# @return [Hash, nil] nil when the card's list isn't one of lists
|
|
324
|
+
def card_row(found, lists)
|
|
325
|
+
list = lists.detect { |candidate| candidate.id == found["idList"] }
|
|
326
|
+
return nil unless list
|
|
327
|
+
|
|
328
|
+
{ title: found["name"], url: found["url"], list: list.name,
|
|
329
|
+
labels: (found["labels"] || []).map { |label| label["name"].to_s }.reject(&:empty?) }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# the list of that name (exactly, or failing that the only one that differs just by case)
|
|
333
|
+
def find_list!(board, list_name)
|
|
334
|
+
lists = board.lists
|
|
335
|
+
list = lists.detect { |candidate| candidate.name == list_name }
|
|
336
|
+
list ||= lists.select { |candidate| candidate.name.casecmp?(list_name) }.then { |found| found.first if found.size == 1 }
|
|
337
|
+
list || raise(Thor::Error, "no list called #{list_name.inspect} in #{board.name}. Lists: #{lists.map(&:name).join(', ')}")
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def label_ids!(board, names)
|
|
341
|
+
return [] if names.empty?
|
|
342
|
+
|
|
343
|
+
labels = board.labels
|
|
344
|
+
names.map do |name|
|
|
345
|
+
label = labels.detect { |candidate| candidate.name.to_s.casecmp?(name) }
|
|
346
|
+
label&.id || raise(Thor::Error, "no label called #{name.inspect} in #{board.name}. " \
|
|
347
|
+
"Labels: #{labels.map { |candidate| candidate.name.to_s }.reject(&:empty?).join(', ')}")
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def refuse_duplicate!(board, title)
|
|
352
|
+
duplicate = client.search_cards(board, %("#{title.delete('"')}")).detect { |found| found["name"].to_s.strip.casecmp?(title) }
|
|
353
|
+
return unless duplicate
|
|
354
|
+
|
|
355
|
+
raise Thor::Error, "a card called #{title.inspect} already exists: #{duplicate['url']} (--force to create another)"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# always from a fresh read of the card, so it sees a checklist or item that was just added
|
|
359
|
+
def find_checklist(card_id, name)
|
|
360
|
+
client.find_card(card_id).checklists.detect { |candidate| candidate.name == name }
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def create_checklist(card_id, name)
|
|
364
|
+
client.find_card(card_id).create_new_checklist(name)
|
|
365
|
+
find_checklist(card_id, name) || raise(Thor::Error, "couldn't create the checklist #{name.inspect}")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# @return [Hash] the one check item whose name is item_text, or failing that the one containing it
|
|
369
|
+
def find_check_item!(card, item_text)
|
|
370
|
+
items = check_items!(card)
|
|
371
|
+
matches = items.select { |item| item["name"].casecmp?(item_text) }
|
|
372
|
+
matches = items.select { |item| item["name"].downcase.include?(item_text.downcase) } if matches.empty?
|
|
373
|
+
return matches.first if matches.size == 1
|
|
374
|
+
|
|
375
|
+
problem = matches.empty? ? "no item matching" : "more than one item matching"
|
|
376
|
+
candidates = (matches.empty? ? items : matches).map { |item| item["name"].inspect }.join(", ")
|
|
377
|
+
raise Thor::Error, "#{problem} #{item_text.inspect} on #{card.url}. Items: #{candidates}"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# @return [Array<Hash>] the check items of the card's checklists (of the --checklist one, if given)
|
|
381
|
+
def check_items!(card)
|
|
382
|
+
name = options[:checklist]
|
|
383
|
+
checklists = card.checklists.select { |candidate| name.nil? || candidate.name == name }
|
|
384
|
+
raise Thor::Error, "no checklist#{" called #{name.inspect}" if name} on #{card.url}" if checklists.empty?
|
|
385
|
+
|
|
386
|
+
checklists.flat_map(&:check_items)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# @param card [Trello::Card]
|
|
390
|
+
# @return [Hash]
|
|
391
|
+
def card_as_hash(card)
|
|
392
|
+
{
|
|
393
|
+
title: card.name,
|
|
394
|
+
description: card.desc,
|
|
395
|
+
url: card.url,
|
|
396
|
+
checklists: card.checklists.map { |checklist| checklist_as_hash(checklist) },
|
|
397
|
+
attachment_urls: card.attachments.map(&:url)
|
|
398
|
+
}
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# @param checklist [Trello::Checklist]
|
|
402
|
+
# @return [Hash]
|
|
403
|
+
def checklist_as_hash(checklist)
|
|
404
|
+
{
|
|
405
|
+
name: checklist.name,
|
|
406
|
+
items: checklist.check_items.map do |item|
|
|
407
|
+
{ name: item["name"], complete: item["state"] == "complete" }
|
|
408
|
+
end
|
|
409
|
+
}
|
|
410
|
+
end
|
|
170
411
|
end
|
|
171
|
-
# rubocop:enable Metrics/ClassLength
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trello_tool
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tim Diggins
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: ruby-trello
|
|
@@ -38,7 +37,6 @@ dependencies:
|
|
|
38
37
|
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '0'
|
|
41
|
-
description:
|
|
42
40
|
email:
|
|
43
41
|
- tim@red56.uk
|
|
44
42
|
executables:
|
|
@@ -73,7 +71,6 @@ metadata:
|
|
|
73
71
|
source_code_uri: https://github.com/timdiggins/trello_tool
|
|
74
72
|
changelog_uri: https://github.com/timdiggins/trello_tool/blob/main/CHANGELOG.md
|
|
75
73
|
rubygems_mfa_required: 'true'
|
|
76
|
-
post_install_message:
|
|
77
74
|
rdoc_options: []
|
|
78
75
|
require_paths:
|
|
79
76
|
- lib
|
|
@@ -88,8 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
85
|
- !ruby/object:Gem::Version
|
|
89
86
|
version: '0'
|
|
90
87
|
requirements: []
|
|
91
|
-
rubygems_version: 3.
|
|
92
|
-
signing_key:
|
|
88
|
+
rubygems_version: 3.6.9
|
|
93
89
|
specification_version: 4
|
|
94
90
|
summary: Tool for doing basic things to a dev trello using the api.
|
|
95
91
|
test_files: []
|