Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions server/command_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
commandTriggerListFiles = "list_files"
commandTriggerAutocompleteTest = "autocomplete_test"
commandTriggerToast = "toast"
commandTriggerInlineAction = "inline_action"

dialogElementNameNumber = "somenumber"
dialogElementNameEmail = "someemail"
Expand Down Expand Up @@ -136,6 +137,15 @@ func (p *Plugin) registerCommands() error {
return errors.Wrapf(err, "failed to register %s command", commandTriggerDialog)
}

if err := p.API.RegisterCommand(&model.Command{
Trigger: commandTriggerInlineAction,
AutoComplete: true,
AutoCompleteHint: "",
AutoCompleteDesc: "Demonstrates inline action buttons in markdown tables.",
}); err != nil {
return errors.Wrapf(err, "failed to register %s command", commandTriggerInlineAction)
}

if err := p.API.RegisterCommand(&model.Command{
Trigger: commandTriggerToast,
AutoComplete: true,
Expand Down Expand Up @@ -303,6 +313,8 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
return p.executeCommandMentions(args), nil
case commandTriggerAutocompleteTest:
return p.executeAutocompleteTest(args), nil
case commandTriggerInlineAction:
return p.executeCommandInlineAction(args), nil
case commandTriggerToast:
return p.executeCommandToast(c, args), nil

Expand Down Expand Up @@ -575,6 +587,48 @@ func (p *Plugin) executeCommandInteractive(args *model.CommandArgs) *model.Comma
return &model.CommandResponse{}
}

func (p *Plugin) executeCommandInlineAction(args *model.CommandArgs) *model.CommandResponse {
message := "### Issue Tracker [Demo Project]\n\n" +
"| ID | Title | Priority | Assignee | Action |\n" +
"|----|-------|----------|----------|--------|\n" +
"| ISS-101 | Login page returns 500 on Safari | High | @alice | [Triage](mmaction://triage?id=ISS-101&title=Login+page+returns+500+on+Safari&priority=high&assignee=alice) |\n" +
"| ISS-102 | Dark mode toggle doesn't persist | Medium | @bob | [Triage](mmaction://triage?id=ISS-102&title=Dark+mode+toggle+doesn't+persist&priority=medium&assignee=bob) |\n" +
"| ISS-103 | CSV export missing header row | Low | _unassigned_ | [Triage](mmaction://triage?id=ISS-103&title=CSV+export+missing+header+row&priority=low&assignee=) |\n" +
"| ISS-104 | API rate limiter blocks batch jobs | Critical | @charlie | [Triage](mmaction://triage?id=ISS-104&title=API+rate+limiter+blocks+batch+jobs&priority=critical&assignee=charlie) |\n" +
"| ISS-105 | Notification badge count off by one | Low | @alice | [Triage](mmaction://triage?id=ISS-105&title=Notification+badge+count+off+by+one&priority=low&assignee=alice) |\n"

post := &model.Post{
ChannelId: args.ChannelId,
RootId: args.RootId,
UserId: p.botID,
Message: message,
Props: model.StringInterface{
// Renamed from "inline_actions" → "mm_blocks_actions" on
// feature/action_buttons. Each entry now also requires
// an explicit "type" field (validated server-side).
"mm_blocks_actions": map[string]any{
"triage": map[string]any{
"type": "external",
"url": fmt.Sprintf("/plugins/%s/inline_action/triage", manifest.Id),
"context": map[string]any{"project": "Demo Project"},
},
},
},
}

_, err := p.API.CreatePost(post)
if err != nil {
const errorMessage = "Failed to create inline action post"
p.API.LogError(errorMessage, "err", err.Error())
return &model.CommandResponse{
ResponseType: model.CommandResponseTypeEphemeral,
Text: errorMessage,
}
}

return &model.CommandResponse{}
}

func (p *Plugin) crash() {
<-time.NewTimer(time.Second).C
y := 0
Expand Down
88 changes: 88 additions & 0 deletions server/http_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func (p *Plugin) initializeAPI() {
interativeRouter.Use(p.withDelay)
interativeRouter.HandleFunc("/button/1", p.handleInteractiveAction)

inlineActionRouter := router.PathPrefix("/inline_action").Subrouter()
inlineActionRouter.HandleFunc("/triage", p.handleInlineActionTriage)

dialogRouter := router.PathPrefix("/dialog").Subrouter()
dialogRouter.Use(p.withDelay)
dialogRouter.HandleFunc("/1", p.handleDialog1)
Expand Down Expand Up @@ -492,6 +495,91 @@ func (p *Plugin) handleInteractiveAction(w http.ResponseWriter, r *http.Request)
p.writeJSON(w, resp)
}

func (p *Plugin) handleInlineActionTriage(w http.ResponseWriter, r *http.Request) {
var request model.PostActionIntegrationRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
p.API.LogError("Failed to decode PostActionIntegrationRequest", "err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()

// Per-click params arrive as URL query string on feature/action_buttons —
// the server merges spec.Query and request.Query into the upstream URL
// via MergeQueryIntoURL before forwarding. Static context fields (like
// "project") still come through request.Context as before.
q := r.URL.Query()
issueID := q.Get("id")
title := q.Get("title")
priority := q.Get("priority")
assignee := q.Get("assignee")
if assignee == "" {
assignee = "unassigned"
}
project, _ := request.Context["project"].(string)

// Build introduction text showing key identifiers passed via mmaction://
intro := fmt.Sprintf(
"**Issue:** %s | **Project:** %s\n**Title:** %s | **Priority:** %s | **Assignee:** @%s",
issueID, project, title, priority, assignee,
Comment thread
sbishel marked this conversation as resolved.
Outdated
)

serverConfig := p.API.GetConfig()
dialogRequest := model.OpenDialogRequest{
TriggerId: request.TriggerId,
URL: fmt.Sprintf("%s/plugins/%s/dialog/3", *serverConfig.ServiceSettings.SiteURL, manifest.Id),
Dialog: model.Dialog{
CallbackId: "triage_" + issueID,
Title: "Triage " + issueID,
IntroductionText: intro,
SubmitLabel: "Submit Triage",
Elements: []model.DialogElement{
{
DisplayName: "QA Resource",
Name: "qa_resource",
Type: "select",
Placeholder: "Assign a QA resource...",
HelpText: "Select a team member to verify the fix.",
DataSource: "users",
},
{
DisplayName: "Due Date",
Name: "due_date",
Type: "date",
HelpText: "Target date for resolution.",
},
{
DisplayName: "Triage Notes",
Name: "notes",
Type: "textarea",
Optional: true,
Placeholder: "Root cause, reproduction steps, next actions...",
HelpText: "These notes will be posted to the channel.",
MaxLength: 500,
},
},
},
}

if appErr := p.API.OpenInteractiveDialog(dialogRequest); appErr != nil {
p.API.LogError("Failed to open triage dialog", "err", appErr.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}

resp := &model.PostActionIntegrationResponse{}
p.writeJSON(w, resp)
}

func formatJSON(v any) string {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return fmt.Sprintf("%+v", v)
}
return string(b)
}

func (p *Plugin) writeJSON(w http.ResponseWriter, response any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
Expand Down
Loading