Skip to content

New: Use content tree API for editor data loading#459

Open
taylortom wants to merge 4 commits intomasterfrom
feature/tree-api-integration
Open

New: Use content tree API for editor data loading#459
taylortom wants to merge 4 commits intomasterfrom
feature/tree-api-integration

Conversation

@taylortom
Copy link
Copy Markdown
Contributor

@taylortom taylortom commented Mar 11, 2026

New

  • Replace full content fetch with lightweight tree endpoint (GET /api/content/tree/:courseId) for initial editor data loading — reduces payload from ~300-500KB to ~10-20KB
  • Staleness check now uses If-Modified-Since conditional requests, returning 304 when content hasn't changed (zero body transfer vs previous POST query)
  • Full documents fetched on demand only when editing individual items (article, block, component, contentObject, config, course, themeEditor)

Update

  • editorDataLoader.jsloadTree() replaces ContentCollection fetch; isOutdated() uses conditional GET instead of POST query; $.ajax used throughout instead of native fetch for consistency with the rest of the codebase; semicolons and spacing restored to match surrounding editor module style
  • Editor index files (article, block, component, contentObject, config, course, themeEditor) — add await model.fetch() before building scaffold forms
  • menuSettings/index.js — no full fetch needed since _menu, _theme, _enabledPlugins are now in tree projection

Dependencies

Testing

  1. Open the editor for an existing course — verify menu/page tree renders correctly
  2. Navigate to edit an article/block/component — verify the edit form loads with all fields
  3. Edit config, course settings, theme editor — verify full data is available
  4. Make an edit and return to menu — verify staleness check triggers a refresh
  5. Check browser network tab — initial load should use /api/content/tree/:id instead of /api/content/query

📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.

taylortom and others added 2 commits March 11, 2026 01:29
Replace full content fetch with lightweight tree endpoint. Editor now
loads projected fields via GET /api/content/tree/:courseId and fetches
full documents on demand when editing individual items. Staleness check
uses If-Modified-Since conditional requests for zero-cost polling.
Origin.editor.data.content is a plain Backbone.Collection with no url
property (created via native fetch in loadTree), so calling .fetch()
on it throws, causing an errorfetchingdata popup on every menu delete
despite the DELETE request succeeding.

Instead of re-fetching, remove the deleted item and its descendants
from the local collection directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the editor’s data-loading strategy to use a lightweight content tree endpoint for initial load and conditional requests for staleness checks, while lazily fetching full documents only when a user opens an item for editing.

Changes:

  • Replace the initial full content fetch with GET /api/content/tree/:courseId and store projected items in Origin.editor.data.content.
  • Update staleness detection to use If-Modified-Since conditional requests (304 when unchanged).
  • Ensure full models are fetched on-demand (await model.fetch()) before building scaffold forms in individual editor routes.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
app/modules/editor/global/editorDataLoader.js Implements tree loading + conditional staleness check; replaces previous full collection fetch logic.
app/modules/editor/article/index.js Fetch full article document before building the edit form.
app/modules/editor/block/index.js Fetch full block document before building the edit form.
app/modules/editor/component/index.js Fetch full component document before building the edit form (existing items only).
app/modules/editor/contentObject/index.js Fetch full page/menu document before building the edit form.
app/modules/editor/config/index.js Fetch full config document before building the edit form.
app/modules/editor/course/index.js Fetch full course document before building the settings form.
app/modules/editor/themeEditor/index.js Fetch full course + config before opening the theme editor UI.
app/modules/editor/contentObject/views/editorMenuView.js Updates client-side deletion handling to remove items from the in-memory tree/collections.
app/modules/editor/menuSettings/index.js Minor whitespace-only change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +95 to +98
var res = await fetch('/api/content/tree/' + courseId, {
headers: { 'If-Modified-Since': lastModified }
})
return res.status !== 304
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isOutdated() performs a fetch() that can reject (e.g. network error) but that call isn’t wrapped in a try/catch, and Preloader.load() awaits isOutdated() outside the surrounding try block. This can cause an unhandled rejection and prevent the editor from recovering gracefully. Consider catching request errors inside isOutdated() (and treating them as “outdated” or routing through handleError()), or move the isOutdated() call inside the existing try/catch in load().

