superthread 0.8.0 → 0.9.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/lib/superthread/cli/cards.rb +47 -29
- data/lib/superthread/cli/checklists.rb +2 -2
- data/lib/superthread/cli/pages.rb +25 -4
- data/lib/superthread/mention_formatter.rb +33 -0
- data/lib/superthread/resources/base.rb +14 -0
- data/lib/superthread/resources/cards.rb +18 -1
- data/lib/superthread/resources/pages.rb +16 -1
- data/lib/superthread/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e15ca652fe609604e401611f3359af9d094872c537a8fb817048631cf5a0cf1
|
|
4
|
+
data.tar.gz: a55736ee16a8ae5b232e0a8c72b3acc42dad25a1bef131b33e50a6dd6f3df950
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e215412fbe73aaa60fb213fa028d78a3eb933f04023fe9889bc7cf20ad9fd969e5b88091929224883b49d06a20ff3db9581a34b7a7c8831e923d7ede8a72a08a
|
|
7
|
+
data.tar.gz: 1f322daa33b43c271173051cbac4b4d414eea4bfc52c90d6216baa74ba569596f8e812a078b4be7226a6e93d8e0ab0281a8f12d368ba4cde05332c79242393d8
|
|
@@ -173,7 +173,7 @@ module Superthread
|
|
|
173
173
|
option :board, type: :string, aliases: "-b", desc: "Board (ID or name, required unless --sprint)"
|
|
174
174
|
option :space, type: :string, aliases: "-s", desc: "Space (required for --sprint, helps resolve board name)"
|
|
175
175
|
option :sprint, type: :string, desc: "Sprint (ID or name, required unless --board)"
|
|
176
|
-
option :content, type: :string, desc: "Card content (HTML)"
|
|
176
|
+
option :content, type: :string, desc: "Card content (HTML). Use {{@Name}} to mention users"
|
|
177
177
|
option :project, type: :string, desc: "Project ID"
|
|
178
178
|
option :start_date, type: :numeric, desc: "Start date (Unix timestamp)"
|
|
179
179
|
option :due_date, type: :numeric, desc: "Due date (Unix timestamp)"
|
|
@@ -205,6 +205,7 @@ module Superthread
|
|
|
205
205
|
|
|
206
206
|
desc "update CARD", "Update a card"
|
|
207
207
|
option :title, type: :string, desc: "New title"
|
|
208
|
+
option :content, type: :string, desc: "Card content (HTML). Use {{@Name}} to mention users"
|
|
208
209
|
option :list, type: :string, aliases: "-l", desc: "Destination list (ID or name)"
|
|
209
210
|
option :board, type: :string, aliases: "-b", desc: "Board (helps resolve list name)"
|
|
210
211
|
option :sprint, type: :string, desc: "Sprint to move card to (ID or name)"
|
|
@@ -215,6 +216,9 @@ module Superthread
|
|
|
215
216
|
option :archived, type: :boolean, desc: "Archive/unarchive"
|
|
216
217
|
# Update an existing card's properties.
|
|
217
218
|
#
|
|
219
|
+
# Content is updated via a separate PUT endpoint since the standard
|
|
220
|
+
# PATCH endpoint does not support content changes.
|
|
221
|
+
#
|
|
218
222
|
# @param card_id [String] the unique identifier of the card to update
|
|
219
223
|
# @return [void]
|
|
220
224
|
def update(card_id)
|
|
@@ -222,36 +226,50 @@ module Superthread
|
|
|
222
226
|
require_space_for_sprint!
|
|
223
227
|
|
|
224
228
|
begin
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
# First update non-move fields
|
|
230
|
-
field_opts = symbolized_options(:title, :priority, :archived)
|
|
231
|
-
field_opts[:epic_id] = options[:epic] if options[:epic]
|
|
232
|
-
client.cards.update(workspace_id, card_id, **field_opts) unless field_opts.empty?
|
|
233
|
-
|
|
234
|
-
# Then move the card (include sprint context)
|
|
235
|
-
move_opts = resolve_list_with_context(options[:list], card_id)
|
|
236
|
-
move_opts[:position] = options[:position] if options[:position]
|
|
237
|
-
card = client.cards.update(workspace_id, card_id, **move_opts)
|
|
238
|
-
else
|
|
239
|
-
opts = symbolized_options(:title, :priority, :archived)
|
|
240
|
-
opts[:epic_id] = options[:epic] if options[:epic]
|
|
241
|
-
opts[:position] = options[:position] if options[:position]
|
|
242
|
-
|
|
243
|
-
if options[:list]
|
|
244
|
-
opts.merge!(resolve_list_with_context(options[:list], card_id))
|
|
245
|
-
elsif options[:sprint]
|
|
246
|
-
# Moving to a sprint without --list — default to first list
|
|
247
|
-
opts[:sprint_id] = sprint_id
|
|
248
|
-
opts[:project_id] = space_id
|
|
249
|
-
sprint_obj = client.sprints.find(workspace_id, sprint_id, space_id: space_id)
|
|
250
|
-
opts[:list_id] = sprint_obj.lists.first&.id
|
|
251
|
-
end
|
|
229
|
+
# Update content via dedicated PUT endpoint (separate from PATCH)
|
|
230
|
+
if options[:content]
|
|
231
|
+
client.cards.update_content(workspace_id, card_id, content: options[:content])
|
|
232
|
+
end
|
|
252
233
|
|
|
253
|
-
|
|
234
|
+
# Update other fields via PATCH (skip if only content was provided)
|
|
235
|
+
has_patch_fields = options[:title] || options[:list] || options[:priority] ||
|
|
236
|
+
!options[:archived].nil? || options[:epic] || options[:position] || options[:sprint]
|
|
237
|
+
|
|
238
|
+
if has_patch_fields
|
|
239
|
+
# WORKAROUND: API ignores title when combined with list_id,
|
|
240
|
+
# so we make separate requests when both are provided.
|
|
241
|
+
# TODO: Remove when API is fixed (https://superthread.com/api/known-issues)
|
|
242
|
+
if options[:list] && (options[:title] || options[:priority] || !options[:archived].nil? || options[:epic])
|
|
243
|
+
# First update non-move fields
|
|
244
|
+
field_opts = symbolized_options(:title, :priority, :archived)
|
|
245
|
+
field_opts[:epic_id] = options[:epic] if options[:epic]
|
|
246
|
+
client.cards.update(workspace_id, card_id, **field_opts) unless field_opts.empty?
|
|
247
|
+
|
|
248
|
+
# Then move the card (include sprint context)
|
|
249
|
+
move_opts = resolve_list_with_context(options[:list], card_id)
|
|
250
|
+
move_opts[:position] = options[:position] if options[:position]
|
|
251
|
+
card = client.cards.update(workspace_id, card_id, **move_opts)
|
|
252
|
+
else
|
|
253
|
+
opts = symbolized_options(:title, :priority, :archived)
|
|
254
|
+
opts[:epic_id] = options[:epic] if options[:epic]
|
|
255
|
+
opts[:position] = options[:position] if options[:position]
|
|
256
|
+
|
|
257
|
+
if options[:list]
|
|
258
|
+
opts.merge!(resolve_list_with_context(options[:list], card_id))
|
|
259
|
+
elsif options[:sprint]
|
|
260
|
+
# Moving to a sprint without --list — default to first list
|
|
261
|
+
opts[:sprint_id] = sprint_id
|
|
262
|
+
opts[:project_id] = space_id
|
|
263
|
+
sprint_obj = client.sprints.find(workspace_id, sprint_id, space_id: space_id)
|
|
264
|
+
opts[:list_id] = sprint_obj.lists.first&.id
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
card = client.cards.update(workspace_id, card_id, **opts)
|
|
268
|
+
end
|
|
254
269
|
end
|
|
270
|
+
|
|
271
|
+
# If only content was updated, fetch the card for output
|
|
272
|
+
card ||= client.cards.find(workspace_id, card_id)
|
|
255
273
|
rescue Superthread::ForbiddenError, Superthread::NotFoundError
|
|
256
274
|
raise Thor::Error, "Card not found: '#{card_id}'. Use 'suth cards list -b BOARD' to see available cards."
|
|
257
275
|
end
|
|
@@ -116,7 +116,7 @@ module Superthread
|
|
|
116
116
|
|
|
117
117
|
desc "add-item CHECKLIST", "Add item to a checklist"
|
|
118
118
|
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
119
|
-
option :title, type: :string, required: true, desc: "Item title"
|
|
119
|
+
option :title, type: :string, required: true, desc: "Item title. Use {{@Name}} to mention users"
|
|
120
120
|
option :checked, type: :boolean, default: false, desc: "Create as checked"
|
|
121
121
|
# Add a new item to an existing checklist.
|
|
122
122
|
#
|
|
@@ -139,7 +139,7 @@ module Superthread
|
|
|
139
139
|
desc "update-item ITEM_ID", "Update a checklist item"
|
|
140
140
|
option :card, type: :string, required: true, aliases: "-c", desc: "Parent card ID"
|
|
141
141
|
option :checklist, type: :string, required: true, desc: "Parent checklist ID"
|
|
142
|
-
option :title, type: :string, desc: "New item title"
|
|
142
|
+
option :title, type: :string, desc: "New item title. Use {{@Name}} to mention users"
|
|
143
143
|
option :checked, type: :boolean, desc: "Mark as checked/unchecked"
|
|
144
144
|
# Update the title or checked state of a checklist item.
|
|
145
145
|
#
|
|
@@ -55,19 +55,40 @@ module Superthread
|
|
|
55
55
|
|
|
56
56
|
desc "update PAGE", "Update a page"
|
|
57
57
|
option :title, type: :string, desc: "New title"
|
|
58
|
+
option :content, type: :string, desc: "Page content (HTML)"
|
|
58
59
|
option :is_public, type: :boolean, desc: "Public visibility"
|
|
59
60
|
option :parent_page, type: :string, desc: "Parent page ID"
|
|
60
61
|
option :archived, type: :boolean, desc: "Archive/unarchive"
|
|
61
62
|
# Updates an existing page's properties.
|
|
62
63
|
#
|
|
64
|
+
# Content is updated via a separate PUT endpoint since the standard
|
|
65
|
+
# PATCH endpoint does not support content changes.
|
|
66
|
+
#
|
|
63
67
|
# @param page_id [String] numeric ID or URL slug of the page to retrieve
|
|
64
68
|
# @return [void]
|
|
65
69
|
def update(page_id)
|
|
66
70
|
handle_error do
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
# Update content via dedicated PUT endpoint (separate from PATCH)
|
|
72
|
+
if options[:content]
|
|
73
|
+
with_not_found("Page not found: '#{page_id}'. Use 'suth pages list' to see available pages.") do
|
|
74
|
+
client.pages.update_content(workspace_id, page_id, content: options[:content])
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Update other fields via PATCH (skip if only content was provided)
|
|
79
|
+
has_patch_fields = options[:title] || !options[:is_public].nil? || !options[:archived].nil? || options[:parent_page]
|
|
80
|
+
|
|
81
|
+
if has_patch_fields
|
|
82
|
+
opts = symbolized_options(:title, :is_public, :archived)
|
|
83
|
+
opts[:parent_page_id] = options[:parent_page] if options[:parent_page]
|
|
84
|
+
page = with_not_found("Page not found: '#{page_id}'. Use 'suth pages list' to see available pages.") do
|
|
85
|
+
client.pages.update(workspace_id, page_id, **opts)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# If only content was updated, fetch the page for output
|
|
90
|
+
page ||= with_not_found("Page not found: '#{page_id}'. Use 'suth pages list' to see available pages.") do
|
|
91
|
+
client.pages.find(workspace_id, page_id)
|
|
71
92
|
end
|
|
72
93
|
output_item page, labels: {id: "Page ID"}
|
|
73
94
|
end
|
|
@@ -32,6 +32,8 @@ module Superthread
|
|
|
32
32
|
PLACEHOLDER_PATTERN = /___ESCAPED_MENTION_(.+?)___END___/
|
|
33
33
|
# @return [Regexp] pattern matching raw HTML mention tags that should use {{@Name}} syntax
|
|
34
34
|
HTML_MENTION_PATTERN = /<(?:user-mention|mention-user)\b/i
|
|
35
|
+
# @return [Regexp] pattern matching plain @Word that is not inside {{@...}} delimiters
|
|
36
|
+
PLAIN_MENTION_PATTERN = /(?<!\{\{)@(\w+)/
|
|
35
37
|
|
|
36
38
|
# @param client [Superthread::Client] the API client for fetching members
|
|
37
39
|
# @param workspace_id [String] the workspace to look up members in
|
|
@@ -46,6 +48,7 @@ module Superthread
|
|
|
46
48
|
# @return [String, nil] content with mentions converted to HTML tags
|
|
47
49
|
def format(content)
|
|
48
50
|
warn_html_mentions(content) if content
|
|
51
|
+
warn_plain_mentions(content) if content
|
|
49
52
|
return content if content.nil? || !content.include?("{{@")
|
|
50
53
|
|
|
51
54
|
member_map = build_member_map
|
|
@@ -99,6 +102,36 @@ module Superthread
|
|
|
99
102
|
"(e.g., '{{@Steve Clarke}} check this')."
|
|
100
103
|
end
|
|
101
104
|
|
|
105
|
+
# Warns when content contains plain @Name mentions that match workspace members.
|
|
106
|
+
# This catches the common mistake of using @Name instead of {{@Name}}.
|
|
107
|
+
#
|
|
108
|
+
# @param content [String] text to check for plain @mentions
|
|
109
|
+
# @return [void]
|
|
110
|
+
def warn_plain_mentions(content)
|
|
111
|
+
return if content.include?("{{@")
|
|
112
|
+
|
|
113
|
+
names = content.scan(PLAIN_MENTION_PATTERN).flatten
|
|
114
|
+
return if names.empty?
|
|
115
|
+
|
|
116
|
+
member_map = build_member_map
|
|
117
|
+
return if member_map.nil?
|
|
118
|
+
|
|
119
|
+
# Also index by first name for multi-word display names
|
|
120
|
+
first_name_map = {}
|
|
121
|
+
member_map.each_value do |info|
|
|
122
|
+
first = info[:name].split.first&.downcase
|
|
123
|
+
first_name_map[first] = info[:name] if first
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
names.each do |name|
|
|
127
|
+
match = member_map[name.downcase]&.fetch(:name) || first_name_map[name.downcase]
|
|
128
|
+
if match
|
|
129
|
+
warn "Warning: Found @#{name} — did you mean {{@#{match}}}? " \
|
|
130
|
+
"Use {{@Name}} syntax to mention users and trigger notifications."
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
102
135
|
# Fetches workspace members and builds a case-insensitive lookup map.
|
|
103
136
|
#
|
|
104
137
|
# @return [Hash{String => Hash}, nil] map of lowercase names to {id:, name:}, or nil on failure
|
|
@@ -95,6 +95,20 @@ module Superthread
|
|
|
95
95
|
)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# Performs a PUT request and returns a typed object.
|
|
99
|
+
#
|
|
100
|
+
# @param path [String] the API endpoint path
|
|
101
|
+
# @param body [Hash{Symbol => Object}, nil] optional request body
|
|
102
|
+
# @param object_class [Class, nil] the model class to instantiate
|
|
103
|
+
# @param unwrap_key [Symbol, nil] key to extract from response before wrapping
|
|
104
|
+
# @return [Superthread::Object] the response wrapped in an object
|
|
105
|
+
def put_object(path, body: nil, object_class: nil, unwrap_key: nil)
|
|
106
|
+
@client.request_object(
|
|
107
|
+
method: :put, path: path, body: body,
|
|
108
|
+
object_class: object_class, unwrap_key: unwrap_key
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
98
112
|
# Performs a DELETE request and returns a typed object.
|
|
99
113
|
#
|
|
100
114
|
# @param path [String] the API endpoint path
|
|
@@ -52,7 +52,7 @@ module Superthread
|
|
|
52
52
|
# @option params [String] :epic_id the new epic identifier
|
|
53
53
|
# @option params [Boolean] :archived whether the card is archived
|
|
54
54
|
# @return [Superthread::Models::Card] the updated card
|
|
55
|
-
# @note Content
|
|
55
|
+
# @note Content updates use a separate PUT endpoint; use {#update_content} instead.
|
|
56
56
|
# @note parent_card_id is not supported on update; the API silently ignores it.
|
|
57
57
|
def update(workspace_id, card_id, **params)
|
|
58
58
|
ws = safe_id("workspace_id", workspace_id)
|
|
@@ -61,6 +61,23 @@ module Superthread
|
|
|
61
61
|
object_class: Models::Card, unwrap_key: :card)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Updates a card's content (description) via the dedicated PUT endpoint.
|
|
65
|
+
#
|
|
66
|
+
# Card content cannot be updated through the standard PATCH endpoint
|
|
67
|
+
# because the web app uses WebSocket collaboration for real-time editing.
|
|
68
|
+
# This method uses the separate PUT endpoint for content updates.
|
|
69
|
+
#
|
|
70
|
+
# @param workspace_id [String] the workspace identifier
|
|
71
|
+
# @param card_id [String] the card identifier
|
|
72
|
+
# @param content [String] the new content as HTML
|
|
73
|
+
# @return [Superthread::Object] a response object with success: true
|
|
74
|
+
def update_content(workspace_id, card_id, content:)
|
|
75
|
+
ws = safe_id("workspace_id", workspace_id)
|
|
76
|
+
card = safe_id("card_id", card_id)
|
|
77
|
+
content = format_mentions(workspace_id, content)
|
|
78
|
+
put_object("/#{ws}/cards/#{card}/content", body: {content: content, is_html: true})
|
|
79
|
+
end
|
|
80
|
+
|
|
64
81
|
# Gets a specific card with full details.
|
|
65
82
|
#
|
|
66
83
|
# @param workspace_id [String] the workspace identifier
|
|
@@ -56,10 +56,10 @@ module Superthread
|
|
|
56
56
|
# @param page_id [String] the page identifier
|
|
57
57
|
# @param params [Hash{Symbol => Object}] the attributes to update
|
|
58
58
|
# @option params [String] :title the new page title
|
|
59
|
-
# @option params [String] :content the new page content as HTML
|
|
60
59
|
# @option params [Boolean] :archived whether the page is archived
|
|
61
60
|
# @option params [String] :parent_page_id the new parent page identifier
|
|
62
61
|
# @return [Superthread::Models::Page] the updated page
|
|
62
|
+
# @note Content updates use a separate PUT endpoint; use {#update_content} instead.
|
|
63
63
|
def update(workspace_id, page_id, **params)
|
|
64
64
|
ws = safe_id("workspace_id", workspace_id)
|
|
65
65
|
page = safe_id("page_id", page_id)
|
|
@@ -67,6 +67,21 @@ module Superthread
|
|
|
67
67
|
object_class: Models::Page, unwrap_key: :page)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Updates a page's content via the dedicated PUT endpoint.
|
|
71
|
+
#
|
|
72
|
+
# Page content cannot be updated through the standard PATCH endpoint.
|
|
73
|
+
# This method uses the separate PUT endpoint for content updates.
|
|
74
|
+
#
|
|
75
|
+
# @param workspace_id [String] the workspace identifier
|
|
76
|
+
# @param page_id [String] the page identifier
|
|
77
|
+
# @param content [String] the new content as HTML
|
|
78
|
+
# @return [Superthread::Object] a response object with success: true
|
|
79
|
+
def update_content(workspace_id, page_id, content:)
|
|
80
|
+
ws = safe_id("workspace_id", workspace_id)
|
|
81
|
+
page = safe_id("page_id", page_id)
|
|
82
|
+
put_object("/#{ws}/pages/#{page}/content", body: {content: content, is_html: true})
|
|
83
|
+
end
|
|
84
|
+
|
|
70
85
|
# Duplicates a page to a destination space.
|
|
71
86
|
#
|
|
72
87
|
# @param workspace_id [String] the workspace identifier
|
data/lib/superthread/version.rb
CHANGED