feat: Paste Components (OLX) into any Unit in Studio (#31969)

* feat: Implement paste button

* chore: improve docs and add tests for python API

* fix: drive-by fix to use a better API for comparing XML

* feat: track which XBlock something was copied from

* feat: add tests

* feat: enable import linter so content_staging's public API is respected

* fix: error seen when trying to paste drag-and-drop-v2 blocks

* fix: use strip_text=True consistently for XML comparisons

* refactor: rename get_user_clipboard_status to get_user_clipboard

* feat: Better error reporting when pasting in Studio

* chore: convert new test suite to pytest assertions

* refactor: push READY status check into the API per review suggestion

* fix: use strip_text=True consistently for XML comparisons

* fix: store "copied_from_block" as a string to avoid Reference field issues

* fix: minor lint error

* refactor: move data types to data.py per OEP-49
This commit is contained in:
Braden MacDonald
2023-04-27 09:58:04 -07:00
committed by GitHub
parent 08c26aa8ab
commit 8ee1f66ffb
20 changed files with 490 additions and 77 deletions

View File

@@ -22,12 +22,14 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
'click .move-button': 'showMoveXBlockModal',
'click .delete-button': 'deleteXBlock',
'click .show-actions-menu-button': 'showXBlockActionsMenu',
'click .new-component-button': 'scrollToNewComponentButtons'
'click .new-component-button': 'scrollToNewComponentButtons',
'click .paste-component-button': 'pasteComponent',
},
options: {
collapsedClass: 'is-collapsed',
canEdit: true // If not specified, assume user has permission to make changes
canEdit: true, // If not specified, assume user has permission to make changes
clipboardData: { content: null },
},
view: 'container_preview',
@@ -100,6 +102,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
}
this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved);
this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel");
},
getViewParameters: function() {
@@ -147,6 +150,11 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
// Re-enable Backbone events for any updated DOM elements
self.delegateEvents();
// Show/hide the paste button
if (!self.isLibraryPage) {
self.initializePasteButton();
}
},
block_added: options && options.block_added
});
@@ -182,6 +190,59 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
}
},
initializePasteButton() {
if (this.options.canEdit) {
// We should have the user's clipboard status.
const data = this.options.clipboardData;
this.refreshPasteButton(data);
// Refresh the status when something is copied on another tab:
this.clipboardBroadcastChannel.onmessage = (event) => { this.refreshPasteButton(event.data); };
} else {
this.$(".paste-component").hide();
}
},
/**
* Given the latest information about the user's clipboard, hide or show the Paste button as appropriate.
*/
refreshPasteButton(data) {
// 'data' is the same data returned by the "get clipboard status" API endpoint
// i.e. /api/content-staging/v1/clipboard/
if (this.options.canEdit && data.content) {
// TODO: check if this is suitable for pasting into a unit
this.$(".paste-component").show();
} else {
this.$(".paste-component").hide();
}
},
/** The user has clicked on the "Paste Component button" */
pasteComponent(event) {
event.preventDefault();
// Get the ID of the container (usually a unit/vertical) that we're pasting into:
const parentElement = this.findXBlockElement(event.target);
const parentLocator = parentElement.data('locator');
// Create a placeholder XBlock while we're pasting:
const $placeholderEl = $(this.createPlaceholderElement());
const addComponentsPanel = $(event.target).closest('.paste-component').prev();
const listPanel = addComponentsPanel.prev();
const scrollOffset = ViewUtils.getScrollOffset(addComponentsPanel);
const placeholderElement = $placeholderEl.appendTo(listPanel);
// Start showing a "Pasting" notification:
ViewUtils.runOperationShowingMessage(gettext('Pasting'), () => {
return $.postJSON(this.getURLRoot() + '/', {
parent_locator: parentLocator,
staged_content: "clipboard",
}).then((data) => {
this.onNewXBlock(placeholderElement, scrollOffset, false, data);
}).fail(() => {
// Remove the placeholder if the paste failed
placeholderElement.remove();
});
});
},
editXBlock: function(event, options) {
event.preventDefault();
@@ -307,6 +368,8 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
const status = data.content?.status;
if (status === "ready") {
// The XBlock has been copied and is ready to use.
this.refreshPasteButton(data); // Update our UI
this.clipboardBroadcastChannel.postMessage(data); // And notify any other open tabs
return data;
} else if (status === "loading") {
// The clipboard is being loaded asynchonously.
@@ -316,6 +379,8 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
$.getJSON(clipboardEndpoint, (pollData) => {
const newStatus = pollData.content?.status;
if (newStatus === "ready") {
this.refreshPasteButton(data);
this.clipboardBroadcastChannel.postMessage(pollData);
deferred.resolve(pollData);
} else if (newStatus === "loading") {
setTimeout(checkStatus, 1_000);

View File

@@ -320,6 +320,22 @@
}
}
}
// New "Paste component" menu, shown on the Unit page to users with a component in their clipboard
.paste-component {
margin: $baseline ($baseline/2);
.paste-component-button {
display: block;
width: 100%;
// Override what we're extending from ui-btn-flat-outline:
&.button {
font-size: 1.5rem;
padding: 10px 0;
}
@extend %ui-btn-flat-outline;
}
}
// outline UI
// --------------------