kood 0.0.1
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.
- data/.gitignore +47 -0
- data/.yardopts +4 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +9 -0
- data/Rakefile +40 -0
- data/bin/kood +4 -0
- data/kood.gemspec +26 -0
- data/lib/kood-plugin-example.rb +10 -0
- data/lib/kood.rb +14 -0
- data/lib/kood/adapter/git.rb +56 -0
- data/lib/kood/adapter/user_config.rb +36 -0
- data/lib/kood/board.rb +160 -0
- data/lib/kood/card.rb +128 -0
- data/lib/kood/cli.rb +121 -0
- data/lib/kood/cli/board.rb +139 -0
- data/lib/kood/cli/card.rb +188 -0
- data/lib/kood/cli/edit.rb +40 -0
- data/lib/kood/cli/helpers/shell.rb +124 -0
- data/lib/kood/cli/helpers/table.rb +195 -0
- data/lib/kood/cli/list.rb +70 -0
- data/lib/kood/cli/plugin.rb +37 -0
- data/lib/kood/cli/switch.rb +10 -0
- data/lib/kood/core.rb +95 -0
- data/lib/kood/errors.rb +8 -0
- data/lib/kood/extensions/grit.rb +65 -0
- data/lib/kood/list.rb +36 -0
- data/lib/kood/version.rb +3 -0
- data/man/kood-board.1 +74 -0
- data/man/kood-board.1.html +150 -0
- data/man/kood-board.1.ronn +65 -0
- data/man/kood-card.1 +40 -0
- data/man/kood-card.1.html +140 -0
- data/man/kood-card.1.ronn +52 -0
- data/spec/kood/cli_spec.rb +280 -0
- data/spec/spec_helper.rb +67 -0
- data/test/kood/cli_test.rb +67 -0
- metadata +198 -0
data/lib/kood/card.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require 'active_support/core_ext'
|
|
2
|
+
require 'toystore'
|
|
3
|
+
|
|
4
|
+
module Kood
|
|
5
|
+
class Card
|
|
6
|
+
include Toy::Store
|
|
7
|
+
|
|
8
|
+
# References
|
|
9
|
+
reference :list, List
|
|
10
|
+
|
|
11
|
+
# Attributes
|
|
12
|
+
attribute :title, String
|
|
13
|
+
attribute :content, String
|
|
14
|
+
attribute :participants, Array
|
|
15
|
+
attribute :labels, Array
|
|
16
|
+
attribute :position, Integer
|
|
17
|
+
attribute :date, Time, default: lambda { Time.now }
|
|
18
|
+
attribute :more, Hash # to store additional user-defined properties
|
|
19
|
+
|
|
20
|
+
def self.get!(id)
|
|
21
|
+
super rescue raise NotFound, "The specified card does not exist."
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.find_all_by_partial_attribute(attrs, search_param, options = {})
|
|
25
|
+
cards = options.key?(:list) ? options[:list].cards : Board.current!.cards
|
|
26
|
+
|
|
27
|
+
attrs.split('_or_').map do |a|
|
|
28
|
+
cards.select do |c|
|
|
29
|
+
# `search_param` may be a normal string or a string representing a regular expression
|
|
30
|
+
c[a].match /#{ search_param }/i or c[a].downcase.include?(search_param.downcase)
|
|
31
|
+
end
|
|
32
|
+
end.flatten
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# If the `unique` option is present, an exception must be raised if:
|
|
36
|
+
# - More than one exact match was found
|
|
37
|
+
# - Zero exact matches were found but more than one partial match was found
|
|
38
|
+
# If `unique` not present, return the first match giving preference to exact matches
|
|
39
|
+
#
|
|
40
|
+
def self.find_by_partial_attribute!(attrs, search_param, options = {})
|
|
41
|
+
must_unique = options[:unique] == true
|
|
42
|
+
|
|
43
|
+
# Find partial (and exact) matches
|
|
44
|
+
matches = find_all_by_partial_attribute(attrs, search_param, options)
|
|
45
|
+
|
|
46
|
+
# If nothing was found, raise an exception
|
|
47
|
+
raise NotFound, "The specified card does not exist." if matches.empty?
|
|
48
|
+
|
|
49
|
+
# Refine the search and retrieve only exact matches
|
|
50
|
+
exact_matches = attrs.split('_or_').map do |a|
|
|
51
|
+
matches.select { |c| c[a].casecmp(search_param).zero? }
|
|
52
|
+
end.flatten
|
|
53
|
+
|
|
54
|
+
if (must_unique and exact_matches.length == 1) or (!must_unique and !exact_matches.empty?)
|
|
55
|
+
exact_matches.first
|
|
56
|
+
elsif matches.length > 1 and must_unique
|
|
57
|
+
raise MultipleDocumentsFound, "Multiple cards match the given criteria."
|
|
58
|
+
else
|
|
59
|
+
matches.first
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# FIXME With humanize "foo_id" becomes "Foo" instead of "Foo Id" which may be confusing
|
|
64
|
+
def pretty_attributes(to_print = [ 'labels', 'participants', 'more' ])
|
|
65
|
+
attrs = self.attributes.dup
|
|
66
|
+
attrs.delete_if { |k, v| v.blank? or k.eql? 'more' or not to_print.include? k }
|
|
67
|
+
attrs.merge! self.more
|
|
68
|
+
max_size = attrs.keys.max_by { |k| k.humanize.size }.humanize.size unless attrs.empty?
|
|
69
|
+
|
|
70
|
+
attrs.map do |key, value|
|
|
71
|
+
case value
|
|
72
|
+
when Array
|
|
73
|
+
"#{ (key.humanize + ":").ljust(max_size + 2) } #{ value.join(', ') }"
|
|
74
|
+
else
|
|
75
|
+
"#{ (key.humanize + ":").ljust(max_size + 2) } #{ value }"
|
|
76
|
+
end
|
|
77
|
+
end.compact.join("\n")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def edit_file
|
|
81
|
+
board = Board.current!
|
|
82
|
+
changed = false
|
|
83
|
+
|
|
84
|
+
adapter.client.with_stash_and_branch(board.id) do
|
|
85
|
+
Dir.chdir(board.root) do
|
|
86
|
+
yield filepath if block_given?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
data = File.read(File.join(board.root, filepath))
|
|
90
|
+
self.attributes = Card.adapter.decode(data)
|
|
91
|
+
changed = self.changed?
|
|
92
|
+
|
|
93
|
+
save! if changed
|
|
94
|
+
adapter.client.git.reset(hard: true)
|
|
95
|
+
end
|
|
96
|
+
changed
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def self.method_missing(meth, *args, &block)
|
|
102
|
+
if meth.to_s =~ /^find_all_by_partial_(.+)$/
|
|
103
|
+
find_all_by_partial_attribute($1, *args)
|
|
104
|
+
elsif meth.to_s =~ /^find_by_partial_(.+)!$/
|
|
105
|
+
find_by_partial_attribute!($1, *args)
|
|
106
|
+
else
|
|
107
|
+
super
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# ToyStore supports adapters per model but this program needs an adapter per instance
|
|
112
|
+
def self.with_adapter(branch, root)
|
|
113
|
+
current_client = adapter.client
|
|
114
|
+
current_options = adapter.options
|
|
115
|
+
|
|
116
|
+
adapter :git, Kood.repo(root), branch: branch, path: 'cards'
|
|
117
|
+
adapter.file_extension = 'md'
|
|
118
|
+
yield
|
|
119
|
+
ensure
|
|
120
|
+
adapter :git, current_client, current_options
|
|
121
|
+
adapter.file_extension = 'md'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def filepath
|
|
125
|
+
File.join('cards', id) + ".#{ adapter.file_extension }"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/kood/cli.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'thor'
|
|
2
|
+
require 'thor/group'
|
|
3
|
+
|
|
4
|
+
require_relative 'cli/helpers/table'
|
|
5
|
+
require_relative 'cli/board'
|
|
6
|
+
require_relative 'cli/switch'
|
|
7
|
+
require_relative 'cli/list'
|
|
8
|
+
require_relative 'cli/card'
|
|
9
|
+
require_relative 'cli/edit'
|
|
10
|
+
require_relative 'cli/plugin'
|
|
11
|
+
|
|
12
|
+
class Kood::CLI < Thor
|
|
13
|
+
namespace :kood
|
|
14
|
+
|
|
15
|
+
class_option 'debug', :desc => "Run Kood in debug mode", :type => :boolean
|
|
16
|
+
class_option 'color', :desc => "Colorization in output", :type => :boolean,
|
|
17
|
+
:default => Kood::Shell.color_support?
|
|
18
|
+
|
|
19
|
+
check_unknown_options!
|
|
20
|
+
|
|
21
|
+
# Thor help is not used for subcommands. Docs for each subcommand are written in the
|
|
22
|
+
# man files.
|
|
23
|
+
#
|
|
24
|
+
# If the `default` key of `method_option` is a method, it will be executed each time
|
|
25
|
+
# this program is called. As an alternative, a default value is specified inside each
|
|
26
|
+
# method. So for example instead of:
|
|
27
|
+
#
|
|
28
|
+
# method_option ... :default => foo()
|
|
29
|
+
# def bar(arg)
|
|
30
|
+
# ...
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# The following is done:
|
|
34
|
+
#
|
|
35
|
+
# method_option ...
|
|
36
|
+
# def bar(arg)
|
|
37
|
+
# arg = foo() if arg.nil?
|
|
38
|
+
# end
|
|
39
|
+
|
|
40
|
+
# Invoked with `kood --help`, `kood help`, `kood help <cmd>` and `kood --help <cmd>`
|
|
41
|
+
def help(cli = nil)
|
|
42
|
+
case cli
|
|
43
|
+
when nil then command = "kood"
|
|
44
|
+
when "boards" then command = "kood-board"
|
|
45
|
+
when "select" then command = "kood-switch"
|
|
46
|
+
when "lists" then command = "kood-list"
|
|
47
|
+
when "cards" then command = "kood-card"
|
|
48
|
+
else command = "kood-#{cli}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
manpages = %w(
|
|
52
|
+
kood-board
|
|
53
|
+
kood-switch
|
|
54
|
+
kood-list
|
|
55
|
+
kood-card)
|
|
56
|
+
|
|
57
|
+
if manpages.include? command # Present a man page for the command
|
|
58
|
+
root = File.expand_path("../../../man", __FILE__)
|
|
59
|
+
exec "man #{ root }/#{ command }.1"
|
|
60
|
+
else
|
|
61
|
+
super # Use thor to output help
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc "version", "Print kood's version information"
|
|
66
|
+
map ['--version', '-v'] => :version
|
|
67
|
+
def version
|
|
68
|
+
puts "kood version #{ Kood::VERSION }"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Reimplement the `start` method in order to catch raised exceptions
|
|
74
|
+
# For example, when running `kood c`, Thor will raise "Ambiguous task c matches ..."
|
|
75
|
+
# FIXME Should not be necessary, since Thor catches exceptions when not in debug mode
|
|
76
|
+
def self.start(given_args=ARGV, config={})
|
|
77
|
+
Grit.debug = given_args.include?('--debug')
|
|
78
|
+
super
|
|
79
|
+
rescue Exception => e
|
|
80
|
+
if given_args.include? '--debug'
|
|
81
|
+
puts e.inspect
|
|
82
|
+
puts e.backtrace
|
|
83
|
+
elsif given_args.include? '--no-color'
|
|
84
|
+
puts e
|
|
85
|
+
else
|
|
86
|
+
puts "\e[31m#{ e }\e[0m"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def no_method_options?
|
|
91
|
+
(options.keys - self.class.class_options.keys).empty?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ok(text)
|
|
95
|
+
# An idea from [git.io/logbook](//git.io/logbook). You should check it out.
|
|
96
|
+
if options.color?
|
|
97
|
+
say text, :green
|
|
98
|
+
else
|
|
99
|
+
puts text
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def error(text)
|
|
104
|
+
if options.color?
|
|
105
|
+
say text, :red
|
|
106
|
+
else
|
|
107
|
+
puts text
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Third-party commands (plugins)
|
|
113
|
+
#
|
|
114
|
+
Kood::CLI.load_plugins
|
|
115
|
+
|
|
116
|
+
# Warn users that non-ascii characters in the arguments may cause errors
|
|
117
|
+
#
|
|
118
|
+
if ARGV.any? { |arg| not arg.ascii_only? }
|
|
119
|
+
puts "For now, please avoid non-ascii characters. We're still working on providing" \
|
|
120
|
+
" full support for utf-8 encoding."
|
|
121
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
class Kood::CLI < Thor
|
|
2
|
+
|
|
3
|
+
desc "board [OPTIONS] [<BOARD-ID>]", "Display and manage boards"
|
|
4
|
+
#
|
|
5
|
+
# Delete a board. If <board-id> is present, the specified board will be deleted.
|
|
6
|
+
# With no arguments, the current board will be deleted.
|
|
7
|
+
method_option :delete, :aliases => '-d', :type => :boolean
|
|
8
|
+
#
|
|
9
|
+
# Copy a board. <board-id> will be copied to <new-board-id>.
|
|
10
|
+
# <board-id> will be kept intact and a new one is created with the exact same data.
|
|
11
|
+
method_option :copy, :aliases => '-c', :type => :string
|
|
12
|
+
#
|
|
13
|
+
# Create a board in an external repository.
|
|
14
|
+
method_option :repo, :aliases => '-r', :type => :string
|
|
15
|
+
def board(board_id = nil)
|
|
16
|
+
# If no arguments and options are present, the command displays all existing boards
|
|
17
|
+
if board_id.nil? and no_method_options?
|
|
18
|
+
list_existing_boards
|
|
19
|
+
|
|
20
|
+
# If the <board-id> argument is present without options, a new board will be created
|
|
21
|
+
elsif no_method_options? or options.repo.present?
|
|
22
|
+
create_board(board_id)
|
|
23
|
+
|
|
24
|
+
else # Since <board-id> is present, operate on the specified board
|
|
25
|
+
operate_on_board(board_id)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
map 'boards' => 'board'
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def operate_on_board(board_id)
|
|
33
|
+
board = get_board_or_current!(board_id) # Raises exception if inexistent
|
|
34
|
+
|
|
35
|
+
copy_board(board) if options.copy.present?
|
|
36
|
+
delete_board(board_id) if options.key? 'delete'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def list_existing_boards
|
|
40
|
+
return error "No boards were found." if Kood.config.boards.empty?
|
|
41
|
+
|
|
42
|
+
max_board_id = Kood.config.boards.max_by { |b| b.id.size }.id.size
|
|
43
|
+
Kood.config.boards.each do |b|
|
|
44
|
+
marker = b.is_current? ? "* " : " "
|
|
45
|
+
visibility = b.published? ? "shared" : "private"
|
|
46
|
+
repo_path = b.root.gsub(/^#{ ENV['HOME'] }/, "~") if b.external?
|
|
47
|
+
visibility = "(#{ visibility }#{ ' at '+ repo_path if b.external? })"
|
|
48
|
+
visibility = set_color(visibility, :black, :bold) if options.color?
|
|
49
|
+
puts marker + b.id.to_s.ljust(max_board_id + 2) + visibility
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_board(board_id)
|
|
54
|
+
board = Kood.config.boards.create(id: board_id, custom_repo: options['repo'])
|
|
55
|
+
|
|
56
|
+
unless board.persisted?
|
|
57
|
+
msgs = board.errors.full_messages.join("\n")
|
|
58
|
+
return error "#{ msgs.gsub('Id', 'Board ID') }."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if Kood.config.boards.size == 1
|
|
62
|
+
board.select
|
|
63
|
+
ok "Board created and selected."
|
|
64
|
+
else
|
|
65
|
+
ok "Board created."
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def copy_board(board)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def delete_board(board_id)
|
|
73
|
+
Kood.config.boards.destroy(board_id)
|
|
74
|
+
ok "Board deleted."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def get_board_or_current!(board_id)
|
|
78
|
+
board_id.nil? ? Kood::Board.current! : Kood::Board.get!(board_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def print_board(board)
|
|
82
|
+
opts = options.color? ? { color: [:black, :bold] } : {}
|
|
83
|
+
num_lists = board.list_ids.size
|
|
84
|
+
header = Kood::Table.new(num_lists)
|
|
85
|
+
body = Kood::Table.new(num_lists)
|
|
86
|
+
|
|
87
|
+
board.lists.each do |list|
|
|
88
|
+
# Setup the header of the table
|
|
89
|
+
header.new_column.add_row(list.id, align: 'center', separator: false)
|
|
90
|
+
|
|
91
|
+
# Setup the body of the table
|
|
92
|
+
column = body.new_column
|
|
93
|
+
list.cards.each do |card|
|
|
94
|
+
card_info = card.title
|
|
95
|
+
if options.color?
|
|
96
|
+
colored_separator = color_separator_with_labels(column.separator, card.labels)
|
|
97
|
+
column.add_row(colored_separator, slice: false)
|
|
98
|
+
else
|
|
99
|
+
labels = card.labels.uniq.map { |l| "##{ l }" }.join(", ")
|
|
100
|
+
card_info += " #{ labels }" unless labels.blank?
|
|
101
|
+
column.add_row(column.separator, separator: false)
|
|
102
|
+
end
|
|
103
|
+
column.add_row(card_info, separator: false)
|
|
104
|
+
column.add_row(card.id.slice(0, 8), opts.merge(separator: false))
|
|
105
|
+
end
|
|
106
|
+
column.add_row(column.separator, slice: false)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Setup the title of the table
|
|
110
|
+
title = Kood::Table.new(1, body.width)
|
|
111
|
+
title.new_column.add_row(board.id, align: 'center')
|
|
112
|
+
|
|
113
|
+
out = [ title.to_s(separator: false) ]
|
|
114
|
+
out << header.separator('first') << header
|
|
115
|
+
out << body unless body.to_s.empty?
|
|
116
|
+
out << body.separator('last')
|
|
117
|
+
|
|
118
|
+
# `join` is used to prevent partial content from being printed if an exception occurs
|
|
119
|
+
puts out.join("\n")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def color_separator_with_labels(separator, labels)
|
|
123
|
+
return separator if labels.blank? or not options.color?
|
|
124
|
+
|
|
125
|
+
hbar = Kood::Shell.horizontal_bar
|
|
126
|
+
colored_bars = labels.map { |l| set_color(hbar * 3, label_to_color(l)) }.uniq
|
|
127
|
+
|
|
128
|
+
if colored_bars.length * 3 > separator.length
|
|
129
|
+
colored_bars = colored_bars[0...separator.length/3-1]
|
|
130
|
+
colored_bars << set_color(hbar * 3, :black, :bold)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
separator[0...-colored_bars.length*3] + colored_bars.join
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def label_to_color(label)
|
|
137
|
+
(Kood.config.labels[label] || 'blue').to_sym
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
require 'active_support/core_ext/hash/except'
|
|
2
|
+
|
|
3
|
+
class Kood::CLI < Thor
|
|
4
|
+
|
|
5
|
+
desc "card [OPTIONS] [<CARD-ID|CARD-TITLE>]", "Display and manage cards"
|
|
6
|
+
#
|
|
7
|
+
# Delete a card. If <card-title> is present, the specified card will be deleted.
|
|
8
|
+
method_option :delete, :aliases => '-d', :type => :boolean
|
|
9
|
+
#
|
|
10
|
+
# Copy a card. <card-title> will be copied to a new card in the given list.
|
|
11
|
+
# <card-title> will be kept intact and a new one is created with the exact same data.
|
|
12
|
+
method_option :copy, :aliases => '-c', :type => :string
|
|
13
|
+
#
|
|
14
|
+
# Show status information of the current board associated with the given user.
|
|
15
|
+
method_option :participant, :aliases => '-p', :type => :string
|
|
16
|
+
#
|
|
17
|
+
# Does the card action in the given list.
|
|
18
|
+
method_option :list, :aliases => '-l', :type => :string
|
|
19
|
+
#
|
|
20
|
+
# Launches the configured editor to modify the card
|
|
21
|
+
method_option :edit, :aliases => '-e', :type => :boolean
|
|
22
|
+
#
|
|
23
|
+
# Set and unset properties (add and remove are useful for properties that are arrays)
|
|
24
|
+
method_option :set, :aliases => '-s', :type => :hash
|
|
25
|
+
method_option :unset, :aliases => '-u', :type => :array
|
|
26
|
+
method_option :add, :aliases => '-a', :type => :array
|
|
27
|
+
method_option :remove, :aliases => '-r', :type => :array
|
|
28
|
+
def card(card_id_or_title = nil)
|
|
29
|
+
Kood::Board.current!.with_context do |current_board|
|
|
30
|
+
card_title = card_id = card_id_or_title
|
|
31
|
+
|
|
32
|
+
# If no arguments and options are specified, display all existing cards
|
|
33
|
+
if card_title.nil? and no_method_options?
|
|
34
|
+
return error "No lists were found." if current_board.lists.empty?
|
|
35
|
+
print_board(current_board)
|
|
36
|
+
|
|
37
|
+
# If <card-title> is present without options, display the card with given ID or title
|
|
38
|
+
elsif card_id_or_title and no_method_options?
|
|
39
|
+
card = Kood::Card.find_by_partial_id_or_title!(card_id_or_title)
|
|
40
|
+
print_card(current_board, card)
|
|
41
|
+
|
|
42
|
+
else # If <card-title> and the `list` option are present, a new card is created
|
|
43
|
+
create_card(card_id_or_title) if card_id_or_title and options.list.present?
|
|
44
|
+
|
|
45
|
+
# Since <card-title> is present, operate on the specified card
|
|
46
|
+
operate_on_card(current_board, card_id)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
map 'cards' => 'card'
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def operate_on_card(current_board, card_id_or_title, card_id = card_id_or_title)
|
|
55
|
+
card = Kood::Card.find_by_partial_id_or_title!(card_id_or_title)
|
|
56
|
+
|
|
57
|
+
copy_card(card) if options.copy.present?
|
|
58
|
+
delete_card(card_id) if options.key? 'delete'
|
|
59
|
+
return edit(card_id) if options.edit.present? # Execute the `edit` task
|
|
60
|
+
|
|
61
|
+
if options.any? { |k,v| %w{ set unset add remove }.include? k }
|
|
62
|
+
set_card_attributes(card) if options.set.present?
|
|
63
|
+
unset_card_attributes(card) if options.unset.present?
|
|
64
|
+
insert_into_card_array_attribute(current_board, card) if options.add.present?
|
|
65
|
+
remove_from_card_array_attribute(current_board, card) if options.remove.present?
|
|
66
|
+
|
|
67
|
+
if card.changed?
|
|
68
|
+
card.save!
|
|
69
|
+
ok "Card updated."
|
|
70
|
+
else
|
|
71
|
+
error "No changes to persist."
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def create_card(card_title)
|
|
77
|
+
list = Kood::List.get! options.list
|
|
78
|
+
list.cards.create(title: card_title, list: list)
|
|
79
|
+
ok "Card created."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def copy_card(card) # TODO Support card copy between boards
|
|
83
|
+
list = options.copy.eql?('copy') ? card.list : Kood::List.get!(options.copy)
|
|
84
|
+
list.cards.create(card.dup.attributes.except 'date', list: list)
|
|
85
|
+
ok "Card copied."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def delete_card(card_id_or_title)
|
|
89
|
+
card = Kood::Card.find_by_partial_id_or_title!(card_id_or_title, unique: true)
|
|
90
|
+
list = card.list
|
|
91
|
+
list.cards.destroy(card.id)
|
|
92
|
+
ok "Card deleted."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Iterates over all pairs of `options.set` and:
|
|
96
|
+
# - If a key is an attribute of card, sets its value
|
|
97
|
+
# - If a key is not an attribute of card, set it in the `more` hash, which is used to
|
|
98
|
+
# store custom attributes defined by the user
|
|
99
|
+
#
|
|
100
|
+
# Example: kood card lorem --set title:lorem description:"lorem ipsum" foo:bar
|
|
101
|
+
#
|
|
102
|
+
def set_card_attributes(card)
|
|
103
|
+
options.set.each do |key, value|
|
|
104
|
+
value = Kood::Shell.type_cast(value) # Convert to float or int if possible
|
|
105
|
+
|
|
106
|
+
if Kood::Card.attribute? key and not %w{ list list_id more }.include? key.to_s
|
|
107
|
+
card[key] = value
|
|
108
|
+
else
|
|
109
|
+
card.more ||= {}
|
|
110
|
+
card.more = card.more.merge(key => value) # Has to be "card.more=" to be consi-
|
|
111
|
+
end # dered changed (merge! wouldn't work)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# It operates in the same way of the `set_card_attributes` method but iterates over
|
|
116
|
+
# the `options.unset` array, instead of an hash
|
|
117
|
+
#
|
|
118
|
+
# Example: kood card lorem --unset title description labels
|
|
119
|
+
#
|
|
120
|
+
def unset_card_attributes(card)
|
|
121
|
+
options.unset.each do |key|
|
|
122
|
+
if Kood::Card.attribute? key and not %w{ title list list_id more }.include? key
|
|
123
|
+
card[key] = nil
|
|
124
|
+
else
|
|
125
|
+
card.more ||= {}
|
|
126
|
+
card.more = card.more.except(key) # Has to be "card.more=" to be considered changed
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def update_card_array_attribute(current_board, card, opt)
|
|
132
|
+
values = opt.eql?(:+) ? options.add : options.remove
|
|
133
|
+
key = values.shift
|
|
134
|
+
|
|
135
|
+
if key.eql? 'participants'
|
|
136
|
+
board_members = values.map do |v|
|
|
137
|
+
current_board.find_potential_member_by_partial_name_or_email(v) || v
|
|
138
|
+
end
|
|
139
|
+
# Since this command may be called by other users with distinct boards, the found
|
|
140
|
+
# board members may be different, so always keep the typed values if this is about
|
|
141
|
+
# removing participants
|
|
142
|
+
values = opt.eql?(:+) ? board_members : (values + board_members)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
if Kood::Card.attribute? key and Kood::Card.attributes[key].type.eql? Array
|
|
146
|
+
card[key] ||= []
|
|
147
|
+
card[key] = opt.eql?(:+) ? (card[key] + values) : (card[key] - values)
|
|
148
|
+
else
|
|
149
|
+
begin
|
|
150
|
+
card.more ||= {}
|
|
151
|
+
card.more[key] ||= []
|
|
152
|
+
new_value = opt.eql?(:+) ? (card.more[key] + values) : (card.more[key] - values)
|
|
153
|
+
card.more = card.more.merge(key => new_value) # Has to be "card.more=" to be consi-
|
|
154
|
+
rescue TypeError, NoMethodError # dered changed (merge! wouldn't work)
|
|
155
|
+
raise Kood::TypeError, "Can't convert the attribute into a list."
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# TODO Example: kood card lorem --add participants David Diogo -a labels bug
|
|
161
|
+
def insert_into_card_array_attribute(current_board, card)
|
|
162
|
+
update_card_array_attribute(current_board, card, :+)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# TODO Example: kood card lorem --remove participants David
|
|
166
|
+
def remove_from_card_array_attribute(current_board, card)
|
|
167
|
+
update_card_array_attribute(current_board, card, :-)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def print_card(board, card)
|
|
171
|
+
# The board's table may not use all terminal's horizontal space. In order to keep
|
|
172
|
+
# things visually similar / aligned, the card table should have the same width.
|
|
173
|
+
width = Kood::Table.new(board.list_ids.size).width
|
|
174
|
+
|
|
175
|
+
table = Kood::Table.new(1, width)
|
|
176
|
+
col = table.new_column
|
|
177
|
+
attribute_list = card.pretty_attributes
|
|
178
|
+
col.add_row(card.title, separator: !(card.content.empty? and attribute_list.empty?))
|
|
179
|
+
col.add_row(card.content) unless card.content.empty?
|
|
180
|
+
col.add_row(attribute_list) unless attribute_list.empty?
|
|
181
|
+
|
|
182
|
+
opts = options.color? ? { color: [:black, :bold] } : {}
|
|
183
|
+
col.add_row("#{ card.id } (created at #{ card.date })", opts)
|
|
184
|
+
|
|
185
|
+
# `join` is used to prevent partial content from being printed if an exception occurs
|
|
186
|
+
puts [table.separator('first'), table, table.separator('last')].join("\n")
|
|
187
|
+
end
|
|
188
|
+
end
|