diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index f5e7e59fcb..eff9f87730 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -735,6 +735,7 @@ class TestDuplicateItem(ItemTest, DuplicateHelper): verify_name(self.seq_usage_key, self.chapter_usage_key, "customized name", display_name="customized name") +@ddt.ddt class TestMoveItem(ItemTest): """ Tests for move item. @@ -744,7 +745,16 @@ class TestMoveItem(ItemTest): Creates the test course structure to build course outline tree. """ super(TestMoveItem, self).setUp() + self.setup_course() + def setup_course(self, default_store=None): + """ + Helper method to create the course. + """ + if not default_store: + default_store = self.store.default_modulestore.get_modulestore_type() + + self.course = CourseFactory.create(default_store=default_store) # Create a parent chapter chap1 = self.create_xblock(parent_usage_key=self.course.location, display_name='chapter1', category='chapter') self.chapter_usage_key = self.response_usage_key(chap1) @@ -821,10 +831,15 @@ class TestMoveItem(ItemTest): self.assertEqual(new_parent_loc, target_usage_key) self.assertNotEqual(parent_loc, new_parent_loc) - def test_move_component(self): + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) + def test_move_component(self, store_type): """ Test move component with different xblock types. + + Arguments: + store_type (ModuleStoreEnum.Type): Type of modulestore to create test course in. """ + self.setup_course(default_store=store_type) for source_usage_key, target_usage_key in [ (self.html_usage_key, self.vert2_usage_key), (self.vert_usage_key, self.seq2_usage_key), diff --git a/cms/static/cms/js/main.js b/cms/static/cms/js/main.js index 6a463521c0..1e5fb698d1 100644 --- a/cms/static/cms/js/main.js +++ b/cms/static/cms/js/main.js @@ -59,10 +59,10 @@ success: callback }); }; - $.postJSON = function(url, data, callback) { + $.postJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign return sendJSON(url, data, callback, 'POST'); }; - $.patchJSON = function(url, data, callback) { + $.patchJSON = function(url, data, callback) { // eslint-disable-line no-param-reassign return sendJSON(url, data, callback, 'PATCH'); }; return domReady(function() { diff --git a/cms/static/js/spec/views/modals/move_xblock_modal_spec.js b/cms/static/js/spec/views/modals/move_xblock_modal_spec.js index 64a89aed4b..f2e8f457b4 100644 --- a/cms/static/js/spec/views/modals/move_xblock_modal_spec.js +++ b/cms/static/js/spec/views/modals/move_xblock_modal_spec.js @@ -1,24 +1,8 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/view_helpers', - 'js/views/modals/move_xblock_modal', 'edx-ui-toolkit/js/utils/html-utils', - 'edx-ui-toolkit/js/utils/string-utils', 'js/models/xblock_info'], - function($, _, AjaxHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, HtmlUtils, StringUtils, XBlockInfo) { + 'js/views/modals/move_xblock_modal', 'js/models/xblock_info'], + function($, _, AjaxHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, XBlockInfo) { 'use strict'; - - var modal, - showModal, - verifyNotificationStatus, - selectTargetParent, - getConfirmationFeedbackTitle, - getUndoConfirmationFeedbackTitle, - getConfirmationFeedbackTitleHtml, - getConfirmationFeedbackMessageHtml, - sourceDisplayName = 'HTML 101', - outlineUrl = '/course/cid?formats=concise', - sourceLocator = 'source-xblock-locator', - targetParentLocator = 'target-parent-xblock-locator', - sourceParentLocator = 'source-parent-xblock-locator'; - describe('MoveXBlockModal', function() { var modal, showModal, @@ -33,6 +17,11 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe display_name: DISPLAY_NAME, category: 'html' }), + sourceParentXBlockInfo: new XBlockInfo({ + id: 'PARENT_ID', + display_name: 'VERT 101', + category: 'vertical' + }), XBlockURLRoot: '/xblock', outlineURL: OUTLINE_URL, XBlockAncestorInfoURL: ANCESTORS_URL @@ -58,7 +47,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe showModal(); expect( modal.$el.find('.modal-header .title').contents().get(0).nodeValue.trim() - ).toEqual('Move: ' + sourceDisplayName); + ).toEqual('Move: ' + DISPLAY_NAME); expect( modal.$el.find('.modal-sr-title').text().trim() ).toEqual('Choose a location to move your component to'); @@ -72,7 +61,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe expect(modal.$el.find('.ui-loading.is-hidden')).not.toExist(); renderViewsSpy = spyOn(modal, 'renderViews'); expect(requests.length).toEqual(2); - AjaxHelpers.expectRequest(requests, 'GET', outlineUrl); + AjaxHelpers.expectRequest(requests, 'GET', OUTLINE_URL); AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.expectRequest(requests, 'GET', ANCESTORS_URL); AjaxHelpers.respondWithJson(requests, {}); @@ -88,204 +77,4 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work"); }); }); - - showModal = function() { - modal = new MoveXBlockModal({ - sourceXBlockInfo: new XBlockInfo({ - id: sourceLocator, - display_name: sourceDisplayName, - category: 'html' - }), - sourceParentXBlockInfo: new XBlockInfo({ - id: sourceParentLocator, - display_name: 'VERT 101', - category: 'vertical' - }), - XBlockUrlRoot: '/xblock', - outlineURL: outlineUrl - }); - modal.show(); - }; - - selectTargetParent = function(parentLocator) { - modal.moveXBlockListView = { - parent_info: { - parent: { - id: parentLocator - } - }, - remove: function() {} // attach a fake remove method - }; - }; - - getConfirmationFeedbackTitle = function(displayName) { - return StringUtils.interpolate( - 'Success! "{displayName}" has been moved.', - { - displayName: displayName - } - ); - }; - - getUndoConfirmationFeedbackTitle = function(displayName) { - return StringUtils.interpolate( - 'Move cancelled. "{sourceDisplayName}" has been moved back to its original location.', - { - sourceDisplayName: displayName - } - ); - }; - - getConfirmationFeedbackTitleHtml = function(parentLocator) { - return StringUtils.interpolate( - '{link_start}Take me to the new location{link_end}', - { - link_start: HtmlUtils.HTML(''), - link_end: HtmlUtils.HTML('') - } - ); - }; - - getConfirmationFeedbackMessageHtml = function(displayName, locator, parentLocator, sourceIndex) { - return HtmlUtils.interpolateHtml( - HtmlUtils.HTML( - '{undoMove}'), - { - displayName: displayName, - sourceLocator: locator, - parentSourceLocator: parentLocator, - targetIndex: sourceIndex, - undoMove: gettext('Undo move') - } - ); - }; - - verifyNotificationStatus = function(requests, notificationSpy, notificationText, sourceIndex) { - var sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare - ViewHelpers.verifyNotificationShowing(notificationSpy, notificationText); - AjaxHelpers.respondWithJson(requests, { - move_source_locator: sourceLocator, - parent_locator: sourceParentLocator, - target_index: sourceIndex - }); - ViewHelpers.verifyNotificationHidden(notificationSpy); - }; - - describe('Move an xblock', function() { - var sendMoveXBlockRequest, - moveXBlockWithSuccess; - - beforeEach(function() { - TemplateHelpers.installTemplates([ - 'basic-modal', - 'modal-button', - 'move-xblock-modal' - ]); - showModal(); - }); - - afterEach(function() { - modal.hide(); - }); - - sendMoveXBlockRequest = function(requests, xblockLocator, parentLocator, targetIndex, sourceIndex) { - var responseData, - expectedData, - sourceIndex = sourceIndex || 0, // eslint-disable-line no-redeclare - moveButton = modal.$el.find('.modal-actions .action-move')[sourceIndex]; - - // select a target item and click - selectTargetParent(parentLocator); - moveButton.click(); - - responseData = expectedData = { - move_source_locator: xblockLocator, - parent_locator: parentLocator - }; - - if (targetIndex !== undefined) { - expectedData = _.extend(expectedData, { - targetIndex: targetIndex - }); - } - - // verify content of request - AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData); - - // send the response - AjaxHelpers.respondWithJson(requests, _.extend(responseData, { - source_index: sourceIndex - })); - }; - - moveXBlockWithSuccess = function(requests) { - var sourceIndex = 0; - sendMoveXBlockRequest(requests, sourceLocator, targetParentLocator); - expect(modal.movedAlertView).toBeDefined(); - expect(modal.movedAlertView.options.title).toEqual(getConfirmationFeedbackTitle(sourceDisplayName)); - expect(modal.movedAlertView.options.titleHtml).toEqual( - getConfirmationFeedbackTitleHtml(targetParentLocator) - ); - expect(modal.movedAlertView.options.messageHtml).toEqual( - getConfirmationFeedbackMessageHtml( - sourceDisplayName, - sourceLocator, - sourceParentLocator, - sourceIndex - ) - ); - }; - - it('moves an xblock when move button is clicked', function() { - var requests = AjaxHelpers.requests(this); - moveXBlockWithSuccess(requests); - }); - - it('undo move an xblock when undo move button is clicked', function() { - var sourceIndex = 0, - requests = AjaxHelpers.requests(this); - moveXBlockWithSuccess(requests); - modal.movedAlertView.undoMoveXBlock({ - target: $(modal.movedAlertView.options.messageHtml.text) - }); - AjaxHelpers.respondWithJson(requests, { - move_source_locator: sourceLocator, - parent_locator: sourceParentLocator, - target_index: sourceIndex - }); - expect(modal.movedAlertView.movedAlertView.options.title).toEqual( - getUndoConfirmationFeedbackTitle(sourceDisplayName) - ); - }); - - it('does not move an xblock when cancel button is clicked', function() { - var sourceIndex = 0; - // select a target parent and click cancel button - selectTargetParent(targetParentLocator); - modal.$el.find('.modal-actions .action-cancel')[sourceIndex].click(); - expect(modal.movedAlertView).toBeNull(); - }); - - it('shows a notification when moving', function() { - var requests = AjaxHelpers.requests(this), - notificationSpy = ViewHelpers.createNotificationSpy(); - // select a target item and click on move - selectTargetParent(targetParentLocator); - modal.$el.find('.modal-actions .action-move').click(); - verifyNotificationStatus(requests, notificationSpy, 'Moving'); - }); - - it('shows a notification when undo moving', function() { - var notificationSpy, - requests = AjaxHelpers.requests(this); - moveXBlockWithSuccess(requests); - notificationSpy = ViewHelpers.createNotificationSpy(); - modal.movedAlertView.undoMoveXBlock({ - target: $(modal.movedAlertView.options.messageHtml.text) - }); - verifyNotificationStatus(requests, notificationSpy, 'Undo moving'); - }); - }); }); diff --git a/cms/static/js/spec/views/move_xblock_spec.js b/cms/static/js/spec/views/move_xblock_spec.js index 2df7c7ebc8..19acf80381 100644 --- a/cms/static/js/spec/views/move_xblock_spec.js +++ b/cms/static/js/spec/views/move_xblock_spec.js @@ -1,14 +1,20 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', 'js/views/move_xblock_list', - 'js/views/move_xblock_breadcrumb', 'js/models/xblock_info'], - function($, _, AjaxHelpers, TemplateHelpers, MoveXBlockListView, MoveXBlockBreadcrumbView, - XBlockInfoModel) { + 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/view_helpers', + 'js/views/modals/move_xblock_modal', 'edx-ui-toolkit/js/utils/html-utils', + 'edx-ui-toolkit/js/utils/string-utils', 'js/models/xblock_info'], + function($, _, AjaxHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, HtmlUtils, StringUtils, XBlockInfo) { 'use strict'; describe('MoveXBlock', function() { - var renderViews, createXBlockInfo, createCourseOutline, moveXBlockBreadcrumbView, - moveXBlockListView, parentChildMap, categoryMap, createChildXBlockInfo, + var modal, showModal, renderViews, createXBlockInfo, createCourseOutline, courseOutlineOptions, + parentChildMap, categoryMap, createChildXBlockInfo, xblockAncestorInfo, courseOutline, verifyBreadcrumbViewInfo, verifyListViewInfo, getDisplayedInfo, clickForwardButton, - clickBreadcrumbButton, verifyXBlockInfo, nextCategory; + clickBreadcrumbButton, verifyXBlockInfo, nextCategory, verifyMoveEnabled, getSentRequests, + verifyNotificationStatus, sendMoveXBlockRequest, moveXBlockWithSuccess, + verifyConfirmationFeedbackTitleHtml, verifyConfirmationFeedbackRedirectLinkHtml, + verifyUndoConfirmationFeedbackTitleHtml, verifyConfirmationFeedbackUndoMoveActionHtml, + sourceDisplayName = 'component_display_name_0', + sourceLocator = 'component_ID_0', + sourceParentLocator = 'unit_ID_0'; parentChildMap = { course: 'section', @@ -24,21 +30,71 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe component: 'component' }; + courseOutlineOptions = { + section: 2, + subsection: 2, + unit: 2, + component: 2 + }; + + xblockAncestorInfo = { + ancestors: [ + { + category: 'vertical', + display_name: 'unit_display_name_0', + id: 'unit_ID_0' + }, + { + category: 'sequential', + display_name: 'subsection_display_name_0', + id: 'subsection_ID_0' + }, + { + category: 'chapter', + display_name: 'section_display_name_0', + id: 'section_ID_0' + }, + { + category: 'course', + display_name: 'Demo Course', + id: 'COURSE_ID_101' + } + ] + }; + beforeEach(function() { - setFixtures( - "
" - ); + setFixtures("
"); TemplateHelpers.installTemplates([ - 'move-xblock-list', - 'move-xblock-breadcrumb' + 'basic-modal', + 'modal-button', + 'move-xblock-modal' ]); + courseOutline = createCourseOutline(courseOutlineOptions); + showModal(); }); afterEach(function() { - moveXBlockBreadcrumbView.remove(); - moveXBlockListView.remove(); + modal.hide(); + courseOutline = null; }); + showModal = function() { + modal = new MoveXBlockModal({ + sourceXBlockInfo: new XBlockInfo({ + id: sourceLocator, + display_name: sourceDisplayName, + category: 'component' + }), + sourceParentXBlockInfo: new XBlockInfo({ + id: sourceParentLocator, + display_name: 'unit_display_name_0', + category: 'vertical' + }), + XBlockUrlRoot: '/xblock' + }); + modal.show(); + }; + /** * Create child XBlock info. * @@ -51,7 +107,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe var childInfo = { category: categoryMap[category], display_name: category + '_display_name_' + xblockIndex, - id: category + '_ID' + id: category + '_ID_' + xblockIndex }; return createXBlockInfo(parentChildMap[category], outlineOptions, childInfo); }; @@ -93,12 +149,12 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe * @returns {Object} */ createCourseOutline = function(outlineOptions) { - var courseOutline = { + var courseXBlockInfo = { category: 'course', display_name: 'Demo Course', id: 'COURSE_ID_101' }; - return createXBlockInfo('section', outlineOptions, courseOutline); + return createXBlockInfo('section', outlineOptions, courseXBlockInfo); }; /** @@ -108,13 +164,8 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe * @param {any} ancestorInfo ancestors info */ renderViews = function(courseOutlineInfo, ancestorInfo) { - moveXBlockBreadcrumbView = new MoveXBlockBreadcrumbView({}); - moveXBlockListView = new MoveXBlockListView( - { - model: new XBlockInfoModel(courseOutlineInfo, {parse: true}), - ancestorInfo: ancestorInfo || {ancestors: []} - } - ); + var ancestorInfo = ancestorInfo || {ancestors: []}; // eslint-disable-line no-redeclare + modal.renderViews(courseOutlineInfo, ancestorInfo); }; /** @@ -123,7 +174,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe * @returns {Object} */ getDisplayedInfo = function() { - var viewEl = moveXBlockListView.$el; + var viewEl = modal.moveXBlockListView.$el; return { categoryText: viewEl.find('.category-text').text().trim(), currentLocationText: viewEl.find('.current-location').text().trim(), @@ -147,7 +198,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe */ verifyListViewInfo = function(category, expectedXBlocksCount, hasCurrentLocation) { var displayedInfo = getDisplayedInfo(); - expect(displayedInfo.categoryText).toEqual(moveXBlockListView.categoriesText[category] + ':'); + expect(displayedInfo.categoryText).toEqual(modal.moveXBlockListView.categoriesText[category] + ':'); expect(displayedInfo.xblockCount).toEqual(expectedXBlocksCount); expect(displayedInfo.xblockDisplayNames).toEqual( _.map(_.range(expectedXBlocksCount), function(xblockIndex) { @@ -174,7 +225,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe * @param {any} xblockIndex XBlock index */ verifyBreadcrumbViewInfo = function(category, xblockIndex) { - var displayedBreadcrumbs = moveXBlockBreadcrumbView.$el.find('.breadcrumbs .bc-container').map( + var displayedBreadcrumbs = modal.moveXBlockBreadcrumbView.$el.find('.breadcrumbs .bc-container').map( function() { return $(this).text().trim(); } ).get(), categories = _.keys(parentChildMap).concat(['component']), @@ -195,14 +246,14 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe */ clickForwardButton = function(buttonIndex) { buttonIndex = buttonIndex || 0; // eslint-disable-line no-param-reassign - moveXBlockListView.$el.find('[data-item-index="' + buttonIndex + '"] button').click(); + modal.moveXBlockListView.$el.find('[data-item-index="' + buttonIndex + '"] button').click(); }; /** * Click on last clickable breadcrumb button. */ clickBreadcrumbButton = function() { - moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); }; /** @@ -231,6 +282,7 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe verifyListViewInfo(category, expectedXBlocksCount, hasCurrentLocation); verifyBreadcrumbViewInfo(category, buttonIndex); + verifyMoveEnabled(category, hasCurrentLocation); if (direction === 'forward') { if (category === 'component') { @@ -248,13 +300,162 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe verifyXBlockInfo(outlineOptions, category, buttonIndex, direction, hasCurrentLocation); }; + /** + * Verify move button is enabled. + * + * @param {String} category XBlock category + * @param {String} hasCurrentLocation do we need to check current location + */ + verifyMoveEnabled = function(category, hasCurrentLocation) { + var isMoveEnabled = !modal.$el.find('.modal-actions .action-move').hasClass('is-disabled'); + if (category === 'component' && !hasCurrentLocation) { + expect(isMoveEnabled).toBeTruthy(); + } else { + expect(isMoveEnabled).toBeFalsy(); + } + }; + + /** + * Verify notification status. + * + * @param {Object} requests requests object + * @param {Object} notificationSpy notification spy + * @param {String} notificationText notification text to be verified + * @param {Integer} sourceIndex source index of the xblock + */ + verifyNotificationStatus = function(requests, notificationSpy, notificationText, sourceIndex) { + var sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare + ViewHelpers.verifyNotificationShowing(notificationSpy, notificationText); + AjaxHelpers.respondWithJson(requests, { + move_source_locator: sourceLocator, + parent_locator: sourceParentLocator, + target_index: sourceIndex + }); + ViewHelpers.verifyNotificationHidden(notificationSpy); + }; + + /** + * Send move xblock request. + * + * @param {Object} requests requests object + * @param {Object} xblockLocator Xblock id location + * @param {Integer} targetIndex target index of the xblock + * @param {Integer} sourceIndex source index of the xblock + */ + sendMoveXBlockRequest = function(requests, xblockLocator, targetIndex, sourceIndex) { + var responseData, + expectedData, + sourceIndex = sourceIndex || 0; // eslint-disable-line no-redeclare + + responseData = expectedData = { + move_source_locator: xblockLocator, + parent_locator: modal.targetParentXBlockInfo.id + }; + + if (targetIndex !== undefined) { + expectedData = _.extend(expectedData, { + targetIndex: targetIndex + }); + } + + // verify content of request + AjaxHelpers.expectJsonRequest(requests, 'PATCH', '/xblock/', expectedData); + + // send the response + AjaxHelpers.respondWithJson(requests, _.extend(responseData, { + source_index: sourceIndex + })); + }; + + /** + * Move xblock with success. + * + * @param {Object} requests requests object + */ + moveXBlockWithSuccess = function(requests) { + // select a target item and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); + }); + modal.$el.find('.modal-actions .action-move').click(); + sendMoveXBlockRequest(requests, sourceLocator); + expect(modal.movedAlertView).toBeDefined(); + verifyConfirmationFeedbackTitleHtml(sourceDisplayName); + verifyConfirmationFeedbackRedirectLinkHtml(); + verifyConfirmationFeedbackUndoMoveActionHtml(); + }; + + /** + * Verify success banner message html has correct title html. + * + * @param {String} displayName XBlock display name + */ + verifyConfirmationFeedbackTitleHtml = function(displayName) { + expect(modal.movedAlertView.$el.find('.title').html().trim()) + .toEqual(StringUtils.interpolate('Success! "{displayName}" has been moved.', + { + displayName: displayName + }) + ); + }; + + /** + * Verify undo success banner message html has correct title html. + * + * @param {String} displayName XBlock display name + */ + verifyUndoConfirmationFeedbackTitleHtml = function(displayName) { + expect(modal.movedAlertView.$el.find('.title').html()).toEqual( + StringUtils.interpolate( + 'Move cancelled. "{sourceDisplayName}" has been moved back to its original location.', + { + sourceDisplayName: displayName + } + ) + ); + }; + + /** + * Verify success banner message html has correct redirect link html. + */ + verifyConfirmationFeedbackRedirectLinkHtml = function() { + expect(modal.movedAlertView.$el.find('.copy').html().indexOf( + HtmlUtils.HTML( + '' + ) !== -1 + )).toBeTruthy(); + }; + + /** + * Verify success banner message html has correct undo move button html. + */ + verifyConfirmationFeedbackUndoMoveActionHtml = function() { + expect(modal.movedAlertView.$el.find('.copy').html().indexOf( + HtmlUtils.HTML( + '' + ) !== -1 + )).toBeTruthy(); + }; + + /** + * Get sent requests. + * + * @returns {Object} + */ + getSentRequests = function() { + return jasmine.Ajax.requests.filter(function(request) { + return request.readyState > 0; + }); + }; + it('renders views with correct information', function() { var outlineOptions = {section: 1, subsection: 1, unit: 1, component: 1}, outline = createCourseOutline(outlineOptions); - renderViews(outline); - verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', false); - verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', false); + renderViews(outline, xblockAncestorInfo); + verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); + verifyXBlockInfo(outlineOptions, 'component', 0, 'backward', true); }); it('shows correct behavior on breadcrumb navigation', function() { @@ -268,43 +469,18 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe _.each(['component', 'unit', 'subsection', 'section'], function(category) { verifyListViewInfo(category, 1); if (category !== 'section') { - moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').last().click(); } }); }); it('shows the correct current location', function() { var outlineOptions = {section: 2, subsection: 2, unit: 2, component: 2}, - outline = createCourseOutline(outlineOptions), - ancestorInfo = { - ancestors: [ - { - category: 'vertical', - display_name: 'unit_display_name_0', - id: 'unit_ID' - }, - { - category: 'sequential', - display_name: 'subsection_display_name_0', - id: 'subsection_ID' - }, - { - category: 'chapter', - display_name: 'section_display_name_0', - id: 'section_ID' - }, - { - category: 'course', - display_name: 'Demo Course', - id: 'COURSE_ID_101' - } - ] - }; - - renderViews(outline, ancestorInfo); + outline = createCourseOutline(outlineOptions); + renderViews(outline, xblockAncestorInfo); verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true); // click the outline breadcrumb to render sections - moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click(); + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click(); verifyXBlockInfo(outlineOptions, 'section', 1, 'forward', false); }); @@ -336,9 +512,114 @@ define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe _.each(_.range(info.forwardClicks), function() { clickForwardButton(); }); - expect(moveXBlockListView.$el.find('.xblock-no-child-message').text().trim()).toEqual(info.message); - moveXBlockListView.undelegateEvents(); - moveXBlockBreadcrumbView.undelegateEvents(); + expect(modal.moveXBlockListView.$el.find('.xblock-no-child-message').text().trim()) + .toEqual(info.message); + modal.moveXBlockListView.undelegateEvents(); + modal.moveXBlockBreadcrumbView.undelegateEvents(); + }); + }); + + describe('Move an xblock', function() { + it('can not move in a disabled state', function() { + verifyMoveEnabled(false); + modal.$el.find('.modal-actions .action-move').click(); + expect(modal.movedAlertView).toBeNull(); + expect(getSentRequests().length).toEqual(0); + }); + + it('move button is disabled when navigating to same parent', function() { + // select a target parent as the same as source parent and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(0); + }); + verifyMoveEnabled('component', true); + }); + + it('move button is enabled when navigating to different parent', function() { + // select a target parent as the different as source parent and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); + }); + verifyMoveEnabled('component', false); + }); + + it('verify move state while navigating', function() { + renderViews(courseOutline, xblockAncestorInfo); + verifyXBlockInfo(courseOutlineOptions, 'section', 0, 'forward', true); + // start from course outline again + modal.moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click(); + verifyXBlockInfo(courseOutlineOptions, 'section', 1, 'forward', false); + }); + + it('move an xblock when move button is clicked', function() { + var requests = AjaxHelpers.requests(this); + moveXBlockWithSuccess(requests); + }); + + it('do not move an xblock when cancel button is clicked', function() { + modal.$el.find('.modal-actions .action-cancel').click(); + expect(modal.movedAlertView).toBeNull(); + expect(getSentRequests().length).toEqual(0); + }); + + it('undo move an xblock when undo move link is clicked', function() { + var sourceIndex = 0, + requests = AjaxHelpers.requests(this); + moveXBlockWithSuccess(requests); + modal.movedAlertView.$el.find('.action-save').click(); + AjaxHelpers.respondWithJson(requests, { + move_source_locator: sourceLocator, + parent_locator: sourceParentLocator, + target_index: sourceIndex + }); + verifyUndoConfirmationFeedbackTitleHtml(sourceDisplayName); + }); + }); + + describe('shows a notification', function() { + it('mini operation message when moving an xblock', function() { + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy(); + // navigate to a target parent and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); + }); + modal.$el.find('.modal-actions .action-move').click(); + verifyNotificationStatus(requests, notificationSpy, 'Moving'); + }); + + it('mini operation message when undo moving an xblock', function() { + var notificationSpy, + requests = AjaxHelpers.requests(this); + moveXBlockWithSuccess(requests); + notificationSpy = ViewHelpers.createNotificationSpy(); + modal.movedAlertView.$el.find('.action-save').click(); + verifyNotificationStatus(requests, notificationSpy, 'Undo moving'); + }); + + it('error message when move request fails', function() { + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy('Error'); + // select a target item and click + renderViews(courseOutline); + _.each(_.range(3), function() { + clickForwardButton(1); + }); + modal.$el.find('.modal-actions .action-move').click(); + AjaxHelpers.respondWithError(requests); + ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work"); + }); + + it('error message when undo move request fails', function() { + var requests = AjaxHelpers.requests(this), + notificationSpy = ViewHelpers.createNotificationSpy('Error'); + moveXBlockWithSuccess(requests); + modal.movedAlertView.$el.find('.action-save').click(); + AjaxHelpers.respondWithError(requests); + ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work"); }); }); }); diff --git a/cms/static/js/views/modals/move_xblock_modal.js b/cms/static/js/views/modals/move_xblock_modal.js index acb45caf3f..f6d1d3fb7e 100644 --- a/cms/static/js/views/modals/move_xblock_modal.js +++ b/cms/static/js/views/modals/move_xblock_modal.js @@ -2,25 +2,29 @@ * The MoveXblockModal to move XBlocks in course. */ define([ - 'jquery', 'backbone', 'underscore', 'gettext', - 'js/views/baseview', 'js/views/modals/base_modal', - 'js/models/xblock_info', 'js/views/move_xblock_list', 'js/views/move_xblock_breadcrumb', - 'common/js/components/views/feedback', + 'jquery', + 'backbone', + 'underscore', + 'gettext', + 'js/views/baseview', 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils', 'edx-ui-toolkit/js/utils/string-utils', + 'common/js/components/views/feedback', + 'js/models/xblock_info', + 'js/views/modals/base_modal', + 'js/views/move_xblock_list', + 'js/views/move_xblock_breadcrumb', 'text!templates/move-xblock-modal.underscore' ], -function($, Backbone, _, gettext, BaseView, BaseModal, XBlockInfoModel, MoveXBlockListView, MoveXBlockBreadcrumbView, - Feedback, XBlockViewUtils, MoveXBlockUtils, HtmlUtils, StringUtils, MoveXblockModalTemplate) { +function($, Backbone, _, gettext, BaseView, XBlockViewUtils, MoveXBlockUtils, HtmlUtils, StringUtils, Feedback, + XBlockInfoModel, BaseModal, MoveXBlockListView, MoveXBlockBreadcrumbView, MoveXblockModalTemplate) { 'use strict'; var MoveXblockModal = BaseModal.extend({ - modalSRTitle: gettext('Choose a location to move your component to'), - events: _.extend({}, BaseModal.prototype.events, { - 'click .action-move': 'moveXBlock' + 'click .action-move:not(.is-disabled)': 'moveXBlock' }), options: $.extend({}, BaseModal.prototype.options, { @@ -40,6 +44,7 @@ function($, Backbone, _, gettext, BaseView, BaseModal, XBlockInfoModel, MoveXBlo this.listenTo(Backbone, 'move:breadcrumbRendered', this.focusModal); this.sourceXBlockInfo = this.options.sourceXBlockInfo; this.sourceParentXBlockInfo = this.options.sourceParentXBlockInfo; + this.targetParentXBlockInfo = null; this.XBlockURLRoot = this.options.XBlockURLRoot; this.XBlockAncestorInfoURL = StringUtils.interpolate( '{urlRoot}/{usageId}?fields=ancestorInfo', @@ -52,10 +57,9 @@ function($, Backbone, _, gettext, BaseView, BaseModal, XBlockInfoModel, MoveXBlo $('.breadcrumb-container').removeClass('is-hidden'); self.renderViews(courseOutlineInfo, ancestorInfo); }); - this.targetParentXBlockInfo = null; this.movedAlertView = null; - this.moveXBlockBreadcrumbView = null; - this.moveXBlockListView = null; + this.isValidMove = false; + this.listenTo(Backbone, 'move:enableMoveOperation', this.enableMoveOperation); }, getTitle: function() { @@ -71,7 +75,8 @@ function($, Backbone, _, gettext, BaseView, BaseModal, XBlockInfoModel, MoveXBlo show: function() { BaseModal.prototype.show.apply(this, [false]); - Feedback.prototype.inFocus.apply(this, [this.options.modalWindowClass]); + this.updateMoveState(false); + MoveXBlockUtils.hideMovedNotification(); }, hide: function() { @@ -122,50 +127,51 @@ function($, Backbone, _, gettext, BaseView, BaseModal, XBlockInfoModel, MoveXBlo ); }, + updateMoveState: function(isValidMove) { + var $moveButton = this.$el.find('.action-move'); + if (isValidMove) { + $moveButton.removeClass('is-disabled'); + } else { + $moveButton.addClass('is-disabled'); + } + }, + + enableMoveOperation: function(targetParentXBlockInfo) { + var isValidMove = false, + sourceParentType = this.sourceParentXBlockInfo.get('category'), + targetParentType = targetParentXBlockInfo.get('category'); + + if (targetParentType === sourceParentType && this.sourceParentXBlockInfo.id !== targetParentXBlockInfo.id) { + isValidMove = true; + this.targetParentXBlockInfo = targetParentXBlockInfo; + } + this.updateMoveState(isValidMove); + }, + moveXBlock: function() { var self = this; - XBlockViewUtils.moveXBlock(self.sourceXBlockInfo.id, self.moveXBlockListView.parent_info.parent.id) - .done(function(response) { - if (response.move_source_locator) { - // hide modal - self.hide(); - // hide xblock element - $("li.studio-xblock-wrapper[data-locator='" + self.sourceXBlockInfo.id + "']").hide(); - if (self.movedAlertView) { - self.movedAlertView.hide(); + XBlockViewUtils.moveXBlock(self.sourceXBlockInfo.id, self.targetParentXBlockInfo.id) + .done(function(response) { + // hide modal + self.hide(); + // hide xblock element + $("li.studio-xblock-wrapper[data-locator='" + self.sourceXBlockInfo.id + "']").hide(); + self.movedAlertView = MoveXBlockUtils.showMovedNotification( + StringUtils.interpolate( + gettext('Success! "{displayName}" has been moved.'), + { + displayName: self.sourceXBlockInfo.get('display_name') } - self.movedAlertView = MoveXBlockUtils.showMovedNotification( - StringUtils.interpolate( - gettext('Success! "{displayName}" has been moved.'), - { - displayName: self.sourceXBlockInfo.get('display_name') - } - ), - StringUtils.interpolate( - gettext('{link_start}Take me to the new location{link_end}'), - { - link_start: HtmlUtils.HTML(''), - link_end: HtmlUtils.HTML('') - } - ), - HtmlUtils.interpolateHtml( - HtmlUtils.HTML( - '{undoMove}' - ), - { - displayName: self.sourceXBlockInfo.get('display_name'), - sourceLocator: self.sourceXBlockInfo.id, - sourceParentLocator: self.sourceParentXBlockInfo.id, - targetIndex: response.source_index, - undoMove: gettext('Undo move') - } - ) - ); + ), + { + sourceDisplayName: self.sourceXBlockInfo.get('display_name'), + sourceLocator: self.sourceXBlockInfo.id, + sourceParentLocator: self.sourceParentXBlockInfo.id, + targetParentLocator: response.parent_locator, + targetIndex: response.source_index } - }); + ); + }); } }); diff --git a/cms/static/js/views/move_xblock_list.js b/cms/static/js/views/move_xblock_list.js index 4e302de97f..5815c9bd18 100644 --- a/cms/static/js/views/move_xblock_list.js +++ b/cms/static/js/views/move_xblock_list.js @@ -63,6 +63,7 @@ function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBloc ) ); Backbone.trigger('move:childrenRendered', this.breadcrumbInfo()); + Backbone.trigger('move:enableMoveOperation', this.parentInfo.parent); return this; }, diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 1f89554e3a..6334ace618 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -198,7 +198,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j modal = new MoveXBlockModal({ sourceXBlockInfo: XBlockUtils.findXBlockInfo(xblockElement, this.model), sourceParentXBlockInfo: XBlockUtils.findXBlockInfo(parentXBlockElement, this.model), - XBlockUrlRoot: this.getURLRoot(), + XBlockURLRoot: this.getURLRoot(), outlineURL: this.options.outlineURL }); diff --git a/cms/static/js/views/utils/move_xblock_utils.js b/cms/static/js/views/utils/move_xblock_utils.js index 22ee26fb9c..820c8abc59 100644 --- a/cms/static/js/views/utils/move_xblock_utils.js +++ b/cms/static/js/views/utils/move_xblock_utils.js @@ -1,69 +1,106 @@ /** * Provides utilities for move xblock. */ -define(['jquery', 'underscore', 'common/js/components/views/feedback_alert', 'js/views/utils/xblock_utils', - 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/string-utils'], - function($, _, AlertView, XBlockViewUtils, MoveXBlockUtils, StringUtils) { - 'use strict'; - var MovedAlertView, showMovedNotification; +define([ + 'jquery', + 'underscore', + 'common/js/components/views/feedback', + 'common/js/components/views/feedback_alert', + 'js/views/utils/xblock_utils', + 'js/views/utils/move_xblock_utils', + 'edx-ui-toolkit/js/utils/string-utils' +], +function($, _, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils, StringUtils) { + 'use strict'; + var redirectLink, undoMoveXBlock, showMovedNotification, hideMovedNotification; - MovedAlertView = AlertView.Confirmation.extend({ - events: _.extend({}, AlertView.Confirmation.prototype.events, { - 'click .action-undo-move': 'undoMoveXBlock' - }), + redirectLink = function(link) { + window.location.href = link; + }; - options: $.extend({}, AlertView.Confirmation.prototype.options), - - initialize: function() { - AlertView.prototype.initialize.apply(this, arguments); - this.movedAlertView = null; - }, - - undoMoveXBlock: function(event) { - var self = this, - $moveButton = $(event.target), - sourceLocator = $moveButton.data('source-locator'), - sourceDisplayName = $moveButton.data('source-display-name'), - sourceParentLocator = $moveButton.data('source-parent-locator'), - targetIndex = $moveButton.data('target-index'); - XBlockViewUtils.moveXBlock(sourceLocator, sourceParentLocator, targetIndex) - .done(function(response) { - // show XBlock element - $('.studio-xblock-wrapper[data-locator="' + response.move_source_locator + '"]').show(); - if (self.movedAlertView) { - self.movedAlertView.hide(); + undoMoveXBlock = function(data) { + XBlockViewUtils.moveXBlock(data.sourceLocator, data.sourceParentLocator, data.targetIndex) + .done(function(response) { + // show XBlock element + $('.studio-xblock-wrapper[data-locator="' + response.move_source_locator + '"]').show(); + showMovedNotification( + StringUtils.interpolate( + gettext('Move cancelled. "{sourceDisplayName}" has been moved back to its original location.'), + { + sourceDisplayName: data.sourceDisplayName } - self.movedAlertView = showMovedNotification( - StringUtils.interpolate( - gettext('Move cancelled. "{sourceDisplayName}" has been moved back to its original ' + - 'location.'), - { - sourceDisplayName: sourceDisplayName - } - ) - ); - }); - } + ) + ); }); + }; - showMovedNotification = function(title, titleHtml, messageHtml) { - var movedAlertView = new MovedAlertView({ + showMovedNotification = function(title, data) { + var movedAlertView; + // data is provided when we click undo move button. + if (data) { + movedAlertView = new AlertView.Confirmation({ title: title, - titleHtml: titleHtml, - messageHtml: messageHtml, - maxShown: 10000 + actions: { + primary: { + text: gettext('Undo move'), + class: 'action-save', + data: JSON.stringify({ + sourceDisplayName: data.sourceDisplayName, + sourceLocator: data.sourceLocator, + sourceParentLocator: data.sourceParentLocator, + targetIndex: data.targetIndex + }), + click: function() { + undoMoveXBlock( + { + sourceDisplayName: data.sourceDisplayName, + sourceLocator: data.sourceLocator, + sourceParentLocator: data.sourceParentLocator, + targetIndex: data.targetIndex + } + ); + } + }, + secondary: [ + { + text: gettext('Take me to the new location'), + class: 'action-cancel', + data: JSON.stringify({ + targetParentLocator: data.targetParentLocator + }), + click: function() { + redirectLink('/container/' + data.targetParentLocator); + } + } + ] + } }); - movedAlertView.show(); - // scroll to top - $.smoothScroll({ - offset: 0, - easing: 'swing', - speed: 1000 + } else { + movedAlertView = new AlertView.Confirmation({ + title: title }); - return movedAlertView; - }; + } + movedAlertView.show(); + // scroll to top + $.smoothScroll({ + offset: 0, + easing: 'swing', + speed: 1000 + }); + movedAlertView.$('.wrapper').first().focus(); + return movedAlertView; + }; - return { - showMovedNotification: showMovedNotification - }; - }); + hideMovedNotification = function() { + var movedAlertView = Feedback.active_alert; + if (movedAlertView) { + AlertView.prototype.hide.apply(movedAlertView); + } + }; + + return { + redirectLink: redirectLink, + showMovedNotification: showMovedNotification, + hideMovedNotification: hideMovedNotification + }; +}); diff --git a/cms/static/js/views/utils/xblock_utils.js b/cms/static/js/views/utils/xblock_utils.js index addf611bf7..860d055f3a 100644 --- a/cms/static/js/views/utils/xblock_utils.js +++ b/cms/static/js/views/utils/xblock_utils.js @@ -94,10 +94,10 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util /** * Moves the specified xblock in a new parent xblock. - * @param {String} sourceLocator The xblock element to be moved. - * @param {String} targetParentLocator Target parent xblock locator of the xblock to be moved, - * new moved xblock would be placed under this xblock. - * @param {String} targetIndex Intended index position of the xblock in parent xblock. If provided, + * @param {String} sourceLocator Locator of xblock element to be moved. + * @param {String} targetParentLocator Locator of the target parent xblock, moved xblock would be placed + * under this xblock. + * @param {Integer} targetIndex Intended index position of the xblock in parent xblock. If provided, * xblock would be placed at the particular index in the parent xblock. * @returns {jQuery promise} A promise representing the moving of the xblock. */ @@ -110,8 +110,8 @@ define(['jquery', 'underscore', 'gettext', 'common/js/components/utils/view_util move_source_locator: sourceLocator, parent_locator: targetParentLocator, target_index: targetIndex - }, function(data) { - moveOperation.resolve(data); + }, function(response) { + moveOperation.resolve(response); }) .fail(function() { moveOperation.reject(); diff --git a/cms/static/sass/elements/_modal-window.scss b/cms/static/sass/elements/_modal-window.scss index 893ac32d17..472c869a27 100644 --- a/cms/static/sass/elements/_modal-window.scss +++ b/cms/static/sass/elements/_modal-window.scss @@ -297,6 +297,11 @@ .ui-loading { box-shadow: none; } + + .modal-actions .action-move.is-disabled { + border: 1px solid $gray-l1 !important; + background: $gray-l1 !important; + } } // upload modal diff --git a/common/static/common/js/components/views/feedback.js b/common/static/common/js/components/views/feedback.js index 4551d522fc..901aaa61fa 100644 --- a/common/static/common/js/components/views/feedback.js +++ b/common/static/common/js/components/views/feedback.js @@ -21,8 +21,6 @@ options: { title: '', message: '', - titleHtml: '', // an optional html that comes after the title. - messageHtml: '', // an optional html that comes after the message. intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc type: null, // "alert", "notification", or "prompt": set by subclass shown: true, // is this view currently being shown? diff --git a/common/static/common/js/components/views/feedback_move.js b/common/static/common/js/components/views/feedback_move.js deleted file mode 100644 index 740521af1c..0000000000 --- a/common/static/common/js/components/views/feedback_move.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * The MovedAlertView to show confirmation message when moving XBlocks. - */ -(function(define) { - 'use strict'; - define(['jquery', 'underscore', 'common/js/components/views/feedback_alert', 'js/views/utils/xblock_utils', - 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/string-utils'], - function($, _, AlertView, XBlockViewUtils, MoveXBlockUtils, StringUtils) { - var MovedAlertView = AlertView.Confirmation.extend({ - events: _.extend({}, AlertView.Confirmation.prototype.events, { - 'click .action-undo-move': 'undoMoveXBlock' - }), - - options: $.extend({}, AlertView.Confirmation.prototype.options), - - initialize: function() { - AlertView.prototype.initialize.apply(this, arguments); - this.movedAlertView = null; - }, - - undoMoveXBlock: function(event) { - var self = this, - $moveButton = $(event.target), - sourceLocator = $moveButton.data('source-locator'), - sourceDisplayName = $moveButton.data('source-display-name'), - sourceParentLocator = $moveButton.data('source-parent-locator'), - targetIndex = $moveButton.data('target-index'); - XBlockViewUtils.moveXBlock(sourceLocator, sourceParentLocator, targetIndex) - .done(function(response) { - // show XBlock element - $('.studio-xblock-wrapper[data-locator="' + response.move_source_locator + '"]').show(); - if (self.movedAlertView) { - self.movedAlertView.hide(); - } - self.movedAlertView = MoveXBlockUtils.showMovedNotification( - StringUtils.interpolate( - gettext('Move cancelled. "{sourceDisplayName}" has been moved back to its original ' + - 'location.'), - { - sourceDisplayName: sourceDisplayName - } - ) - ); - }); - } - }); - return MovedAlertView; - }); -}).call(this, define || RequireJS.define); diff --git a/common/static/common/templates/components/system-feedback.underscore b/common/static/common/templates/components/system-feedback.underscore index 625bc574a1..9168abc9d5 100644 --- a/common/static/common/templates/components/system-feedback.underscore +++ b/common/static/common/templates/components/system-feedback.underscore @@ -15,9 +15,8 @@ <% } %>
-

<%- title %><% if(titleHtml) { %> <%= titleHtml %> <% } %>

+

<%- title %>

<% if(obj.message) { %>

<%- message %>

<% } %> - <% if(messageHtml) { %> <%= messageHtml %> <% } %>
<% if(obj.actions) { %>