diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index e63fafc77b..983603dace 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -131,7 +131,12 @@ function TemplatesStrings (BaseString) { NEW_LINK: t.s('Please click on an available node to form a new link.'), UNLINK: t.s('UNLINK'), READ_ONLY_PROMPT_VALUES: t.s('The following promptable values were provided when this node was created:'), - READ_ONLY_NO_PROMPT_VALUES: t.s('No promptable values were provided when this node was created.') + READ_ONLY_NO_PROMPT_VALUES: t.s('No promptable values were provided when this node was created.'), + UNSAVED_CHANGES_HEADER: t.s('WARNING: UNSAVED CHANGES'), + UNSAVED_CHANGES_PROMPT_TEXT: t.s('Are you sure you want to exit the Workflow Creator without saving your changes?'), + EXIT: t.s('EXIT'), + CANCEL: t.s('CANCEL'), + SAVE_AND_EXIT: t.s('SAVE & EXIT') }; } diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less index e2f0e39a2e..760ceafc7d 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.block.less @@ -114,7 +114,8 @@ color: @btn-txt; } -.WorkflowMaker-deleteOverlay { +.WorkflowMaker-deleteOverlay, +.WorkflowMaker-unsavedChangesOverlay { height: 100%; width: 100%; position: absolute; diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index dc1ca3fe48..d3352fd46d 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -7,11 +7,11 @@ export default ['$scope', 'TemplatesService', 'ProcessErrors', '$q', 'PromptService', 'TemplatesStrings', 'WorkflowChartService', - 'Wait', '$state', + 'Wait', '$state', '$transitions', function ($scope, TemplatesService, ProcessErrors, $q, PromptService, TemplatesStrings, WorkflowChartService, - Wait, $state + Wait, $state, $transitions ) { let deletedNodeIds = []; @@ -32,6 +32,13 @@ export default ['$scope', 'TemplatesService', 'showLinkForm': false }; + $scope.workflowChangesUnsaved = false; + $scope.workflowChangesStarted = false; + + $scope.cancelUnsavedChanges = () => { + $scope.unsavedChangesVisible = false; + } + let getNodes = () => { Wait('start'); TemplatesService.getWorkflowJobTemplateNodes($scope.workflowJobTemplateObj.id, page) @@ -72,6 +79,8 @@ export default ['$scope', 'TemplatesService', Wait('start'); + $scope.unsavedChangesVisible = false; + let buildSendableNodeData = (node) => { // Create the node let sendableNodeData = { @@ -364,6 +373,8 @@ export default ['$scope', 'TemplatesService', return $q.all(associatePromises.concat(credentialPromises)) .then(() => { Wait('stop'); + $scope.workflowChangesUnsaved = false; + $scope.workflowChangesStarted = false; $scope.closeDialog(); }).catch(({ data, status }) => { Wait('stop'); @@ -396,6 +407,8 @@ export default ['$scope', 'TemplatesService', $q.all(deletePromises) .then(() => { Wait('stop'); + $scope.workflowChangesUnsaved = false; + $scope.workflowChangesStarted = false; $scope.closeDialog(); $state.transitionTo('templates'); }).catch(({ data, status }) => { @@ -410,6 +423,7 @@ export default ['$scope', 'TemplatesService', /* ADD NODE FUNCTIONS */ $scope.startAddNodeWithoutChild = (parent) => { + $scope.workflowChangesStarted = true; if ($scope.nodeConfig) { $scope.cancelNodeForm(); } @@ -448,6 +462,7 @@ export default ['$scope', 'TemplatesService', }; $scope.startAddNodeWithChild = (link) => { + $scope.workflowChangesStarted = true; if ($scope.nodeConfig) { $scope.cancelNodeForm(); } @@ -494,6 +509,7 @@ export default ['$scope', 'TemplatesService', }; $scope.confirmNodeForm = (selectedTemplate, promptData, edgeType) => { + $scope.workflowChangesUnsaved = true; const nodeId = $scope.nodeConfig.nodeId; if ($scope.nodeConfig.mode === "add") { if (selectedTemplate && edgeType && edgeType.value) { @@ -543,6 +559,7 @@ export default ['$scope', 'TemplatesService', }; $scope.cancelNodeForm = () => { + $scope.workflowChangesStarted = false; const nodeId = $scope.nodeConfig.nodeId; if ($scope.nodeConfig.mode === "add") { // Remove the placeholder node from the array @@ -599,6 +616,7 @@ export default ['$scope', 'TemplatesService', /* EDIT NODE FUNCTIONS */ $scope.startEditNode = (nodeToEdit) => { + $scope.workflowChangesStarted = true; if ($scope.linkConfig) { $scope.cancelLinkForm(); } @@ -625,6 +643,7 @@ export default ['$scope', 'TemplatesService', /* LINK FUNCTIONS */ $scope.startEditLink = (linkToEdit) => { + $scope.workflowChangesStarted = true; const setupLinkEdit = () => { // Determine whether or not this link can be removed @@ -677,6 +696,7 @@ export default ['$scope', 'TemplatesService', }; $scope.selectNodeForLinking = (node) => { + $scope.workflowChangesStarted = true; if ($scope.nodeConfig) { $scope.cancelNodeForm(); } @@ -772,6 +792,7 @@ export default ['$scope', 'TemplatesService', }; $scope.confirmLinkForm = (newEdgeType) => { + $scope.workflowChangesUnsaved = true; $scope.graphState.arrayOfLinksForChart.forEach((link) => { if (link.source.id === $scope.linkConfig.source.id && link.target.id === $scope.linkConfig.target.id) { link.edgeType = newEdgeType; @@ -792,6 +813,7 @@ export default ['$scope', 'TemplatesService', }; $scope.unlink = () => { + $scope.workflowChangesUnsaved = true; // Remove the link for( let i = $scope.graphState.arrayOfLinksForChart.length; i--; ){ const link = $scope.graphState.arrayOfLinksForChart[i]; @@ -807,6 +829,7 @@ export default ['$scope', 'TemplatesService', }; $scope.cancelLinkForm = () => { + $scope.workflowChangesStarted = false; if ($scope.linkConfig.mode === "add" && $scope.linkConfig.target) { $scope.graphState.arrayOfLinksForChart.splice($scope.graphState.arrayOfLinksForChart.length-1, 1); let targetIsOrphaned = true; @@ -838,16 +861,19 @@ export default ['$scope', 'TemplatesService', /* DELETE NODE FUNCTIONS */ $scope.startDeleteNode = (nodeToDelete) => { + $scope.workflowChangesStarted = true; $scope.nodeToBeDeleted = nodeToDelete; $scope.deleteOverlayVisible = true; }; $scope.cancelDeleteNode = () => { + $scope.workflowChangesStarted = false; $scope.nodeToBeDeleted = null; $scope.deleteOverlayVisible = false; }; $scope.confirmDeleteNode = () => { + $scope.workflowChangesUnsaved = true; if ($scope.nodeToBeDeleted) { const nodeId = $scope.nodeToBeDeleted.id; diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js index add6d5cd65..0a7f9ac1c9 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.directive.js @@ -63,10 +63,15 @@ export default ['templateUrl', 'CreateDialog', 'Wait', '$state', '$window', }); scope.closeDialog = function() { - $('#workflow-modal-dialog').dialog('destroy'); - $('body').removeClass('WorkflowMaker-preventBodyScrolling'); + if (scope.workflowChangesUnsaved || scope.workflowChangesStarted) { + scope.unsavedChangesVisible = true; + } else { + scope.unsavedChangesVisible = false; + $('#workflow-modal-dialog').dialog('destroy'); + $('body').removeClass('WorkflowMaker-preventBodyScrolling'); - $state.go('^'); + $state.go('^'); + } }; function onResize(){ diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html index 2f83bbd08b..d04f2c2264 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.partial.html @@ -17,8 +17,38 @@