Skip to content

Commit 8299bb9

Browse files
committed
Address PR review feedback on attach updates
1 parent e8db9f8 commit 8299bb9

6 files changed

Lines changed: 124 additions & 4 deletions

File tree

internal/commands/card.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,22 @@ var cardUpdateCmd = &cobra.Command{
375375

376376
cardNumber := args[0]
377377

378+
hasDescriptionInput := cardUpdateDescription != "" || cardUpdateDescriptionFile != ""
378379
description, err := resolveRichTextContent(cardUpdateDescription, cardUpdateDescriptionFile)
379380
if err != nil {
380381
return err
381382
}
383+
if len(cardUpdateAttach) > 0 && !hasDescriptionInput {
384+
currentData, _, err := getSDK().Cards().Get(cmd.Context(), cardNumber)
385+
if err != nil {
386+
return convertSDKError(err)
387+
}
388+
if current, ok := normalizeAny(currentData).(map[string]any); ok {
389+
if currentDescription, ok := current["description_html"].(string); ok {
390+
description = currentDescription
391+
}
392+
}
393+
}
382394
description, err = appendInlineAttachmentsToContent(description, cardUpdateAttach)
383395
if err != nil {
384396
return err

internal/commands/card_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,43 @@ func TestCardUpdate(t *testing.T) {
630630
t.Errorf("expected description %q, got %v", expected, body["description"])
631631
}
632632
})
633+
634+
t.Run("preserves existing description when only attach is provided", func(t *testing.T) {
635+
tempDir := t.TempDir()
636+
attachPath := writeTestAttachmentFile(t, tempDir, "update.txt", "update")
637+
638+
mock := NewMockClient()
639+
mock.GetResponse = &client.APIResponse{
640+
StatusCode: 200,
641+
Data: map[string]any{
642+
"id": "abc",
643+
"description_html": "<p>Existing description</p>",
644+
},
645+
}
646+
mock.PatchResponse = &client.APIResponse{StatusCode: 200, Data: map[string]any{"id": "abc"}}
647+
mock.UploadFileResponse = &client.APIResponse{StatusCode: 200, Data: map[string]any{"attachable_sgid": "sgid-update"}}
648+
649+
SetTestModeWithSDK(mock)
650+
SetTestConfig("token", "account", "https://api.example.com")
651+
defer resetTest()
652+
653+
cardUpdateAttach = []string{attachPath}
654+
err := cardUpdateCmd.RunE(cardUpdateCmd, []string{"42"})
655+
cardUpdateAttach = nil
656+
657+
assertExitCode(t, err, 0)
658+
if len(mock.GetCalls) == 0 || mock.GetCalls[0].Path != "/cards/42" {
659+
t.Fatalf("expected existing card fetch before update, got %#v", mock.GetCalls)
660+
}
661+
body := mock.PatchCalls[0].Body.(map[string]any)
662+
expected := strings.Join([]string{
663+
"<p>Existing description</p>",
664+
`<action-text-attachment sgid="sgid-update"></action-text-attachment>`,
665+
}, "\n")
666+
if body["description"] != expected {
667+
t.Errorf("expected description %q, got %v", expected, body["description"])
668+
}
669+
})
633670
}
634671

635672
func TestCardDelete(t *testing.T) {

internal/commands/comment.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,32 @@ var commentUpdateCmd = &cobra.Command{
203203
return newRequiredFlagError("card")
204204
}
205205

206+
commentID := args[0]
207+
cardNumber := commentUpdateCard
208+
209+
hasBodyInput := commentUpdateBody != "" || commentUpdateBodyFile != ""
206210
body, err := resolveRichTextContent(commentUpdateBody, commentUpdateBodyFile)
207211
if err != nil {
208212
return err
209213
}
214+
if len(commentUpdateAttach) > 0 && !hasBodyInput {
215+
currentData, _, err := getSDK().Comments().Get(cmd.Context(), cardNumber, commentID)
216+
if err != nil {
217+
return convertSDKError(err)
218+
}
219+
if current, ok := normalizeAny(currentData).(map[string]any); ok {
220+
if currentBody, ok := current["body"].(map[string]any); ok {
221+
if currentHTML, ok := currentBody["html"].(string); ok {
222+
body = currentHTML
223+
}
224+
}
225+
}
226+
}
210227
body, err = appendInlineAttachmentsToContent(body, commentUpdateAttach)
211228
if err != nil {
212229
return err
213230
}
214231

215-
commentID := args[0]
216-
cardNumber := commentUpdateCard
217-
218232
req := &generated.UpdateCommentRequest{}
219233
if body != "" {
220234
req.Body = body

internal/commands/comment_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,48 @@ func TestCommentUpdate(t *testing.T) {
374374
}
375375
})
376376

