Skip to content

Commit

Permalink
Adding TimePicker block element
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Oxtoby committed Oct 25, 2020
1 parent fad2669 commit 8a55d73
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 34 deletions.
51 changes: 43 additions & 8 deletions SlackNet.EventsExample/AppHome.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SlackNet.Blocks;
using SlackNet.Events;
Expand All @@ -11,9 +12,15 @@ namespace SlackNet.EventsExample
public class AppHome : IEventHandler<AppHomeOpened>, IBlockActionHandler<ButtonAction>, 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;
Expand Down Expand Up @@ -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()
}
},
Expand All @@ -77,23 +84,29 @@ public async Task Handle(ButtonAction action, BlockActionRequest request)
BlockId = "multi_select_block",
Element = new StaticMultiSelectMenu
{
ActionId = "multi_select",
ActionId = MultiSelectActionId,
Options = ExampleOptions()
}
},
new InputBlock
{
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
{
Label = "Radio options",
BlockId = "radio_block",
Element = new RadioButtonGroup
{
ActionId = "radio",
ActionId = RadioActionId,
Options = ExampleOptions()
}
},
Expand All @@ -103,7 +116,7 @@ public async Task Handle(ButtonAction action, BlockActionRequest request)
BlockId = "checkbox_block",
Element = new CheckboxGroup
{
ActionId = "checkbox",
ActionId = CheckboxActionId,
Options = ExampleOptions()
}
},
Expand All @@ -113,7 +126,7 @@ public async Task Handle(ButtonAction action, BlockActionRequest request)
BlockId = "single_user_block",
Element = new UserSelectMenu
{
ActionId = "single_user"
ActionId = SingleUserActionId
}
}
},
Expand All @@ -134,10 +147,32 @@ public async Task Handle(ButtonAction action, BlockActionRequest request)

public async Task<ViewSubmissionResponse> Handle(ViewSubmission viewSubmission)
{
var state = viewSubmission.View.State;
var values = new Dictionary<string, string>
{

{ "Input", state.GetValue<PlainTextInputValue>(InputActionId).Value },
{ "Single-select", state.GetValue<StaticSelectValue>(SingleSelectActionId).SelectedOption?.Text.Text ?? "none" },
{ "Multi-select", string.Join(", ", state.GetValue<StaticMultiSelectValue>(MultiSelectActionId).SelectedOptions.Select(o => o.Text).DefaultIfEmpty("none")) },
{ "Date", state.GetValue<DatePickerValue>(DatePickerActionId).SelectedDate?.ToString("yyyy-MM-dd") ?? "none" },
{ "Time", state.GetValue<TimePickerValue>(TimePickerActionId).SelectedTime?.ToString("hh\\:mm") ?? "none" },
{ "Radio options", state.GetValue<RadioButtonGroupValue>(RadioActionId).SelectedOption?.Text.Text ?? "none" },
{ "Checkbox options", string.Join(", ", state.GetValue<CheckboxGroupValue>(CheckboxActionId).SelectedOptions.Select(o => o.Text).DefaultIfEmpty("none")) },
{ "Single user select", state.GetValue<UserSelectValue>(SingleUserActionId).SelectedUser ?? "none" }
};

await _slack.Chat.PostMessage(new Message
{
Channel = await UserIm(viewSubmission.User).ConfigureAwait(false),
Text = $"You entered: {viewSubmission.View.State.GetValue<PlainTextInputValue>(InputActionId).Value}"
Text = $"You entered: {state.GetValue<PlainTextInputValue>(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;
Expand Down
34 changes: 34 additions & 0 deletions SlackNet.Tests/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HasTimeSpanProperty>(@"{""required"":""01:02""}")
.Assert(o =>
{
o.Required.ShouldBe(new TimeSpan(1, 2, 0));
o.Optional.ShouldBeNull();
});

Deserialize<HasTimeSpanProperty>(@"{""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<T>(string json) => JsonConvert.DeserializeObject<T>(json, _sut);
Expand Down Expand Up @@ -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; }
}
}
}
3 changes: 1 addition & 2 deletions SlackNet/Blocks/DatePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ namespace SlackNet.Blocks
{
/// <summary>
/// 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.
/// </summary>
[SlackType("datepicker")]
public class DatePicker : ActionElement, IInputBlockElement
{
public DatePicker() : base("datepicker") { }

/// <summary>
/// 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.
/// </summary>
public PlainText Placeholder { get; set; }

Expand Down
35 changes: 35 additions & 0 deletions SlackNet/Blocks/TimePicker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;

namespace SlackNet.Blocks
{
/// <summary>
/// An element which allows selection of a time of day.
/// </summary>
[SlackType("timepicker")]
public class TimePicker : ActionElement, IInputBlockElement
{
public TimePicker() : base("timepicker") { }

/// <summary>
/// A plain text object that defines the placeholder text shown on the timepicker.
/// </summary>
public PlainText Placeholder { get; set; }

/// <summary>
/// The initial time that is selected when the element is loaded.
/// </summary>
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; }
}
}
3 changes: 2 additions & 1 deletion SlackNet/Default.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
27 changes: 4 additions & 23 deletions SlackNet/Serialization/EnumNameConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace SlackNet
{
class EnumNameConverter : JsonConverter
class EnumNameConverter : JsonConverter<Enum>
{
private readonly NamingStrategy _namingStrategy;
public EnumNameConverter(NamingStrategy namingStrategy) => _namingStrategy = namingStrategy;
Expand All @@ -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)
Expand All @@ -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<Enum>()
.FirstOrDefault(e => SerializedName(e) == name);
Expand All @@ -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;
}
}
}
37 changes: 37 additions & 0 deletions SlackNet/Serialization/JsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Reflection;
using Newtonsoft.Json;

namespace SlackNet
{
abstract class JsonConverter<T> : 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);
}
}
30 changes: 30 additions & 0 deletions SlackNet/Serialization/TimeSpanConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Globalization;
using Newtonsoft.Json;

namespace SlackNet
{
class TimeSpanConverter : JsonConverter<TimeSpan>
{
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);
}
}
}
}

0 comments on commit 8a55d73

Please sign in to comment.