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,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shale"
|
|
4
|
+
|
|
5
|
+
module Superthread
|
|
6
|
+
# Base class for all Shale-based API response models.
|
|
7
|
+
# Provides a consistent interface compatible with existing code patterns.
|
|
8
|
+
#
|
|
9
|
+
# @example Defining a model
|
|
10
|
+
# class Card < Superthread::Model
|
|
11
|
+
# attribute :id, Shale::Type::String
|
|
12
|
+
# attribute :title, Shale::Type::String
|
|
13
|
+
# attribute :priority, Shale::Type::Integer
|
|
14
|
+
# attribute :members, Member, collection: true
|
|
15
|
+
#
|
|
16
|
+
# def priority_name
|
|
17
|
+
# { 1 => 'urgent', 2 => 'high', 3 => 'medium', 4 => 'low' }[priority]
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Using a model
|
|
22
|
+
# card = Card.from_hash(json_data)
|
|
23
|
+
# card.title # => "My Card"
|
|
24
|
+
# card.priority # => 1
|
|
25
|
+
# card.priority_name # => "urgent"
|
|
26
|
+
# card.to_h # => { id: "123", title: "My Card", ... }
|
|
27
|
+
class Model < Shale::Mapper
|
|
28
|
+
class << self
|
|
29
|
+
# Check if this is a Shale-based model.
|
|
30
|
+
# Used by the client to determine deserialization method.
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean] Always true for Model subclasses
|
|
33
|
+
def shale_model?
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check if a given class is a Shale-based model.
|
|
38
|
+
#
|
|
39
|
+
# @param klass [Class] the class to check
|
|
40
|
+
# @return [Boolean] true if the class responds to shale_model? and returns true
|
|
41
|
+
def shale_class?(klass)
|
|
42
|
+
klass.respond_to?(:shale_model?) && klass.shale_model?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Construct a model from a hash (API response).
|
|
46
|
+
# This is the primary factory method used by the client.
|
|
47
|
+
#
|
|
48
|
+
# @param data [Hash] The hash data from the API
|
|
49
|
+
# @return [Model] The constructed model instance
|
|
50
|
+
def from_response(data)
|
|
51
|
+
return nil if data.nil?
|
|
52
|
+
|
|
53
|
+
# Shale's from_hash expects string keys, so we deep-transform
|
|
54
|
+
from_hash(deep_stringify_keys(data))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Recursively stringify hash keys for Shale compatibility.
|
|
60
|
+
#
|
|
61
|
+
# @param obj [Object] Object to process
|
|
62
|
+
# @return [Object] Object with stringified keys
|
|
63
|
+
def deep_stringify_keys(obj)
|
|
64
|
+
case obj
|
|
65
|
+
when Hash
|
|
66
|
+
obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
|
|
67
|
+
when Array
|
|
68
|
+
obj.map { |v| deep_stringify_keys(v) }
|
|
69
|
+
else
|
|
70
|
+
obj
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
public
|
|
75
|
+
|
|
76
|
+
# Construct a collection of models from an array of hashes.
|
|
77
|
+
#
|
|
78
|
+
# @param items [Array<Hash>] Array of hash data
|
|
79
|
+
# @return [Array<Model>] Array of model instances
|
|
80
|
+
def from_response_array(items)
|
|
81
|
+
return [] if items.nil?
|
|
82
|
+
|
|
83
|
+
items.map { |item| from_response(item) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Convert to a hash with symbol keys.
|
|
88
|
+
# Provides compatibility with existing code expecting symbol keys.
|
|
89
|
+
# Uses Shale's to_hash internally, then symbolizes keys.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash] Hash representation with symbol keys
|
|
92
|
+
def to_h
|
|
93
|
+
deep_symbolize_keys(to_hash)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Recursively symbolize hash keys.
|
|
99
|
+
#
|
|
100
|
+
# @param obj [Object] Object to process
|
|
101
|
+
# @return [Object] Object with symbolized keys
|
|
102
|
+
def deep_symbolize_keys(obj)
|
|
103
|
+
case obj
|
|
104
|
+
when Hash
|
|
105
|
+
obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
|
|
106
|
+
when Array
|
|
107
|
+
obj.map { |v| deep_symbolize_keys(v) }
|
|
108
|
+
else
|
|
109
|
+
obj
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
public
|
|
114
|
+
|
|
115
|
+
# Access attribute by key (symbol or string).
|
|
116
|
+
# Provides hash-like access for compatibility.
|
|
117
|
+
#
|
|
118
|
+
# @param key [Symbol, String] The attribute name
|
|
119
|
+
# @return [Object] The attribute value
|
|
120
|
+
def [](key)
|
|
121
|
+
send(key.to_sym)
|
|
122
|
+
rescue NoMethodError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if a key/attribute exists.
|
|
127
|
+
#
|
|
128
|
+
# @param key [Symbol, String] The attribute name
|
|
129
|
+
# @return [Boolean] True if the attribute is defined
|
|
130
|
+
def key?(key)
|
|
131
|
+
respond_to?(key.to_sym)
|
|
132
|
+
end
|
|
133
|
+
alias_method :has_key?, :key?
|
|
134
|
+
|
|
135
|
+
# String representation for debugging.
|
|
136
|
+
#
|
|
137
|
+
# @return [String] Debug representation
|
|
138
|
+
def inspect
|
|
139
|
+
attrs = self.class.attributes.keys.map do |attr|
|
|
140
|
+
value = send(attr)
|
|
141
|
+
"#{attr}: #{value.inspect}"
|
|
142
|
+
end.join(", ")
|
|
143
|
+
"#<#{self.class.name} #{attrs}>"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Comparison by attribute values.
|
|
147
|
+
#
|
|
148
|
+
# @param other [Object] Object to compare
|
|
149
|
+
# @return [Boolean] True if equal
|
|
150
|
+
def ==(other)
|
|
151
|
+
return false unless other.is_a?(self.class)
|
|
152
|
+
|
|
153
|
+
self.class.attributes.keys.all? do |attr|
|
|
154
|
+
send(attr) == other.send(attr)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
alias_method :eql?, :==
|
|
158
|
+
|
|
159
|
+
# Hash code based on attributes.
|
|
160
|
+
#
|
|
161
|
+
# @return [Integer] Hash code
|
|
162
|
+
def hash
|
|
163
|
+
to_h.hash
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
protected
|
|
167
|
+
|
|
168
|
+
# Converts Unix timestamp (seconds) to Time.
|
|
169
|
+
#
|
|
170
|
+
# Use in helper methods for timestamp fields.
|
|
171
|
+
#
|
|
172
|
+
# @param ts [Integer, nil] Unix timestamp in seconds
|
|
173
|
+
# @return [Time, nil] Time object or nil
|
|
174
|
+
def timestamp_to_time(ts)
|
|
175
|
+
ts && ts != 0 && Time.at(ts)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Superthread board.
|
|
6
|
+
#
|
|
7
|
+
# Boards are containers for lists (columns) that organize cards in a
|
|
8
|
+
# Kanban-style workflow. Each board belongs to a team/workspace.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# board = client.boards.find(workspace_id, board_id)
|
|
12
|
+
# board.title # => "Sprint Backlog"
|
|
13
|
+
# board.lists # => [#<Superthread::Models::List ...>]
|
|
14
|
+
# board.lists.first.title # => "To Do"
|
|
15
|
+
# board.archived? # => false
|
|
16
|
+
class Board < Superthread::Model
|
|
17
|
+
include Concerns::Archivable
|
|
18
|
+
include Concerns::Presentable
|
|
19
|
+
include Concerns::Timestampable
|
|
20
|
+
|
|
21
|
+
detail_fields :id, :title, :time_created, :time_updated
|
|
22
|
+
list_columns :id, :title
|
|
23
|
+
|
|
24
|
+
# @!attribute [rw] id
|
|
25
|
+
# @return [String] unique board identifier
|
|
26
|
+
attribute :id, Shale::Type::String
|
|
27
|
+
|
|
28
|
+
# @!attribute [rw] team_id
|
|
29
|
+
# @return [String] ID of the team/workspace this board belongs to
|
|
30
|
+
attribute :team_id, Shale::Type::String
|
|
31
|
+
|
|
32
|
+
# @!attribute [rw] title
|
|
33
|
+
# @return [String] display title of the board
|
|
34
|
+
attribute :title, Shale::Type::String
|
|
35
|
+
|
|
36
|
+
# @!attribute [rw] user_id
|
|
37
|
+
# @return [String] ID of the user who created the board
|
|
38
|
+
attribute :user_id, Shale::Type::String
|
|
39
|
+
|
|
40
|
+
# @!attribute [rw] time_created
|
|
41
|
+
# @return [Integer] Unix timestamp when the board was created
|
|
42
|
+
attribute :time_created, Shale::Type::Integer
|
|
43
|
+
|
|
44
|
+
# @!attribute [rw] time_updated
|
|
45
|
+
# @return [Integer] Unix timestamp when the board was last updated
|
|
46
|
+
attribute :time_updated, Shale::Type::Integer
|
|
47
|
+
|
|
48
|
+
# @!attribute [rw] lists
|
|
49
|
+
# @return [Array<List>] columns/lists contained in this board
|
|
50
|
+
attribute :lists, List, collection: true
|
|
51
|
+
|
|
52
|
+
# @!attribute [rw] archived
|
|
53
|
+
# @return [Hash{String => Object}, nil] archive metadata with user_id and time_archived, or nil if not archived
|
|
54
|
+
attribute :archived, Shale::Type::Value
|
|
55
|
+
|
|
56
|
+
timestamps :time_created, :time_updated
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Superthread card (task/issue).
|
|
6
|
+
#
|
|
7
|
+
# Cards are the primary work items in Superthread, containing title,
|
|
8
|
+
# description, assignees, checklists, and other metadata. They live
|
|
9
|
+
# on boards within lists and can be linked to sprints and projects.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# card = client.cards.find(workspace_id, card_id)
|
|
13
|
+
# card.title # => "Implement feature X"
|
|
14
|
+
# card.status # => "started"
|
|
15
|
+
# card.priority # => 4
|
|
16
|
+
# card.priority_name # => "urgent"
|
|
17
|
+
# card.members # => [#<Superthread::Models::Member ...>]
|
|
18
|
+
# card.archived? # => false
|
|
19
|
+
# card.created_at # => 2024-01-15 10:30:00 -0800
|
|
20
|
+
class Card < Superthread::Model
|
|
21
|
+
include Concerns::Archivable
|
|
22
|
+
include Concerns::Presentable
|
|
23
|
+
include Concerns::Timestampable
|
|
24
|
+
|
|
25
|
+
detail_fields :id, :title, :status, :priority, :list_title, :board_title, :time_created, :time_updated
|
|
26
|
+
list_columns :id, :title, :status, :priority, :list_title
|
|
27
|
+
|
|
28
|
+
# @!attribute [rw] id
|
|
29
|
+
# @return [String] unique card identifier
|
|
30
|
+
attribute :id, Shale::Type::String
|
|
31
|
+
|
|
32
|
+
# @!attribute [rw] type
|
|
33
|
+
# @return [String] card type (e.g., "card", "epic")
|
|
34
|
+
attribute :type, Shale::Type::String
|
|
35
|
+
|
|
36
|
+
# @!attribute [rw] team_id
|
|
37
|
+
# @return [String] ID of the team/workspace this card belongs to
|
|
38
|
+
attribute :team_id, Shale::Type::String
|
|
39
|
+
|
|
40
|
+
# @!attribute [rw] project_id
|
|
41
|
+
# @return [String] ID of the project this card belongs to
|
|
42
|
+
attribute :project_id, Shale::Type::String
|
|
43
|
+
|
|
44
|
+
# @!attribute [rw] title
|
|
45
|
+
# @return [String] display title of the card
|
|
46
|
+
attribute :title, Shale::Type::String
|
|
47
|
+
|
|
48
|
+
# @!attribute [rw] content
|
|
49
|
+
# @return [String] HTML content/description of the card
|
|
50
|
+
attribute :content, Shale::Type::String
|
|
51
|
+
|
|
52
|
+
# @!attribute [rw] schema
|
|
53
|
+
# @return [Object] JSON schema for custom fields, can be complex
|
|
54
|
+
attribute :schema, Shale::Type::Value
|
|
55
|
+
|
|
56
|
+
# @!attribute [rw] status
|
|
57
|
+
# @return [String] workflow status (e.g., "open", "started", "closed")
|
|
58
|
+
attribute :status, Shale::Type::String
|
|
59
|
+
|
|
60
|
+
# @!attribute [rw] priority
|
|
61
|
+
# @return [Integer] priority level (4=urgent, 3=high, 2=medium, 1=low)
|
|
62
|
+
attribute :priority, Shale::Type::Integer
|
|
63
|
+
|
|
64
|
+
# @!attribute [rw] estimate
|
|
65
|
+
# @return [Float] story point or time estimate
|
|
66
|
+
attribute :estimate, Shale::Type::Float
|
|
67
|
+
|
|
68
|
+
# @!attribute [rw] board_id
|
|
69
|
+
# @return [String] ID of the board this card is on
|
|
70
|
+
attribute :board_id, Shale::Type::String
|
|
71
|
+
|
|
72
|
+
# @!attribute [rw] board_title
|
|
73
|
+
# @return [String] title of the board this card is on
|
|
74
|
+
attribute :board_title, Shale::Type::String
|
|
75
|
+
|
|
76
|
+
# @!attribute [rw] list_id
|
|
77
|
+
# @return [String] ID of the list/column this card is in
|
|
78
|
+
attribute :list_id, Shale::Type::String
|
|
79
|
+
|
|
80
|
+
# @!attribute [rw] list_title
|
|
81
|
+
# @return [String] title of the list/column this card is in
|
|
82
|
+
attribute :list_title, Shale::Type::String
|
|
83
|
+
|
|
84
|
+
# @!attribute [rw] list_color
|
|
85
|
+
# @return [String] color of the list/column this card is in
|
|
86
|
+
attribute :list_color, Shale::Type::String
|
|
87
|
+
|
|
88
|
+
# @!attribute [rw] sprint_id
|
|
89
|
+
# @return [String] ID of the sprint this card is assigned to
|
|
90
|
+
attribute :sprint_id, Shale::Type::String
|
|
91
|
+
|
|
92
|
+
# @!attribute [rw] owner_id
|
|
93
|
+
# @return [String] ID of the card owner
|
|
94
|
+
attribute :owner_id, Shale::Type::String
|
|
95
|
+
|
|
96
|
+
# @!attribute [rw] user_id
|
|
97
|
+
# @return [String] ID of the user who created the card
|
|
98
|
+
attribute :user_id, Shale::Type::String
|
|
99
|
+
|
|
100
|
+
# @!attribute [rw] user_id_updated
|
|
101
|
+
# @return [String] ID of the user who last updated the card
|
|
102
|
+
attribute :user_id_updated, Shale::Type::String
|
|
103
|
+
|
|
104
|
+
# @!attribute [rw] start_date
|
|
105
|
+
# @return [Integer] Unix timestamp when work started
|
|
106
|
+
attribute :start_date, Shale::Type::Integer
|
|
107
|
+
|
|
108
|
+
# @!attribute [rw] due_date
|
|
109
|
+
# @return [Integer] Unix timestamp when the card is due
|
|
110
|
+
attribute :due_date, Shale::Type::Integer
|
|
111
|
+
|
|
112
|
+
# @!attribute [rw] completed_date
|
|
113
|
+
# @return [Integer] Unix timestamp when the card was completed
|
|
114
|
+
attribute :completed_date, Shale::Type::Integer
|
|
115
|
+
|
|
116
|
+
# @!attribute [rw] time_created
|
|
117
|
+
# @return [Integer] Unix timestamp when the card was created
|
|
118
|
+
attribute :time_created, Shale::Type::Integer
|
|
119
|
+
|
|
120
|
+
# @!attribute [rw] time_updated
|
|
121
|
+
# @return [Integer] Unix timestamp when the card was last updated
|
|
122
|
+
attribute :time_updated, Shale::Type::Integer
|
|
123
|
+
|
|
124
|
+
# @!attribute [rw] total_comments
|
|
125
|
+
# @return [Integer] number of comments on this card
|
|
126
|
+
attribute :total_comments, Shale::Type::Integer
|
|
127
|
+
|
|
128
|
+
# @!attribute [rw] total_files
|
|
129
|
+
# @return [Integer] number of files attached to this card
|
|
130
|
+
attribute :total_files, Shale::Type::Integer
|
|
131
|
+
|
|
132
|
+
# @!attribute [rw] is_watching
|
|
133
|
+
# @return [Boolean] whether the current user is watching this card
|
|
134
|
+
attribute :is_watching, Shale::Type::Boolean
|
|
135
|
+
|
|
136
|
+
# @!attribute [rw] is_bookmarked
|
|
137
|
+
# @return [Boolean] whether the current user has bookmarked this card
|
|
138
|
+
attribute :is_bookmarked, Shale::Type::Boolean
|
|
139
|
+
|
|
140
|
+
# @!attribute [rw] archived_list
|
|
141
|
+
# @return [Boolean] whether the card's list is archived
|
|
142
|
+
attribute :archived_list, Shale::Type::Boolean
|
|
143
|
+
|
|
144
|
+
# @!attribute [rw] archived_board
|
|
145
|
+
# @return [Boolean] whether the card's board is archived
|
|
146
|
+
attribute :archived_board, Shale::Type::Boolean
|
|
147
|
+
|
|
148
|
+
# @!attribute [rw] members
|
|
149
|
+
# @return [Array<Member>] users assigned to this card
|
|
150
|
+
attribute :members, Member, collection: true
|
|
151
|
+
|
|
152
|
+
# @!attribute [rw] tags
|
|
153
|
+
# @return [Array<Tag>] labels/tags applied to this card
|
|
154
|
+
attribute :tags, Tag, collection: true
|
|
155
|
+
|
|
156
|
+
# @!attribute [rw] checklists
|
|
157
|
+
# @return [Array<Checklist>] checklists attached to this card
|
|
158
|
+
attribute :checklists, Checklist, collection: true
|
|
159
|
+
|
|
160
|
+
# @!attribute [rw] archived
|
|
161
|
+
# @return [Hash{String => Object}, nil] archive metadata with user_id and time_archived
|
|
162
|
+
attribute :archived, Shale::Type::Value
|
|
163
|
+
|
|
164
|
+
# @!attribute [rw] parent_card
|
|
165
|
+
# @return [Hash{String => Object}, nil] raw parent card data, use {#parent} for CardRef
|
|
166
|
+
attribute :parent_card, Shale::Type::Value
|
|
167
|
+
|
|
168
|
+
# @!attribute [rw] child_cards
|
|
169
|
+
# @return [Array<Hash>, nil] raw child card data, use {#children} for CardRef array
|
|
170
|
+
attribute :child_cards, Shale::Type::Value
|
|
171
|
+
|
|
172
|
+
# @!attribute [rw] linked_cards
|
|
173
|
+
# @return [Array<Hash>, nil] raw linked card data, use {#links} for LinkedCardRef array
|
|
174
|
+
attribute :linked_cards, Shale::Type::Value
|
|
175
|
+
|
|
176
|
+
# @!attribute [rw] epic
|
|
177
|
+
# @return [Hash{String => Object}, nil] epic this card belongs to
|
|
178
|
+
attribute :epic, Shale::Type::Value
|
|
179
|
+
|
|
180
|
+
timestamps :time_created, :time_updated
|
|
181
|
+
timestamps start_date: :start_time, due_date: :due_time, completed_date: :completed_time
|
|
182
|
+
|
|
183
|
+
# Check if the card is being watched.
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] True if watching
|
|
186
|
+
def watching?
|
|
187
|
+
!!is_watching
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Check if the card is bookmarked.
|
|
191
|
+
#
|
|
192
|
+
# @return [Boolean] True if bookmarked
|
|
193
|
+
def bookmarked?
|
|
194
|
+
!!is_bookmarked
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Human-readable priority name.
|
|
198
|
+
#
|
|
199
|
+
# @return [String, nil] priority label (urgent, high, medium, low)
|
|
200
|
+
def priority_name
|
|
201
|
+
Cli::Formatter::PRIORITY_LABELS[priority]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Returns the parent card as a CardRef, or nil if none.
|
|
205
|
+
#
|
|
206
|
+
# @return [CardRef, nil] lightweight reference to the parent card
|
|
207
|
+
def parent
|
|
208
|
+
return nil if parent_card.nil? || parent_card.empty?
|
|
209
|
+
CardRef.new(parent_card)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Returns child cards as an array of CardRef objects.
|
|
213
|
+
#
|
|
214
|
+
# @return [Array<CardRef>] lightweight references to child cards
|
|
215
|
+
def children
|
|
216
|
+
return [] if child_cards.nil? || !child_cards.is_a?(Array)
|
|
217
|
+
child_cards.map { |c| CardRef.new(c) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns linked cards as an array of LinkedCardRef objects.
|
|
221
|
+
#
|
|
222
|
+
# @return [Array<LinkedCardRef>] lightweight references to linked cards with relationship types
|
|
223
|
+
def links
|
|
224
|
+
return [] if linked_cards.nil? || !linked_cards.is_a?(Array)
|
|
225
|
+
linked_cards.map { |c| LinkedCardRef.new(c) }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Returns linked cards grouped by relationship type.
|
|
229
|
+
#
|
|
230
|
+
# @return [Hash{String => Array<LinkedCardRef>}] links organized by relationship type
|
|
231
|
+
def links_by_type
|
|
232
|
+
links.group_by(&:relationship)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Represents a linked card with relationship type.
|
|
237
|
+
#
|
|
238
|
+
# Extends Card with linked_card_type attribute to indicate how this
|
|
239
|
+
# card relates to another (blocks, blocked_by, related, duplicates).
|
|
240
|
+
#
|
|
241
|
+
# @example
|
|
242
|
+
# linked = card.linked_cards.first
|
|
243
|
+
# linked.relationship # => "blocks"
|
|
244
|
+
# linked.title # => "Other Card"
|
|
245
|
+
class LinkedCard < Card
|
|
246
|
+
# @!attribute [rw] linked_card_type
|
|
247
|
+
# @return [String] relationship type (blocks, blocked_by, related, duplicates)
|
|
248
|
+
attribute :linked_card_type, Shale::Type::String
|
|
249
|
+
|
|
250
|
+
# Returns the relationship type between this card and the linked card.
|
|
251
|
+
#
|
|
252
|
+
# @return [String] relationship type (blocks, blocked_by, related, duplicates)
|
|
253
|
+
def relationship
|
|
254
|
+
linked_card_type
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Lightweight reference to a card (id + title only).
|
|
259
|
+
#
|
|
260
|
+
# Used for parent/child relationships to avoid circular model references.
|
|
261
|
+
# Provides just enough information to identify and display a card.
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# ref = card.parent
|
|
265
|
+
# ref.id # => "abc123"
|
|
266
|
+
# ref.title # => "Parent Task"
|
|
267
|
+
# ref.to_s # => "Parent Task (abc123)"
|
|
268
|
+
class CardRef
|
|
269
|
+
# @return [String] unique card identifier
|
|
270
|
+
attr_reader :id
|
|
271
|
+
|
|
272
|
+
# @return [String] display title of the card
|
|
273
|
+
attr_reader :title
|
|
274
|
+
|
|
275
|
+
# Creates a new card reference from API response data.
|
|
276
|
+
#
|
|
277
|
+
# @param data [Hash{String => Object}, Hash{Symbol => Object}] raw card data from API
|
|
278
|
+
def initialize(data)
|
|
279
|
+
# API returns card_id for child/linked cards, id for parent
|
|
280
|
+
@id = data["card_id"] || data[:card_id] || data["id"] || data[:id]
|
|
281
|
+
@title = data["title"] || data[:title]
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Returns a human-readable string representation.
|
|
285
|
+
#
|
|
286
|
+
# @return [String] title with ID in parentheses
|
|
287
|
+
def to_s
|
|
288
|
+
"#{title} (#{id})"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Lightweight reference to a linked card with relationship type.
|
|
293
|
+
#
|
|
294
|
+
# Extends CardRef to include the relationship type indicating how
|
|
295
|
+
# this card relates to another card.
|
|
296
|
+
#
|
|
297
|
+
# @example
|
|
298
|
+
# ref = card.links.first
|
|
299
|
+
# ref.relationship # => "blocks"
|
|
300
|
+
# ref.to_s # => "Task Name (abc123)"
|
|
301
|
+
class LinkedCardRef < CardRef
|
|
302
|
+
# @return [String] relationship type (blocks, blocked_by, related, duplicates)
|
|
303
|
+
attr_reader :relationship
|
|
304
|
+
|
|
305
|
+
# Creates a new linked card reference from API response data.
|
|
306
|
+
#
|
|
307
|
+
# @param data [Hash{String => Object}, Hash{Symbol => Object}] raw linked card data from API
|
|
308
|
+
def initialize(data)
|
|
309
|
+
super
|
|
310
|
+
@relationship = data["linked_card_type"] || data[:linked_card_type]
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Returns a human-readable string representation.
|
|
314
|
+
#
|
|
315
|
+
# @return [String] title with ID in parentheses
|
|
316
|
+
def to_s
|
|
317
|
+
"#{title} (#{id})"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a checklist on a card.
|
|
6
|
+
#
|
|
7
|
+
# Checklists contain a collection of items that can be checked off.
|
|
8
|
+
# They track progress and provide completion statistics.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# checklist = card.checklists.first
|
|
12
|
+
# checklist.title # => "Requirements"
|
|
13
|
+
# checklist.items.count # => 3
|
|
14
|
+
# checklist.items.first.title # => "Write specs"
|
|
15
|
+
# checklist.items.first.checked? # => true
|
|
16
|
+
# checklist.progress # => 66.7
|
|
17
|
+
class Checklist < Superthread::Model
|
|
18
|
+
include Concerns::Presentable
|
|
19
|
+
include Concerns::Timestampable
|
|
20
|
+
|
|
21
|
+
presents_as(:title) { "#{title} (#{completed_count}/#{total_count})" }
|
|
22
|
+
|
|
23
|
+
detail_fields :id, :title, :card_id, :time_created
|
|
24
|
+
list_columns :id, :title
|
|
25
|
+
|
|
26
|
+
# @!attribute [rw] id
|
|
27
|
+
# @return [String] unique checklist identifier
|
|
28
|
+
attribute :id, Shale::Type::String
|
|
29
|
+
|
|
30
|
+
# @!attribute [rw] title
|
|
31
|
+
# @return [String] display title of the checklist
|
|
32
|
+
attribute :title, Shale::Type::String
|
|
33
|
+
|
|
34
|
+
# @!attribute [rw] content
|
|
35
|
+
# @return [String] optional description or notes
|
|
36
|
+
attribute :content, Shale::Type::String
|
|
37
|
+
|
|
38
|
+
# @!attribute [rw] card_id
|
|
39
|
+
# @return [String] ID of the card this checklist belongs to
|
|
40
|
+
attribute :card_id, Shale::Type::String
|
|
41
|
+
|
|
42
|
+
# @!attribute [rw] user_id
|
|
43
|
+
# @return [String] ID of the user who created the checklist
|
|
44
|
+
attribute :user_id, Shale::Type::String
|
|
45
|
+
|
|
46
|
+
# @!attribute [rw] time_created
|
|
47
|
+
# @return [Integer] Unix timestamp when the checklist was created
|
|
48
|
+
attribute :time_created, Shale::Type::Integer
|
|
49
|
+
|
|
50
|
+
# @!attribute [rw] time_updated
|
|
51
|
+
# @return [Integer] Unix timestamp when the checklist was last updated
|
|
52
|
+
attribute :time_updated, Shale::Type::Integer
|
|
53
|
+
|
|
54
|
+
# @!attribute [rw] items
|
|
55
|
+
# @return [Array<ChecklistItem>] items in this checklist
|
|
56
|
+
attribute :items, ChecklistItem, collection: true
|
|
57
|
+
|
|
58
|
+
timestamps :time_created, :time_updated
|
|
59
|
+
|
|
60
|
+
# Count of completed items.
|
|
61
|
+
#
|
|
62
|
+
# @return [Integer] Number of checked items
|
|
63
|
+
def completed_count
|
|
64
|
+
(items || []).count(&:checked?)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Total number of items.
|
|
68
|
+
#
|
|
69
|
+
# @return [Integer] Total items
|
|
70
|
+
def total_count
|
|
71
|
+
(items || []).count
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Progress as a percentage.
|
|
75
|
+
#
|
|
76
|
+
# @return [Float] Percentage complete (0.0 - 100.0)
|
|
77
|
+
def progress
|
|
78
|
+
return 0.0 if total_count.zero?
|
|
79
|
+
|
|
80
|
+
(completed_count.to_f / total_count * 100).round(1)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if all items are complete.
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] True if all items checked
|
|
86
|
+
def complete?
|
|
87
|
+
total_count.positive? && completed_count == total_count
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|