377+
t.Run("preserves existing body when only attach is provided", func(t *testing.T) {
378+
tempDir := t.TempDir()
379+
attachPath := writeTestAttachmentFile(t, tempDir, "update.txt", "update")
380+
381+
mock := NewMockClient()
382+
mock.GetResponse = &client.APIResponse{
383+
StatusCode: 200,
384+
Data: map[string]any{
385+
"id": "comment-1",
386+
"body": map[string]any{
387+
"html": "<p>Existing comment</p>",
388+
"plain_text": "Existing comment",
389+
},
390+
},
391+
}
392+
mock.PatchResponse = &client.APIResponse{StatusCode: 200, Data: map[string]any{"id": "comment-1"}}
393+
mock.UploadFileResponse = &client.APIResponse{StatusCode: 200, Data: map[string]any{"attachable_sgid": "sgid-update"}}
394+
395+
SetTestModeWithSDK(mock)
396+
SetTestConfig("token", "account", "https://api.example.com")
397+
defer resetTest()
398+
399+
commentUpdateCard = "42"
400+
commentUpdateAttach = []string{attachPath}
401+
err := commentUpdateCmd.RunE(commentUpdateCmd, []string{"comment-1"})
402+
commentUpdateCard = ""
403+
commentUpdateAttach = nil
404+
405+
assertExitCode(t, err, 0)
406+
if len(mock.GetCalls) == 0 || mock.GetCalls[0].Path != "/cards/42/comments/comment-1" {
407+
t.Fatalf("expected existing comment fetch before update, got %#v", mock.GetCalls)
408+
}
409+
body := mock.PatchCalls[0].Body.(map[string]any)
410+
expected := strings.Join([]string{
411+
"<p>Existing comment</p>",
412+
`<action-text-attachment sgid="sgid-update"></action-text-attachment>`,
413+
}, "\n")
414+
if body["body"] != expected {
415+
t.Errorf("expected body %q, got %v", expected, body["body"])
416+
}
417+
})
418+
377419
t.Run("requires card flag", func(t *testing.T) {
378420
mock := NewMockClient()
379421
SetTestModeWithSDK(mock)

internal/commands/markdown.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ var md = goldmark.New(
1818
// backtickAttachmentRegex matches backtick-wrapped action-text-attachment tags
1919
// that goldmark didn't convert (because they were inside HTML blocks).
2020
var backtickAttachmentRegex = regexp.MustCompile("`(<action-text-attachment[^>]*>)(</action-text-attachment>)`")
21+
var blockMarkdownRegex = regexp.MustCompile("(?m)^(#{1,6}\\s|[*+-]\\s|\\d+\\.\\s|>\\s|```|~~~)")
22+
var inlineMarkdownRegex = regexp.MustCompile(`(\*\*[^*]+\*\*|__[^_]+__|~~[^~]+~~|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|(^|[[:space:][:punct:]])[*_][^*_\n]+[*_]([[:space:][:punct:]]|$))`)
2123

2224
// containsMarkdownOrHTML checks whether content has HTML tags or markdown
2325
// syntax that would benefit from conversion.
2426
func containsMarkdownOrHTML(content string) bool {
25-
return strings.ContainsAny(content, "<>`")
27+
if strings.ContainsAny(content, "<>`") {
28+
return true
29+
}
30+
return blockMarkdownRegex.MatchString(content) || inlineMarkdownRegex.MatchString(content)
2631
}
2732

2833
// markdownToHTML converts markdown content to HTML. Raw HTML in the input is

internal/commands/markdown_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ func TestMarkdownToHTML(t *testing.T) {
1818
input: "Just a simple comment",
1919
exactOutput: "Just a simple comment",
2020
},
21+
{
22+
name: "common markdown emphasis is converted",
23+
input: "This is **bold** and _italic_",
24+
shouldContain: []string{"<strong>bold</strong>", "<em>italic</em>"},
25+
},
26+
{
27+
name: "markdown list is converted",
28+
input: "- first\n- second",
29+
shouldContain: []string{"<ul>", "<li>first</li>", "<li>second</li>"},
30+
},
2131
{
2232
name: "backtick-wrapped attachment tag is escaped",
2333
input: "Manually construct the `<action-text-attachment sgid=\"...\" content-type=\"application/vnd.actiontext.mention\"></action-text-attachment>`",

0 commit comments

Comments
 (0)