Skip to content

Commit 39bceaf

Browse files
feat: Adds helpers, documentation for experimental blocks feature (#1460)
* Adds experimental blocks feature
1 parent 7e2e45d commit 39bceaf

File tree

3 files changed

+465
-0
lines changed

3 files changed

+465
-0
lines changed

x/blocks/blocks.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package blocks
2+
3+
// New creates a new QueryResponse with the given blocks. This is the top level block.
4+
// and should be used to return all subsections unless you wish to create the struct manually.
5+
func New(blocks ...Block) QueryResponse {
6+
return QueryResponse{
7+
CadenceResponseType: "formattedData",
8+
Format: "blocks",
9+
Blocks: blocks,
10+
}
11+
}
12+
13+
// Creates a markdown block with the given text.
14+
func NewMarkdownSection(markdownText string) Block {
15+
return Block{
16+
Type: "section",
17+
Format: "text/markdown",
18+
ComponentOptions: &ComponentOptions{
19+
Text: markdownText,
20+
},
21+
}
22+
}
23+
24+
// NewDivider creates a divider in the UI.
25+
func NewDivider() Block {
26+
return Block{
27+
Type: "divider",
28+
}
29+
}
30+
31+
// Creates a set of actions for signalling the workflow.
32+
func NewSignalActions(elements ...Element) Block {
33+
return Block{
34+
Type: "actions",
35+
Elements: elements,
36+
}
37+
}
38+
39+
// NewSignalButton creates a button that will signal the workflow with the given signal name and value.
40+
// the signal value can be nil if no value is needed.
41+
func NewSignalButton(text string, signalName string, signalValue interface{}) Element {
42+
return Element{
43+
Type: "button",
44+
ComponentOptions: &ComponentOptions{
45+
Type: "plain_text",
46+
Text: text,
47+
},
48+
Action: &Action{
49+
Type: "signal",
50+
SignalName: signalName,
51+
SignalValue: signalValue,
52+
},
53+
}
54+
}
55+
56+
// NewSignalButtonWithExternalWorkflow creates a button that will signal the workflow with the given signal name and value,
57+
// and will also start the external workflow with the given workflow ID and run ID.
58+
//
59+
// The RunID should be optional and the latest workflow will be selected i it's not provided
60+
func NewSignalButtonWithExternalWorkflow(text string, signalName string, signalValue interface{}, workflowID string, runID string) Element {
61+
return Element{
62+
Type: "button",
63+
ComponentOptions: &ComponentOptions{
64+
Type: "plain_text",
65+
Text: text,
66+
},
67+
Action: &Action{
68+
Type: "signal",
69+
SignalName: signalName,
70+
SignalValue: signalValue,
71+
WorkflowID: workflowID,
72+
RunID: runID,
73+
},
74+
}
75+
}
76+
77+
// Query response is the overall wrapper struct that will be returned to the client.
78+
// There's nothing special about this Go code specifically, the actual UI
79+
// only cares about the JSON structure returned, but this is a helpful wrapper
80+
type QueryResponse struct {
81+
CadenceResponseType string `json:"cadenceResponseType"`
82+
Format string `json:"format"`
83+
Blocks []Block `json:"blocks"`
84+
}
85+
86+
// A section in the workflow Query response that will be rendered
87+
type Block struct {
88+
Type string `json:"type"`
89+
Format string `json:"format,omitempty"`
90+
ComponentOptions *ComponentOptions `json:"componentOptions,omitempty"`
91+
Elements []Element `json:"elements,omitempty"`
92+
}
93+
94+
type Element struct {
95+
Type string `json:"type"`
96+
ComponentOptions *ComponentOptions `json:"componentOptions,omitempty"`
97+
Action *Action `json:"action,omitempty"`
98+
}
99+
100+
type ComponentOptions struct {
101+
Type string `json:"type,omitempty"`
102+
Text string `json:"text,omitempty"`
103+
}
104+
105+
// Action signifying something such as a button
106+
type Action struct {
107+
Type string `json:"type"`
108+
SignalName string `json:"signal_name,omitempty"`
109+
SignalValue interface{} `json:"signal_value,omitempty"`
110+
WorkflowID string `json:"workflow_id,omitempty"`
111+
RunID string `json:"run_id,omitempty"`
112+
}

x/blocks/blocks_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package blocks
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func make() string {
11+
12+
r := New(
13+
NewMarkdownSection("## Lunch options\nWe're voting on where to order lunch today. Select the option you want to vote for."),
14+
NewDivider(),
15+
NewMarkdownSection("## Votes\n ... vote table"),
16+
NewMarkdownSection("## Menu\n ... menu options"),
17+
NewSignalActions(
18+
NewSignalButton("Farmhouse", "lunch_order", map[string]string{"location": "farmhouse - red thai curry", "requests": "spicy"}),
19+
NewSignalButtonWithExternalWorkflow("Ethiopian", "no_lunch_order_walk_in_person", nil, "in-person-order-workflow", ""),
20+
NewSignalButton("Ler Ros", "lunch_order", map[string]string{"location": "Ler Ros", "meal": "tofo Bahn Mi"}),
21+
),
22+
)
23+
24+
d, err := json.Marshal(r)
25+
if err != nil {
26+
panic(err)
27+
}
28+
return string(d)
29+
}
30+
31+
func TestExample(t *testing.T) {
32+
expectedJSON := `
33+
{
34+
"cadenceResponseType": "formattedData",
35+
"format": "blocks",
36+
"blocks": [
37+
{
38+
"type": "section",
39+
"format": "text/markdown",
40+
"componentOptions": {
41+
"text": "## Lunch options\nWe're voting on where to order lunch today. Select the option you want to vote for."
42+
}
43+
},
44+
{
45+
"type": "divider"
46+
},
47+
{
48+
"type": "section",
49+
"format": "text/markdown",
50+
"componentOptions": {
51+
"text": "## Votes\n ... vote table"
52+
}
53+
},
54+
{
55+
"type": "section",
56+
"format": "text/markdown",
57+
"componentOptions": {
58+
"text": "## Menu\n ... menu options"
59+
}
60+
},
61+
{
62+
"type": "actions",
63+
"elements": [
64+
{
65+
"type": "button",
66+
"componentOptions": {
67+
"type": "plain_text",
68+
"text": "Farmhouse"
69+
},
70+
"action": {
71+
"type": "signal",
72+
"signal_name": "lunch_order",
73+
"signal_value": {
74+
"location": "farmhouse - red thai curry",
75+
"requests": "spicy"
76+
}
77+
}
78+
},
79+
{
80+
"type": "button",
81+
"componentOptions": {
82+
"type": "plain_text",
83+
"text": "Ethiopian"
84+
},
85+
"action": {
86+
"type": "signal",
87+
"signal_name": "no_lunch_order_walk_in_person",
88+
"workflow_id": "in-person-order-workflow"
89+
}
90+
},
91+
{
92+
"type": "button",
93+
"componentOptions": {
94+
"type": "plain_text",
95+
"text": "Ler Ros"
96+
},
97+
"action": {
98+
"type": "signal",
99+
"signal_name": "lunch_order",
100+
"signal_value": {
101+
"location": "Ler Ros",
102+
"meal": "tofo Bahn Mi"
103+
}
104+
}
105+
}
106+
]
107+
}
108+
]
109+
}
110+
`
111+
112+
var expected interface{}
113+
_ = json.Unmarshal([]byte(expectedJSON), &expected)
114+
115+
var actual interface{}
116+
117+
example := make()
118+
_ = json.Unmarshal([]byte(example), &actual)
119+
120+
assert.Equal(t, expected, actual)
121+
}

0 commit comments

Comments
 (0)