Repository navigation
feat: support widget ID aliases - #335
Open
brian-smith-tcril wants to merge 1 commit into
Open
brian-smith-tcril wants to merge 1 commit into
brian-smith-tcril wants to merge 1 commit into
Conversation
A widget operation can declare `idAliases`, other IDs the widget answers to. Relative operations (INSERT_AFTER, INSERT_BEFORE, REPLACE, REMOVE, OPTIONS) whose `relatedId` names an alias apply as if it named the widget's `id`, and `WidgetList.byId` and `withoutId` match aliases too. This is the widget-side mirror of slot `idAliases` (openedx#178): an app can rename a widget ID and keep the old one as an alias instead of silently breaking the site configs that target it. Resolves openedx#334 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
widget-id-aliases
branch
from
October 31, 2026 04:32
565a8a9 to
dcf8c80
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A widget operation can declare
idAliases, other ids the widget answers to. A relative operation (INSERT_AFTER,INSERT_BEFORE,REPLACE,REMOVE,OPTIONS) whoserelatedIdnames an alias applies as if it named the widget'sid, andWidgetList.byIdandwithoutIdmatch aliases too. This is the widget-side mirror of slotidAliasesfrom #178, so an app can rename a widget id and keep the old one as an alias for a release instead of silently breaking every site config that targets it. No change for widgets without aliases: every id comparison stays exact for them, and every API change is an optional field, an optional trailing parameter or one new exported helper.Resolves #334.
What changed
WidgetIdentityPropsandIdentifiedWidgetgainidAliases?: string[]; the TSDoc on the former documents the field and the deprecation pattern.matchesWidgetId(identity, id)inruntime/slots/widget/utils.tsxcompares an id against a widget'sidandidAliases.findRelatedWidgetIndex,byId,withoutIdand the relative-operation sort inuseSortedWidgetOperationscompare through it.createIdentifiedWidgetcopiesidAliasesonto the identified widget and passes it toWidgetProvider, whoseWidgetContextgainswidgetIdAliases.useWidgetOptionsForId(slotId, widgetId, widgetIdAliases?)takes the aliases as an optional third parameter;useWidgetOptionsreads them from the context. Two-argument callers are unchanged.relatedId: the first widget in list order, as duplicate ids resolve today.byId/withoutId, precedence in both orders andidentifiedinruntime/slots/widget/utils.test.tsx; a newruntime/slots/widget/hooks.test.tsxfor the sort and the options hooks; each of the five relatedId operations against an alias through site config on a renderedSlotinruntime/slots/Slot.test.tsx.Testing
npm run typecheck,npm run lintandnpm testpass. Manual pass on the shell dev site's slot showcase with its relative operations retargeted at aliases through a local edit: every section rendered as it does unedited, and a control pointed at an undeclared id was a no-op. The first consumer is openedx/frontend-app-learner-dashboard#878, adopting aliases for its two renamed header link ids.Decisions
Seven decisions made while implementing, recorded below.
Full decision log
One match helper, exported.
matchesWidgetId(identity, id)inruntime/slots/widget/utils.tsxreturnsidentity.id === id || (identity.idAliases?.includes(id) ?? false), with the parameter typed structurally as{ id: string; idAliases?: string[] }so both an operation carryingWidgetIdentityPropsand anIdentifiedWidgetfit. It is exported becausehooks.tsuses it for the sort and the options hook, and sincewidget/index.ts,slots/index.tsandruntime/index.tseach re-exportutilswithexport *, it is package API; the otherhasWidget*helpers in that file stay unexported because nothing outside the file reads them.The options hook takes the aliases as an optional third parameter, with no default.
useWidgetOptionsForId(slotId, widgetId, widgetIdAliases?)keeps the two-argument form working for existing callers. No= []default: a fresh array on every render would be a newuseMemodependency each time and recompute the merge on every render for every widget without aliases. The hook builds{ id: widgetId, idAliases: widgetIdAliases }for the helper instead.Precedence is list order.
findRelatedWidgetIndexreturns the first widget whose id or aliases match, which is what it did for duplicate ids before. A widget whose primary id matches does not outrank an earlier widget whose alias matches. The test covers both orders.The sort's guard is untouched.
isWidgetRelativeOperationstill narrows toINSERT_AFTERandINSERT_BEFORE, soREPLACE,REMOVEandOPTIONSget no dependency ordering, aliases or not. Pre-existing, and widening it changes which operations the sort reorders, so it is its own change if wanted.Docs are the TSDoc on
WidgetIdentityProps.idAliases. Written in the style of the per-member comments onWidgetOperationTypes(one paragraph, two spaces after a period, "ID" capitalised as those comments do). Nothing underdocs/or in the slot showcase documents slotidAliaseseither, so there was no prose to extend and no showcase entry is added.Tests follow the three existing shapes. The alias cases for
createWidgetsextendruntime/slots/widget/utils.test.tsxas two typedWidgetAppendOperationliterals, anapplyToAliasedWidgetsclosure that runscreateWidgetson them plus one operation, and an inline operation literal per case; the existingmakeAppendOphelper is left to the existing cases. The hooks get a newruntime/slots/widget/hooks.test.tsxin the shape ofruntime/slots/layout/hooks.test.tsx, with../hooksmocked. The end-to-end cases, each ofINSERT_AFTER,INSERT_BEFORE,REPLACE,REMOVEandOPTIONSwith an alias as itsrelatedIdapplied through site config to a renderedSlot, form a describe of their own inruntime/slots/Slot.test.tsxbeside feat: support slot ID aliases #178's slot-alias tests; theOPTIONScase reads the options back throughuseWidgetOptionsin the aliased widget. Every test name says which operation it runs.Identity reads in
createIdentifiedWidgetgo through one destructure.id,idAliases,roleandslotIdare on everyWidgetRendererOperation, so they are destructured once at the top and used in the returned widget and theWidgetProviderprops; before,keyused the local whilewidgetIdandrolereadoperation.again. The renderer fields (component,element,url,title) stay asoperation.reads inside their guards, since the guards narrowoperation, not a local. No value changes. Found in self-review; folded in because the function was already being edited.Manual testing
What the automated suite cannot show: the slot showcase rendering in a browser with its relative operations retargeted at widget id aliases, through the dev server and the real site config path.
Environment
The frontend-base checkout on
widget-id-aliasesat565a8a9,npm run dev(builds, then serves the shell's dev site athttp://apps.local.openedx.io:8080).A browser on
/slots(the slot showcase). The four sections under "UI Widget Operations" that use arelatedIdare "Slot with inserted elements", "Slot with replaced element", "Slot with removed element" and "Slot with widget options (default)".One local, uncommitted edit to
shell/dev/slotShowcase/app.tsxfor the alias boxes: the four targeted widgets (insertingChild2,replacingChild2,removingChild2,widgetOptionsChild2) gain anidAliasesof their own id with.legacyappended, and the fiverelatedIds (both inserts, the replace, the remove, the options) point at those.legacyids. No widget id changes. As a change indicator, the "UI Widget Operations" heading inshell/dev/slotShowcase/SlotShowcasePage.tsxbecomes "UI Widget Operations (alias edit live)". Reverted at the end. The edit as a diff:Verify by hand
/slots. Inserted: Widget One, Widget Five (WidgetOperationTypes.INSERT_BEFORE), Widget Two, Widget Four (WidgetOperationTypes.INSERT_AFTER), Widget Three. Replaced: Widget One, Widget Four (WidgetOperationTypes.REPLACE), Widget Three. Removed: Widget One, Widget Three. Widget options: Foo, Bar.INSERT_AFTERandINSERT_BEFOREthrough the alias — "Slot with inserted elements" is unchanged from the baseline: Widget One, Widget Five, Widget Two, Widget Four, Widget Three.REPLACEthrough the alias — "Slot with replaced element" is unchanged: Widget One, Widget Four, Widget Three.REMOVEthrough the alias — "Slot with removed element" is unchanged: Widget One, Widget Three.OPTIONSthrough the alias — "Slot with widget options (default)" is unchanged: Foo, Bar.REMOVEoperation'srelatedIdfrom...removingChild2.legacyto...removingChild2.missing, reload. "Slot with removed element" shows Widget One, Widget Two, Widget Three. So the previous box matched through the alias and nothing else.git checkout -- shell/dev/slotShowcase;git statusreports a clean tree.Left to the automated suite
byIdandwithoutId, precedence when an id and an alias both match,identifiedcarryingidAliases:runtime/slots/widget/utils.test.tsx.useWidgetOptionsreading aliases fromWidgetContext:runtime/slots/widget/hooks.test.tsx.Slot:runtime/slots/Slot.test.tsx.Results
2026-10-30, Brian. All eight boxes passed on the dev site at
565a8a9. Baseline matched. After the edit the dev server rebuilt on its own; a browser refresh, with no restart, showed the "(alias edit live)" heading, and all four sections rendered exactly as the baseline, so both inserts, the replace, the remove and the options each applied through the alias. Control: with theREMOVEpointed atremovingChild2.missing, Widget Two came back. Clean up left a clean tree.🤖 Generated with Claude Code