Move modal show course outline with breadcrumb
TNL-6060
This commit is contained in:
committed by
Mushtaq Ali
parent
bfeeeff708
commit
c7dc83c378
@@ -20,7 +20,7 @@ from xblock.exceptions import NoSuchHandlerError
|
||||
from xblock.plugin import PluginMissingError
|
||||
from xblock.runtime import Mixologist
|
||||
|
||||
from contentstore.utils import get_lms_link_for_item, get_xblock_aside_instance
|
||||
from contentstore.utils import get_lms_link_for_item, reverse_course_url, get_xblock_aside_instance
|
||||
from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name
|
||||
from contentstore.views.item import create_xblock_info, add_container_page_publishing_info, StudioEditModuleRuntime
|
||||
|
||||
@@ -165,6 +165,7 @@ def container_handler(request, usage_key_string):
|
||||
'subsection': subsection,
|
||||
'section': section,
|
||||
'new_unit_category': 'vertical',
|
||||
'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)),
|
||||
'ancestor_xblocks': ancestor_xblocks,
|
||||
'component_templates': component_templates,
|
||||
'xblock_info': xblock_info,
|
||||
|
||||
@@ -282,8 +282,9 @@
|
||||
'js/spec/views/pages/library_users_spec',
|
||||
'js/spec/views/modals/base_modal_spec',
|
||||
'js/spec/views/modals/edit_xblock_spec',
|
||||
'js/spec/views/modals/move_xblock_spec',
|
||||
'js/spec/views/modals/move_xblock_modal_spec',
|
||||
'js/spec/views/modals/validation_error_modal_spec',
|
||||
'js/spec/views/move_xblock_spec',
|
||||
'js/spec/views/settings/main_spec',
|
||||
'js/spec/factories/xblock_validation_spec',
|
||||
'js/certificates/spec/models/certificate_spec',
|
||||
|
||||
75
cms/static/js/spec/views/modals/move_xblock_modal_spec.js
Normal file
75
cms/static/js/spec/views/modals/move_xblock_modal_spec.js
Normal file
@@ -0,0 +1,75 @@
|
||||
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', 'js/models/xblock_info'],
|
||||
function($, _, AjaxHelpers, TemplateHelpers, ViewHelpers, MoveXBlockModal, XBlockInfo) {
|
||||
'use strict';
|
||||
describe('MoveXBlockModal', function() {
|
||||
var modal,
|
||||
showModal,
|
||||
DISPLAY_NAME = 'HTML 101',
|
||||
OUTLINE_URL = '/course/cid?format=concise',
|
||||
ANCESTORS_URL = '/xblock/USAGE_ID?fields=ancestorInfo';
|
||||
|
||||
showModal = function() {
|
||||
modal = new MoveXBlockModal({
|
||||
sourceXBlockInfo: new XBlockInfo({
|
||||
id: 'USAGE_ID',
|
||||
display_name: DISPLAY_NAME,
|
||||
category: 'html'
|
||||
}),
|
||||
XBlockURLRoot: '/xblock',
|
||||
outlineURL: OUTLINE_URL,
|
||||
XBlockAncestorInfoURL: ANCESTORS_URL
|
||||
|
||||
});
|
||||
modal.show();
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures('<div id="page-notification"></div><div id="reader-feedback"></div>');
|
||||
TemplateHelpers.installTemplates([
|
||||
'basic-modal',
|
||||
'modal-button',
|
||||
'move-xblock-modal'
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
modal.hide();
|
||||
});
|
||||
|
||||
it('rendered as expected', function() {
|
||||
showModal();
|
||||
expect(
|
||||
modal.$el.find('.modal-header .title').contents().get(0).nodeValue.trim()
|
||||
).toEqual('Move: ' + DISPLAY_NAME);
|
||||
expect(
|
||||
modal.$el.find('.modal-sr-title').text().trim()
|
||||
).toEqual('Choose a location to move your component to');
|
||||
expect(modal.$el.find('.modal-actions .action-primary.action-move').text()).toEqual('Move');
|
||||
});
|
||||
|
||||
it('sends request to fetch course outline', function() {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
renderViewsSpy;
|
||||
showModal();
|
||||
expect(modal.$el.find('.ui-loading.is-hidden')).not.toExist();
|
||||
renderViewsSpy = spyOn(modal, 'renderViews');
|
||||
expect(requests.length).toEqual(2);
|
||||
AjaxHelpers.expectRequest(requests, 'GET', OUTLINE_URL);
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
AjaxHelpers.expectRequest(requests, 'GET', ANCESTORS_URL);
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
expect(renderViewsSpy).toHaveBeenCalled();
|
||||
expect(modal.$el.find('.ui-loading.is-hidden')).toExist();
|
||||
});
|
||||
|
||||
it('shows error notification when fetch course outline request fails', function() {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
notificationSpy = ViewHelpers.createNotificationSpy('Error');
|
||||
showModal();
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
ViewHelpers.verifyNotificationShowing(notificationSpy, "Studio's having trouble saving your work");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
define(['jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers', 'js/views/modals/move_xblock_modal', 'js/models/xblock_info'],
|
||||
function($, _, AjaxHelpers, TemplateHelpers, MoveXBlockModal, XBlockInfo) {
|
||||
'use strict';
|
||||
describe('MoveXBlockModal', function() {
|
||||
var modal,
|
||||
showModal,
|
||||
DISPLAY_NAME = 'HTML 101';
|
||||
|
||||
showModal = function() {
|
||||
modal = new MoveXBlockModal({
|
||||
sourceXBlockInfo: new XBlockInfo({
|
||||
id: 'testCourse/branch/draft/block/verticalFFF',
|
||||
display_name: DISPLAY_NAME,
|
||||
category: 'html'
|
||||
}),
|
||||
XBlockUrlRoot: '/xblock'
|
||||
});
|
||||
modal.show();
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
TemplateHelpers.installTemplates([
|
||||
'basic-modal',
|
||||
'modal-button',
|
||||
'move-xblock-modal'
|
||||
]);
|
||||
showModal();
|
||||
});
|
||||
|
||||
it('rendered as expected', function() {
|
||||
expect(modal.$el.find('.modal-header .title').text()).toEqual('Move: ' + DISPLAY_NAME);
|
||||
expect(modal.$el.find('.modal-actions .action-primary.action-move').text()).toEqual('Move');
|
||||
});
|
||||
});
|
||||
});
|
||||
345
cms/static/js/spec/views/move_xblock_spec.js
Normal file
345
cms/static/js/spec/views/move_xblock_spec.js
Normal file
@@ -0,0 +1,345 @@
|
||||
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) {
|
||||
'use strict';
|
||||
describe('MoveXBlock', function() {
|
||||
var renderViews, createXBlockInfo, createCourseOutline, moveXBlockBreadcrumbView,
|
||||
moveXBlockListView, parentChildMap, categoryMap, createChildXBlockInfo,
|
||||
verifyBreadcrumbViewInfo, verifyListViewInfo, getDisplayedInfo, clickForwardButton,
|
||||
clickBreadcrumbButton, verifyXBlockInfo, nextCategory;
|
||||
|
||||
parentChildMap = {
|
||||
course: 'section',
|
||||
section: 'subsection',
|
||||
subsection: 'unit',
|
||||
unit: 'component'
|
||||
};
|
||||
|
||||
categoryMap = {
|
||||
section: 'chapter',
|
||||
subsection: 'sequential',
|
||||
unit: 'vertical',
|
||||
component: 'component'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures(
|
||||
"<div class='breadcrumb-container'></div><div class='xblock-list-container'></div>"
|
||||
);
|
||||
TemplateHelpers.installTemplates([
|
||||
'move-xblock-list',
|
||||
'move-xblock-breadcrumb'
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
moveXBlockBreadcrumbView.remove();
|
||||
moveXBlockListView.remove();
|
||||
});
|
||||
|
||||
/**
|
||||
* Create child XBlock info.
|
||||
*
|
||||
* @param {String} category XBlock category
|
||||
* @param {Object} outlineOptions options according to which outline was created
|
||||
* @param {Object} xblockIndex XBlock Index
|
||||
* @returns
|
||||
*/
|
||||
createChildXBlockInfo = function(category, outlineOptions, xblockIndex) {
|
||||
var childInfo = {
|
||||
category: categoryMap[category],
|
||||
display_name: category + '_display_name_' + xblockIndex,
|
||||
id: category + '_ID'
|
||||
};
|
||||
return createXBlockInfo(parentChildMap[category], outlineOptions, childInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create parent XBlock info.
|
||||
*
|
||||
* @param {String} category XBlock category
|
||||
* @param {Object} outlineOptions options according to which outline was created
|
||||
* @param {Object} outline ouline info being constructed
|
||||
* @returns {Object}
|
||||
*/
|
||||
createXBlockInfo = function(category, outlineOptions, outline) {
|
||||
var childInfo = {
|
||||
category: categoryMap[category],
|
||||
display_name: category,
|
||||
children: []
|
||||
},
|
||||
xblocks;
|
||||
|
||||
xblocks = outlineOptions[category];
|
||||
if (!xblocks) {
|
||||
return outline;
|
||||
}
|
||||
|
||||
outline.child_info = childInfo; // eslint-disable-line no-param-reassign
|
||||
_.each(_.range(xblocks), function(xblockIndex) {
|
||||
childInfo.children.push(
|
||||
createChildXBlockInfo(category, outlineOptions, xblockIndex)
|
||||
);
|
||||
});
|
||||
return outline;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create course outline.
|
||||
*
|
||||
* @param {Object} outlineOptions options according to which outline was created
|
||||
* @returns {Object}
|
||||
*/
|
||||
createCourseOutline = function(outlineOptions) {
|
||||
var courseOutline = {
|
||||
category: 'course',
|
||||
display_name: 'Demo Course',
|
||||
id: 'COURSE_ID_101'
|
||||
};
|
||||
return createXBlockInfo('section', outlineOptions, courseOutline);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render breadcrumb and XBlock list view.
|
||||
*
|
||||
* @param {any} courseOutlineInfo course outline info
|
||||
* @param {any} ancestorInfo ancestors info
|
||||
*/
|
||||
renderViews = function(courseOutlineInfo, ancestorInfo) {
|
||||
moveXBlockBreadcrumbView = new MoveXBlockBreadcrumbView({});
|
||||
moveXBlockListView = new MoveXBlockListView(
|
||||
{
|
||||
model: new XBlockInfoModel(courseOutlineInfo, {parse: true}),
|
||||
ancestorInfo: ancestorInfo || {ancestors: []}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract displayed XBlock list info.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getDisplayedInfo = function() {
|
||||
var viewEl = moveXBlockListView.$el;
|
||||
return {
|
||||
categoryText: viewEl.find('.category-text').text().trim(),
|
||||
currentLocationText: viewEl.find('.current-location').text().trim(),
|
||||
xblockCount: viewEl.find('.xblock-item').length,
|
||||
xblockDisplayNames: viewEl.find('.xblock-item .xblock-displayname').map(
|
||||
function() { return $(this).text().trim(); }
|
||||
).get(),
|
||||
forwardButtonSRTexts: viewEl.find('.xblock-item .forward-sr-text').map(
|
||||
function() { return $(this).text().trim(); }
|
||||
).get(),
|
||||
forwardButtonCount: viewEl.find('.fa-arrow-right.forward-sr-icon').length
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify displayed XBlock list info.
|
||||
*
|
||||
* @param {String} category XBlock category
|
||||
* @param {Integer} expectedXBlocksCount number of XBlock childs displayed
|
||||
* @param {Boolean} hasCurrentLocation do we need to check current location
|
||||
*/
|
||||
verifyListViewInfo = function(category, expectedXBlocksCount, hasCurrentLocation) {
|
||||
var displayedInfo = getDisplayedInfo();
|
||||
expect(displayedInfo.categoryText).toEqual(moveXBlockListView.categoriesText[category] + ':');
|
||||
expect(displayedInfo.xblockCount).toEqual(expectedXBlocksCount);
|
||||
expect(displayedInfo.xblockDisplayNames).toEqual(
|
||||
_.map(_.range(expectedXBlocksCount), function(xblockIndex) {
|
||||
return category + '_display_name_' + xblockIndex;
|
||||
})
|
||||
);
|
||||
if (category !== 'component') {
|
||||
if (hasCurrentLocation) {
|
||||
expect(displayedInfo.currentLocationText).toEqual('(Current location)');
|
||||
}
|
||||
expect(displayedInfo.forwardButtonSRTexts).toEqual(
|
||||
_.map(_.range(expectedXBlocksCount), function() {
|
||||
return 'Click for children';
|
||||
})
|
||||
);
|
||||
expect(displayedInfo.forwardButtonCount).toEqual(expectedXBlocksCount);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify rendered breadcrumb info.
|
||||
*
|
||||
* @param {any} category XBlock category
|
||||
* @param {any} xblockIndex XBlock index
|
||||
*/
|
||||
verifyBreadcrumbViewInfo = function(category, xblockIndex) {
|
||||
var displayedBreadcrumbs = moveXBlockBreadcrumbView.$el.find('.breadcrumbs .bc-container').map(
|
||||
function() { return $(this).text().trim(); }
|
||||
).get(),
|
||||
categories = _.keys(parentChildMap).concat(['component']),
|
||||
visitedCategories = categories.slice(0, _.indexOf(categories, category));
|
||||
|
||||
expect(displayedBreadcrumbs).toEqual(
|
||||
_.map(visitedCategories, function(visitedCategory) {
|
||||
return visitedCategory === 'course' ?
|
||||
'Course Outline' : visitedCategory + '_display_name_' + xblockIndex;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Click forward button in the list of displayed XBlocks.
|
||||
*
|
||||
* @param {any} buttonIndex forward button index
|
||||
*/
|
||||
clickForwardButton = function(buttonIndex) {
|
||||
buttonIndex = buttonIndex || 0; // eslint-disable-line no-param-reassign
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the parent or child category of current XBlock.
|
||||
*
|
||||
* @param {String} direction `forward` or `backward`
|
||||
* @param {String} category XBlock category
|
||||
* @returns {String}
|
||||
*/
|
||||
nextCategory = function(direction, category) {
|
||||
return direction === 'forward' ? parentChildMap[category] : _.invert(parentChildMap)[category];
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify renderd info of breadcrumbs and XBlock list.
|
||||
*
|
||||
* @param {Object} outlineOptions options according to which outline was created
|
||||
* @param {String} category XBlock category
|
||||
* @param {Integer} buttonIndex forward button index
|
||||
* @param {String} direction `forward` or `backward`
|
||||
* @param {String} hasCurrentLocation do we need to check current location
|
||||
* @returns
|
||||
*/
|
||||
verifyXBlockInfo = function(outlineOptions, category, buttonIndex, direction, hasCurrentLocation) {
|
||||
var expectedXBlocksCount = outlineOptions[category];
|
||||
|
||||
verifyListViewInfo(category, expectedXBlocksCount, hasCurrentLocation);
|
||||
verifyBreadcrumbViewInfo(category, buttonIndex);
|
||||
|
||||
if (direction === 'forward') {
|
||||
if (category === 'component') {
|
||||
return;
|
||||
}
|
||||
clickForwardButton(buttonIndex);
|
||||
} else if (direction === 'backward') {
|
||||
if (category === 'section') {
|
||||
return;
|
||||
}
|
||||
clickBreadcrumbButton();
|
||||
}
|
||||
category = nextCategory(direction, category); // eslint-disable-line no-param-reassign
|
||||
|
||||
verifyXBlockInfo(outlineOptions, category, buttonIndex, direction, hasCurrentLocation);
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('shows correct behavior on breadcrumb navigation', function() {
|
||||
var outline = createCourseOutline({section: 1, subsection: 1, unit: 1, component: 1});
|
||||
|
||||
renderViews(outline);
|
||||
_.each(_.range(3), function() {
|
||||
clickForwardButton();
|
||||
});
|
||||
|
||||
_.each(['component', 'unit', 'subsection', 'section'], function(category) {
|
||||
verifyListViewInfo(category, 1);
|
||||
if (category !== 'section') {
|
||||
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);
|
||||
verifyXBlockInfo(outlineOptions, 'section', 0, 'forward', true);
|
||||
// click the outline breadcrumb to render sections
|
||||
moveXBlockBreadcrumbView.$el.find('.bc-container button').first().click();
|
||||
verifyXBlockInfo(outlineOptions, 'section', 1, 'forward', false);
|
||||
});
|
||||
|
||||
it('shows correct message when parent has no children', function() {
|
||||
var outlinesInfo = [
|
||||
{
|
||||
outline: createCourseOutline({}),
|
||||
message: 'This course has no sections'
|
||||
},
|
||||
{
|
||||
outline: createCourseOutline({section: 1}),
|
||||
message: 'This section has no subsections',
|
||||
forwardClicks: 1
|
||||
},
|
||||
{
|
||||
outline: createCourseOutline({section: 1, subsection: 1}),
|
||||
message: 'This subsection has no units',
|
||||
forwardClicks: 2
|
||||
},
|
||||
{
|
||||
outline: createCourseOutline({section: 1, subsection: 1, unit: 1}),
|
||||
message: 'This unit has no components',
|
||||
forwardClicks: 3
|
||||
}
|
||||
];
|
||||
|
||||
_.each(outlinesInfo, function(info) {
|
||||
renderViews(info.outline);
|
||||
_.each(_.range(info.forwardClicks), function() {
|
||||
clickForwardButton();
|
||||
});
|
||||
expect(moveXBlockListView.$el.find('.xblock-no-child-message').text().trim()).toEqual(info.message);
|
||||
moveXBlockListView.undelegateEvents();
|
||||
moveXBlockBreadcrumbView.undelegateEvents();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@
|
||||
* button on the modal.
|
||||
* primaryActionButtonType: A string to be used as type for primary action button.
|
||||
* primaryActionButtonTitle: A string to be used as title for primary action button.
|
||||
* showEditorModeButtons: Whether to show editor mode button in the modal header.
|
||||
*/
|
||||
define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
|
||||
function($, _, gettext, BaseView) {
|
||||
@@ -41,7 +42,8 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
|
||||
viewSpecificClasses: '',
|
||||
addPrimaryActionButton: false,
|
||||
primaryActionButtonType: 'save',
|
||||
primaryActionButtonTitle: gettext('Save')
|
||||
primaryActionButtonTitle: gettext('Save'),
|
||||
showEditorModeButtons: true
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
@@ -66,6 +68,8 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
|
||||
type: this.options.modalType,
|
||||
size: this.options.modalSize,
|
||||
title: this.getTitle(),
|
||||
modalSRTitle: this.options.modalSRTitle,
|
||||
showEditorModeButtons: this.options.showEditorModeButtons,
|
||||
viewSpecificClasses: this.options.viewSpecificClasses
|
||||
}));
|
||||
this.addActionButtons();
|
||||
|
||||
@@ -4,27 +4,44 @@
|
||||
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',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'text!templates/move-xblock-modal.underscore'
|
||||
],
|
||||
function($, Backbone, _, gettext, BaseView, BaseModal, Feedback, StringUtils, MoveXblockModalTemplate) {
|
||||
function($, Backbone, _, gettext, BaseView, BaseModal, XBlockInfoModel, MoveXBlockListView, MoveXBlockBreadcrumbView,
|
||||
Feedback, StringUtils, MoveXblockModalTemplate) {
|
||||
'use strict';
|
||||
|
||||
var MoveXblockModal = BaseModal.extend({
|
||||
options: $.extend({}, BaseModal.prototype.options, {
|
||||
modalName: 'move-xblock',
|
||||
modalSize: 'med',
|
||||
modalSize: 'lg',
|
||||
showEditorModeButtons: false,
|
||||
addPrimaryActionButton: true,
|
||||
primaryActionButtonType: 'move',
|
||||
primaryActionButtonTitle: gettext('Move')
|
||||
viewSpecificClasses: 'move-modal',
|
||||
primaryActionButtonTitle: gettext('Move'),
|
||||
modalSRTitle: gettext('Choose a location to move your component to')
|
||||
}),
|
||||
|
||||
initialize: function() {
|
||||
var self = this;
|
||||
BaseModal.prototype.initialize.call(this);
|
||||
this.listenTo(Backbone, 'move:breadcrumbRendered', this.focusModal);
|
||||
this.sourceXBlockInfo = this.options.sourceXBlockInfo;
|
||||
this.XBlockUrlRoot = this.options.sourceXBlockInfo;
|
||||
this.XBlockURLRoot = this.options.XBlockURLRoot;
|
||||
this.XBlockAncestorInfoURL = StringUtils.interpolate(
|
||||
'{urlRoot}/{usageId}?fields=ancestorInfo',
|
||||
{urlRoot: this.XBlockURLRoot, usageId: this.sourceXBlockInfo.get('id')}
|
||||
);
|
||||
this.outlineURL = this.options.outlineURL;
|
||||
this.options.title = this.getTitle();
|
||||
this.fetchCourseOutline().done(function(courseOutlineInfo, ancestorInfo) {
|
||||
$('.ui-loading').addClass('is-hidden');
|
||||
$('.breadcrumb-container').removeClass('is-hidden');
|
||||
self.renderViews(courseOutlineInfo, ancestorInfo);
|
||||
});
|
||||
},
|
||||
|
||||
getTitle: function() {
|
||||
@@ -40,12 +57,54 @@ function($, Backbone, _, gettext, BaseView, BaseModal, Feedback, StringUtils, Mo
|
||||
|
||||
show: function() {
|
||||
BaseModal.prototype.show.apply(this, [false]);
|
||||
Feedback.prototype.inFocus.apply(this, [this.options.modalWindowClass]);
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
if (this.moveXBlockListView) {
|
||||
this.moveXBlockListView.remove();
|
||||
}
|
||||
if (this.moveXBlockBreadcrumbView) {
|
||||
this.moveXBlockBreadcrumbView.remove();
|
||||
}
|
||||
BaseModal.prototype.hide.apply(this);
|
||||
Feedback.prototype.outFocus.apply(this);
|
||||
},
|
||||
|
||||
focusModal: function() {
|
||||
Feedback.prototype.inFocus.apply(this, [this.options.modalWindowClass]);
|
||||
$(this.options.modalWindowClass).focus();
|
||||
},
|
||||
|
||||
fetchCourseOutline: function() {
|
||||
return $.when(
|
||||
this.fetchData(this.outlineURL),
|
||||
this.fetchData(this.XBlockAncestorInfoURL)
|
||||
);
|
||||
},
|
||||
|
||||
fetchData: function(url) {
|
||||
var deferred = $.Deferred();
|
||||
$.ajax({
|
||||
url: url,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
type: 'GET'
|
||||
}).done(function(data) {
|
||||
deferred.resolve(data);
|
||||
}).fail(function() {
|
||||
deferred.reject();
|
||||
});
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
renderViews: function(courseOutlineInfo, ancestorInfo) {
|
||||
this.moveXBlockBreadcrumbView = new MoveXBlockBreadcrumbView({});
|
||||
this.moveXBlockListView = new MoveXBlockListView(
|
||||
{
|
||||
model: new XBlockInfoModel(courseOutlineInfo, {parse: true}),
|
||||
ancestorInfo: ancestorInfo
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
52
cms/static/js/views/move_xblock_breadcrumb.js
Normal file
52
cms/static/js/views/move_xblock_breadcrumb.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* MoveXBlockBreadcrumb show breadcrumbs to move back to parent.
|
||||
*/
|
||||
define([
|
||||
'jquery', 'backbone', 'underscore', 'gettext',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'text!templates/move-xblock-breadcrumb.underscore'
|
||||
],
|
||||
function($, Backbone, _, gettext, HtmlUtils, StringUtils, MoveXBlockBreadcrumbViewTemplate) {
|
||||
'use strict';
|
||||
|
||||
var MoveXBlockBreadcrumb = Backbone.View.extend({
|
||||
el: '.breadcrumb-container',
|
||||
|
||||
defaultRenderOptions: {
|
||||
breadcrumbs: ['Course Outline']
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .parent-nav-button': 'handleBreadcrumbButtonPress'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.template = HtmlUtils.template(MoveXBlockBreadcrumbViewTemplate);
|
||||
this.listenTo(Backbone, 'move:childrenRendered', this.render);
|
||||
},
|
||||
|
||||
render: function(options) {
|
||||
HtmlUtils.setHtml(
|
||||
this.$el,
|
||||
this.template(_.extend({}, this.defaultRenderOptions, options))
|
||||
);
|
||||
Backbone.trigger('move:breadcrumbRendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler for breadcrumb button press.
|
||||
*
|
||||
* @param {Object} event
|
||||
*/
|
||||
handleBreadcrumbButtonPress: function(event) {
|
||||
Backbone.trigger(
|
||||
'move:breadcrumbButtonPressed',
|
||||
$(event.target).data('parentIndex')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return MoveXBlockBreadcrumb;
|
||||
});
|
||||
201
cms/static/js/views/move_xblock_list.js
Normal file
201
cms/static/js/views/move_xblock_list.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* XBlockListView shows list of XBlocks in a particular category(section, subsection, vertical etc).
|
||||
*/
|
||||
define([
|
||||
'jquery', 'backbone', 'underscore', 'gettext',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'js/views/utils/xblock_utils',
|
||||
'text!templates/move-xblock-list.underscore'
|
||||
],
|
||||
function($, Backbone, _, gettext, HtmlUtils, StringUtils, XBlockUtils, MoveXBlockListViewTemplate) {
|
||||
'use strict';
|
||||
|
||||
var XBlockListView = Backbone.View.extend({
|
||||
el: '.xblock-list-container',
|
||||
|
||||
// parent info of currently displayed children
|
||||
parentInfo: {},
|
||||
// currently displayed children XBlocks info
|
||||
childrenInfo: {},
|
||||
// list of visited parent XBlocks, needed for backward navigation
|
||||
visitedAncestors: null,
|
||||
|
||||
// parent to child relation map
|
||||
categoryRelationMap: {
|
||||
course: 'section',
|
||||
section: 'subsection',
|
||||
subsection: 'unit',
|
||||
unit: 'component'
|
||||
},
|
||||
|
||||
categoriesText: {
|
||||
section: gettext('Sections'),
|
||||
subsection: gettext('Subsections'),
|
||||
unit: gettext('Units'),
|
||||
component: gettext('Components')
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .button-forward': 'renderChildren'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.visitedAncestors = [];
|
||||
this.template = HtmlUtils.template(MoveXBlockListViewTemplate);
|
||||
this.ancestorInfo = options.ancestorInfo;
|
||||
this.listenTo(Backbone, 'move:breadcrumbButtonPressed', this.handleBreadcrumbButtonPress);
|
||||
this.renderXBlockInfo();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
HtmlUtils.setHtml(
|
||||
this.$el,
|
||||
this.template(
|
||||
{
|
||||
xblocks: this.childrenInfo.children,
|
||||
noChildText: this.getNoChildText(),
|
||||
categoryText: this.getCategoryText(),
|
||||
parentDisplayname: this.parentInfo.parent.get('display_name'),
|
||||
XBlocksCategory: this.childrenInfo.category,
|
||||
currentLocationIndex: this.getCurrentLocationIndex()
|
||||
}
|
||||
)
|
||||
);
|
||||
Backbone.trigger('move:childrenRendered', this.breadcrumbInfo());
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Forward button press handler. This will render all the childs of an XBlock.
|
||||
*
|
||||
* @param {Object} event
|
||||
*/
|
||||
renderChildren: function(event) {
|
||||
this.renderXBlockInfo(
|
||||
'forward',
|
||||
$(event.target).closest('.xblock-item').data('itemIndex')
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Breadcrumb button press event handler. Render all the childs of an XBlock.
|
||||
*
|
||||
* @param {any} newParentIndex Index of a parent XBlock
|
||||
*/
|
||||
handleBreadcrumbButtonPress: function(newParentIndex) {
|
||||
this.renderXBlockInfo('backward', newParentIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render XBlocks based on `forward` or `backward` navigation.
|
||||
*
|
||||
* @param {any} direction `forward` or `backward`
|
||||
* @param {any} newParentIndex Index of a parent XBlock
|
||||
*/
|
||||
renderXBlockInfo: function(direction, newParentIndex) {
|
||||
if (direction === undefined) {
|
||||
this.parentInfo.parent = this.model;
|
||||
} else if (direction === 'forward') {
|
||||
// clicked child is the new parent
|
||||
this.parentInfo.parent = this.childrenInfo.children[newParentIndex];
|
||||
} else if (direction === 'backward') {
|
||||
// new parent will be one of visitedAncestors
|
||||
this.parentInfo.parent = this.visitedAncestors[newParentIndex];
|
||||
// remove visited ancestors
|
||||
this.visitedAncestors.splice(newParentIndex);
|
||||
}
|
||||
|
||||
this.visitedAncestors.push(this.parentInfo.parent);
|
||||
|
||||
if (this.parentInfo.parent.get('child_info')) {
|
||||
this.childrenInfo.children = this.parentInfo.parent.get('child_info').children;
|
||||
} else {
|
||||
this.childrenInfo.children = [];
|
||||
}
|
||||
|
||||
this.setDisplayedXBlocksCategories();
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set parent and child XBlock categories.
|
||||
*/
|
||||
setDisplayedXBlocksCategories: function() {
|
||||
this.parentInfo.category = XBlockUtils.getXBlockType(
|
||||
this.parentInfo.parent.get('category'),
|
||||
this.visitedAncestors[this.visitedAncestors.length - 2]
|
||||
);
|
||||
this.childrenInfo.category = this.categoryRelationMap[this.parentInfo.category];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get index of source XBlock.
|
||||
*
|
||||
* @returns {any} Integer or undefined
|
||||
*/
|
||||
getCurrentLocationIndex: function() {
|
||||
var category, ancestorXBlock, currentLocationIndex;
|
||||
|
||||
if (this.childrenInfo.category === 'component' || this.childrenInfo.children.length === 0) {
|
||||
return currentLocationIndex;
|
||||
}
|
||||
|
||||
category = this.childrenInfo.children[0].get('category');
|
||||
ancestorXBlock = _.find(
|
||||
this.ancestorInfo.ancestors, function(ancestor) { return ancestor.category === category; }
|
||||
);
|
||||
|
||||
if (ancestorXBlock) {
|
||||
_.each(this.childrenInfo.children, function(xblock, index) {
|
||||
if (ancestorXBlock.display_name === xblock.get('display_name') &&
|
||||
ancestorXBlock.id === xblock.get('id')) {
|
||||
currentLocationIndex = index;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return currentLocationIndex;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get category text for currently displayed children.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
getCategoryText: function() {
|
||||
return this.categoriesText[this.childrenInfo.category];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get text when a parent XBlock has no children.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
getNoChildText: function() {
|
||||
return StringUtils.interpolate(
|
||||
gettext('This {parentCategory} has no {childCategory}'),
|
||||
{
|
||||
parentCategory: this.parentInfo.category,
|
||||
childCategory: this.categoriesText[this.childrenInfo.category].toLowerCase()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Construct breadcurmb info.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
breadcrumbInfo: function() {
|
||||
return {
|
||||
breadcrumbs: _.map(this.visitedAncestors, function(ancestor) {
|
||||
return ancestor.get('category') === 'course' ?
|
||||
gettext('Course Outline') : ancestor.get('display_name');
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return XBlockListView;
|
||||
});
|
||||
@@ -196,7 +196,8 @@ define(['jquery', 'underscore', 'gettext', 'js/views/pages/base_page', 'common/j
|
||||
var xblockElement = this.findXBlockElement(event.target),
|
||||
modal = new MoveXBlockModal({
|
||||
sourceXBlockInfo: XBlockUtils.findXBlockInfo(xblockElement, this.model),
|
||||
XBlockUrlRoot: this.getURLRoot()
|
||||
XBlockURLRoot: this.getURLRoot(),
|
||||
outlineURL: this.options.outlineURL
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -344,6 +344,20 @@
|
||||
.btn-default.edit-button {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.stack-move-icon {
|
||||
font-size: 0.52em;
|
||||
|
||||
@include rtl {
|
||||
.fa-file-o {
|
||||
@include transform(rotateY(180deg));
|
||||
}
|
||||
|
||||
.fa-arrow-right {
|
||||
@include transform(rotate(180deg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,20 @@
|
||||
// specific modal overrides
|
||||
// ------------------------
|
||||
|
||||
// Move XBlock Modal
|
||||
.modal-window.move-modal {
|
||||
top: 10% !important;
|
||||
}
|
||||
|
||||
.move-xblock-modal {
|
||||
.modal-content {
|
||||
padding: ($baseline/2) ($baseline/2) ($baseline*1.25) ($baseline/2);
|
||||
}
|
||||
.ui-loading {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// upload modal
|
||||
.assetupload-modal {
|
||||
|
||||
|
||||
@@ -278,3 +278,5 @@ $body-line-height: golden-ratio(.875em, 1);
|
||||
// carried over from LMS for xmodules
|
||||
$action-primary-active-bg: #1AA1DE !default; // $m-blue
|
||||
$very-light-text: $white !default;
|
||||
|
||||
$color-background-alternate: rgb(242, 248, 251) !default;
|
||||
|
||||
@@ -331,3 +331,114 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.move-xblock-modal {
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
margin-bottom: ($baseline/4);
|
||||
border: 1px solid $btn-lms-border;
|
||||
padding: ($baseline/2);
|
||||
background: $color-background-alternate;
|
||||
|
||||
.breadcrumbs {
|
||||
|
||||
.bc-container {
|
||||
@include font-size(14);
|
||||
display: inline-block;
|
||||
|
||||
.breadcrumb-fa-icon {
|
||||
padding: 0 ($baseline/4);
|
||||
|
||||
@include rtl {
|
||||
@include transform(rotate(180deg));
|
||||
}
|
||||
}
|
||||
|
||||
&.last {
|
||||
.parent-displayname {
|
||||
@include font-size(18);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bc-container:not(.last) {
|
||||
button, .parent-displayname {
|
||||
text-decoration: underline;
|
||||
color: $ui-link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
@include margin-left($baseline/2);
|
||||
@include font-size(14);
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.xblock-items-container {
|
||||
max-height: ($baseline*15);
|
||||
overflow-y: auto;
|
||||
|
||||
.xblock-item {
|
||||
& > * {
|
||||
width: 100%;
|
||||
color: $uxpl-blue-hover-active;
|
||||
}
|
||||
|
||||
.component {
|
||||
display: block;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.button-forward, .component {
|
||||
border: none;
|
||||
padding: ($baseline/2);
|
||||
}
|
||||
|
||||
.button-forward {
|
||||
.xblock-displayname {
|
||||
@include float(left);
|
||||
}
|
||||
|
||||
.forward-sr-icon {
|
||||
@include float(right);
|
||||
|
||||
@include rtl {
|
||||
@include transform(rotate(180deg));
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background: $color-background-alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xblock-no-child-message {
|
||||
@include text-align(center);
|
||||
display: block;
|
||||
padding: ($baseline*2);
|
||||
}
|
||||
}
|
||||
|
||||
.truncate {
|
||||
max-width: 90%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.current-location {
|
||||
@include float(left);
|
||||
@include margin-left($baseline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
</li>
|
||||
<li class="action-item action-move">
|
||||
<button data-tooltip="${_("Move")}" class="btn-default move-button action-button">
|
||||
<span class="icon fa fa-folder-o" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Move")}</span>
|
||||
<span class="stack-move-icon fa-stack fa-lg">
|
||||
<span class="fa fa-file-o fa-stack-2x fa-fw" aria-hidden="true"></span>
|
||||
<span class="fa fa-arrow-right fa-stack-1x fa-fw" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="sr">${_("Move")}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="action-item action-delete">
|
||||
|
||||
@@ -43,7 +43,8 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
"${action | n, js_escaped_string}",
|
||||
{
|
||||
isUnitPage: ${is_unit_page | n, dump_js_escaped_json},
|
||||
canEdit: true
|
||||
canEdit: true,
|
||||
outlineURL: "${outline_url | n, js_escaped_string}"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,9 +5,18 @@
|
||||
<div class="modal-window <%- viewSpecificClasses %> modal-<%- size %> modal-type-<%- type %>" tabindex="-1" aria-labelledby="modal-window-title">
|
||||
<div class="<%- name %>-modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-window-title" class="title modal-window-title"><%- title %></h2>
|
||||
<ul class="editor-modes action-list action-modes">
|
||||
</ul>
|
||||
<h2 id="modal-window-title" class="title modal-window-title">
|
||||
<%- title %>
|
||||
<% if (modalSRTitle) { %>
|
||||
<span class="sr modal-sr-title">
|
||||
<%- modalSRTitle %>
|
||||
</span>
|
||||
<% } %>
|
||||
</h2>
|
||||
<% if (showEditorModeButtons) { %>
|
||||
<ul class="editor-modes action-list action-modes">
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
</div>
|
||||
|
||||
17
cms/templates/js/move-xblock-breadcrumb.underscore
Normal file
17
cms/templates/js/move-xblock-breadcrumb.underscore
Normal file
@@ -0,0 +1,17 @@
|
||||
<nav class="breadcrumbs" aria-label="Course Outline breadcrumb">
|
||||
<% _.each(breadcrumbs.slice(0, -1), function (breadcrumb, index, items) { %>
|
||||
<ol class="bc-container bc-<%- index %>">
|
||||
<li class="bc-container-content">
|
||||
<button class="parent-nav-button" data-parent-index="<%- index %>">
|
||||
<%- breadcrumb %>
|
||||
</button>
|
||||
<span class="fa fa-angle-right breadcrumb-fa-icon" aria-hidden="true"></span>
|
||||
</li>
|
||||
</ol>
|
||||
<% }) %>
|
||||
<ol class="bc-container bc-<%- breadcrumbs.length - 1 %> last">
|
||||
<li class="bc-container-content">
|
||||
<span class="parent-displayname"><%- breadcrumbs[breadcrumbs.length - 1] %></span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
46
cms/templates/js/move-xblock-list.underscore
Normal file
46
cms/templates/js/move-xblock-list.underscore
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="xblock-items-category">
|
||||
<span class="sr">
|
||||
<%
|
||||
// Translators: message will be like `Units in Homework - Question Styles`, `Subsections in Example 1 - Getting started` etc.
|
||||
%>
|
||||
<%- StringUtils.interpolate(
|
||||
gettext("{categoryText} in {parentDisplayname}"),
|
||||
{categoryText: categoryText, parentDisplayname: parentDisplayname}
|
||||
)
|
||||
%>
|
||||
</span>
|
||||
<span class="category-text" aria-hidden="true">
|
||||
<%- categoryText %>:
|
||||
</span>
|
||||
</div>
|
||||
<ul class="xblock-items-container">
|
||||
<% for (var i = 0; i < xblocks.length; i++) {
|
||||
var xblock = xblocks[i];
|
||||
%>
|
||||
<li class="xblock-item" data-item-index="<%- i %>">
|
||||
<% if (XBlocksCategory === 'component') { %>
|
||||
<span class="xblock-displayname component truncate">
|
||||
<%- xblock.get('display_name') %>
|
||||
</span>
|
||||
<% } else { %>
|
||||
<button class="button-forward" >
|
||||
<span class="xblock-displayname truncate">
|
||||
<%- xblock.get('display_name') %>
|
||||
</span>
|
||||
<% if(currentLocationIndex === i) { %>
|
||||
<span class="current-location">
|
||||
(<%- gettext('Current location') %>)
|
||||
</span>
|
||||
<% } %>
|
||||
<span class="icon fa fa-arrow-right forward-sr-icon" aria-hidden="true"></span>
|
||||
<span class="sr forward-sr-text"><%- gettext("Click for children") %></span>
|
||||
</button>
|
||||
<% } %>
|
||||
</li>
|
||||
<% } %>
|
||||
<% if(xblocks.length === 0) { %>
|
||||
<span class="xblock-no-child-message">
|
||||
<%- noChildText %>
|
||||
</span>
|
||||
<% } %>
|
||||
</ul>
|
||||
@@ -1,4 +1,10 @@
|
||||
<div class='breadcrumb-container'>
|
||||
</div>
|
||||
<div class='treeview-container'>
|
||||
<div class="ui-loading">
|
||||
<p>
|
||||
<span class="spin">
|
||||
<span class="icon fa fa-refresh" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="copy"><%- gettext('Loading') %></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class='breadcrumb-container is-hidden'></div>
|
||||
<div class='xblock-list-container'></div>
|
||||
|
||||
@@ -92,8 +92,11 @@ messages = xblock.validate().to_json()
|
||||
|
||||
<li class="action-item action-move">
|
||||
<button data-tooltip="${_("Move")}" class="btn-default move-button action-button">
|
||||
<span class="icon fa fa-folder-o" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Move")}</span>
|
||||
<span class="stack-move-icon fa-stack fa-lg ">
|
||||
<span class="fa fa-file-o fa-stack-2x fa-fw" aria-hidden="true"></span>
|
||||
<span class="fa fa-arrow-right fa-stack-1x fa-fw" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="sr">${_("Move")}</span>
|
||||
</button>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
Reference in New Issue
Block a user