feat!: Drop the legacy Tabs UI (#37557)

This change drops the legacy studio custom pages UI aka. the tab edit
page.

This work is part of https://github.com/openedx/edx-platform/issues/36108

BREAKING CHANGE: The 'legacy_studio.custom_pages' waffle flag has been
removed and the code will work as if this flag is permanently set to
False.

Co-authored-by: Kyle McCormick <kyle@axim.org>
This commit is contained in:
Feanil Patel
2025-11-20 18:36:10 -05:00
committed by GitHub
parent d929cdb7fa
commit b1f01ed8fa
18 changed files with 28 additions and 912 deletions

View File

@@ -1,27 +0,0 @@
import * as TabsModel from 'js/models/explicit_url';
import * as TabsEditView from 'js/views/tabs';
import * as xmoduleLoader from 'xmodule';
import './base';
import 'cms/js/main';
import 'xblock/cms.runtime.v1';
import 'xmodule/js/src/xmodule'; // Force the XBlockToXModuleShim to load for Static Tabs
// eslint-disable-next-line no-unused-expressions
'use strict';
export default function EditTabsFactory(courseLocation, explicitUrl) {
xmoduleLoader.done(function() {
var model = new TabsModel({
id: courseLocation,
explicit_url: explicitUrl
}),
editView;
editView = new TabsEditView({
el: $('.tab-list'),
model: model,
mast: $('.wrapper-mast')
});
});
}
export {EditTabsFactory};

View File

@@ -1,14 +0,0 @@
/**
* A model that simply allows the update URL to be passed
* in as an argument.
*/
define(['backbone'], function(Backbone) {
return Backbone.Model.extend({
defaults: {
explicit_url: ''
},
url: function() {
return this.get('explicit_url');
}
});
});

View File

@@ -1,12 +0,0 @@
define(['js/models/explicit_url'],
function(Model) {
describe('Model ', function() {
it('allows url to be passed in constructor', function() {
expect(new Model({explicit_url: '/fancy/url'}).url()).toBe('/fancy/url');
});
it('returns empty string if url not set', function() {
expect(new Model().url()).toBe('');
});
});
}
);

View File

@@ -1,266 +0,0 @@
import $ from 'jquery';
import ViewUtils from 'common/js/components/utils/view_utils';
import edit_helpers from 'js/spec_helpers/edit_helpers';
import ModuleEdit from 'js/views/module_edit';
import ModuleModel from 'js/models/module_info';
import 'xmodule/js/src/xmodule';
describe('ModuleEdit', function() {
beforeEach(function() {
this.stubModule = new ModuleModel({
id: 'stub-id'
});
setFixtures('<ul>\n'
+ '<li class="component" id="stub-id" data-locator="stub-id">\n'
+ ' <div class="component-editor">\n'
+ ' <div class="module-editor">\n'
// eslint-disable-next-line no-template-curly-in-string
+ ' ${editor}\n'
+ ' </div>\n'
+ ' <a href="#" class="save-button">Save</a>\n'
+ ' <a href="#" class="cancel-button">Cancel</a>\n'
+ ' </div>\n'
+ ' <div class="component-actions">\n'
+ ' <a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>\n'
+ ' <a href="#" class="delete-button"><span class="delete-icon white">'
+ '</span>Delete</a>\n'
+ ' </div>\n'
+ ' <span class="drag-handle action"></span>\n'
+ ' <section class="xblock xblock-student_view xmodule_display xmodule_stub"'
+ ' data-type="StubModule">\n'
+ ' <div id="stub-module-content"/>\n'
+ ' </section>\n'
+ '</li>\n'
+ '</ul>');
edit_helpers.installEditTemplates(true);
spyOn($, 'ajax').and.returnValue(this.moduleData);
this.moduleEdit = new ModuleEdit({
el: $('.component'),
model: this.stubModule,
onDelete: jasmine.createSpy()
});
return this.moduleEdit;
});
describe('class definition', function() {
it('sets the correct tagName', function() {
return expect(this.moduleEdit.tagName).toEqual('li');
});
it('sets the correct className', function() {
return expect(this.moduleEdit.className).toEqual('component');
});
});
describe('methods', function() {
describe('initialize', function() {
beforeEach(function() {
spyOn(ModuleEdit.prototype, 'render');
this.moduleEdit = new ModuleEdit({
el: $('.component'),
model: this.stubModule,
onDelete: jasmine.createSpy()
});
return this.moduleEdit;
});
it('renders the module editor', function() {
return expect(ModuleEdit.prototype.render).toHaveBeenCalled();
});
});
describe('render', function() {
beforeEach(function() {
edit_helpers.installEditTemplates(true);
spyOn(this.moduleEdit, 'loadDisplay');
spyOn(this.moduleEdit, 'delegateEvents');
spyOn($.fn, 'append');
spyOn(ViewUtils, 'loadJavaScript').and.returnValue($.Deferred().resolve().promise());
window.MockXBlock = function() {
return {};
};
// eslint-disable-next-line no-void
window.loadedXBlockResources = void 0;
this.moduleEdit.render();
return $.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
});
afterEach(function() {
window.MockXBlock = null;
return window.MockXBlock;
});
it('loads the module preview via ajax on the view element', function() {
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/student_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
expect(this.moduleEdit.loadDisplay).toHaveBeenCalled();
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads the editing view via ajax on demand', function() {
var mockXBlockEditorHtml;
expect($.ajax).not.toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
this.moduleEdit.clickEditButton({
preventDefault: jasmine.createSpy('event.preventDefault')
});
mockXBlockEditorHtml = readFixtures('templates/mock/mock-xblock-editor.underscore');
$.ajax.calls.mostRecent().args[0].success({
html: mockXBlockEditorHtml,
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
], [
'hash2', {
kind: 'url',
mimetype: 'text/css',
data: 'css-url'
}
], [
'hash3', {
kind: 'text',
mimetype: 'application/javascript',
data: 'inline-js'
}
], [
'hash4', {
kind: 'url',
mimetype: 'application/javascript',
data: 'js-url'
}
], [
'hash5', {
placement: 'head',
mimetype: 'text/html',
data: 'head-html'
}
], [
'hash6', {
placement: 'not-head',
mimetype: 'text/html',
data: 'not-head-html'
}
]
]
});
expect($.ajax).toHaveBeenCalledWith({
url: '/xblock/' + this.moduleEdit.model.id + '/studio_view',
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: jasmine.any(Function)
});
return expect(this.moduleEdit.delegateEvents).toHaveBeenCalled();
});
it('loads inline css from fragments', function() {
var args = '<style type="text/css">inline-css</style>';
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads css urls from fragments', function() {
var args = '<link rel="stylesheet" href="css-url" type="text/css">';
return expect($('head').append).toHaveBeenCalledWith(args);
});
it('loads inline js from fragments', function() {
return expect($('head').append).toHaveBeenCalledWith('<script>inline-js</script>');
});
it('loads js urls from fragments', function() {
return expect(ViewUtils.loadJavaScript).toHaveBeenCalledWith('js-url');
});
it('loads head html', function() {
return expect($('head').append).toHaveBeenCalledWith('head-html');
});
it("doesn't load body html", function() {
return expect($.fn.append).not.toHaveBeenCalledWith('not-head-html');
});
it("doesn't reload resources", function() {
var count;
count = $('head').append.calls.count();
$.ajax.calls.mostRecent().args[0].success({
html: '<div>Response html 2</div>',
resources: [
[
'hash1', {
kind: 'text',
mimetype: 'text/css',
data: 'inline-css'
}
]
]
});
return expect($('head').append.calls.count()).toBe(count);
});
});
describe('loadDisplay', function() {
beforeEach(function() {
spyOn(XBlock, 'initializeBlock');
return this.moduleEdit.loadDisplay();
});
it('loads the .xmodule-display inside the module editor', function() {
expect(XBlock.initializeBlock).toHaveBeenCalled();
var sel = '.xblock-student_view';
return expect(XBlock.initializeBlock.calls.mostRecent().args[0].get(0)).toBe($(sel).get(0));
});
});
});
});

