openedx / openedx/openedx-core
[FE] Build the competency-selection tree, Course Search, and gradeable-subsection browse UI for Competency Criteria Associations
@AShatsila is already working on this.
Since Sep 1, 2026.
- Dominant language
- Python
- Stars
- 10
- Forks
- 32
- Avg merge
- 2d 17h
- Merged PRs (30d)
- 12
Description
Use Case
As a Platform Administrator configuring Competency Criteria associations, I want to select a competency from the taxonomy's competency tree and then search the courses I have access to (filtered by course-run start date), browse a course's sections and gradeable subsections, so that I can find the exact gradeable subsection to attach criteria to without opening each course individually.
Acceptance Criteria
Scenario: Competency tree lists the taxonomy's competencies
Given I open the Manage & Apply Competencies page for a competency taxonomy
Then the left panel shows that taxonomy's competencies as a nested tree
Scenario: Selecting a competency drives the right-hand panel
Given the competency tree is shown
When I select any competency in the tree
Then it becomes the active competency
And the right-hand course-browse panel loads for that competency
And this is true for every row in the tree, whether or not it has sub-competencies
Scenario: A competency with sub-competencies is selectable like any other
Given the competency tree shows a competency that has sub-competencies beneath it
When I click that competency's row
Then it becomes the active competency
And the right-hand course-browse panel loads for it
And it behaves no differently from clicking a competency that has no sub-competencies
Scenario: Clicking a row selects it without expanding it
Given a collapsed competency that has sub-competencies
When I click its row
Then it becomes the active competency
And its sub-competencies stay collapsed
Scenario: The expand/collapse control only expands and collapses
Given a competency that has sub-competencies
When I click its expand/collapse arrow
Then its sub-competencies show or hide
And the active competency does not change
And the right-hand panel does not change or reload
Scenario: A row's two controls are separately reachable and distinguishable
Given the competency tree is shown
When I move through a row that has sub-competencies using a keyboard
Then I can reach both selecting the competency and expanding it as distinct controls
And each is announced as a distinct action
Scenario: No competency selected yet
Given I open the page and have not selected a competency
Then the right-hand column is not rendered at all
And nothing occupies that space: no panel, no background, no border, and no prompt
And the competency tree is the only content on the page
And no course search runs
Scenario: The right-hand column appears on the first selection
Given I have not selected a competency and the right-hand column is absent
When I select a competency
Then the right-hand column renders for the first time
And it shows the course-browse panel for that competency
Scenario: Search courses by title
Given I have selected a competency and am viewing the course-browse panel
When I enter a title filter
Then the course list narrows to accessible courses whose title matches
Scenario: Filter courses by start-date range
Given I am viewing the panel
When I set a start and end boundary in the date-range picker
Then the list shows only courses whose course-run start date falls within that range
And the UI makes clear the filter is on course start date
Scenario: Default view shows all accessible courses
Given I have entered no title filter and no date range
When the panel loads
Then it shows all courses I have access to
Scenario: Expand a course to browse its gradeable subsections
Given a course appears in the results
When I expand it
Then its sections and gradeable subsections are displayed
And the course and section rows are navigation only, not selectable as association targets
Scenario: Course search fails
Given the #669 course-search call fails
Then an inline error state is shown, distinct from an empty-results state
Scenario: No matching courses
Given the search/filter matches no accessible course
Then an empty state is shown
Scenario: Small-screen rendering
Given I view the page on a small screen
Then the competency tree, search, date picker, and expand/collapse remain usable
Designs
Description
Current state
No implementation exists. #623 built only a placeholder Competency Management page (/taxonomy/:taxonomyId/competencies) whose real content is deferred to future tickets; this is one. There is no left-hand competency tree in code yet: #648 designs the shell and that tree, but no implementation ticket builds it, and this is the first ticket that needs a competency actually selected. Studio's existing StudioHome course listing is general and competency-unaware, with no date-range filter and no drill-down into a course's gradeable structure.
Requested change
Build the Manage & Apply Competencies associations workspace: a minimal two-panel layout with a competency-selection tree on the left and the course-browsing panel on the right, driven by the selected competency.
- Minimal competency-selection tree (left panel). Render the current taxonomy's competencies as a selectable tree (nested competencies with their codes). Selecting a competency sets the active competency and populates/refreshes the right-hand panel against it. Reuse the existing tag-tree rendering; this is the minimal functional tree, not the finished shell. Expand/collapse polish, switcher styling, empty-state design, and responsive spec are #648's; the Competency ID badge on rows is a separate ticket's. Neither is built here.
- Course search with a start-date range filter (right panel). A title search plus a date-range picker, scoped to the selected competency. Use React Date Picker lightly styled with Paragon (Paragon has no native range picker). For MVP the range filters on course-run start date only; make that explicit to the user ("courses whose start date falls between X and Y"). Results come from #669 (paginated accessible courses).
- Browse each course's outline. Expanding a course reveals its sections and gradeable subsections (from the course outline — see #669 /
CourseIndexView). Sections are navigation; only gradeable subsections are the actionable leaves. For this ticket the subsections are read-only (browse only); making them actionable for association is #672.
Out of scope
- The full Manage & Apply shell design and tree polish — the switcher styling, expand/collapse-all affordances, empty states, and responsive spec (#648). This ticket builds only the minimal functional tree + selection needed to drive the right panel.
- The Competency ID badge on tree rows (a separate ticket).
- Any backend endpoint — this consumes #669 (courses + outlines) and reuses the existing taxonomy tags API for the tree; it builds none.
- Competency-to-course or section-level association — only gradeable subsections are associable (confirmed).
- The select/deselect-association UI itself — that is ticket #672, a separate design/build ticket. This ticket (#670) ends at browsing-and-surfacing: it renders the gradeable-subsection rows and nothing more. #672 adds its own select control to those rows.
- End-date filtering (deferred past MVP).
- The "Programs" button / program "P" icon, and any course-level associate button — these were removed from the design; do not build them regardless of what a screenshot shows.
- The "Demonstrate Mastery For" rule header and its controls, and "Import Competency Framework" — separate, unrelated tickets.
Open Questions
- [non-blocking, owner: architect] Boundary with the #648 shell implementation. #648 is being reframed as a parent UX-design ticket with implementation children, one of which builds the full shell + competency tree. If that shell-implementation child lands, this ticket's minimal tree should be absorbed/superseded by it, not duplicated. Until it exists, the minimal tree lives here. Confirm the intended sequencing so the tree is built once.
- [BLOCKING, owner: BA] What was this ticket's "different confusion"? An earlier note flagged this ticket for redoing "due to a different confusion" without specifying what. This rewrite assumes it's the course-vs-subsection level correction (associate at gradeable-subsection level; courses/sections are navigation). Confirm before this is treated as final.
- [BLOCKING, owner: architect/implementer] Where does the outline come from? Either #669 returns outlines inline, or this UI lazily calls the existing
CourseIndexView(GET /api/contentstore/v1/course_index/{course_id}) per expanded course. This ticket's data layer should be written to whichever #669 lands on. [recommended: lazy per-course CourseIndexView call on expand — lighter, reuses an existing endpoint] - [non-blocking, owner: designer] Figma refresh. The Figma is being updated (date picker representation; removing Programs button / P icon / course-associate button). Low rework risk for the tree/search/outline structure; re-verify the out-of-scope removal list against the refreshed Figma before build.
Context
- Decision: #648 is being reframed as a parent UX-design ticket with implementation children. Because this is the first ticket that needs a competency selected, the minimal tree + selection belongs here rather than deferred to a later ticket.
- Decision (MVP): the course filter is on course-run start date only; end-date filtering is deferred. React Date Picker (lightly styled with Paragon) fills the gap since Paragon has no native range picker.
- Decision: criteria are associated only at the gradeable-subsection level; courses and sections are navigation, not association targets.
- Competency tree data: the taxonomy tags list API already returns a competency taxonomy's tags (already satisfied, no backend work); the MFE data layer already carries them (
TagDataviauseTagListDatainsrc/taxonomy/data/apiHooks.ts, nested byTagTreeinsrc/taxonomy/tag-list/tagTree.ts). No backend or data-layer change for the tree. CourseIndexView(GET /api/contentstore/v1/course_index/{course_id},openedx-platform): existing full course-outline endpoint (sections→subsections, withgradedflag); the likely per-course outline source.- Tree-rendering precedents (frontend; none a drop-in):
src/taxonomy/tag-list/tagTree.ts(TagTreenests a taxonomy's tags — the competency tree's basis);src/taxonomy/tree-table/NestedRows/getSubRows(arbitrary-depth recursion, bundled with tag-CRUD context to strip);src/course-outline/OutlineTree.tsx(real 3-level course tree, hardcoded depth and dnd/XBlock-coupled). ReuseTagTreefor the competency tree; borrow the recursion pattern for the course outline. src/generic/datepicker-control/DatepickerControl.jsx(wrapsreact-datepicker): the single-date picker to compose into a from/to range.- #623: the placeholder Competency Management page this workspace lives inside.
Technical Notes
Files to Create
| File | Purpose |
|---|---|
src/taxonomy/competency-management/associations/CompetencyAssociationsPanel.tsx |
Two-panel container: left competency tree + right course-browse panel; owns the selected-competency state that drives the right panel |
src/taxonomy/competency-management/associations/CompetencyTree.tsx |
Minimal selectable competency tree for the current taxonomy; reuses TagTree/tree-table recursion; emits the selected competency. No ID badge (separate ticket), no CRUD affordances |
src/taxonomy/competency-management/course-search/index.ts |
Public interface exporting the browse panel component |
src/taxonomy/competency-management/course-search/CourseSearchBrowse.tsx |
Search + date-range filter + paginated course list; each row expands to the outline tree; takes the active competency as input |
src/taxonomy/competency-management/course-search/CourseOutlineTree.tsx |
Recursive section→gradeable-subsection renderer; borrows NestedRows' recursion pattern, no TanStack/CRUD context |
src/taxonomy/competency-management/*/messages.ts |
i18n strings |
src/taxonomy/competency-management/course-search/data/api.ts |
getCourseSearchResults(params) (#669), getCourseOutline(courseKey) (CourseIndexView or #669 inline, per decision) |
src/taxonomy/competency-management/course-search/data/apiHooks.ts |
React Query hooks for course search and outline |
src/taxonomy/competency-management/course-search/data/queryKeys.ts |
Query-key factory |
src/taxonomy/competency-management/**/*.test.tsx |
RTL: tree renders + selection drives the panel + nothing rendered for the right-hand column before a selection; search, date filter (start-date semantics), expand, empty/error states |
Files to Modify
| File | Nature |
|---|---|
src/taxonomy/competency-management/index.ts |
Re-export CompetencyAssociationsPanel alongside CompetencyManagementPage (from #623) |
| the #623 Competency Management page component | Mount CompetencyAssociationsPanel as the page's real content, replacing the placeholder |
Implementation Notes
Competency tree (minimal). Reuse useTagListData (src/taxonomy/data/apiHooks.ts), which already returns a taxonomy's tags, and TagTree (src/taxonomy/tag-list/tagTree.ts) to render the current taxonomy's competencies as a nested, selectable tree. The taxonomy id comes from the /taxonomy/:taxonomyId/competencies route (#623). Track the selected competency in CompetencyAssociationsPanel state and pass it (tag id / external_id) to the browse panel as its active competency. Keep it minimal: no ID badge (separate ticket), no create/remove or other CRUD affordances, no switcher/empty-state/responsive polish (#648). Until a selection is made, the right panel is not rendered at all: no container, no background, no border, and no placeholder or prompt inside it. The right-hand column mounts only once a competency is selected, and no course query runs before that.
Course browse. Compose two data sources client-side: #669 for the course list (start-date filter, pagination) and the outline per course (lazily on expand, via CourseIndexView unless #669 returns it inline).
Date filter: compose two DatepickerControl instances into a from/to range bound to #669's start-date parameter; label it as filtering on course start date. Don't send an end-date filter (MVP).
Build CourseOutlineTree as a plain recursive component (borrow NestedRows' childRowsData/depth shape; skip its TanStack table and CRUD context). Filter outline nodes to sections (navigation) and gradeable subsections (the actionable leaves) using the outline's graded flag; don't render unit/component levels. Subsections are read-only here and emit nothing: no row click handler and no selection callback. #672 adds its own per-subsection select control to these rows when it lands, calling associateSubsection(objectId, courseId) and hiding that control per subsection according to the author's write access to the course. Neither the course id nor that permission is expressible through an always-on click on the whole row, so do not build one here in anticipation of #672.
MFE-only; no ADR. Reuses frontend-app-authoring conventions (React Query, feature-folder, Paragon, defineMessages).
Example Resolution Prompt
In
frontend-app-authoring, build the Competency Criteria associations workspace atsrc/taxonomy/competency-management/.CompetencyAssociationsPanel.tsx: a two-panel layout that owns the selected-competency state. Left panelCompetencyTree.tsx: render the current taxonomy's competencies (taxonomy id from the/taxonomy/:taxonomyId/competenciesroute, #623) as a minimal selectable nested tree, reusinguseTagListData(src/taxonomy/data/apiHooks.ts) andTagTree(src/taxonomy/tag-list/tagTree.ts); selecting a competency sets it active and drives the right panel; no ID badge (a separate ticket), no CRUD, no shell polish (#648). Right panelcourse-search/CourseSearchBrowse.tsx: until a competency is selected the whole right-hand column is not rendered at all (no container, background, border, or prompt) and no query runs; once selected, a title search + a from/to date-range filter built from twosrc/generic/datepicker-control/DatepickerControl.jsxinstances, bound to #669's course-search endpoint (accessible courses filtered by course-run start date only; label it as such;). Paginated course list; each row expands. On expand, renderCourseOutlineTree.tsx: fetch the course outline (via the existingCourseIndexView,GET /api/contentstore/v1/course_index/{course_id}, unless the team has #669 return it inline — confirm), a plain recursive renderer (model onsrc/taxonomy/tree-table/NestedRows.tsx's recursion, without its TanStack table orTreeTableContext) showing sections (navigation) and gradeable subsections (read-only leaves, filtered by the outlinegradedflag). Gradeable subsections are read-only leaves with no click handler and no selection callback; #672 adds its own select control to these rows later. Mount the panel as the #623 page's real content. Do NOT build any backend endpoint, any course-level or section-level association, a Programs button, or a course-associate button. Tests: tree renders and selection drives the panel, nothing rendered for the right-hand column before a selection, title search, start-date filtering, expand-to-outline, empty and error states, small-screen.
Blocked by: #669 (the accessible-courses-by-date endpoint + course outlines) — a TBD contract; wire against it and flag real paths as fill-in-later. The competency tree itself has no new backend dependency — it reuses the existing taxonomy tags list API (a competency is a Tag in a competency taxonomy; that endpoint already returns them).
Repo: frontend-app-authoring. MFE-only. This ticket is #670; the select/deselect-association UI is #672.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.