feat: copy/paste unit from within a unit in Studio - feature flagged (#33724)
(requires the contentstore.enable_copy_paste_units waffle flag)
This commit is contained in:
@@ -75,6 +75,7 @@ function(
|
||||
$body.click(function() {
|
||||
$('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown');
|
||||
$('.nav-dd .nav-item .title').removeClass('is-selected');
|
||||
$('.custom-dropdown .dropdown-options').hide();
|
||||
});
|
||||
|
||||
$('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) {
|
||||
|
||||
@@ -581,33 +581,6 @@ describe('Container Subviews', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PublishHistory', function() {
|
||||
var lastPublishCss = '.wrapper-last-publish';
|
||||
|
||||
it('renders never published when the block is unpublished', function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
published: false, published_on: null, published_by: null
|
||||
});
|
||||
expect(containerPage.$(lastPublishCss).text()).toContain('Never published');
|
||||
});
|
||||
|
||||
it('renders the last published date and user when the block is published', function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml);
|
||||
fetch({
|
||||
published: true, published_on: 'Jul 01, 2014 at 12:45 UTC', published_by: 'amako'
|
||||
});
|
||||
expect(containerPage.$(lastPublishCss).text())
|
||||
.toContain('Last published Jul 01, 2014 at 12:45 UTC by amako');
|
||||
});
|
||||
|
||||
it('renders correctly when the block is published without publish info', function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml);
|
||||
fetch({
|
||||
published: true, published_on: null, published_by: null
|
||||
});
|
||||
expect(containerPage.$(lastPublishCss).text()).toContain('Previously published');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message Area', function() {
|
||||
var messageSelector = '.container-message .warning',
|
||||
|
||||
@@ -77,6 +77,7 @@ function($, _, Backbone, gettext, BasePage,
|
||||
model: this.model
|
||||
});
|
||||
this.messageView.render();
|
||||
this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel");
|
||||
// Display access message on units and split test components
|
||||
if (!this.isLibraryPage) {
|
||||
this.containerAccessView = new ContainerSubviews.ContainerAccess({
|
||||
@@ -89,7 +90,8 @@ function($, _, Backbone, gettext, BasePage,
|
||||
el: this.$('#publish-unit'),
|
||||
model: this.model,
|
||||
// When "Discard Changes" is clicked, the whole page must be re-rendered.
|
||||
renderPage: this.render
|
||||
renderPage: this.render,
|
||||
clipboardBroadcastChannel: this.clipboardBroadcastChannel,
|
||||
});
|
||||
this.xblockPublisher.render();
|
||||
|
||||
@@ -120,7 +122,6 @@ function($, _, Backbone, gettext, BasePage,
|
||||
}
|
||||
|
||||
this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved);
|
||||
this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel");
|
||||
},
|
||||
|
||||
getViewParameters: function() {
|
||||
@@ -175,6 +176,7 @@ function($, _, Backbone, gettext, BasePage,
|
||||
if (!self.isLibraryPage && !self.isLibraryContentPage) {
|
||||
self.initializePasteButton();
|
||||
}
|
||||
|
||||
},
|
||||
block_added: options && options.block_added
|
||||
});
|
||||
|
||||
@@ -107,7 +107,8 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H
|
||||
events: {
|
||||
'click .action-publish': 'publish',
|
||||
'click .action-discard': 'discardChanges',
|
||||
'click .action-staff-lock': 'toggleStaffLock'
|
||||
'click .action-staff-lock': 'toggleStaffLock',
|
||||
'click .action-copy': 'copyToClipboard'
|
||||
},
|
||||
|
||||
// takes XBlockInfo as a model
|
||||
@@ -117,6 +118,7 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H
|
||||
this.template = this.loadTemplate('publish-xblock');
|
||||
this.model.on('sync', this.onSync, this);
|
||||
this.renderPage = this.options.renderPage;
|
||||
this.clipboardBroadcastChannel = this.options.clipboardBroadcastChannel;
|
||||
},
|
||||
|
||||
onSync: function(model) {
|
||||
@@ -174,6 +176,50 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H
|
||||
});
|
||||
},
|
||||
|
||||
copyToClipboard: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const clipboardEndpoint = "/api/content-staging/v1/clipboard/";
|
||||
const usageKeyToCopy = this.model.get('id');
|
||||
// Start showing a "Copying" notification:
|
||||
ViewUtils.runOperationShowingMessage(gettext('Copying'), () => {
|
||||
return $.postJSON(
|
||||
clipboardEndpoint,
|
||||
{ usage_key: usageKeyToCopy },
|
||||
).then((data) => {
|
||||
const status = data.content?.status;
|
||||
if (status === "ready") {
|
||||
// something that enables the paste button in the actions dropdown
|
||||
this.clipboardBroadcastChannel.postMessage(data);
|
||||
return data;
|
||||
} else if (status === "loading") {
|
||||
// The clipboard is being loaded asynchonously.
|
||||
// Poll the endpoint until the copying process is complete:
|
||||
const deferred = $.Deferred();
|
||||
const checkStatus = () => {
|
||||
$.getJSON(clipboardEndpoint, (pollData) => {
|
||||
const newStatus = pollData.content?.status;
|
||||
if (newStatus === "ready") {
|
||||
// something that enables the paste button in actions dropdown
|
||||
this.clipboardBroadcastChannel.postMessage(pollData);
|
||||
deferred.resolve(pollData);
|
||||
} else if (newStatus === "loading") {
|
||||
setTimeout(checkStatus, 1_000);
|
||||
} else {
|
||||
deferred.reject();
|
||||
throw new Error(`Unexpected clipboard status "${newStatus}" in successful API response.`);
|
||||
}
|
||||
})
|
||||
}
|
||||
setTimeout(checkStatus, 1_000);
|
||||
return deferred;
|
||||
} else {
|
||||
throw new Error(`Unexpected clipboard status "${status}" in successful API response.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
discardChanges: function(e) {
|
||||
var xblockInfo = this.model,
|
||||
renderPage = this.renderPage;
|
||||
|
||||
@@ -8,7 +8,7 @@ function($, _, gettext, ViewUtils, ModuleUtils, XBlockInfo, StringUtils) {
|
||||
|
||||
var addXBlock, duplicateXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState,
|
||||
getXBlockVisibilityClass, getXBlockListTypeClass, updateXBlockFields, getXBlockType, findXBlockInfo,
|
||||
moveXBlock;
|
||||
moveXBlock, pasteXBlock;
|
||||
|
||||
/**
|
||||
* Represents the possible visibility states for an xblock:
|
||||
@@ -69,6 +69,85 @@ function($, _, gettext, ViewUtils, ModuleUtils, XBlockInfo, StringUtils) {
|
||||
});
|
||||
};
|
||||
|
||||
pasteXBlock = function(target) {
|
||||
var parentLocator = target.data('parent'),
|
||||
displayName = target.data('default-name');
|
||||
|
||||
return ViewUtils.runOperationShowingMessage(gettext('Pasting'), () => {
|
||||
return $.postJSON(ModuleUtils.getUpdateUrl(), {
|
||||
parent_locator: parentLocator,
|
||||
staged_content: "clipboard",
|
||||
}).then((data) => {
|
||||
return data;
|
||||
});
|
||||
}).done((data) => {
|
||||
const {
|
||||
conflicting_files: conflictingFiles,
|
||||
error_files: errorFiles,
|
||||
new_files: newFiles,
|
||||
} = data.static_file_notices;
|
||||
|
||||
const notices = [];
|
||||
if (errorFiles.length) {
|
||||
notices.push((next) => new PromptView.Error({
|
||||
title: gettext("Some errors occurred"),
|
||||
message: (
|
||||
gettext("The following required files could not be added to the course:") +
|
||||
" " + errorFiles.join(", ")
|
||||
),
|
||||
actions: {primary: {text: gettext("OK"), click: (x) => { x.hide(); next(); }}},
|
||||
}));
|
||||
}
|
||||
if (conflictingFiles.length) {
|
||||
notices.push((next) => new PromptView.Warning({
|
||||
title: gettext("You may need to update a file(s) manually"),
|
||||
message: (
|
||||
gettext(
|
||||
"The following files already exist in this course but don't match the " +
|
||||
"version used by the component you pasted:"
|
||||
) + " " + conflictingFiles.join(", ")
|
||||
),
|
||||
actions: {primary: {text: gettext("OK"), click: (x) => { x.hide(); next(); }}},
|
||||
}));
|
||||
}
|
||||
if (newFiles.length) {
|
||||
notices.push(() => new NotificationView.Info({
|
||||
title: gettext("New file(s) added to Files & Uploads."),
|
||||
message: (
|
||||
gettext("The following required files were imported to this course:") +
|
||||
" " + newFiles.join(", ")
|
||||
),
|
||||
actions: {
|
||||
primary: {
|
||||
text: gettext('View files'),
|
||||
click: function(notification) {
|
||||
const article = document.querySelector('[data-course-assets]');
|
||||
const assetsUrl = $(article).attr('data-course-assets');
|
||||
window.location.href = assetsUrl;
|
||||
return;
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
text: gettext('Dismiss'),
|
||||
click: function(notification) {
|
||||
return notification.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (notices.length) {
|
||||
// Show the notices, one at a time:
|
||||
const showNext = () => {
|
||||
const view = notices.shift()(showNext);
|
||||
view.show();
|
||||
}
|
||||
// Delay to avoid conflict with the "Pasting..." notification.
|
||||
setTimeout(showNext, 1250);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Duplicates the specified xblock element in its parent xblock.
|
||||
* @param {jquery Element} xblockElement The xblock element to be duplicated.
|
||||
@@ -308,6 +387,7 @@ function($, _, gettext, ViewUtils, ModuleUtils, XBlockInfo, StringUtils) {
|
||||
getXBlockListTypeClass: getXBlockListTypeClass,
|
||||
updateXBlockFields: updateXBlockFields,
|
||||
getXBlockType: getXBlockType,
|
||||
findXBlockInfo: findXBlockInfo
|
||||
findXBlockInfo: findXBlockInfo,
|
||||
pasteXBlock: pasteXBlock
|
||||
};
|
||||
});
|
||||
|
||||
@@ -14,6 +14,10 @@ function($, _, ViewUtils, BaseView, XBlock, HtmlUtils) {
|
||||
'click .notification-action-button': 'fireNotificationActionEvent'
|
||||
},
|
||||
|
||||
options: {
|
||||
clipboardData: { content: null },
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
BaseView.prototype.initialize.call(this);
|
||||
this.view = this.options.view;
|
||||
|
||||
Reference in New Issue
Block a user