From 39b48987ac64b8cdbf0e31f0ae10aa096fcbcfd3 Mon Sep 17 00:00:00 2001 From: Jeremy Lindblom Date: Wed, 30 Sep 2020 09:09:06 -0700 Subject: [PATCH] Updates HasOptions to account for initialOptions (#12) Udpates all HasOptions-using elements. Also adds setExtra to base Element object to allow for patching on new Slack features in a non-conflicting way prior to the library being updated. --- src/Element.php | 31 +++- src/Inputs/Checkboxes.php | 33 ++-- src/Inputs/OverflowMenu.php | 34 ++-- src/Inputs/RadioButtons.php | 47 ++---- src/Inputs/SelectMenus/ChannelSelectMenu.php | 18 +++ .../SelectMenus/ConversationSelectMenu.php | 38 +++++ src/Inputs/SelectMenus/ExternalSelectMenu.php | 2 +- .../MultiConversationSelectMenu.php | 20 +++ .../SelectMenus/MultiExternalSelectMenu.php | 2 +- src/Inputs/SelectMenus/MultiSelectMenu.php | 4 +- .../SelectMenus/MultiStaticSelectMenu.php | 42 +---- src/Inputs/SelectMenus/StaticSelectMenu.php | 37 ++--- src/Partials/HasOptionGroups.php | 2 +- src/Partials/HasOptions.php | 152 ++++++++++++++++-- src/Partials/Option.php | 93 +++++++++-- src/Partials/OptionGroup.php | 22 ++- src/Partials/OptionsConfig.php | 84 ++++++++++ src/Partials/Text.php | 20 ++- src/Surfaces/AppHome.php | 5 + src/Surfaces/Message.php | 6 + src/Surfaces/Modal.php | 6 + tests/ElementTest.php | 21 +++ tests/Inputs/CheckBoxesTest.php | 1 - tests/Inputs/OverflowMenuTest.php | 3 +- tests/Inputs/RadioButtonsTest.php | 1 - tests/manual/menus.php | 7 +- 26 files changed, 551 insertions(+), 180 deletions(-) create mode 100644 src/Partials/OptionsConfig.php diff --git a/src/Element.php b/src/Element.php index 62dd652..f3ebe3c 100644 --- a/src/Element.php +++ b/src/Element.php @@ -11,6 +11,9 @@ abstract class Element implements JsonSerializable /** @var Element|null */ protected $parent; + /** @var array */ + protected $extra; + /** * @return static */ @@ -46,6 +49,26 @@ public function getType(): string return Type::mapClass(static::class); } + /** + * Allows setting arbitrary extra fields on an element. + * + * Ideally, this is only used to allow setting new Slack features that are not yet implemented in this library. + * + * @param string $key + * @param mixed $value + * @return static + */ + public function setExtra(string $key, $value): Element + { + if (!is_scalar($value) && !($value instanceof Element)) { + throw new Exception('Invalid extra field set in %d.', [static::class]); + } + + $this->extra[$key] = $value; + + return $this; + } + /** * @throws Exception if the block kit item is invalid (e.g., missing data). */ @@ -59,7 +82,13 @@ public function toArray(): array $this->validate(); $type = $this->getType(); - return !in_array($type, Type::HIDDEN_TYPES, true) ? compact('type') : []; + $data = !in_array($type, Type::HIDDEN_TYPES, true) ? compact('type') : []; + + foreach ($this->extra ?? [] as $key => $value) { + $data[$key] = $value instanceof Element ? $value->toArray() : $value; + } + + return $data; } /** diff --git a/src/Inputs/Checkboxes.php b/src/Inputs/Checkboxes.php index 923f5ec..2944ff9 100644 --- a/src/Inputs/Checkboxes.php +++ b/src/Inputs/Checkboxes.php @@ -4,34 +4,29 @@ namespace Jeremeamia\Slack\BlockKit\Inputs; -use Jeremeamia\Slack\BlockKit\Exception; -use Jeremeamia\Slack\BlockKit\Partials\HasOptions; -use Jeremeamia\Slack\BlockKit\Partials\Option; +use Jeremeamia\Slack\BlockKit\Partials\{HasOptions, OptionsConfig}; class Checkboxes extends InputElement { use HasConfirm; use HasOptions; - private const MIN_OPTIONS = 1; - private const MAX_OPTIONS = 10; + protected function getOptionsConfig(): OptionsConfig + { + return OptionsConfig::new() + ->setMinOptions(1) + ->setMaxOptions(10) + ->setMaxInitialOptions(10); + } public function validate(): void { - $this->validateOptions(); + $this->validateInitialOptions(); if (!empty($this->confirm)) { $this->confirm->validate(); } - - if (count($this->options) > self::MAX_OPTIONS) { - throw new Exception('Option Size cannot exceed %d', [self::MAX_OPTIONS]); - } - - if (count($this->options) < self::MIN_OPTIONS) { - throw new Exception('Option Size must be at least %d', [self::MIN_OPTIONS]); - } } /** @@ -39,15 +34,7 @@ public function validate(): void */ public function toArray(): array { - $data = parent::toArray(); - - foreach ($this->options as $option) { - if ($option->isInitial()) { - $data['initial_options'][] = $option->toArray(); - } - } - - $data += $this->getOptionsAsArray(); + $data = parent::toArray() + $this->getOptionsAsArray() + $this->getInitialOptionsAsArray(); if (!empty($this->confirm)) { $data['confirm'] = $this->confirm->toArray(); diff --git a/src/Inputs/OverflowMenu.php b/src/Inputs/OverflowMenu.php index 6d42512..184b4ba 100644 --- a/src/Inputs/OverflowMenu.php +++ b/src/Inputs/OverflowMenu.php @@ -4,33 +4,39 @@ namespace Jeremeamia\Slack\BlockKit\Inputs; -use Jeremeamia\Slack\BlockKit\Exception; -use Jeremeamia\Slack\BlockKit\Partials\HasOptions; +use Jeremeamia\Slack\BlockKit\Partials\{HasOptions, Option, OptionsConfig}; class OverflowMenu extends InputElement { use HasConfirm; use HasOptions; - private const MIN_OPTIONS = 2; - private const MAX_OPTIONS = 5; + protected function getOptionsConfig(): OptionsConfig + { + return OptionsConfig::new() + ->setMinOptions(2) + ->setMaxOptions(5) + ->setMaxInitialOptions(0); + } - public function validate(): void + /** + * @param string $text + * @param string $value + * @param string $url + * @return static + */ + public function urlOption(string $text, string $value, string $url): self { + return $this->addOption(Option::new($text, $value)->url($url)); + } + public function validate(): void + { $this->validateOptions(); if (!empty($this->confirm)) { $this->confirm->validate(); } - - if (count($this->options) > self::MAX_OPTIONS) { - throw new Exception('Option Size cannot exceed %d', [self::MAX_OPTIONS]); - } - - if (count($this->options) < self::MIN_OPTIONS) { - throw new Exception('Option Size must be at least %d', [self::MIN_OPTIONS]); - } } /** @@ -38,8 +44,8 @@ public function validate(): void */ public function toArray(): array { - $data = parent::toArray(); + $data += $this->getOptionsAsArray(); if (!empty($this->confirm)) { diff --git a/src/Inputs/RadioButtons.php b/src/Inputs/RadioButtons.php index 1b3c0e3..89ac7e2 100644 --- a/src/Inputs/RadioButtons.php +++ b/src/Inputs/RadioButtons.php @@ -4,47 +4,29 @@ namespace Jeremeamia\Slack\BlockKit\Inputs; -use Jeremeamia\Slack\BlockKit\Exception; -use Jeremeamia\Slack\BlockKit\Partials\HasOptions; -use Jeremeamia\Slack\BlockKit\Partials\Option; +use Jeremeamia\Slack\BlockKit\Partials\{HasOptions, OptionsConfig}; class RadioButtons extends InputElement { use HasConfirm; use HasOptions; - private const MIN_OPTIONS = 1; - private const MAX_OPTIONS = 10; + protected function getOptionsConfig(): OptionsConfig + { + return OptionsConfig::new() + ->setMinOptions(1) + ->setMaxOptions(10) + ->setMaxInitialOptions(1); + } public function validate(): void { - $this->validateOptions(); - - $hasInitial = false; - - foreach ($this->options as $option) { - $option->validate(); - - if ($option->isInitial()) { - if ($hasInitial) { - throw new Exception('Only one initial Option is allowed for Radio Buttons'); - } - $hasInitial = true; - } - } + $this->validateInitialOptions(); if (!empty($this->confirm)) { $this->confirm->validate(); } - - if (count($this->options) > self::MAX_OPTIONS) { - throw new Exception('Option Size cannot exceed %d', [self::MAX_OPTIONS]); - } - - if (count($this->options) < self::MIN_OPTIONS) { - throw new Exception('Option Size must be at least %d', [self::MIN_OPTIONS]); - } } /** @@ -52,16 +34,7 @@ public function validate(): void */ public function toArray(): array { - $data = parent::toArray(); - - foreach ($this->options as $option) { - if ($option->isInitial()) { - $data['initial_option'] = $option->toArray(); - break; - } - } - - $data += $this->getOptionsAsArray(); + $data = parent::toArray() + $this->getOptionsAsArray() + $this->getInitialOptionsAsArray(); if (!empty($this->confirm)) { $data['confirm'] = $this->confirm->toArray(); diff --git a/src/Inputs/SelectMenus/ChannelSelectMenu.php b/src/Inputs/SelectMenus/ChannelSelectMenu.php index 6592b55..2d2d99d 100644 --- a/src/Inputs/SelectMenus/ChannelSelectMenu.php +++ b/src/Inputs/SelectMenus/ChannelSelectMenu.php @@ -9,6 +9,9 @@ class ChannelSelectMenu extends SelectMenu /** @var string */ private $initialChannel; + /** @var bool */ + private $responseUrlEnabled; + /** * @param string $initialChannel * @return static @@ -20,6 +23,17 @@ public function initialChannel(string $initialChannel): self return $this; } + /** + * @param bool $enabled + * @return static + */ + public function responseUrlEnabled(bool $enabled): self + { + $this->responseUrlEnabled = $enabled; + + return $this; + } + /** * @return array */ @@ -31,6 +45,10 @@ public function toArray(): array $data['initial_channel'] = $this->initialChannel; } + if (!empty($this->responseUrlEnabled)) { + $data['response_url_enabled'] = $this->responseUrlEnabled; + } + return $data; } } diff --git a/src/Inputs/SelectMenus/ConversationSelectMenu.php b/src/Inputs/SelectMenus/ConversationSelectMenu.php index f4c7c01..2a10be5 100644 --- a/src/Inputs/SelectMenus/ConversationSelectMenu.php +++ b/src/Inputs/SelectMenus/ConversationSelectMenu.php @@ -9,6 +9,12 @@ class ConversationSelectMenu extends SelectMenu /** @var string */ private $initialConversation; + /** @var bool */ + private $responseUrlEnabled; + + /** @var bool */ + private $defaultToCurrentConversation; + /** * @param string $initialConversation * @return static @@ -20,6 +26,30 @@ public function initialConversation(string $initialConversation): self return $this; } + /** + * @param bool $enabled + * @return static + */ + public function responseUrlEnabled(bool $enabled): self + { + $this->responseUrlEnabled = $enabled; + + return $this; + } + + /** + * @param bool $enabled + * @return static + */ + public function defaultToCurrentConversation(bool $enabled): self + { + $this->defaultToCurrentConversation = $enabled; + + return $this; + } + + // @TODO: filter - https://api.slack.com/reference/block-kit/block-elements#conversation_select + /** * @return array */ @@ -31,6 +61,14 @@ public function toArray(): array $data['initial_conversation'] = $this->initialConversation; } + if (!empty($this->responseUrlEnabled)) { + $data['response_url_enabled'] = $this->responseUrlEnabled; + } + + if (!empty($this->defaultToCurrentConversation)) { + $data['default_to_current_conversation'] = $this->defaultToCurrentConversation; + } + return $data; } } diff --git a/src/Inputs/SelectMenus/ExternalSelectMenu.php b/src/Inputs/SelectMenus/ExternalSelectMenu.php index 3d1e2a6..8687591 100644 --- a/src/Inputs/SelectMenus/ExternalSelectMenu.php +++ b/src/Inputs/SelectMenus/ExternalSelectMenu.php @@ -21,7 +21,7 @@ class ExternalSelectMenu extends SelectMenu */ public function initialOption(string $name, string $value): self { - $this->initialOption = new Option($name, $value); + $this->initialOption = Option::new($name, $value); $this->initialOption->setParent($this); return $this; diff --git a/src/Inputs/SelectMenus/MultiConversationSelectMenu.php b/src/Inputs/SelectMenus/MultiConversationSelectMenu.php index 0415c78..8f280ea 100644 --- a/src/Inputs/SelectMenus/MultiConversationSelectMenu.php +++ b/src/Inputs/SelectMenus/MultiConversationSelectMenu.php @@ -9,6 +9,9 @@ class MultiConversationSelectMenu extends MultiSelectMenu /** @var string[] */ private $initialConversations; + /** @var bool */ + private $defaultToCurrentConversation; + /** * @param string[] $initialConversations * @return static @@ -20,6 +23,19 @@ public function initialConversations(array $initialConversations): self return $this; } + /** + * @param bool $enabled + * @return static + */ + public function defaultToCurrentConversation(bool $enabled): self + { + $this->defaultToCurrentConversation = $enabled; + + return $this; + } + + // @TODO: filter - https://api.slack.com/reference/block-kit/block-elements#conversation_select + /** * @return array */ @@ -31,6 +47,10 @@ public function toArray(): array $data['initial_conversations'] = $this->initialConversations; } + if (!empty($this->defaultToCurrentConversation)) { + $data['default_to_current_conversation'] = $this->defaultToCurrentConversation; + } + return $data; } } diff --git a/src/Inputs/SelectMenus/MultiExternalSelectMenu.php b/src/Inputs/SelectMenus/MultiExternalSelectMenu.php index fd11a2f..f5f7ff6 100644 --- a/src/Inputs/SelectMenus/MultiExternalSelectMenu.php +++ b/src/Inputs/SelectMenus/MultiExternalSelectMenu.php @@ -21,7 +21,7 @@ class MultiExternalSelectMenu extends MultiSelectMenu public function initialOptions(array $options): self { foreach ($options as $name => $value) { - $option = new Option((string) $name, (string) $value); + $option = Option::new((string) $name, (string) $value); $option->setParent($this); $this->initialOptions[] = $option; } diff --git a/src/Inputs/SelectMenus/MultiSelectMenu.php b/src/Inputs/SelectMenus/MultiSelectMenu.php index c7a1a39..4fc9e66 100644 --- a/src/Inputs/SelectMenus/MultiSelectMenu.php +++ b/src/Inputs/SelectMenus/MultiSelectMenu.php @@ -6,8 +6,8 @@ abstract class MultiSelectMenu extends SelectMenu { - /** @var int */ - private $maxSelectedItems; + /** @var int|null */ + protected $maxSelectedItems; /** * @param int $maxSelectedItems diff --git a/src/Inputs/SelectMenus/MultiStaticSelectMenu.php b/src/Inputs/SelectMenus/MultiStaticSelectMenu.php index c36c473..e2d327a 100644 --- a/src/Inputs/SelectMenus/MultiStaticSelectMenu.php +++ b/src/Inputs/SelectMenus/MultiStaticSelectMenu.php @@ -4,55 +4,29 @@ namespace Jeremeamia\Slack\BlockKit\Inputs\SelectMenus; -use Jeremeamia\Slack\BlockKit\Partials\HasOptionGroups; -use Jeremeamia\Slack\BlockKit\Partials\Option; +use Jeremeamia\Slack\BlockKit\Partials\{HasOptionGroups, OptionsConfig}; class MultiStaticSelectMenu extends MultiSelectMenu { use HasOptionGroups; - /** @var Option[] */ - private $initialOptions; - - /** - * @param array $options - * @return self - */ - public function initialOptions(array $options): self + protected function getOptionsConfig(): OptionsConfig { - foreach ($options as $name => $value) { - $option = new Option((string) $name, (string) $value); - $option->setParent($this); - $this->initialOptions[] = $option; - } - - return $this; + return OptionsConfig::new() + ->setMinOptions(1) + ->setMaxOptions(100) + ->setMaxInitialOptions($this->maxSelectedItems ?? null); } public function validate(): void { parent::validate(); - $this->validateOptionGroups(); - - if (!empty($this->initialOptions)) { - foreach ($this->initialOptions as $option) { - $option->validate(); - } - } + $this->validateInitialOptions(); } public function toArray(): array { - $data = parent::toArray(); - $data += $this->getOptionGroupsAsArray(); - - if (!empty($this->initialOptions)) { - $data['initial_options'] = array_map(function (Option $option) { - return $option->toArray(); - }, $this->initialOptions); - } - - return $data; + return parent::toArray() + $this->getOptionGroupsAsArray() + $this->getInitialOptionsAsArray(); } } diff --git a/src/Inputs/SelectMenus/StaticSelectMenu.php b/src/Inputs/SelectMenus/StaticSelectMenu.php index 7b7b954..95f825a 100644 --- a/src/Inputs/SelectMenus/StaticSelectMenu.php +++ b/src/Inputs/SelectMenus/StaticSelectMenu.php @@ -4,27 +4,18 @@ namespace Jeremeamia\Slack\BlockKit\Inputs\SelectMenus; -use Jeremeamia\Slack\BlockKit\Partials\HasOptionGroups; -use Jeremeamia\Slack\BlockKit\Partials\Option; +use Jeremeamia\Slack\BlockKit\Partials\{HasOptionGroups, OptionsConfig}; class StaticSelectMenu extends SelectMenu { use HasOptionGroups; - /** @var Option */ - private $initialOption; - - /** - * @param string $name - * @param string $value - * @return self - */ - public function initialOption(string $name, string $value): self + protected function getOptionsConfig(): OptionsConfig { - $this->initialOption = new Option($name, $value); - $this->initialOption->setParent($this); - - return $this; + return OptionsConfig::new() + ->setMinOptions(1) + ->setMaxOptions(100) + ->setMaxInitialOptions(1); } public function validate(): void @@ -32,21 +23,13 @@ public function validate(): void parent::validate(); $this->validateOptionGroups(); - - if (!empty($this->initialOption)) { - $this->initialOption->validate(); - } + $this->validateInitialOptions(); } public function toArray(): array { - $data = parent::toArray(); - $data += $this->getOptionGroupsAsArray(); - - if (!empty($this->initialOption)) { - $data['initial_option'] = $this->initialOption->toArray(); - } - - return $data; + return parent::toArray() + + $this->getOptionGroupsAsArray() + + $this->getInitialOptionsAsArray(); } } diff --git a/src/Partials/HasOptionGroups.php b/src/Partials/HasOptionGroups.php index 12e0bcc..82d21f3 100644 --- a/src/Partials/HasOptionGroups.php +++ b/src/Partials/HasOptionGroups.php @@ -33,7 +33,7 @@ public function optionGroups(array $optionGroups): self */ public function optionGroup(string $label, array $options): self { - $group = new OptionGroup($label, $options); + $group = OptionGroup::new($label, $options); $group->setParent($this); $this->optionGroups[] = $group; diff --git a/src/Partials/HasOptions.php b/src/Partials/HasOptions.php index cd6141a..570f4e4 100644 --- a/src/Partials/HasOptions.php +++ b/src/Partials/HasOptions.php @@ -9,16 +9,59 @@ trait HasOptions { /** @var Option[]|array */ - private $options; + private $options = []; + + /** @var Option[]|array */ + private $initialOptions = []; + + /** @var OptionsConfig|null */ + private $config; /** - * @param array $options + * @return OptionsConfig + */ + private function config(): OptionsConfig + { + if (!$this->config) { + $this->config = $this->getOptionsConfig(); + } + + return $this->config; + } + + /** + * @return OptionsConfig + */ + protected function getOptionsConfig(): OptionsConfig + { + return new OptionsConfig(); + } + + /** + * @param Option $option + * @param bool $isInitial * @return static */ - public function options(array $options): self + public function addOption(Option $option, bool $isInitial = false): self { - foreach ($options as $text => $value) { - $this->option((string) $text, (string) $value); + $option->setParent($this); + $this->options[] = $option; + + if ($isInitial) { + $this->initialOptions[] = $option; + } + + return $this; + } + + /** + * @param Option[] $options + * @return static + */ + public function addOptions(array $options): self + { + foreach ($options as $option) { + $this->addOption($option); } return $this; @@ -32,22 +75,92 @@ public function options(array $options): self */ public function option(string $text, string $value, bool $isInitial = false): self { - $option = new Option($text, $value, $isInitial); - $option->setParent($this); - $this->options[] = $option; + return $this->addOption(Option::new($text, $value), $isInitial); + } + + /** + * @param array|string[] $options + * @return static + */ + public function options(array $options): self + { + foreach ($options as $text => $value) { + $this->addOption(Option::new((string) $text, (string) $value)); + } + + return $this; + } + + /** + * @param string $name + * @param string $value + * @return self + */ + public function initialOption(string $name, string $value): self + { + $initialOption = Option::new($name, $value); + $initialOption->setParent($this); + $this->initialOptions[] = $initialOption; return $this; } - protected function validateOptions() + /** + * @param array $options + * @return self + */ + public function initialOptions(array $options): self + { + foreach ($options as $name => $value) { + $this->initialOption((string) $name, (string) $value); + } + + return $this; + } + + protected function validateOptions(): void { - if (empty($this->options)) { - throw new Exception('You must provide "options".'); + $minOptions = (int) $this->config()->getMinOptions(); + if (empty($this->options) || count($this->options) < $minOptions) { + throw new Exception('You must provide at least %d "options" for %s.', [$minOptions, static::class]); + } + + $maxOptions = $this->config()->getMaxOptions(); + if ($maxOptions !== null && count($this->options) > $maxOptions) { + throw new Exception('You must not provide more than %d "options" for %s.', [$maxOptions, static::class]); } foreach ($this->options as $option) { $option->validate(); } + + $maxInitialOptions = $this->config()->getMaxInitialOptions(); + if ($maxInitialOptions !== null && count($this->initialOptions) > $maxInitialOptions) { + throw new Exception( + 'You must not provide more than %d "initial_options" for %s.', + [$maxInitialOptions, static::class] + ); + } + + foreach ($this->initialOptions as $initialOption) { + $initialOption->validate(); + } + } + + protected function validateInitialOptions(): void + { + $maxInitialOptions = $this->config()->getMaxInitialOptions(); + + if ($maxInitialOptions !== null && count($this->initialOptions) > $maxInitialOptions) { + throw new Exception( + 'You must not provide more than %d "initial_options" for %s.', + [$maxInitialOptions, static::class] + ); + } + + foreach ($this->initialOptions as $initialOption) { + $initialOption->validate(); + } } protected function getOptionsAsArray(): array @@ -56,4 +169,21 @@ protected function getOptionsAsArray(): array return $option->toArray(); }, $this->options)]; } + + protected function getInitialOptionsAsArray(): array + { + if (empty($this->initialOptions)) { + return []; + } + + $maxInitialOptions = (int) $this->config()->getMaxInitialOptions(); + + if ($maxInitialOptions === 1) { + return ['initial_option' => $this->initialOptions[0]->toArray()]; + } + + return ['initial_options' => array_map(function (Option $initialOption) { + return $initialOption->toArray(); + }, $this->initialOptions)]; + } } diff --git a/src/Partials/Option.php b/src/Partials/Option.php index 1d7ac52..dc69b73 100644 --- a/src/Partials/Option.php +++ b/src/Partials/Option.php @@ -4,8 +4,11 @@ namespace Jeremeamia\Slack\BlockKit\Partials; -use Jeremeamia\Slack\BlockKit\{Element, Exception}; +use Jeremeamia\Slack\BlockKit\{Element, Exception, Type}; +/** + * @see https://api.slack.com/reference/block-kit/composition-objects#option + */ class Option extends Element { /** @var PlainText */ @@ -14,17 +17,30 @@ class Option extends Element /** @var string */ private $value; - /** @var bool */ - private $isInitial = false; + /** @var PlainText Description text for option. NOTE: Radio Button and Checkbox groups only. */ + private $description; - public function __construct(?string $text = null, string $value, bool $isInitial = false) + /** @var string URL to load in browser when option is clicked. NOTE: Overflow Menu only. */ + private $url; + + /** + * @param string|null $text + * @param string|null $value + * @return Option + */ + public static function new(?string $text = null, ?string $value = null): self { + $option = new self(); + if ($text !== null) { - $this->text($text); + $option->text($text); } - $this->value($value); - $this->isInitial = $isInitial; + if ($value !== null) { + $option->value($value); + } + + return $option; } /** @@ -59,11 +75,34 @@ public function value(string $value): self } /** - * @return bool + * @param PlainText $description + * @return self + */ + public function setDescription(PlainText $description): self + { + $this->description = $description->setParent($this); + + return $this; + } + + /** + * @param string $description + * @return static + */ + public function description(string $description): self + { + return $this->setDescription(new PlainText($description)); + } + + /** + * @param string $url + * @return static */ - public function isInitial(): bool + public function url(string $url): self { - return $this->isInitial; + $this->url = $url; + + return $this; } public function validate(): void @@ -72,11 +111,29 @@ public function validate(): void throw new Exception('Option element must contain a "text" element'); } - $this->text->validate(); + $this->text->validateWithLength(75, 1); - if (!is_string($this->value) || strlen($this->value) === 0) { + if (!is_string($this->value)) { throw new Exception('Option element must have a "value" value'); } + + Text::validateString($this->value, 75, 1); + + $parent = $this->getParent(); + + if (!empty($this->description)) { + $this->description->validateWithLength(75, 1); + if ($parent && !in_array($parent->getType(), [Type::CHECKBOXES, Type::RADIO_BUTTONS], true)) { + throw new Exception('Option "description" can only be applied to checkbox and radio button groups.'); + } + } + + if (!empty($this->url)) { + Text::validateString($this->url, 3000); + if ($parent && $parent->getType() !== Type::OVERFLOW_MENU) { + throw new Exception('Option "url" can only be applied to overflow menus.'); + } + } } /** @@ -84,9 +141,19 @@ public function validate(): void */ public function toArray(): array { - return parent::toArray() + [ + $data = [ 'text' => $this->text->toArray(), 'value' => $this->value, ]; + + if (!empty($this->description)) { + $data['description'] = $this->description->toArray(); + } + + if (!empty($this->url)) { + $data['url'] = $this->url; + } + + return parent::toArray() + $data; } } diff --git a/src/Partials/OptionGroup.php b/src/Partials/OptionGroup.php index a79f903..0af0625 100644 --- a/src/Partials/OptionGroup.php +++ b/src/Partials/OptionGroup.php @@ -13,15 +13,29 @@ class OptionGroup extends Element /** @var PlainText */ private $label; - public function __construct(?string $label = null, array $options = []) + /** + * @param string|null $label + * @param array|null $options + * @return OptionGroup + */ + public static function new(?string $label = null, ?array $options = null): self { + $optionGroup = new self(); + if ($label !== null) { - $this->label($label); + $optionGroup->label($label); } - if (!empty($options)) { - $this->options($options); + if ($options !== null) { + $optionGroup->options($options); } + + return $optionGroup; + } + + protected function getOptionsConfig(): OptionsConfig + { + return OptionsConfig::new()->setMinOptions(1)->setMaxOptions(100); } /** diff --git a/src/Partials/OptionsConfig.php b/src/Partials/OptionsConfig.php new file mode 100644 index 0000000..8f5a384 --- /dev/null +++ b/src/Partials/OptionsConfig.php @@ -0,0 +1,84 @@ +minOptions = 1; + } + + /** + * @return int|null + */ + public function getMinOptions(): ?int + { + return $this->minOptions; + } + + /** + * @param int|null $minOptions + * @return self + */ + public function setMinOptions(?int $minOptions): self + { + $this->minOptions = $minOptions; + + return $this; + } + + /** + * @return int|null + */ + public function getMaxOptions(): ?int + { + return $this->maxOptions; + } + + /** + * @param int|null $maxOptions + * @return self + */ + public function setMaxOptions(?int $maxOptions): self + { + $this->maxOptions = $maxOptions; + + return $this; + } + + /** + * @return int|null + */ + public function getMaxInitialOptions(): ?int + { + return $this->maxInitialOptions; + } + + /** + * @param int|null $maxInitialOptions + * @return self + */ + public function setMaxInitialOptions(?int $maxInitialOptions): self + { + $this->maxInitialOptions = $maxInitialOptions; + + return $this; + } +} diff --git a/src/Partials/Text.php b/src/Partials/Text.php index 6325eb1..8530609 100644 --- a/src/Partials/Text.php +++ b/src/Partials/Text.php @@ -24,7 +24,7 @@ public function text(string $text): self public function validate(): void { - $this->validateWithLength(); + self::validateString($this->text); } /** @@ -35,15 +35,27 @@ public function validate(): void */ public function validateWithLength(?int $max = null, int $min = 0): void { - if (!is_string($this->text)) { + self::validateString($this->text, $max, $min); + } + + /** + * Validate string length for textual element properties. + * + * @param string $text String to validate. + * @param int|null $max Max length, or null if it doesn't have a max. + * @param int $min Min length, defaults to 0. + */ + public static function validateString(string $text, ?int $max = null, int $min = 0): void + { + if (!is_string($text)) { throw new Exception('Text element must have a "text" value'); } - if (strlen($this->text) < $min) { + if (strlen($text) < $min) { throw new Exception('Text element must have a "text" value with a length of at least %d', [$min]); } - if (is_int($max) && strlen($this->text) > $max) { + if (is_int($max) && strlen($text) > $max) { throw new Exception('Text element must have a "text" value with a length of at most %d', [$max]); } } diff --git a/src/Surfaces/AppHome.php b/src/Surfaces/AppHome.php index 4aacdf4..c19a5f1 100644 --- a/src/Surfaces/AppHome.php +++ b/src/Surfaces/AppHome.php @@ -4,6 +4,11 @@ namespace Jeremeamia\Slack\BlockKit\Surfaces; +/** + * The App Home tab is a persistent, yet dynamic interface for apps that lives within the App Home for a user. + * + * @see https://api.slack.com/surfaces + */ class AppHome extends Surface { // Nothing special. diff --git a/src/Surfaces/Message.php b/src/Surfaces/Message.php index f0df9d1..ed76ab4 100644 --- a/src/Surfaces/Message.php +++ b/src/Surfaces/Message.php @@ -4,6 +4,12 @@ namespace Jeremeamia\Slack\BlockKit\Surfaces; +/** + * App-published messages are dynamic yet transient spaces. They allow users to complete workflows among their + * Slack conversations. + * + * @see https://api.slack.com/surfaces + */ class Message extends Surface { public const EPHEMERAL = 'ephemeral'; diff --git a/src/Surfaces/Modal.php b/src/Surfaces/Modal.php index 4ed7a2a..c89b5de 100644 --- a/src/Surfaces/Modal.php +++ b/src/Surfaces/Modal.php @@ -6,6 +6,12 @@ use Jeremeamia\Slack\BlockKit\{Blocks\Input, Exception, Partials\PlainText, Type}; +/** + * Modals provide focused spaces ideal for requesting and collecting data from users, or temporarily displaying dynamic + * and interactive information. + * + * @see https://api.slack.com/surfaces + */ class Modal extends Surface { private const MAX_LENGTH_TITLE = 24; diff --git a/tests/ElementTest.php b/tests/ElementTest.php index d883791..f17f73b 100644 --- a/tests/ElementTest.php +++ b/tests/ElementTest.php @@ -42,6 +42,27 @@ public function testCanUseNewAsFactoryForChildClasses() $this->assertInstanceOf(Section::class, $element); } + public function testCanSetExtraFieldsForArbitraryData() + { + $element = $this->getMockElement(); + + $element->setExtra('fizz', 'buzz'); + + $this->assertJsonData($element, [ + 'type' => 'mock', + 'text' => 'foo', + 'fizz' => 'buzz', + ]); + } + + public function testErrorIfExtraFieldIsInvalid() + { + $element = $this->getMockElement(); + + $this->expectException(Exception::class); + $element->setExtra('fizz', new \SplQueue()); + } + private function getMockElement(bool $valid = true): Element { return new class ($valid) extends Element { diff --git a/tests/Inputs/CheckBoxesTest.php b/tests/Inputs/CheckBoxesTest.php index 95e77b8..9630e8f 100644 --- a/tests/Inputs/CheckBoxesTest.php +++ b/tests/Inputs/CheckBoxesTest.php @@ -7,7 +7,6 @@ use Jeremeamia\Slack\BlockKit\Exception; use Jeremeamia\Slack\BlockKit\Inputs\Checkboxes; use Jeremeamia\Slack\BlockKit\Partials\Confirm; -use Jeremeamia\Slack\BlockKit\Partials\Option; use Jeremeamia\Slack\BlockKit\Tests\TestCase; use Jeremeamia\Slack\BlockKit\Type; diff --git a/tests/Inputs/OverflowMenuTest.php b/tests/Inputs/OverflowMenuTest.php index f170a91..9bd58d6 100644 --- a/tests/Inputs/OverflowMenuTest.php +++ b/tests/Inputs/OverflowMenuTest.php @@ -19,7 +19,7 @@ public function testOverflowMenuWithConfirm() { $input = (new OverflowMenu('overflow-identifier')) ->option('foo', 'foo') - ->option('bar', 'bar') + ->urlOption('bar', 'bar', 'https://example.org') ->option('foobar', 'foobar') ->setConfirm(new Confirm('Choose', 'Do you really want to choose this?', 'Yes choose')); @@ -40,6 +40,7 @@ public function testOverflowMenuWithConfirm() 'text' => 'bar', ], 'value' => 'bar', + 'url' => 'https://example.org' ], [ 'text' => [ diff --git a/tests/Inputs/RadioButtonsTest.php b/tests/Inputs/RadioButtonsTest.php index 4f80a15..4e34b7c 100644 --- a/tests/Inputs/RadioButtonsTest.php +++ b/tests/Inputs/RadioButtonsTest.php @@ -7,7 +7,6 @@ use Jeremeamia\Slack\BlockKit\Exception; use Jeremeamia\Slack\BlockKit\Inputs\RadioButtons; use Jeremeamia\Slack\BlockKit\Partials\Confirm; -use Jeremeamia\Slack\BlockKit\Partials\Option; use Jeremeamia\Slack\BlockKit\Tests\TestCase; use Jeremeamia\Slack\BlockKit\Type; diff --git a/tests/manual/menus.php b/tests/manual/menus.php index 28630c6..f2ef5b4 100644 --- a/tests/manual/menus.php +++ b/tests/manual/menus.php @@ -25,9 +25,8 @@ ->forStaticOptions() ->placeholder('Choose a letter?') ->option('a', 'x') - ->option('b', 'y') - ->option('c', 'z') - ->initialOption('b', 'y'); + ->option('b', 'y', true) + ->option('c', 'z'); $msg->newActions('b3') ->newSelectMenu('m4') ->forStaticOptions() @@ -68,7 +67,7 @@ ->mrkdwnText('Select from Overflow Menu') ->newOverflowMenuAccessory('m6') ->option('foo', 'foo') - ->option('bar', 'bar') + ->urlOption('bar', 'bar', 'https://example.org') ->option('foobar', 'foobar') ->setConfirm(new Confirm('Choose', 'Do you really want to choose this?', 'Yes choose')); //echo Slack::newRenderer()->forJson()->render($msg) . "\n";