View File

@@ -1,109 +0,0 @@
(function() {
'use strict';
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
var key;
for (key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function Ctor() {
this.constructor = child;
}
Ctor.prototype = parent.prototype;
child.prototype = new Ctor();
child.__super__ = parent.prototype;
return child;
};
define(['jquery', 'underscore', 'gettext', 'xblock/runtime.v1', 'js/views/xblock', 'js/views/modals/edit_xblock'],
function($, _, gettext, XBlock, XBlockView, EditXBlockModal) {
var ModuleEdit = (function(_super) {
__extends(ModuleEdit, _super);
// eslint-disable-next-line no-shadow
function ModuleEdit() {
return ModuleEdit.__super__.constructor.apply(this, arguments);
}
ModuleEdit.prototype.tagName = 'li';
ModuleEdit.prototype.className = 'component';
ModuleEdit.prototype.editorMode = 'editor-mode';
ModuleEdit.prototype.events = {
'click .edit-button': 'clickEditButton',
'click .delete-button': 'onDelete'
};
ModuleEdit.prototype.initialize = function() {
this.onDelete = this.options.onDelete;
return this.render();
};
ModuleEdit.prototype.loadDisplay = function() {
var xblockElement;
xblockElement = this.$el.find('.xblock-student_view');
if (xblockElement.length > 0) {
return XBlock.initializeBlock(xblockElement);
}
};
ModuleEdit.prototype.createItem = function(parent, payload, callback) {
var _this = this;
if (_.isNull(callback)) {
callback = function() {};
}
payload.parent_locator = parent;
return $.postJSON(this.model.urlRoot + '/', payload, function(data) {
_this.model.set({
id: data.locator
});
_this.$el.data('locator', data.locator);
_this.$el.data('courseKey', data.courseKey);
return _this.render();
}).success(callback);
};
ModuleEdit.prototype.loadView = function(viewName, target, callback) {
var _this = this;
if (this.model.id) {
return $.ajax({
url: '' + (decodeURIComponent(this.model.url())) + '/' + viewName,
type: 'GET',
cache: false,
headers: {
Accept: 'application/json'
},
success: function(fragment) {
return _this.renderXBlockFragment(fragment, target).done(callback);
}
});
}
};
ModuleEdit.prototype.render = function() {
var _this = this;
return this.loadView('student_view', this.$el, function() {
_this.loadDisplay();
return _this.delegateEvents();
});
};
ModuleEdit.prototype.clickEditButton = function(event) {
var modal;
event.preventDefault();
modal = new EditXBlockModal();
return modal.edit(this.$el, this.model, {
refresh: _.bind(this.render, this)
});
};
return ModuleEdit;
}(XBlockView));
return ModuleEdit;
});
}).call(this);

