slack_blocks/elems/overflow.rs
1//! # Overflow Menu
2//!
3//! This is like a cross between a button and a select menu -
4//! when a user clicks on this overflow button,
5//! they will be presented with a list of options to choose from.
6//!
7//! Unlike the select menu, there is no typeahead field,
8//! and the button always appears with an ellipsis ("…"),
9//! rather than customisable text.
10//!
11//! [slack api docs 🔗]
12//!
13//! Works in [blocks 🔗]: Section, Actions
14//!
15//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#overflow
16//! [blocks 🔗]: https://api.slack.com/reference/block-kit/blocks
17
18use std::borrow::Cow;
19
20use serde::{Deserialize as De, Serialize as Ser};
21#[cfg(feature = "validation")]
22use validator::Validate;
23
24#[cfg(feature = "validation")]
25use crate::val_helpr::*;
26use crate::{compose::{opt::AllowUrl, Confirm, Opt},
27 text};
28
29type MyOpt<'a> = Opt<'a, text::Plain, AllowUrl>;
30
31/// # Overflow Menu
32///
33/// This is like a cross between a button and a select menu -
34/// when a user clicks on this overflow button,
35/// they will be presented with a list of options to choose from.
36///
37/// Unlike the select menu, there is no typeahead field,
38/// and the button always appears with an ellipsis ("…"),
39/// rather than customisable text.
40///
41/// [slack api docs 🔗]
42///
43/// Works in [blocks 🔗]: Section, Actions
44///
45/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#overflow
46/// [blocks 🔗]: https://api.slack.com/reference/block-kit/blocks
47#[derive(Clone, Debug, Hash, PartialEq, Ser, De)]
48#[cfg_attr(feature = "validation", derive(Validate))]
49pub struct Overflow<'a> {
50 #[cfg_attr(feature = "validation", validate(length(max = 255)))]
51 action_id: Cow<'a, str>,
52
53 #[cfg_attr(feature = "validation", validate(length(min = 2, max = 5)))]
54 #[cfg_attr(feature = "validation", validate)]
55 options: Vec<MyOpt<'a>>,
56
57 #[cfg_attr(feature = "validation", validate)]
58 #[serde(skip_serializing_if = "Option::is_none")]
59 confirm: Option<Confirm>,
60}
61
62impl<'a> Overflow<'a> {
63 /// Construct a new Overflow Menu.
64 ///
65 /// # Example
66 /// See example of `build::OverflowBuilder`
67 pub fn builder() -> build::OverflowBuilderInit<'a> {
68 build::OverflowBuilderInit::new()
69 }
70
71 /// Validate that this select element agrees with Slack's model requirements
72 ///
73 /// # Errors
74 /// - length of `action_id` greater than 255
75 /// - length of `options` less than 2 or greater than 5
76 /// - one or more of `options` is invalid (**TODO**)
77 /// - `confirm` is set and an invalid `Confirm`
78 ///
79 /// # Example
80 /// ```
81 /// use slack_blocks::{compose::Opt, elems::Overflow};
82 ///
83 /// fn repeat<T: Copy>(el: T, n: usize) -> impl Iterator<Item = T> {
84 /// std::iter::repeat(el).take(n)
85 /// }
86 ///
87 /// let long_string: String = repeat('a', 256).collect();
88 ///
89 /// let opt = Opt::builder().text_plain("foo")
90 /// .value("bar")
91 /// .no_url()
92 /// .build();
93 ///
94 /// let opts: Vec<Opt<_, _>> = repeat(&opt, 6).map(|o| o.clone()).collect();
95 ///
96 /// let input = Overflow::builder().action_id(long_string) // invalid
97 /// .options(opts) // also invalid
98 /// .build();
99 ///
100 /// assert!(matches!(input.validate(), Err(_)))
101 /// ```
102 #[cfg(feature = "validation")]
103 #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
104 pub fn validate(&self) -> ValidationResult {
105 Validate::validate(self)
106 }
107}
108
109/// Overflow menu builder
110pub mod build {
111 use std::marker::PhantomData;
112
113 use super::*;
114 use crate::build::*;
115
116 /// Required builder methods
117 #[allow(non_camel_case_types)]
118 pub mod method {
119 /// OverflowBuilder.action_id
120 #[derive(Copy, Clone, Debug)]
121 pub struct action_id;
122 /// OverflowBuilder.options
123 #[derive(Copy, Clone, Debug)]
124 pub struct options;
125 }
126
127 /// Initial state for overflow builder
128 pub type OverflowBuilderInit<'a> =
129 OverflowBuilder<'a,
130 RequiredMethodNotCalled<method::action_id>,
131 RequiredMethodNotCalled<method::options>>;
132
133 /// Overflow Menu Builder
134 ///
135 /// Allows you to construct safely, with compile-time checks
136 /// on required setter methods.
137 ///
138 /// # Required Methods
139 /// `OverflowBuilder::build()` is only available if these methods have been called:
140 /// - `action_id`
141 /// - `options`
142 ///
143 /// # Example
144 /// ```
145 /// use slack_blocks::{elems::Overflow, compose::Opt};
146 ///
147 /// Overflow::builder()
148 /// .action_id("foo")
149 /// .options(vec![
150 /// Opt::builder()
151 /// .text_plain("Open in browser")
152 /// .value("open_ext")
153 /// .url("https://foo.org")
154 /// .build(),
155 /// Opt::builder()
156 /// .text_plain("Do stuff")
157 /// .value("do_stuff")
158 /// .no_url()
159 /// .build(),
160 /// ]);
161 /// ```
162 #[derive(Debug)]
163 pub struct OverflowBuilder<'a, A, O> {
164 action_id: Option<Cow<'a, str>>,
165 options: Option<Vec<MyOpt<'a>>>,
166 confirm: Option<Confirm>,
167 state: PhantomData<(A, O)>,
168 }
169
170 impl<'a, A, O> OverflowBuilder<'a, A, O> {
171 /// Create a new empty builder
172 pub fn new() -> Self {
173 Self { action_id: None,
174 options: None,
175 confirm: None,
176 state: PhantomData::<_> }
177 }
178
179 /// Cast the internal static builder state to some other arbitrary state
180 fn cast_state<A2, O2>(self) -> OverflowBuilder<'a, A2, O2> {
181 OverflowBuilder { action_id: self.action_id,
182 options: self.options,
183 confirm: self.confirm,
184 state: PhantomData::<_> }
185 }
186
187 /// Set `action_id` (**Required**)
188 ///
189 /// An identifier for the action triggered when a menu option is selected.
190 ///
191 /// You can use this when you receive an interaction payload to [identify the source of the action 🔗].
192 ///
193 /// Should be unique among all other `action_id`s in the containing block.
194 ///
195 /// Maximum length for this field is 255 characters.
196 ///
197 /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
198 pub fn action_id<T>(mut self,
199 action_id: T)
200 -> OverflowBuilder<'a, Set<method::action_id>, O>
201 where T: Into<Cow<'a, str>>
202 {
203 self.action_id = Some(action_id.into());
204 self.cast_state()
205 }
206
207 /// Set `options` (**Required**)
208 ///
209 /// An array of [option objects 🔗] to display in the menu.
210 ///
211 /// Maximum number of options is 5, minimum is 2.
212 ///
213 /// [option objects 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option
214 pub fn options<U>(mut self,
215 options: Vec<Opt<'a, text::Plain, U>>)
216 -> OverflowBuilder<'a, A, Set<method::options>> {
217 self.options =
218 Some(options.into_iter().map(|o| o.as_allow_url()).collect());
219 self.cast_state()
220 }
221
222 /// Append an option to `options`
223 ///
224 /// Maximum number of options is 5, minimum is 2.
225 pub fn option<U>(mut self,
226 option: Opt<'a, text::Plain, U>)
227 -> OverflowBuilder<'a, A, Set<method::options>> {
228 let options = match self.options {
229 | Some(mut options) => {
230 options.push(option.as_allow_url());
231 options
232 },
233 | None => vec![option.as_allow_url()],
234 };
235
236 self.options = Some(options);
237 self.cast_state()
238 }
239
240 /// Allows using an XML child to append an option.
241 #[cfg(feature = "blox")]
242 #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
243 pub fn child<U>(self,
244 option: Opt<'a, text::Plain, U>)
245 -> OverflowBuilder<'a, A, Set<method::options>> {
246 self.option(option)
247 }
248
249 /// Set `confirm` (Optional)
250 ///
251 /// A [confirm object 🔗] that defines an optional confirmation dialog that appears after a menu item is selected.
252 ///
253 /// [confirm object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#confirm
254 pub fn confirm(mut self, confirm: Confirm) -> Self {
255 self.confirm = Some(confirm);
256 self
257 }
258 }
259
260 impl<'a> OverflowBuilder<'a, Set<method::action_id>, Set<method::options>> {
261 /// All done building, now give me a darn overflow menu!
262 ///
263 /// > `no method name 'build' found for struct 'OverflowBuilder<...>'`?
264 /// Make sure all required setter methods have been called. See docs for `OverflowBuilder`.
265 ///
266 /// ```compile_fail
267 /// use slack_blocks::elems::Overflow;
268 ///
269 /// let foo = Overflow::builder().build(); // Won't compile!
270 /// ```
271 ///
272 /// ```
273 /// use slack_blocks::{compose::Opt, elems::Overflow};
274 ///
275 /// let foo = Overflow::builder().action_id("bar")
276 /// .options(vec![Opt::builder().text_plain("foo")
277 /// .value("bar")
278 /// .no_url()
279 /// .build()])
280 /// .build();
281 /// ```
282 pub fn build(self) -> Overflow<'a> {
283 Overflow { action_id: self.action_id.unwrap(),
284 options: self.options.unwrap(),
285 confirm: self.confirm }
286 }
287 }
288}