slocks 0.1.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 +7 -0
- data/README.md +386 -0
- data/Rakefile +12 -0
- data/lib/slocks/action_view_monkeys.rb +63 -0
- data/lib/slocks/blocks_builder.rb +531 -0
- data/lib/slocks/modal_builder.rb +35 -0
- data/lib/slocks/railtie.rb +19 -0
- data/lib/slocks/rich_text_builder.rb +102 -0
- data/lib/slocks/template_handler.rb +79 -0
- data/lib/slocks/version.rb +5 -0
- data/lib/slocks.rb +12 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 61ae8a8e25f7e6aec74c585aa238882b34821bb2a274315dfdc4ffa79b89edee
|
|
4
|
+
data.tar.gz: 6268cd40385da4328fb3b1523a4b3f21cc33d699300c74a6f2cc4422f74d884c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 425ebb760ef81e84762c5f83e483cdf9e2b19615c4717efc3724e79803556a7f8dff3443abdcfab36647dea3ca86e761d22f6d6dc46ef1de261933f09fbc2a52
|
|
7
|
+
data.tar.gz: 4cb8df895f37394fef5375dbb8e0530315fe074408b129a55b5ec4e4bee854fe1c5e9923c6b2dbda2553cdc0502c95037162d701cdf6c267cfa580a1b3e68402
|
data/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Slocks
|
|
2
|
+
|
|
3
|
+
**Slocks** is a DSL & a Rails template handler for generating Slack [Block Kit](https://docs.slack.dev/block-kit/) surfaces.
|
|
4
|
+
|
|
5
|
+
No implied warranty. Solid chance it won't work.
|
|
6
|
+
|
|
7
|
+
## Why Slocks?
|
|
8
|
+
I've lost too many hours of my life to
|
|
9
|
+
\*write\*→\*⌘C\*→\*⌘-Tab to Block Kit Builder\*→\*⌘V\*→"dammit"→\*⌘-Tab\*→\*rewrite\*→\*repeat\*.
|
|
10
|
+
|
|
11
|
+
No more.
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add to your Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem 'slocks'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bundle install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
Create a template.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# app/views/notifications/order_shipped.slack_blocks
|
|
32
|
+
|
|
33
|
+
header "📦 Your order has shipped!"
|
|
34
|
+
|
|
35
|
+
section "*Order ##{@order.id}* is on its way", markdown: true
|
|
36
|
+
|
|
37
|
+
divider
|
|
38
|
+
|
|
39
|
+
section "Tracking:",
|
|
40
|
+
fields: [
|
|
41
|
+
mrkdwn_text("*Carrier:* #{@order.carrier}"),
|
|
42
|
+
mrkdwn_text("*Tracking:* `#{@order.tracking_number}`")
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
actions [ button("Track Package", "track_#{@order.id}", style: "primary", url: @order.tracking_url) ]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Render it in your controller or service:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class OrdersController < ApplicationController
|
|
52
|
+
def mark
|
|
53
|
+
@user = current_user
|
|
54
|
+
|
|
55
|
+
payload = render_to_string(
|
|
56
|
+
template: 'notifications/welcome',
|
|
57
|
+
formats: [:slack_message]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
slack_client.chat_postMessage(
|
|
61
|
+
channel: @user.slack_id,
|
|
62
|
+
**JSON.parse(payload)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Blocks Reference
|
|
69
|
+
|
|
70
|
+
### Layout Blocks
|
|
71
|
+
|
|
72
|
+
#### Header
|
|
73
|
+
```ruby
|
|
74
|
+
header "🎉 Great news, Baltimore!"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Section
|
|
78
|
+
```ruby
|
|
79
|
+
# simple text
|
|
80
|
+
section "hello world"
|
|
81
|
+
|
|
82
|
+
# mrkdwn text
|
|
83
|
+
section "to *bold*ly _italic_ize...", markdown: true
|
|
84
|
+
|
|
85
|
+
# fields
|
|
86
|
+
section "details:",
|
|
87
|
+
fields: [
|
|
88
|
+
mrkdwn_text("*computer:* over"),
|
|
89
|
+
mrkdwn_text("*virus:* VERY yes.")
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# accessorizing a little (images, buttons, etc)
|
|
93
|
+
section "click to continue",
|
|
94
|
+
accessory: button("click me, i dare you", "explode_action")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### Divider
|
|
98
|
+
```ruby
|
|
99
|
+
divider
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Context
|
|
103
|
+
```ruby
|
|
104
|
+
context [
|
|
105
|
+
mrkdwn_text("Last updated: #{time_ago_in_words(@updated_at)} ago"),
|
|
106
|
+
image_element("https://crouton.net/crouton.png", "icon")
|
|
107
|
+
]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Actions
|
|
111
|
+
(some these won't do anything unless you use them somewhere expecting accessories! it'd do you a lot of good to read Slack's Block Kit docs before you try anything...)
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
actions [
|
|
115
|
+
button("Approve", "approve", style: "primary"),
|
|
116
|
+
button("Reject", "reject", style: "danger"),
|
|
117
|
+
button("More Info", "info")
|
|
118
|
+
]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Image
|
|
122
|
+
```ruby
|
|
123
|
+
image "https://example.com/photo.jpg", "Photo description",
|
|
124
|
+
title: "Amazing Photo"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Input
|
|
128
|
+
```ruby
|
|
129
|
+
input "Your name",
|
|
130
|
+
plain_text_input("name_input", placeholder: "Enter your name")
|
|
131
|
+
|
|
132
|
+
input "Choose an option",
|
|
133
|
+
select_menu("option_select",
|
|
134
|
+
placeholder: "Select one",
|
|
135
|
+
options: [
|
|
136
|
+
option("Option 1", "opt1"),
|
|
137
|
+
option("Option 2", "opt2")
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### File
|
|
143
|
+
```ruby
|
|
144
|
+
file "F123ABC456", source: "remote"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Video
|
|
148
|
+
```ruby
|
|
149
|
+
video "How to use Slocks",
|
|
150
|
+
"https://example.com/video",
|
|
151
|
+
"https://example.com/thumbnail.jpg",
|
|
152
|
+
"https://example.com/video.mp4",
|
|
153
|
+
alt_text: "Tutorial video"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Block Elements
|
|
157
|
+
|
|
158
|
+
#### Buttons
|
|
159
|
+
```ruby
|
|
160
|
+
button("Click me", "action_id")
|
|
161
|
+
button("Primary", "action", style: "primary")
|
|
162
|
+
button("Danger", "delete", style: "danger")
|
|
163
|
+
button("Link", "link", url: "https://example.com")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Select Menus
|
|
167
|
+
```ruby
|
|
168
|
+
# Static select
|
|
169
|
+
select_menu("action_id",
|
|
170
|
+
placeholder: "Choose one",
|
|
171
|
+
options: [
|
|
172
|
+
option("Label 1", "value1"),
|
|
173
|
+
option("Label 2", "value2")
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Multi-select
|
|
178
|
+
multi_select_menu("action_id",
|
|
179
|
+
placeholder: "Choose multiple",
|
|
180
|
+
options: [...]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# User select
|
|
184
|
+
users_select("user_action", placeholder: "Select a user")
|
|
185
|
+
|
|
186
|
+
# Channel select
|
|
187
|
+
channels_select("channel_action", placeholder: "Select a channel")
|
|
188
|
+
|
|
189
|
+
# Conversation select
|
|
190
|
+
conversations_select("convo_action", placeholder: "Select a conversation")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Inputs
|
|
194
|
+
```ruby
|
|
195
|
+
# Text input
|
|
196
|
+
plain_text_input("action_id",
|
|
197
|
+
placeholder: "Type here...",
|
|
198
|
+
multiline: true
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Email input
|
|
202
|
+
email_input("email_action", placeholder: "your@email.com")
|
|
203
|
+
|
|
204
|
+
# URL input
|
|
205
|
+
url_input("url_action", placeholder: "https://...")
|
|
206
|
+
|
|
207
|
+
# Number input
|
|
208
|
+
number_input("number_action",
|
|
209
|
+
is_decimal_allowed: true,
|
|
210
|
+
min_value: 0,
|
|
211
|
+
max_value: 100
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Date & Time Pickers
|
|
216
|
+
```ruby
|
|
217
|
+
date_picker("date_action", placeholder: "Select a date")
|
|
218
|
+
time_picker("time_action", placeholder: "Select a time")
|
|
219
|
+
datetime_picker("datetime_action")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Other Elements
|
|
223
|
+
```ruby
|
|
224
|
+
# Checkboxes
|
|
225
|
+
checkboxes("check_action", [
|
|
226
|
+
option("Option 1", "val1"),
|
|
227
|
+
option("Option 2", "val2")
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
# Radio buttons
|
|
231
|
+
radio_buttons("radio_action", [
|
|
232
|
+
option("Choice 1", "c1"),
|
|
233
|
+
option("Choice 2", "c2")
|
|
234
|
+
])
|
|
235
|
+
|
|
236
|
+
# Overflow menu
|
|
237
|
+
overflow("overflow_action", [
|
|
238
|
+
option("Edit", "edit"),
|
|
239
|
+
option("Delete", "delete")
|
|
240
|
+
])
|
|
241
|
+
|
|
242
|
+
# File input
|
|
243
|
+
file_input("file_action", filetypes: ["pdf", "doc"], max_files: 5)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Composition Objects
|
|
247
|
+
|
|
248
|
+
#### Text Objects
|
|
249
|
+
```ruby
|
|
250
|
+
mrkdwn_text("*Bold* and _italic_")
|
|
251
|
+
plain_text("Plain text", emoji: true)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Option & Option Groups
|
|
255
|
+
```ruby
|
|
256
|
+
option("Display text", "value")
|
|
257
|
+
option("With description", "val", description: "More details")
|
|
258
|
+
|
|
259
|
+
option_group("Group label", [
|
|
260
|
+
option("Opt 1", "v1"),
|
|
261
|
+
option("Opt 2", "v2")
|
|
262
|
+
])
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### Confirm Dialog
|
|
266
|
+
```ruby
|
|
267
|
+
confirm_dialog(
|
|
268
|
+
title: "Are you sure?",
|
|
269
|
+
text: "This action cannot be undone",
|
|
270
|
+
confirm: "Yes, delete it",
|
|
271
|
+
deny: "Cancel",
|
|
272
|
+
style: "danger"
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Modals
|
|
277
|
+
|
|
278
|
+
Create a modal template at `app/views/modals/feedback.slack_modal`:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
title "Send Feedback"
|
|
282
|
+
submit "Submit"
|
|
283
|
+
close "Cancel"
|
|
284
|
+
callback "feedback_modal"
|
|
285
|
+
|
|
286
|
+
section "How was your experience?", markdown: true
|
|
287
|
+
|
|
288
|
+
input "Rating",
|
|
289
|
+
select_menu("rating",
|
|
290
|
+
placeholder: "Choose rating",
|
|
291
|
+
options: [
|
|
292
|
+
option("⭐⭐⭐⭐⭐ Excellent", "5"),
|
|
293
|
+
option("⭐⭐⭐⭐ Good", "4"),
|
|
294
|
+
option("⭐⭐⭐ Okay", "3"),
|
|
295
|
+
option("⭐⭐ Poor", "2"),
|
|
296
|
+
option("⭐ Bad", "1")
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
input "Comments",
|
|
301
|
+
plain_text_input("comments",
|
|
302
|
+
placeholder: "Tell us more...",
|
|
303
|
+
multiline: true
|
|
304
|
+
),
|
|
305
|
+
optional: true
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Render it:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
modal_json = render_to_string(
|
|
312
|
+
template: 'modals/feedback',
|
|
313
|
+
formats: [:slack_modal]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
slack_client.views_open(
|
|
317
|
+
trigger_id: params[:trigger_id],
|
|
318
|
+
view: JSON.parse(modal_json)
|
|
319
|
+
)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Rails Helpers
|
|
323
|
+
|
|
324
|
+
All Rails helpers work in templates:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
section "Order summary:",
|
|
328
|
+
fields: [
|
|
329
|
+
mrkdwn_text("*Items:* #{pluralize(@order.items.count, 'item')}"),
|
|
330
|
+
mrkdwn_text("*Total:* #{number_to_currency(@order.total)}"),
|
|
331
|
+
mrkdwn_text("*Ordered:* #{time_ago_in_words(@order.created_at)} ago")
|
|
332
|
+
]
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Partials
|
|
336
|
+
|
|
337
|
+
Render partials just like ERB:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
# app/views/orders/_order.slack_blocks
|
|
341
|
+
section "*Order ##{@order.id}*",
|
|
342
|
+
fields: [
|
|
343
|
+
mrkdwn_text("*Status:* #{@order.status}"),
|
|
344
|
+
mrkdwn_text("*Total:* #{number_to_currency(@order.total)}")
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
# app/views/orders/summary.slack_blocks
|
|
348
|
+
header "Your Orders"
|
|
349
|
+
|
|
350
|
+
render @orders # Renders _order.slack_blocks for each order
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Rich Text (Advanced)
|
|
354
|
+
|
|
355
|
+
For rich text blocks with complex formatting:
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
rich_text do
|
|
359
|
+
section do
|
|
360
|
+
text "Hello ", bold: true
|
|
361
|
+
text "world!", italic: true
|
|
362
|
+
emoji "wave"
|
|
363
|
+
link "https://example.com", text: "Click here"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
list(style: "bullet") do
|
|
367
|
+
text "First item"
|
|
368
|
+
text "Second item"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Contributing
|
|
374
|
+
|
|
375
|
+
Bug reports are welcome, but pull requests are much _much_ welcomer.
|
|
376
|
+
|
|
377
|
+
This repo lives on GitHub at https://github.com/24c02/slocks. Happy hacking!
|
|
378
|
+
## License
|
|
379
|
+
|
|
380
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
381
|
+
|
|
382
|
+
## Credits
|
|
383
|
+
|
|
384
|
+
built with <3️ by nora and various LLM-slop providers.
|
|
385
|
+
|
|
386
|
+
heavily inspired by Akira Matsuda's fantastic [JB](https://github.com/amatsuda/jb/) gem.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slocks
|
|
4
|
+
module TemplateRenderer
|
|
5
|
+
module JSONizer
|
|
6
|
+
def render_template(_view, template, *)
|
|
7
|
+
rendered_template = super
|
|
8
|
+
if template.respond_to?(:handler) && [
|
|
9
|
+
Slocks::TemplateHandler, Slocks::ModalTemplateHandler, Slocks::TemplateHandlerDispatcher
|
|
10
|
+
].include?(template.handler)
|
|
11
|
+
rendered_template.instance_variable_set :@body,
|
|
12
|
+
rendered_template.body.to_json
|
|
13
|
+
end
|
|
14
|
+
rendered_template
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class TemplateResult < SimpleDelegator
|
|
20
|
+
def to_s
|
|
21
|
+
__getobj__
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module CollectionRendererExtension
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def render_collection(_collection, _view, _path, template, _layout, _block)
|
|
29
|
+
obj = super
|
|
30
|
+
if template.respond_to?(:handler) && [Slocks::TemplateHandler, Slocks::ModalTemplateHandler, Slocks::TemplateHandlerDispatcher].include?(template.handler)
|
|
31
|
+
if obj.is_a?(ActionView::AbstractRenderer::RenderedCollection::EmptyCollection)
|
|
32
|
+
def obj.body = []
|
|
33
|
+
else
|
|
34
|
+
def obj.body = @rendered_templates.map(&:body)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
obj
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module PartialRendererExtension
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def render_collection(_view, template)
|
|
45
|
+
obj = super
|
|
46
|
+
if template.respond_to?(:handler) && [Slocks::TemplateHandler, Slocks::ModalTemplateHandler, Slocks::TemplateHandlerDispatcher].include?(template.handler)
|
|
47
|
+
if obj.is_a?(ActionView::AbstractRenderer::RenderedCollection::EmptyCollection)
|
|
48
|
+
def obj.body = []
|
|
49
|
+
else
|
|
50
|
+
def obj.body = @rendered_templates.map(&:body)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
obj
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
::ActionView::TemplateRenderer.prepend ::Slocks::TemplateRenderer::JSONizer
|
|
59
|
+
begin
|
|
60
|
+
::ActionView::CollectionRenderer.prepend ::Slocks::CollectionRendererExtension
|
|
61
|
+
rescue NameError
|
|
62
|
+
::ActionView::PartialRenderer.prepend ::Slocks::PartialRendererExtension
|
|
63
|
+
end
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slocks
|
|
4
|
+
class BlocksBuilder
|
|
5
|
+
attr_reader :blocks
|
|
6
|
+
|
|
7
|
+
def initialize(context)
|
|
8
|
+
@context = context
|
|
9
|
+
@blocks = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def header(text)
|
|
13
|
+
@blocks << {
|
|
14
|
+
type: "header",
|
|
15
|
+
text: {
|
|
16
|
+
type: "plain_text",
|
|
17
|
+
text: text
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def section(text = nil, accessory: nil, fields: nil, markdown: false)
|
|
23
|
+
block = { type: "section" }
|
|
24
|
+
|
|
25
|
+
if text
|
|
26
|
+
block[:text] = {
|
|
27
|
+
type: markdown ? "mrkdwn" : "plain_text",
|
|
28
|
+
text: text
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
block[:accessory] = accessory if accessory
|
|
33
|
+
block[:fields] = fields if fields
|
|
34
|
+
|
|
35
|
+
@blocks << block
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def simple_section(text)
|
|
39
|
+
section(text, markdown: true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def divider
|
|
43
|
+
@blocks << { type: "divider" }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def context(elements)
|
|
47
|
+
@blocks << {
|
|
48
|
+
type: "context",
|
|
49
|
+
elements: elements
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def context_actions(elements, block_id: nil)
|
|
54
|
+
@blocks << {
|
|
55
|
+
type: "context_actions",
|
|
56
|
+
elements:,
|
|
57
|
+
block_id:
|
|
58
|
+
}.compact
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def actions(elements)
|
|
62
|
+
@blocks << {
|
|
63
|
+
type: "actions",
|
|
64
|
+
elements: elements
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def image(image_url, alt_text, title: nil)
|
|
69
|
+
block = {
|
|
70
|
+
type: "image",
|
|
71
|
+
image_url: image_url,
|
|
72
|
+
alt_text: alt_text
|
|
73
|
+
}
|
|
74
|
+
block[:title] = { type: "plain_text", text: title } if title
|
|
75
|
+
@blocks << block
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def input(label, element, optional: false, hint: nil, block_id: nil, dispatch_action: nil)
|
|
79
|
+
block = {
|
|
80
|
+
type: "input",
|
|
81
|
+
label: { type: "plain_text", text: label },
|
|
82
|
+
element: element
|
|
83
|
+
}
|
|
84
|
+
block[:block_id] = block_id if block_id
|
|
85
|
+
block[:optional] = optional if optional
|
|
86
|
+
block[:hint] = { type: "plain_text", text: hint } if hint
|
|
87
|
+
block[:dispatch_action] = dispatch_action if dispatch_action
|
|
88
|
+
@blocks << block
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def file(external_id, source: "remote", block_id: nil)
|
|
92
|
+
block = {
|
|
93
|
+
type: "file",
|
|
94
|
+
external_id: external_id,
|
|
95
|
+
source: source
|
|
96
|
+
}
|
|
97
|
+
block[:block_id] = block_id if block_id
|
|
98
|
+
@blocks << block
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def video(title, title_url, thumbnail_url, video_url, alt_text:, description: nil, author_name: nil,
|
|
102
|
+
provider_name: nil, provider_icon_url: nil, block_id: nil)
|
|
103
|
+
block = {
|
|
104
|
+
type: "video",
|
|
105
|
+
title: { type: "plain_text", text: title },
|
|
106
|
+
title_url:,
|
|
107
|
+
thumbnail_url:,
|
|
108
|
+
video_url:,
|
|
109
|
+
alt_text:,
|
|
110
|
+
description: description ? { type: "plain_text", text: description } : nil,
|
|
111
|
+
author_name:,
|
|
112
|
+
provider_name:,
|
|
113
|
+
provider_icon_url:,
|
|
114
|
+
block_id:
|
|
115
|
+
}.compact
|
|
116
|
+
@blocks << block
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def rich_text(&block)
|
|
120
|
+
builder = RichTextBuilder.new
|
|
121
|
+
builder.instance_eval(&block) if block_given?
|
|
122
|
+
@blocks << {
|
|
123
|
+
type: "rich_text",
|
|
124
|
+
elements: builder.elements
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def image_element(image_url, alt_text)
|
|
129
|
+
{
|
|
130
|
+
type: "image",
|
|
131
|
+
image_url: image_url,
|
|
132
|
+
alt_text: alt_text
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def mrkdwn_text(text)
|
|
137
|
+
{
|
|
138
|
+
type: "mrkdwn",
|
|
139
|
+
text: text
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def plain_text(text, emoji: true)
|
|
144
|
+
{
|
|
145
|
+
type: "plain_text",
|
|
146
|
+
text: text,
|
|
147
|
+
emoji: emoji
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def button(text, action_id, value: nil, url: nil, style: nil, confirm: nil, accessibility_label: nil)
|
|
152
|
+
{
|
|
153
|
+
type: "button",
|
|
154
|
+
text: { type: "plain_text", text: text },
|
|
155
|
+
action_id:,
|
|
156
|
+
value:,
|
|
157
|
+
url:,
|
|
158
|
+
style:,
|
|
159
|
+
confirm:,
|
|
160
|
+
accessibility_label:
|
|
161
|
+
}.compact
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def checkboxes(action_id, options, initial_options: nil, confirm: nil, focus_on_load: nil)
|
|
165
|
+
{
|
|
166
|
+
type: "checkboxes",
|
|
167
|
+
action_id:,
|
|
168
|
+
options:,
|
|
169
|
+
initial_options:,
|
|
170
|
+
confirm:,
|
|
171
|
+
focus_on_load:
|
|
172
|
+
}.compact
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def date_picker(action_id, placeholder: nil, initial_date: nil, confirm: nil, focus_on_load: nil)
|
|
176
|
+
{
|
|
177
|
+
type: "datepicker",
|
|
178
|
+
action_id:,
|
|
179
|
+
placeholder: placeholder ? { type: "plain_text", text: placeholder } : nil,
|
|
180
|
+
initial_date:,
|
|
181
|
+
confirm:,
|
|
182
|
+
focus_on_load:
|
|
183
|
+
}.compact
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def datetime_picker(action_id, initial_date_time: nil, confirm: nil, focus_on_load: nil)
|
|
187
|
+
{
|
|
188
|
+
type: "datetimepicker",
|
|
189
|
+
action_id:,
|
|
190
|
+
initial_date_time:,
|
|
191
|
+
confirm:,
|
|
192
|
+
focus_on_load:
|
|
193
|
+
}.compact
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def time_picker(action_id, placeholder: nil, initial_time: nil, confirm: nil, focus_on_load: nil, timezone: nil)
|
|
197
|
+
{
|
|
198
|
+
type: "timepicker",
|
|
199
|
+
action_id:,
|
|
200
|
+
placeholder: placeholder ? { type: "plain_text", text: placeholder } : nil,
|
|
201
|
+
initial_time:,
|
|
202
|
+
confirm:,
|
|
203
|
+
focus_on_load:,
|
|
204
|
+
timezone:
|
|
205
|
+
}.compact
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def plain_text_input(action_id, placeholder: nil, initial_value: nil, multiline: nil, min_length: nil,
|
|
209
|
+
max_length: nil, dispatch_action_config: nil, focus_on_load: nil)
|
|
210
|
+
{
|
|
211
|
+
type: "plain_text_input",
|
|
212
|
+
action_id:,
|
|
213
|
+
placeholder: placeholder ? { type: "plain_text", text: placeholder } : nil,
|
|
214
|
+
initial_value:,
|
|
215
|
+
multiline:,
|
|
216
|
+
min_length:,
|
|
217
|
+
max_length:,
|
|
218
|
+
dispatch_action_config:,
|
|
219
|
+
focus_on_load:
|
|
220
|
+
}.compact
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def email_input(action_id, placeholder: nil, initial_value: nil, dispatch_action_config: nil, focus_on_load: nil)
|
|
224
|
+
{
|
|
225
|
+
type: "email_text_input",
|
|
226
|
+
action_id:,
|
|
227
|
+
placeholder: placeholder ? { type: "plain_text", text: placeholder } : nil,
|
|
228
|
+
initial_value:,
|
|
229
|
+
dispatch_action_config:,
|
|
230
|
+
focus_on_load:
|
|
231
|
+
}.compact
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def url_input(action_id, placeholder: nil, initial_value: nil, dispatch_action_config: nil, focus_on_load: nil)
|
|
235
|
+
{
|
|
236
|
+
type: "url_text_input",
|
|
237
|
+
action_id:,
|
|
238
|
+
placeholder: placeholder ? { type: "plain_text", text: placeholder } : nil,
|
|
239
|
+
initial_value:,
|
|
240
|
+
dispatch_action_config:,
|
|
241
|
+
focus_on_load:
|
|
242
|
+
}.compact
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def number_input(action_id, is_decimal_allowed:, placeholder: nil, initial_value: nil, min_value: nil,
|
|
246
|
+
max_value: nil, dispatch_action_config: nil, focus_on_load: nil)
|
|
247
|
+
{
|
|
248
|
+
type: "number_input",
|
|
249
|
+
action_id:,
|
|
250
|
+
is_decimal_allowed:,
|
|
251
|
+
placeholder: placeholder ? { type: "plain_text", text: placeholder } : nil,
|
|
252
|
+
initial_value:,
|
|
253
|
+
min_value:,
|
|
254
|
+
max_value:,
|
|
255
|
+
dispatch_action_config:,
|
|
256
|
+
focus_on_load:
|
|
257
|
+
}.compact
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def radio_buttons(action_id, options, initial_option: nil, confirm: nil, focus_on_load: nil)
|
|
261
|
+
{
|
|
262
|
+
type: "radio_buttons",
|
|
263
|
+
action_id:,
|
|
264
|
+
options:,
|
|
265
|
+
initial_option:,
|
|
266
|
+
confirm:,
|
|
267
|
+
focus_on_load:
|
|
268
|
+
}.compact
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def select_menu(action_id, placeholder:, options: nil, option_groups: nil, initial_option: nil, confirm: nil,
|
|
272
|
+
focus_on_load: nil)
|
|
273
|
+
{
|
|
274
|
+
type: "static_select",
|
|
275
|
+
action_id:,
|
|
276
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
277
|
+
options:,
|
|
278
|
+
option_groups:,
|
|
279
|
+
initial_option:,
|
|
280
|
+
confirm:,
|
|
281
|
+
focus_on_load:
|
|
282
|
+
}.compact
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def multi_select_menu(action_id, placeholder:, options: nil, option_groups: nil, initial_options: nil,
|
|
286
|
+
max_selected_items: nil, confirm: nil, focus_on_load: nil)
|
|
287
|
+
{
|
|
288
|
+
type: "multi_static_select",
|
|
289
|
+
action_id:,
|
|
290
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
291
|
+
options:,
|
|
292
|
+
option_groups:,
|
|
293
|
+
initial_options:,
|
|
294
|
+
max_selected_items:,
|
|
295
|
+
confirm:,
|
|
296
|
+
focus_on_load:
|
|
297
|
+
}.compact
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def users_select(action_id, placeholder:, initial_user: nil, confirm: nil, focus_on_load: nil)
|
|
301
|
+
{
|
|
302
|
+
type: "users_select",
|
|
303
|
+
action_id:,
|
|
304
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
305
|
+
initial_user:,
|
|
306
|
+
confirm:,
|
|
307
|
+
focus_on_load:
|
|
308
|
+
}.compact
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def multi_users_select(action_id, placeholder:, initial_users: nil, max_selected_items: nil, confirm: nil,
|
|
312
|
+
focus_on_load: nil)
|
|
313
|
+
{
|
|
314
|
+
type: "multi_users_select",
|
|
315
|
+
action_id:,
|
|
316
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
317
|
+
initial_users:,
|
|
318
|
+
max_selected_items:,
|
|
319
|
+
confirm:,
|
|
320
|
+
focus_on_load:
|
|
321
|
+
}.compact
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def conversations_select(action_id, placeholder:, initial_conversation: nil, default_to_current_conversation: nil,
|
|
325
|
+
filter: nil, confirm: nil, focus_on_load: nil, response_url_enabled: nil)
|
|
326
|
+
{
|
|
327
|
+
type: "conversations_select",
|
|
328
|
+
action_id:,
|
|
329
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
330
|
+
initial_conversation:,
|
|
331
|
+
default_to_current_conversation:,
|
|
332
|
+
filter:,
|
|
333
|
+
confirm:,
|
|
334
|
+
focus_on_load:,
|
|
335
|
+
response_url_enabled:
|
|
336
|
+
}.compact
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def multi_conversations_select(action_id, placeholder:, initial_conversations: nil, max_selected_items: nil,
|
|
340
|
+
default_to_current_conversation: nil, filter: nil, confirm: nil, focus_on_load: nil)
|
|
341
|
+
{
|
|
342
|
+
type: "multi_conversations_select",
|
|
343
|
+
action_id:,
|
|
344
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
345
|
+
initial_conversations:,
|
|
346
|
+
max_selected_items:,
|
|
347
|
+
default_to_current_conversation:,
|
|
348
|
+
filter:,
|
|
349
|
+
confirm:,
|
|
350
|
+
focus_on_load:
|
|
351
|
+
}.compact
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def channels_select(action_id, placeholder:, initial_channel: nil, confirm: nil, focus_on_load: nil,
|
|
355
|
+
response_url_enabled: nil)
|
|
356
|
+
{
|
|
357
|
+
type: "channels_select",
|
|
358
|
+
action_id:,
|
|
359
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
360
|
+
initial_channel:,
|
|
361
|
+
confirm:,
|
|
362
|
+
focus_on_load:,
|
|
363
|
+
response_url_enabled:
|
|
364
|
+
}.compact
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def multi_channels_select(action_id, placeholder:, initial_channels: nil, max_selected_items: nil, confirm: nil,
|
|
368
|
+
focus_on_load: nil)
|
|
369
|
+
{
|
|
370
|
+
type: "multi_channels_select",
|
|
371
|
+
action_id:,
|
|
372
|
+
placeholder: { type: "plain_text", text: placeholder },
|
|
373
|
+
initial_channels:,
|
|
374
|
+
max_selected_items:,
|
|
375
|
+
confirm:,
|
|
376
|
+
focus_on_load:
|
|
377
|
+
}.compact
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def overflow(action_id, options, confirm: nil)
|
|
381
|
+
{
|
|
382
|
+
type: "overflow",
|
|
383
|
+
action_id:,
|
|
384
|
+
options:,
|
|
385
|
+
confirm:
|
|
386
|
+
}.compact
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def file_input(action_id, filetypes: nil, max_files: nil)
|
|
390
|
+
{
|
|
391
|
+
type: "file_input",
|
|
392
|
+
action_id:,
|
|
393
|
+
filetypes:,
|
|
394
|
+
max_files:
|
|
395
|
+
}.compact
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def rich_text_input(action_id, placeholder: nil, initial_value: nil, dispatch_action_config: nil,
|
|
399
|
+
focus_on_load: nil)
|
|
400
|
+
{
|
|
401
|
+
type: "rich_text_input",
|
|
402
|
+
action_id:,
|
|
403
|
+
placeholder: placeholder ? { type: "plain_text", text: placeholder } : nil,
|
|
404
|
+
initial_value:,
|
|
405
|
+
dispatch_action_config:,
|
|
406
|
+
focus_on_load:
|
|
407
|
+
}.compact
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def workflow_button(text, workflow:, style: nil, accessibility_label: nil)
|
|
411
|
+
{
|
|
412
|
+
type: "workflow_button",
|
|
413
|
+
text: { type: "plain_text", text: text },
|
|
414
|
+
workflow:,
|
|
415
|
+
style:,
|
|
416
|
+
accessibility_label:
|
|
417
|
+
}.compact
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def feedback_buttons(positive_text, positive_value, negative_text, negative_value, action_id: nil,
|
|
421
|
+
positive_accessibility_label: nil, negative_accessibility_label: nil)
|
|
422
|
+
{
|
|
423
|
+
type: "feedback_buttons",
|
|
424
|
+
action_id:,
|
|
425
|
+
positive_button: {
|
|
426
|
+
text: { type: "plain_text", text: positive_text },
|
|
427
|
+
value: positive_value,
|
|
428
|
+
accessibility_label: positive_accessibility_label
|
|
429
|
+
}.compact,
|
|
430
|
+
negative_button: {
|
|
431
|
+
text: { type: "plain_text", text: negative_text },
|
|
432
|
+
value: negative_value,
|
|
433
|
+
accessibility_label: negative_accessibility_label
|
|
434
|
+
}.compact
|
|
435
|
+
}.compact
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def icon_button(icon, text, action_id, value: nil, confirm: nil, accessibility_label: nil,
|
|
439
|
+
visible_to_user_ids: nil)
|
|
440
|
+
{
|
|
441
|
+
type: "icon_button",
|
|
442
|
+
icon:,
|
|
443
|
+
text: { type: "plain_text", text: text },
|
|
444
|
+
action_id:,
|
|
445
|
+
value:,
|
|
446
|
+
confirm:,
|
|
447
|
+
accessibility_label:,
|
|
448
|
+
visible_to_user_ids:
|
|
449
|
+
}.compact
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def option(text, value, description: nil, url: nil)
|
|
453
|
+
opt = {
|
|
454
|
+
text: { type: "plain_text", text: text },
|
|
455
|
+
value: value
|
|
456
|
+
}
|
|
457
|
+
opt[:description] = { type: "plain_text", text: description } if description
|
|
458
|
+
opt[:url] = url if url
|
|
459
|
+
opt
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def option_group(label, options)
|
|
463
|
+
{
|
|
464
|
+
label: { type: "plain_text", text: label },
|
|
465
|
+
options: options
|
|
466
|
+
}
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def confirm_dialog(title:, text:, confirm:, deny:, style: nil)
|
|
470
|
+
dialog = {
|
|
471
|
+
title: { type: "plain_text", text: title },
|
|
472
|
+
text: { type: "mrkdwn", text: text },
|
|
473
|
+
confirm: { type: "plain_text", text: confirm },
|
|
474
|
+
deny: { type: "plain_text", text: deny }
|
|
475
|
+
}
|
|
476
|
+
dialog[:style] = style if style
|
|
477
|
+
dialog
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def dispatch_action_config(*trigger_actions_on)
|
|
481
|
+
{
|
|
482
|
+
trigger_actions_on: trigger_actions_on
|
|
483
|
+
}
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def filter(include: nil, exclude_external_shared_channels: nil, exclude_bot_users: nil)
|
|
487
|
+
f = {}
|
|
488
|
+
f[:include] = include if include
|
|
489
|
+
f[:exclude_external_shared_channels] = exclude_external_shared_channels if exclude_external_shared_channels
|
|
490
|
+
f[:exclude_bot_users] = exclude_bot_users if exclude_bot_users
|
|
491
|
+
f
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def render(partial, locals: {})
|
|
495
|
+
if partial.respond_to?(:to_ary)
|
|
496
|
+
partial.flat_map do |item|
|
|
497
|
+
render_partial_for_item(item, locals)
|
|
498
|
+
end
|
|
499
|
+
else
|
|
500
|
+
render_partial_for_item(partial, locals)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
505
|
+
if @context.respond_to?(method)
|
|
506
|
+
@context.public_send(method, *args, **kwargs, &block)
|
|
507
|
+
else
|
|
508
|
+
super
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def respond_to_missing?(method, include_private = false)
|
|
513
|
+
@context.respond_to?(method, include_private) || super
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
private
|
|
517
|
+
|
|
518
|
+
def render_partial_for_item(item, locals)
|
|
519
|
+
partial_name = item.class.name.underscore
|
|
520
|
+
partial_path = "#{partial_name.pluralize}/#{partial_name}"
|
|
521
|
+
|
|
522
|
+
result = @context.render(
|
|
523
|
+
partial: partial_path,
|
|
524
|
+
locals: locals.merge(partial_name.to_sym => item),
|
|
525
|
+
formats: [:slack_blocks]
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
JSON.parse(result)["blocks"] || []
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slocks
|
|
4
|
+
class ModalBuilder < BlocksBuilder
|
|
5
|
+
attr_reader :modal_title, :submit_text, :close_text, :callback_id
|
|
6
|
+
|
|
7
|
+
def title(text)
|
|
8
|
+
@modal_title = text
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def submit(text)
|
|
12
|
+
@submit_text = text
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def close(text)
|
|
16
|
+
@close_text = text
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def callback(id)
|
|
20
|
+
@callback_id = id
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
modal = {
|
|
25
|
+
type: "modal",
|
|
26
|
+
title: { type: "plain_text", text: @modal_title },
|
|
27
|
+
blocks: @blocks,
|
|
28
|
+
callback_id: @callback_id
|
|
29
|
+
}.compact
|
|
30
|
+
modal[:submit] = { type: "plain_text", text: @submit_text } if @submit_text
|
|
31
|
+
modal[:close] = { type: "plain_text", text: @close_text } if @close_text
|
|
32
|
+
modal
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_view"
|
|
4
|
+
|
|
5
|
+
module Slocks
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
initializer "slocks.register_template_handler" do
|
|
8
|
+
ActiveSupport.on_load(:action_view) do
|
|
9
|
+
require "slocks/action_view_monkeys"
|
|
10
|
+
require "slocks/template_handler"
|
|
11
|
+
|
|
12
|
+
Mime::Type.register "application/json", :slack_message
|
|
13
|
+
Mime::Type.register "application/json", :slack_modal
|
|
14
|
+
|
|
15
|
+
ActionView::Template.register_template_handler(:slocks, Slocks::TemplateHandlerDispatcher)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slocks
|
|
4
|
+
class RichTextBuilder
|
|
5
|
+
attr_reader :elements
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@elements = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def section(&block)
|
|
12
|
+
section_builder = RichTextSectionBuilder.new
|
|
13
|
+
section_builder.instance_eval(&block) if block_given?
|
|
14
|
+
@elements << {
|
|
15
|
+
type: "rich_text_section",
|
|
16
|
+
elements: section_builder.elements
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def list(style:, &block)
|
|
21
|
+
list_builder = RichTextSectionBuilder.new
|
|
22
|
+
list_builder.instance_eval(&block) if block_given?
|
|
23
|
+
@elements << {
|
|
24
|
+
type: "rich_text_list",
|
|
25
|
+
style: style,
|
|
26
|
+
elements: list_builder.elements
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def preformatted(&block)
|
|
31
|
+
section_builder = RichTextSectionBuilder.new
|
|
32
|
+
section_builder.instance_eval(&block) if block_given?
|
|
33
|
+
@elements << {
|
|
34
|
+
type: "rich_text_preformatted",
|
|
35
|
+
elements: section_builder.elements
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def quote(&block)
|
|
40
|
+
section_builder = RichTextSectionBuilder.new
|
|
41
|
+
section_builder.instance_eval(&block) if block_given?
|
|
42
|
+
@elements << {
|
|
43
|
+
type: "rich_text_quote",
|
|
44
|
+
elements: section_builder.elements
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class RichTextSectionBuilder
|
|
50
|
+
attr_reader :elements
|
|
51
|
+
|
|
52
|
+
def initialize
|
|
53
|
+
@elements = []
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def text(text, bold: nil, italic: nil, strike: nil, code: nil)
|
|
57
|
+
element = { type: "text", text: text }
|
|
58
|
+
style = {}
|
|
59
|
+
style[:bold] = true if bold
|
|
60
|
+
style[:italic] = true if italic
|
|
61
|
+
style[:strike] = true if strike
|
|
62
|
+
style[:code] = true if code
|
|
63
|
+
element[:style] = style unless style.empty?
|
|
64
|
+
@elements << element
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def link(url, text: nil, unsafe: nil, style: nil)
|
|
68
|
+
element = { type: "link", url: url }
|
|
69
|
+
element[:text] = text if text
|
|
70
|
+
element[:unsafe] = unsafe if unsafe
|
|
71
|
+
element[:style] = style if style
|
|
72
|
+
@elements << element
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def emoji(name)
|
|
76
|
+
@elements << { type: "emoji", name: name }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def channel(channel_id)
|
|
80
|
+
@elements << { type: "channel", channel_id: channel_id }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def user(user_id)
|
|
84
|
+
@elements << { type: "user", user_id: user_id }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def usergroup(usergroup_id)
|
|
88
|
+
@elements << { type: "usergroup", usergroup_id: usergroup_id }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def date(timestamp, format:, fallback: nil, link: nil)
|
|
92
|
+
element = { type: "date", timestamp: timestamp, format: format }
|
|
93
|
+
element[:fallback] = fallback if fallback
|
|
94
|
+
element[:link] = link if link
|
|
95
|
+
@elements << element
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def broadcast(range:)
|
|
99
|
+
@elements << { type: "broadcast", range: range }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/class/attribute"
|
|
4
|
+
|
|
5
|
+
module Slocks
|
|
6
|
+
class TemplateHandlerDispatcher
|
|
7
|
+
def self.call(template, source = nil)
|
|
8
|
+
case template.format&.to_sym
|
|
9
|
+
when :slack_modal
|
|
10
|
+
ModalTemplateHandler.call(template, source)
|
|
11
|
+
else
|
|
12
|
+
BlocksTemplateHandler.call(template, source)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.default_format = :slack_message
|
|
17
|
+
def self.handles_encoding? = true
|
|
18
|
+
|
|
19
|
+
def self.translate_location(spot, backtrace_location, source)
|
|
20
|
+
BlocksTemplateHandler.translate_location(spot, backtrace_location, source)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class BlocksTemplateHandler
|
|
25
|
+
class << self
|
|
26
|
+
def default_format = :slack_message
|
|
27
|
+
|
|
28
|
+
def preamble = <<~EOP
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@__slocks_builder ||= Slocks::BlocksBuilder.new(self)
|
|
32
|
+
def method_missing(m, *a, **k, &b)
|
|
33
|
+
@__slocks_builder.respond_to?(m) ? @__slocks_builder.public_send(m, *a, **k, &b) : super
|
|
34
|
+
end
|
|
35
|
+
EOP
|
|
36
|
+
|
|
37
|
+
def call(template, source = nil)
|
|
38
|
+
source ||= template.source
|
|
39
|
+
<<~EOS
|
|
40
|
+
#{preamble}
|
|
41
|
+
#{source}
|
|
42
|
+
{ blocks: @__slocks_builder.blocks }
|
|
43
|
+
EOS
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def translate_location(spot, _backtrace_location, _source)
|
|
47
|
+
# eat your heart out rails-core
|
|
48
|
+
# this sucks but seems to work
|
|
49
|
+
plen = preamble.lines.size + 2
|
|
50
|
+
spot[:first_lineno] -= plen
|
|
51
|
+
spot[:last_lineno] -= plen
|
|
52
|
+
spot[:script_lines] = spot[:script_lines][plen..-4]
|
|
53
|
+
spot
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handles_encoding? = true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class ModalTemplateHandler < BlocksTemplateHandler
|
|
61
|
+
class << self
|
|
62
|
+
def default_format = :slack_modal
|
|
63
|
+
|
|
64
|
+
def call(template, source = nil)
|
|
65
|
+
source ||= template.source
|
|
66
|
+
<<~EOS
|
|
67
|
+
@__slocks_modal_builder ||= Slocks::ModalBuilder.new(self)
|
|
68
|
+
def method_missing(m, *a, **k, &b);
|
|
69
|
+
@__slocks_modal_builder.respond_to?(m) ? @__slocks_modal_builder.public_send(m, *a, **k, &b) : super;
|
|
70
|
+
end
|
|
71
|
+
#{source}
|
|
72
|
+
@__slocks_modal_builder.to_h
|
|
73
|
+
EOS
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
TemplateHandler = BlocksTemplateHandler
|
|
79
|
+
end
|
data/lib/slocks.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "slocks/version"
|
|
4
|
+
require_relative "slocks/rich_text_builder"
|
|
5
|
+
require_relative "slocks/blocks_builder"
|
|
6
|
+
require_relative "slocks/modal_builder"
|
|
7
|
+
require_relative "slocks/template_handler"
|
|
8
|
+
require_relative "slocks/railtie" if defined?(Rails::Railtie)
|
|
9
|
+
|
|
10
|
+
module Slocks
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: slocks
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- 24c02
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionview
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activesupport
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
description: A Rails template handler that provides a clean DSL for building Slack
|
|
41
|
+
Block Kit blocks and modals
|
|
42
|
+
email:
|
|
43
|
+
- slocks@noras.email
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- README.md
|
|
49
|
+
- Rakefile
|
|
50
|
+
- lib/slocks.rb
|
|
51
|
+
- lib/slocks/action_view_monkeys.rb
|
|
52
|
+
- lib/slocks/blocks_builder.rb
|
|
53
|
+
- lib/slocks/modal_builder.rb
|
|
54
|
+
- lib/slocks/railtie.rb
|
|
55
|
+
- lib/slocks/rich_text_builder.rb
|
|
56
|
+
- lib/slocks/template_handler.rb
|
|
57
|
+
- lib/slocks/version.rb
|
|
58
|
+
homepage: https://github.com/24c02/slocks
|
|
59
|
+
licenses: []
|
|
60
|
+
metadata:
|
|
61
|
+
homepage_uri: https://github.com/24c02/slocks
|
|
62
|
+
source_code_uri: https://github.com/24c02/slocks
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: 3.2.0
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.9
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Rails template handler for Slack Block Kit
|
|
80
|
+
test_files: []
|