Suggested change
var res = await fetch('/api/content/tree/' + courseId, {
headers: { 'If-Modified-Since': lastModified }
})
return res.status !== 304
try {
var res = await fetch('/api/content/tree/' + courseId, {
headers: { 'If-Modified-Since': lastModified }
})
return res.status !== 304
} catch (e) {
// On network or fetch error, force a reload next time
lastModified = undefined
return true
}

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +31
if (await isOutdated()) {
try {
await Promise.all([
new Promise (async (resolve) => {
const content = new ContentCollection(undefined, { _courseId: Origin.location.route1 });
content.queryOptions = { limit: 0 };
await content.fetch();
Origin.editor.data.content = content;
Origin.editor.data.course = content.findWhere({ _type: 'course' });
Origin.editor.data.config = content.findWhere({ _type: 'config' });
if(!Origin.editor.data.course || !Origin.editor.data.config) {
return handleError();
}
resolve()
}),
new Promise (async (resolve) => {
const componentTypes = new ContentPluginCollection(undefined, { type: 'component' });
await componentTypes.fetch();
Origin.editor.data.componentTypes = componentTypes;
resolve()
})
]);
} catch(e) {
return handleError();
loadTree(),
loadComponentTypes()
])
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the tree has changed, isOutdated() makes a conditional GET to /api/content/tree/:id that returns 200, and then Preloader.load() immediately calls loadTree() which does another GET to the same endpoint. This doubles requests on every refresh when content is stale. A more efficient approach is to combine the staleness check and load into a single request (e.g., have loadTree() send If-Modified-Since and early-return on 304, or have isOutdated() return the response/body for reuse).

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +62
async function loadTree () {
var courseId = Origin.location.route1
var res = await fetch('/api/content/tree/' + courseId)
if (!res.ok) throw new Error('Failed to fetch content tree')
lastModified = res.headers.get('Last-Modified')
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module introduces use of the global fetch API for editor data loading. fetch isn’t used elsewhere in app/, and there’s no fetch polyfill in the repo, while other data access uses Backbone/jQuery. To avoid runtime issues in environments without fetch (and to keep consistent request handling), consider switching these calls to $.ajax/Backbone sync, or explicitly adding/including a fetch polyfill as part of the app’s required libraries.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +68
var content = new Backbone.Collection(items.map(function (item) {
return new ContentModel(item)
}), { comparator: '_sortOrder' })
content.findWhere = Backbone.Collection.prototype.findWhere
content.where = Backbone.Collection.prototype.where
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In loadTree(), reassigning content.findWhere / content.where from Backbone.Collection.prototype is redundant (the instance already inherits these methods in Backbone 1.4.0) and adds noise. If the goal is to ensure models created later are ContentModels, it’d be clearer to construct the collection with { model: ContentModel, comparator: '_sortOrder' } and remove the manual method assignments.

Suggested change
var content = new Backbone.Collection(items.map(function (item) {
return new ContentModel(item)
}), { comparator: '_sortOrder' })
content.findWhere = Backbone.Collection.prototype.findWhere
content.where = Backbone.Collection.prototype.where
var content = new Backbone.Collection(items, {
model: ContentModel,
comparator: '_sortOrder'
})

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +7
define(function (require) {
var _ = require('underscore')
var Backbone = require('backbone')
var ContentModel = require('core/models/contentModel')
var ContentPluginCollection = require('core/collections/contentPluginCollection')
var Origin = require('core/origin')
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

editorDataLoader.js switches to a no-semicolon style and slightly different spacing in the define(function (require) { signature, which is inconsistent with surrounding editor modules (e.g. app/modules/editor/course/index.js uses semicolons throughout). Keeping style consistent within app/modules/editor will make diffs smaller and maintenance easier; consider retaining the existing semicolon-based style here.

Copilot uses AI. Check for mistakes.
…ader

isOutdated() now returns the fetched tree data on 200 so loadTree() can
reuse it instead of hitting the same endpoint a second time. The await
on isOutdated() is moved inside the existing try/catch so network errors
are handled gracefully. Also removes redundant Backbone method
reassignments in favour of the idiomatic model collection option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@taylortom
Copy link
Copy Markdown
Contributor Author

@copilot Thanks for the review — addressed the following in 0f08de3:

Double request on stale content (comment #2): isOutdated() now returns the parsed response body ({ items, lastModified }) when the tree endpoint returns 200, so loadTree() reuses it instead of making a second request to the same endpoint.

Unhandled rejection (comment #1): Moved the await isOutdated() call inside the existing try/catch block in load(), so network errors and non-ok responses are caught and routed through handleError(). Also added an explicit check for non-ok responses in isOutdated().

Redundant method assignments (comment #4): Removed the manual findWhere/where reassignments and passed model: ContentModel in the collection options instead.

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 27, 2026

@copilot Thanks for the review — addressed the following in 0f08de3:

Double request on stale content (comment #2): isOutdated() now returns the parsed response body ({ items, lastModified }...

Addressed the two remaining reviewer comments in e277615:

  • fetch → $.ajax (comment Fix references to EditorData plugins #3): Converted both HTTP calls to use $.ajaxloadTree uses success/error callbacks to capture Last-Modified from the response header, and isOutdated uses complete to correctly handle both 304 and 200 status codes.
  • Semicolons (comment Fix API filter params #5): Restored semicolons and define(function(require) spacing to match the original file style and surrounding editor modules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants