Commit b5726a68 by cahrens

Front-end work for duplicating components on the unit page.

STUD-1186
parent 58e6f60e
......@@ -9,6 +9,8 @@ Blades: Video player start-end time range is now shown even before Play is
clicked. Video player VCR time shows correct non-zero total time for YouTube
videos even before Play is clicked. BLD-529.
Studio: Add ability to duplicate components on the unit page.
Blades: Adds CookieStorage utility for video player that provides convenient
way to work with cookies.
......
......@@ -101,3 +101,14 @@ Feature: CMS.Component Adding
And I add a "Blank Advanced Problem" "Advanced Problem" component
And I delete all components
Then I see no components
Scenario: I can duplicate a component
Given I am in Studio editing a new unit
And I add a "Blank Common Problem" "Problem" component
And I add a "Multiple Choice" "Problem" component
And I duplicate the "0" component
Then I see a Problem component with display name "Duplicate of 'Blank Common Problem'" in position "1"
And I reload the page
Then I see a Problem component with display name "Blank Common Problem" in position "0"
And I see a Problem component with display name "Duplicate of 'Blank Common Problem'" in position "1"
And I see a Problem component with display name "Multiple Choice" in position "2"
......@@ -132,3 +132,19 @@ def delete_one_component(step):
def edit_and_save_component(step):
world.css_click('.edit-button')
world.css_click('.save-button')
@step(u'I duplicate the "([^"]*)" component$')
def duplicated_component(step, index):
duplicate_btn_css = 'a.duplicate-button'
world.css_click(duplicate_btn_css, int(index))
@step(u'I see a Problem component with display name "([^"]*)" in position "([^"]*)"$')
def see_component_in_position(step, display_name, index):
component_css = 'section.xmodule_CapaModule'
def find_problem(_driver):
return world.css_text(component_css, int(index)).startswith(display_name.upper())
world.wait_for(find_problem, timeout_msg='Did not find the duplicated problem')
......@@ -288,8 +288,8 @@ class TestDuplicateItem(ItemTest):
# Uses default display_name of 'Text' from HTML component.
verify_name(self.html_locator, self.seq_locator, "Duplicate of 'Text'")
# The sequence does not have a display_name set, so None gets included as the string 'None'.
verify_name(self.seq_locator, self.chapter_locator, "Duplicate of 'None'")
# The sequence does not have a display_name set, so category is shown.
verify_name(self.seq_locator, self.chapter_locator, "Duplicate of sequential")
# Now send a custom display name for the duplicate.
verify_name(self.seq_locator, self.chapter_locator, "customized name", display_name="customized name")
......
......@@ -321,7 +321,10 @@ def _duplicate_item(parent_location, duplicate_source_location, display_name=Non
if display_name is not None:
duplicate_metadata['display_name'] = display_name
else:
duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name)
if source_item.display_name is None:
duplicate_metadata['display_name'] = _("Duplicate of {0}").format(source_item.category)
else:
duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name)
get_modulestore(category).create_and_save_xmodule(
dest_location,
......
......@@ -214,6 +214,8 @@ define([
"js/spec/views/baseview_spec",
"js/spec/views/paging_spec",
"js/spec/views/unit_spec"
# these tests are run separate in the cms-squire suite, due to process
# isolation issues with Squire.js
# "coffee/spec/views/assets_spec"
......
define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
"js/views/feedback_notification", "js/views/metadata", "js/collections/metadata"
"js/utils/modal", "jquery.inputnumber", "xmodule"],
"js/utils/modal", "jquery.inputnumber", "xmodule", "coffee/src/main"],
(Backbone, $, _, gettext, XBlock, NotificationView, MetadataView, MetadataCollection, ModalUtils) ->
class ModuleEdit extends Backbone.View
tagName: 'li'
......@@ -62,7 +62,7 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
changedMetadata: ->
return _.extend(@metadataEditor.getModifiedMetadataValues(), @customMetadata())
createItem: (parent, payload) ->
createItem: (parent, payload, callback=->) ->
payload.parent_locator = parent
$.postJSON(
@model.urlRoot
......@@ -71,7 +71,7 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
@model.set(id: data.locator)
@$el.data('locator', data.locator)
@render()
)
).success(callback)
render: ->
if @model.id
......
......@@ -13,6 +13,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
'click .create-draft': 'createDraft'
'click .publish-draft': 'publishDraft'
'change .visibility-select': 'setVisibility'
"click .component-actions .duplicate-button": 'duplicateComponent'
initialize: =>
@visibilityView = new UnitEditView.Visibility(
......@@ -86,7 +87,7 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
@$newComponentItem.removeClass('adding')
@$newComponentItem.find('.rendered-component').remove()
saveNewComponent: (event) =>
createComponent: (event, data, message, success_callback) =>
event.preventDefault()
editor = new ModuleEditView(
......@@ -94,20 +95,52 @@ define ["jquery", "jquery.ui", "gettext", "backbone",
model: new ModuleModel()
)
@$newComponentItem.before(editor.$el)
notification = new NotificationView.Mini
title: gettext(message) + '…'
notification.show()
callback = ->
notification.hide()
success_callback()
analytics.track message,
course: course_location_analytics
unit_id: unit_location_analytics
type: editor.$el.data('locator')
editor.createItem(
@$el.data('locator'),
$(event.currentTarget).data()
data,
callback
)
analytics.track "Added a Component",
course: course_location_analytics
unit_id: unit_location_analytics
type: $(event.currentTarget).data('location')
return editor
saveNewComponent: (event) =>
success_callback = =>
@$newComponentItem.before(editor.$el)
editor = @createComponent(
event, $(event.currentTarget).data(),
"Adding",
success_callback
)
@closeNewComponent(event)
duplicateComponent: (event) =>
$component = $(event.currentTarget).parents('.component')
source_locator = $component.data('locator')
success_callback = ->
$component.after(editor.$el)
$('html, body').animate({
scrollTop: editor.$el.offset().top
}, 500)
editor = @createComponent(
event,
{duplicate_source_locator: source_locator},
"Duplicating",
success_callback
)
components: => @$('.component').map((idx, el) -> $(el).data('locator')).get()
wait: (value) =>
......
define(["coffee/src/views/unit", "js/models/module_info", "js/spec/create_sinon", "js/views/feedback_notification",
"jasmine-stealth"],
function (UnitEditView, ModuleModel, create_sinon, NotificationView) {
var verifyJSON = function (requests, json) {
var request = requests[requests.length - 1];
expect(request.url).toEqual("/xblock");
expect(request.method).toEqual("POST");
expect(request.requestBody).toEqual(json);
};
var verifyComponents = function (unit, locators) {
var components = unit.$(".component");
expect(components.length).toBe(locators.length);
for (var i=0; i < locators.length; i++) {
expect($(components[i]).data('locator')).toBe(locators[i]);
}
};
var verifyNotification = function (notificationSpy, text, requests) {
expect(notificationSpy.constructor).toHaveBeenCalled();
expect(notificationSpy.show).toHaveBeenCalled();
expect(notificationSpy.hide).not.toHaveBeenCalled();
var options = notificationSpy.constructor.mostRecentCall.args[0];
expect(options.title).toMatch(text);
create_sinon.respondWithJson(requests, {"locator": "new_item"});
expect(notificationSpy.hide).toHaveBeenCalled();
};
describe('duplicateComponent ', function () {
var unit;
var clickDuplicate = function (index) {
unit.$(".duplicate-button")[index].click();
};
beforeEach(function () {
setFixtures(
'<div class="main-wrapper edit-state-draft" data-locator="unit_locator"> \
<ol class="components"> \
<li class="component" data-locator="loc_1"> \
<div class="wrapper wrapper-component-editor"> \
</div> \
<div class="component-actions"> \
<a href="#" class="duplicate-button standard"><span class="duplicate-icon icon-copy"></span>Duplicate</a> \
</div> \
</li> \
<li class="component" data-locator="loc_2"> \
<div class="wrapper wrapper-component-editor"> \
</div> \
<div class="component-actions"> \
<a href="#" class="duplicate-button standard"><span class="duplicate-icon icon-copy"></span>Duplicate</a> \
</div> \
</li> \
</ol> \
'
);
unit = new UnitEditView({
el: $('.main-wrapper'),
model: new ModuleModel({
id: 'unit_locator',
state: 'draft'
})
});
});
it('sends the correct JSON to the server', function () {
var requests = create_sinon.requests(this);
clickDuplicate(0);
verifyJSON(requests, '{"duplicate_source_locator":"loc_1","parent_locator":"unit_locator"}');
});
it('inserts duplicated component immediately after source upon success and shows notification', function () {
var requests = create_sinon.requests(this);
clickDuplicate(0);
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
verifyComponents(unit, ['loc_1', 'duplicated_item', 'loc_2']);
});
it('inserts duplicated component at end if last duplicated', function () {
var requests = create_sinon.requests(this);
clickDuplicate(1);
create_sinon.respondWithJson(requests, {"locator": "duplicated_item"});
verifyComponents(unit, ['loc_1', 'loc_2', 'duplicated_item']);
});
it('shows a notification while duplicating', function () {
var notificationSpy = spyOnConstructor(NotificationView, "Mini", ["show", "hide"]);
notificationSpy.show.andReturn(notificationSpy);
var requests = create_sinon.requests(this);
clickDuplicate(0);
verifyNotification(notificationSpy, /Duplicating/, requests);
});
it('does not insert duplicated component upon failure', function () {
var server = create_sinon.server(500, this);
clickDuplicate(0);
server.respond();
verifyComponents(unit, ['loc_1', 'loc_2']);
});
});
describe('saveNewComponent ', function () {
var unit;
var clickNewComponent = function () {
unit.$(".new-component .new-component-type a.single-template").click();
};
beforeEach(function () {
setFixtures(
'<div class="main-wrapper edit-state-draft" data-locator="unit_locator"> \
<ol class="components"> \
<li class="component" data-locator="loc_1"> \
<div class="wrapper wrapper-component-editor"> \
</div> \
</li> \
<li class="component" data-locator="loc_2"> \
<div class="wrapper wrapper-component-editor"> \
</div> \
</li> \
<li class="new-component-item adding"> \
<div class="new-component"> \
<ul class="new-component-type"> \
<li> \
<a href="#" class="single-template" data-type="discussion" data-category="discussion"/> \
</li> \
</ul> \
</div> \
</li> \
</ol> \
'
);
unit = new UnitEditView({
el: $('.main-wrapper'),
model: new ModuleModel({
id: 'unit_locator',
state: 'draft'
})
});
});
it('sends the correct JSON to the server', function () {
var requests = create_sinon.requests(this);
clickNewComponent();
verifyJSON(requests, '{"category":"discussion","type":"discussion","parent_locator":"unit_locator"}');
});
it('inserts new component at end', function () {
var requests = create_sinon.requests(this);
clickNewComponent();
create_sinon.respondWithJson(requests, {"locator": "new_item"});
verifyComponents(unit, ['loc_1', 'loc_2', 'new_item']);
});
it('shows a notification while creating', function () {
var notificationSpy = spyOnConstructor(NotificationView, "Mini", ["show", "hide"]);
notificationSpy.show.andReturn(notificationSpy);
var requests = create_sinon.requests(this);
clickNewComponent();
verifyNotification(notificationSpy, /Adding/, requests);
});
it('does not insert duplicated component upon failure', function () {
var server = create_sinon.server(500, this);
clickNewComponent();
server.respond();
verifyComponents(unit, ['loc_1', 'loc_2']);
});
});
}
);
......@@ -689,7 +689,8 @@ hr.divide {
}
.edit-button.standard,
.delete-button.standard {
.delete-button.standard,
.duplicate-button.standard {
@extend %t-action4;
@include white-button;
float: left;
......@@ -698,7 +699,8 @@ hr.divide {
font-weight: 400;
.edit-icon,
.delete-icon {
.delete-icon,
.duplicate-icon{
margin-right: 4px;
}
}
......
......@@ -105,6 +105,13 @@
}
}
.duplicate-icon {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 2px;
}
.visibility-toggle {
.toggle-icon {
display: inline-block;
......
......@@ -163,6 +163,10 @@
margin-right: 12px;
}
}
.duplicate-button.standard {
display: none;
}
}
.edit-static-page {
......
......@@ -29,6 +29,7 @@
<div class="component-actions">
<a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
<a href="#" class="duplicate-button standard"><span class="duplicate-icon icon-copy"></span>${_("Duplicate")}</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
</div>
<span data-tooltip="${_("Drag to reorder")}" class="drag-handle"></span>
......
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