block-kit 1.0.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/.rspec +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +8 -0
- data/lib/block_kit/base.rb +190 -0
- data/lib/block_kit/blocks.rb +50 -0
- data/lib/block_kit/composition/confirmation_dialog.rb +48 -0
- data/lib/block_kit/composition/conversation_filter.rb +42 -0
- data/lib/block_kit/composition/dispatch_action_config.rb +33 -0
- data/lib/block_kit/composition/input_parameter.rb +19 -0
- data/lib/block_kit/composition/mrkdwn.rb +15 -0
- data/lib/block_kit/composition/option.rb +41 -0
- data/lib/block_kit/composition/option_group.rb +27 -0
- data/lib/block_kit/composition/overflow_option.rb +21 -0
- data/lib/block_kit/composition/plain_text.rb +15 -0
- data/lib/block_kit/composition/slack_file.rb +36 -0
- data/lib/block_kit/composition/text.rb +29 -0
- data/lib/block_kit/composition/trigger.rb +33 -0
- data/lib/block_kit/composition/workflow.rb +19 -0
- data/lib/block_kit/composition.rb +19 -0
- data/lib/block_kit/concerns/confirmable.rb +21 -0
- data/lib/block_kit/concerns/conversation_selection.rb +26 -0
- data/lib/block_kit/concerns/dispatchable.rb +24 -0
- data/lib/block_kit/concerns/dsl_generation.rb +76 -0
- data/lib/block_kit/concerns/external.rb +19 -0
- data/lib/block_kit/concerns/focusable_on_load.rb +17 -0
- data/lib/block_kit/concerns/has_initial_option.rb +23 -0
- data/lib/block_kit/concerns/has_initial_options.rb +23 -0
- data/lib/block_kit/concerns/has_option_groups.rb +37 -0
- data/lib/block_kit/concerns/has_options.rb +28 -0
- data/lib/block_kit/concerns/has_placeholder.rb +21 -0
- data/lib/block_kit/concerns/has_rich_text_elements.rb +83 -0
- data/lib/block_kit/concerns/plain_text_emoji_assignment.rb +39 -0
- data/lib/block_kit/concerns.rb +18 -0
- data/lib/block_kit/elements/base.rb +23 -0
- data/lib/block_kit/elements/base_button.rb +45 -0
- data/lib/block_kit/elements/button.rb +27 -0
- data/lib/block_kit/elements/channels_select.rb +22 -0
- data/lib/block_kit/elements/checkboxes.rb +14 -0
- data/lib/block_kit/elements/conversations_select.rb +24 -0
- data/lib/block_kit/elements/date_picker.rb +21 -0
- data/lib/block_kit/elements/datetime_picker.rb +21 -0
- data/lib/block_kit/elements/email_text_input.rb +24 -0
- data/lib/block_kit/elements/external_select.rb +21 -0
- data/lib/block_kit/elements/file_input.rb +33 -0
- data/lib/block_kit/elements/image.rb +53 -0
- data/lib/block_kit/elements/multi_channels_select.rb +31 -0
- data/lib/block_kit/elements/multi_conversations_select.rb +33 -0
- data/lib/block_kit/elements/multi_external_select.rb +21 -0
- data/lib/block_kit/elements/multi_select.rb +21 -0
- data/lib/block_kit/elements/multi_static_select.rb +12 -0
- data/lib/block_kit/elements/multi_users_select.rb +31 -0
- data/lib/block_kit/elements/number_input.rb +52 -0
- data/lib/block_kit/elements/overflow.rb +23 -0
- data/lib/block_kit/elements/plain_text_input.rb +66 -0
- data/lib/block_kit/elements/radio_buttons.rb +14 -0
- data/lib/block_kit/elements/rich_text_input.rb +26 -0
- data/lib/block_kit/elements/select.rb +18 -0
- data/lib/block_kit/elements/static_select.rb +12 -0
- data/lib/block_kit/elements/time_picker.rb +43 -0
- data/lib/block_kit/elements/url_text_input.rb +24 -0
- data/lib/block_kit/elements/users_select.rb +17 -0
- data/lib/block_kit/elements/workflow_button.rb +18 -0
- data/lib/block_kit/elements.rb +36 -0
- data/lib/block_kit/fixers/associated.rb +27 -0
- data/lib/block_kit/fixers/base.rb +30 -0
- data/lib/block_kit/fixers/null_value.rb +42 -0
- data/lib/block_kit/fixers/truncate.rb +35 -0
- data/lib/block_kit/fixers.rb +11 -0
- data/lib/block_kit/layout/actions.rb +84 -0
- data/lib/block_kit/layout/base.rb +23 -0
- data/lib/block_kit/layout/context.rb +48 -0
- data/lib/block_kit/layout/divider.rb +9 -0
- data/lib/block_kit/layout/file.rb +19 -0
- data/lib/block_kit/layout/header.rb +22 -0
- data/lib/block_kit/layout/image.rb +61 -0
- data/lib/block_kit/layout/input.rb +119 -0
- data/lib/block_kit/layout/markdown.rb +19 -0
- data/lib/block_kit/layout/rich_text/elements/broadcast.rb +22 -0
- data/lib/block_kit/layout/rich_text/elements/channel.rb +21 -0
- data/lib/block_kit/layout/rich_text/elements/color.rb +20 -0
- data/lib/block_kit/layout/rich_text/elements/date.rb +34 -0
- data/lib/block_kit/layout/rich_text/elements/emoji.rb +27 -0
- data/lib/block_kit/layout/rich_text/elements/link.rb +28 -0
- data/lib/block_kit/layout/rich_text/elements/mention_style.rb +27 -0
- data/lib/block_kit/layout/rich_text/elements/text.rb +23 -0
- data/lib/block_kit/layout/rich_text/elements/text_style.rb +23 -0
- data/lib/block_kit/layout/rich_text/elements/user.rb +21 -0
- data/lib/block_kit/layout/rich_text/elements/usergroup.rb +21 -0
- data/lib/block_kit/layout/rich_text/elements.rb +35 -0
- data/lib/block_kit/layout/rich_text/list.rb +41 -0
- data/lib/block_kit/layout/rich_text/preformatted.rb +19 -0
- data/lib/block_kit/layout/rich_text/quote.rb +19 -0
- data/lib/block_kit/layout/rich_text/section.rb +11 -0
- data/lib/block_kit/layout/rich_text.rb +53 -0
- data/lib/block_kit/layout/section.rb +152 -0
- data/lib/block_kit/layout/video.rb +71 -0
- data/lib/block_kit/layout.rb +35 -0
- data/lib/block_kit/surfaces/base.rb +173 -0
- data/lib/block_kit/surfaces/home.rb +40 -0
- data/lib/block_kit/surfaces/message.rb +140 -0
- data/lib/block_kit/surfaces/modal.rb +74 -0
- data/lib/block_kit/surfaces.rb +11 -0
- data/lib/block_kit/typed_array.rb +114 -0
- data/lib/block_kit/typed_set.rb +78 -0
- data/lib/block_kit/types/array.rb +45 -0
- data/lib/block_kit/types/blocks.rb +37 -0
- data/lib/block_kit/types/generic.rb +45 -0
- data/lib/block_kit/types/option.rb +48 -0
- data/lib/block_kit/types/set.rb +35 -0
- data/lib/block_kit/types/text.rb +80 -0
- data/lib/block_kit/types.rb +17 -0
- data/lib/block_kit/validators/array_inclusion_validator.rb +55 -0
- data/lib/block_kit/validators/associated_validator.rb +61 -0
- data/lib/block_kit/validators.rb +8 -0
- data/lib/block_kit/version.rb +5 -0
- data/lib/block_kit.rb +38 -0
- metadata +190 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockKit
|
|
4
|
+
module Layout
|
|
5
|
+
class RichText < Base
|
|
6
|
+
self.type = :rich_text
|
|
7
|
+
|
|
8
|
+
autoload :Elements, "block_kit/layout/rich_text/elements"
|
|
9
|
+
autoload :List, "block_kit/layout/rich_text/list"
|
|
10
|
+
autoload :Preformatted, "block_kit/layout/rich_text/preformatted"
|
|
11
|
+
autoload :Quote, "block_kit/layout/rich_text/quote"
|
|
12
|
+
autoload :Section, "block_kit/layout/rich_text/section"
|
|
13
|
+
|
|
14
|
+
SUPPORTED_ELEMENTS = [
|
|
15
|
+
RichText::List,
|
|
16
|
+
RichText::Preformatted,
|
|
17
|
+
RichText::Quote,
|
|
18
|
+
RichText::Section
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
attribute :elements, Types::Array.of(Types::Blocks.new(*SUPPORTED_ELEMENTS))
|
|
22
|
+
validates :elements, presence: true, "block_kit/validators/associated": true
|
|
23
|
+
fixes :elements, associated: true
|
|
24
|
+
|
|
25
|
+
dsl_method :elements, as: :list, type: RichText::List
|
|
26
|
+
dsl_method :elements, as: :preformatted, type: RichText::Preformatted
|
|
27
|
+
dsl_method :elements, as: :quote, type: RichText::Quote
|
|
28
|
+
dsl_method :elements, as: :section, type: RichText::Section
|
|
29
|
+
|
|
30
|
+
alias_method :rich_text_list, :list
|
|
31
|
+
alias_method :rich_text_preformatted, :preformatted
|
|
32
|
+
alias_method :rich_text_quote, :quote
|
|
33
|
+
alias_method :rich_text_section, :section
|
|
34
|
+
|
|
35
|
+
def initialize(attributes = {})
|
|
36
|
+
attributes = attributes.with_indifferent_access
|
|
37
|
+
attributes[:elements] ||= []
|
|
38
|
+
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def append(element)
|
|
43
|
+
elements << element
|
|
44
|
+
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def as_json(*)
|
|
49
|
+
super.merge(elements: elements&.map(&:as_json)).compact
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockKit
|
|
4
|
+
module Layout
|
|
5
|
+
class Section < Base
|
|
6
|
+
self.type = :section
|
|
7
|
+
|
|
8
|
+
MAX_TEXT_LENGTH = 3000
|
|
9
|
+
MAX_FIELDS = 10
|
|
10
|
+
MAX_FIELD_TEXT_LENGTH = 2000
|
|
11
|
+
SUPPORTED_ELEMENTS = [
|
|
12
|
+
Elements::Button,
|
|
13
|
+
Elements::ChannelsSelect,
|
|
14
|
+
Elements::Checkboxes,
|
|
15
|
+
Elements::ConversationsSelect,
|
|
16
|
+
Elements::DatePicker,
|
|
17
|
+
Elements::ExternalSelect,
|
|
18
|
+
Elements::Image,
|
|
19
|
+
Elements::MultiChannelsSelect,
|
|
20
|
+
Elements::MultiConversationsSelect,
|
|
21
|
+
Elements::MultiExternalSelect,
|
|
22
|
+
Elements::MultiStaticSelect,
|
|
23
|
+
Elements::MultiUsersSelect,
|
|
24
|
+
Elements::Overflow,
|
|
25
|
+
Elements::RadioButtons,
|
|
26
|
+
Elements::StaticSelect,
|
|
27
|
+
Elements::TimePicker,
|
|
28
|
+
Elements::UsersSelect,
|
|
29
|
+
Elements::WorkflowButton
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
attribute :text, Types::Generic.of_type(Composition::Text)
|
|
33
|
+
attribute :fields, Types::Array.of(Composition::Text)
|
|
34
|
+
attribute :accessory, Types::Blocks.new(*SUPPORTED_ELEMENTS)
|
|
35
|
+
attribute :expand, :boolean
|
|
36
|
+
|
|
37
|
+
validates :text, length: {maximum: MAX_TEXT_LENGTH}, presence: {allow_nil: true}
|
|
38
|
+
fixes :text, truncate: {maximum: MAX_TEXT_LENGTH}
|
|
39
|
+
|
|
40
|
+
validates :fields, length: {maximum: MAX_FIELDS, message: "is too long (maximum is %{count} fields)"}, presence: {allow_nil: true}
|
|
41
|
+
fixes :fields, truncate: {maximum: MAX_FIELDS}
|
|
42
|
+
fix :truncate_long_fields
|
|
43
|
+
fix :remove_blank_fields
|
|
44
|
+
|
|
45
|
+
validates :accessory, "block_kit/validators/associated": true
|
|
46
|
+
fixes :accessory, associated: true
|
|
47
|
+
|
|
48
|
+
validate :has_text_or_fields
|
|
49
|
+
validate :fields_are_valid
|
|
50
|
+
|
|
51
|
+
def expand?
|
|
52
|
+
!!expand
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
dsl_method :text, as: :mrkdwn, type: Composition::Mrkdwn, required_fields: [:text], yields: false
|
|
56
|
+
dsl_method :text, as: :plain_text, type: Composition::PlainText, required_fields: [:text], yields: false
|
|
57
|
+
dsl_method :fields, as: :mrkdwn_field, type: Composition::Mrkdwn, required_fields: [:text], yields: false
|
|
58
|
+
dsl_method :fields, as: :plain_text_field, type: Composition::PlainText, required_fields: [:text], yields: false
|
|
59
|
+
dsl_method :accessory, as: :button, type: Elements::Button, required_fields: [:text]
|
|
60
|
+
dsl_method :accessory, as: :channels_select, type: Elements::ChannelsSelect
|
|
61
|
+
dsl_method :accessory, as: :checkboxes, type: Elements::Checkboxes
|
|
62
|
+
dsl_method :accessory, as: :conversations_select, type: Elements::ConversationsSelect
|
|
63
|
+
dsl_method :accessory, as: :datepicker, type: Elements::DatePicker
|
|
64
|
+
dsl_method :accessory, as: :external_select, type: Elements::ExternalSelect
|
|
65
|
+
dsl_method :accessory, as: :multi_channels_select, type: Elements::MultiChannelsSelect
|
|
66
|
+
dsl_method :accessory, as: :multi_conversations_select, type: Elements::MultiConversationsSelect
|
|
67
|
+
dsl_method :accessory, as: :multi_external_select, type: Elements::MultiExternalSelect
|
|
68
|
+
dsl_method :accessory, as: :multi_static_select, type: Elements::MultiStaticSelect, mutually_exclusive_fields: [:options, :option_groups]
|
|
69
|
+
dsl_method :accessory, as: :multi_users_select, type: Elements::MultiUsersSelect
|
|
70
|
+
dsl_method :accessory, as: :overflow, type: Elements::Overflow
|
|
71
|
+
dsl_method :accessory, as: :radio_buttons, type: Elements::RadioButtons
|
|
72
|
+
dsl_method :accessory, as: :static_select, type: Elements::StaticSelect, mutually_exclusive_fields: [:options, :option_groups]
|
|
73
|
+
dsl_method :accessory, as: :timepicker, type: Elements::TimePicker
|
|
74
|
+
dsl_method :accessory, as: :users_select, type: Elements::UsersSelect
|
|
75
|
+
dsl_method :accessory, as: :workflow_button, type: Elements::WorkflowButton, required_fields: [:text]
|
|
76
|
+
|
|
77
|
+
alias_method :channel_select, :channels_select
|
|
78
|
+
alias_method :conversation_select, :conversations_select
|
|
79
|
+
alias_method :date_picker, :datepicker
|
|
80
|
+
alias_method :multi_channel_select, :multi_channels_select
|
|
81
|
+
alias_method :multi_conversation_select, :multi_conversations_select
|
|
82
|
+
alias_method :multi_user_select, :multi_users_select
|
|
83
|
+
alias_method :overflow_menu, :overflow
|
|
84
|
+
alias_method :time_picker, :timepicker
|
|
85
|
+
alias_method :user_select, :users_select
|
|
86
|
+
|
|
87
|
+
def field(text:, type: :mrkdwn, verbatim: nil, emoji: nil)
|
|
88
|
+
case type.to_sym
|
|
89
|
+
when :mrkdwn
|
|
90
|
+
mrkdwn_field(text: text, verbatim: verbatim)
|
|
91
|
+
when :plain_text
|
|
92
|
+
plain_text_field(text: text, emoji: emoji)
|
|
93
|
+
else
|
|
94
|
+
raise ArgumentError, "Invalid field type: #{type} (must be mrkdwn or plain_text)"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def image(alt_text:, image_url: nil, slack_file: nil)
|
|
99
|
+
if (image_url.nil? && slack_file.nil?) || (image_url && slack_file)
|
|
100
|
+
raise ArgumentError, "Must provide either image_url or slack_file, but not both."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
self.accessory = Elements::Image.new(alt_text: alt_text, image_url: image_url, slack_file: slack_file)
|
|
104
|
+
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def as_json(*)
|
|
109
|
+
super.merge(
|
|
110
|
+
text: text&.as_json,
|
|
111
|
+
fields: fields&.map(&:as_json),
|
|
112
|
+
accessory: accessory&.as_json,
|
|
113
|
+
expand: expand
|
|
114
|
+
).compact
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# Pure ActiveModel validations get a little too complicated with this combo, as
|
|
120
|
+
# we need to not only make sure at least one of these two is present, but that
|
|
121
|
+
# neither is _blank_. It's easier to just do this in a custom validation and
|
|
122
|
+
# keep the individual validations focused on presence while allowing nil.
|
|
123
|
+
def has_text_or_fields
|
|
124
|
+
if text.blank? && fields.blank?
|
|
125
|
+
errors.add(:base, "must have either text or fields")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def fields_are_valid
|
|
130
|
+
fields&.each_with_index do |field, i|
|
|
131
|
+
if field.length > MAX_FIELD_TEXT_LENGTH
|
|
132
|
+
errors.add("fields[#{i}]", "is invalid: text is too long (maximum is #{MAX_FIELD_TEXT_LENGTH} characters)")
|
|
133
|
+
elsif field.blank?
|
|
134
|
+
errors.add("fields[#{i}]", "is invalid: text can't be blank")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def truncate_long_fields
|
|
140
|
+
Array(fields).select { |field| field.length > MAX_FIELD_TEXT_LENGTH }.each do |field|
|
|
141
|
+
field.text = field.text.truncate(MAX_FIELD_TEXT_LENGTH)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def remove_blank_fields
|
|
146
|
+
Array(fields).select(&:blank?).each do |field|
|
|
147
|
+
fields.delete(field)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module BlockKit
|
|
6
|
+
module Layout
|
|
7
|
+
class Video < Base
|
|
8
|
+
self.type = :video
|
|
9
|
+
|
|
10
|
+
MAX_ALT_TEXT_LENGTH = 2000
|
|
11
|
+
MAX_AUTHOR_NAME_LENGTH = 50
|
|
12
|
+
MAX_DESCRIPTION_LENGTH = 200
|
|
13
|
+
MAX_URL_LENGTH = 3000
|
|
14
|
+
MAX_PROVIDER_NAME_LENGTH = 50
|
|
15
|
+
MAX_TITLE_LENGTH = 200
|
|
16
|
+
|
|
17
|
+
attribute :alt_text, :string
|
|
18
|
+
attribute :author_name, :string
|
|
19
|
+
plain_text_attribute :description
|
|
20
|
+
attribute :provider_icon_url, :string
|
|
21
|
+
attribute :provider_name, :string
|
|
22
|
+
plain_text_attribute :title
|
|
23
|
+
attribute :title_url, :string
|
|
24
|
+
attribute :thumbnail_url, :string
|
|
25
|
+
attribute :video_url, :string
|
|
26
|
+
|
|
27
|
+
include Concerns::PlainTextEmojiAssignment.new(:description, :title)
|
|
28
|
+
|
|
29
|
+
validates :alt_text, presence: true, length: {maximum: MAX_ALT_TEXT_LENGTH}
|
|
30
|
+
fixes :alt_text, truncate: {maximum: MAX_ALT_TEXT_LENGTH}
|
|
31
|
+
|
|
32
|
+
validates :author_name, presence: true, length: {maximum: MAX_AUTHOR_NAME_LENGTH}, allow_nil: true
|
|
33
|
+
fixes :author_name, truncate: {maximum: MAX_AUTHOR_NAME_LENGTH}, null_value: {error_types: [:blank]}
|
|
34
|
+
|
|
35
|
+
validates :description, presence: true, length: {maximum: MAX_DESCRIPTION_LENGTH}, allow_nil: true
|
|
36
|
+
fixes :description, truncate: {maximum: MAX_DESCRIPTION_LENGTH}, null_value: {error_types: [:blank]}
|
|
37
|
+
|
|
38
|
+
validates :provider_icon_url, presence: true, length: {maximum: MAX_URL_LENGTH}, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[https http]), message: "is not a valid URI"}, allow_nil: true
|
|
39
|
+
fixes :provider_icon_url, truncate: {maximum: MAX_URL_LENGTH, dangerous: true, omission: ""}, null_value: {error_types: [:blank]}
|
|
40
|
+
|
|
41
|
+
validates :provider_name, presence: true, length: {maximum: MAX_PROVIDER_NAME_LENGTH}, allow_nil: true
|
|
42
|
+
fixes :provider_name, truncate: {maximum: MAX_PROVIDER_NAME_LENGTH}, null_value: {error_types: [:blank]}
|
|
43
|
+
|
|
44
|
+
validates :title, presence: true, length: {maximum: MAX_TITLE_LENGTH}
|
|
45
|
+
fixes :title, truncate: {maximum: MAX_TITLE_LENGTH}
|
|
46
|
+
|
|
47
|
+
validates :title_url, presence: true, length: {maximum: MAX_URL_LENGTH}, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[https]), message: "is not a valid HTTPS URI"}, allow_nil: true
|
|
48
|
+
fixes :title_url, truncate: {maximum: MAX_URL_LENGTH, dangerous: true, omission: ""}, null_value: {error_types: [:blank]}
|
|
49
|
+
|
|
50
|
+
validates :thumbnail_url, presence: true, length: {maximum: MAX_URL_LENGTH}, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[https http]), message: "is not a valid URI"}
|
|
51
|
+
fixes :thumbnail_url, truncate: {maximum: MAX_URL_LENGTH, dangerous: true, omission: ""}
|
|
52
|
+
|
|
53
|
+
validates :video_url, presence: true, length: {maximum: MAX_URL_LENGTH}, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[https]), message: "is not a valid HTTPS URI"}
|
|
54
|
+
fixes :video_url, truncate: {maximum: MAX_URL_LENGTH, dangerous: true, omission: ""}
|
|
55
|
+
|
|
56
|
+
def as_json(*)
|
|
57
|
+
super.merge(
|
|
58
|
+
alt_text: alt_text,
|
|
59
|
+
author_name: author_name,
|
|
60
|
+
description: description&.as_json,
|
|
61
|
+
provider_icon_url: provider_icon_url,
|
|
62
|
+
provider_name: provider_name,
|
|
63
|
+
title: title&.as_json,
|
|
64
|
+
title_url: title_url,
|
|
65
|
+
thumbnail_url: thumbnail_url,
|
|
66
|
+
video_url: video_url
|
|
67
|
+
).compact
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockKit
|
|
4
|
+
module Layout
|
|
5
|
+
autoload :Base, "block_kit/layout/base"
|
|
6
|
+
|
|
7
|
+
autoload :Actions, "block_kit/layout/actions"
|
|
8
|
+
autoload :Context, "block_kit/layout/context"
|
|
9
|
+
autoload :Divider, "block_kit/layout/divider"
|
|
10
|
+
autoload :File, "block_kit/layout/file"
|
|
11
|
+
autoload :Header, "block_kit/layout/header"
|
|
12
|
+
autoload :Image, "block_kit/layout/image"
|
|
13
|
+
autoload :Input, "block_kit/layout/input"
|
|
14
|
+
autoload :Markdown, "block_kit/layout/markdown"
|
|
15
|
+
autoload :RichText, "block_kit/layout/rich_text"
|
|
16
|
+
autoload :Section, "block_kit/layout/section"
|
|
17
|
+
autoload :Video, "block_kit/layout/video"
|
|
18
|
+
|
|
19
|
+
def self.all
|
|
20
|
+
@all ||= [
|
|
21
|
+
Actions,
|
|
22
|
+
Context,
|
|
23
|
+
Divider,
|
|
24
|
+
File,
|
|
25
|
+
Header,
|
|
26
|
+
Image,
|
|
27
|
+
Input,
|
|
28
|
+
Markdown,
|
|
29
|
+
RichText,
|
|
30
|
+
Section,
|
|
31
|
+
Video
|
|
32
|
+
].freeze
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module BlockKit
|
|
6
|
+
module Surfaces
|
|
7
|
+
class Base < BlockKit::Base
|
|
8
|
+
self.type = :surface
|
|
9
|
+
|
|
10
|
+
MAX_BLOCKS = 100
|
|
11
|
+
SUPPORTED_BLOCKS = [
|
|
12
|
+
Layout::Actions,
|
|
13
|
+
Layout::Context,
|
|
14
|
+
Layout::Divider,
|
|
15
|
+
Layout::Header,
|
|
16
|
+
Layout::Image,
|
|
17
|
+
Layout::Input,
|
|
18
|
+
Layout::RichText,
|
|
19
|
+
Layout::Section,
|
|
20
|
+
Layout::Video
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
attribute :blocks, Types::Array.of(Types::Blocks.new(*SUPPORTED_BLOCKS))
|
|
24
|
+
attribute :private_metadata, :string
|
|
25
|
+
attribute :callback_id, :string
|
|
26
|
+
attribute :external_id, :string
|
|
27
|
+
|
|
28
|
+
validates :blocks, presence: true, length: {maximum: MAX_BLOCKS, message: "is too long (maximum is %{count} blocks)"}, "block_kit/validators/associated": true
|
|
29
|
+
validate :no_unsupported_elements
|
|
30
|
+
fix :remove_unsupported_elements, dangerous: true
|
|
31
|
+
fixes :blocks, truncate: {maximum: MAX_BLOCKS, dangerous: true}, associated: true
|
|
32
|
+
|
|
33
|
+
validates :private_metadata, length: {maximum: 3000}, allow_nil: true
|
|
34
|
+
validates :callback_id, length: {maximum: 255}, allow_nil: true
|
|
35
|
+
validates :external_id, length: {maximum: 255}, allow_nil: true
|
|
36
|
+
validate :only_one_element_focuses_on_load
|
|
37
|
+
fix :unset_focus_on_load_on_all_elements
|
|
38
|
+
|
|
39
|
+
dsl_method :blocks, as: :actions, type: Layout::Actions
|
|
40
|
+
dsl_method :blocks, as: :context, type: Layout::Context
|
|
41
|
+
dsl_method :blocks, as: :divider, type: Layout::Divider, yields: false
|
|
42
|
+
dsl_method :blocks, as: :header, type: Layout::Header, required_fields: [:text], yields: false
|
|
43
|
+
dsl_method :blocks, as: :input, type: Layout::Input
|
|
44
|
+
dsl_method :blocks, as: :rich_text, type: Layout::RichText
|
|
45
|
+
dsl_method :blocks, as: :section, type: Layout::Section
|
|
46
|
+
dsl_method :blocks, as: :video, type: Layout::Video, required_fields: [:alt_text, :title, :thumbnail_url, :video_url], yields: false
|
|
47
|
+
|
|
48
|
+
def initialize(attributes = {})
|
|
49
|
+
raise NotImplementedError, "#{self.class} is an abstract class and can't be instantiated." if instance_of?(Base)
|
|
50
|
+
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def image(alt_text:, image_url: nil, slack_file: nil, title: nil, emoji: nil, block_id: nil)
|
|
55
|
+
if (image_url.nil? && slack_file.nil?) || (image_url && slack_file)
|
|
56
|
+
raise ArgumentError, "Must provide either image_url or slack_file, but not both."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
append(Layout::Image.new(image_url: image_url, slack_file: slack_file, alt_text: alt_text, title: title, block_id: block_id, emoji: emoji))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Overridden to return `self`, allowing chaining.
|
|
63
|
+
def append(block)
|
|
64
|
+
blocks << block
|
|
65
|
+
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def as_json(*)
|
|
70
|
+
super.merge(
|
|
71
|
+
blocks: blocks&.map(&:as_json),
|
|
72
|
+
private_metadata: private_metadata,
|
|
73
|
+
callback_id: callback_id,
|
|
74
|
+
external_id: external_id
|
|
75
|
+
).compact
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def no_unsupported_elements
|
|
81
|
+
unsupported_elements = unsupported_elements_by_path
|
|
82
|
+
return if unsupported_elements.empty?
|
|
83
|
+
|
|
84
|
+
errors.add(:blocks, "contains unsupported elements")
|
|
85
|
+
|
|
86
|
+
unsupported_elements.each do |path, element|
|
|
87
|
+
errors.add(path, "is invalid: #{element.class.type} is not a supported element for this surface")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Crawls through deeply nested blocks, looking for blocks that impelement the
|
|
92
|
+
# BlockKit::Concerns::FocusableOnLoad concern, and ensures that only one of them
|
|
93
|
+
# has `focus_on_load` set to true.
|
|
94
|
+
def only_one_element_focuses_on_load
|
|
95
|
+
focusable_blocks = focused_blocks_by_path
|
|
96
|
+
|
|
97
|
+
if focusable_blocks.size > 1
|
|
98
|
+
errors.add(:blocks, "can't have more than one element with focus_on_load set to true")
|
|
99
|
+
|
|
100
|
+
focusable_blocks.each do |path, element|
|
|
101
|
+
errors.add(path, "is invalid: can't set focus_on_load when other elements have set focus_on_load")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def unsupported_elements_by_path
|
|
107
|
+
unsupported_elements = {}
|
|
108
|
+
|
|
109
|
+
blocks.each_with_index do |block, index|
|
|
110
|
+
# Context block elements are globally supported, so we don't need to check them.
|
|
111
|
+
case block
|
|
112
|
+
when Layout::Actions
|
|
113
|
+
block.elements.each_with_index do |element, element_index|
|
|
114
|
+
unless self.class::SUPPORTED_ELEMENTS.include?(element.class)
|
|
115
|
+
unsupported_elements["blocks[#{index}].elements[#{element_index}]"] = element
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
when Layout::Input
|
|
119
|
+
if block.element.present? && !self.class::SUPPORTED_ELEMENTS.include?(block.element.class)
|
|
120
|
+
unsupported_elements["blocks[#{index}].element"] = block.element
|
|
121
|
+
end
|
|
122
|
+
when Layout::Section
|
|
123
|
+
if block.accessory.present? && !self.class::SUPPORTED_ELEMENTS.include?(block.accessory.class)
|
|
124
|
+
unsupported_elements["blocks[#{index}].accessory"] = block.accessory
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
unsupported_elements
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def focused_blocks_by_path
|
|
133
|
+
focused_blocks = {}
|
|
134
|
+
|
|
135
|
+
blocks.each_with_index do |block, i|
|
|
136
|
+
case block
|
|
137
|
+
when Layout::Actions
|
|
138
|
+
block.elements.each_with_index do |element, ei|
|
|
139
|
+
if element.respond_to?(:focus_on_load) && element.focus_on_load
|
|
140
|
+
focused_blocks["blocks[#{i}].elements[#{ei}]"] = element
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
when Layout::Input
|
|
144
|
+
focused_blocks["blocks[#{i}].element"] = block.element if block.element.respond_to?(:focus_on_load) && block.element.focus_on_load
|
|
145
|
+
when Layout::Section
|
|
146
|
+
focused_blocks["blocks[#{i}].accessory"] = block.accessory if block.accessory.respond_to?(:focus_on_load) && block.accessory.focus_on_load
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
focused_blocks
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def remove_unsupported_elements
|
|
154
|
+
blocks.each do |block|
|
|
155
|
+
case block
|
|
156
|
+
when Layout::Actions
|
|
157
|
+
block.elements.delete_if { |element| !self.class::SUPPORTED_ELEMENTS.include?(element.class) }
|
|
158
|
+
when Layout::Input
|
|
159
|
+
block.element = nil unless self.class::SUPPORTED_ELEMENTS.include?(block.element.class)
|
|
160
|
+
when Layout::Section
|
|
161
|
+
block.accessory = nil unless self.class::SUPPORTED_ELEMENTS.include?(block.accessory.class)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def unset_focus_on_load_on_all_elements
|
|
167
|
+
focused_blocks_by_path.values.each do |element|
|
|
168
|
+
element.focus_on_load = false if element.respond_to?(:focus_on_load)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module BlockKit
|
|
6
|
+
module Surfaces
|
|
7
|
+
class Home < Base
|
|
8
|
+
self.type = :home
|
|
9
|
+
|
|
10
|
+
SUPPORTED_ELEMENTS = [
|
|
11
|
+
Elements::Button,
|
|
12
|
+
Elements::ChannelsSelect,
|
|
13
|
+
Elements::Checkboxes,
|
|
14
|
+
Elements::ConversationsSelect,
|
|
15
|
+
Elements::DatePicker,
|
|
16
|
+
Elements::ExternalSelect,
|
|
17
|
+
Elements::Image,
|
|
18
|
+
Elements::MultiChannelsSelect,
|
|
19
|
+
Elements::MultiConversationsSelect,
|
|
20
|
+
Elements::MultiExternalSelect,
|
|
21
|
+
Elements::MultiStaticSelect,
|
|
22
|
+
Elements::MultiUsersSelect,
|
|
23
|
+
Elements::Overflow,
|
|
24
|
+
Elements::PlainTextInput,
|
|
25
|
+
Elements::RadioButtons,
|
|
26
|
+
Elements::RichTextInput,
|
|
27
|
+
Elements::StaticSelect,
|
|
28
|
+
Elements::TimePicker,
|
|
29
|
+
Elements::UsersSelect
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
def initialize(attributes = {})
|
|
33
|
+
attributes = attributes.with_indifferent_access
|
|
34
|
+
attributes[:blocks] ||= []
|
|
35
|
+
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
module BlockKit
|
|
6
|
+
module Surfaces
|
|
7
|
+
class Message < BlockKit::Base
|
|
8
|
+
self.type = :message
|
|
9
|
+
|
|
10
|
+
MAX_BLOCKS = 50
|
|
11
|
+
SUPPORTED_ELEMENTS = [
|
|
12
|
+
Elements::Button,
|
|
13
|
+
Elements::ChannelsSelect,
|
|
14
|
+
Elements::Checkboxes,
|
|
15
|
+
Elements::ConversationsSelect,
|
|
16
|
+
Elements::DatePicker,
|
|
17
|
+
Elements::DatetimePicker,
|
|
18
|
+
Elements::ExternalSelect,
|
|
19
|
+
Elements::Image,
|
|
20
|
+
Elements::MultiChannelsSelect,
|
|
21
|
+
Elements::MultiConversationsSelect,
|
|
22
|
+
Elements::MultiExternalSelect,
|
|
23
|
+
Elements::MultiStaticSelect,
|
|
24
|
+
Elements::MultiUsersSelect,
|
|
25
|
+
Elements::Overflow,
|
|
26
|
+
Elements::PlainTextInput,
|
|
27
|
+
Elements::RadioButtons,
|
|
28
|
+
Elements::StaticSelect,
|
|
29
|
+
Elements::TimePicker,
|
|
30
|
+
Elements::UsersSelect,
|
|
31
|
+
Elements::WorkflowButton
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
attribute :text, :string
|
|
35
|
+
attribute :blocks, Types::Array.of(Types::Blocks.new(*Layout.all))
|
|
36
|
+
attribute :thread_ts, :string
|
|
37
|
+
attribute :mrkdwn, :boolean
|
|
38
|
+
|
|
39
|
+
validates :text, presence: true
|
|
40
|
+
validates :blocks, length: {maximum: MAX_BLOCKS, message: "is too long (maximum is %{count} blocks)"}, "block_kit/validators/associated": true
|
|
41
|
+
validate :no_unsupported_elements
|
|
42
|
+
fix :remove_unsupported_elements, dangerous: true
|
|
43
|
+
fixes :blocks, truncate: {maximum: MAX_BLOCKS, dangerous: true}, associated: true
|
|
44
|
+
|
|
45
|
+
dsl_method :blocks, as: :actions, type: Layout::Actions
|
|
46
|
+
dsl_method :blocks, as: :context, type: Layout::Context
|
|
47
|
+
dsl_method :blocks, as: :divider, type: Layout::Divider, yields: false
|
|
48
|
+
dsl_method :blocks, as: :file, type: Layout::File, required_fields: [:external_id], yields: false
|
|
49
|
+
dsl_method :blocks, as: :header, type: Layout::Header, required_fields: [:text], yields: false
|
|
50
|
+
dsl_method :blocks, as: :input, type: Layout::Input
|
|
51
|
+
dsl_method :blocks, as: :markdown, type: Layout::Markdown, required_fields: [:text], yields: false
|
|
52
|
+
dsl_method :blocks, as: :rich_text, type: Layout::RichText
|
|
53
|
+
dsl_method :blocks, as: :section, type: Layout::Section
|
|
54
|
+
dsl_method :blocks, as: :video, type: Layout::Video, required_fields: [:alt_text, :title, :thumbnail_url, :video_url], yields: false
|
|
55
|
+
|
|
56
|
+
def initialize(attributes = {})
|
|
57
|
+
attributes = attributes.with_indifferent_access
|
|
58
|
+
attributes[:blocks] ||= []
|
|
59
|
+
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def image(alt_text:, image_url: nil, slack_file: nil, title: nil, emoji: nil, block_id: nil)
|
|
64
|
+
if (image_url.nil? && slack_file.nil?) || (image_url && slack_file)
|
|
65
|
+
raise ArgumentError, "Must provide either image_url or slack_file, but not both."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
append(Layout::Image.new(image_url: image_url, slack_file: slack_file, alt_text: alt_text, title: title, block_id: block_id, emoji: emoji))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Overridden to return `self`, allowing chaining.
|
|
72
|
+
def append(block)
|
|
73
|
+
blocks << block
|
|
74
|
+
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def as_json(*)
|
|
79
|
+
super().except(:type).merge(
|
|
80
|
+
text: text,
|
|
81
|
+
blocks: blocks&.map(&:as_json),
|
|
82
|
+
thread_ts: thread_ts,
|
|
83
|
+
mrkdwn: mrkdwn
|
|
84
|
+
).compact
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def no_unsupported_elements
|
|
90
|
+
unsupported_elements = unsupported_elements_by_path
|
|
91
|
+
return if unsupported_elements.empty?
|
|
92
|
+
|
|
93
|
+
errors.add(:blocks, "contains unsupported elements")
|
|
94
|
+
|
|
95
|
+
unsupported_elements.each do |path, element|
|
|
96
|
+
errors.add(path, "is invalid: #{element.class.type} is not a supported element for this surface")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def unsupported_elements_by_path
|
|
101
|
+
unsupported_elements = {}
|
|
102
|
+
|
|
103
|
+
blocks.each_with_index do |block, index|
|
|
104
|
+
# Context block elements are globally supported, so we don't need to check them.
|
|
105
|
+
case block
|
|
106
|
+
when Layout::Actions
|
|
107
|
+
block.elements.each_with_index do |element, element_index|
|
|
108
|
+
unless SUPPORTED_ELEMENTS.include?(element.class)
|
|
109
|
+
unsupported_elements["blocks[#{index}].elements[#{element_index}]"] = element
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
when Layout::Input
|
|
113
|
+
if block.element.present? && !SUPPORTED_ELEMENTS.include?(block.element.class)
|
|
114
|
+
unsupported_elements["blocks[#{index}].element"] = block.element
|
|
115
|
+
end
|
|
116
|
+
when Layout::Section
|
|
117
|
+
if block.accessory.present? && !SUPPORTED_ELEMENTS.include?(block.accessory.class)
|
|
118
|
+
unsupported_elements["blocks[#{index}].accessory"] = block.accessory
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
unsupported_elements
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def remove_unsupported_elements
|
|
127
|
+
blocks.each do |block|
|
|
128
|
+
case block
|
|
129
|
+
when Layout::Actions
|
|
130
|
+
block.elements.delete_if { |element| !SUPPORTED_ELEMENTS.include?(element.class) }
|
|
131
|
+
when Layout::Input
|
|
132
|
+
block.element = nil unless SUPPORTED_ELEMENTS.include?(block.element.class)
|
|
133
|
+
when Layout::Section
|
|
134
|
+
block.accessory = nil unless SUPPORTED_ELEMENTS.include?(block.accessory.class)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|