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
|