From 997dcee78f411397bf48a5144320aed6542453af Mon Sep 17 00:00:00 2001 From: Meik Date: Mon, 13 Jul 2026 11:51:11 +0200 Subject: [PATCH] fix(search): open unassigned tickets in Matrix42 --- .../Helper/TicketExternalLinkHelperTest.cs | 106 ++++++++++++++++++ .../Basics/Helper/TicketDeepLinkHelper.cs | 87 +++++++++++++- 2 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 F4SD.Cockpit.Client.Test/Basics/Helper/TicketExternalLinkHelperTest.cs diff --git a/F4SD.Cockpit.Client.Test/Basics/Helper/TicketExternalLinkHelperTest.cs b/F4SD.Cockpit.Client.Test/Basics/Helper/TicketExternalLinkHelperTest.cs new file mode 100644 index 0000000..9492f88 --- /dev/null +++ b/F4SD.Cockpit.Client.Test/Basics/Helper/TicketExternalLinkHelperTest.cs @@ -0,0 +1,106 @@ +using C4IT.FASD.Base; +using FasdDesktopUi.Basics.Helper; + +namespace F4SD.Cockpit.Client.Test.Basics.Helper; + +public class TicketExternalLinkHelperTest +{ + [Fact] + public void HasValidUserIdentity_NullIdentities_ReturnsFalse() + { + var relation = CreateTicketRelation(); + + Assert.False(TicketExternalLinkHelper.HasValidUserIdentity(relation)); + } + + [Fact] + public void HasValidUserIdentity_EmptyUserId_ReturnsFalse() + { + var relation = CreateTicketRelation(); + relation.Identities = + [ + new cF4sdIdentityEntry { Class = enumFasdInformationClass.Ticket, Id = relation.id }, + new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = Guid.Empty } + ]; + + Assert.False(TicketExternalLinkHelper.HasValidUserIdentity(relation)); + } + + [Fact] + public void HasValidUserIdentity_ValidUserId_ReturnsTrue() + { + var relation = CreateTicketRelation(); + relation.Identities = + [ + new cF4sdIdentityEntry { Class = enumFasdInformationClass.User, Id = Guid.NewGuid() } + ]; + + Assert.True(TicketExternalLinkHelper.HasValidUserIdentity(relation)); + } + + [Fact] + public void TryGetTicketLink_UsesProvidedPreviewLink() + { + var relation = CreateTicketRelation(); + relation.Infos = new Dictionary + { + ["DirectLinkPreview"] = "https://m42.example/wm/ticket/preview" + }; + + var result = TicketExternalLinkHelper.TryGetTicketLink(relation, null, out var ticketLink); + + Assert.True(result); + Assert.Equal("https://m42.example/wm/ticket/preview", ticketLink.TrimEnd('/')); + } + + [Fact] + public void TryGetTicketLink_BuildsPreviewLinkForOverviewRelation() + { + var relation = CreateTicketRelation(); + relation.Infos = new Dictionary + { + ["ActivityType"] = "SPSActivityTypeIncident" + }; + + var result = TicketExternalLinkHelper.TryGetTicketLink(relation, "m42.example", out var ticketLink); + + Assert.True(result); + Assert.Equal( + $"https://m42.example/wm/app-ServiceDesk/notSet/preview-object/SPSActivityTypeIncident/{relation.id:D}/0/", + ticketLink); + } + + [Fact] + public void TryGetTicketLink_UsesDefaultTicketActivityType() + { + var relation = CreateTicketRelation(); + + var result = TicketExternalLinkHelper.TryGetTicketLink(relation, "https://m42.example/", out var ticketLink); + + Assert.True(result); + Assert.Contains("/SPSActivityTypeTicket/", ticketLink); + } + + [Fact] + public void TryGetTicketLink_MissingTicketId_ReturnsFalse() + { + var relation = CreateTicketRelation(); + relation.id = Guid.Empty; + + var result = TicketExternalLinkHelper.TryGetTicketLink(relation, "https://m42.example", out var ticketLink); + + Assert.False(result); + Assert.Null(ticketLink); + } + + private static cF4sdApiSearchResultRelation CreateTicketRelation() + { + return new cF4sdApiSearchResultRelation + { + Type = enumF4sdSearchResultClass.Ticket, + Name = "TCK00001", + DisplayName = "TCK00001", + id = Guid.NewGuid() + }; + } +} diff --git a/FasdDesktopUi/Basics/Helper/TicketDeepLinkHelper.cs b/FasdDesktopUi/Basics/Helper/TicketDeepLinkHelper.cs index 5e9af38..ae43b05 100644 --- a/FasdDesktopUi/Basics/Helper/TicketDeepLinkHelper.cs +++ b/FasdDesktopUi/Basics/Helper/TicketDeepLinkHelper.cs @@ -13,6 +13,8 @@ namespace FasdDesktopUi.Basics.Helper { internal static class TicketExternalLinkHelper { + private const string DefaultTicketActivityType = "SPSActivityTypeTicket"; + internal static bool TryOpenTicketRelationExternally(cF4sdApiSearchResultRelation relation) { try @@ -25,15 +27,21 @@ namespace FasdDesktopUi.Basics.Helper var processing = ShouldOpenExternally(ticketType); // check if we have a valid user id in the id list => if not we could open this ticket only extern. - var hasUser = relation.Identities.Any(e => (e.Class == enumFasdInformationClass.User && e.Id != null && e.Id != Guid.Empty)); + var hasUser = HasValidUserIdentity(relation); if (!hasUser) processing = enumTicketProcessing.Extern; if (processing == enumTicketProcessing.Intern) return false; - if (relation?.Infos?.TryGetValue("TicketLink", out var ticketLink) == true && !string.IsNullOrWhiteSpace(ticketLink)) - new cBrowsers().Start("default", ticketLink); + var m42Server = cCockpitConfiguration.Instance?.m42ServerConfiguration?.Server; + if (!TryGetTicketLink(relation, m42Server, out var ticketLink)) + { + LogEntry($"Could not resolve an external Matrix42 link for ticket '{relation.DisplayName ?? relation.Name}'.", LogLevels.Warning); + return false; + } + + new cBrowsers().Start("default", ticketLink); return processing == enumTicketProcessing.Extern; } @@ -45,6 +53,79 @@ namespace FasdDesktopUi.Basics.Helper return false; } + internal static bool HasValidUserIdentity(cF4sdApiSearchResultRelation relation) + { + return relation?.Identities?.Any(identity => + identity.Class == enumFasdInformationClass.User && identity.Id != Guid.Empty) == true; + } + + internal static bool TryGetTicketLink(cF4sdApiSearchResultRelation relation, string m42Server, out string ticketLink) + { + ticketLink = null; + if (relation == null || relation.Type != enumF4sdSearchResultClass.Ticket) + return false; + + if (TryGetHttpLink(relation, "TicketLink", out ticketLink) || + TryGetHttpLink(relation, "DirectLinkPreview", out ticketLink)) + { + return true; + } + + if (relation.id == Guid.Empty || string.IsNullOrWhiteSpace(m42Server)) + return false; + + var server = m42Server.Trim(); + if (!server.Contains("://")) + server = $"https://{server}"; + + if (!Uri.TryCreate(server, UriKind.Absolute, out var serverUri) || + (serverUri.Scheme != Uri.UriSchemeHttp && serverUri.Scheme != Uri.UriSchemeHttps)) + { + return false; + } + + var activityType = GetActivityType(relation); + var encodedActivityType = Uri.EscapeDataString(activityType); + ticketLink = $"{serverUri.Scheme}://{serverUri.Authority}/wm/app-ServiceDesk/notSet/preview-object/{encodedActivityType}/{relation.id:D}/0/"; + return true; + } + + private static bool TryGetHttpLink(cF4sdApiSearchResultRelation relation, string key, out string ticketLink) + { + ticketLink = null; + if (relation?.Infos?.TryGetValue(key, out var configuredLink) != true || string.IsNullOrWhiteSpace(configuredLink)) + return false; + + if (!Uri.TryCreate(configuredLink, UriKind.Absolute, out var linkUri) || + (linkUri.Scheme != Uri.UriSchemeHttp && linkUri.Scheme != Uri.UriSchemeHttps)) + { + return false; + } + + ticketLink = linkUri.AbsoluteUri; + return true; + } + + private static string GetActivityType(cF4sdApiSearchResultRelation relation) + { + if (relation?.Infos?.TryGetValue("ActivityType", out var activityType) == true && !string.IsNullOrWhiteSpace(activityType)) + return activityType.Trim(); + + if (relation?.Infos?.TryGetValue("TicketType", out var ticketTypeValue) == true && + Enum.TryParse(ticketTypeValue, true, out enumTicketType ticketType)) + { + switch (ticketType) + { + case enumTicketType.Incident: + return "SPSActivityTypeIncident"; + case enumTicketType.ServiceRequest: + return "SPSActivityTypeServiceRequest"; + } + } + + return DefaultTicketActivityType; + } + private static enumTicketType GetTicketType(cF4sdApiSearchResultRelation relation) { if (relation?.Infos != null && relation.Infos.TryGetValue("TicketType", out var ticketTypeValue))