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.
Files changed (120) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +183 -0
  6. data/Rakefile +8 -0
  7. data/lib/block_kit/base.rb +190 -0
  8. data/lib/block_kit/blocks.rb +50 -0
  9. data/lib/block_kit/composition/confirmation_dialog.rb +48 -0
  10. data/lib/block_kit/composition/conversation_filter.rb +42 -0
  11. data/lib/block_kit/composition/dispatch_action_config.rb +33 -0
  12. data/lib/block_kit/composition/input_parameter.rb +19 -0
  13. data/lib/block_kit/composition/mrkdwn.rb +15 -0
  14. data/lib/block_kit/composition/option.rb +41 -0
  15. data/lib/block_kit/composition/option_group.rb +27 -0
  16. data/lib/block_kit/composition/overflow_option.rb +21 -0
  17. data/lib/block_kit/composition/plain_text.rb +15 -0
  18. data/lib/block_kit/composition/slack_file.rb +36 -0
  19. data/lib/block_kit/composition/text.rb +29 -0
  20. data/lib/block_kit/composition/trigger.rb +33 -0
  21. data/lib/block_kit/composition/workflow.rb +19 -0
  22. data/lib/block_kit/composition.rb +19 -0
  23. data/lib/block_kit/concerns/confirmable.rb +21 -0
  24. data/lib/block_kit/concerns/conversation_selection.rb +26 -0
  25. data/lib/block_kit/concerns/dispatchable.rb +24 -0
  26. data/lib/block_kit/concerns/dsl_generation.rb +76 -0
  27. data/lib/block_kit/concerns/external.rb +19 -0
  28. data/lib/block_kit/concerns/focusable_on_load.rb +17 -0
  29. data/lib/block_kit/concerns/has_initial_option.rb +23 -0
  30. data/lib/block_kit/concerns/has_initial_options.rb +23 -0
  31. data/lib/block_kit/concerns/has_option_groups.rb +37 -0
  32. data/lib/block_kit/concerns/has_options.rb +28 -0
  33. data/lib/block_kit/concerns/has_placeholder.rb +21 -0
  34. data/lib/block_kit/concerns/has_rich_text_elements.rb +83 -0
  35. data/lib/block_kit/concerns/plain_text_emoji_assignment.rb +39 -0
  36. data/lib/block_kit/concerns.rb +18 -0
  37. data/lib/block_kit/elements/base.rb +23 -0
  38. data/lib/block_kit/elements/base_button.rb +45 -0
  39. data/lib/block_kit/elements/button.rb +27 -0
  40. data/lib/block_kit/elements/channels_select.rb +22 -0
  41. data/lib/block_kit/elements/checkboxes.rb +14 -0
  42. data/lib/block_kit/elements/conversations_select.rb +24 -0
  43. data/lib/block_kit/elements/date_picker.rb +21 -0
  44. data/lib/block_kit/elements/datetime_picker.rb +21 -0
  45. data/lib/block_kit/elements/email_text_input.rb +24 -0
  46. data/lib/block_kit/elements/external_select.rb +21 -0
  47. data/lib/block_kit/elements/file_input.rb +33 -0
  48. data/lib/block_kit/elements/image.rb +53 -0
  49. data/lib/block_kit/elements/multi_channels_select.rb +31 -0
  50. data/lib/block_kit/elements/multi_conversations_select.rb +33 -0
  51. data/lib/block_kit/elements/multi_external_select.rb +21 -0
  52. data/lib/block_kit/elements/multi_select.rb +21 -0
  53. data/lib/block_kit/elements/multi_static_select.rb +12 -0
  54. data/lib/block_kit/elements/multi_users_select.rb +31 -0
  55. data/lib/block_kit/elements/number_input.rb +52 -0
  56. data/lib/block_kit/elements/overflow.rb +23 -0
  57. data/lib/block_kit/elements/plain_text_input.rb +66 -0
  58. data/lib/block_kit/elements/radio_buttons.rb +14 -0
  59. data/lib/block_kit/elements/rich_text_input.rb +26 -0
  60. data/lib/block_kit/elements/select.rb +18 -0
  61. data/lib/block_kit/elements/static_select.rb +12 -0
  62. data/lib/block_kit/elements/time_picker.rb +43 -0
  63. data/lib/block_kit/elements/url_text_input.rb +24 -0
  64. data/lib/block_kit/elements/users_select.rb +17 -0
  65. data/lib/block_kit/elements/workflow_button.rb +18 -0
  66. data/lib/block_kit/elements.rb +36 -0
  67. data/lib/block_kit/fixers/associated.rb +27 -0
  68. data/lib/block_kit/fixers/base.rb +30 -0
  69. data/lib/block_kit/fixers/null_value.rb +42 -0
  70. data/lib/block_kit/fixers/truncate.rb +35 -0
  71. data/lib/block_kit/fixers.rb +11 -0
  72. data/lib/block_kit/layout/actions.rb +84 -0
  73. data/lib/block_kit/layout/base.rb +23 -0
  74. data/lib/block_kit/layout/context.rb +48 -0
  75. data/lib/block_kit/layout/divider.rb +9 -0
  76. data/lib/block_kit/layout/file.rb +19 -0
  77. data/lib/block_kit/layout/header.rb +22 -0
  78. data/lib/block_kit/layout/image.rb +61 -0
  79. data/lib/block_kit/layout/input.rb +119 -0
  80. data/lib/block_kit/layout/markdown.rb +19 -0
  81. data/lib/block_kit/layout/rich_text/elements/broadcast.rb +22 -0
  82. data/lib/block_kit/layout/rich_text/elements/channel.rb +21 -0
  83. data/lib/block_kit/layout/rich_text/elements/color.rb +20 -0
  84. data/lib/block_kit/layout/rich_text/elements/date.rb +34 -0
  85. data/lib/block_kit/layout/rich_text/elements/emoji.rb +27 -0
  86. data/lib/block_kit/layout/rich_text/elements/link.rb +28 -0
  87. data/lib/block_kit/layout/rich_text/elements/mention_style.rb +27 -0
  88. data/lib/block_kit/layout/rich_text/elements/text.rb +23 -0
  89. data/lib/block_kit/layout/rich_text/elements/text_style.rb +23 -0
  90. data/lib/block_kit/layout/rich_text/elements/user.rb +21 -0
  91. data/lib/block_kit/layout/rich_text/elements/usergroup.rb +21 -0
  92. data/lib/block_kit/layout/rich_text/elements.rb +35 -0
  93. data/lib/block_kit/layout/rich_text/list.rb +41 -0
  94. data/lib/block_kit/layout/rich_text/preformatted.rb +19 -0
  95. data/lib/block_kit/layout/rich_text/quote.rb +19 -0
  96. data/lib/block_kit/layout/rich_text/section.rb +11 -0
  97. data/lib/block_kit/layout/rich_text.rb +53 -0
  98. data/lib/block_kit/layout/section.rb +152 -0
  99. data/lib/block_kit/layout/video.rb +71 -0
  100. data/lib/block_kit/layout.rb +35 -0
  101. data/lib/block_kit/surfaces/base.rb +173 -0
  102. data/lib/block_kit/surfaces/home.rb +40 -0
  103. data/lib/block_kit/surfaces/message.rb +140 -0
  104. data/lib/block_kit/surfaces/modal.rb +74 -0
  105. data/lib/block_kit/surfaces.rb +11 -0
  106. data/lib/block_kit/typed_array.rb +114 -0
  107. data/lib/block_kit/typed_set.rb +78 -0
  108. data/lib/block_kit/types/array.rb +45 -0
  109. data/lib/block_kit/types/blocks.rb +37 -0
  110. data/lib/block_kit/types/generic.rb +45 -0
  111. data/lib/block_kit/types/option.rb +48 -0
  112. data/lib/block_kit/types/set.rb +35 -0
  113. data/lib/block_kit/types/text.rb +80 -0
  114. data/lib/block_kit/types.rb +17 -0
  115. data/lib/block_kit/validators/array_inclusion_validator.rb +55 -0
  116. data/lib/block_kit/validators/associated_validator.rb +61 -0
  117. data/lib/block_kit/validators.rb +8 -0
  118. data/lib/block_kit/version.rb +5 -0
  119. data/lib/block_kit.rb +38 -0
  120. metadata +190 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module HasPlaceholder
