diff --git a/SlackNet.EventsExample/AppHome.cs b/SlackNet.EventsExample/AppHome.cs index 41fab93..1f1ea4f 100644 --- a/SlackNet.EventsExample/AppHome.cs +++ b/SlackNet.EventsExample/AppHome.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using SlackNet.Blocks; using SlackNet.Events; @@ -11,9 +12,15 @@ namespace SlackNet.EventsExample public class AppHome : IEventHandler, IBlockActionHandler, IViewSubmissionHandler { private const string OpenModal = "open_modal"; - private const string InlineCheckboxes = "checkboxes"; private const string InputBlockId = "input_block"; private const string InputActionId = "text_input"; + private const string SingleSelectActionId = "single_select"; + private const string MultiSelectActionId = "multi_select"; + private const string DatePickerActionId = "date_picker"; + private const string TimePickerActionId = "time_picker"; + private const string RadioActionId = "radio"; + private const string CheckboxActionId = "checkbox"; + private const string SingleUserActionId = "single_user"; public static readonly string ModalCallbackId = "home_modal"; private readonly ISlackApiClient _slack; @@ -67,7 +74,7 @@ public async Task Handle(ButtonAction action, BlockActionRequest request) BlockId = "single_select_block", Element = new StaticSelectMenu { - ActionId = "single_select", + ActionId = SingleSelectActionId, Options = ExampleOptions() } }, @@ -77,7 +84,7 @@ public async Task Handle(ButtonAction action, BlockActionRequest request) BlockId = "multi_select_block", Element = new StaticMultiSelectMenu { - ActionId = "multi_select", + ActionId = MultiSelectActionId, Options = ExampleOptions() } }, @@ -85,7 +92,13 @@ public async Task Handle(ButtonAction action, BlockActionRequest request) { Label = "Date", BlockId = "date_block", - Element = new DatePicker { ActionId = "date_picker" } + Element = new DatePicker { ActionId = DatePickerActionId } + }, + new InputBlock + { + Label = "Time", + BlockId = "time_block", + Element = new TimePicker { ActionId = TimePickerActionId } }, new InputBlock { @@ -93,7 +106,7 @@ public async Task Handle(ButtonAction action, BlockActionRequest request) BlockId = "radio_block", Element = new RadioButtonGroup { - ActionId = "radio", + ActionId = RadioActionId, Options = ExampleOptions() } }, @@ -103,7 +116,7 @@ public async Task Handle(ButtonAction action, BlockActionRequest request) BlockId = "checkbox_block", Element = new CheckboxGroup { - ActionId = "checkbox", + ActionId = CheckboxActionId, Options = ExampleOptions() } }, @@ -113,7 +126,7 @@ public async Task Handle(ButtonAction action, BlockActionRequest request) BlockId = "single_user_block", Element = new UserSelectMenu { - ActionId = "single_user" + ActionId = SingleUserActionId } } }, @@ -134,10 +147,32 @@ public async Task Handle(ButtonAction action, BlockActionRequest request) public async Task Handle(ViewSubmission viewSubmission) { + var state = viewSubmission.View.State; + var values = new Dictionary + { + + { "Input", state.GetValue(InputActionId).Value }, + { "Single-select", state.GetValue(SingleSelectActionId).SelectedOption?.Text.Text ?? "none" }, + { "Multi-select", string.Join(", ", state.GetValue(MultiSelectActionId).SelectedOptions.Select(o => o.Text).DefaultIfEmpty("none")) }, + { "Date", state.GetValue(DatePickerActionId).SelectedDate?.ToString("yyyy-MM-dd") ?? "none" }, + { "Time", state.GetValue(TimePickerActionId).SelectedTime?.ToString("hh\\:mm") ?? "none" }, + { "Radio options", state.GetValue(RadioActionId).SelectedOption?.Text.Text ?? "none" }, + { "Checkbox options", string.Join(", ", state.GetValue(CheckboxActionId).SelectedOptions.Select(o => o.Text).DefaultIfEmpty("none")) }, + { "Single user select", state.GetValue(SingleUserActionId).SelectedUser ?? "none" } + }; + await _slack.Chat.PostMessage(new Message { Channel = await UserIm(viewSubmission.User).ConfigureAwait(false), - Text = $"You entered: {viewSubmission.View.State.GetValue(InputActionId).Value}" + Text = $"You entered: {state.GetValue(InputActionId).Value}", + Blocks = + { + new SectionBlock + { + Text = new Markdown("You entered:\n" + + string.Join("\n", values.Select(kv => $"*{kv.Key}:* {kv.Value}"))) + } + } }).ConfigureAwait(false); return ViewSubmissionResponse.Null; diff --git a/SlackNet.Tests/SerializationTests.cs b/SlackNet.Tests/SerializationTests.cs index 7c32c06..1697b55 100644 --- a/SlackNet.Tests/SerializationTests.cs +++ b/SlackNet.Tests/SerializationTests.cs @@ -173,6 +173,34 @@ public void DateTime_DeserializedFromDateString() }); } + [Test] + public void TimeSpan_SerializedAsHoursAndMinutes() + { + Serialize(new HasTimeSpanProperty { Required = new TimeSpan(1, 2, 3) }) + .ShouldBe(@"{""required"":""01:02""}"); + + Serialize(new HasTimeSpanProperty { Required = new TimeSpan(13, 14, 15), Optional = new TimeSpan(16, 17, 18) }) + .ShouldBe(@"{""required"":""13:14"",""optional"":""16:17""}"); + } + + [Test] + public void TimeSpan_DeserializedFromHoursAndMinutes() + { + Deserialize(@"{""required"":""01:02""}") + .Assert(o => + { + o.Required.ShouldBe(new TimeSpan(1, 2, 0)); + o.Optional.ShouldBeNull(); + }); + + Deserialize(@"{""required"":""13:14"",""optional"":""16:17""}") + .Assert(o => + { + o.Required.ShouldBe(new TimeSpan(13, 14, 0)); + o.Optional.ShouldBe(new TimeSpan(16, 17, 0)); + }); + } + private string Serialize(object obj) => JsonConvert.SerializeObject(obj, _sut); private T Deserialize(string json) => JsonConvert.DeserializeObject(json, _sut); @@ -224,5 +252,11 @@ class HasDateTimeProperty public DateTime Required { get; set; } public DateTime? Optional { get; set; } } + + class HasTimeSpanProperty + { + public TimeSpan Required { get; set; } + public TimeSpan? Optional { get; set; } + } } } diff --git a/SlackNet/Blocks/DatePicker.cs b/SlackNet/Blocks/DatePicker.cs index afa7419..515ba9c 100644 --- a/SlackNet/Blocks/DatePicker.cs +++ b/SlackNet/Blocks/DatePicker.cs @@ -4,7 +4,6 @@ namespace SlackNet.Blocks { /// /// An element which lets users easily select a date from a calendar style UI. - /// Date picker elements can be used inside of section and actions blocks. /// [SlackType("datepicker")] public class DatePicker : ActionElement, IInputBlockElement @@ -12,7 +11,7 @@ public class DatePicker : ActionElement, IInputBlockElement public DatePicker() : base("datepicker") { } /// - /// A plain text object that defines the placeholder text shown on the menu. + /// A plain text object that defines the placeholder text shown on the datepicker. /// public PlainText Placeholder { get; set; } diff --git a/SlackNet/Blocks/TimePicker.cs b/SlackNet/Blocks/TimePicker.cs new file mode 100644 index 0000000..7e9e709 --- /dev/null +++ b/SlackNet/Blocks/TimePicker.cs @@ -0,0 +1,35 @@ +using System; + +namespace SlackNet.Blocks +{ + /// + /// An element which allows selection of a time of day. + /// + [SlackType("timepicker")] + public class TimePicker : ActionElement, IInputBlockElement + { + public TimePicker() : base("timepicker") { } + + /// + /// A plain text object that defines the placeholder text shown on the timepicker. + /// + public PlainText Placeholder { get; set; } + + /// + /// The initial time that is selected when the element is loaded. + /// + public TimeSpan? InitialTime { get; set; } + } + + [SlackType("timepicker")] + public class TimePickerAction : BlockAction + { + public TimeSpan? SelectedTime { get; set; } + } + + [SlackType("timepicker")] + public class TimePickerValue : ElementValue + { + public TimeSpan? SelectedTime { get; set; } + } +} \ No newline at end of file diff --git a/SlackNet/Default.cs b/SlackNet/Default.cs index 94e6061..65e9cce 100644 --- a/SlackNet/Default.cs +++ b/SlackNet/Default.cs @@ -28,11 +28,12 @@ private static JsonSerializerSettings SerializerSettings(ISlackTypeResolver slac Converters = { new EnumNameConverter(namingStrategy), + new TimeSpanConverter(), new SlackTypeConverter(slackTypeResolver) } }; } - + public static ISlackTypeResolver SlackTypeResolver() => new SlackTypeResolver(AssembliesContainingSlackTypes); public static ISlackTypeResolver SlackTypeResolver(params Assembly[] assembliesContainingSlackTypes) => new SlackTypeResolver(assembliesContainingSlackTypes); diff --git a/SlackNet/Serialization/EnumNameConverter.cs b/SlackNet/Serialization/EnumNameConverter.cs index f5b85f9..30383f8 100644 --- a/SlackNet/Serialization/EnumNameConverter.cs +++ b/SlackNet/Serialization/EnumNameConverter.cs @@ -8,7 +8,7 @@ namespace SlackNet { - class EnumNameConverter : JsonConverter + class EnumNameConverter : JsonConverter { private readonly NamingStrategy _namingStrategy; public EnumNameConverter(NamingStrategy namingStrategy) => _namingStrategy = namingStrategy; @@ -21,13 +21,8 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s writer.WriteValue(SerializedName((Enum)value)); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override Enum ReadJsonValue(JsonReader reader, Type objectType, Enum existingValue, bool hasExistingValue, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) - return IsNullable(objectType) - ? throw new JsonSerializationException(string.Format(CultureInfo.InvariantCulture, "Cannot convert null value to {0}.", objectType)) - : (object)null; - try { if (reader.TokenType == JsonToken.String) @@ -43,7 +38,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist throw new JsonSerializationException(string.Format(CultureInfo.InvariantCulture, "Unexpected token {0} when parsing enum.", reader.TokenType)); } - private object ParseEnumName(Type type, string name) => + private Enum ParseEnumName(Type type, string name) => Enum.GetValues(type) .Cast() .FirstOrDefault(e => SerializedName(e) == name); @@ -62,19 +57,5 @@ private string SerializedName(Enum enumValue) return _namingStrategy.GetPropertyName(explicitName ?? enumText, explicitName != null); } - - public override bool CanConvert(Type objectType) => typeof(Enum).GetTypeInfo().IsAssignableFrom(UnderlyingType(objectType).GetTypeInfo()); - - private static bool IsNullable(Type objectType) - { - var typeInfo = objectType.GetTypeInfo(); - return typeInfo.IsGenericType - && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>); - } - - private static Type UnderlyingType(Type objectType) => - IsNullable(objectType) - ? Nullable.GetUnderlyingType(objectType) - : objectType; } -} +} \ No newline at end of file diff --git a/SlackNet/Serialization/JsonConverter.cs b/SlackNet/Serialization/JsonConverter.cs new file mode 100644 index 0000000..24ecc8b --- /dev/null +++ b/SlackNet/Serialization/JsonConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Globalization; +using System.Reflection; +using Newtonsoft.Json; + +namespace SlackNet +{ + abstract class JsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => + typeof(T).GetTypeInfo().IsAssignableFrom(UnderlyingType(objectType).GetTypeInfo()); + + protected static Type UnderlyingType(Type objectType) => + IsNullable(objectType) + ? Nullable.GetUnderlyingType(objectType) + : objectType; + + protected static bool IsNullable(Type objectType) + { + var typeInfo = objectType.GetTypeInfo(); + return typeInfo.IsGenericType + && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return IsNullable(objectType) + ? (object)null + : throw new JsonSerializationException(string.Format(CultureInfo.InvariantCulture, "Cannot convert null value to {0}.", objectType)); + + return ReadJsonValue(reader, objectType, existingValue == null ? default : (T)existingValue, existingValue != null, serializer); + } + + protected abstract T ReadJsonValue(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer); + } +} \ No newline at end of file diff --git a/SlackNet/Serialization/TimeSpanConverter.cs b/SlackNet/Serialization/TimeSpanConverter.cs new file mode 100644 index 0000000..1b8c121 --- /dev/null +++ b/SlackNet/Serialization/TimeSpanConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using Newtonsoft.Json; + +namespace SlackNet +{ + class TimeSpanConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var timeSpan = (TimeSpan?)value; + if (timeSpan.HasValue) + writer.WriteValue(timeSpan.Value.ToString("hh\\:mm")); + else + writer.WriteNull(); + } + + protected override TimeSpan ReadJsonValue(JsonReader reader, Type objectType, TimeSpan existingValue, bool hasExistingValue, JsonSerializer serializer) + { + try + { + return TimeSpan.Parse(reader.Value.ToString()); + } + catch (Exception ex) + { + throw new JsonSerializationException(string.Format(CultureInfo.InvariantCulture, "Error converting value {0} to type '{1}'", reader.Value, objectType), ex); + } + } + } +} \ No newline at end of file