From e712ef602d0c0d6fed130a17b4a3df3c640077eb Mon Sep 17 00:00:00 2001 From: Darrick Joo Date: Mon, 20 Jul 2026 15:08:15 +0200 Subject: [PATCH 1/2] [main] bug 640937 - Reset workflow templates no longer removes steps from active workflows ResetWorkflowTemplates deleted workflow steps filtered only by the 'MS-' template code prefix, which also matched active workflows created from templates (code MS-XYZ-01, Template=false), emptying them. Scope step/argument deletion to each template's exact Workflow Code so active workflows are never affected. Also add a confirmation dialog and clearer tooltip to the Reset Microsoft Templates action, plus regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../System/Workflow/WorkflowSetup.Codeunit.al | 23 ++-- .../System/Workflow/WorkflowTemplates.Page.al | 5 +- .../Workflow/CopyWorkflowTests.Codeunit.al | 102 ++++++++++++++++++ 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/src/Layers/W1/BaseApp/System/Workflow/WorkflowSetup.Codeunit.al b/src/Layers/W1/BaseApp/System/Workflow/WorkflowSetup.Codeunit.al index 621aff5a181..b646a85523f 100644 --- a/src/Layers/W1/BaseApp/System/Workflow/WorkflowSetup.Codeunit.al +++ b/src/Layers/W1/BaseApp/System/Workflow/WorkflowSetup.Codeunit.al @@ -261,16 +261,21 @@ codeunit 1502 "Workflow Setup" begin Workflow.SetRange(Template, true); Workflow.SetFilter(Code, '%1', GetWorkflowTemplateToken() + '*'); - Workflow.DeleteAll(); - - WorkflowStep.SetFilter("Workflow Code", '%1', GetWorkflowTemplateToken() + '*'); - if WorkflowStep.FindSet() then begin + if Workflow.FindSet() then repeat - WorkflowStepArgument.SetRange(ID, WorkflowStep.Argument); - WorkflowStepArgument.DeleteAll(); - until WorkflowStep.Next() = 0; - WorkflowStep.DeleteAll(); - end; + // Only delete the steps that belong to the template itself. Workflows created from a + // template keep the template token prefix in their code (e.g. MS-XYZ-01) but are not + // templates, so filtering steps by the token wildcard would also delete their steps. + WorkflowStep.SetRange("Workflow Code", Workflow.Code); + if WorkflowStep.FindSet() then begin + repeat + WorkflowStepArgument.SetRange(ID, WorkflowStep.Argument); + WorkflowStepArgument.DeleteAll(); + until WorkflowStep.Next() = 0; + WorkflowStep.DeleteAll(); + end; + until Workflow.Next() = 0; + Workflow.DeleteAll(); InitWorkflow(); end; diff --git a/src/Layers/W1/BaseApp/System/Workflow/WorkflowTemplates.Page.al b/src/Layers/W1/BaseApp/System/Workflow/WorkflowTemplates.Page.al index b0e785ae6cc..c287b20fa5b 100644 --- a/src/Layers/W1/BaseApp/System/Workflow/WorkflowTemplates.Page.al +++ b/src/Layers/W1/BaseApp/System/Workflow/WorkflowTemplates.Page.al @@ -85,12 +85,14 @@ page 1505 "Workflow Templates" Caption = 'Reset Microsoft Templates'; Visible = not IsLookupMode; Image = ResetStatus; - ToolTip = 'Recreate all Microsoft templates'; + ToolTip = 'Delete and recreate the built-in Microsoft workflow templates. This affects only the templates, not the workflows that were created from them.'; trigger OnAction() var WorkflowSetup: Codeunit "Workflow Setup"; begin + if not Confirm(ResetTemplatesQst) then + exit; WorkflowSetup.ResetWorkflowTemplates(); Initialize(); end; @@ -151,6 +153,7 @@ page 1505 "Workflow Templates" var WorkflowSetup: Codeunit "Workflow Setup"; QueryClosePageLookupErr: Label 'Select a workflow template to continue, or choose Cancel to close the page.'; + ResetTemplatesQst: Label 'This will delete and recreate all Microsoft workflow templates. Do you want to continue?'; DescriptionStyle: Text; protected var diff --git a/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al b/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al index 54e43e91ef3..5bd4467264f 100644 --- a/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al +++ b/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al @@ -350,6 +350,72 @@ codeunit 134306 "Copy Workflow Tests" Assert.AreEqual(0, FromWorkflow.Count, 'A new workflow was created.') end; + [Test] + [HandlerFunctions('ConfirmHandlerYes')] + [Scope('OnPrem')] + procedure TestResetTemplatesKeepsStepsOfWorkflowCreatedFromTemplate() + var + TemplateWorkflow: Record Workflow; + WorkflowFromTemplate: Record Workflow; + WorkflowStep: Record "Workflow Step"; + WorkflowSetup: Codeunit "Workflow Setup"; + WorkflowTemplatesPage: TestPage "Workflow Templates"; + TemplateCode: Code[20]; + WorkflowFromTemplateCode: Code[20]; + begin + // [SCENARIO 640937] Resetting Microsoft templates must not remove the steps of active + // workflows that were created from a template and still carry the template code prefix. + + // [GIVEN] A workflow template whose code starts with the Microsoft template token, with steps. + Initialize(); + TemplateCode := CopyStr(WorkflowSetup.GetWorkflowTemplateToken() + 'BUG640937', 1, MaxStrLen(TemplateWorkflow.Code)); + CreateTwoStepWorkflowWithCode(TemplateWorkflow, TemplateCode); + TemplateWorkflow.Validate(Template, true); + TemplateWorkflow.Modify(true); + + // [GIVEN] An active (non-template) workflow created from that template: its code keeps the + // template token prefix (e.g. MS-...-01) and it has its own steps. + WorkflowFromTemplateCode := CopyStr(TemplateCode + '-01', 1, MaxStrLen(WorkflowFromTemplate.Code)); + CreateTwoStepWorkflowWithCode(WorkflowFromTemplate, WorkflowFromTemplateCode); + + // [WHEN] The user runs Reset Microsoft Templates and confirms. + WorkflowTemplatesPage.OpenView(); + WorkflowTemplatesPage."Reset Templates".Invoke(); + + // [THEN] The active workflow still exists and keeps all of its steps. + Assert.IsTrue(WorkflowFromTemplate.Get(WorkflowFromTemplateCode), 'The workflow created from the template must not be deleted.'); + WorkflowStep.SetRange("Workflow Code", WorkflowFromTemplateCode); + Assert.IsFalse(WorkflowStep.IsEmpty(), 'The steps of the workflow created from the template must not be deleted.'); + end; + + [Test] + [HandlerFunctions('ConfirmHandlerNo')] + [Scope('OnPrem')] + procedure TestResetTemplatesIsSkippedWhenConfirmationDeclined() + var + TemplateWorkflow: Record Workflow; + WorkflowSetup: Codeunit "Workflow Setup"; + WorkflowTemplatesPage: TestPage "Workflow Templates"; + TemplateCode: Code[20]; + begin + // [SCENARIO 640937] The Reset Microsoft Templates action asks for confirmation and does + // nothing when the user declines. + + // [GIVEN] A Microsoft workflow template with steps. + Initialize(); + TemplateCode := CopyStr(WorkflowSetup.GetWorkflowTemplateToken() + 'BUG640937', 1, MaxStrLen(TemplateWorkflow.Code)); + CreateTwoStepWorkflowWithCode(TemplateWorkflow, TemplateCode); + TemplateWorkflow.Validate(Template, true); + TemplateWorkflow.Modify(true); + + // [WHEN] The user runs Reset Microsoft Templates but declines the confirmation. + WorkflowTemplatesPage.OpenView(); + WorkflowTemplatesPage."Reset Templates".Invoke(); + + // [THEN] Nothing is reset: the template still exists. + Assert.IsTrue(TemplateWorkflow.Get(TemplateCode), 'The templates must not be reset when the confirmation is declined.'); + end; + local procedure Initialize() begin LibraryWorkflow.DeleteAllExistingWorkflows(); @@ -439,5 +505,41 @@ codeunit 134306 "Copy Workflow Tests" until FromWorkflowRule.Next() = 0; until ToWorkflowStep.Next() = 0; end; + + local procedure CreateTwoStepWorkflowWithCode(var Workflow: Record Workflow; NewWorkflowCode: Code[20]) + var + WorkflowEvent: Record "Workflow Event"; + WorkflowRule: Record "Workflow Rule"; + WorkflowResponseHandling: Codeunit "Workflow Response Handling"; + EntryPointEventID: Integer; + ResponseID: Integer; + begin + Workflow.Init(); + Workflow.Code := NewWorkflowCode; + Workflow.Description := CopyStr(LibraryUtility.GenerateRandomXMLText(MaxStrLen(Workflow.Description)), 1, MaxStrLen(Workflow.Description)); + Workflow.Category := LibraryWorkflow.CreateWorkflowCategory(); + Workflow.Template := false; + Workflow.Insert(true); + + CreateAnyPurchaseHeaderEvent(WorkflowEvent); + EntryPointEventID := LibraryWorkflow.InsertEntryPointEventStep(Workflow, WorkflowEvent."Function Name"); + ResponseID := LibraryWorkflow.InsertResponseStep(Workflow, WorkflowResponseHandling.CreateNotificationEntryCode(), EntryPointEventID); + + LibraryWorkflow.InsertEventArgument(EntryPointEventID, StrSubstNo(AmountCondXmlTemplateTxt, Format(LibraryRandom.RandDec(1000, 2)))); + LibraryWorkflow.InsertEventRule(EntryPointEventID, 0, WorkflowRule.Operator::Changed); + LibraryWorkflow.InsertNotificationArgument(ResponseID, UserId, 0, ''); + end; + + [ConfirmHandler] + procedure ConfirmHandlerYes(Question: Text[1024]; var Reply: Boolean) + begin + Reply := true; + end; + + [ConfirmHandler] + procedure ConfirmHandlerNo(Question: Text[1024]; var Reply: Boolean) + begin + Reply := false; + end; } From c4b451b0b131ecddcac96886fecb9d1baa94614e Mon Sep 17 00:00:00 2001 From: Darrick Joo Date: Wed, 22 Jul 2026 09:28:10 +0200 Subject: [PATCH 2/2] Drive reset-template test ConfirmHandlers with enqueued expectations Verify the confirm dialog fires exactly once with the expected question, per the UI-handlers-in-tests guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Workflow/CopyWorkflowTests.Codeunit.al | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al b/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al index 5bd4467264f..20c66fac006 100644 --- a/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al +++ b/src/Layers/W1/Tests/Workflow/CopyWorkflowTests.Codeunit.al @@ -14,6 +14,8 @@ codeunit 134306 "Copy Workflow Tests" AmountCondXmlTemplateTxt: Label 'SORTING(Document Type,No.) WHERE(Amount=FILTER(%1))SORTING(Document Type,Document No.,Line No.)', Locked = true; LibraryUtility: Codeunit "Library - Utility"; LibraryRandom: Codeunit "Library - Random"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; + ResetTemplatesConfirmQst: Label 'delete and recreate all Microsoft workflow templates', Locked = true; [Test] [Scope('OnPrem')] @@ -351,7 +353,7 @@ codeunit 134306 "Copy Workflow Tests" end; [Test] - [HandlerFunctions('ConfirmHandlerYes')] + [HandlerFunctions('ConfirmHandler')] [Scope('OnPrem')] procedure TestResetTemplatesKeepsStepsOfWorkflowCreatedFromTemplate() var @@ -379,6 +381,8 @@ codeunit 134306 "Copy Workflow Tests" CreateTwoStepWorkflowWithCode(WorkflowFromTemplate, WorkflowFromTemplateCode); // [WHEN] The user runs Reset Microsoft Templates and confirms. + LibraryVariableStorage.Enqueue(ResetTemplatesConfirmQst); + LibraryVariableStorage.Enqueue(true); WorkflowTemplatesPage.OpenView(); WorkflowTemplatesPage."Reset Templates".Invoke(); @@ -386,10 +390,11 @@ codeunit 134306 "Copy Workflow Tests" Assert.IsTrue(WorkflowFromTemplate.Get(WorkflowFromTemplateCode), 'The workflow created from the template must not be deleted.'); WorkflowStep.SetRange("Workflow Code", WorkflowFromTemplateCode); Assert.IsFalse(WorkflowStep.IsEmpty(), 'The steps of the workflow created from the template must not be deleted.'); + LibraryVariableStorage.AssertEmpty(); end; [Test] - [HandlerFunctions('ConfirmHandlerNo')] + [HandlerFunctions('ConfirmHandler')] [Scope('OnPrem')] procedure TestResetTemplatesIsSkippedWhenConfirmationDeclined() var @@ -409,15 +414,19 @@ codeunit 134306 "Copy Workflow Tests" TemplateWorkflow.Modify(true); // [WHEN] The user runs Reset Microsoft Templates but declines the confirmation. + LibraryVariableStorage.Enqueue(ResetTemplatesConfirmQst); + LibraryVariableStorage.Enqueue(false); WorkflowTemplatesPage.OpenView(); WorkflowTemplatesPage."Reset Templates".Invoke(); // [THEN] Nothing is reset: the template still exists. Assert.IsTrue(TemplateWorkflow.Get(TemplateCode), 'The templates must not be reset when the confirmation is declined.'); + LibraryVariableStorage.AssertEmpty(); end; local procedure Initialize() begin + LibraryVariableStorage.Clear(); LibraryWorkflow.DeleteAllExistingWorkflows(); end; @@ -531,15 +540,10 @@ codeunit 134306 "Copy Workflow Tests" end; [ConfirmHandler] - procedure ConfirmHandlerYes(Question: Text[1024]; var Reply: Boolean) + procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean) begin - Reply := true; - end; - - [ConfirmHandler] - procedure ConfirmHandlerNo(Question: Text[1024]; var Reply: Boolean) - begin - Reply := false; + Assert.ExpectedConfirm(LibraryVariableStorage.DequeueText(), Question); + Reply := LibraryVariableStorage.DequeueBoolean(); end; }