diff --git a/src/Inc.TeamAssistant.Appraiser.Application/Services/AppraiserSettingSectionProvider.cs b/src/Inc.TeamAssistant.Appraiser.Application/Services/AppraiserSettingSectionProvider.cs index 9d40b889..9938b76a 100644 --- a/src/Inc.TeamAssistant.Appraiser.Application/Services/AppraiserSettingSectionProvider.cs +++ b/src/Inc.TeamAssistant.Appraiser.Application/Services/AppraiserSettingSectionProvider.cs @@ -1,15 +1,16 @@ using Inc.TeamAssistant.Appraiser.Domain; using Inc.TeamAssistant.Primitives.FeatureProperties; +using Inc.TeamAssistant.Primitives.Languages; namespace Inc.TeamAssistant.Appraiser.Application.Services; internal sealed class AppraiserSettingSectionProvider : ISettingSectionProvider { - private readonly IReadOnlyDictionary _storyType = new Dictionary + private readonly IReadOnlyDictionary _storyType = new Dictionary { - [StoryType.Fibonacci] = "Constructor_FormSectionSetSettingsFibonacciDescription", - [StoryType.TShirt] = "Constructor_FormSectionSetSettingsTShirtDescription", - [StoryType.PowerOfTwo] = "Constructor_FormSectionSetSettingsPowerOfTwoDescription" + [StoryType.Fibonacci] = new("Constructor_FormSectionSetSettingsFibonacciDescription"), + [StoryType.TShirt] = new("Constructor_FormSectionSetSettingsTShirtDescription"), + [StoryType.PowerOfTwo] = new("Constructor_FormSectionSetSettingsPowerOfTwoDescription") }; public string FeatureName => "Appraiser"; @@ -19,12 +20,12 @@ public IReadOnlyCollection GetSections() return [ new SettingSection( - "Constructor_FormSectionSetSettingsAppraiserHeader", - "Constructor_FormSectionSetSettingsAppraiserHelp", + new("Constructor_FormSectionSetSettingsAppraiserHeader"), + new("Constructor_FormSectionSetSettingsAppraiserHelp"), [ new( AppraiserProperties.StoryTypeKey, - "Constructor_FormSectionSetSettingsStoryTypeFieldLabel", + new("Constructor_FormSectionSetSettingsStoryTypeFieldLabel"), GetValuesForStoryType().ToArray()) ]) ]; diff --git a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/FibonacciEstimationStrategy.cs b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/FibonacciEstimationStrategy.cs index 44182577..0a929818 100644 --- a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/FibonacciEstimationStrategy.cs +++ b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/FibonacciEstimationStrategy.cs @@ -31,10 +31,7 @@ public Estimation GetValue(int value) if (Estimations.TryGetValue(value, out var estimation)) return estimation; - throw new ArgumentOutOfRangeException( - nameof(value), - value, - $"Value is not valid for {nameof(FibonacciEstimationStrategy)}."); + throw new ArgumentOutOfRangeException(nameof(value), value, "State out of range."); } public int GetWeight(Story story) diff --git a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/PowerOfTwoEstimationStrategy.cs b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/PowerOfTwoEstimationStrategy.cs index d42baf4f..05228c77 100644 --- a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/PowerOfTwoEstimationStrategy.cs +++ b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/PowerOfTwoEstimationStrategy.cs @@ -31,10 +31,7 @@ public Estimation GetValue(int value) if (Estimations.TryGetValue(value, out var estimation)) return estimation; - throw new ArgumentOutOfRangeException( - nameof(value), - value, - $"Value is not valid for {nameof(FibonacciEstimationStrategy)}."); + throw new ArgumentOutOfRangeException(nameof(value), value, "State out of range."); } public int GetWeight(Story story) diff --git a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/TShirtEstimationStrategy.cs b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/TShirtEstimationStrategy.cs index aed665b1..3508fdd2 100644 --- a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/TShirtEstimationStrategy.cs +++ b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategies/TShirtEstimationStrategy.cs @@ -30,10 +30,7 @@ public Estimation GetValue(int value) if (Estimations.TryGetValue(value, out var estimation)) return estimation; - throw new ArgumentOutOfRangeException( - nameof(value), - value, - $"Value is not valid for {nameof(TShirtEstimationStrategy)}."); + throw new ArgumentOutOfRangeException(nameof(value), value, "State out of range."); } public int GetWeight(Story story) diff --git a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategyFactory.cs b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategyFactory.cs index dd189be5..188baa18 100644 --- a/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategyFactory.cs +++ b/src/Inc.TeamAssistant.Appraiser.Domain/EstimationStrategyFactory.cs @@ -24,8 +24,6 @@ public static IEstimationStrategy Create(StoryType storyType) { return Strategies.TryGetValue(storyType, out var strategy) ? strategy - : throw new ArgumentOutOfRangeException(nameof(storyType), - storyType, - $"StoryType is not valid for {nameof(EstimationStrategyFactory)}."); + : throw new ArgumentOutOfRangeException(nameof(storyType), storyType, "State out of range."); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/GeoJsonParser.cs b/src/Inc.TeamAssistant.CheckIn.Geo/GeoJsonParser.cs deleted file mode 100644 index 0751041f..00000000 --- a/src/Inc.TeamAssistant.CheckIn.Geo/GeoJsonParser.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Inc.TeamAssistant.CheckIn.Geo; - -internal sealed class GeoJsonParser -{ - public IEnumerable Convert(string json) - { - return Convert(JsonConvert.DeserializeObject(json)); - } - - private IEnumerable Convert(dynamic json) - { - foreach (var coordinates in ToPoints(json.geometry)) - { - yield return new ParsedGeoJson - { - Id = json.id, - Geometry = coordinates, - Properties = ((IEnumerable>)json.properties).ToDictionary( - k => k.Key.ToLowerInvariant(), - v => v.Value.ToString()) - }; - } - } - - private IEnumerable ToPoints(dynamic geometry) - { - if (null == geometry) yield break; - switch ((string)geometry.type) - { - case "Polygon": - yield return ((IEnumerable)ParseCoordinates(geometry.coordinates[0])).ToArray(); - break; - case "MultiPolygon": - foreach (dynamic item in geometry.coordinates) - { - yield return ((IEnumerable)ParseCoordinates(item[0])).ToArray(); - } - break; - default: - throw new NotImplementedException((string)geometry.type); - } - } - - private IEnumerable ParseCoordinates(dynamic coordinates) - { - foreach (var coordinate in coordinates) - { - yield return [(float)coordinate[0], (float)coordinate[1]]; - } - } -} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/GeoParser.cs b/src/Inc.TeamAssistant.CheckIn.Geo/GeoParser.cs new file mode 100644 index 00000000..16137172 --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/GeoParser.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using Inc.TeamAssistant.CheckIn.Application.Contracts; +using Inc.TeamAssistant.CheckIn.Geo.Models; + +namespace Inc.TeamAssistant.CheckIn.Geo; + +internal sealed class GeoParser +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + TypeInfoResolver = new GeometryResolver() + }; + + public IEnumerable Parse(IEnumerable json) + { + ArgumentNullException.ThrowIfNull(json); + + foreach (var line in json) + { + var item = JsonSerializer.Deserialize(line, JsonSerializerOptions); + + if (item is not null) + yield return new Region( + item.Properties.Name, + item.Id, + item.Properties.Type == "country" ? RegionType.Country : RegionType.Ocean, + item.Geometry.GetCoordinates().ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/GeoService.cs b/src/Inc.TeamAssistant.CheckIn.Geo/GeoService.cs index 58186e58..b34517ae 100644 --- a/src/Inc.TeamAssistant.CheckIn.Geo/GeoService.cs +++ b/src/Inc.TeamAssistant.CheckIn.Geo/GeoService.cs @@ -1,33 +1,33 @@ using GeoTimeZone; using Inc.TeamAssistant.CheckIn.Application.Contracts; +using Inc.TeamAssistant.CheckIn.Geo.Models; namespace Inc.TeamAssistant.CheckIn.Geo; internal sealed class GeoService : IGeoService { - private readonly GeoJsonParser _geoJsonParser; private readonly Region[] _regions; - public GeoService(RegionLoader regionLoader, GeoJsonParser geoJsonParser) + public GeoService(RegionLoader regionLoader, GeoParser geoParser) { - _geoJsonParser = geoJsonParser; - _regions = ParseInput(regionLoader.LoadFile()).ToArray(); + ArgumentNullException.ThrowIfNull(regionLoader); + ArgumentNullException.ThrowIfNull(geoParser); + + _regions = geoParser.Parse(regionLoader.LoadFile()).ToArray(); } public Region? FindCountry(double lat, double lng, params RegionType[] types) { - var coords = new[] { (float)lng, (float)lat }; - var subset = types.Any() + ArgumentNullException.ThrowIfNull(types); + + var point = new Point((float)lng, (float)lat); + var items = types.Any() ? _regions.Where(x => types.Any(y => y == x.Type)) : _regions; - foreach (var country in subset) - { - if (InPolygon(coords, country.Polygon)) - { - return country; - } - } + foreach (var item in items) + if (InPolygon(point, item.Polygon)) + return item; return null; } @@ -38,37 +38,21 @@ public TimeZoneInfo GetTimeZone(double lat, double lng) return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId.Result); } - private bool InPolygon(float[] point, float[][] polygon) + private bool InPolygon(Point point, float[][] polygon) { - var polygonLength = polygon.Length; - var c = false; + ArgumentNullException.ThrowIfNull(point); + ArgumentNullException.ThrowIfNull(polygon); + + var result = false; var i = 0; var j = 0; - for (i = 0, j = polygonLength - 1; i < polygonLength; j = i++) - { - if (polygon[i][1] > point[1] != (polygon[j][1] > point[1]) && - point[0] < (polygon[j][0] - polygon[i][0]) * (point[1] - polygon[i][1]) / + + for (i = 0, j = polygon.Length - 1; i < polygon.Length; j = i++) + if (polygon[i][1] > point.Lat != polygon[j][1] > point.Lat && + point.Lng < (polygon[j][0] - polygon[i][0]) * (point.Lat - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0]) - { - c = !c; - } - } + result = !result; - return c; - } - - private IEnumerable ParseInput(IEnumerable geojson) - { - foreach (var line in geojson) - { - foreach (var polygon in _geoJsonParser.Convert(line)) - { - yield return new Region( - polygon.Properties["name"], - polygon.Id, - polygon.Properties["type"] == "country" ? RegionType.Country : RegionType.Ocean, - polygon.Geometry); - } - } + return result; } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/GeometryResolver.cs b/src/Inc.TeamAssistant.CheckIn.Geo/GeometryResolver.cs new file mode 100644 index 00000000..ab21fb39 --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/GeometryResolver.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Inc.TeamAssistant.CheckIn.Geo.Models; + +namespace Inc.TeamAssistant.CheckIn.Geo; + +internal sealed class GeometryResolver : DefaultJsonTypeInfoResolver +{ + public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(options); + + var jsonTypeInfo = base.GetTypeInfo(type, options); + + if (jsonTypeInfo.Type == typeof(IGeometry)) + { + jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions + { + TypeDiscriminatorPropertyName = "type", + IgnoreUnrecognizedTypeDiscriminators = false, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization, + DerivedTypes = + { + new JsonDerivedType(typeof(PolygonGeometry), "Polygon"), + new JsonDerivedType(typeof(MultiPolygonGeometry), "MultiPolygon"), + } + }; + } + + return jsonTypeInfo; + } +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/Inc.TeamAssistant.CheckIn.Geo.csproj b/src/Inc.TeamAssistant.CheckIn.Geo/Inc.TeamAssistant.CheckIn.Geo.csproj index e7a15c9c..84f43f60 100644 --- a/src/Inc.TeamAssistant.CheckIn.Geo/Inc.TeamAssistant.CheckIn.Geo.csproj +++ b/src/Inc.TeamAssistant.CheckIn.Geo/Inc.TeamAssistant.CheckIn.Geo.csproj @@ -3,7 +3,6 @@ - \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/Models/GeoItem.cs b/src/Inc.TeamAssistant.CheckIn.Geo/Models/GeoItem.cs new file mode 100644 index 00000000..c6389006 --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/Models/GeoItem.cs @@ -0,0 +1,6 @@ +namespace Inc.TeamAssistant.CheckIn.Geo.Models; + +internal sealed record GeoItem( + string Id, + GeoProperties Properties, + IGeometry Geometry); \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/Models/GeoProperties.cs b/src/Inc.TeamAssistant.CheckIn.Geo/Models/GeoProperties.cs new file mode 100644 index 00000000..9661b525 --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/Models/GeoProperties.cs @@ -0,0 +1,3 @@ +namespace Inc.TeamAssistant.CheckIn.Geo.Models; + +internal sealed record GeoProperties(string Type, string Name); \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/Models/IGeometry.cs b/src/Inc.TeamAssistant.CheckIn.Geo/Models/IGeometry.cs new file mode 100644 index 00000000..ee415eb8 --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/Models/IGeometry.cs @@ -0,0 +1,6 @@ +namespace Inc.TeamAssistant.CheckIn.Geo.Models; + +internal interface IGeometry +{ + IEnumerable GetCoordinates(); +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/Models/MultiPolygonGeometry.cs b/src/Inc.TeamAssistant.CheckIn.Geo/Models/MultiPolygonGeometry.cs new file mode 100644 index 00000000..eb54e575 --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/Models/MultiPolygonGeometry.cs @@ -0,0 +1,12 @@ +namespace Inc.TeamAssistant.CheckIn.Geo.Models; + +internal sealed record MultiPolygonGeometry(float[][][][] Coordinates) + : IGeometry +{ + public IEnumerable GetCoordinates() + { + foreach (var coordinate in Coordinates) + foreach (var item in coordinate[0]) + yield return [item[0], item[1]]; + } +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/Models/Point.cs b/src/Inc.TeamAssistant.CheckIn.Geo/Models/Point.cs new file mode 100644 index 00000000..16c2ea9e --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/Models/Point.cs @@ -0,0 +1,3 @@ +namespace Inc.TeamAssistant.CheckIn.Geo.Models; + +internal sealed record Point(float Lng, float Lat); \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/Models/PolygonGeometry.cs b/src/Inc.TeamAssistant.CheckIn.Geo/Models/PolygonGeometry.cs new file mode 100644 index 00000000..09dfb3db --- /dev/null +++ b/src/Inc.TeamAssistant.CheckIn.Geo/Models/PolygonGeometry.cs @@ -0,0 +1,11 @@ +namespace Inc.TeamAssistant.CheckIn.Geo.Models; + +internal sealed record PolygonGeometry(float[][][] Coordinates) + : IGeometry +{ + public IEnumerable GetCoordinates() + { + foreach (var item in Coordinates[0]) + yield return [item[0], item[1]]; + } +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/ParsedGeoJson.cs b/src/Inc.TeamAssistant.CheckIn.Geo/ParsedGeoJson.cs deleted file mode 100644 index 17034bce..00000000 --- a/src/Inc.TeamAssistant.CheckIn.Geo/ParsedGeoJson.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Inc.TeamAssistant.CheckIn.Geo; - -internal sealed class ParsedGeoJson -{ - public string Id { get; set; } = default!; - public IDictionary Properties { get; set; } = default!; - public float[][] Geometry { get; set; } = default!; -} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.CheckIn.Geo/ServiceCollectionExtensions.cs b/src/Inc.TeamAssistant.CheckIn.Geo/ServiceCollectionExtensions.cs index 9a809dd8..e0ef8bc9 100644 --- a/src/Inc.TeamAssistant.CheckIn.Geo/ServiceCollectionExtensions.cs +++ b/src/Inc.TeamAssistant.CheckIn.Geo/ServiceCollectionExtensions.cs @@ -12,7 +12,7 @@ public static IServiceCollection AddCheckInGeo(this IServiceCollection services, services .AddSingleton(sp => ActivatorUtilities.CreateInstance(sp, webRootPath)) - .AddSingleton() + .AddSingleton() .AddSingleton(); return services; diff --git a/src/Inc.TeamAssistant.Connector.Application/QueryHandlers/GetWidgets/Converters/DashboardSettingsConverter.cs b/src/Inc.TeamAssistant.Connector.Application/QueryHandlers/GetWidgets/Converters/DashboardSettingsConverter.cs index 461a92f9..f153f8b6 100644 --- a/src/Inc.TeamAssistant.Connector.Application/QueryHandlers/GetWidgets/Converters/DashboardSettingsConverter.cs +++ b/src/Inc.TeamAssistant.Connector.Application/QueryHandlers/GetWidgets/Converters/DashboardSettingsConverter.cs @@ -13,7 +13,11 @@ public static IReadOnlyCollection Convert( ArgumentNullException.ThrowIfNull(features); return settings.Widgets - .Select(w => new WidgetDto(w.Type, w.Position, CanEnabled(w), w.IsEnabled)) + .Select(w => new WidgetDto( + w.Type, + w.Feature, + w.Position, + CanEnabled(w), w.IsEnabled)) .ToArray(); bool CanEnabled(DashboardWidget widget) => diff --git a/src/Inc.TeamAssistant.Connector.Application/Services/ContextCommandConverter.cs b/src/Inc.TeamAssistant.Connector.Application/Services/ContextCommandConverter.cs index 783911c9..66a9a7c6 100644 --- a/src/Inc.TeamAssistant.Connector.Application/Services/ContextCommandConverter.cs +++ b/src/Inc.TeamAssistant.Connector.Application/Services/ContextCommandConverter.cs @@ -68,10 +68,7 @@ private static BotCommandScope ToBotCommandScope(ContextScope contextScope) { ContextScope.Chats => BotCommandScope.AllGroupChats(), ContextScope.Private => BotCommandScope.Default(), - _ => throw new ArgumentOutOfRangeException( - nameof(contextScope), - contextScope, - "ContextScope value was out of range.") + _ => throw new ArgumentOutOfRangeException(nameof(contextScope), contextScope, "State out of range.") }; } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Connector.DataAccess/Messages.cs b/src/Inc.TeamAssistant.Connector.DataAccess/Messages.cs new file mode 100644 index 00000000..368b934c --- /dev/null +++ b/src/Inc.TeamAssistant.Connector.DataAccess/Messages.cs @@ -0,0 +1,8 @@ +using Inc.TeamAssistant.Primitives.Languages; + +namespace Inc.TeamAssistant.Connector.DataAccess; + +public static class Messages +{ + public static readonly MessageId Connector_PersonNotFound = new(nameof(Connector_PersonNotFound)); +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Connector.DataAccess/TeamAccessor.cs b/src/Inc.TeamAssistant.Connector.DataAccess/TeamAccessor.cs index 437fe6e5..63152bc2 100644 --- a/src/Inc.TeamAssistant.Connector.DataAccess/TeamAccessor.cs +++ b/src/Inc.TeamAssistant.Connector.DataAccess/TeamAccessor.cs @@ -44,6 +44,15 @@ public async Task> GetTeammates( return await _personRepository.Find(personId, token); } + public async Task GetPerson(long personId, CancellationToken token) + { + var person = await _personRepository.Find(personId, token); + if (person is null) + throw new TeamAssistantUserException(Messages.Connector_PersonNotFound, personId); + + return person; + } + public async Task GetClientLanguage(Guid botId, long personId, CancellationToken token) { return await _clientLanguageRepository.Get(botId, personId, token); diff --git a/src/Inc.TeamAssistant.Connector.Model/Queries/GetWidgets/WidgetDto.cs b/src/Inc.TeamAssistant.Connector.Model/Queries/GetWidgets/WidgetDto.cs index 633d3a8a..310bf990 100644 --- a/src/Inc.TeamAssistant.Connector.Model/Queries/GetWidgets/WidgetDto.cs +++ b/src/Inc.TeamAssistant.Connector.Model/Queries/GetWidgets/WidgetDto.cs @@ -2,6 +2,7 @@ namespace Inc.TeamAssistant.Connector.Model.Queries.GetWidgets; public sealed record WidgetDto( string Type, + string Feature, int Position, bool CanEnabled, bool IsEnabled) diff --git a/src/Inc.TeamAssistant.Gateway/Apps/MainApp.razor b/src/Inc.TeamAssistant.Gateway/Apps/MainApp.razor index 8f7a41a3..bb3d1992 100644 --- a/src/Inc.TeamAssistant.Gateway/Apps/MainApp.razor +++ b/src/Inc.TeamAssistant.Gateway/Apps/MainApp.razor @@ -4,8 +4,8 @@ @using Microsoft.AspNetCore.Components.Web @using Inc.TeamAssistant.WebUI.Features -@inject IRenderContext RenderContext @inject AnalyticsOptions AnalyticsOptions +@inject IRenderContext RenderContext diff --git a/src/Inc.TeamAssistant.Gateway/Services/Clients/MessageProviderCached.cs b/src/Inc.TeamAssistant.Gateway/Services/Clients/MessageProviderCached.cs index d095c0c0..e851b439 100644 --- a/src/Inc.TeamAssistant.Gateway/Services/Clients/MessageProviderCached.cs +++ b/src/Inc.TeamAssistant.Gateway/Services/Clients/MessageProviderCached.cs @@ -6,18 +6,15 @@ namespace Inc.TeamAssistant.Gateway.Services.Clients; internal sealed class MessageProviderCached : IMessageProvider { private readonly IMemoryCache _memoryCache; - private readonly ILogger _logger; private readonly IMessageProvider _messageProvider; private readonly TimeSpan _cacheAbsoluteExpiration; public MessageProviderCached( IMemoryCache memoryCache, - ILogger logger, IMessageProvider messageProvider, TimeSpan cacheAbsoluteExpiration) { _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _messageProvider = messageProvider ?? throw new ArgumentNullException(nameof(messageProvider)); _cacheAbsoluteExpiration = cacheAbsoluteExpiration; } @@ -32,13 +29,7 @@ public async Task>> Get(Cancellati c.SetAbsoluteExpiration(_cacheAbsoluteExpiration); return await _messageProvider.Get(token); }); - - if (cacheItem is null) - { - _logger.LogWarning("Can not get object with key {CacheKey} from cache", cacheKey); - return await _messageProvider.Get(token); - } - return cacheItem; + return cacheItem ?? await _messageProvider.Get(token); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Gateway/Services/ServerCore/QuickResponseCodeGeneratorCached.cs b/src/Inc.TeamAssistant.Gateway/Services/ServerCore/QuickResponseCodeGeneratorCached.cs index 394ae848..b6161648 100644 --- a/src/Inc.TeamAssistant.Gateway/Services/ServerCore/QuickResponseCodeGeneratorCached.cs +++ b/src/Inc.TeamAssistant.Gateway/Services/ServerCore/QuickResponseCodeGeneratorCached.cs @@ -6,18 +6,15 @@ namespace Inc.TeamAssistant.Gateway.Services.ServerCore; internal sealed class QuickResponseCodeGeneratorCached : IQuickResponseCodeGenerator { private readonly IMemoryCache _memoryCache; - private readonly ILogger _logger; private readonly IQuickResponseCodeGenerator _generator; private readonly TimeSpan _cacheAbsoluteExpiration; public QuickResponseCodeGeneratorCached( IMemoryCache memoryCache, - ILogger logger, IQuickResponseCodeGenerator generator, TimeSpan cacheAbsoluteExpiration) { _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _generator = generator ?? throw new ArgumentNullException(nameof(generator)); _cacheAbsoluteExpiration = cacheAbsoluteExpiration; } @@ -37,11 +34,7 @@ public string Generate(string data, string foreground, string background) return _generator.Generate(data, foreground, background); }); - - if (cacheItem is not null) - return cacheItem; - _logger.LogWarning("Can not get object with key {CacheKey} from cache", key); - return _generator.Generate(key, foreground, background); + return cacheItem ?? _generator.Generate(key, foreground, background); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Gateway/wwwroot/langs/en.json b/src/Inc.TeamAssistant.Gateway/wwwroot/langs/en.json index c4e8a6f4..205db892 100644 --- a/src/Inc.TeamAssistant.Gateway/wwwroot/langs/en.json +++ b/src/Inc.TeamAssistant.Gateway/wwwroot/langs/en.json @@ -94,7 +94,6 @@ "Navigation_Logout": "Logout", "CheckIn_GetStarted": "To get started, please add the bot to your chat room", - "CheckIn_PageTitle": "Our locations", "CheckIn_DefaultLayerTitle": "All", "CheckIn_ConnectLinkText": "Use {0} to see our locations", "CheckIn_AddLocationHelp": "Add location to the map", @@ -123,15 +122,16 @@ "Reviewer_NeedEndReview": "There is a review that needs to be done.", "Reviewer_NeedCorrection": "Revisions need to be made.", "Reviewer_StatsAttempts": "Attempts: {0}", - "Reviewer_StatsFirstTouch": "First touch time: {0} | {1} (average)", - "Reviewer_StatsFirstTouchAverage": "First touch time (average): {0}", - "Reviewer_StatsReview": "Review time: {0} | {1} (average)", - "Reviewer_StatsReviewAverage": "Review time (average): {0}", - "Reviewer_StatsCorrection": "Correction time: {0} | {1} (average)", - "Reviewer_StatsCorrectionAverage": "Correction time (average): {0}", + "Reviewer_StatsFirstTouch": "First touch: {0} | {1} (average)", + "Reviewer_StatsFirstTouchAverage": "First touch (average): {0}", + "Reviewer_StatsReview": "Review: {0} | {1} (average)", + "Reviewer_StatsReviewAverage": "Review (average): {0}", + "Reviewer_StatsCorrection": "Correction: {0} | {1} (average)", + "Reviewer_StatsCorrectionAverage": "Correction (average): {0}", "Reviewer_PreviewTitle": "Previewing a draft of the task for review.", "Reviewer_PreviewReviewerTemplate": "The task will be assigned to {0}", "Reviewer_PreviewCheckDescription": "You need to provide a link to the source code and a description.", + "Reviewer_PreviewCheckTeammate": "Participant {0} is not part of team {1}.", "Reviewer_PreviewEditHelp": "To edit a draft, please edit the original post.", "Reviewer_PreviewMoveToReview": "Submit", "Reviewer_PreviewRemoveDraft": "Cancel", @@ -189,8 +189,11 @@ "Dashboard_ReviewStateAccept": "Accept", "Dashboard_ReviewStats": "Stats", "Dashboard_FirstTouch": "First touch", + "Dashboard_FirstTouchHelp": "Time spent before the review begins", "Dashboard_Correction": "Correction", + "Dashboard_CorrectionHelp": "Time spent on adjustments", "Dashboard_Review": "Review", + "Dashboard_ReviewHelp": "Time spent on the review", "Dashboard_NoData": "No data", "Dashboard_TeammatesWidgetTitle": "Teammates", "Dashboard_BotWidgetTitle": "Bot has not selected", @@ -230,6 +233,7 @@ "Dashboard_Settings": "Settings", "Dashboard_Apply": "Apply", "Dashboard_SettingsApplied": "Settings applied", + "Dashboard_DisableWidgetHelpTemplate": "To enable the widget, you need to activate the feature \"{0}\"", "Footer_GroupNavigation": "Navigation", "Footer_GroupTech": "Tech", @@ -277,10 +281,10 @@ "Constructor_FeatureReviewerName": "Tasks review", "Constructor_FeatureRandomCoffeeName": "Random coffee", "Constructor_FeatureCheckInName": "Check in on map", - "Constructor_FeatureAppraiserDescription": "Conduct an estimate of tasks", - "Constructor_FeatureReviewerDescription": "Organize code review process", - "Constructor_FeatureRandomCoffeeDescription": "Collect a random coffee meetings", - "Constructor_FeatureCheckInDescription": "Manage distributed development teams", + "Constructor_FeatureAppraiserDescription": "Evaluate tasks using the planning poker method. Fibonacci numbers and degrees of two, as well as T-shirt sizes, are available for evaluation.", + "Constructor_FeatureReviewerDescription": "Organization of the code review process: passing tasks for review, notifying participants, and reviewing statistics.", + "Constructor_FeatureRandomCoffeeDescription": "Holding informal meetings over a cup of coffee, with random pairing of participants selected at intervals.", + "Constructor_FeatureCheckInDescription": "Manage a distributed development team by marking your location on the map and finding the locations of your colleagues.", "Constructor_FeatureAdd": "Add", "Constructor_FeatureRemove": "Remove", "Constructor_FormSectionFeaturesAvailableEmptyText": "No available features", diff --git a/src/Inc.TeamAssistant.Gateway/wwwroot/langs/ru.json b/src/Inc.TeamAssistant.Gateway/wwwroot/langs/ru.json index dd08c7ec..a4a7901f 100644 --- a/src/Inc.TeamAssistant.Gateway/wwwroot/langs/ru.json +++ b/src/Inc.TeamAssistant.Gateway/wwwroot/langs/ru.json @@ -59,7 +59,7 @@ "MetaAppraiserTitle": "Телеграм бот для оценки задач через planning poker", "MetaAppraiserDescription": "Инструмент для организации командной оценки задач с использованием planning poker online", "MetaAppraiserKeywords": "Телеграм бот, оценка задач, оценка, planning poker, planning poker online", - "MetaCheckInTitle": "Наши геолокаций", + "MetaCheckInTitle": "Наше местоположение", "MetaCheckInDescription": "Карта с указанием наших геолокаций", "MetaCheckInKeywords": "Карта, геолокация, регистрация геолокации, бот для регистрации геолокации, бот для регистрации геолокации в telegram", @@ -69,7 +69,7 @@ "OgAppraiserTitle": "Оценщик", "OgAppraiserDescription": "Телеграм бот для оценки задач через planning poker", "OgAppraiserImageText": "Оценщик - телеграм бот для оценки задач через planning poker", - "OgCheckInTitle": "Перемещения", + "OgCheckInTitle": "Наше местоположение", "OgCheckInDescription": "Телеграм бот для регистрации перемещений", "OgCheckInImageText": "CheckIn - телеграм бот для регистрации перемещений", @@ -94,7 +94,6 @@ "Navigation_Logout": "Выйти", "CheckIn_GetStarted": "Добавьте бота в чат для начала работы", - "CheckIn_PageTitle": "Наше местоположение", "CheckIn_DefaultLayerTitle": "Все", "CheckIn_ConnectLinkText": "Перейдите по ссылке {0} чтобы увидеть наши локации", "CheckIn_AddLocationHelp": "Добавить мою геолокацию на карту", @@ -123,15 +122,16 @@ "Reviewer_NeedEndReview": "Необходимо провести ревью.", "Reviewer_NeedCorrection": "Необходимо внести изменения.", "Reviewer_StatsAttempts": "Кол-во попыток: {0}", - "Reviewer_StatsFirstTouch": "Время первого касания: {0} | {1} (среднее)", - "Reviewer_StatsFirstTouchAverage": "Время первого касания (среднее): {0}", - "Reviewer_StatsReview": "Время ревью: {0} | {1} (среднее)", - "Reviewer_StatsReviewAverage": "Время ревью (среднее): {0}", - "Reviewer_StatsCorrection": "Время коррекции: {0} | {1} (среднее)", - "Reviewer_StatsCorrectionAverage": "Время коррекции (среднее): {0}", + "Reviewer_StatsFirstTouch": "Первое касание: {0} | {1} (среднее)", + "Reviewer_StatsFirstTouchAverage": "Первое касание (среднее): {0}", + "Reviewer_StatsReview": "Ревью: {0} | {1} (среднее)", + "Reviewer_StatsReviewAverage": "Ревью (среднее): {0}", + "Reviewer_StatsCorrection": "Коррекция: {0} | {1} (среднее)", + "Reviewer_StatsCorrectionAverage": "Коррекция (среднее): {0}", "Reviewer_PreviewTitle": "Предварительный просмотр черновика задачи на ревью.", "Reviewer_PreviewReviewerTemplate": "Задача будет назначена на {0}", "Reviewer_PreviewCheckDescription": "Необходимо указать ссылку на исходный код и описание.", + "Reviewer_PreviewCheckTeammate": "Участник {0} не подключен к команде {1}.", "Reviewer_PreviewEditHelp": "Для редактирования черновика необходимо отредактировать исходное сообщение.", "Reviewer_PreviewMoveToReview": "Отправить на ревью", "Reviewer_PreviewRemoveDraft": "Отменить", @@ -189,8 +189,11 @@ "Dashboard_ReviewStateAccept": "Принята", "Dashboard_ReviewStats": "Статистика", "Dashboard_FirstTouch": "Время первого касания", + "Dashboard_FirstTouchHelp": "Время, затраченное до начала проверки", "Dashboard_Correction": "Время корректировки", + "Dashboard_CorrectionHelp": "Время, затраченное на корректировку", "Dashboard_Review": "Время ревью", + "Dashboard_ReviewHelp": "Время, затраченное на проверку", "Dashboard_NoData": "Нет данных", "Dashboard_TeammatesWidgetTitle": "Товарищи по команде", "Dashboard_BotWidgetTitle": "Бот не выбран", @@ -230,6 +233,7 @@ "Dashboard_Settings": "Настройка", "Dashboard_Apply": "Применить", "Dashboard_SettingsApplied": "Настройки применены", + "Dashboard_DisableWidgetHelpTemplate": "Для включения виджета необходимо активировать функцию «{0}»", "Footer_GroupNavigation": "Навигация", "Footer_GroupTech": "Технологии", @@ -277,10 +281,10 @@ "Constructor_FeatureReviewerName": "Код ревью", "Constructor_FeatureRandomCoffeeName": "Проведение кофейных перерывов", "Constructor_FeatureCheckInName": "Отметки на карте", - "Constructor_FeatureAppraiserDescription": "Проведите оценку задач", - "Constructor_FeatureReviewerDescription": "Организуйте процесс рецензирования кода", - "Constructor_FeatureRandomCoffeeDescription": "Собирайте случайные встречи за чашкой кофе", - "Constructor_FeatureCheckInDescription": "Управляйте распределенными командами разработчиков", + "Constructor_FeatureAppraiserDescription": "Проведение оценки задач при помощи planning poker. Для оценки доступны числа Фибоначчи, майки, степени двойки.", + "Constructor_FeatureReviewerDescription": "Организация процесса code review. Передача задачи на ревью, оповещения участников, статистика по ревью.", + "Constructor_FeatureRandomCoffeeDescription": "Проведение неформальных встреч за чашкой кофе. Случайное формирование пар участников с выбранной периодичностью.", + "Constructor_FeatureCheckInDescription": "Управление распределенной командой разработчиков. Отмечайте свое местоположение на карте и узнавайте местоположение коллег.", "Constructor_FeatureAdd": "Добавить", "Constructor_FeatureRemove": "Удалить", "Constructor_FormSectionFeaturesAvailableEmptyText": "Нет доступных функций", diff --git a/src/Inc.TeamAssistant.Primitives/FeatureProperties/SelectValue.cs b/src/Inc.TeamAssistant.Primitives/FeatureProperties/SelectValue.cs index 110e12b2..757b9f89 100644 --- a/src/Inc.TeamAssistant.Primitives/FeatureProperties/SelectValue.cs +++ b/src/Inc.TeamAssistant.Primitives/FeatureProperties/SelectValue.cs @@ -1,3 +1,5 @@ +using Inc.TeamAssistant.Primitives.Languages; + namespace Inc.TeamAssistant.Primitives.FeatureProperties; -public sealed record SelectValue(string MessageId, string Value); \ No newline at end of file +public sealed record SelectValue(MessageId MessageId, string Value); \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingItem.cs b/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingItem.cs index 5b36149e..a90a752a 100644 --- a/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingItem.cs +++ b/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingItem.cs @@ -1,6 +1,8 @@ +using Inc.TeamAssistant.Primitives.Languages; + namespace Inc.TeamAssistant.Primitives.FeatureProperties; public sealed record SettingItem( string PropertyName, - string LabelMessageId, + MessageId LabelMessageId, IReadOnlyCollection Values); \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingSection.cs b/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingSection.cs index b3ab12e3..052ade13 100644 --- a/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingSection.cs +++ b/src/Inc.TeamAssistant.Primitives/FeatureProperties/SettingSection.cs @@ -1,6 +1,8 @@ +using Inc.TeamAssistant.Primitives.Languages; + namespace Inc.TeamAssistant.Primitives.FeatureProperties; public sealed record SettingSection( - string HeaderMessageId, - string HelpMessageId, + MessageId HeaderMessageId, + MessageId HelpMessageId, IReadOnlyCollection SettingItems); \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Primitives/GlobalSettings.cs b/src/Inc.TeamAssistant.Primitives/GlobalSettings.cs index a8ec78aa..ec26c89f 100644 --- a/src/Inc.TeamAssistant.Primitives/GlobalSettings.cs +++ b/src/Inc.TeamAssistant.Primitives/GlobalSettings.cs @@ -11,4 +11,6 @@ public static class GlobalSettings public static readonly string LinkForConnectTemplate = "https://t.me/{0}?start={1}"; public static readonly TimeSpan MinLoadingDelay = TimeSpan.FromMilliseconds(700); + + public static string TimeFormat = @"d\.hh\:mm"; } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Primitives/ITeamAccessor.cs b/src/Inc.TeamAssistant.Primitives/ITeamAccessor.cs index 151c6c7e..db8ba5ec 100644 --- a/src/Inc.TeamAssistant.Primitives/ITeamAccessor.cs +++ b/src/Inc.TeamAssistant.Primitives/ITeamAccessor.cs @@ -7,5 +7,6 @@ public interface ITeamAccessor Task<(Guid BotId, string TeamName)> GetTeamContext(Guid teamId, CancellationToken token); Task> GetTeammates(Guid teamId, DateTimeOffset now, CancellationToken token); Task FindPerson(long personId, CancellationToken token); + Task GetPerson(long personId, CancellationToken token); Task GetClientLanguage(Guid botId, long personId, CancellationToken token); } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Primitives/Icons.cs b/src/Inc.TeamAssistant.Primitives/Icons.cs new file mode 100644 index 00000000..fb1e8630 --- /dev/null +++ b/src/Inc.TeamAssistant.Primitives/Icons.cs @@ -0,0 +1,15 @@ +namespace Inc.TeamAssistant.Primitives; + +public static class Icons +{ + public static readonly string TrendUp = "👍"; + public static readonly string TrendDown = "👎"; + public static readonly string Alert = "❗"; + public static readonly string Waiting = "⏳"; + public static readonly string InProgress = "🤩"; + public static readonly string OnCorrection = "😱"; + public static readonly string Accept = "🤝"; + public static readonly string Comment = "💬"; + public static readonly string Start = "⭐"; + public static readonly string Ok = "👌"; +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.RandomCoffee.Application/Services/RandomCoffeeSettingSectionProvider.cs b/src/Inc.TeamAssistant.RandomCoffee.Application/Services/RandomCoffeeSettingSectionProvider.cs index d1c8f77e..d5bc18a3 100644 --- a/src/Inc.TeamAssistant.RandomCoffee.Application/Services/RandomCoffeeSettingSectionProvider.cs +++ b/src/Inc.TeamAssistant.RandomCoffee.Application/Services/RandomCoffeeSettingSectionProvider.cs @@ -12,16 +12,16 @@ public IReadOnlyCollection GetSections() return [ new SettingSection( - "Constructor_FormSectionSetSettingsRandomCoffeeHeader", - "Constructor_FormSectionSetSettingsRandomCoffeeHelp", + new("Constructor_FormSectionSetSettingsRandomCoffeeHeader"), + new("Constructor_FormSectionSetSettingsRandomCoffeeHelp"), [ new( RandomCoffeeProperties.RoundIntervalKey, - "Constructor_FormSectionSetSettingsRoundIntervalFieldLabel", + new("Constructor_FormSectionSetSettingsRoundIntervalFieldLabel"), GetValuesForRoundInterval().ToArray()), new( RandomCoffeeProperties.VotingIntervalKey, - "Constructor_FormSectionSetSettingsVotingIntervalFieldLabel", + new("Constructor_FormSectionSetSettingsVotingIntervalFieldLabel"), GetValuesForVotingInterval().ToArray()) ]) ]; @@ -29,17 +29,17 @@ public IReadOnlyCollection GetSections() private IEnumerable GetValuesForRoundInterval() { - yield return new SelectValue("Constructor_FormSectionSetSettingsRoundInterval1Description", "7.00:00:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsRoundInterval2Description", "14.00:00:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsRoundInterval3Description", "21.00:00:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsRoundInterval4Description", "28.00:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsRoundInterval1Description"), "7.00:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsRoundInterval2Description"), "14.00:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsRoundInterval3Description"), "21.00:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsRoundInterval4Description"), "28.00:00:00"); } private IEnumerable GetValuesForVotingInterval() { - yield return new SelectValue("Constructor_FormSectionSetSettingsVotingInterval1Description", "02:00:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsVotingInterval2Description", "04:00:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsVotingInterval3Description", "1.00:00:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsVotingInterval4Description", "2.00:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsVotingInterval1Description"), "02:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsVotingInterval2Description"), "04:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsVotingInterval3Description"), "1.00:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsVotingInterval4Description"), "2.00:00:00"); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.RandomCoffee.Application/Services/ScheduleService.cs b/src/Inc.TeamAssistant.RandomCoffee.Application/Services/ScheduleService.cs index c1c8b3dd..455b668e 100644 --- a/src/Inc.TeamAssistant.RandomCoffee.Application/Services/ScheduleService.cs +++ b/src/Inc.TeamAssistant.RandomCoffee.Application/Services/ScheduleService.cs @@ -49,20 +49,20 @@ protected override async Task ExecuteAsync(CancellationToken token) private async Task Execute(CancellationToken token) { var now = DateTimeOffset.UtcNow; - var randomCoffeeEntries = await _reader.GetByDate(now, token); + var entries = await _reader.GetByDate(now, token); - foreach (var randomCoffeeEntry in randomCoffeeEntries) + foreach (var entry in entries) { - if (!await _holidayService.IsWorkTime(randomCoffeeEntry.BotId, now, token)) + if (!await _holidayService.IsWorkTime(entry.BotId, now, token)) continue; - var messageContext = MessageContext.CreateFromBackground(randomCoffeeEntry.BotId, randomCoffeeEntry.ChatId); + var messageContext = MessageContext.CreateFromBackground(entry.BotId, entry.ChatId); - IDialogCommand command = randomCoffeeEntry.State switch + IDialogCommand command = entry.State switch { - RandomCoffeeState.Waiting => new SelectPairsCommand(messageContext, randomCoffeeEntry.Id), + RandomCoffeeState.Waiting => new SelectPairsCommand(messageContext, entry.Id), RandomCoffeeState.Idle => new InviteForCoffeeCommand(messageContext, OnDemand: false), - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(nameof(entry.State), entry.State, "State out of range.") }; await _commandExecutor.Execute(command, token); diff --git a/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/EditDraft/EditDraftCommandHandler.cs b/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/EditDraft/EditDraftCommandHandler.cs index b0d4d8ca..c51b47fa 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/EditDraft/EditDraftCommandHandler.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/EditDraft/EditDraftCommandHandler.cs @@ -28,9 +28,11 @@ public async Task Handle(EditDraftCommand command, CancellationTo if (draft is not null) { draft.WithDescription(command.Description); - + if (command.MessageContext.TargetPersonId.HasValue) draft.WithTargetPerson(command.MessageContext.TargetPersonId.Value); + else + draft.WithoutTargetPerson(); var notification = await _reviewMessageBuilder.Build(draft, command.MessageContext.LanguageId, token); diff --git a/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToDecline/MoveToDeclineCommandHandler.cs b/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToDecline/MoveToDeclineCommandHandler.cs index 28f7a114..9a34010a 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToDecline/MoveToDeclineCommandHandler.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToDecline/MoveToDeclineCommandHandler.cs @@ -1,7 +1,9 @@ using Inc.TeamAssistant.Primitives; +using Inc.TeamAssistant.Primitives.Bots; using Inc.TeamAssistant.Primitives.Commands; using Inc.TeamAssistant.Primitives.Exceptions; using Inc.TeamAssistant.Reviewer.Application.Contracts; +using Inc.TeamAssistant.Reviewer.Domain; using Inc.TeamAssistant.Reviewer.Model.Commands.MoveToDecline; using MediatR; @@ -9,27 +11,30 @@ namespace Inc.TeamAssistant.Reviewer.Application.CommandHandlers.MoveToDecline; internal sealed class MoveToDeclineCommandHandler : IRequestHandler { - private readonly ITaskForReviewRepository _taskForReviewRepository; + private readonly ITaskForReviewRepository _repository; private readonly IReviewMessageBuilder _reviewMessageBuilder; private readonly ITeamAccessor _teamAccessor; + private readonly IBotAccessor _botAccessor; public MoveToDeclineCommandHandler( - ITaskForReviewRepository taskForReviewRepository, + ITaskForReviewRepository repository, IReviewMessageBuilder reviewMessageBuilder, - ITeamAccessor teamAccessor) + ITeamAccessor teamAccessor, + IBotAccessor botAccessor) { - _taskForReviewRepository = - taskForReviewRepository ?? throw new ArgumentNullException(nameof(taskForReviewRepository)); - _reviewMessageBuilder = - reviewMessageBuilder ?? throw new ArgumentNullException(nameof(reviewMessageBuilder)); + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _reviewMessageBuilder = reviewMessageBuilder ?? throw new ArgumentNullException(nameof(reviewMessageBuilder)); _teamAccessor = teamAccessor ?? throw new ArgumentNullException(nameof(teamAccessor)); + _botAccessor = botAccessor ?? throw new ArgumentNullException(nameof(botAccessor)); } public async Task Handle(MoveToDeclineCommand command, CancellationToken token) { ArgumentNullException.ThrowIfNull(command); - var taskForReview = await _taskForReviewRepository.GetById(command.TaskId, token); + var taskForReview = await _repository.GetById(command.TaskId, token); + var botContext = await _botAccessor.GetBotContext(taskForReview.BotId, token); + if (!taskForReview.CanAccept()) return CommandResult.Empty; @@ -41,7 +46,7 @@ public async Task Handle(MoveToDeclineCommand command, Cancellati if (owner is null) throw new TeamAssistantUserException(Messages.Connector_PersonNotFound, taskForReview.OwnerId); - taskForReview.Decline(DateTimeOffset.UtcNow); + taskForReview.Decline(DateTimeOffset.UtcNow, botContext.GetNotificationIntervals()); var notifications = await _reviewMessageBuilder.Build( command.MessageContext.ChatMessage.MessageId, @@ -50,7 +55,7 @@ public async Task Handle(MoveToDeclineCommand command, Cancellati owner, token); - await _taskForReviewRepository.Upsert(taskForReview, token); + await _repository.Upsert(taskForReview, token); return CommandResult.Build(notifications.ToArray()); } diff --git a/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToReview/Validators/MoveToReviewCommandValidator.cs b/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToReview/Validators/MoveToReviewCommandValidator.cs index e9afd85a..2466bc03 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToReview/Validators/MoveToReviewCommandValidator.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/CommandHandlers/MoveToReview/Validators/MoveToReviewCommandValidator.cs @@ -21,14 +21,25 @@ public MoveToReviewCommandValidator( RuleFor(e => e.DraftId) .NotEmpty() - .MustAsync(HasDescriptionAndLinks) - .WithMessage("'Description' must contains a link to the source code and some description"); + .CustomAsync(ValidateDraft); } - private async Task HasDescriptionAndLinks(Guid draftId, CancellationToken token) + private async Task ValidateDraft( + Guid draftId, + ValidationContext context, + CancellationToken token) { + ArgumentNullException.ThrowIfNull(context); + var draft = await _draftTaskForReviewRepository.GetById(draftId, token); - return _draftTaskForReviewService.HasDescriptionAndLinks(draft.Description); + if (!_draftTaskForReviewService.HasDescriptionAndLinks(draft.Description)) + context.AddFailure( + nameof(draft.Description), + "Must contains a link to the source code and some description"); + + if (draft.TargetPersonId.HasValue && + !await _draftTaskForReviewService.HasTeammate(draft.TeamId, draft.TargetPersonId.Value, token)) + context.AddFailure(nameof(draft.TargetPersonId), "Teammate not found"); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Reviewer.Application/Contracts/IReviewMessageBuilder.cs b/src/Inc.TeamAssistant.Reviewer.Application/Contracts/IReviewMessageBuilder.cs index 29704528..1ce791e7 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/Contracts/IReviewMessageBuilder.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/Contracts/IReviewMessageBuilder.cs @@ -9,12 +9,12 @@ public interface IReviewMessageBuilder { Task> Build( int messageId, - TaskForReview taskForReview, + TaskForReview task, Person reviewer, Person owner, CancellationToken token); - Task Push(TaskForReview taskForReview, CancellationToken token); + Task Push(TaskForReview task, CancellationToken token); Task Build(DraftTaskForReview draft, LanguageId languageId, CancellationToken token); } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Reviewer.Application/Messages.cs b/src/Inc.TeamAssistant.Reviewer.Application/Messages.cs index e6099c74..e57d02ef 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/Messages.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/Messages.cs @@ -32,6 +32,7 @@ internal static class Messages public static readonly MessageId Reviewer_PreviewTitle = new(nameof(Reviewer_PreviewTitle)); public static readonly MessageId Reviewer_PreviewReviewerTemplate = new(nameof(Reviewer_PreviewReviewerTemplate)); public static readonly MessageId Reviewer_PreviewCheckDescription = new(nameof(Reviewer_PreviewCheckDescription)); + public static readonly MessageId Reviewer_PreviewCheckTeammate = new(nameof(Reviewer_PreviewCheckTeammate)); public static readonly MessageId Reviewer_PreviewEditHelp = new(nameof(Reviewer_PreviewEditHelp)); public static readonly MessageId Reviewer_PreviewMoveToReview = new(nameof(Reviewer_PreviewMoveToReview)); public static readonly MessageId Reviewer_PreviewRemoveDraft = new(nameof(Reviewer_PreviewRemoveDraft)); diff --git a/src/Inc.TeamAssistant.Reviewer.Application/Services/DraftTaskForReviewService.cs b/src/Inc.TeamAssistant.Reviewer.Application/Services/DraftTaskForReviewService.cs index ff86ef4d..ab3d4d20 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/Services/DraftTaskForReviewService.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/Services/DraftTaskForReviewService.cs @@ -8,11 +8,22 @@ namespace Inc.TeamAssistant.Reviewer.Application.Services; internal sealed class DraftTaskForReviewService { private readonly IDraftTaskForReviewRepository _draftTaskForReviewRepository; + private readonly ITeamAccessor _teamAccessor; - public DraftTaskForReviewService(IDraftTaskForReviewRepository draftTaskForReviewRepository) + public DraftTaskForReviewService( + IDraftTaskForReviewRepository draftTaskForReviewRepository, + ITeamAccessor teamAccessor) { _draftTaskForReviewRepository = draftTaskForReviewRepository ?? throw new ArgumentNullException(nameof(draftTaskForReviewRepository)); + _teamAccessor = teamAccessor ?? throw new ArgumentNullException(nameof(teamAccessor)); + } + + public async Task HasTeammate(Guid teamId, long personId, CancellationToken token) + { + var teammates = await _teamAccessor.GetTeammates(teamId, DateTimeOffset.UtcNow, token); + + return teammates.Any(t => t.Id == personId); } public bool HasDescriptionAndLinks(string description) diff --git a/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewMessageBuilder.cs b/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewMessageBuilder.cs index 4fbc1947..b22fa2b0 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewMessageBuilder.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewMessageBuilder.cs @@ -1,7 +1,6 @@ using System.Text; using Inc.TeamAssistant.Holidays.Extensions; using Inc.TeamAssistant.Primitives; -using Inc.TeamAssistant.Primitives.Exceptions; using Inc.TeamAssistant.Primitives.Languages; using Inc.TeamAssistant.Primitives.Notifications; using Inc.TeamAssistant.Reviewer.Application.Contracts; @@ -13,84 +12,87 @@ namespace Inc.TeamAssistant.Reviewer.Application.Services; internal sealed class ReviewMessageBuilder : IReviewMessageBuilder { - private const string TimeFormat = @"d\.hh\:mm"; - private readonly IMessageBuilder _messageBuilder; private readonly ITeamAccessor _teamAccessor; - private readonly ITaskForReviewReader _taskForReviewReader; - private readonly IReviewMetricsProvider _reviewMetricsProvider; + private readonly ITaskForReviewReader _taskReader; + private readonly IReviewMetricsProvider _metricsProvider; private readonly ReviewTeamMetricsFactory _metricsFactory; - private readonly DraftTaskForReviewService _service; + private readonly DraftTaskForReviewService _draftService; public ReviewMessageBuilder( IMessageBuilder messageBuilder, ITeamAccessor teamAccessor, - ITaskForReviewReader taskForReviewReader, - IReviewMetricsProvider reviewMetricsProvider, + ITaskForReviewReader taskReader, + IReviewMetricsProvider metricsProvider, ReviewTeamMetricsFactory metricsFactory, - DraftTaskForReviewService service) + DraftTaskForReviewService draftService) { _messageBuilder = messageBuilder ?? throw new ArgumentNullException(nameof(messageBuilder)); _teamAccessor = teamAccessor ?? throw new ArgumentNullException(nameof(teamAccessor)); - _taskForReviewReader = taskForReviewReader ?? throw new ArgumentNullException(nameof(taskForReviewReader)); - _reviewMetricsProvider = reviewMetricsProvider ?? throw new ArgumentNullException(nameof(reviewMetricsProvider)); + _taskReader = taskReader ?? throw new ArgumentNullException(nameof(taskReader)); + _metricsProvider = metricsProvider ?? throw new ArgumentNullException(nameof(metricsProvider)); _metricsFactory = metricsFactory ?? throw new ArgumentNullException(nameof(metricsFactory)); - _service = service ?? throw new ArgumentNullException(nameof(service)); + _draftService = draftService ?? throw new ArgumentNullException(nameof(draftService)); } public async Task> Build( int messageId, - TaskForReview taskForReview, + TaskForReview task, Person reviewer, Person owner, CancellationToken token) { - ArgumentNullException.ThrowIfNull(taskForReview); + ArgumentNullException.ThrowIfNull(task); ArgumentNullException.ThrowIfNull(reviewer); ArgumentNullException.ThrowIfNull(owner); - var metricsByTeam = _reviewMetricsProvider.Get(taskForReview.TeamId); - var metricsByTask = await _metricsFactory.Create(taskForReview, token); + var metricsByTeam = _metricsProvider.Get(task.TeamId); + var metricsByTask = await _metricsFactory.Create(task, token); + var hasOwnerAction = messageId == task.OwnerMessageId; var notifications = new List { - await MessageForTeam(taskForReview, reviewer, owner, metricsByTeam, metricsByTask, token), - await MessageForReviewer(taskForReview, metricsByTeam, metricsByTask, token) + await MessageForTeam(task, reviewer, owner, metricsByTeam, metricsByTask, token), + await MessageForReviewer(task, metricsByTeam, metricsByTask, token) }; - if (!taskForReview.ReviewerMessageId.HasValue && taskForReview.OriginalReviewerId.HasValue) - notifications.Add(await HideControlsForReviewer(taskForReview.OriginalReviewerId.Value, messageId, taskForReview, token)); + if (!task.ReviewerMessageId.HasValue && task.OriginalReviewerId.HasValue) + notifications.Add(await HideControlsForOriginalReviewer( + task.OriginalReviewerId.Value, + messageId, + task, + token)); - if (taskForReview.State == TaskForReviewState.OnCorrection || messageId == taskForReview.OwnerMessageId) - notifications.Add(await MessageForOwner(taskForReview, metricsByTeam, metricsByTask, token)); + if (task.State == TaskForReviewState.OnCorrection || hasOwnerAction) + notifications.Add(await MessageForOwner(task, metricsByTeam, metricsByTask, token)); - if (taskForReview.State == TaskForReviewState.Accept) - notifications.Add(await ReviewFinish(taskForReview, token)); + if (task.State == TaskForReviewState.Accept) + notifications.Add(await ReviewFinish(task, token)); return notifications; } - public async Task Push(TaskForReview taskForReview, CancellationToken token) + public async Task Push(TaskForReview task, CancellationToken token) { - ArgumentNullException.ThrowIfNull(taskForReview); + ArgumentNullException.ThrowIfNull(task); - var totalTime = taskForReview.GetTotalTime(DateTimeOffset.UtcNow); + var totalTime = task.GetTotalTime(DateTimeOffset.UtcNow); - return taskForReview switch + return task switch { { State: TaskForReviewState.New or TaskForReviewState.InProgress, ReviewerMessageId: not null } => await CreatePushMessage( - taskForReview.BotId, - taskForReview.ReviewerId, - taskForReview.ReviewerMessageId.Value, + task.BotId, + task.ReviewerId, + task.ReviewerMessageId.Value, Messages.Reviewer_NeedEndReview, totalTime, token), { State: TaskForReviewState.OnCorrection, OwnerMessageId: not null } => await CreatePushMessage( - taskForReview.BotId, - taskForReview.OwnerId, - taskForReview.OwnerMessageId.Value, + task.BotId, + task.OwnerId, + task.OwnerMessageId.Value, Messages.Reviewer_NeedCorrection, totalTime, token), @@ -106,35 +108,48 @@ public async Task Build( ArgumentNullException.ThrowIfNull(draft); ArgumentNullException.ThrowIfNull(languageId); - var messageBuilder = new StringBuilder(); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_PreviewTitle, languageId)); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(draft.Description); + var teamContext = await _teamAccessor.GetTeamContext(draft.TeamId, token); + var reviewTargetMessageTemplate = await _messageBuilder.Build( + Messages.Reviewer_PreviewReviewerTemplate, + languageId); + var builder = new StringBuilder(); + + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_PreviewTitle, languageId)); + builder.AppendLine(); + builder.AppendLine(draft.Description); + builder.AppendLine(); if (draft.TargetPersonId.HasValue) { - var targetPersonMessageTemplate = await _messageBuilder.Build( - Messages.Reviewer_PreviewReviewerTemplate, - languageId); - var targetPerson = await _teamAccessor.FindPerson(draft.TargetPersonId.Value, token); - if (targetPerson is null) - throw new TeamAssistantUserException(Messages.Connector_PersonNotFound, draft.TargetPersonId.Value); + var reviewTarget = await _teamAccessor.GetPerson(draft.TargetPersonId.Value, token); + builder.AppendLine(string.Format(reviewTargetMessageTemplate, reviewTarget.DisplayName)); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(string.Format(targetPersonMessageTemplate, targetPerson.DisplayName)); + if (!await _draftService.HasTeammate(draft.TeamId, draft.TargetPersonId.Value, token)) + { + builder.AppendLine(); + builder.Append(Icons.Alert); + builder.Append(await _messageBuilder.Build( + Messages.Reviewer_PreviewCheckTeammate, + languageId, + reviewTarget.DisplayName, + teamContext.TeamName)); + builder.AppendLine(); + } } - - if (!_service.HasDescriptionAndLinks(draft.Description)) + else + builder.AppendLine(string.Format(reviewTargetMessageTemplate, teamContext.TeamName)); + + if (!_draftService.HasDescriptionAndLinks(draft.Description)) { - messageBuilder.AppendLine(); - messageBuilder.Append('❗'); - messageBuilder.Append(await _messageBuilder.Build(Messages.Reviewer_PreviewCheckDescription, languageId)); - messageBuilder.AppendLine(); + builder.AppendLine(); + builder.Append(Icons.Alert); + builder.Append(await _messageBuilder.Build(Messages.Reviewer_PreviewCheckDescription, languageId)); + builder.AppendLine(); } - messageBuilder.AppendLine(); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_PreviewEditHelp, languageId)); - var message = messageBuilder.ToString(); + builder.AppendLine(); + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_PreviewEditHelp, languageId)); + var message = builder.ToString(); var notificationMessage = draft.PreviewMessageId.HasValue ? NotificationMessage.Edit(new ChatMessage(draft.ChatId, draft.PreviewMessageId.Value), message) @@ -163,305 +178,225 @@ await _messageBuilder.Build(Messages.Reviewer_PreviewRemoveDraft, languageId), var languageId = await _teamAccessor.GetClientLanguage(botId, personId, token); - var messageBuilder = new StringBuilder(); - messageBuilder.AppendLine(await _messageBuilder.Build(messageTextId, languageId)); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(await _messageBuilder.Build( + var builder = new StringBuilder(); + builder.AppendLine(await _messageBuilder.Build(messageTextId, languageId)); + builder.AppendLine(); + builder.AppendLine(await _messageBuilder.Build( Messages.Reviewer_TotalTime, languageId, - workTimeTotal.ToString(TimeFormat))); + workTimeTotal.ToString(GlobalSettings.TimeFormat))); return NotificationMessage - .Create(personId, messageBuilder.ToString()) + .Create(personId, builder.ToString()) .ReplyTo(messageId); } private async Task MessageForTeam( - TaskForReview taskForReview, + TaskForReview task, Person reviewer, Person owner, ReviewTeamMetrics metricsByTeam, ReviewTeamMetrics metricsByTask, CancellationToken token) { - ArgumentNullException.ThrowIfNull(taskForReview); + ArgumentNullException.ThrowIfNull(task); ArgumentNullException.ThrowIfNull(reviewer); ArgumentNullException.ThrowIfNull(owner); ArgumentNullException.ThrowIfNull(metricsByTeam); ArgumentNullException.ThrowIfNull(metricsByTask); Func attachPersons = n => n; - var languageId = await _teamAccessor.GetClientLanguage(taskForReview.BotId, owner.Id, token); - var state = taskForReview.State switch - { - TaskForReviewState.New => "⏳", - TaskForReviewState.InProgress => "🤩", - TaskForReviewState.OnCorrection => "😱", - TaskForReviewState.Accept => "🤝", - _ => throw new ArgumentOutOfRangeException($"State {taskForReview.State} out of range for {nameof(TaskForReviewState)}.") - }; - var reviewerTargetMessageKey = taskForReview.HasConcreteReviewer + var languageId = await _teamAccessor.GetClientLanguage(task.BotId, owner.Id, token); + var reviewerTargetMessageKey = task.HasConcreteReviewer ? Messages.Reviewer_TargetManually : Messages.Reviewer_TargetAutomatically; - var messageBuilder = new StringBuilder(); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_NewTaskForReview, languageId)); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_Owner, languageId, owner.DisplayName)); + var builder = new StringBuilder(); + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_NewTaskForReview, languageId)); + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_Owner, languageId, owner.DisplayName)); - messageBuilder.Append(await _messageBuilder.Build(reviewerTargetMessageKey, languageId)); - reviewer.Append(messageBuilder, (p, o) => attachPersons += n => n.AttachPerson(p, o)); - messageBuilder.AppendLine(); - - messageBuilder.AppendLine(); - messageBuilder.AppendLine(taskForReview.Description); - messageBuilder.AppendLine(state); + builder.Append(await _messageBuilder.Build(reviewerTargetMessageKey, languageId)); + reviewer.Append(builder, (p, o) => attachPersons += n => n.AttachPerson(p, o)); + builder.AppendLine(); - await AddStats( - messageBuilder, - taskForReview, - metricsByTeam, - metricsByTask, - languageId, - hasReviewMetrics: true, - hasCorrectionMetrics: true); - var message = messageBuilder.ToString(); + builder.AppendLine(); + builder.AppendLine(task.Description); + builder.AppendLine(StateAsIcon(task)); + + var stats = await ReviewStatsBuilder + .Create(_messageBuilder) + .WithReviewMetrics() + .WithCorrectionMetrics() + .Build(task, metricsByTeam, metricsByTask, languageId); + builder.Append(stats); - var notification = taskForReview.MessageId.HasValue - ? NotificationMessage.Edit(new ChatMessage(taskForReview.ChatId, taskForReview.MessageId.Value), message) + var message = builder.ToString(); + var notification = task.MessageId.HasValue + ? NotificationMessage.Edit(new ChatMessage(task.ChatId, task.MessageId.Value), message) : NotificationMessage - .Create(taskForReview.ChatId, message) + .Create(task.ChatId, message) .AddHandler((c, p) => new AttachMessageCommand( c, - taskForReview.Id, + task.Id, int.Parse(p), MessageType.Shared.ToString())); return attachPersons(notification); } - private async Task HideControlsForReviewer( + private async Task HideControlsForOriginalReviewer( long reviewerId, int messageId, - TaskForReview taskForReview, + TaskForReview task, CancellationToken token) { - ArgumentNullException.ThrowIfNull(taskForReview); + ArgumentNullException.ThrowIfNull(task); - var languageId = await _teamAccessor.GetClientLanguage(taskForReview.BotId, reviewerId, token); + var languageId = await _teamAccessor.GetClientLanguage(task.BotId, reviewerId, token); - var messageBuilder = new StringBuilder(); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_NeedReview, languageId)); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(taskForReview.Description); + var builder = new StringBuilder(); + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_NeedReview, languageId)); + builder.AppendLine(); + builder.AppendLine(task.Description); - return NotificationMessage.Edit(new(reviewerId, messageId), messageBuilder.ToString()); + return NotificationMessage.Edit(new(reviewerId, messageId), builder.ToString()); } private async Task MessageForReviewer( - TaskForReview taskForReview, + TaskForReview task, ReviewTeamMetrics metricsByTeam, ReviewTeamMetrics metricsByTask, CancellationToken token) { - ArgumentNullException.ThrowIfNull(taskForReview); + ArgumentNullException.ThrowIfNull(task); ArgumentNullException.ThrowIfNull(metricsByTeam); ArgumentNullException.ThrowIfNull(metricsByTask); - var languageId = await _teamAccessor.GetClientLanguage(taskForReview.BotId, taskForReview.ReviewerId, token); - var messageBuilder = new StringBuilder(); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_NeedReview, languageId)); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(taskForReview.Description); + var languageId = await _teamAccessor.GetClientLanguage(task.BotId, task.ReviewerId, token); + var builder = new StringBuilder(); + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_NeedReview, languageId)); + builder.AppendLine(); + builder.AppendLine(task.Description); - await AddStats( - messageBuilder, - taskForReview, - metricsByTeam, - metricsByTask, - languageId, - hasReviewMetrics: true, - hasCorrectionMetrics: false); + var stats = await ReviewStatsBuilder + .Create(_messageBuilder) + .WithReviewMetrics() + .Build(task, metricsByTeam, metricsByTask, languageId); + builder.Append(stats); - var message = messageBuilder.ToString(); - var notification = taskForReview.ReviewerMessageId.HasValue - ? NotificationMessage.Edit(new(taskForReview.ReviewerId, taskForReview.ReviewerMessageId.Value), message) + var message = builder.ToString(); + var notification = task.ReviewerMessageId.HasValue + ? NotificationMessage.Edit(new(task.ReviewerId, task.ReviewerMessageId.Value), message) : NotificationMessage - .Create(taskForReview.ReviewerId, message) + .Create(task.ReviewerId, message) .AddHandler((c, p) => new AttachMessageCommand( c, - taskForReview.Id, + task.Id, int.Parse(p), MessageType.Reviewer.ToString())); - if (taskForReview.State == TaskForReviewState.New) + if (task.State == TaskForReviewState.New) { var inProgressButton = await _messageBuilder.Build(Messages.Reviewer_MoveToInProgress, languageId); - notification.WithButton(new Button(inProgressButton, $"{CommandList.MoveToInProgress}{taskForReview.Id:N}")); + notification.WithButton(new Button( + inProgressButton, + $"{CommandList.MoveToInProgress}{task.Id:N}")); var fromDate = DateTimeOffset.UtcNow.GetLastDayOfWeek(DayOfWeek.Monday); - var hasReassign = await _taskForReviewReader.HasReassignFromDate(taskForReview.ReviewerId, fromDate, token); - if (!taskForReview.OriginalReviewerId.HasValue && !hasReassign) + var hasReassign = await _taskReader.HasReassignFromDate(task.ReviewerId, fromDate, token); + if (!task.OriginalReviewerId.HasValue && !hasReassign) { var reassignReviewButton = await _messageBuilder.Build(Messages.Reviewer_Reassign, languageId); - notification.WithButton(new Button(reassignReviewButton, $"{CommandList.ReassignReview}{taskForReview.Id:N}")); + notification.WithButton(new Button( + reassignReviewButton, + $"{CommandList.ReassignReview}{task.Id:N}")); } } - if (taskForReview.State == TaskForReviewState.InProgress) + if (task.State == TaskForReviewState.InProgress) { var moveToAcceptButton = await _messageBuilder.Build(Messages.Reviewer_MoveToAccept, languageId); - notification.WithButton(new Button(moveToAcceptButton, $"{CommandList.Accept}{taskForReview.Id:N}")); + notification.WithButton(new Button(moveToAcceptButton, $"{CommandList.Accept}{task.Id:N}")); var moveToDeclineButton = await _messageBuilder.Build(Messages.Reviewer_MoveToDecline, languageId); - notification.WithButton(new Button(moveToDeclineButton, $"{CommandList.Decline}{taskForReview.Id:N}")); + notification.WithButton(new Button(moveToDeclineButton, $"{CommandList.Decline}{task.Id:N}")); } return notification; } private async Task MessageForOwner( - TaskForReview taskForReview, + TaskForReview task, ReviewTeamMetrics metricsByTeam, ReviewTeamMetrics metricsByTask, CancellationToken token) { - ArgumentNullException.ThrowIfNull(taskForReview); + ArgumentNullException.ThrowIfNull(task); ArgumentNullException.ThrowIfNull(metricsByTeam); ArgumentNullException.ThrowIfNull(metricsByTask); - var languageId = await _teamAccessor.GetClientLanguage(taskForReview.BotId, taskForReview.OwnerId, token); - var messageBuilder = new StringBuilder(); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_ReviewDeclined, languageId)); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(taskForReview.Description); + var languageId = await _teamAccessor.GetClientLanguage(task.BotId, task.OwnerId, token); + var builder = new StringBuilder(); + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_ReviewDeclined, languageId)); + builder.AppendLine(); + builder.AppendLine(task.Description); - await AddStats( - messageBuilder, - taskForReview, - metricsByTeam, - metricsByTask, - languageId, - hasReviewMetrics: false, - hasCorrectionMetrics: true); + var stats = await ReviewStatsBuilder + .Create(_messageBuilder) + .WithCorrectionMetrics() + .Build(task, metricsByTeam, metricsByTask, languageId); + builder.Append(stats); - var message = messageBuilder.ToString(); - var notification = taskForReview.OwnerMessageId.HasValue - ? NotificationMessage.Edit(new(taskForReview.OwnerId, taskForReview.OwnerMessageId.Value), message) + var message = builder.ToString(); + var notification = task.OwnerMessageId.HasValue + ? NotificationMessage.Edit(new(task.OwnerId, task.OwnerMessageId.Value), message) : NotificationMessage - .Create(taskForReview.OwnerId, message) + .Create(task.OwnerId, message) .AddHandler((c, p) => new AttachMessageCommand( c, - taskForReview.Id, + task.Id, int.Parse(p), MessageType.Owner.ToString())); - if (taskForReview.State == TaskForReviewState.OnCorrection) + if (task.State == TaskForReviewState.OnCorrection) { var moveToNextRoundButton = await _messageBuilder.Build(Messages.Reviewer_MoveToNextRound, languageId); notification.WithButton(new Button(moveToNextRoundButton, - $"{CommandList.MoveToNextRound}{taskForReview.Id:N}")); + $"{CommandList.MoveToNextRound}{task.Id:N}")); } return notification; } - private async Task ReviewFinish(TaskForReview taskForReview, CancellationToken token) + private async Task ReviewFinish(TaskForReview task, CancellationToken token) { - ArgumentNullException.ThrowIfNull(taskForReview); + ArgumentNullException.ThrowIfNull(task); - var languageId = await _teamAccessor.GetClientLanguage(taskForReview.BotId, taskForReview.OwnerId, token); - var totalTime = taskForReview.GetTotalTime(DateTimeOffset.UtcNow); + var languageId = await _teamAccessor.GetClientLanguage(task.BotId, task.OwnerId, token); + var totalTime = task.GetTotalTime(DateTimeOffset.UtcNow); - var messageBuilder = new StringBuilder(); - messageBuilder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_Accepted, languageId)); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(taskForReview.Description); - messageBuilder.AppendLine(); - messageBuilder.AppendLine(await _messageBuilder.Build( + var builder = new StringBuilder(); + builder.AppendLine(await _messageBuilder.Build(Messages.Reviewer_Accepted, languageId)); + builder.AppendLine(); + builder.AppendLine(task.Description); + builder.AppendLine(); + builder.AppendLine(await _messageBuilder.Build( Messages.Reviewer_TotalTime, languageId, - totalTime.ToString(TimeFormat))); + totalTime.ToString(GlobalSettings.TimeFormat))); - return NotificationMessage.Create(taskForReview.OwnerId, messageBuilder.ToString()); + return NotificationMessage.Create(task.OwnerId, builder.ToString()); } - private async Task AddStats( - StringBuilder builder, - TaskForReview taskForReview, - ReviewTeamMetrics metricsByTeam, - ReviewTeamMetrics metricsByTask, - LanguageId languageId, - bool hasReviewMetrics, - bool hasCorrectionMetrics) + private string StateAsIcon(TaskForReview task) { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(taskForReview); - ArgumentNullException.ThrowIfNull(metricsByTeam); - ArgumentNullException.ThrowIfNull(metricsByTask); - ArgumentNullException.ThrowIfNull(languageId); - - builder.AppendLine(); - var attempts = taskForReview.GetAttempts(); - if (attempts.HasValue) - builder.AppendLine(await _messageBuilder.Build( - Messages.Reviewer_StatsAttempts, - languageId, - attempts.Value)); - - if (taskForReview.State == TaskForReviewState.Accept) + return task.State switch { - const string trendUp = "👍"; - const string trendDown = "👎"; - - if (hasReviewMetrics) - { - var firstTouchTrend = metricsByTask.FirstTouch <= metricsByTeam.FirstTouch ? trendUp : trendDown; - var firstTouchMessage = await _messageBuilder.Build( - Messages.Reviewer_StatsFirstTouch, - languageId, - metricsByTask.FirstTouch.ToString(TimeFormat), - metricsByTeam.FirstTouch.ToString(TimeFormat)); - builder.AppendLine($"{firstTouchMessage} {firstTouchTrend}"); - - var reviewTrend = metricsByTask.Review <= metricsByTeam.Review ? trendUp : trendDown; - var reviewMessage = await _messageBuilder.Build( - Messages.Reviewer_StatsReview, - languageId, - metricsByTask.Review.ToString(TimeFormat), - metricsByTeam.Review.ToString(TimeFormat)); - builder.AppendLine($"{reviewMessage} {reviewTrend}"); - } - - if (hasCorrectionMetrics && attempts.HasValue) - { - var correctionTrend = metricsByTask.Correction <= metricsByTeam.Correction ? trendUp : trendDown; - var correctionMessage = await _messageBuilder.Build( - Messages.Reviewer_StatsCorrection, - languageId, - metricsByTask.Correction.ToString(TimeFormat), - metricsByTeam.Correction.ToString(TimeFormat)); - builder.AppendLine($"{correctionMessage} {correctionTrend}"); - } - } - else - { - if (hasReviewMetrics) - { - builder.AppendLine(await _messageBuilder.Build( - Messages.Reviewer_StatsFirstTouchAverage, - languageId, - metricsByTeam.FirstTouch.ToString(TimeFormat))); - builder.AppendLine(await _messageBuilder.Build( - Messages.Reviewer_StatsReviewAverage, - languageId, - metricsByTeam.Review.ToString(TimeFormat))); - } - - if (hasCorrectionMetrics && attempts.HasValue) - builder.AppendLine(await _messageBuilder.Build( - Messages.Reviewer_StatsCorrectionAverage, - languageId, - metricsByTeam.Correction.ToString(TimeFormat))); - } + TaskForReviewState.New => Icons.Waiting, + TaskForReviewState.InProgress => Icons.InProgress, + TaskForReviewState.OnCorrection => Icons.OnCorrection, + TaskForReviewState.Accept => Icons.Accept, + _ => throw new ArgumentOutOfRangeException(nameof(task.State), task.State, "State out of range.") + }; } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewStatsBuilder.cs b/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewStatsBuilder.cs new file mode 100644 index 00000000..2088d1e1 --- /dev/null +++ b/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewStatsBuilder.cs @@ -0,0 +1,141 @@ +using System.Text; +using Inc.TeamAssistant.Primitives; +using Inc.TeamAssistant.Primitives.Languages; +using Inc.TeamAssistant.Reviewer.Application.Contracts; +using Inc.TeamAssistant.Reviewer.Domain; + +namespace Inc.TeamAssistant.Reviewer.Application.Services; + +internal sealed class ReviewStatsBuilder +{ + private readonly IMessageBuilder _messageBuilder; + private readonly StringBuilder _builder; + + private bool _hasReviewMetrics; + private bool _hasCorrectionMetrics; + + private ReviewStatsBuilder(IMessageBuilder messageBuilder, StringBuilder builder) + { + _messageBuilder = messageBuilder ?? throw new ArgumentNullException(nameof(messageBuilder)); + _builder = builder ?? throw new ArgumentNullException(nameof(builder)); + } + + public static ReviewStatsBuilder Create(IMessageBuilder messageBuilder) + { + ArgumentNullException.ThrowIfNull(messageBuilder); + + return new ReviewStatsBuilder(messageBuilder, new StringBuilder()); + } + + public ReviewStatsBuilder WithReviewMetrics() + { + _hasReviewMetrics = true; + + return this; + } + + public ReviewStatsBuilder WithCorrectionMetrics() + { + _hasCorrectionMetrics = true; + + return this; + } + + public async Task Build( + TaskForReview task, + ReviewTeamMetrics metricsByTeam, + ReviewTeamMetrics metricsByTask, + LanguageId languageId) + { + ArgumentNullException.ThrowIfNull(task); + ArgumentNullException.ThrowIfNull(metricsByTeam); + ArgumentNullException.ThrowIfNull(metricsByTask); + ArgumentNullException.ThrowIfNull(languageId); + + _builder.AppendLine(); + var attempts = task.GetAttempts(); + if (attempts.HasValue) + _builder.AppendLine(await _messageBuilder.Build( + Messages.Reviewer_StatsAttempts, + languageId, + attempts.Value)); + + if (task.State == TaskForReviewState.Accept) + await ByAccept(metricsByTeam, metricsByTask, languageId, attempts); + else + await ByInProgress(metricsByTeam, languageId, attempts); + + return _builder.ToString(); + } + + private async Task ByInProgress(ReviewTeamMetrics metricsByTeam, LanguageId languageId, int? attempts) + { + ArgumentNullException.ThrowIfNull(metricsByTeam); + ArgumentNullException.ThrowIfNull(languageId); + + if (_hasReviewMetrics) + { + _builder.AppendLine(await _messageBuilder.Build( + Messages.Reviewer_StatsFirstTouchAverage, + languageId, + metricsByTeam.FirstTouch.ToString(GlobalSettings.TimeFormat))); + _builder.AppendLine(await _messageBuilder.Build( + Messages.Reviewer_StatsReviewAverage, + languageId, + metricsByTeam.Review.ToString(GlobalSettings.TimeFormat))); + } + + if (_hasCorrectionMetrics && attempts.HasValue) + _builder.AppendLine(await _messageBuilder.Build( + Messages.Reviewer_StatsCorrectionAverage, + languageId, + metricsByTeam.Correction.ToString(GlobalSettings.TimeFormat))); + } + + private async Task ByAccept( + ReviewTeamMetrics metricsByTeam, + ReviewTeamMetrics metricsByTask, + LanguageId languageId, + int? attempts) + { + ArgumentNullException.ThrowIfNull(metricsByTeam); + ArgumentNullException.ThrowIfNull(metricsByTask); + ArgumentNullException.ThrowIfNull(languageId); + + if (_hasReviewMetrics) + { + var firstTouchTrend = metricsByTask.FirstTouch <= metricsByTeam.FirstTouch + ? Icons.TrendUp + : Icons.TrendDown; + var firstTouchMessage = await _messageBuilder.Build( + Messages.Reviewer_StatsFirstTouch, + languageId, + metricsByTask.FirstTouch.ToString(GlobalSettings.TimeFormat), + metricsByTeam.FirstTouch.ToString(GlobalSettings.TimeFormat)); + _builder.AppendLine($"{firstTouchMessage} {firstTouchTrend}"); + + var reviewTrend = metricsByTask.Review <= metricsByTeam.Review + ? Icons.TrendUp + : Icons.TrendDown; + var reviewMessage = await _messageBuilder.Build( + Messages.Reviewer_StatsReview, + languageId, + metricsByTask.Review.ToString(GlobalSettings.TimeFormat), + metricsByTeam.Review.ToString(GlobalSettings.TimeFormat)); + _builder.AppendLine($"{reviewMessage} {reviewTrend}"); + } + + if (_hasCorrectionMetrics && attempts.HasValue) + { + var correctionTrend = metricsByTask.Correction <= metricsByTeam.Correction + ? Icons.TrendUp + : Icons.TrendDown; + var correctionMessage = await _messageBuilder.Build( + Messages.Reviewer_StatsCorrection, + languageId, + metricsByTask.Correction.ToString(GlobalSettings.TimeFormat), + metricsByTeam.Correction.ToString(GlobalSettings.TimeFormat)); + _builder.AppendLine($"{correctionMessage} {correctionTrend}"); + } + } +} \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewerSettingSectionProvider.cs b/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewerSettingSectionProvider.cs index 6bdee13c..84e46232 100644 --- a/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewerSettingSectionProvider.cs +++ b/src/Inc.TeamAssistant.Reviewer.Application/Services/ReviewerSettingSectionProvider.cs @@ -1,14 +1,15 @@ using Inc.TeamAssistant.Primitives.FeatureProperties; +using Inc.TeamAssistant.Primitives.Languages; using Inc.TeamAssistant.Reviewer.Domain; namespace Inc.TeamAssistant.Reviewer.Application.Services; internal sealed class ReviewerSettingSectionProvider : ISettingSectionProvider { - private readonly IReadOnlyDictionary _storyType = new Dictionary + private readonly IReadOnlyDictionary _storyType = new Dictionary { - [NextReviewerType.RoundRobin] = "Constructor_FormSectionSetSettingsRoundRobinDescription", - [NextReviewerType.Random] = "Constructor_FormSectionSetSettingsRandomDescription" + [NextReviewerType.RoundRobin] = new("Constructor_FormSectionSetSettingsRoundRobinDescription"), + [NextReviewerType.Random] = new("Constructor_FormSectionSetSettingsRandomDescription") }; public string FeatureName => "Reviewer"; @@ -18,20 +19,20 @@ public IReadOnlyCollection GetSections() return [ new SettingSection( - "Constructor_FormSectionSetSettingsReviewerHeader", - "Constructor_FormSectionSetSettingsReviewerHelp", + new("Constructor_FormSectionSetSettingsReviewerHeader"), + new("Constructor_FormSectionSetSettingsReviewerHelp"), [ new( ReviewerProperties.NextReviewerTypeKey, - "Constructor_FormSectionSetSettingsNextReviewerStrategyFieldLabel", + new("Constructor_FormSectionSetSettingsNextReviewerStrategyFieldLabel"), GetValuesForNextReviewerType().ToArray()), new( ReviewerProperties.WaitingNotificationIntervalKey, - "Constructor_FormSectionSetSettingsWaitingNotificationIntervalFieldLabel", + new("Constructor_FormSectionSetSettingsWaitingNotificationIntervalFieldLabel"), GetValuesForNotificationInterval().ToArray()), new( ReviewerProperties.InProgressNotificationIntervalKey, - "Constructor_FormSectionSetSettingsInProgressNotificationIntervalFieldLabel", + new("Constructor_FormSectionSetSettingsInProgressNotificationIntervalFieldLabel"), GetValuesForNotificationInterval().ToArray()) ]) ]; @@ -46,9 +47,9 @@ private IEnumerable GetValuesForNextReviewerType() private IEnumerable GetValuesForNotificationInterval() { - yield return new SelectValue("Constructor_FormSectionSetSettingsNotificationInterval1Description", "00:30:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsNotificationInterval2Description", "01:00:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsNotificationInterval3Description", "01:30:00"); - yield return new SelectValue("Constructor_FormSectionSetSettingsNotificationInterval4Description", "02:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsNotificationInterval1Description"), "00:30:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsNotificationInterval2Description"), "01:00:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsNotificationInterval3Description"), "01:30:00"); + yield return new SelectValue(new("Constructor_FormSectionSetSettingsNotificationInterval4Description"), "02:00:00"); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.Reviewer.Domain/DraftTaskForReview.cs b/src/Inc.TeamAssistant.Reviewer.Domain/DraftTaskForReview.cs index 3375201c..1f5b7a08 100644 --- a/src/Inc.TeamAssistant.Reviewer.Domain/DraftTaskForReview.cs +++ b/src/Inc.TeamAssistant.Reviewer.Domain/DraftTaskForReview.cs @@ -58,6 +58,13 @@ public DraftTaskForReview WithTargetPerson(long personId) return this; } + public DraftTaskForReview WithoutTargetPerson() + { + TargetPersonId = null; + + return this; + } + public DraftTaskForReview WithPreviewMessage(int messageId) { PreviewMessageId = messageId; diff --git a/src/Inc.TeamAssistant.Reviewer.Domain/TaskForReview.cs b/src/Inc.TeamAssistant.Reviewer.Domain/TaskForReview.cs index e325bce7..cc560eea 100644 --- a/src/Inc.TeamAssistant.Reviewer.Domain/TaskForReview.cs +++ b/src/Inc.TeamAssistant.Reviewer.Domain/TaskForReview.cs @@ -65,10 +65,7 @@ public void AttachMessage(MessageType messageType, int messageId) OwnerMessageId = messageId; break; default: - throw new ArgumentOutOfRangeException( - nameof(messageType), - messageType, - "MessageType was not supported."); + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, "State out of range."); } } @@ -92,12 +89,17 @@ public void Accept(DateTimeOffset now) public void MoveToAccept() => State = TaskForReviewState.Accept; - public void Decline(DateTimeOffset now) + public void Decline(DateTimeOffset now, NotificationIntervals notificationIntervals) { + ArgumentNullException.ThrowIfNull(notificationIntervals); + AddReviewInterval(ReviewerId, now); State = TaskForReviewState.OnCorrection; NextNotification = now; + + if (ReviewIntervals.All(i => i.State != TaskForReviewState.OnCorrection)) + SetNextNotificationTime(now, notificationIntervals); } public bool CanMoveToNextRound() => State == TaskForReviewState.OnCorrection; diff --git a/src/Inc.TeamAssistant.WebUI/Extensions/IJsFunction.cs b/src/Inc.TeamAssistant.WebUI/Extensions/IJsFunction.cs index 9cb6e084..06fa7251 100644 --- a/src/Inc.TeamAssistant.WebUI/Extensions/IJsFunction.cs +++ b/src/Inc.TeamAssistant.WebUI/Extensions/IJsFunction.cs @@ -5,4 +5,6 @@ public interface IJsFunction string Identifier { get; } object?[]? Args { get; } + + Action? OnExecuted { get; } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Extensions/JsFunctions.cs b/src/Inc.TeamAssistant.WebUI/Extensions/JsFunctions.cs index 4c31a2d8..f0a6961d 100644 --- a/src/Inc.TeamAssistant.WebUI/Extensions/JsFunctions.cs +++ b/src/Inc.TeamAssistant.WebUI/Extensions/JsFunctions.cs @@ -4,26 +4,32 @@ public static class JsFunctions { public static IJsFunction GetTimezone() { - return new JsFunction("window.browserJsFunctions.getTimezone", args: null); + return new JsFunction("window.browserJsFunctions.getTimezone", postAction: null, args: null); } - public static IJsFunction ChangeUrl(string url) + public static IJsFunction ChangeUrl(string url, Action onChanged) { - ArgumentException.ThrowIfNullOrWhiteSpace(url); - - return new JsFunction("window.browserJsFunctions.changeUrl", url); + ArgumentNullException.ThrowIfNull(url); + ArgumentNullException.ThrowIfNull(onChanged); + + return new JsFunction( + "window.browserJsFunctions.changeUrl", + () => onChanged(url), + url); } private sealed record JsFunction : IJsFunction { public string Identifier { get; } + public Action? OnExecuted { get; } public object?[]? Args { get; } - internal JsFunction(string identifier, params object?[]? args) + internal JsFunction(string identifier, Action? postAction, params object?[]? args) { ArgumentException.ThrowIfNullOrWhiteSpace(identifier); Identifier = identifier; + OnExecuted = postAction; Args = args; } } diff --git a/src/Inc.TeamAssistant.WebUI/Extensions/JsRuntimeExtensions.cs b/src/Inc.TeamAssistant.WebUI/Extensions/JsRuntimeExtensions.cs index f5a86565..6a8ecc44 100644 --- a/src/Inc.TeamAssistant.WebUI/Extensions/JsRuntimeExtensions.cs +++ b/src/Inc.TeamAssistant.WebUI/Extensions/JsRuntimeExtensions.cs @@ -9,7 +9,11 @@ public static async ValueTask Execute(this IJSRuntime jsRuntime, ArgumentNullException.ThrowIfNull(jsRuntime); ArgumentNullException.ThrowIfNull(jsFunction); - return await jsRuntime.InvokeAsync(jsFunction.Identifier, jsFunction.Args); + var result = await jsRuntime.InvokeAsync(jsFunction.Identifier, jsFunction.Args); + + jsFunction.OnExecuted?.Invoke(); + + return result; } public static async ValueTask Execute(this IJSRuntime jsRuntime, IJsFunction jsFunction) @@ -18,5 +22,7 @@ public static async ValueTask Execute(this IJSRuntime jsRuntime, IJsFunction @foreach (var item in _items) { - + @item.AssessmentDate.ToString("dd-MM-yyyy") - @item.StoriesCount @Resources[Messages.GUI_Tasks] } @@ -24,13 +24,8 @@ private IReadOnlyCollection _items = Array.Empty(); - private string MoveToItem(AssessmentHistoryDto historyItem) - { - if (historyItem is null) - throw new ArgumentNullException(nameof(historyItem)); - - return LinkBuilder.Build($"assessment-history/{TeamId:N}/{historyItem.AssessmentDate:yyyy-MM-dd}"); - } + private string CreateMoveToHistoryLink(AssessmentHistoryDto historyItem) + => NavRouter.CreateRoute($"assessment-history/{TeamId:N}/{historyItem.AssessmentDate:yyyy-MM-dd}"); protected override Task OnParametersSetAsync() => Load(); diff --git a/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionHistoryPage.razor b/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionHistoryPage.razor index 6e173569..87e8e34a 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionHistoryPage.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionHistoryPage.razor @@ -7,9 +7,8 @@ @inject IAppraiserService AppraiserService @inject ResourcesManager Resources -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter @inject RequestProcessor RequestProcessor -@inject IJSRuntime JsRuntime > _breadcrumbs = Array.Empty>(); + private IReadOnlyCollection> _breadcrumbs = Array.Empty>(); private IReadOnlyCollection _items = Array.Empty(); protected override async Task OnParametersSetAsync() @@ -72,8 +71,8 @@ _breadcrumbs = [ - new(Resources[Messages.GUI_AssessmentSession], LinkBuilder.Build($"assessment-session/{TeamId:N}")), - new(Date, LinkBuilder.Build($"assessment-history/{TeamId:N}/{Date}")) + new(Resources[Messages.GUI_AssessmentSession], NavRouter.CreateRoute($"assessment-session/{TeamId:N}")), + new(Date, NavRouter.CreateRoute($"assessment-history/{TeamId:N}/{Date}")) ]; await Load(); @@ -81,11 +80,11 @@ private async Task OnViewChanged(AssessmentType assessmentType) { - var url = LinkBuilder.Build($"assessment-history/{TeamId:N}/{Date}/{assessmentType}".ToLowerInvariant()); + var routeSegment = $"assessment-history/{TeamId:N}/{Date}/{assessmentType}".ToLowerInvariant(); _currentView = assessmentType; - - await JsRuntime.Execute(JsFunctions.ChangeUrl(url)); + + await NavRouter.MoveToRoute(routeSegment, RoutingType.Browser); } private async Task Load() diff --git a/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionPage.razor b/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionPage.razor index cdff5d70..5246cb55 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionPage.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/AssessmentSessionPage.razor @@ -10,9 +10,8 @@ @inject IAppraiserService AppraiserService @inject IServiceProvider ServiceProvider @inject ResourcesManager Resources -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter @inject RequestProcessor RequestProcessor -@inject IJSRuntime JsRuntime > _breadcrumbs = Array.Empty>(); + private IReadOnlyCollection> _breadcrumbs = Array.Empty>(); private GetActiveStoryResult _item = new(string.Empty, string.Empty, Story: null); private IAsyncDisposable? _eventListener; @@ -99,8 +98,8 @@ var date = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd"); _breadcrumbs = [ - new(Resources[Messages.GUI_AssessmentSession], LinkBuilder.Build($"assessment-session/{Id:N}")), - new(date, LinkBuilder.Build($"assessment-history/{Id:N}/{date}")) + new(Resources[Messages.GUI_AssessmentSession], NavRouter.CreateRoute($"assessment-session/{Id:N}")), + new(date, NavRouter.CreateRoute($"assessment-history/{Id:N}/{date}")) ]; await Load(); @@ -146,11 +145,11 @@ private async Task OnViewChanged(AssessmentType assessmentType) { - var url = LinkBuilder.Build($"assessment-session/{Id:N}/{assessmentType}".ToLowerInvariant()); + var routeSegment = $"assessment-session/{Id:N}/{assessmentType}".ToLowerInvariant(); _currentView = assessmentType; - - await JsRuntime.Execute(JsFunctions.ChangeUrl(url)); + + await NavRouter.MoveToRoute(routeSegment, RoutingType.Browser); } public async ValueTask DisposeAsync() diff --git a/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/Breadcrumbs.razor b/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/Breadcrumbs.razor index d96163e0..a2b2ed9a 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/Breadcrumbs.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/AssessmentSession/Breadcrumbs.razor @@ -9,5 +9,5 @@ @code { [Parameter, EditorRequired] - public IReadOnlyCollection> Items { get; set; } = Array.Empty>(); + public IReadOnlyCollection> Items { get; set; } = Array.Empty>(); } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Features/Auth/RedirectToLogin.razor b/src/Inc.TeamAssistant.WebUI/Features/Auth/RedirectToLogin.razor index 71560874..39e6100c 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Auth/RedirectToLogin.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Auth/RedirectToLogin.razor @@ -1,10 +1,8 @@ -@inject NavigationManager NavigationManager +@inject NavRouter NavRouter @code { - private string CurrentUrl => NavigationManager.Uri; - - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - NavigationManager.NavigateTo($"/login?returnUrl={CurrentUrl}"); + await NavRouter.MoveToRoute($"login?returnUrl={NavRouter.CurrentUrl}"); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/BotSelector.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/BotSelector.razor index 3f5d71c2..673efd0f 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/BotSelector.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/BotSelector.razor @@ -1,10 +1,9 @@ @using Inc.TeamAssistant.Connector.Model.Queries.GetBotsByCurrentUser @inject IBotService BotService -@inject NavigationManager NavigationManager @inject ResourcesManager Resources @inject RequestProcessor RequestProcessor -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter @@ -25,7 +24,7 @@ { - + @Resources[Messages.Dashboard_MoveToDashboard] @@ -74,16 +73,16 @@ }); } - private void MoveToAdd() => MoveToEdit(botId: null); + private Task MoveToAdd() => MoveToEdit(botId: null); - private void MoveToEdit(Guid? botId) + private async Task MoveToEdit(Guid? botId) { var stage = Stage.CheckBot.ToString().ToLowerInvariant(); - var link = botId.HasValue + var routeSegment = botId.HasValue ? $"constructor/{botId.Value:N}/{stage}" : $"constructor/{stage}"; - NavigationManager.NavigateTo(LinkBuilder.Build(link)); + await NavRouter.MoveToRoute(routeSegment); } private void MoveToRemove(BotDto bot) @@ -93,13 +92,13 @@ _confirmDialog?.Open(); } - private string MoveToDashboardLink(BotDto bot) + private string CreateMoveToDashboardLink(BotDto bot) { var teamId = bot.Teams.FirstOrDefault()?.Id; return teamId.HasValue - ? LinkBuilder.Build($"dashboard/{bot.Id:N}/{teamId.Value:N}") - : LinkBuilder.Build($"dashboard/{bot.Id:N}"); + ? NavRouter.CreateRoute($"dashboard/{bot.Id:N}/{teamId.Value:N}") + : NavRouter.CreateRoute($"dashboard/{bot.Id:N}"); } private async Task Remove() diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/NavigationStages.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/NavigationStages.razor index a992e585..5ebecd72 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/NavigationStages.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/NavigationStages.razor @@ -26,7 +26,7 @@ ? "nav-stages__item_can-move" : string.Empty; - private string GetStageTitle(Stage stage) => $"Constructor_Stage{stage}"; + private MessageId GetStageTitle(Stage stage) => new($"Constructor_Stage{stage}"); private void MoveTo(Stage stage) { diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage1/CheckBot.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage1/CheckBot.razor index 931b7d0c..87a7fd8e 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage1/CheckBot.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage1/CheckBot.razor @@ -47,7 +47,7 @@ public StagesState StagesState { get; set; } = default!; [Parameter, EditorRequired] - public Func LinkFactory { get; set; } = default!; + public Func LinkFactory { get; set; } = default!; [Parameter, EditorRequired] public Func MoveToNext { get; set; } = default!; diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/InputFeatures.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/InputFeatures.razor index f8b36d8b..82faeb55 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/InputFeatures.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/InputFeatures.razor @@ -3,7 +3,7 @@ @inherits InputBase> @inject DragAndDropService DragAndDropService -@inject ResourcesManager ResourcesManager +@inject ResourcesManager Resources @inject FeaturesFactory FeaturesFactory
@@ -24,7 +24,7 @@ Description="@FeaturesFactory.CreateDescription(feature.Name)"> @@ -32,7 +32,7 @@ } else { -

@FeaturesAvailableEmptyText

+

@Resources[Messages.Constructor_FormSectionFeaturesAvailableEmptyText]

}
@@ -60,7 +60,7 @@ } else { -

@FeaturesSelectedEmptyText

+

@Resources[Messages.Constructor_FormSectionFeaturesSelectedEmptyText]

} @if (Validation is not null) { @@ -73,21 +73,9 @@ [Parameter, EditorRequired] public IReadOnlyCollection Features { get; set; } = default!; - [Parameter, EditorRequired] - public string AddText { get; set; } = default!; - - [Parameter, EditorRequired] - public string RemoveText { get; set; } = default!; - [Parameter, EditorRequired] public EventCallback> ValuesChanged { get; set; } - [Parameter] - public string? FeaturesAvailableEmptyText { get; set; } - - [Parameter] - public string? FeaturesSelectedEmptyText { get; set; } - [Parameter] public RenderFragment? Validation { get; set; } diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/SelectFeatures.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/SelectFeatures.razor index edcaf1b9..37f9b7f2 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/SelectFeatures.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage2/SelectFeatures.razor @@ -17,11 +17,7 @@ Value="_formModel.FeatureIds" ValueExpression="@(() => _formModel.FeatureIds)" ValuesChanged="(IEnumerable v) => _formModel.SetFeatures(v)" - Features="StagesState.AvailableFeatures" - AddText="@Resources[Messages.Constructor_FeatureAdd]" - RemoveText="@Resources[Messages.Constructor_FeatureRemove]" - FeaturesAvailableEmptyText="@Resources[Messages.Constructor_FormSectionFeaturesAvailableEmptyText]" - FeaturesSelectedEmptyText="@Resources[Messages.Constructor_FormSectionFeaturesSelectedEmptyText]"> + Features="StagesState.AvailableFeatures"> @@ -37,7 +33,7 @@ public StagesState StagesState { get; set; } = default!; [Parameter, EditorRequired] - public Func LinkFactory { get; set; } = default!; + public Func LinkFactory { get; set; } = default!; [Parameter, EditorRequired] public Func MoveToNext { get; set; } = default!; diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/CalendarEditor.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/CalendarEditor.razor index 6d6d7f12..4c374227 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/CalendarEditor.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/CalendarEditor.razor @@ -184,7 +184,7 @@ StateHasChanged(); } - private string GetDayOfWeekTitle(DayOfWeek dayOfWeek) => $"Constructor_DayOfWeek{dayOfWeek}"; + private MessageId GetDayOfWeekTitle(DayOfWeek dayOfWeek) => new($"Constructor_DayOfWeek{dayOfWeek}"); public async Task SubmitForm() { diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/SetSettings.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/SetSettings.razor index 5bdc0f4f..62640dcf 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/SetSettings.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage3/SetSettings.razor @@ -1,5 +1,3 @@ -@using Inc.TeamAssistant.Primitives.Languages - @inject IServiceProvider ServiceProvider @inject ResourcesManager Resources @@ -82,7 +80,7 @@ public StagesState StagesState { get; set; } = default!; [Parameter, EditorRequired] - public Func LinkFactory { get; set; } = default!; + public Func LinkFactory { get; set; } = default!; [Parameter, EditorRequired] public Func MoveToNext { get; set; } = default!; diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage4/Complete.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage4/Complete.razor index d05ee19d..290d7d52 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage4/Complete.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/Stage4/Complete.razor @@ -1,5 +1,4 @@ @using Inc.TeamAssistant.Primitives.FeatureProperties -@using Inc.TeamAssistant.Primitives.Languages @inject IBotService BotService @inject IServiceProvider ServiceProvider @@ -152,7 +151,7 @@ public StagesState StagesState { get; set; } = default!; [Parameter, EditorRequired] - public Func LinkFactory { get; set; } = default!; + public Func LinkFactory { get; set; } = default!; [Parameter, EditorRequired] public Func MoveToNext { get; set; } = default!; diff --git a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/StagesPage.razor b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/StagesPage.razor index 5b21c7d8..8a71b183 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/StagesPage.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Constructor/Stages/StagesPage.razor @@ -15,10 +15,8 @@ @inject ICalendarService CalendarService @inject IServiceProvider ServiceProvider @inject ResourcesManager Resources -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter @inject RequestProcessor RequestProcessor -@inject IJSRuntime JsRuntime -@inject NavigationManager NavigationManager - + @context.AssessmentDate.ToString("dd-MM-yyyy") @@ -42,7 +42,7 @@ }
@@ -90,13 +90,15 @@ }); } - private string GetAssessmentSessionUrl() => LinkBuilder.Build($"assessment-session/{TeamId:N}"); - - private string MoveToItem(DateOnly date) => LinkBuilder.Build($"assessment-history/{TeamId:N}/{date:yyyy-MM-dd}"); + private string CreateAssessmentSessionLink() => NavRouter.CreateRoute($"assessment-session/{TeamId:N}"); + + private string CreateMoveToHistoryLink(DateOnly date) + => NavRouter.CreateRoute($"assessment-history/{TeamId:N}/{date:yyyy-MM-dd}"); private async Task Changed(DateOnly date) { _date = date; + await Load(); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserHistoryWidget.razor.css b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserHistoryWidget.razor.css index 87d71f19..1280676e 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserHistoryWidget.razor.css +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserHistoryWidget.razor.css @@ -1,11 +1,7 @@ -.component-container { - height: calc(100% - 136px); -} .component-actions { display: flex; justify-content: right; - margin-top: 20px; - padding: 0 10px; + margin: 10px; } ::deep .no-data { height: 100%; diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserIntegrationWidget.razor.css b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserIntegrationWidget.razor.css index 0469e3ea..e7efa018 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserIntegrationWidget.razor.css +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Appraiser/AppraiserIntegrationWidget.razor.css @@ -1,16 +1,13 @@ .component-container { position: relative; - height: calc(100% - 30px); } .appraiser-integration__body { padding: 0 10px; - height: calc(100% - 70px); } .component-actions { display: flex; justify-content: right; - margin-top: 20px; - padding: 0 10px; + margin: 10px; } .appraiser-integration__button { margin-left: 10px; @@ -34,7 +31,4 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); -} -::deep form { - height: 100%; } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/BotNotSelected.razor b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/BotNotSelected.razor index 05622160..c09c6f27 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/BotNotSelected.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/BotNotSelected.razor @@ -1,10 +1,10 @@ @inject ResourcesManager Resources -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter

- @Resources[Messages.Dashboard_SelectTeam] + @Resources[Messages.Dashboard_SelectTeam] @Resources[Messages.Dashboard_CreateBot]

diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/CheckIn/MapWidget.razor b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/CheckIn/MapWidget.razor index 81164c0d..036243b2 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/CheckIn/MapWidget.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/CheckIn/MapWidget.razor @@ -1,6 +1,6 @@ @inject ICheckInService CheckInService @inject ResourcesManager Resources -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter @inject RequestProcessor RequestProcessor @@ -20,7 +20,7 @@ @if (_formModel.MapId.HasValue) { - var mapUrl = LinkBuilder.Build($"map/{_formModel.MapId.Value:N}"); + var mapUrl = NavRouter.CreateRoute($"map/{_formModel.MapId.Value:N}");
diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardPage.razor b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardPage.razor index bfb1503e..93194ca8 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardPage.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardPage.razor @@ -15,7 +15,7 @@ @inject IBotService BotService @inject ResourcesManager Resources @inject RequestProcessor RequestProcessor -@inject IJSRuntime JsRuntime +@inject NavRouter NavRouter $"dashboard/{botContext.BotId:N}/{botContext.TeamId:N}", (true, false) => $"dashboard/{botContext.BotId:N}", _ => "dashboard" }; - + _botId = botContext.BotId; _teamId = botContext.TeamId; - - await JsRuntime.Execute(JsFunctions.ChangeUrl(url)); + + await NavRouter.MoveToRoute(routeSegment); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardTeamSelector.razor b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardTeamSelector.razor index b767133e..40ac3b8d 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardTeamSelector.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/DashboardTeamSelector.razor @@ -1,10 +1,9 @@ @using Inc.TeamAssistant.Connector.Model.Queries.GetBotsByCurrentUser @inject IBotService BotService -@inject NavigationManager NavigationManager @inject ResourcesManager Resources @inject RequestProcessor RequestProcessor -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter
- + @ConvertToString(context.FirstTouch)FT
@@ -62,7 +63,7 @@ @ConvertToString(context.Correction)C

- + @ConvertToString(context.Review)R @@ -149,17 +150,37 @@ return builder.ToString(); } - private string ConvertToString(TimeSpan value) - { - const string timeFormat = @"d\.hh\:mm"; + private string ConvertToString(TimeSpan value) => value.ToString(GlobalSettings.TimeFormat); + + private string FirstTouchTitle() + { + var builder = new StringBuilder(); - return value.ToString(timeFormat); + builder.AppendLine(Resources[Messages.Dashboard_FirstTouch]); + builder.Append(Resources[Messages.Dashboard_FirstTouchHelp]); + + return builder.ToString(); } private string CorrectionTitle(TaskForReviewDto task) { - return task.Iterations == 0 + var builder = new StringBuilder(); + + builder.AppendLine(task.Iterations == 0 ? Resources[Messages.Dashboard_Correction] - : $"{Resources[Messages.Dashboard_Correction]} ({task.Iterations})"; + : $"{Resources[Messages.Dashboard_Correction]} ({task.Iterations})"); + builder.Append(Resources[Messages.Dashboard_CorrectionHelp]); + + return builder.ToString(); + } + + private string ReviewTitle() + { + var builder = new StringBuilder(); + + builder.AppendLine(Resources[Messages.Dashboard_Review]); + builder.Append(Resources[Messages.Dashboard_ReviewHelp]); + + return builder.ToString(); } } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor index 00c0efca..f88f73ac 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor @@ -5,6 +5,7 @@ @inject ResourcesManager Resources @inject DragAndDropService DragAndDropService @inject IServiceProvider ServiceProvider +@inject FeaturesFactory FeaturesFactory @@ -27,6 +28,7 @@
Widgets { get; set; } = Array.Empty(); + public IReadOnlyCollection Widgets { get; set; } = default!; private ContentDialog? _contentDialog; @@ -152,6 +154,16 @@ } private string ToWidgetTitle(string type) => WidgetsLookup.GetValueOrDefault(type, type); + + private string FeatureHelp(DashboardSettingsItem item) + { + if (item.CanEnabled) + return string.Empty; + + var featureName = FeaturesFactory.CreateName(item.Feature); + + return string.Format(Resources[Messages.Dashboard_DisableWidgetHelpTemplate], featureName); + } private void HandleDrop(DashboardSettingsItem[] items, DashboardSettingsItem target) { diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor.css b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor.css index 60c16dad..ee719844 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor.css +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettings.razor.css @@ -4,8 +4,7 @@ justify-content: right; } .widget-settings__body { - margin: 20px; - height: calc(100% - 90px); + margin: 10px; } .widget-settings__item { background-color: #555; @@ -19,15 +18,12 @@ border: 3px #555 solid; } .widget-settings_on-drag .widget-settings__item { - border: 3px #AAA dashed; + border: 3px #aaa dashed; } .widget-settings__actions { display: flex; justify-content: right; - padding: 0 10px; -} -.widget-settings { - height: calc(100% - 20px); + margin: 0 10px 10px 10px; } .widget-settings__drag { display: flex; @@ -35,9 +31,6 @@ .widget-settings__button, .widget-settings__label, .widget-settings__drag, ::deep .widget-settings__checkbox { margin-right: 10px; } -::deep form { - height: 100%; -} @media (max-width: 767.98px) { .dashboard-settings { display: none; diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsFormModel.cs b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsFormModel.cs index 9f4f53a3..8fe53181 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsFormModel.cs +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsFormModel.cs @@ -16,6 +16,7 @@ public DashboardSettingsFormModel Apply(IReadOnlyCollection widgets) _items.AddRange(widgets.Select(w => new DashboardSettingsItem { Type = w.Type, + Feature = w.Feature, Position = w.Position, CanEnabled = w.CanEnabled, IsVisible = w.IsVisible diff --git a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsItem.cs b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsItem.cs index de0b8153..d50166d6 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsItem.cs +++ b/src/Inc.TeamAssistant.WebUI/Features/Dashboard/Settings/DashboardSettingsItem.cs @@ -3,6 +3,7 @@ namespace Inc.TeamAssistant.WebUI.Features.Dashboard.Settings; public sealed class DashboardSettingsItem { public string Type { get; set; } = string.Empty; + public string Feature { get; set; } = string.Empty; public int Position { get; set; } public bool CanEnabled { get; set; } public bool IsVisible { get; set; } diff --git a/src/Inc.TeamAssistant.WebUI/Features/Layouts/CultureSelector.razor b/src/Inc.TeamAssistant.WebUI/Features/Layouts/CultureSelector.razor index 26cfc736..690bce2f 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Layouts/CultureSelector.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Layouts/CultureSelector.razor @@ -1,37 +1,41 @@ -@using Inc.TeamAssistant.Primitives.Languages +@implements IDisposable -@inject NavigationManager NavigationManager +@inject NavRouter NavRouter -@foreach (var culture in LanguageSettings.LanguageIds) +@foreach (var language in _languages) { - var path = $"/{culture.Value}/{_cleanUrl}"; - - - @culture.Value + + @language.Culture } @code { - private string _cleanUrl = string.Empty; + private IReadOnlyCollection<(string Culture, string Path)> _languages = Array.Empty<(string, string)>(); + private IDisposable? _routerScope; protected override void OnParametersSet() { - Load(); + SetLanguages(); - NavigationManager.LocationChanged += (s, e) => Load(); + _routerScope = NavRouter.OnRouteChanged(SetLanguages); } - private void Load() + private void SetLanguages() { - var cleanUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); - - foreach (var languageId in LanguageSettings.LanguageIds) - cleanUrl = cleanUrl.TrimStart($"/{languageId.Value}".ToArray()); + var routeWithoutLanguage = NavRouter.GetRouteSegment(); - _cleanUrl = cleanUrl; + _languages = LanguageSettings.LanguageIds + .Select(l => (l.Value, $"/{l.Value}/{routeWithoutLanguage}")) + .ToArray(); StateHasChanged(); } - private void MoveTo(string path) => NavigationManager.NavigateTo(path, forceLoad: true); + private Task MoveTo(string path) => NavRouter.MoveToRoute(path, RoutingType.Server); + + public void Dispose() => _routerScope?.Dispose(); } \ No newline at end of file diff --git a/src/Inc.TeamAssistant.WebUI/Features/Layouts/MainFooter.razor b/src/Inc.TeamAssistant.WebUI/Features/Layouts/MainFooter.razor index 45d47c94..3ee1bc1c 100644 --- a/src/Inc.TeamAssistant.WebUI/Features/Layouts/MainFooter.razor +++ b/src/Inc.TeamAssistant.WebUI/Features/Layouts/MainFooter.razor @@ -1,6 +1,6 @@ @inject IRenderContext RenderContext @inject ResourcesManager Resources -@inject LinkBuilder LinkBuilder +@inject NavRouter NavRouter