From ce2f0d0d75ce89514d6d5376cfa63ddee1bae31d Mon Sep 17 00:00:00 2001 From: Nikola Kukrika Date: Wed, 4 Dec 2024 12:41:29 +0100 Subject: [PATCH 1/2] Moving Agent to System App --- .../App/Agent/ExtensionLogo.png | Bin 0 -> 2408 bytes .../Interaction/AgentMessage.Codeunit.al | 75 +++ .../Interaction/AgentMessageImpl.Codeunit.al | 80 ++++ .../Agent/Interaction/AgentTask.Codeunit.al | 41 ++ .../Interaction/AgentTaskImpl.Codeunit.al | 202 +++++++++ .../Agent/Interaction/AgentTaskList.Page.al | 187 ++++++++ .../Interaction/AgentTaskMessageCard.Page.al | 167 +++++++ .../Interaction/AgentTaskMessageList.Page.al | 95 ++++ .../Interaction/AgentTaskStepList.Page.al | 81 ++++ .../Permissions/AgentObjects.PermissionSet.al | 23 + .../App/Agent/Setup/Agent.Codeunit.al | 174 +++++++ .../Agent/Setup/AgentAccessControl.Page.al | 134 ++++++ .../App/Agent/Setup/AgentCard.Page.al | 228 ++++++++++ .../App/Agent/Setup/AgentImpl.Codeunit.al | 429 ++++++++++++++++++ .../App/Agent/Setup/AgentList.Page.al | 93 ++++ .../Setup/SelectAgentAccessControl.Page.al | 179 ++++++++ .../App/Agent/TaskPane/TaskDetails.Page.al | 132 ++++++ .../App/Agent/TaskPane/TaskTimeline.Page.al | 270 +++++++++++ .../App/Agent/TaskPane/Tasks.Page.al | 131 ++++++ src/System Application/App/Agent/app.json | 48 ++ .../src/UserSubformPermissions.PageExt.al | 32 ++ .../App/Security Groups/app.json | 8 + .../SecurityGroupsObjects.PermissionSet.al | 2 + .../src/InheritedPermissionSetsPart.Page.al | 120 +++++ .../src/SecurityGroupPermissionSets.Page.al | 15 +- .../src/UserSecurityGroupsPart.Page.al | 79 ++++ .../App/User Permissions/app.json | 4 + .../UserPermissionsObjects.PermissionSet.al | 3 +- .../src/UserPermissions.Codeunit.al | 13 + .../src/UserPermissionsImpl.Codeunit.al | 26 ++ .../User Permissions/src/UserSubform.Page.al | 142 ++++++ .../src/UserSettings.Codeunit.al | 34 ++ .../src/UserSettingsImpl.Codeunit.al | 8 + 33 files changed, 3243 insertions(+), 12 deletions(-) create mode 100644 src/System Application/App/Agent/ExtensionLogo.png create mode 100644 src/System Application/App/Agent/Interaction/AgentMessage.Codeunit.al create mode 100644 src/System Application/App/Agent/Interaction/AgentMessageImpl.Codeunit.al create mode 100644 src/System Application/App/Agent/Interaction/AgentTask.Codeunit.al create mode 100644 src/System Application/App/Agent/Interaction/AgentTaskImpl.Codeunit.al create mode 100644 src/System Application/App/Agent/Interaction/AgentTaskList.Page.al create mode 100644 src/System Application/App/Agent/Interaction/AgentTaskMessageCard.Page.al create mode 100644 src/System Application/App/Agent/Interaction/AgentTaskMessageList.Page.al create mode 100644 src/System Application/App/Agent/Interaction/AgentTaskStepList.Page.al create mode 100644 src/System Application/App/Agent/Permissions/AgentObjects.PermissionSet.al create mode 100644 src/System Application/App/Agent/Setup/Agent.Codeunit.al create mode 100644 src/System Application/App/Agent/Setup/AgentAccessControl.Page.al create mode 100644 src/System Application/App/Agent/Setup/AgentCard.Page.al create mode 100644 src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al create mode 100644 src/System Application/App/Agent/Setup/AgentList.Page.al create mode 100644 src/System Application/App/Agent/Setup/SelectAgentAccessControl.Page.al create mode 100644 src/System Application/App/Agent/TaskPane/TaskDetails.Page.al create mode 100644 src/System Application/App/Agent/TaskPane/TaskTimeline.Page.al create mode 100644 src/System Application/App/Agent/TaskPane/Tasks.Page.al create mode 100644 src/System Application/App/Agent/app.json create mode 100644 src/System Application/App/Permission Sets/src/UserSubformPermissions.PageExt.al create mode 100644 src/System Application/App/Security Groups/src/InheritedPermissionSetsPart.Page.al create mode 100644 src/System Application/App/Security Groups/src/UserSecurityGroupsPart.Page.al create mode 100644 src/System Application/App/User Permissions/src/UserSubform.Page.al diff --git a/src/System Application/App/Agent/ExtensionLogo.png b/src/System Application/App/Agent/ExtensionLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..f53be3156b9b6164dfc7e90c0043ef0bdd1ddc9f GIT binary patch literal 2408 zcmaJ@c~lc=67K|p5CM&Hh(I_L0nu<|89=~9xraqA<(MS{ivlY?b-2+CB*75y3?3sQ z4q+7pQ4R$~%#sfAfZ-e!L0mcv0RYem@b?Y_02D%^08I^KiAk+aL>8?d{G-zVVD&oHi+VAdD?@l21H5y%O3qs%KE8bD!`EEZw++`UO;#&;JgV~>&KV$zfB~>T2tY>zdO$e< zU;rC{jQWyAA8s)N4n#Ru;Zn?SNf&v#i)!?ImM9Wfn~MPi^#EC_4`_mDdO|@giaZK| z5kNyfyg2HMqQiD78HeV!7td;zCdit!vk<#b+QYs2P1Gyg^CL<*yYS&j9yK+N30{WX z)ogHzUB8jX1Rq78@CG*wG=`V<cZQ@3S7pbl9 zfz=OCITl1;X(u1_!zz2H9;okhe~5hluts_O2@IxlfA;m3>XV;Bef`gBj-{uBARCHW z0>xZrU%y&5J5i1}J}wX$C~CRW=vo&f2>iV#4qS!7ZH)V-`G}#L#+#jgL@V`g|JIv< zf+B%-NB^DIwcN4!v8akQ7aFS<3H6zJ2!g18Nn^h^Y`CFv5GI|R_oN4sUv_xP+;bMK z=FL8(Z^UJt7KPd%LFj{<14!~|P@s%KmlnFR3updu!amFI-!Lfr1-+`{aJ*E6H-DV9 zknuLyIhS47_0;3^c+_JsS>g6xImca}1Ss+5vlVI;bF4UBR1c5PaK)WNZERjgo>xW4P%XdaA9Q9!7$MAmqA7IMx*0F|Zuo2{%^7{2Tko5XGX=6q{; zl~DJCHDsuGL!h@X!S3mtDXMpjk7z*v&cJ4Kk@S(uR#9`6{LS}IjC041U21W475&eH zrgDd)toR_O>~UY~nZg3}Am+E`8$TR@IBg&0=@k(B2|Srg!zXGIHqlt-@q65{lSYdB zXd4%@Q?X>H_mq$GW9_yehG?(y+-@W_R3>RU{m}Xxm!4CPc%Ka(69Q^3SZcW6|a6AB3t{jt{imO~$i#$oi~8l3C?tE0DIBn79lul`ZPavnAxn@#Y{d zlF`#{CI6I0;!+F)%T^H<8!pVdW7*qfM`xa;X~4V_p4;N1mo1B+nTjVy-wvt5sn0it z`eKHu&c~(16U6_#y4?Y~MkOLivZF`pX0BhFUgk1kku^svJv5PJuF9Sr-9=YI=}rg_ zRt$C00>`P|GNUD;I?kwX1^n~2UM$VHlH=$QQ(@=qQDB2oXA)FNI|+N0MFUt z9h_z?9-eZdYCanZb?r_>?y_`>FTJ)w@v-VbvXS|_L`_{`=oR;$Y-Nqx=_8Eu`U?Af zIz_2n1)Fx>Lk@X6B`g$fwAXkyqB~vN=J9TK5u_(4SVmxSgw}SoFE1-p&W0!?S zLPjZcvvM#gIEq+iBNs{JOpj%f(s&+kK97F}Yx*Uo4KqHqqaQg(r4%)Px9T2C zLG0uZe@kc2+9p-0N{6g}9qks-GW8%Dfl+W+>vy7jHIsH36OwpvE+wBdE%r!A(qz&; zVv4v;S%!)NCWn$qQL^X7z2RbuN&)yD-9Z^HZM-!Zt<&7q+nSM8Qg8{Yv|MeTg{=~qda4zeCzW;oFZU06H1_%Y+ERLai;WG(RV6I5KGgyYB)LKsV;s?C(SyQ)cq54P z$pG&O)z<+Md&Zge^jfCbO#*!|aNO3i?gIYhf{n~4xXtts2CiH8x^{YQoyJ5zd+NLE zRO(heKF_M?{w+Ug6|uYNAy|gaZd6|uFIEJ3vIV4;jA0#L*F1EGNp`>GqD7?*P?Lo# zS!3&i3TYiFxO5$;9-ET8HL1Uv8NR0T|C7a^OZ_p8d^xzN=q;Z}OV)&j TThioJs%?PJ9`Cx{ae4m*g9kO? literal 0 HcmV?d00001 diff --git a/src/System Application/App/Agent/Interaction/AgentMessage.Codeunit.al b/src/System Application/App/Agent/Interaction/AgentMessage.Codeunit.al new file mode 100644 index 0000000000..1ec3011219 --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentMessage.Codeunit.al @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +codeunit 4307 "Agent Message" +{ + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Get the message text for the given agent task message. + /// + /// Agent task message. + /// The body of the agent task message. + [Scope('OnPrem')] + procedure GetText(var AgentTaskMessage: Record "Agent Task Message"): Text + var + AgentMessageImpl: Codeunit "Agent Message Impl."; + begin + exit(AgentMessageImpl.GetMessageText(AgentTaskMessage)); + end; + + /// + /// Updates the message text. + /// + /// The message record to update. + /// New message text to set. + [Scope('OnPrem')] + procedure UpdateText(var AgentTaskMessage: Record "Agent Task Message"; NewMessageText: Text) + var + AgentMessageImpl: Codeunit "Agent Message Impl."; + begin + AgentMessageImpl.UpdateText(AgentTaskMessage, NewMessageText); + end; + + /// + /// Check if it is possible to edit the message. + /// + /// Agent task message to verify. + /// If it is possible to change the message. + [Scope('OnPrem')] + procedure IsEditable(var AgentTaskMessage: Record "Agent Task Message"): Boolean + var + AgentMessageImpl: Codeunit "Agent Message Impl."; + begin + exit(AgentMessageImpl.IsMessageEditable(AgentTaskMessage)); + end; + + /// + /// Sets the message status to sent. + /// + /// Agent task message to update status. + [Scope('OnPrem')] + procedure SetStatusToSent(var AgentTaskMessage: Record "Agent Task Message") + var + AgentMessageImpl: Codeunit "Agent Message Impl."; + begin + AgentMessageImpl.SetStatusToSent(AgentTaskMessage); + end; + + /// + /// Downloads the attachments for a specific message. + /// + /// Message to download attachments for. + [Scope('OnPrem')] + procedure DownloadAttachments(var AgentTaskMessage: Record "Agent Task Message") + var + AgentMessageImpl: Codeunit "Agent Message Impl."; + begin + AgentMessageImpl.DownloadAttachments(AgentTaskMessage); + end; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Interaction/AgentMessageImpl.Codeunit.al b/src/System Application/App/Agent/Interaction/AgentMessageImpl.Codeunit.al new file mode 100644 index 0000000000..17a727fd55 --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentMessageImpl.Codeunit.al @@ -0,0 +1,80 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +codeunit 4308 "Agent Message Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure GetMessageText(var AgentTaskMessage: Record "Agent Task Message"): Text + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + ContentInStream: InStream; + ContentText: Text; + begin + AgentTaskMessage.CalcFields(Content); + AgentTaskMessage.Content.CreateInStream(ContentInStream, AgentTaskImpl.GetDefaultEncoding()); + ContentInStream.Read(ContentText); + exit(ContentText); + end; + + procedure UpdateText(var AgentTaskMessage: Record "Agent Task Message"; NewMessageText: Text) + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + ContentOutStream: OutStream; + begin + Clear(AgentTaskMessage.Content); + AgentTaskMessage.Content.CreateOutStream(ContentOutStream, AgentTaskImpl.GetDefaultEncoding()); + ContentOutStream.Write(NewMessageText); + AgentTaskMessage.Modify(true); + end; + + procedure IsMessageEditable(var AgentTaskMessage: Record "Agent Task Message"): Boolean + begin + if AgentTaskMessage.Type <> AgentTaskMessage.Type::Output then + exit(false); + + exit(AgentTaskMessage.Status = AgentTaskMessage.Status::Draft); + end; + + procedure SetStatusToSent(var AgentTaskMessage: Record "Agent Task Message") + begin + UpdateAgentTaskMessageStatus(AgentTaskMessage, AgentTaskMessage.Status::Sent); + end; + + procedure DownloadAttachments(var AgentTaskMessage: Record "Agent Task Message") + var + AgentTaskFile: Record "Agent Task File"; + AgentTaskMessageAttachment: Record "Agent Task Message Attachment"; + AgentTaskImpl: Codeunit "Agent Task Impl."; + InStream: InStream; + FileName: Text; + DownloadDialogTitleLbl: Label 'Download Email Attachment'; + begin + AgentTaskMessageAttachment.SetRange("Task ID", AgentTaskMessage."Task ID"); + AgentTaskMessageAttachment.SetRange("Message ID", AgentTaskMessage.ID); + if not AgentTaskMessageAttachment.FindSet() then + exit; + + repeat + if not AgentTaskFile.Get(AgentTaskMessageAttachment."Task ID", AgentTaskMessageAttachment."File ID") then + exit; + + FileName := AgentTaskFile."File Name"; + AgentTaskFile.CalcFields(Content); + AgentTaskFile.Content.CreateInStream(InStream, AgentTaskImpl.GetDefaultEncoding()); + File.DownloadFromStream(InStream, DownloadDialogTitleLbl, '', '', FileName); + until AgentTaskMessageAttachment.Next() = 0; + end; + + procedure UpdateAgentTaskMessageStatus(var AgentTaskMessage: Record "Agent Task Message"; Status: Option) + begin + AgentTaskMessage.Status := Status; + AgentTaskMessage.Modify(true); + end; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Interaction/AgentTask.Codeunit.al b/src/System Application/App/Agent/Interaction/AgentTask.Codeunit.al new file mode 100644 index 0000000000..d2571da29b --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentTask.Codeunit.al @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +codeunit 4303 "Agent Task" +{ + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Check if a task exists for the given agent user and conversation + /// + /// The user security ID of the agent. + /// The conversation ID to check. + /// True if task exists, false if not. + [Scope('OnPrem')] + procedure TaskExists(AgentUserSecurityId: Guid; ConversationId: Text): Boolean + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + exit(AgentTaskImpl.TaskExists(AgentUserSecurityId, ConversationId)); + end; + + /// + /// Create a new task message for the given agent user and conversation. + /// If task does not exist, it will be created. + /// + /// Specifies from address. + /// The message text for the task. + /// Current Agent Task to which the message will be added. + [Scope('OnPrem')] + procedure CreateTaskMessage(From: Text[250]; MessageText: Text; ExternalMessageId: Text[2048]; var CurrentAgentTask: Record "Agent Task") + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + AgentTaskImpl.CreateTaskMessage(From, MessageText, ExternalMessageId, CurrentAgentTask); + end; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Interaction/AgentTaskImpl.Codeunit.al b/src/System Application/App/Agent/Interaction/AgentTaskImpl.Codeunit.al new file mode 100644 index 0000000000..539b10fb6d --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentTaskImpl.Codeunit.al @@ -0,0 +1,202 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +using System.Integration; +using System.Environment; + +codeunit 4300 "Agent Task Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure SetMessageText(var AgentTaskMessage: Record "Agent Task Message"; MessageText: Text) + var + ContentOutStream: OutStream; + begin + Clear(AgentTaskMessage.Content); + AgentTaskMessage.Content.CreateOutStream(ContentOutStream, GetDefaultEncoding()); + ContentOutStream.Write(MessageText); + AgentTaskMessage.Modify(true); + end; + + procedure GetStepsDoneCount(var AgentTask: Record "Agent Task"): Integer + var + AgentTaskStep: Record "Agent Task Step"; + begin + AgentTaskStep.SetRange("Task ID", AgentTask."ID"); + AgentTaskStep.ReadIsolation := IsolationLevel::ReadCommitted; + exit(AgentTaskStep.Count()); + end; + + procedure GetDetailsForAgentTaskStep(var AgentTaskStep: Record "Agent Task Step"): Text + var + ContentInStream: InStream; + ContentText: Text; + begin + AgentTaskStep.CalcFields(Details); + AgentTaskStep.Details.CreateInStream(ContentInStream, GetDefaultEncoding()); + ContentInStream.Read(ContentText); + exit(ContentText); + end; + + procedure ShowTaskSteps(var AgentTask: Record "Agent Task") + var + AgentTaskStep: Record "Agent Task Step"; + begin + AgentTaskStep.SetRange("Task ID", AgentTask.ID); + Page.Run(Page::"Agent Task Step List", AgentTaskStep); + end; + + procedure CreateTaskMessage(From: Text[250]; MessageText: Text; var CurrentAgentTask: Record "Agent Task") + begin + CreateTaskMessage(From, MessageText, '', CurrentAgentTask); + end; + + procedure CreateTaskMessage(From: Text[250]; MessageText: Text; ExternalMessageId: Text[2048]; var CurrentAgentTask: Record "Agent Task") + var + AgentTask: Record "Agent Task"; + AgentTaskMessage: Record "Agent Task Message"; + begin + if MessageText = '' then + Error(MessageTextMustBeProvidedErr); + + if not AgentTask.Get(CurrentAgentTask.RecordId) then begin + AgentTask."Agent User Security ID" := CurrentAgentTask."Agent User Security ID"; + AgentTask."Created By" := UserSecurityId(); + AgentTask."Needs Attention" := false; + AgentTask.Status := AgentTask.Status::Paused; + AgentTask.Title := CurrentAgentTask.Title; + AgentTask."External ID" := CurrentAgentTask."External ID"; + AgentTask.Insert(); + end; + + AgentTaskMessage."Task ID" := AgentTask.ID; + AgentTaskMessage."Type" := AgentTaskMessage."Type"::Input; + AgentTaskMessage."External ID" := ExternalMessageId; + AgentTaskMessage.From := From; + AgentTaskMessage.Insert(); + + SetMessageText(AgentTaskMessage, MessageText); + + // Only change the status if the task is in a status where it can be started again. + // If the task is running, we should not change the state, as platform will pickup a new message automatically. + if ((AgentTask.Status = AgentTask.Status::Paused) or (AgentTask.Status = AgentTask.Status::Completed)) then begin + AgentTask.Status := AgentTask.Status::Ready; + AgentTask.Modify(true); + end; + end; + + procedure CreateUserInterventionTaskStep(UserInterventionRequestStep: Record "Agent Task Step") + begin + CreateUserInterventionTaskStep(UserInterventionRequestStep, '', -1); + end; + + procedure CreateUserInterventionTaskStep(UserInterventionRequestStep: Record "Agent Task Step"; UserInput: Text) + begin + CreateUserInterventionTaskStep(UserInterventionRequestStep, UserInput, -1); + end; + + procedure CreateUserInterventionTaskStep(UserInterventionRequestStep: Record "Agent Task Step"; SelectedSuggestionId: Integer) + begin + CreateUserInterventionTaskStep(UserInterventionRequestStep, '', SelectedSuggestionId); + end; + + procedure CreateUserInterventionTaskStep(UserInterventionRequestStep: Record "Agent Task Step"; UserInput: Text; SelectedSuggestionId: Integer) + var + AgentTask: Record "Agent Task"; + AgentTaskStep: Record "Agent Task Step"; + DetailsOutStream: OutStream; + DetailsJson: JsonObject; + begin + AgentTask.Get(UserInterventionRequestStep."Task ID"); + + AgentTaskStep."Task ID" := AgentTask.ID; + AgentTaskStep."Type" := AgentTaskStep."Type"::"User Intervention"; + AgentTaskStep.Description := 'User intervention'; + DetailsJson.Add('interventionRequestStepNumber', UserInterventionRequestStep."Step Number"); + if UserInput <> '' then + DetailsJson.Add('userInput', UserInput); + if SelectedSuggestionId >= 0 then + DetailsJson.Add('selectedSuggestionId', SelectedSuggestionId); + AgentTaskStep.CalcFields(Details); + Clear(AgentTaskStep.Details); + AgentTaskStep.Details.CreateOutStream(DetailsOutStream, GetDefaultEncoding()); + DetailsJson.WriteTo(DetailsOutStream); + AgentTaskStep.Insert(); + end; + + procedure StopTask(var AgentTask: Record "Agent Task"; AgentTaskStatus: enum "Agent Task Status"; UserConfirm: Boolean) + begin + if ((AgentTask.Status = AgentTaskStatus) and (AgentTask."Needs Attention" = false)) then + exit; // Task is already stopped and does not need attention. + + if UserConfirm then + if not Confirm(AreYouSureThatYouWantToStopTheTaskQst) then + exit; + + AgentTask.Status := AgentTaskStatus; + AgentTask."Needs Attention" := false; + AgentTask.Modify(true); + end; + + procedure RestartTask(var AgentTask: Record "Agent Task"; UserConfirm: Boolean) + begin + if UserConfirm then + if not Confirm(AreYouSureThatYouWantToRestartTheTaskQst) then + exit; + + AgentTask."Needs Attention" := false; + AgentTask.Status := AgentTask.Status::Ready; + AgentTask.Modify(true); + end; + + procedure TaskExists(AgentUserSecurityID: Guid; ConversationId: Text): Boolean + var + AgentTask: Record "Agent Task"; + begin + AgentTask.SetRange("Agent User Security ID", AgentUserSecurityID); + AgentTask.ReadIsolation(IsolationLevel::ReadCommitted); + AgentTask.SetRange("External ID", ConversationId); + exit(not AgentTask.IsEmpty()); + end; + + procedure GetDefaultEncoding(): TextEncoding + begin + exit(TextEncoding::UTF8); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Action Triggers", GetAgentTaskMessagePageId, '', true, true)] + local procedure OnGetAgentTaskMessagePageId(var PageId: Integer) + begin + PageId := Page::"Agent Task Message Card"; + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Action Triggers", GetPageSummary, '', true, true)] + local procedure OnGetGetPageSummary(PageId: Integer; Bookmark: Text; var Summary: Text) + var + PageSummaryParameters: Record "Page Summary Parameters"; + PageSummaryProvider: Codeunit "Page Summary Provider"; + begin + if PageId = 0 then begin + Summary := ''; + exit; + end; + + PageSummaryParameters."Page ID" := PageId; +#pragma warning disable AA0139 + PageSummaryParameters.Bookmark := Bookmark; +#pragma warning restore AA0139 + PageSummaryParameters."Include Binary Data" := false; + Summary := PageSummaryProvider.GetPageSummary(PageSummaryParameters); + end; + + var + MessageTextMustBeProvidedErr: Label 'You must provide a message text.'; + AreYouSureThatYouWantToRestartTheTaskQst: Label 'Are you sure that you want to restart the task?'; + AreYouSureThatYouWantToStopTheTaskQst: Label 'Are you sure that you want to stop the task?'; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Interaction/AgentTaskList.Page.al b/src/System Application/App/Agent/Interaction/AgentTaskList.Page.al new file mode 100644 index 0000000000..85c2b144ad --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentTaskList.Page.al @@ -0,0 +1,187 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +page 4300 "Agent Task List" +{ + PageType = List; + ApplicationArea = All; + UsageCategory = Administration; + SourceTable = "Agent Task"; + Caption = 'Agent Tasks'; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + AdditionalSearchTerms = 'Agent Tasks, Agent Task, Agent, Agent Log, Agent Logs'; + SourceTableView = sorting("Last Step Timestamp") order(descending); + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(AgentConversations) + { + field(TaskID; Rec.ID) + { + Caption = 'Task ID'; + } + field(Title; Rec.Title) + { + Caption = 'Title'; + } + field(LastStepTimestamp; Rec."Last Step Timestamp") + { + Caption = 'Last Updated'; + } + field(LastStepNumber; Rec."Last Step Number") + { + } + field(Status; Rec.Status) + { + Caption = 'Status'; + ToolTip = 'Specifies the status of the agent task.'; + } + field(NeedsAttention; Rec."Needs Attention") + { + Caption = 'Needs Attention'; + ToolTip = 'Specifies whether the task needs attention.'; + } + field(CreatedAt; Rec.SystemCreatedAt) + { + Caption = 'Created at'; + ToolTip = 'Specifies the date and time when the agent task was created.'; + } + field(ID; Rec.ID) + { + Caption = 'ID'; + trigger OnDrillDown() + begin + ShowTaskMessages(); + end; + } + field(NumberOfStepsDone; NumberOfStepsDone) + { + Caption = 'Steps Done'; + ToolTip = 'Specifies the number of steps that have been done for the specific task.'; + + trigger OnDrillDown() + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + AgentTaskImpl.ShowTaskSteps(Rec); + end; + } + field("Created By"; Rec."Created By Full Name") + { + Caption = 'Created by'; + Tooltip = 'Specifies the full name of the user that created the agent task.'; + } + field("Agent Display Name"; Rec."Agent Display Name") + { + Caption = 'Agent'; + ToolTip = 'Specifies the agent that is associated with the task.'; + } + field(CreatedByID; Rec."Created By") + { + Visible = false; + } + field(AgentUserSecurityID; Rec."Agent User Security ID") + { + Visible = false; + } + } + } + } + actions + { + area(Processing) + { + action(ViewTaskMessage) + { + ApplicationArea = All; + Caption = 'View messages'; + ToolTip = 'Show messages for the selected task.'; + Image = ShowList; + + trigger OnAction() + begin + ShowTaskMessages(); + end; + } + action(ViewTaskSteps) + { + ApplicationArea = All; + Caption = 'View steps'; + ToolTip = 'Show steps for the selected task.'; + Image = TaskList; + + trigger OnAction() + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + AgentTaskImpl.ShowTaskSteps(Rec); + end; + } + action(Stop) + { + ApplicationArea = All; + Caption = 'Stop'; + ToolTip = 'Stop the selected task.'; + Image = Stop; + + trigger OnAction() + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + AgentTaskImpl.StopTask(Rec, Rec."Status"::"Stopped by User", true); + CurrPage.Update(false); + end; + } + } + area(Promoted) + { + group(Category_Process) + { + actionref(ViewTaskMessage_Promoted; ViewTaskMessage) + { + } + actionref(ViewTaskSteps_Promoted; ViewTaskSteps) + { + } + } + } + } + + trigger OnAfterGetRecord() + begin + UpdateControls(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateControls(); + end; + + local procedure UpdateControls() + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + NumberOfStepsDone := AgentTaskImpl.GetStepsDoneCount(Rec); + end; + + local procedure ShowTaskMessages() + var + AgentTaskMessage: Record "Agent Task Message"; + begin + AgentTaskMessage.SetRange("Task ID", Rec.ID); + Page.Run(Page::"Agent Task Message List", AgentTaskMessage); + end; + + var + NumberOfStepsDone: Integer; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Interaction/AgentTaskMessageCard.Page.al b/src/System Application/App/Agent/Interaction/AgentTaskMessageCard.Page.al new file mode 100644 index 0000000000..24c02a6c2e --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentTaskMessageCard.Page.al @@ -0,0 +1,167 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +page 4308 "Agent Task Message Card" +{ + PageType = Card; + ApplicationArea = All; + SourceTable = "Agent Task Message"; + InsertAllowed = false; + ModifyAllowed = true; + DeleteAllowed = false; + Caption = 'Agent Task Message'; + DataCaptionExpression = ''; + Extensible = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + group(General) + { + field(LastModifiedAt; Rec.SystemModifiedAt) + { + Caption = 'Last modified at'; + ToolTip = 'Specifies the date and time when the message was last modified.'; + } + field(CreatedAt; Rec.SystemCreatedAt) + { + Caption = 'Created at'; + ToolTip = 'Specifies the date and time when the message was created.'; + } + field(TaskID; Rec."Task Id") + { + Caption = 'Task ID'; + Visible = false; + } + field(MessageID; Rec."ID") + { + Caption = 'ID'; + Visible = false; + } + field(MessageType; Rec.Type) + { + Caption = 'Type'; + } + field(MessageFrom; Rec.From) + { + Visible = Rec.Type = Rec.Type::Input; + Caption = 'From'; + Editable = false; + } + field(Status; Rec.Status) + { + Caption = 'Status'; + Editable = false; + } + field(AttachmentsCount; AttachmentsCount) + { + Caption = 'Attachments'; + ToolTip = 'Specifies the number of attachments that are associated with the message.'; + Editable = false; + } + } + + group(Message) + { + Caption = 'Message'; + Editable = IsMessageEditable; + field(MessageText; GlobalMessageText) + { + ShowCaption = false; + Caption = 'Message'; + ToolTip = 'Specifies the message text.'; + MultiLine = true; + ExtendedDatatype = RichContent; + Editable = IsMessageEditable; + + trigger OnValidate() + var + AgentMessage: Codeunit "Agent Message"; + begin + AgentMessage.UpdateText(Rec, GlobalMessageText); + end; + + } + } + } + + } + + actions + { + area(Processing) + { + action(DownloadAttachment) + { + ApplicationArea = All; + Caption = 'Download attachments'; + ToolTip = 'Download the attachment.'; + Image = Download; + Enabled = AttachmentsCount > 0; + + trigger OnAction() + begin + DownloadAttachments(); + end; + } + } + area(Promoted) + { + group(Category_Process) + { + actionref(DownloadAttachment_Promoted; DownloadAttachment) + { + } + } + } + } + + trigger OnAfterGetRecord() + begin + UpdateControls(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateControls(); + end; + + local procedure UpdateControls() + var + AgentTaskMessageAttachment: Record "Agent Task Message Attachment"; + AgentMessage: Codeunit "Agent Message"; + begin + GlobalMessageText := AgentMessage.GetText(Rec); + IsMessageEditable := AgentMessage.IsEditable(Rec); + + AgentTaskMessageAttachment.SetRange("Task ID", Rec."Task ID"); + AgentTaskMessageAttachment.SetRange("Message ID", Rec.ID); + + AttachmentsCount := AgentTaskMessageAttachment.Count(); + if Rec.Type = Rec.Type::Output then + CurrPage.Caption(OutgoingMessageTxt); + if Rec.Type = Rec.Type::Input then + CurrPage.Caption(IncomingMessageTxt); + end; + + local procedure DownloadAttachments() + var + AgentMessage: Codeunit "Agent Message"; + begin + AgentMessage.DownloadAttachments(Rec); + end; + + var + GlobalMessageText: Text; + IsMessageEditable: Boolean; + AttachmentsCount: Integer; + OutgoingMessageTxt: Label 'Outgoing message'; + IncomingMessageTxt: Label 'Incoming message'; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Interaction/AgentTaskMessageList.Page.al b/src/System Application/App/Agent/Interaction/AgentTaskMessageList.Page.al new file mode 100644 index 0000000000..ad3a375cc2 --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentTaskMessageList.Page.al @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +page 4301 "Agent Task Message List" +{ + PageType = List; + ApplicationArea = All; + Caption = 'Agent Task Messages'; + UsageCategory = Administration; + SourceTable = "Agent Task Message"; + CardPageId = "Agent Task Message Card"; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + Editable = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(GroupName) + { + field(LastModifiedAt; Rec.SystemModifiedAt) + { + Caption = 'Last modified at'; + ToolTip = 'Specifies the date and time when the message was last modified.'; + } + field(CreatedAt; Rec.SystemCreatedAt) + { + Caption = 'Created at'; + ToolTip = 'Specifies the date and time when the message was created.'; + } + field(Status; Rec.Status) + { + Caption = 'Status'; + BlankZero = true; + BlankNumbers = BlankZero; + } + field("Created By Full Name"; Rec."Created By Full Name") + { + Caption = 'Created by'; + } + field(MessageType; Rec.Type) + { + Caption = 'Type'; + } + field(MessageText; GlobalMessageText) + { + Caption = 'Message'; + ToolTip = 'Specifies the message text.'; + + trigger OnDrillDown() + begin + Message(GlobalMessageText); + end; + } + field(TaskID; Rec."Task Id") + { + Visible = false; + Caption = 'Task ID'; + } + field(MessageId; Rec."ID") + { + Caption = 'ID'; + } + } + } + } + + trigger OnAfterGetRecord() + begin + UpdateControls(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateControls(); + end; + + local procedure UpdateControls() + var + AgentMessageImpl: Codeunit "Agent Message Impl."; + begin + GlobalMessageText := AgentMessageImpl.GetMessageText(Rec); + end; + + var + GlobalMessageText: Text; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Interaction/AgentTaskStepList.Page.al b/src/System Application/App/Agent/Interaction/AgentTaskStepList.Page.al new file mode 100644 index 0000000000..22c61dda7e --- /dev/null +++ b/src/System Application/App/Agent/Interaction/AgentTaskStepList.Page.al @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +page 4303 "Agent Task Step List" +{ + PageType = List; + ApplicationArea = All; + UsageCategory = Administration; + SourceTable = "Agent Task Step"; + Caption = 'Agent Task Steps'; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + Editable = false; + SourceTableView = sorting("Step Number") order(descending); + Extensible = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(AgentConversationActionLog) + { + field(StepNumber; Rec."Step Number") + { + Caption = 'Step Number'; + } + field(TaskID; Rec."Task ID") + { + Visible = false; + Caption = 'Task ID'; + } + field(Description; Rec.Description) + { + Caption = 'Description'; + } + field(Details; DetailsTxt) + { + Caption = 'Details'; + ToolTip = 'Specifies the step details.'; + + trigger OnDrillDown() + begin + Message(DetailsTxt); + end; + } + field("User Full Name"; Rec."User Full Name") + { + Caption = 'User Full Name'; + Tooltip = 'Specifies the full name of the user that was involved in performing the step..'; + } + } + } + } + + trigger OnAfterGetRecord() + begin + UpdateControls(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateControls(); + end; + + local procedure UpdateControls() + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + DetailsTxt := AgentTaskImpl.GetDetailsForAgentTaskStep(Rec); + end; + + var + DetailsTxt: Text; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Permissions/AgentObjects.PermissionSet.al b/src/System Application/App/Agent/Permissions/AgentObjects.PermissionSet.al new file mode 100644 index 0000000000..4d8e02fc41 --- /dev/null +++ b/src/System Application/App/Agent/Permissions/AgentObjects.PermissionSet.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +permissionset 4300 "Agent - Objects" +{ + Access = Internal; + Assignable = false; + + Permissions = codeunit "Agent Task Impl." = X, + page "Agent Access Control" = X, + page "Agent Card" = X, + page "Agent List" = X, + page "Agent Task List" = X, + page "Agent Task Message Card" = X, + page "Agent Task Message List" = X, + page "Agent Task Step List" = X, + codeunit "Agent Impl." = X, + codeunit "Agent Task" = X; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Setup/Agent.Codeunit.al b/src/System Application/App/Agent/Setup/Agent.Codeunit.al new file mode 100644 index 0000000000..edf43ef83e --- /dev/null +++ b/src/System Application/App/Agent/Setup/Agent.Codeunit.al @@ -0,0 +1,174 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +using System.Reflection; +using System.Security.AccessControl; + +codeunit 4321 Agent +{ + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Creates a new agent. + /// The agent will be in the disabled state, with the users that can interact with the agent setup. + /// + /// The metadata provider of the agent. + /// User name for the agent. + /// Display name for the agent. + /// Instructions for the agent that will be used to complete the tasks. + /// The list of users that can configure or interact with the agent. + /// The ID of the agent. +#pragma warning disable AS0026 + [Scope('OnPrem')] + procedure Create(AgentMetadataProvider: Enum "Agent Metadata Provider"; UserName: Code[50]; UserDisplayName: Text[80]; var TempAgentAccessControl: Record "Agent Access Control" temporary): Guid +#pragma warning restore AS0026 + var + AgentImpl: Codeunit "Agent Impl."; + begin + exit(AgentImpl.CreateAgent(AgentMetadataProvider, UserName, UserDisplayName, TempAgentAccessControl)); + end; + + /// + /// Activates the agent + /// + /// The user security ID of the agent. + [Scope('OnPrem')] + procedure Activate(AgentUserSecurityID: Guid) + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.Activate(AgentUserSecurityID); + end; + + /// + /// Deactivates the agent + /// + /// The user security ID of the agent. + [Scope('OnPrem')] + procedure Deactivate(AgentUserSecurityID: Guid) + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.Deactivate(AgentUserSecurityID); + end; + + /// + /// Get the display name of the agent. + /// + /// The user security ID of the agent. + [Scope('OnPrem')] + procedure GetDisplayName(AgentUserSecurityID: Guid): Text[80] + var + AgentImpl: Codeunit "Agent Impl."; + begin + exit(AgentImpl.GetDisplayName(AgentUserSecurityID)); + end; + + /// + /// Get the user name of the agent. + /// + /// The user security ID of the agent. + [Scope('OnPrem')] + procedure GetUserName(AgentUserSecurityID: Guid): Code[50] + var + AgentImpl: Codeunit "Agent Impl."; + begin + exit(AgentImpl.GetUserName(AgentUserSecurityID)); + end; + + /// + /// Sets the display name of the agent. + /// + /// The user security ID of the agent. + /// The display name of the agent. + [Scope('OnPrem')] + procedure SetDisplayName(AgentUserSecurityID: Guid; DisplayName: Text[80]) + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.SetDisplayName(AgentUserSecurityID, DisplayName); + end; + + /// + /// Set the instructions which agent will use to complete the tasks. + /// + /// The agent which instructions will be set. + /// Instructions for the agent that will be used to complete the tasks. + [Scope('OnPrem')] + procedure SetInstructions(AgentUserSecurityID: Guid; Instructions: SecretText) + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.SetInstructions(AgentUserSecurityID, Instructions); + end; + + /// + /// Checks if the agent is active. + /// + /// The user security ID of the agent. + /// If the agent is active. + [Scope('OnPrem')] + procedure IsActive(AgentUserSecurityID: Guid): Boolean + var + AgentImpl: Codeunit "Agent Impl."; + begin + exit(AgentImpl.IsActive(AgentUserSecurityID)); + end; + + /// + /// Assigns the permission set to the agent. + /// + /// The user security ID of the agent. + /// Profile to set to the agent. + [Scope('OnPrem')] + procedure SetProfile(AgentUserSecurityID: Guid; var AllProfile: Record "All Profile") + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.SetProfile(AgentUserSecurityID, AllProfile); + end; + + /// + /// Assigns the permission set to the agent. + /// + /// The user security ID of the agent. + /// Permission sets to assign + [Scope('OnPrem')] + procedure AssignPermissionSet(AgentUserSecurityID: Guid; var AggregatePermissionSet: Record "Aggregate Permission Set") + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.AssignPermissionSets(AgentUserSecurityID, CompanyName(), AggregatePermissionSet); + end; + + /// + /// Gets the users that can manage or give tasks to the agent. + /// + /// Security ID of the agent. + /// List of users that can manage or give tasks to the agent. + [Scope('OnPrem')] + procedure GetUserAccess(AgentUserSecurityID: Guid; var TempAgentAccessControl: Record "Agent Access Control" temporary) + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.GetUserAccess(AgentUserSecurityID, TempAgentAccessControl); + end; + + /// + /// Sets the users that can manage or give tasks to the agent. Existing set of users will be replaced with a new set. + /// + /// Security ID of the agent. + /// List of users that can manage or give tasks to the agent. + [Scope('OnPrem')] + procedure UpdateAccess(AgentUserSecurityID: Guid; var TempAgentAccessControl: Record "Agent Access Control" temporary) + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.UpdateAgentAccessControl(AgentUserSecurityID, TempAgentAccessControl); + end; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Setup/AgentAccessControl.Page.al b/src/System Application/App/Agent/Setup/AgentAccessControl.Page.al new file mode 100644 index 0000000000..f9cc77b28a --- /dev/null +++ b/src/System Application/App/Agent/Setup/AgentAccessControl.Page.al @@ -0,0 +1,134 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +using System.Security.AccessControl; + +page 4320 "Agent Access Control" +{ + PageType = ListPart; + ApplicationArea = All; + SourceTable = "Agent Access Control"; + Caption = 'Agent Access Control'; + MultipleNewLines = false; + Extensible = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(Main) + { + field(UserName; UserName) + { + Caption = 'User Name'; + ToolTip = 'Specifies the name of the User that can access the agent.'; + TableRelation = User; + + trigger OnValidate() + begin + ValidateUserName(UserName); + end; + } + field(UserFullName; UserFullName) + { + Caption = 'User Full Name'; + ToolTip = 'Specifies the Full Name of the User that can access the agent.'; + Editable = false; + } + field(CanConfigureAgent; Rec."Can Configure Agent") + { + Caption = 'Can Configure'; + Tooltip = 'Specifies whether the user can configure the agent.'; + + trigger OnValidate() + var + AgentImpl: Codeunit "Agent Impl."; + begin + if not Rec."Can Configure Agent" then + AgentImpl.VerifyOwnerExists(Rec); + end; + } + } + } + } + + trigger OnAfterGetRecord() + begin + UpdateGlobalVariables(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateGlobalVariables(); + end; + + trigger OnDeleteRecord(): Boolean + var + AgentImpl: Codeunit "Agent Impl."; + begin + AgentImpl.VerifyOwnerExists(Rec); + end; + + local procedure ValidateUserName(NewUserName: Text) + var + User: Record "User"; + UserGuid: Guid; + begin + if Evaluate(UserGuid, NewUserName) then begin + User.Get(UserGuid); + UpdateUser(User."User Security ID"); + UpdateGlobalVariables(); + exit; + end; + + User.SetRange("User Name", NewUserName); + if not User.FindFirst() then begin + User.SetFilter("User Name", '@*''''' + NewUserName + '''''*'); + User.FindFirst(); + end; + + UpdateUser(User."User Security ID"); + UpdateGlobalVariables(); + end; + + local procedure UpdateUser(NewUserID: Guid) + var + RecordExists: Boolean; + begin + RecordExists := Rec.Find(); + + if RecordExists then + Error(CannotUpdateUserErr); + + Rec."User Security ID" := NewUserID; + Rec.Insert(true); + end; + + local procedure UpdateGlobalVariables() + var + User: Record "User"; + begin + Clear(UserFullName); + Clear(UserName); + + if IsNullGuid(Rec."User Security ID") then + exit; + + if not User.Get(Rec."User Security ID") then + exit; + + UserName := User."User Name"; + UserFullName := User."Full Name"; + end; + + var + UserFullName: Text[80]; + UserName: Code[50]; + CannotUpdateUserErr: Label 'You cannot change the User. Delete and create the entry again.'; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Setup/AgentCard.Page.al b/src/System Application/App/Agent/Setup/AgentCard.Page.al new file mode 100644 index 0000000000..1ec4a99eb6 --- /dev/null +++ b/src/System Application/App/Agent/Setup/AgentCard.Page.al @@ -0,0 +1,228 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +using System.Security.User; +using System.Environment.Configuration; + +page 4315 "Agent Card" +{ + PageType = Card; + ApplicationArea = All; + SourceTable = Agent; + Caption = 'Agent Card'; + RefreshOnActivate = true; + DataCaptionExpression = Rec."User Name"; + InsertAllowed = false; + DeleteAllowed = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + group(General) + { + Caption = 'General'; + field("Agent Metadata Provider"; Rec."Agent Metadata Provider") + { + ShowMandatory = true; + ApplicationArea = Basic, Suite; + Caption = 'Type'; + Tooltip = 'Specifies the type of the agent.'; + Editable = false; + } + field(UserName; Rec."User Name") + { + ShowMandatory = true; + ApplicationArea = Basic, Suite; + Caption = 'User Name'; + Tooltip = 'Specifies the name of the user that is associated with the agent.'; + Editable = false; + } + + field(DisplayName; Rec."Display Name") + { + ShowMandatory = true; + ApplicationArea = Basic, Suite; + Caption = 'Display Name'; + Tooltip = 'Specifies the display name of the user that is associated with the agent.'; + Editable = false; + } + group(UserSettingsGroup) + { + ShowCaption = false; + field(AgentProfile; ProfileDisplayName) + { + ApplicationArea = Basic, Suite; + Caption = 'Profile'; + ToolTip = 'Specifies the profile that is associated with the agent.'; + Editable = false; + + trigger OnAssistEdit() + var + AgentImpl: Codeunit "Agent Impl."; + begin + if AgentImpl.ProfileLookup(UserSettingsRecord) then + AgentImpl.UpdateAgentUserSettings(UserSettingsRecord); + end; + } + } + field(State; Rec.State) + { + ApplicationArea = Basic, Suite; + Importance = Standard; + Caption = 'State'; + ToolTip = 'Specifies if the agent is enabled or disabled.'; + + trigger OnValidate() + begin + ChangeState(); + UpdateControls(); + end; + } + } + + part(Permissions; "User Subform") + { + Editable = ControlsEditable; + ApplicationArea = Basic, Suite; + Caption = 'Agent Permission Sets'; + SubPageLink = "User Security ID" = field("User Security ID"); + } + part(UserAccess; "Agent Access Control") + { + Editable = ControlsEditable; + ApplicationArea = Basic, Suite; + Caption = 'User Access'; + SubPageLink = "Agent User Security ID" = field("User Security ID"); + } + } + } + actions + { + area(Navigation) + { + action(AgentSetup) + { + ApplicationArea = Basic, Suite; + Caption = 'Setup'; + ToolTip = 'Set up agent'; + Image = SetupLines; + + trigger OnAction() + begin + OpenSetupPage(); + end; + } + action(UserSettingsAction) + { + ApplicationArea = Basic, Suite; + Caption = 'User Settings'; + ToolTip = 'Set up the profile and regional settings for the agent.'; + Image = SetupLines; + + trigger OnAction() + var + UserSettings: Codeunit "User Settings"; + begin + Rec.TestField("User Security ID"); + UserSettings.GetUserSettings(Rec."User Security ID", UserSettingsRecord); + Page.RunModal(Page::"User Settings", UserSettingsRecord); + end; + } + action(AgentTasks) + { + ApplicationArea = All; + Caption = 'Agent Tasks'; + ToolTip = 'View agent tasks'; + Image = Log; + + trigger OnAction() + var + AgentTask: Record "Agent Task"; + begin + AgentTask.SetRange("Agent User Security ID", Rec."User Security ID"); + Page.Run(Page::"Agent Task List", AgentTask); + end; + } + } + area(Promoted) + { + group(Category_Process) + { + actionref(AgentSetup_Promoted; AgentSetup) + { + } + actionref(UserSettings_Promoted; UserSettingsAction) + { + } + actionref(AgentTasks_Promoted; AgentTasks) + { + } + } + } + } + + local procedure UpdateControls() + var + AgentImpl: Codeunit "Agent Impl."; + UserSettings: Codeunit "User Settings"; + begin + if not IsNullGuid(Rec."User Security ID") then begin + UserSettings.GetUserSettings(Rec."User Security ID", UserSettingsRecord); + ProfileDisplayName := AgentImpl.GetProfileName(UserSettingsRecord.Scope, UserSettingsRecord."App ID", UserSettingsRecord."Profile ID"); + end; + + ControlsEditable := Rec.State = Rec.State::Disabled; + end; + + local procedure ChangeState() + var + ConfirmOpenSetupPage: Boolean; + begin + if Rec."Setup Page ID" = 0 then + exit; + + if Rec.State = Rec.State::Disabled then + exit; + + ConfirmOpenSetupPage := false; + + if GuiAllowed() then + ConfirmOpenSetupPage := Confirm(OpenConfigurationPageQst); + + if not ConfirmOpenSetupPage then + Error(YouCannotEnableAgentWithoutUsingConfigurationPageErr); + + Rec.Find(); + OpenSetupPage(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateControls(); + end; + + local procedure OpenSetupPage() + var + TempAgent: Record Agent temporary; + begin + TempAgent.Copy(Rec); + TempAgent.Insert(); + Page.RunModal(Rec."Setup Page ID", TempAgent); + CurrPage.Update(false); + end; + + + var + UserSettingsRecord: Record "User Settings"; + ProfileDisplayName: Text; + ControlsEditable: Boolean; + OpenConfigurationPageQst: Label 'To activate the agent, use the setup page. Would you like to open this page now?'; + YouCannotEnableAgentWithoutUsingConfigurationPageErr: Label 'You can''t activate the agent from this page. Use the action to set up and activate the agent.'; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al b/src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al new file mode 100644 index 0000000000..8682edfa92 --- /dev/null +++ b/src/System Application/App/Agent/Setup/AgentImpl.Codeunit.al @@ -0,0 +1,429 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +using System.Environment.Configuration; +using System.Reflection; +using System.Environment; +using System.Security.AccessControl; + +codeunit 4301 "Agent Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata Agent = rim, + tabledata "All Profile" = r, + tabledata Company = r, + tabledata "Agent Access Control" = d, + tabledata "Application User Settings" = rim, + tabledata User = r, + tabledata "User Personalization" = rim; + + internal procedure CreateAgent(AgentMetadataProvider: Enum "Agent Metadata Provider"; AgentUserName: Code[50]; AgentUserDisplayName: Text[80]; var TempAgentAccessControl: Record "Agent Access Control" temporary): Guid + var + Agent: Record Agent; + begin + Agent."Agent Metadata Provider" := AgentMetadataProvider; + Agent."User Name" := AgentUserName; + Agent."Display Name" := AgentUserDisplayName; + Agent.Insert(true); + + if TempAgentAccessControl.IsEmpty() then + GetUserAccess(Agent, TempAgentAccessControl, true); + + AssignCompany(Agent."User Security ID", CompanyName()); + UpdateAgentAccessControl(TempAgentAccessControl, Agent); + + exit(Agent."User Security ID"); + end; + + internal procedure Activate(AgentUserSecurityID: Guid) + begin + ChangeAgentState(AgentUserSecurityID, true); + end; + + internal procedure Deactivate(AgentUserSecurityID: Guid) + begin + ChangeAgentState(AgentUserSecurityID, false); + end; + + [NonDebuggable] + internal procedure SetInstructions(AgentUserSecurityID: Guid; Instructions: SecretText) + var + Agent: Record Agent; + InstructionsOutStream: OutStream; + begin + Agent.Get(AgentUserSecurityID); + Clear(Agent.Instructions); + Agent.Instructions.CreateOutStream(InstructionsOutStream, GetDefaultEncoding()); + InstructionsOutStream.Write(Instructions.Unwrap()); + Agent.Modify(true); + end; + + internal procedure GetInstructions(var Agent: Record Agent): Text + var + InstructionsInStream: InStream; + InstructionsText: Text; + begin + if IsNullGuid(Agent."User Security ID") then + exit; + + Agent.CalcFields(Instructions); + if not Agent.Instructions.HasValue() then + exit(''); + + Agent.Instructions.CreateInStream(InstructionsInStream, GetDefaultEncoding()); + InstructionsInStream.Read(InstructionsText); + exit(InstructionsText); + end; + + internal procedure InsertCurrentOwnerIfNoOwnersDefined(var Agent: Record Agent; var AgentAccessControl: Record "Agent Access Control") + begin + SetOwnerFilters(AgentAccessControl); + AgentAccessControl.SetRange("Agent User Security ID", Agent."User Security ID"); + if not AgentAccessControl.IsEmpty() then + exit; + InsertCurrentOwner(Agent."User Security ID", AgentAccessControl); + end; + + internal procedure InsertCurrentOwner(AgentUserSecurityID: Guid; var AgentAccessControl: Record "Agent Access Control") + begin + AgentAccessControl."Can Configure Agent" := true; + AgentAccessControl."Agent User Security ID" := AgentUserSecurityID; + AgentAccessControl."User Security ID" := UserSecurityId(); + AgentAccessControl.Insert(); + end; + + internal procedure VerifyOwnerExists(AgentAccessControlModified: Record "Agent Access Control") + var + ExistingAgentAccessControl: Record "Agent Access Control"; + begin + if (AgentAccessControlModified."Can Configure Agent") then + exit; + + SetOwnerFilters(ExistingAgentAccessControl); + ExistingAgentAccessControl.SetFilter("User Security ID", '<>%1', AgentAccessControlModified."User Security ID"); + ExistingAgentAccessControl.SetRange("Agent User Security ID", AgentAccessControlModified."Agent User Security ID"); + + if ExistingAgentAccessControl.IsEmpty() then + Error(OneOwnerMustBeDefinedForAgentErr); + end; + + internal procedure GetUserAccess(AgentUserSecurityID: Guid; var TempAgentAccessControl: Record "Agent Access Control" temporary) + var + Agent: Record Agent; + begin + GetAgent(Agent, AgentUserSecurityID); + + GetUserAccess(Agent, TempAgentAccessControl, false); + end; + + local procedure GetUserAccess(var Agent: Record Agent; var TempAgentAccessControl: Record "Agent Access Control" temporary; InsertCurrentUserAsOwner: Boolean) + var + AgentAccessControl: Record "Agent Access Control"; + begin + TempAgentAccessControl.Reset(); + TempAgentAccessControl.DeleteAll(); + + AgentAccessControl.SetRange("Agent User Security ID", Agent."User Security ID"); + if AgentAccessControl.IsEmpty() then begin + if not InsertCurrentUserAsOwner then + exit; + + InsertCurrentOwnerIfNoOwnersDefined(Agent, TempAgentAccessControl); + exit; + end; + + AgentAccessControl.FindSet(); + repeat + TempAgentAccessControl.Copy(AgentAccessControl); + TempAgentAccessControl.Insert(); + until AgentAccessControl.Next() = 0; + end; + + internal procedure SetProfile(AgentUserSecurityID: Guid; var AllProfile: Record "All Profile") + var + Agent: Record Agent; + UserSettingsRecord: Record "User Settings"; + UserSettings: Codeunit "User Settings"; + begin + GetAgent(Agent, AgentUserSecurityID); + + UserSettings.GetUserSettings(Agent."User Security ID", UserSettingsRecord); + UpdateProfile(AllProfile, UserSettingsRecord); + UpdateAgentUserSettings(UserSettingsRecord); + end; + + internal procedure AssignCompany(AgentUserSecurityID: Guid; CompanyName: Text) + var + Agent: Record Agent; + UserSettingsRecord: Record "User Settings"; + UserSettings: Codeunit "User Settings"; + begin + GetAgent(Agent, AgentUserSecurityID); + + UserSettings.GetUserSettings(Agent."User Security ID", UserSettingsRecord); +#pragma warning disable AA0139 + UserSettingsRecord.Company := CompanyName(); +#pragma warning restore AA0139 + UpdateAgentUserSettings(UserSettingsRecord); + end; + + internal procedure GetUserName(AgentUserSecurityID: Guid): Code[50] + var + Agent: Record Agent; + begin + GetAgent(Agent, AgentUserSecurityID); + + exit(Agent."User Name"); + end; + + internal procedure GetDisplayName(AgentUserSecurityID: Guid): Text[80] + var + Agent: Record Agent; + begin + GetAgent(Agent, AgentUserSecurityID); + + exit(Agent."Display Name") + end; + + internal procedure SetDisplayName(AgentUserSecurityID: Guid; DisplayName: Text[80]) + var + Agent: Record Agent; + begin + GetAgent(Agent, AgentUserSecurityID); + + Agent."Display Name" := DisplayName; + Agent.Modify(true); + end; + + internal procedure IsActive(AgentUserSecurityID: Guid): Boolean + var + Agent: Record Agent; + begin + GetAgent(Agent, AgentUserSecurityID); + + exit(Agent.State = Agent.State::Enabled); + end; + + internal procedure UpdateAgentAccessControl(AgentUserSecurityID: Guid; var TempAgentAccessControl: Record "Agent Access Control" temporary) + var + Agent: Record Agent; + begin + if not Agent.Get(AgentUserSecurityID) then + Error(AgentDoesNotExistErr); + + UpdateAgentAccessControl(TempAgentAccessControl, Agent); + end; + + # Region TODO: Update System App signatures to use the codeunit 9175 "User Settings Impl." + internal procedure UpdateAgentUserSettings(NewUserSettings: Record "User Settings") + var + UserPersonalization: Record "User Personalization"; + begin + UserPersonalization.Get(NewUserSettings."User Security ID"); + + UserPersonalization."Language ID" := NewUserSettings."Language ID"; + UserPersonalization."Locale ID" := NewUserSettings."Locale ID"; + UserPersonalization.Company := NewUserSettings.Company; + UserPersonalization."Time Zone" := NewUserSettings."Time Zone"; + UserPersonalization."Profile ID" := NewUserSettings."Profile ID"; +#pragma warning disable AL0432 // All profiles are now in the tenant scope + UserPersonalization.Scope := NewUserSettings.Scope; +#pragma warning restore AL0432 + UserPersonalization."App ID" := NewUserSettings."App ID"; + UserPersonalization.Modify(); + end; + + procedure ProfileLookup(var UserSettingsRec: Record "User Settings"): Boolean + var + TempAllProfile: Record "All Profile" temporary; + begin + PopulateProfiles(TempAllProfile); + + if TempAllProfile.Get(UserSettingsRec.Scope, UserSettingsRec."App ID", UserSettingsRec."Profile ID") then; + if Page.RunModal(Page::Roles, TempAllProfile) = Action::LookupOK then begin + UpdateProfile(TempAllProfile, UserSettingsRec); + exit(true); + end; + exit(false); + end; + + internal procedure UpdateProfile(var TempAllProfile: Record "All Profile" temporary; var UserSettingsRec: Record "User Settings") + begin + UserSettingsRec."Profile ID" := TempAllProfile."Profile ID"; + UserSettingsRec."App ID" := TempAllProfile."App ID"; + UserSettingsRec.Scope := TempAllProfile.Scope; + end; + + procedure PopulateProfiles(var TempAllProfile: Record "All Profile" temporary) + var + AllProfile: Record "All Profile"; + DescriptionFilterTxt: Label 'Navigation menu only.'; + UserCreatedAppNameTxt: Label '(User-created)'; + begin + TempAllProfile.Reset(); + TempAllProfile.DeleteAll(); + AllProfile.SetRange(Enabled, true); + AllProfile.SetFilter(Description, '<> %1', DescriptionFilterTxt); + if AllProfile.FindSet() then + repeat + TempAllProfile := AllProfile; + if IsNullGuid(TempAllProfile."App ID") then + TempAllProfile."App Name" := UserCreatedAppNameTxt; + TempAllProfile.Insert(); + until AllProfile.Next() = 0; + end; + + procedure GetProfileName(Scope: Option System,Tenant; AppID: Guid; ProfileID: Code[30]) ProfileName: Text + var + AllProfile: Record "All Profile"; + begin + // If current profile has been changed, then find it and update the description; else, get the default + if not AllProfile.Get(Scope, AppID, ProfileID) then + exit; + + ProfileName := AllProfile.Caption; + end; + + internal procedure AssignPermissionSets(var UserSID: Guid; PermissionCompanyName: Text; var AggregatePermissionSet: Record "Aggregate Permission Set") + var + AccessControl: Record "Access Control"; + begin + if not AggregatePermissionSet.FindSet() then + exit; + + repeat + AccessControl."App ID" := AggregatePermissionSet."App ID"; + AccessControl."User Security ID" := UserSID; + AccessControl."Role ID" := AggregatePermissionSet."Role ID"; + AccessControl.Scope := AggregatePermissionSet.Scope; +#pragma warning disable AA0139 + AccessControl."Company Name" := PermissionCompanyName; +#pragma warning restore AA0139 + AccessControl.Insert(); + until AggregatePermissionSet.Next() = 0; + end; + #endregion + + local procedure GetAgent(var Agent: Record Agent; UserSecurityID: Guid) + begin + Agent.SetAutoCalcFields(Instructions); + if not Agent.Get(UserSecurityID) then + Error(AgentDoesNotExistErr); + end; + + local procedure ChangeAgentState(UserSecurityID: Guid; Enabled: Boolean) + var + Agent: Record Agent; + + begin + GetAgent(Agent, UserSecurityId); + + if Enabled then + Agent.State := Agent.State::Enabled + else + Agent.State := Agent.State::Disabled; + + Agent.Modify(); + end; + + local procedure UpdateAgentAccessControl(var TempAgentAccessControl: Record "Agent Access Control" temporary; var Agent: Record Agent) + begin + // We must delete or update the user doing the change the last to avoid removing permissions that are needed to commit the change + UpdateUsersOtherThanMainUser(TempAgentAccessControl, Agent); + + // Update the user at the end + UpdateUserDoingTheChange(TempAgentAccessControl, Agent); + end; + + local procedure UpdateUsersOtherThanMainUser(var TempAgentAccessControl: Record "Agent Access Control" temporary; var Agent: Record Agent) + var + AgentAccessControl: Record "Agent Access Control"; + begin + AgentAccessControl.SetRange("Agent User Security ID", Agent."User Security ID"); + AgentAccessControl.SetFilter("User Security ID", '<>%1', UserSecurityId()); + if AgentAccessControl.FindSet() then + repeat + if not TempAgentAccessControl.Get(AgentAccessControl."Agent User Security ID", AgentAccessControl."User Security ID") then + AgentAccessControl.Delete(true); + until AgentAccessControl.Next() = 0; + + AgentAccessControl.Reset(); + TempAgentAccessControl.Reset(); + TempAgentAccessControl.SetFilter("User Security ID", '<>%1', UserSecurityId()); + if not TempAgentAccessControl.FindSet() then + exit; + + repeat + if AgentAccessControl.Get(Agent."User Security ID", TempAgentAccessControl."User Security ID") then begin + AgentAccessControl.TransferFields(TempAgentAccessControl, true); + AgentAccessControl."Agent User Security ID" := Agent."User Security ID"; + AgentAccessControl.Modify(); + end else begin + AgentAccessControl.TransferFields(TempAgentAccessControl, true); + AgentAccessControl."Agent User Security ID" := Agent."User Security ID"; + AgentAccessControl.Insert(); + end; + until TempAgentAccessControl.Next() = 0; + end; + + local procedure UpdateUserDoingTheChange(var TempAgentAccessControl: Record "Agent Access Control" temporary; var Agent: Record Agent) + var + AgentAccessControl: Record "Agent Access Control"; + begin + TempAgentAccessControl.SetFilter("User Security ID", UserSecurityId()); + if not TempAgentAccessControl.FindFirst() then begin + if AgentAccessControl.Get(Agent."User Security ID", UserSecurityId()) then + AgentAccessControl.Delete(); + + exit; + end; + + if AgentAccessControl.Get(Agent."User Security ID", UserSecurityId()) then begin + AgentAccessControl.TransferFields(TempAgentAccessControl, true); + AgentAccessControl."Agent User Security ID" := Agent."User Security ID"; + AgentAccessControl.Modify(); + exit; + end else begin + AgentAccessControl.TransferFields(TempAgentAccessControl, true); + AgentAccessControl."Agent User Security ID" := Agent."User Security ID"; + AgentAccessControl.Insert(); + exit; + end; + end; + + procedure SelectAgent(var Agent: Record "Agent") + begin + Agent.SetRange(State, Agent.State::Enabled); + if Agent.Count() = 0 then + Error(NoActiveAgentsErr); + + if Agent.Count() = 1 then begin + Agent.FindFirst(); + exit; + end; + + if not (Page.RunModal(Page::"Agent List", Agent) in [Action::LookupOK, Action::OK]) then + Error(''); + end; + + local procedure SetOwnerFilters(var AgentAccessControl: Record "Agent Access Control") + begin + AgentAccessControl.SetFilter("Can Configure Agent", '%1', true); + end; + + local procedure GetDefaultEncoding(): TextEncoding + begin + exit(TextEncoding::UTF8); + end; + + var + OneOwnerMustBeDefinedForAgentErr: Label 'One owner must be defined for the agent.'; + AgentDoesNotExistErr: Label 'Agent does not exist.'; + NoActiveAgentsErr: Label 'There are no active agents setup on the system.'; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Setup/AgentList.Page.al b/src/System Application/App/Agent/Setup/AgentList.Page.al new file mode 100644 index 0000000000..62bb45d7f6 --- /dev/null +++ b/src/System Application/App/Agent/Setup/AgentList.Page.al @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +page 4316 "Agent List" +{ + PageType = List; + ApplicationArea = All; + UsageCategory = Administration; + SourceTable = "Agent"; + Caption = 'Agents'; + CardPageId = "Agent Card"; + AdditionalSearchTerms = 'Agent, Agents, Copilot, Automation, AI'; + Editable = false; + InsertAllowed = false; + DeleteAllowed = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(Main) + { + field(UserName; Rec."User Name") + { + Caption = 'User Name'; + } + field(DisplayName; Rec."Display Name") + { + Caption = 'Display Name'; + } + field(State; Rec.State) + { + Caption = 'State'; + } + } + } + } + actions + { + area(Processing) + { + action(AgentSetup) + { + ApplicationArea = Basic, Suite; + Caption = 'Setup'; + ToolTip = 'Set up the agent'; + Image = SetupLines; + + trigger OnAction() + var + TempAgent: Record Agent temporary; + begin + TempAgent.Copy(Rec); + TempAgent.Insert(); + Page.RunModal(Rec."Setup Page ID", TempAgent); + end; + } + action(AgentTasks) + { + ApplicationArea = All; + Caption = 'Agent Tasks'; + ToolTip = 'View agent tasks'; + Image = Log; + + trigger OnAction() + var + AgentTask: Record "Agent Task"; + begin + AgentTask.SetRange("Agent User Security ID", Rec."User Security ID"); + Page.Run(Page::"Agent Task List", AgentTask); + end; + } + } + area(Promoted) + { + group(Category_Process) + { + actionref(AgentSetup_Promoted; AgentSetup) + { + } + actionref(AgentTasks_Promoted; AgentTasks) + { + } + } + } + } +} \ No newline at end of file diff --git a/src/System Application/App/Agent/Setup/SelectAgentAccessControl.Page.al b/src/System Application/App/Agent/Setup/SelectAgentAccessControl.Page.al new file mode 100644 index 0000000000..5e2b143ecf --- /dev/null +++ b/src/System Application/App/Agent/Setup/SelectAgentAccessControl.Page.al @@ -0,0 +1,179 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +using System.Security.AccessControl; + +page 4321 "Select Agent Access Control" +{ + PageType = StandardDialog; + ApplicationArea = All; + SourceTable = "Agent Access Control"; + SourceTableTemporary = true; + Caption = 'Select users to manage tasks and configure the agent'; + MultipleNewLines = false; + Extensible = false; + DataCaptionExpression = ''; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(Main) + { + field(UserName; UserName) + { + Caption = 'User Name'; + ToolTip = 'Specifies the name of the User that can access the agent.'; + TableRelation = User; + + trigger OnValidate() + begin + ValidateUserName(UserName); + CurrPage.Update(true); + end; + } + field(UserFullName; UserFullName) + { + Caption = 'User Full Name'; + ToolTip = 'Specifies the Full Name of the User that can access the agent.'; + Editable = false; + } + field(CanConfigureAgent; Rec."Can Configure Agent") + { + Caption = 'Can configure'; + Tooltip = 'Specifies whether the user can configure the agent.'; + + trigger OnValidate() + begin + if not Rec."Can Configure Agent" then + VerifyOwnerExists(); + end; + } + } + } + } + + trigger OnAfterGetRecord() + begin + UpdateGlobalVariables(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateGlobalVariables(); + end; + + trigger OnDeleteRecord(): Boolean + begin + VerifyOwnerExists(); + end; + + trigger OnOpenPage() + var + AgentImpl: Codeunit "Agent Impl."; + begin + if Rec.GetFilter("Agent User Security ID") <> '' then + Evaluate(AgentUserSecurityID, Rec.GetFilter("Agent User Security ID")); + + if Rec.Count() = 0 then + AgentImpl.InsertCurrentOwner(Rec."Agent User Security ID", Rec); + end; + + local procedure ValidateUserName(NewUserName: Text) + var + User: Record "User"; + UserGuid: Guid; + begin + if Evaluate(UserGuid, NewUserName) then begin + User.Get(UserGuid); + UpdateUser(User."User Security ID"); + UpdateGlobalVariables(); + exit; + end; + + User.SetRange("User Name", NewUserName); + if not User.FindFirst() then begin + User.SetFilter("User Name", '@*''''' + NewUserName + '''''*'); + User.FindFirst(); + end; + + UpdateUser(User."User Security ID"); + UpdateGlobalVariables(); + end; + + local procedure UpdateUser(NewUserID: Guid) + var + TempAgentAccessControl: Record "Agent Access Control" temporary; + RecordExists: Boolean; + begin + RecordExists := Rec.Find(); + + if RecordExists then begin + TempAgentAccessControl.Copy(Rec); + Rec.Delete(); + Rec.Copy(TempAgentAccessControl); + end; + + Rec."User Security ID" := NewUserID; + Rec."Agent User Security ID" := AgentUserSecurityID; + Rec.Insert(true); + VerifyOwnerExists(); + end; + + local procedure UpdateGlobalVariables() + var + User: Record "User"; + begin + Clear(UserFullName); + Clear(UserName); + + if IsNullGuid(Rec."User Security ID") then + exit; + + if not User.Get(Rec."User Security ID") then + exit; + + UserName := User."User Name"; + UserFullName := User."Full Name"; + end; + + [Scope('OnPrem')] + procedure GetAgentUserAccess(var TempAgentAccessControl: Record "Agent Access Control" temporary) + begin + TempAgentAccessControl.Reset(); + TempAgentAccessControl.DeleteAll(); + + if Rec.FindSet() then + repeat + TempAgentAccessControl.Copy(Rec); + TempAgentAccessControl.Insert(); + until Rec.Next() = 0; + end; + + local procedure VerifyOwnerExists() + var + TempAgentAccessControl: Record "Agent Access Control" temporary; + begin + TempAgentAccessControl.Copy(Rec); + Rec.SetFilter("Can Configure Agent", '%1', true); + Rec.SetFilter("User Security ID", '<>%1', Rec."User Security ID"); + if Rec.IsEmpty() then begin + Rec.Copy(TempAgentAccessControl); + Error(OneOwnerMustBeDefinedForAgentErr); + end; + + Rec.Copy(TempAgentAccessControl); + end; + + var + UserFullName: Text[80]; + UserName: Code[50]; + AgentUserSecurityID: Guid; + OneOwnerMustBeDefinedForAgentErr: Label 'One owner must be defined for the agent.'; +} \ No newline at end of file diff --git a/src/System Application/App/Agent/TaskPane/TaskDetails.Page.al b/src/System Application/App/Agent/TaskPane/TaskDetails.Page.al new file mode 100644 index 0000000000..de3f567f62 --- /dev/null +++ b/src/System Application/App/Agent/TaskPane/TaskDetails.Page.al @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +page 4313 "Task Details" +{ + PageType = ListPart; + ApplicationArea = All; + SourceTable = "Agent Task Timeline Entry Step"; + Caption = 'Agent Task Timeline Entry Step'; + Editable = false; + InsertAllowed = false; + ModifyAllowed = false; + DeleteAllowed = false; + Extensible = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(content) + { + repeater(Steps) + { + field(ClientContext; ClientContext) + { + Caption = 'Client Context'; + ToolTip = 'Specifies the client context.'; + } + } + } + } + + actions + { + area(Processing) + { + +#pragma warning disable AW0005 + action(Confirm) +#pragma warning restore AW0005 + { + Caption = 'Confirm'; + ToolTip = 'Confirms the timeline entry.'; + + trigger OnAction() + begin + AddUserInterventionTaskStep(); + end; + } +#pragma warning disable AW0005 + action(DiscardStep) +#pragma warning restore AW0005 + { + Caption = 'Discard step'; + ToolTip = 'Discard the timeline entry.'; + trigger OnAction() + begin + SkipStep(); + end; + } + } + } + + trigger OnAfterGetRecord() + begin + SetClientContext(); + end; + + local procedure SetClientContext() + var + InStream: InStream; + begin + // Clear old value + Clear(ClientContext); + + if Rec.CalcFields("Client Context") then + if Rec."Client Context".HasValue() then begin + Rec."Client Context".CreateInStream(InStream); + ClientContext.Read(InStream); + end; + end; + + local procedure AddUserInterventionTaskStep() + var + UserInterventionRequestStep: Record "Agent Task Step"; + TaskTimelineEntry: Record "Agent Task Timeline Entry"; + UserInput: Text; + begin + TaskTimelineEntry.SetRange("Task ID", Rec."Task ID"); + TaskTimelineEntry.SetRange(ID, Rec."Timeline Entry ID"); + TaskTimelineEntry.SetRange("Last Step Type", TaskTimelineEntry."Last Step Type"::"User Intervention Request"); + if TaskTimelineEntry.FindLast() then begin + case TaskTimelineEntry."User Intervention Request Type" of + TaskTimelineEntry."User Intervention Request Type"::ReviewMessage: + UserInput := ''; + else + UserInput := UserMessage; //ToDo: Will be implemented when we have a message field. + end; + if UserInterventionRequestStep.Get(TaskTimelineEntry."Task ID", TaskTimelineEntry."Last Step Number") then + AgentTaskImpl.CreateUserInterventionTaskStep(UserInterventionRequestStep, UserInput); + end; + end; + + local procedure SkipStep() + var + TaskTimelineEntry: Record "Agent Task Timeline Entry"; + AgentTaskMessage: Record "Agent Task Message"; + begin + + if not TaskTimelineEntry.Get(Rec."Task ID", Rec."Timeline Entry ID") then + exit; + + case TaskTimelineEntry.Type of + TaskTimelineEntry.Type::OutputMessage: + if AgentTaskMessage.Get(TaskTimelineEntry."Primary Page Record ID") then begin + AgentTaskMessage.Status := AgentTaskMessage.Status::Discarded; + AgentTaskMessage.Modify(true); + end; + end; + end; + + var + AgentTaskImpl: Codeunit "Agent Task Impl."; + ClientContext: BigText; + UserMessage: Text; +} + + diff --git a/src/System Application/App/Agent/TaskPane/TaskTimeline.Page.al b/src/System Application/App/Agent/TaskPane/TaskTimeline.Page.al new file mode 100644 index 0000000000..ad7eb7fb1e --- /dev/null +++ b/src/System Application/App/Agent/TaskPane/TaskTimeline.Page.al @@ -0,0 +1,270 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +using System.Security.AccessControl; + +page 4307 "Task Timeline" +{ + PageType = ListPart; + ApplicationArea = All; + SourceTable = "Agent Task Timeline Entry"; + Caption = 'Agent Task Timeline'; + InsertAllowed = false; + DeleteAllowed = false; + Extensible = false; + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(content) + { + field(SelectedSuggestionId; SelectedSuggestionId) + { + Editable = true; + Caption = 'Selected Suggestion ID'; + ToolTip = 'Specifies the selected suggestion ID for the user intervention request.'; + } + repeater(TaskTimeline) + { + field(Header; Rec.Title) + { + Caption = 'Header'; + ToolTip = 'Specifies the header of the timeline entry.'; + } + field(Summary; GlobalPageSummary) + { + Caption = 'Summary'; + ToolTip = 'Specifies the summary of the timeline entry.'; + } + field(PrimaryPageQuery; GlobalPageQuery) + { + Caption = 'Primary Page Query'; + ToolTip = 'Specifies the primary page query of the timeline entry.'; + } + field(Description; GlobalDescription) + { + Caption = 'Description'; + ToolTip = 'Specifies the description of the timeline entry.'; + } + field(Category; Rec.Category) + { + } + field(Type; Rec.Type) + { + } + field(ConfirmationStatus; ConfirmationStatusOption) + { + Caption = 'Confirmation Status'; + ToolTip = 'Specifies the confirmation status of the timeline entry.'; + OptionCaption = ' ,ConfirmationNotRequired,ReviewConfirmationRequired,ReviewConfirmed,StopConfirmationRequired,StopConfirmed,Discarded'; + } + field(ConfirmedBy; GlobalConfirmedBy) + { + Caption = 'Confirmed By'; + ToolTip = 'Specifies the user who confirmed the timeline entry.'; + } + field(ConfirmedAt; GlobalConfirmedAt) + { + Caption = 'Confirmed At'; + ToolTip = 'Specifies the date and time when the timeline entry was confirmed.'; + } + field(Annotations; GlobalAnnotations) + { + Caption = 'Annotations'; + Tooltip = 'Specifies the annotations for the timeline entry, such as additional messages to surface to the user.'; + } + field(Importance; Rec.Importance) + { + } + field(UserInterventionRequestType; Rec."User Intervention Request Type") + { + Caption = 'User Intervention Request Type'; + ToolTip = 'Specifies the type of user intervention request when this entry is an intervention request.'; + } + field(Suggestions; GlobalSuggestions) + { + Caption = 'Suggestions'; + ToolTip = 'Specifies the suggestions for the user intervention request.'; + } + field(CreatedAt; Rec.SystemCreatedAt) + { + Caption = 'First Step Created At'; + ToolTip = 'Specifies the date and time when the timeline entry was created.'; + } + } + } + } + actions + { + area(Processing) + { +#pragma warning disable AW0005 + action(Send) +#pragma warning restore AW0005 + { + Caption = 'Send'; + ToolTip = 'Sends the selected instructions to the agent.'; + Scope = Repeater; + trigger OnAction() + var + UserInterventionRequestStep: Record "Agent Task Step"; + AgentTaskImpl: Codeunit "Agent Task Impl."; + SelectedSuggestionIdInt: Integer; + begin + if UserInterventionRequestStep.Get(Rec."Task ID", Rec."Last Step Number") then + if UserInterventionRequestStep.Type = "Agent Task Step Type"::"User Intervention Request" then + if Evaluate(SelectedSuggestionIdInt, SelectedSuggestionId) then + AgentTaskImpl.CreateUserInterventionTaskStep(UserInterventionRequestStep, SelectedSuggestionIdInt); + end; + } + +#pragma warning disable AW0005 + action(Retry) +#pragma warning restore AW0005 + { + Caption = 'Retry'; + ToolTip = 'Retries the task.'; + Scope = Repeater; + trigger OnAction() + var + UserInterventionRequestStep: Record "Agent Task Step"; + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + if UserInterventionRequestStep.Get(Rec."Task ID", Rec."Last Step Number") then + if UserInterventionRequestStep.Type = "Agent Task Step Type"::"User Intervention Request" then + AgentTaskImpl.CreateUserInterventionTaskStep(UserInterventionRequestStep); + end; + + } + } + } + + trigger OnOpenPage() + begin + SelectedSuggestionId := ''; + Rec.SetRange(Importance, Rec.Importance::Primary); + end; + + trigger OnAfterGetRecord() + begin + SetTaskTimelineDetails(); + end; + + local procedure SetTaskTimelineDetails() + var + InStream: InStream; + ConfirmationStepType: Enum "Agent Task Step Type"; + StepNumber: Integer; + begin + // Clear old values + GlobalConfirmedBy := ''; + GlobalConfirmedAt := 0DT; + Clear(GlobalPageSummary); + Clear(GlobalPageQuery); + Clear(GlobalAnnotations); + Clear(GlobalSuggestions); + + GlobalDescription := Rec.Description; + + if Rec.CalcFields("Primary Page Summary", "Primary Page Query", "Annotations") then begin + if Rec."Primary Page Summary".HasValue then begin + Rec."Primary Page Summary".CreateInStream(InStream, TextEncoding::UTF8); + GlobalPageSummary.Read(InStream); + Clear(InStream); + end; + if Rec."Primary Page Query".HasValue then begin + Rec."Primary Page Query".CreateInStream(InStream, TextEncoding::UTF8); + GlobalPageQuery.Read(InStream); + Clear(InStream); + end; + if Rec."Annotations".HasValue then begin + Rec."Annotations".CreateInStream(InStream, TextEncoding::UTF8); + GlobalAnnotations.Read(InStream); + Clear(InStream); + end; + end; + + if Rec.Type = Rec.Type::UserInterventionRequest then begin + Rec.CalcFields("Suggestions"); + if Rec.Suggestions.HasValue then begin + Rec.Suggestions.CreateInStream(InStream, TextEncoding::UTF8); + GlobalSuggestions.Read(InStream); + Clear(InStream); + end; + end; + + ConfirmationStatusOption := ConfirmationStatusOption::ConfirmationNotRequired; + StepNumber := Rec."Last Step Number"; + if (Rec."Last Step Type" <> "Agent Task Step Type"::Stop) and (Rec."Last User Intervention Step" > 0) then + StepNumber := Rec."Last User Intervention Step" + else + if Rec."Last Step Type" <> "Agent Task Step Type"::Stop then + // We know that there is no user intervention step for this timeline entry, and the last step is not a stop step. + exit; + if not TryGetConfirmationDetails(StepNumber, GlobalConfirmedBy, GlobalConfirmedAt, ConfirmationStepType) then + exit; + + case + ConfirmationStepType of + "Agent Task Step Type"::"User Intervention Request": + ConfirmationStatusOption := ConfirmationStatusOption::ReviewConfirmationRequired; + "Agent Task Step Type"::"User Intervention": + ConfirmationStatusOption := ConfirmationStatusOption::ReviewConfirmed; + "Agent Task Step Type"::Stop: + ConfirmationStatusOption := ConfirmationStatusOption::StopConfirmed; + else + ConfirmationStatusOption := ConfirmationStatusOption::ConfirmationNotRequired; + end; + end; + + local procedure TryGetConfirmationDetails(StepNumber: Integer; var By: Text[250]; var At: DateTime; var ConfirmationStepType: Enum "Agent Task Step Type"): Boolean + var + TaskTimelineEntryStep: Record "Agent Task Timeline Entry Step"; + User: Record User; + begin + if StepNumber <= 0 then + exit(false); + + TaskTimelineEntryStep.SetRange("Task ID", Rec."Task ID"); + TaskTimelineEntryStep.SetRange("Timeline Entry ID", Rec.ID); + TaskTimelineEntryStep.SetRange("Step Number", StepNumber); + if not TaskTimelineEntryStep.FindLast() then + exit(false); + + ConfirmationStepType := TaskTimelineEntryStep.Type; + if TaskTimelineEntryStep.Type = "Agent Task Step Type"::"User Intervention Request" then + exit(true); + + if ((TaskTimelineEntryStep.Type <> "Agent Task Step Type"::"User Intervention") and + (TaskTimelineEntryStep.Type <> "Agent Task Step Type"::Stop)) then + exit(false); + + User.SetRange("User Security ID", TaskTimelineEntryStep."User Security ID"); + if User.FindFirst() then + if User."Full Name" <> '' then + By := User."Full Name" + else + By := User."User Name"; + + At := Rec.SystemModifiedAt; + exit(true); + end; + + var + GlobalPageSummary: BigText; + GlobalPageQuery: BigText; + GlobalAnnotations: BigText; + GlobalSuggestions: BigText; + GlobalDescription: Text[2048]; + GlobalConfirmedBy: Text[250]; + GlobalConfirmedAt: DateTime; + ConfirmationStatusOption: Option " ",ConfirmationNotRequired,ReviewConfirmationRequired,ReviewConfirmed,StopConfirmationRequired,StopConfirmed,Discarded; + SelectedSuggestionId: Text[3]; +} + + diff --git a/src/System Application/App/Agent/TaskPane/Tasks.Page.al b/src/System Application/App/Agent/TaskPane/Tasks.Page.al new file mode 100644 index 0000000000..92ed3dbf1b --- /dev/null +++ b/src/System Application/App/Agent/TaskPane/Tasks.Page.al @@ -0,0 +1,131 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Agents; + +page 4306 Tasks +{ + PageType = ListPlus; + ApplicationArea = All; + SourceTable = "Agent Task Pane Entry"; + Caption = 'Agent Tasks'; + Editable = true; + InsertAllowed = false; + DeleteAllowed = false; + Extensible = false; + SourceTableView = sorting("Last Step Timestamp") order(descending); + InherentEntitlements = X; + InherentPermissions = X; + + layout + { + area(Content) + { + repeater(AgentTasks) + { + Editable = false; + field(TaskId; Rec."Task ID") + { + } + field(TaskNeedsAttention; Rec."Needs Attention") + { + } + field(TaskIndicator; Rec.Indicator) + { + } + field(TaskStatus; Rec.Status) + { + } + field(TaskHeader; Rec.Title) + { + Caption = 'Header'; + ToolTip = 'Specifies the header of the task.'; + } + field(TaskSummary; TaskSummary) + { + Caption = 'Summary'; + ToolTip = 'Specifies the summary of the task.'; + } + field(TaskStartedOn; Rec.SystemCreatedAt) + { + Caption = 'Started On'; + ToolTip = 'Specifies the date and time when the task was started.'; + } + field(TaskLastStepCompletedOn; Rec."Last Step Timestamp") + { + Caption = 'Last Step Completed On'; + } + field(TaskStepType; Rec."Current Entry Type") + { + Caption = 'Step Type'; + ToolTip = 'Specifies the type of the last step.'; + } + } + } + + area(FactBoxes) + { + part(Timeline; "Task Timeline") + { + SubPageLink = "Task ID" = field("Task ID"); + UpdatePropagation = Both; + Editable = true; + } + + part(Details; "Task Details") + { + Provider = Timeline; + SubPageLink = "Task ID" = field("Task ID"), "Timeline Entry ID" = field(ID); + Editable = true; + } + } + } + actions + { + area(Processing) + { +#pragma warning disable AW0005 + action(StopTask) +#pragma warning restore AW0005 + { + Caption = 'Stop task'; + ToolTip = 'Stops the task.'; + Enabled = true; + Scope = Repeater; + trigger OnAction() + var + AgentTask: Record "Agent Task"; + AgentTaskImpl: Codeunit "Agent Task Impl."; + begin + AgentTask.Get(Rec."Task ID"); + AgentTaskImpl.StopTask(AgentTask, AgentTask."Status"::"Stopped by User", false); + end; + } + } + } + + trigger OnAfterGetRecord() + begin + SetTaskDetails(); + end; + + local procedure SetTaskDetails() + var + InStream: InStream; + begin + // Clear old values + Clear(TaskSummary); + + Rec.CalcFields("Summary"); + if Rec."Summary".HasValue() then begin + Rec."Summary".CreateInStream(InStream); + TaskSummary.Read(InStream); + end; + end; + + var + TaskSummary: BigText; +} + diff --git a/src/System Application/App/Agent/app.json b/src/System Application/App/Agent/app.json new file mode 100644 index 0000000000..d7410592a5 --- /dev/null +++ b/src/System Application/App/Agent/app.json @@ -0,0 +1,48 @@ +{ + "id": "95f7c0f8-d9b8-4538-80f9-6e17f6f7b91a", + "name": "Agent", + "publisher": "Microsoft", + "brief": "Enables managing of agents", + "description": "Provides functionality for setting up, enabling and disabling, interacting and auditing agents.", + "version": "25.2.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009", + "EULA": "https://go.microsoft.com/fwlink/?LinkId=847985", + "help": "https://go.microsoft.com/fwlink/?linkid=868966", + "url": "https://go.microsoft.com/fwlink/?LinkId=724011", + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=868966", + "logo": "ExtensionLogo.png", + "screenshots": [], + "platform": "25.2.0.0", + "target": "OnPrem", + "idRanges": [ + { + "from": 3000, + "to": 5000 + } + ], + "dependencies": [ + { + "id": "dd4b9f8a-b018-4f69-a614-efdb744c5330", + "name": "Page Summary Provider", + "publisher": "Microsoft", + "version": "25.2.0.0" + }, + { + "id": "7b9b59f5-a68d-4271-b11a-0d3b9c0938dd", + "name": "User Settings", + "publisher": "Microsoft", + "version": "25.2.0.0" + }, + { + "id": "c56e3ef4-7ab0-4636-ae87-013a62f12213", + "name": "User Permissions", + "publisher": "Microsoft", + "version": "25.2.0.0" + } + ], + "propagateDependencies": true, + "features": [ + "TranslationFile", + "NoImplicitWith" + ] +} \ No newline at end of file diff --git a/src/System Application/App/Permission Sets/src/UserSubformPermissions.PageExt.al b/src/System Application/App/Permission Sets/src/UserSubformPermissions.PageExt.al new file mode 100644 index 0000000000..f80238aa2f --- /dev/null +++ b/src/System Application/App/Permission Sets/src/UserSubformPermissions.PageExt.al @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Security.AccessControl; + +using System.Security.User; + +pageextension 9862 "User Subform Permissions" extends "User Subform" +{ + actions + { + addlast(Processing) + { + action(Permissions) + { + ApplicationArea = Basic, Suite; + Caption = 'Permissions'; + Image = Permission; + ToolTip = 'View or edit which feature objects that users need to access and set up the related permissions in permission sets that you can assign to the users of the database.'; + + trigger OnAction() + var + PermissionSetRelation: Codeunit "Permission Set Relation"; + begin + PermissionSetRelation.OpenPermissionSetPage(Rec."Role Name", Rec."Role ID", Rec."App ID", Rec.Scope); + end; + } + } + } +} \ No newline at end of file diff --git a/src/System Application/App/Security Groups/app.json b/src/System Application/App/Security Groups/app.json index e7cdfdfd83..b507edc791 100644 --- a/src/System Application/App/Security Groups/app.json +++ b/src/System Application/App/Security Groups/app.json @@ -76,6 +76,14 @@ "from": 9031, "to": 9031 }, + { + "from": 9821, + "to": 9821 + }, + { + "from": 9848, + "to": 9848 + }, { "from": 9866, "to": 9877 diff --git a/src/System Application/App/Security Groups/permissions/SecurityGroupsObjects.PermissionSet.al b/src/System Application/App/Security Groups/permissions/SecurityGroupsObjects.PermissionSet.al index 84d6002b02..1a60bc6f31 100644 --- a/src/System Application/App/Security Groups/permissions/SecurityGroupsObjects.PermissionSet.al +++ b/src/System Application/App/Security Groups/permissions/SecurityGroupsObjects.PermissionSet.al @@ -20,5 +20,7 @@ permissionset 9031 "Security Groups - Objects" page "Security Group Members Part" = X, page "Security Group Permission Sets" = X, page "Security Groups" = X, + page "User Security Groups Part" = X, + page "Inherited Permission Sets Part" = X, xmlport "Export/Import Security Groups" = X; } diff --git a/src/System Application/App/Security Groups/src/InheritedPermissionSetsPart.Page.al b/src/System Application/App/Security Groups/src/InheritedPermissionSetsPart.Page.al new file mode 100644 index 0000000000..0a35ee6f88 --- /dev/null +++ b/src/System Application/App/Security Groups/src/InheritedPermissionSetsPart.Page.al @@ -0,0 +1,120 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Security.AccessControl; + +page 9821 "Inherited Permission Sets Part" +{ + Caption = 'Permission Sets from Security Groups'; + Editable = false; + PageType = ListPart; + SourceTable = "Access Control"; + SourceTableTemporary = true; + + layout + { + area(content) + { + repeater(Group) + { + Caption = 'User Permissions'; + field(PermissionSet; Rec."Role ID") + { + ApplicationArea = Basic, Suite; + Caption = 'Permission Set'; + ToolTip = 'Specifies the ID of a permission set.'; + Style = Unfavorable; + StyleExpr = PermissionSetNotFound; + + trigger OnDrillDown() + var + PermissionSetRelation: Codeunit "Permission Set Relation"; + begin + PermissionSetRelation.OpenPermissionSetPage('', Rec."Role ID", Rec."App ID", Rec.Scope); + end; + } + field(Company; Rec."Company Name") + { + ApplicationArea = Basic, Suite; + Caption = 'Company'; + ToolTip = 'Specifies the company that the permission set applies to.'; + } + } + } + } + + trigger OnAfterGetRecord() + var + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + PermissionSetNotFound := not AggregatePermissionSet.Get(Rec.Scope, Rec."App ID", Rec."Role ID"); + end; + + trigger OnOpenPage() + begin + if not IsInitializedByCaller then + Refresh(); + end; + + [Scope('OnPrem')] + procedure Refresh() + var + SecurityGroupMemberBuffer: Record "Security Group Member Buffer"; + SecurityGroup: Codeunit "Security Group"; + begin + SecurityGroup.GetMembers(SecurityGroupMemberBufferToRefresh); + SecurityGroupMemberBuffer.Copy(SecurityGroupMemberBufferToRefresh, true); + Refresh(SecurityGroupMemberBuffer); + end; + + [Scope('OnPrem')] + procedure Refresh(var SecurityGroupMemberBuffer: Record "Security Group Member Buffer") + var + AccessControl: Record "Access Control"; + TempDummyAccessControl: Record "Access Control" temporary; + SecurityGroup: Codeunit "Security Group"; + GroupUserSecId: Guid; + begin + if not SecurityGroupMemberBuffer.FindSet() then + exit; + + TempDummyAccessControl.Copy(Rec, true); + TempDummyAccessControl.Reset(); + TempDummyAccessControl.DeleteAll(); + + repeat + GroupUserSecId := SecurityGroup.GetGroupUserSecurityId(SecurityGroupMemberBuffer."Security Group Code"); + AccessControl.SetRange("User Security ID", GroupUserSecId); + if AccessControl.FindSet() then + repeat + if not Rec.Get(SecurityGroupMemberBuffer."User Security ID", AccessControl."Role ID", AccessControl."Company Name", AccessControl.Scope, AccessControl."App ID") then begin + Rec.TransferFields(AccessControl); + Rec."User Security ID" := SecurityGroupMemberBuffer."User Security ID"; + Rec.Insert(); + end; + until AccessControl.Next() = 0; + until SecurityGroupMemberBuffer.Next() = 0; + + CurrPage.Update(false); + end; + + [Scope('OnPrem')] + procedure SetRecordToRefresh(var SecurityGroupMemberBuffer: Record "Security Group Member Buffer") + begin + SecurityGroupMemberBufferToRefresh.Copy(SecurityGroupMemberBuffer, true); + end; + + [Scope('OnPrem')] + procedure SetInitializedByCaller() + begin + IsInitializedByCaller := true; + end; + + var + SecurityGroupMemberBufferToRefresh: Record "Security Group Member Buffer"; + PermissionSetNotFound: Boolean; + IsInitializedByCaller: Boolean; +} + diff --git a/src/System Application/App/Security Groups/src/SecurityGroupPermissionSets.Page.al b/src/System Application/App/Security Groups/src/SecurityGroupPermissionSets.Page.al index ffbb182476..cc3826a7bb 100644 --- a/src/System Application/App/Security Groups/src/SecurityGroupPermissionSets.Page.al +++ b/src/System Application/App/Security Groups/src/SecurityGroupPermissionSets.Page.al @@ -5,6 +5,8 @@ namespace System.Security.AccessControl; +using System.Security.User; + /// /// View and edit the permission sets associated with a security group. /// @@ -81,22 +83,13 @@ page 9868 "Security Group Permission Sets" trigger OnAction() var TempAggregatePermissionSet: Record "Aggregate Permission Set" temporary; - AccessControl: Record "Access Control"; PermissionSetRelation: Codeunit "Permission Set Relation"; + UserPermissions: Codeunit "User Permissions"; begin if not PermissionSetRelation.LookupPermissionSet(true, TempAggregatePermissionSet) then exit; - if TempAggregatePermissionSet.FindSet() then - repeat - if not AccessControl.Get(Rec."User Security ID", TempAggregatePermissionSet."Role ID", '', TempAggregatePermissionSet.Scope, TempAggregatePermissionSet."App ID") then begin - AccessControl."User Security ID" := Rec."User Security ID"; - AccessControl."Role ID" := TempAggregatePermissionSet."Role ID"; - AccessControl.Scope := TempAggregatePermissionSet.Scope; - AccessControl."App ID" := TempAggregatePermissionSet."App ID"; - AccessControl.Insert(); - end; - until TempAggregatePermissionSet.Next() = 0; + UserPermissions.AssignPermissionSets(Rec."User Security ID", '', TempAggregatePermissionSet); end; } } diff --git a/src/System Application/App/Security Groups/src/UserSecurityGroupsPart.Page.al b/src/System Application/App/Security Groups/src/UserSecurityGroupsPart.Page.al new file mode 100644 index 0000000000..139e9db825 --- /dev/null +++ b/src/System Application/App/Security Groups/src/UserSecurityGroupsPart.Page.al @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Security.AccessControl; + +page 9848 "User Security Groups Part" +{ + Caption = 'Security Groups'; + PageType = ListPart; + PopulateAllFields = true; + SourceTable = "Security Group Member Buffer"; + Editable = false; + + layout + { + area(content) + { + repeater(Group) + { + field(SecurityGroupCode; Rec."Security Group Code") + { + ApplicationArea = Basic, Suite; + Caption = 'Code'; + ToolTip = 'Specifies the security group code.'; + + trigger OnDrillDown() + var + SecurityGroupBuffer: Record "Security Group Buffer"; + SecurityGroups: Page "Security Groups"; + begin + SecurityGroupBuffer.SetRange(Code, Rec."Security Group Code"); + SecurityGroups.SetTableView(SecurityGroupBuffer); + SecurityGroups.Run(); + end; + } + field("Security Group Name"; Rec."Security Group Name") + { + ApplicationArea = Basic, Suite; + Caption = 'Name'; + ToolTip = 'Specifies the name of the security group.'; + } + } + } + } + + trigger OnOpenPage() + var + SecurityGroup: Codeunit "Security Group"; + begin + if not IsInitializedByCaller then + SecurityGroup.GetMembers(Rec); + end; + + [Scope('OnPrem')] + procedure Refresh(var SecurityGroupMemberBuffer: Record "Security Group Member Buffer") + begin + Rec.Copy(SecurityGroupMemberBuffer, true); + CurrPage.Update(false); + end; + + [Scope('OnPrem')] + procedure GetSourceRecord(var SecurityGroupMemberBuffer: Record "Security Group Member Buffer") + begin + IsInitializedByCaller := true; + SecurityGroupMemberBuffer.Copy(Rec, true); + end; + + [Scope('OnPrem')] + procedure SetInitializedByCaller() + begin + IsInitializedByCaller := true; + end; + + var + IsInitializedByCaller: Boolean; +} + diff --git a/src/System Application/App/User Permissions/app.json b/src/System Application/App/User Permissions/app.json index e210779236..dc18fb3a56 100644 --- a/src/System Application/App/User Permissions/app.json +++ b/src/System Application/App/User Permissions/app.json @@ -42,6 +42,10 @@ "from": 166, "to": 166 }, + { + "from": 9801, + "to": 9801 + }, { "from": 9854, "to": 9854 diff --git a/src/System Application/App/User Permissions/permissions/UserPermissionsObjects.PermissionSet.al b/src/System Application/App/User Permissions/permissions/UserPermissionsObjects.PermissionSet.al index c8f06ab217..6048d250b1 100644 --- a/src/System Application/App/User Permissions/permissions/UserPermissionsObjects.PermissionSet.al +++ b/src/System Application/App/User Permissions/permissions/UserPermissionsObjects.PermissionSet.al @@ -12,5 +12,6 @@ permissionset 166 "User Permissions - Objects" Access = Internal; Assignable = false; - Permissions = page "Lookup Permission Set" = X; + Permissions = page "Lookup Permission Set" = X, + page "User Subform" = X; } diff --git a/src/System Application/App/User Permissions/src/UserPermissions.Codeunit.al b/src/System Application/App/User Permissions/src/UserPermissions.Codeunit.al index 101b67dc0b..97ca5b74f6 100644 --- a/src/System Application/App/User Permissions/src/UserPermissions.Codeunit.al +++ b/src/System Application/App/User Permissions/src/UserPermissions.Codeunit.al @@ -84,6 +84,19 @@ codeunit 152 "User Permissions" end; #endif + /// + /// Assign a permission set to a given user + /// + /// The user's security ID. + /// The company for which to give the permission + /// Permission sets to assign + procedure AssignPermissionSets(var UserSecurityId: Guid; CompanyName: Text; var AggregatePermissionSet: Record "Aggregate Permission Set") + var + UserPermissionsImpl: Codeunit "User Permissions Impl."; + begin + UserPermissionsImpl.AssignPermissionSets(UserSecurityId, CompanyName, AggregatePermissionSet); + end; + /// /// Gets the effective permissions for the current user in the current company. /// diff --git a/src/System Application/App/User Permissions/src/UserPermissionsImpl.Codeunit.al b/src/System Application/App/User Permissions/src/UserPermissionsImpl.Codeunit.al index c25d9a25cd..372e08079c 100644 --- a/src/System Application/App/User Permissions/src/UserPermissionsImpl.Codeunit.al +++ b/src/System Application/App/User Permissions/src/UserPermissionsImpl.Codeunit.al @@ -290,6 +290,32 @@ codeunit 153 "User Permissions Impl." Evaluate(TempExpandedPermission."Execute Permission", SelectStr(5, PermissionMask)); end; + procedure AssignPermissionSets(var UserSecurityId: Guid; CompanyName: Text; var AggregatePermissionSet: Record "Aggregate Permission Set") + begin + if not AggregatePermissionSet.FindSet() then + exit; + + repeat + AssignPermissionSet(UserSecurityId, CompanyName, AggregatePermissionSet); + until AggregatePermissionSet.Next() = 0; + end; + + procedure AssignPermissionSet(var UserSecurityId: Guid; CompanyName: Text; var AggregatePermissionSet: Record "Aggregate Permission Set") + var + AccessControl: Record "Access Control"; + begin + if AccessControl.Get(UserSecurityId, AggregatePermissionSet."Role ID", '', AggregatePermissionSet.Scope, AggregatePermissionSet."App ID") then + exit; + + AccessControl."App ID" := AggregatePermissionSet."App ID"; + AccessControl."User Security ID" := UserSecurityId; + AccessControl."Role ID" := AggregatePermissionSet."Role ID"; + AccessControl.Scope := AggregatePermissionSet.Scope; +#pragma warning disable AA0139 + AccessControl."Company Name" := CompanyName; +#pragma warning restore AA0139 + AccessControl.Insert(); + end; /// /// An event that indicates that subscribers should set the result that should be returned when the CanManageUsersOnTenant is called. /// diff --git a/src/System Application/App/User Permissions/src/UserSubform.Page.al b/src/System Application/App/User Permissions/src/UserSubform.Page.al new file mode 100644 index 0000000000..dee79309e6 --- /dev/null +++ b/src/System Application/App/User Permissions/src/UserSubform.Page.al @@ -0,0 +1,142 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Security.User; + +using System.Security.AccessControl; + +page 9801 "User Subform" +{ + Caption = 'User Permission Sets'; + DelayedInsert = true; + PageType = ListPart; + SourceTable = "Access Control"; + + layout + { + area(content) + { + repeater(Group) + { + Caption = 'User Permissions'; + field(PermissionSet; Rec."Role ID") + { + ApplicationArea = Basic, Suite; + Caption = 'Permission Set'; + ToolTip = 'Specifies the ID of a security role that has been assigned to this Windows login in the current database.'; + Style = Unfavorable; + StyleExpr = PermissionSetNotFound; + + trigger OnLookup(var Text: Text): Boolean + var + LookupPermissionSet: Page "Lookup Permission Set"; + begin + LookupPermissionSet.LookupMode := true; + if LookupPermissionSet.RunModal() = ACTION::LookupOK then begin + LookupPermissionSet.GetRecord(PermissionSetLookupRecord); + Text := PermissionSetLookupRecord."Role ID"; + PermissionSetLookupRecord.SetRecFilter(); + exit(true); + end; + end; + + trigger OnValidate() + begin + PermissionSetLookupRecord.SetRange("Role ID", Rec."Role ID"); + PermissionSetLookupRecord.FindFirst(); + + if PermissionSetLookupRecord.Count > 1 then + Error(MultipleRoleIDErr, Rec."Role ID"); + + Rec.Scope := PermissionSetLookupRecord.Scope; + Rec."App ID" := PermissionSetLookupRecord."App ID"; + PermissionScope := Format(PermissionSetLookupRecord.Scope); + + Rec.CalcFields("App Name", "Role Name"); + PermissionSetLookupRecord.Reset(); + end; + } + field(Description; Rec."Role Name") + { + ApplicationArea = Basic, Suite; + Caption = 'Description'; + DrillDown = false; + Editable = false; + ToolTip = 'Specifies the name of the security role that has been given to this Windows login in the current database.'; + } + field(Company; Rec."Company Name") + { + ApplicationArea = Basic, Suite; + Caption = 'Company'; + ToolTip = 'Specifies the name of the company that this role is limited to for this Windows login.'; + } + field(ExtensionName; Rec."App Name") + { + ApplicationArea = Basic, Suite; + Caption = 'Extension Name'; + DrillDown = false; + Editable = false; + ToolTip = 'Specifies the name of the extension.'; + } + field(PermissionScope; PermissionScope) + { + ApplicationArea = Basic, Suite; + Caption = 'Permission Scope'; + Editable = false; + ToolTip = 'Specifies the scope of the permission set.'; + } + } + } + } + + var + PermissionSetLookupRecord: Record "Aggregate Permission Set"; + User: Record User; + MultipleRoleIDErr: Label 'The permission set %1 is defined multiple times in this context. Use the lookup button to select the relevant permission set.', Comment = '%1 will be replaced with a Role ID code value from the Permission Set table'; + PermissionScope: Text; + PermissionSetNotFound: Boolean; + + trigger OnAfterGetRecord() + var + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + if User."User Name" <> '' then + CurrPage.Caption := User."User Name"; + + PermissionScope := Format(Rec.Scope); + + PermissionSetNotFound := false; + if not (Rec."Role ID" in ['SUPER', 'SECURITY']) then begin + PermissionSetNotFound := not AggregatePermissionSet.Get(Rec.Scope, Rec."App ID", Rec."Role ID"); + + if PermissionSetNotFound then + OnPermissionSetNotFound(); + end; + end; + + trigger OnInsertRecord(BelowxRec: Boolean): Boolean + begin + User.TestField("User Name"); + Rec.CalcFields("App Name", Rec."Role Name"); + end; + + trigger OnModifyRecord(): Boolean + begin + Rec.CalcFields("App Name", Rec."Role Name"); + end; + + trigger OnNewRecord(BelowxRec: Boolean) + begin + if User.Get(Rec."User Security ID") then; + Rec.CalcFields("App Name", Rec."Role Name"); + PermissionScope := ''; + end; + + [IntegrationEvent(false, false)] + local procedure OnPermissionSetNotFound() + begin + end; +} + diff --git a/src/System Application/App/User Settings/src/UserSettings.Codeunit.al b/src/System Application/App/User Settings/src/UserSettings.Codeunit.al index 669284a383..39c1cd98aa 100644 --- a/src/System Application/App/User Settings/src/UserSettings.Codeunit.al +++ b/src/System Application/App/User Settings/src/UserSettings.Codeunit.al @@ -91,6 +91,40 @@ codeunit 9176 "User Settings" UserSettingsImpl.GetAllowedCompaniesForCurrentUser(TempCompany); end; + /// + /// Allows the user to select the new profile for given User Settings + /// + /// User settings to update with the new profile + procedure LookupProfile(var UserSettingsRec: Record "User Settings") + var + UserSettingsImpl: Codeunit "User Settings Impl."; + begin + UserSettingsImpl.ProfileLookup(UserSettingsRec); + end; + + /// + /// Gets a profile name for the given user settings. + /// + /// User settings to get the profile name. + /// + procedure GetProfileName(UserSettingsRec: Record "User Settings"): Text + var + UserSettingsImpl: Codeunit "User Settings Impl."; + begin + UserSettingsImpl.GetProfileName(UserSettingsRec.Scope, UserSettingsRec."App ID", UserSettingsRec."Profile ID"); + end; + + /// + /// Updates the user settings for given user + /// + /// + procedure UpdateUserSettings(var UserSettings: Record "User Settings") + var + UserSettingsImpl: Codeunit "User Settings Impl."; + begin + UserSettingsImpl.UpdateUserSettings(UserSettings); + end; + /// /// Integration event to get the default profile. /// diff --git a/src/System Application/App/User Settings/src/UserSettingsImpl.Codeunit.al b/src/System Application/App/User Settings/src/UserSettingsImpl.Codeunit.al index fc0d340a00..09beb6ba96 100644 --- a/src/System Application/App/User Settings/src/UserSettingsImpl.Codeunit.al +++ b/src/System Application/App/User Settings/src/UserSettingsImpl.Codeunit.al @@ -154,6 +154,14 @@ codeunit 9175 "User Settings Impl." GetUserSettings(UserSettings."User Security ID", UserSettings); end; + procedure UpdateUserSettings(NewUserSettings: Record "User Settings") + var + CurrentUserSettings: Record "User Settings"; + begin + GetUserSettings(NewUserSettings."User Security ID", CurrentUserSettings); + UpdateCurrentUsersSettings(CurrentUserSettings, NewUserSettings); + end; + procedure UpdateUserSettings(OldUserSettings: Record "User Settings"; NewUserSettings: Record "User Settings") var UserSettings: Codeunit "User Settings"; From 9e45facd8030e6e6e3b36f3675b4cc93f24a033e Mon Sep 17 00:00:00 2001 From: Nikola Kukrika Date: Wed, 4 Dec 2024 12:44:27 +0100 Subject: [PATCH 2/2] Update to app.json --- src/System Application/App/app.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/System Application/App/app.json b/src/System Application/App/app.json index 048dfb3fa9..fc6413da93 100644 --- a/src/System Application/App/app.json +++ b/src/System Application/App/app.json @@ -21,6 +21,11 @@ "id": "9856ae4f-d1a7-46ef-89bb-6ef056398228", "name": "System Application Test Library", "publisher": "Microsoft" + }, + { + "id": "1a3ac64b-0e25-2345-8e9b-eab2a74b9e9a", + "name": "Agent Developer Toolkit", + "publisher": "Microsoft" } ], "screenshots": [],