Commit e9b8e17f by Mushtaq Ali

Enable disable move - TNL-6063

parent e856f07b
......@@ -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),
......
......@@ -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() {
......
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('<a href="/container/' + parentLocator + '">'),
link_end: HtmlUtils.HTML('</a>')
}
);
};
getConfirmationFeedbackMessageHtml = function(displayName, locator, parentLocator, sourceIndex) {
return HtmlUtils.interpolateHtml(
HtmlUtils.HTML(
'<a class="action-undo-move" href="#" data-source-display-name="{displayName}" ' +
'data-source-locator="{sourceLocator}" data-source-parent-locator="{parentSourceLocator}" ' +
'data-target-index="{targetIndex}">{undoMove}</a>'),
{
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');
});
});
});
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(
"<div class='breadcrumb-container'></div><div class='xblock-list-container'></div>"
);
setFixtures("<div id='page-alert'></div>");
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(
'<button class="action-secondary action-cancel">Take me to the new location</button>'
) !== -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(
'<button class="action-primary action-save">Undo Move</button>'
) !== -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");
});
});
});
......
......@@ -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('<a href="/container/' + response.parent_locator + '">'),
link_end: HtmlUtils.HTML('</a>')
}
),
HtmlUtils.interpolateHtml(
HtmlUtils.HTML(
'<a class="action-undo-move" href="#" data-source-display-name="{displayName}" ' +
'data-source-locator="{sourceLocator}" ' +
'data-source-parent-locator="{sourceParentLocator}" ' +
'data-target-index="{targetIndex}">{undoMove}</a>'
),
{
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
}
});
);
});
}
});
......
......@@ -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;
},
......
......@@ -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
});
......
/**
* 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;
};
hideMovedNotification = function() {
var movedAlertView = Feedback.active_alert;
if (movedAlertView) {
AlertView.prototype.hide.apply(movedAlertView);
}
};
return {
showMovedNotification: showMovedNotification
};
});
return {
redirectLink: redirectLink,
showMovedNotification: showMovedNotification,
hideMovedNotification: hideMovedNotification
};
});
......@@ -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();
......
......@@ -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
......
......@@ -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?
......
/**
* 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);
......@@ -15,9 +15,8 @@
<% } %>
<div class="copy">
<h2 class="title title-3" id="<%= type %>-<%= intent %>-title"><%- title %><% if(titleHtml) { %> <%= titleHtml %> <% } %></h2>
<h2 class="title title-3" id="<%= type %>-<%= intent %>-title"><%- title %></h2>
<% if(obj.message) { %><p class="message" id="<%= type %>-<%= intent %>-description"><%- message %></p><% } %>
<% if(messageHtml) { %> <%= messageHtml %> <% } %>
</div>
<% if(obj.actions) { %>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment