Skip to content

Commit

Permalink
feat: copy/paste unit from within a unit in Studio - feature flagged (#…
Browse files Browse the repository at this point in the history
…33724)

(requires the contentstore.enable_copy_paste_units waffle flag)
  • Loading branch information
pkulkark authored and bradenmacdonald committed Dec 1, 2023
1 parent 97b8cb9 commit 79f95d7
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 62 deletions.
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 @@ -69,6 +69,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
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 @@ -81,7 +82,8 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
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 All @@ -105,7 +107,6 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
}

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

getViewParameters: function() {
Expand Down Expand Up @@ -158,6 +159,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
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 @@ -106,7 +106,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 @@ -116,6 +117,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 @@ -173,6 +175,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;
}
}
}
}

Expand Down
Loading

0 comments on commit 79f95d7

Please sign in to comment.