View File

@@ -1,203 +0,0 @@
/* globals analytics, course_location_analytics */
(function(analytics, course_location_analytics) {
'use strict';
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
var key;
for (key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function Ctor() {
this.constructor = child;
}
Ctor.prototype = parent.prototype;
child.prototype = new Ctor();
child.__super__ = parent.prototype;
return child;
};
define(['underscore', 'jquery', 'jquery.ui', 'backbone', 'common/js/components/views/feedback_prompt',
'common/js/components/views/feedback_notification', 'js/views/module_edit',
'js/models/module_info', 'js/utils/module'],
function(_, $, ui, Backbone, PromptView, NotificationView, ModuleEditView, ModuleModel, ModuleUtils) {
var TabsEdit;
TabsEdit = (function(_super) {
__extends(TabsEdit, _super);
// eslint-disable-next-line no-shadow
function TabsEdit() {
var self = this;
this.deleteTab = function() {
return TabsEdit.prototype.deleteTab.apply(self, arguments);
};
this.addNewTab = function() {
return TabsEdit.prototype.addNewTab.apply(self, arguments);
};
this.tabMoved = function() {
return TabsEdit.prototype.tabMoved.apply(self, arguments);
};
this.toggleVisibilityOfTab = function() {
return TabsEdit.prototype.toggleVisibilityOfTab.apply(self, arguments);
};
this.initialize = function() {
return TabsEdit.prototype.initialize.apply(self, arguments);
};
return TabsEdit.__super__.constructor.apply(this, arguments);
}
TabsEdit.prototype.initialize = function(options) {
var self = this;
this.$('.component').each(function(idx, element) {
var model;
model = new ModuleModel({
id: $(element).data('locator')
});
return new ModuleEditView({
el: element,
onDelete: self.deleteTab,
model: model
});
});
this.options = _.extend({}, options);
this.options.mast.find('.new-tab').on('click', this.addNewTab);
$('.add-pages .new-tab').on('click', this.addNewTab);
$('.toggle-checkbox').on('click', this.toggleVisibilityOfTab);
return this.$('.course-nav-list').sortable({
handle: '.drag-handle',
update: this.tabMoved,
helper: 'clone',
opacity: '0.5',
placeholder: 'component-placeholder',
forcePlaceholderSize: true,
axis: 'y',
items: '> .is-movable'
});
};
TabsEdit.prototype.toggleVisibilityOfTab = function(event) {
var checkbox_element, saving, tab_element;
checkbox_element = event.target;
tab_element = $(checkbox_element).parents('.course-tab')[0];
saving = new NotificationView.Mini({
title: gettext('Saving')
});
saving.show();
return $.ajax({
type: 'POST',
url: this.model.url(),
data: JSON.stringify({
tab_id_locator: {
tab_id: $(tab_element).data('tab-id'),
tab_locator: $(tab_element).data('locator')
},
is_hidden: $(checkbox_element).is(':checked')
}),
contentType: 'application/json'
}).success(function() {
return saving.hide();
});
};
TabsEdit.prototype.tabMoved = function() {
var saving, tabs;
tabs = [];
this.$('.course-tab').each(function(idx, element) {
return tabs.push({
tab_id: $(element).data('tab-id'),
tab_locator: $(element).data('locator')
});
});
analytics.track('Reordered Pages', {
course: course_location_analytics
});
saving = new NotificationView.Mini({
title: gettext('Saving')
});
saving.show();
return $.ajax({
type: 'POST',
url: this.model.url(),
data: JSON.stringify({
tabs: tabs
}),
contentType: 'application/json'
}).success(function() {
return saving.hide();
});
};
TabsEdit.prototype.addNewTab = function(event) {
var editor;
event.preventDefault();
editor = new ModuleEditView({
onDelete: this.deleteTab,
model: new ModuleModel()
});
$('.new-component-item').before(editor.$el);
editor.$el.addClass('course-tab is-movable');
editor.$el.addClass('new');
setTimeout(function() {
return editor.$el.removeClass('new');
}, 1000);
$('html, body').animate({
scrollTop: $('.new-component-item').offset().top
}, 500);
editor.createItem(this.model.get('id'), {
category: 'static_tab'
});
return analytics.track('Added Page', {
course: course_location_analytics
});
};
TabsEdit.prototype.deleteTab = function(event) {
var confirm;
confirm = new PromptView.Warning({
title: gettext('Delete Page Confirmation'),
message: gettext('Are you sure you want to delete this page? This action cannot be undone.'),
actions: {
primary: {
text: gettext('OK'),
click: function(view) {
var $component, deleting;
view.hide();
$component = $(event.currentTarget).parents('.component');
analytics.track('Deleted Page', {
course: course_location_analytics,
id: $component.data('locator')
});
deleting = new NotificationView.Mini({
title: gettext('Deleting')
});
deleting.show();
return $.ajax({
type: 'DELETE',
url: ModuleUtils.getUpdateUrl($component.data('locator'))
}).success(function() {
$component.remove();
return deleting.hide();
});
}
},
secondary: [
{
text: gettext('Cancel'),
click: function(view) {
return view.hide();
}
}
]
}
});
return confirm.show();
};
return TabsEdit;
}(Backbone.View));
return TabsEdit;
});
}).call(this, analytics, course_location_analytics);