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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +492 -0
  5. data/exe/suth +19 -0
  6. data/lib/superthread/cli/accounts.rb +240 -0
  7. data/lib/superthread/cli/activity.rb +210 -0
  8. data/lib/superthread/cli/base.rb +355 -0
  9. data/lib/superthread/cli/boards.rb +131 -0
  10. data/lib/superthread/cli/cards.rb +530 -0
  11. data/lib/superthread/cli/checklists.rb +223 -0
  12. data/lib/superthread/cli/comments.rb +86 -0
  13. data/lib/superthread/cli/completion.rb +306 -0
  14. data/lib/superthread/cli/concerns/board_resolvable.rb +70 -0
  15. data/lib/superthread/cli/concerns/confirmable.rb +55 -0
  16. data/lib/superthread/cli/concerns/date_parsable.rb +196 -0
  17. data/lib/superthread/cli/concerns/list_resolvable.rb +53 -0
  18. data/lib/superthread/cli/concerns/space_resolvable.rb +52 -0
  19. data/lib/superthread/cli/concerns/sprint_resolvable.rb +55 -0
  20. data/lib/superthread/cli/concerns/tag_resolvable.rb +49 -0
  21. data/lib/superthread/cli/concerns/user_resolvable.rb +52 -0
  22. data/lib/superthread/cli/concerns/workspace_resolvable.rb +83 -0
  23. data/lib/superthread/cli/config.rb +129 -0
  24. data/lib/superthread/cli/formatter.rb +388 -0
  25. data/lib/superthread/cli/lists.rb +85 -0
  26. data/lib/superthread/cli/main.rb +121 -0
  27. data/lib/superthread/cli/members.rb +19 -0
  28. data/lib/superthread/cli/notes.rb +64 -0
  29. data/lib/superthread/cli/pages.rb +128 -0
  30. data/lib/superthread/cli/projects.rb +124 -0
  31. data/lib/superthread/cli/replies.rb +94 -0
  32. data/lib/superthread/cli/search.rb +34 -0
  33. data/lib/superthread/cli/setup.rb +253 -0
  34. data/lib/superthread/cli/spaces.rb +141 -0
  35. data/lib/superthread/cli/sprints.rb +32 -0
  36. data/lib/superthread/cli/tags.rb +86 -0
  37. data/lib/superthread/cli/ui/gum_prompt.rb +58 -0
  38. data/lib/superthread/cli/ui/plain_prompt.rb +73 -0
  39. data/lib/superthread/cli/ui.rb +263 -0
  40. data/lib/superthread/cli/workspaces.rb +105 -0
  41. data/lib/superthread/cli.rb +12 -0
  42. data/lib/superthread/client.rb +207 -0
  43. data/lib/superthread/configuration.rb +354 -0
  44. data/lib/superthread/connection.rb +57 -0
  45. data/lib/superthread/error.rb +164 -0
  46. data/lib/superthread/mention_formatter.rb +96 -0
  47. data/lib/superthread/model.rb +178 -0
  48. data/lib/superthread/models/board.rb +59 -0
  49. data/lib/superthread/models/card.rb +321 -0
  50. data/lib/superthread/models/checklist.rb +91 -0
  51. data/lib/superthread/models/checklist_item.rb +69 -0
  52. data/lib/superthread/models/comment.rb +71 -0
  53. data/lib/superthread/models/concerns/archivable.rb +32 -0
  54. data/lib/superthread/models/concerns/presentable.rb +113 -0
  55. data/lib/superthread/models/concerns/timestampable.rb +91 -0
  56. data/lib/superthread/models/list.rb +67 -0
  57. data/lib/superthread/models/member.rb +40 -0
  58. data/lib/superthread/models/note.rb +56 -0
  59. data/lib/superthread/models/page.rb +70 -0
  60. data/lib/superthread/models/project.rb +83 -0
  61. data/lib/superthread/models/space.rb +71 -0
  62. data/lib/superthread/models/sprint.rb +53 -0
  63. data/lib/superthread/models/tag.rb +52 -0
  64. data/lib/superthread/models/team.rb +68 -0
  65. data/lib/superthread/models/user.rb +76 -0
  66. data/lib/superthread/models.rb +12 -0
  67. data/lib/superthread/object.rb +285 -0
  68. data/lib/superthread/objects/collection.rb +179 -0
  69. data/lib/superthread/resources/base.rb +204 -0
  70. data/lib/superthread/resources/boards.rb +150 -0
  71. data/lib/superthread/resources/cards.rb +363 -0
  72. data/lib/superthread/resources/comments.rb +163 -0
  73. data/lib/superthread/resources/notes.rb +61 -0
  74. data/lib/superthread/resources/pages.rb +110 -0
  75. data/lib/superthread/resources/projects.rb +117 -0
  76. data/lib/superthread/resources/search.rb +46 -0
  77. data/lib/superthread/resources/spaces.rb +104 -0
  78. data/lib/superthread/resources/sprints.rb +37 -0
  79. data/lib/superthread/resources/tags.rb +52 -0
  80. data/lib/superthread/resources/users.rb +29 -0
  81. data/lib/superthread/version.rb +6 -0
  82. data/lib/superthread/version_checker.rb +174 -0
  83. data/lib/superthread.rb +30 -0
  84. 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