Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FC-0009] [FAL-3496] Copy/paste unit from within a unit #33724

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cms/static/js/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
27 changes: 0 additions & 27 deletions cms/static/js/spec/views/pages/container_subviews_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 4 additions & 2 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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();

Expand Down Expand Up @@ -120,7 +122,6 @@ function($, _, Backbone, gettext, BasePage,
}

this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved);
this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel");
},

getViewParameters: function() {
Expand Down Expand Up @@ -175,6 +176,7 @@ function($, _, Backbone, gettext, BasePage,
if (!self.isLibraryPage && !self.isLibraryContentPage) {
self.initializePasteButton();
}

},
block_added: options && options.block_added
});
Expand Down
48 changes: 47 additions & 1 deletion cms/static/js/views/pages/container_subviews.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
84 changes: 82 additions & 2 deletions cms/static/js/views/utils/xblock_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -308,6 +387,7 @@ function($, _, gettext, ViewUtils, ModuleUtils, XBlockInfo, StringUtils) {
getXBlockListTypeClass: getXBlockListTypeClass,
updateXBlockFields: updateXBlockFields,
getXBlockType: getXBlockType,
findXBlockInfo: findXBlockInfo
findXBlockInfo: findXBlockInfo,
pasteXBlock: pasteXBlock
};
});
4 changes: 4 additions & 0 deletions cms/static/js/views/xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions cms/static/sass/elements/_navigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ $seq-nav-height: 40px;
ol {
display: flex;

.custom-dropdown {
position: relative;
display: inline-flex;
}

li {
box-sizing: border-box;
min-width: 40px;
Expand All @@ -300,6 +305,47 @@ $seq-nav-height: 40px;
@include border-right-style(solid);
}

.dropdown-main-button {
border-right: 1px solid #e7e7e7 !important;
}

.dropdown-toggle-button {
width: 15% !important;

&:hover {
border-bottom: 1px solid #e7e7e7 !important;
}
}

.dropdown-options {
position: absolute;
top: 100%;
z-index: 1000;
background-color: #ffffff;
min-width: 265px;
right: 0;

li {
padding: 0.5em 1em;
cursor: pointer;

a {
display: block;
width: 100%;
color: black;
}

.checkmark {
float: right;
margin-left: 10px;
}
}
}

.dropdown-options li:hover {
background-color: #f1f1f1;
}

button {
@extend %ui-fake-link;
@extend %ui-clear-button;
Expand Down
13 changes: 13 additions & 0 deletions cms/static/sass/views/_container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,19 @@
color: $gray-l1;
}
}

.action-copy {
width: 100%;
border-color: #0075b4;
padding-top: 10px;
padding-bottom: 10px;
line-height: 24px;
border-radius: 4px;

&:hover {
@extend %btn-primary-blue;
}
pkulkark marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down
Loading
Loading