6
+ MAX_TEXT_LENGTH = 150
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ plain_text_attribute :placeholder
12
+ validates :placeholder, presence: true, length: {maximum: MAX_TEXT_LENGTH}, allow_nil: true
13
+ fixes :placeholder, truncate: {maximum: MAX_TEXT_LENGTH}, null_value: {error_types: [:blank]}
14
+ end
15
+
16
+ def as_json(*)
17
+ super.merge(placeholder: placeholder&.as_json).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module HasRichTextElements
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute :elements, Types::Array.of(Types::Blocks.new(*Layout::RichText::Elements.all))
10
+ validates :elements, presence: true, "block_kit/validators/associated": true
11
+ fixes :elements, associated: true
12
+
13
+ dsl_method :elements, as: :broadcast, type: Layout::RichText::Elements::Broadcast, required_fields: [:range], yields: false
14
+ dsl_method :elements, as: :color, type: Layout::RichText::Elements::Color, required_fields: [:value], yields: false
15
+ dsl_method :elements, as: :date, type: Layout::RichText::Elements::Date, required_fields: [:timestamp, :format], yields: false
16
+ dsl_method :elements, as: :emoji, type: Layout::RichText::Elements::Emoji, required_fields: [:name], yields: false
17
+ end
18
+
19
+ def initialize(attributes = {})
20
+ attributes = attributes.with_indifferent_access
21
+ attributes[:elements] ||= []
22
+
23
+ super
24
+ end
25
+
26
+ def channel(channel_id:, styles: [])
27
+ style = if styles.present?
28
+ styles = Array(styles).map { |s| [s.to_s, true] }.to_h
29
+ Layout::RichText::Elements::MentionStyle.new(**styles.slice(*Layout::RichText::Elements::MentionStyle.attribute_names))
30
+ end
31
+
32
+ append(Layout::RichText::Elements::Channel.new(channel_id: channel_id, style: style))
33
+ end
34
+
35
+ def link(url:, text:, unsafe: nil, styles: [])
36
+ style = if styles.present?
37
+ styles = Array(styles).map { |s| [s.to_s, true] }.to_h
38
+ Layout::RichText::Elements::TextStyle.new(**styles.slice(*Layout::RichText::Elements::TextStyle.attribute_names))
39
+ end
40
+
41
+ append(Layout::RichText::Elements::Link.new(url: url, text: text, unsafe: unsafe, style: style))
42
+ end
43
+
44
+ def text(text:, styles: [])
45
+ style = if styles.present?
46
+ styles = Array(styles).map { |s| [s.to_s, true] }.to_h
47
+ Layout::RichText::Elements::TextStyle.new(**styles.slice(*Layout::RichText::Elements::TextStyle.attribute_names))
48
+ end
49
+
50
+ append(Layout::RichText::Elements::Text.new(text: text, style: style))
51
+ end
52
+
53
+ def usergroup(usergroup_id:, styles: [])
54
+ style = if styles.present?
55
+ styles = Array(styles).map { |s| [s.to_s, true] }.to_h
56
+ Layout::RichText::Elements::MentionStyle.new(**styles.slice(*Layout::RichText::Elements::MentionStyle.attribute_names))
57
+ end
58
+
59
+ append(Layout::RichText::Elements::Usergroup.new(usergroup_id: usergroup_id, style: style))
60
+ end
61
+ alias_method :user_group, :usergroup
62
+
63
+ def user(user_id:, styles: [])
64
+ style = if styles.present?
65
+ styles = Array(styles).map { |s| [s.to_s, true] }.to_h
66
+ Layout::RichText::Elements::MentionStyle.new(**styles.slice(*Layout::RichText::Elements::MentionStyle.attribute_names))
67
+ end
68
+
69
+ append(Layout::RichText::Elements::User.new(user_id: user_id, style: style))
70
+ end
71
+
72
+ def append(element)
73
+ elements << element
74
+
75
+ self
76
+ end
77
+
78
+ def as_json(*)
79
+ super.merge(elements: elements&.map(&:as_json)).compact
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ module PlainTextEmojiAssignment
6
+ def self.new(*attributes)
7
+ Module.new do
8
+ define_method(:initialize) do |attrs = {}|
9
+ raise ArgumentError, "expected `attributes' to be a Hash, got #{attrs.class}" unless attrs.is_a?(Hash)
10
+
11
+ emoji = attrs.delete(:emoji)
12
+
13
+ super(attrs)
14
+
15
+ unless emoji.nil?
16
+ attributes.each do |attribute|
17
+ text = public_send(attribute)
18
+ public_send(:"#{attribute}=", Composition::PlainText.new) if text.nil?
19
+ public_send(attribute).emoji = emoji
20
+ end
21
+ end
22
+ end
23
+
24
+ attributes.each do |attribute|
25
+ define_method(:"#{attribute}=") do |value|
26
+ # Attempt to preserve the existing text object's emoji attribute if present
27
+ text = public_send(attribute)
28
+ if !text&.emoji.nil? && value.is_a?(String)
29
+ value = Composition::PlainText.new(text: value, emoji: text.emoji)
30
+ end
31
+
32
+ super(value)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Concerns
5
+ autoload :Confirmable, "block_kit/concerns/confirmable"
6
+ autoload :ConversationSelection, "block_kit/concerns/conversation_selection"
7
+ autoload :Dispatchable, "block_kit/concerns/dispatchable"
8
+ autoload :External, "block_kit/concerns/external"
9
+ autoload :FocusableOnLoad, "block_kit/concerns/focusable_on_load"
10
+ autoload :HasInitialOption, "block_kit/concerns/has_initial_option"
11
+ autoload :HasInitialOptions, "block_kit/concerns/has_initial_options"
12
+ autoload :HasOptions, "block_kit/concerns/has_options"
13
+ autoload :HasOptionGroups, "block_kit/concerns/has_option_groups"
14
+ autoload :HasPlaceholder, "block_kit/concerns/has_placeholder"
15
+ autoload :HasRichTextElements, "block_kit/concerns/has_rich_text_elements"
16
+ autoload :PlainTextEmojiAssignment, "block_kit/concerns/plain_text_emoji_assignment"
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class Base < BlockKit::Base
6
+ MAX_ACTION_ID_LENGTH = 255
7
+
8
+ attribute :action_id, :string
9
+ validates :action_id, presence: true, length: {maximum: MAX_ACTION_ID_LENGTH}, allow_nil: true
10
+ fixes :action_id, truncate: {maximum: MAX_ACTION_ID_LENGTH, dangerous: true, omission: ""}, null_value: {error_types: [:blank]}
11
+
12
+ def initialize(attributes = {})
13
+ raise NotImplementedError, "#{self.class} is an abstract class and can't be instantiated." if instance_of?(Base)
14
+
15
+ super
16
+ end
17
+
18
+ def as_json(*)
19
+ super.merge(action_id: action_id).compact
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class BaseButton < Base
6
+ self.type = :button
7
+
8
+ MAX_TEXT_LENGTH = 75
9
+ MAX_ACCESSIBILITY_LABEL_LENGTH = 75
10
+ VALID_STYLES = [
11
+ PRIMARY = "primary",
12
+ DANGER = "danger"
13
+ ].freeze
14
+
15
+ plain_text_attribute :text
16
+ attribute :style, :string
17
+ attribute :accessibility_label, :string
18
+
19
+ include Concerns::PlainTextEmojiAssignment.new(:text)
20
+
21
+ validates :text, presence: true, length: {maximum: MAX_TEXT_LENGTH}
22
+ fixes :text, truncate: {maximum: MAX_TEXT_LENGTH}
23
+
24
+ validates :style, presence: true, inclusion: {in: VALID_STYLES}, allow_nil: true
25
+ fixes :style, null_value: {error_types: [:inclusion]}
26
+
27
+ validates :accessibility_label, presence: true, length: {maximum: MAX_ACCESSIBILITY_LABEL_LENGTH}, allow_nil: true
28
+ fixes :accessibility_label, truncate: {maximum: MAX_ACCESSIBILITY_LABEL_LENGTH}, null_value: {error_types: [:blank]}
29
+
30
+ def initialize(attributes = {})
31
+ raise NotImplementedError, "#{self.class} is an abstract class and can't be instantiated." if instance_of?(BaseButton)
32
+
33
+ super
34
+ end
35
+
36
+ def as_json(*)
37
+ super.merge(
38
+ text: text&.as_json,
39
+ style: style,
40
+ accessibility_label: accessibility_label
41
+ ).compact
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Elements
7
+ class Button < BaseButton
8
+ MAX_URL_LENGTH = 3000
9
+ MAX_VALUE_LENGTH = 2000
10
+
11
+ include Concerns::Confirmable
12
+
13
+ attribute :url, :string
14
+ attribute :value, :string
15
+
16
+ validates :url, presence: true, format: {with: URI::DEFAULT_PARSER.make_regexp, message: "is not a valid URI", allow_blank: true}, length: {maximum: MAX_URL_LENGTH}, allow_nil: true
17
+ fixes :url, truncate: {maximum: MAX_URL_LENGTH, dangerous: true, omission: ""}, null_value: {error_types: [:blank]}
18
+
19
+ validates :value, presence: true, length: {maximum: MAX_VALUE_LENGTH}, allow_nil: true
20
+ fixes :value, truncate: {maximum: MAX_VALUE_LENGTH, dangerous: true, omission: ""}, null_value: {error_types: [:blank]}
21
+
22
+ def as_json(*)
23
+ super.merge(url: url, value: value).compact
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class ChannelsSelect < Select
6
+ self.type = :channels_select
7
+
8
+ attribute :initial_channel, :string
9
+ attribute :response_url_enabled, :boolean
10
+
11
+ validates :initial_channel, presence: true, allow_nil: true
12
+ fixes :initial_channel, null_value: {error_types: [:blank]}
13
+
14
+ def as_json(*)
15
+ super.merge(
16
+ initial_channel: initial_channel,
17
+ response_url_enabled: response_url_enabled
18
+ ).compact
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class Checkboxes < Base
6
+ self.type = :checkboxes
7
+
8
+ include Concerns::Confirmable
9
+ include Concerns::HasOptions.new(limit: 10)
10
+ include Concerns::HasInitialOptions
11
+ include Concerns::FocusableOnLoad
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class ConversationsSelect < Select
6
+ self.type = :conversations_select
7
+
8
+ include Concerns::ConversationSelection
9
+
10
+ attribute :initial_conversation, :string
11
+ attribute :response_url_enabled, :boolean
12
+
13
+ validates :initial_conversation, presence: true, allow_nil: true
14
+ fixes :initial_conversation, null_value: {error_types: [:blank]}
15
+
16
+ def as_json(*)
17
+ super.merge(
18
+ initial_conversation: initial_conversation,
19
+ response_url_enabled: response_url_enabled
20
+ ).compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class DatePicker < Base
6
+ self.type = :datepicker
7
+
8
+ include Concerns::Confirmable
9
+ include Concerns::FocusableOnLoad
10
+ include Concerns::HasPlaceholder
11
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
12
+
13
+ attribute :initial_date, :date
14
+ validates :initial_date, presence: true, allow_nil: true
15
+
16
+ def as_json(*)
17
+ super.merge(initial_date: initial_date&.iso8601).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class DatetimePicker < Base
6
+ self.type = :datetimepicker
7
+
8
+ include Concerns::Confirmable
9
+ include Concerns::FocusableOnLoad
10
+
11
+ attribute :initial_date_time, :datetime
12
+ validates :initial_date_time, presence: true, allow_nil: true
13
+
14
+ alias_attribute :initial_datetime, :initial_date_time
15
+
16
+ def as_json(*)
17
+ super.merge(initial_date_time: initial_date_time&.to_i).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Elements
7
+ class EmailTextInput < Base
8
+ self.type = :email_text_input
9
+
10
+ include Concerns::Dispatchable
11
+ include Concerns::FocusableOnLoad
12
+ include Concerns::HasPlaceholder
13
+ include Concerns::PlainTextEmojiAssignment.new(:placeholder)
14
+
15
+ attribute :initial_value, :string
16
+ validates :initial_value, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}, allow_nil: true
17
+ fixes :initial_value, null_value: {error_types: [:blank, :invalid]}
18
+
19
+ def as_json(*)
20
+ super.merge(initial_value: initial_value).compact
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class ExternalSelect < Select
6
+ self.type = :external_select
7
+
8
+ include Concerns::External
9
+
10
+ attribute :initial_option, Types::Generic.of_type(Composition::Option)
11
+ validates :initial_option, "block_kit/validators/associated": true, allow_nil: true
12
+ fixes :initial_option, associated: true
13
+
14
+ dsl_method :initial_option, required_fields: [:text, :value], yields: false
15
+
16
+ def as_json(*)
17
+ super.merge(initial_option: initial_option&.as_json).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/inclusion" # Necessary for `max_files.in?(1..10)`
4
+
5
+ module BlockKit
6
+ module Elements
7
+ class FileInput < Base
8
+ self.type = :file_input
9
+
10
+ VALID_MAX_FILES_RANGE = 1..10
11
+
12
+ attribute :filetypes, Types::Set.of(:string)
13
+ attribute :max_files, :integer
14
+
15
+ validates :filetypes, presence: true, allow_nil: true
16
+ validates :max_files, presence: true, numericality: {in: VALID_MAX_FILES_RANGE, only_integer: true}, allow_nil: true
17
+ fix :clamp_max_files
18
+
19
+ def as_json(*)
20
+ super.merge(
21
+ filetypes: filetypes&.to_a,
22
+ max_files: max_files
23
+ ).compact
24
+ end
25
+
26
+ private
27
+
28
+ def clamp_max_files
29
+ self.max_files = max_files&.clamp(VALID_MAX_FILES_RANGE)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BlockKit
6
+ module Elements
7
+ class Image < BlockKit::Base
8
+ self.type = :image
9
+
10
+ MAX_ALT_TEXT_LENGTH = 2000
11
+ MAX_IMAGE_URL_LENGTH = 3000
12
+
13
+ attribute :alt_text, :string
14
+ attribute :image_url, :string
15
+ attribute :slack_file, Types::Generic.of_type(Composition::SlackFile)
16
+
17
+ validates :alt_text, presence: true, length: {maximum: MAX_ALT_TEXT_LENGTH}
18
+ fixes :alt_text, truncate: {maximum: MAX_ALT_TEXT_LENGTH}
19
+
20
+ validates :image_url, presence: true, length: {maximum: MAX_IMAGE_URL_LENGTH}, format: {with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: "is not a valid URI", allow_blank: true}, allow_nil: true
21
+ fixes :image_url, truncate: {maximum: MAX_IMAGE_URL_LENGTH, dangerous: true, omission: ""}, null_value: {error_types: [:blank]}
22
+
23
+ validate :slack_file_or_url_present
24
+
25
+ def slack_file(attrs = {})
26
+ attrs = attrs.with_indifferent_access
27
+ return super() unless attrs.key?(:id) || attrs.key?(:url)
28
+
29
+ raise ArgumentError, "mutually exclusive keywords: :id, :url" if attrs.key?(:id) && attrs.key?(:url)
30
+
31
+ self.slack_file = Composition::SlackFile.new(**attrs.slice(:id, :url))
32
+
33
+ self
34
+ end
35
+
36
+ def as_json(*)
37
+ super.merge(
38
+ alt_text: alt_text,
39
+ image_url: image_url,
40
+ slack_file: slack_file&.as_json
41
+ ).compact
42
+ end
43
+
44
+ private
45
+
46
+ def slack_file_or_url_present
47
+ if (slack_file.nil? && image_url.nil?) || (slack_file.present? && image_url.present?)
48
+ errors.add(:base, "must have either a slack_file or image_url but not both")
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class MultiChannelsSelect < MultiSelect
6
+ self.type = :multi_channels_select
7
+
8
+ attribute :initial_channels, Types::Set.of(:string)
9
+ validate :initial_channels_are_all_present
10
+ fix :remove_blank_initial_channels
11
+
12
+ def as_json(*)
13
+ super.merge(initial_channels: initial_channels&.to_a).compact
14
+ end
15
+
16
+ private
17
+
18
+ def initial_channels_are_all_present
19
+ if Array(initial_channels).any?(&:blank?)
20
+ errors.add(:initial_channels, "must not contain blank values")
21
+ end
22
+ end
23
+
24
+ def remove_blank_initial_channels
25
+ return unless initial_channels.present?
26
+
27
+ initial_channels.reject!(&:blank?)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class MultiConversationsSelect < MultiSelect
6
+ self.type = :multi_conversations_select
7
+
8
+ include Concerns::ConversationSelection
9
+
10
+ attribute :initial_conversations, Types::Set.of(:string)
11
+ validate :initial_conversations_are_all_present
12
+ fix :remove_blank_initial_conversations
13
+
14
+ def as_json(*)
15
+ super.merge(initial_conversations: initial_conversations&.to_a).compact
16
+ end
17
+
18
+ private
19
+
20
+ def initial_conversations_are_all_present
21
+ if Array(initial_conversations).any?(&:blank?)
22
+ errors.add(:initial_conversations, "must not contain blank values")
23
+ end
24
+ end
25
+
26
+ def remove_blank_initial_conversations
27
+ return unless initial_conversations.present?
28
+
29
+ initial_conversations.reject!(&:blank?)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class MultiExternalSelect < MultiSelect
6
+ self.type = :multi_external_select
7
+
8
+ include Concerns::External
9
+
10
+ attribute :initial_options, Types::Array.of(Composition::Option)
11
+ validates :initial_options, "block_kit/validators/associated": true, allow_nil: true
12
+ fixes :initial_options, associated: true
13
+
14
+ dsl_method :initial_options, as: :initial_option, required_fields: [:text, :value], yields: false
15
+
16
+ def as_json(*)
17
+ super.merge(initial_options: initial_options&.map(&:as_json)).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class MultiSelect < Select
6
+ attribute :max_selected_items, :integer
7
+ validates :max_selected_items, presence: true, numericality: {only_integer: true, greater_than: 0}, allow_nil: true
8
+ fixes :max_selected_items, null_value: {error_types: [:greater_than]}
9
+
10
+ def initialize(attributes = {})
11
+ raise NotImplementedError, "#{self.class} is an abstract class and can't be instantiated." if instance_of?(MultiSelect)
12
+
13
+ super
14
+ end
15
+
16
+ def as_json(*)
17
+ super.merge(max_selected_items: max_selected_items).compact
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class MultiStaticSelect < MultiSelect
6
+ self.type = :multi_static_select
7
+
8
+ include Concerns::HasOptionGroups.new(limit: 100, options_limit: 100)
9
+ include Concerns::HasInitialOptions
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockKit
4
+ module Elements
5
+ class MultiUsersSelect < MultiSelect
6
+ self.type = :multi_users_select
7
+
8
+ attribute :initial_users, Types::Set.of(:string)
9
+ validate :initial_users_are_all_present
10
+ fix :remove_blank_initial_users
11
+
12
+ def as_json(*)
13
+ super.merge(initial_users: initial_users&.to_a).compact
14
+ end
15
+
16
+ private
17
+
18
+ def initial_users_are_all_present
19
+ if Array(initial_users).any?(&:blank?)
20
+ errors.add(:initial_users, "must not contain blank values")
21
+ end
22
+ end
23
+
24
+ def remove_blank_initial_users
25
+ return unless initial_users.present?
26
+
27
+ initial_users.reject!(&:blank?)
28
+ end
29
+ end
30
+ end
31
+ end