Skip to content

[FE] Show competency criteria whose gradeable subsection has been deleted #826

Open

Description

@thelmick-unicon

User Story

As a course author managing a competency's criteria, I want a criterion whose gradeable subsection has been deleted to be shown as unresolvable, in order to recognize a rule that a learner can no longer complete.

Acceptance Criteria

Scenario: A criterion whose subsection was deleted is still listed
  Given a criteria group contains a criterion for a gradeable subsection
    And that subsection has since been deleted from its course
  When I open that competency's criteria on the Competency Management page
  Then the criterion is listed in its rule box alongside the group's other criteria
    And in place of a subsection title it shows a "Content unavailable" chip, with the Warning icon
    next to the text

Scenario: An unresolvable criterion is distinguishable at a glance
  Given a criteria group contains one criterion whose subsection still exists and one whose
    subsection was deleted
  When I view that group
  Then the deleted subsection's criterion shows the "Content unavailable" chip and the Warning
    icon, which the other criterion does not
    And the criterion whose subsection still exists shows its subsection title as it does today

Scenario: The course the deleted subsection belonged to is named
  Given a criterion whose subsection was deleted
  When I view it
  Then the "From within ..." header of the rule box that holds the criterion names the course
    the subsection belonged to
    And two unresolvable criteria from different courses are told apart by their rule boxes'
    "From within ..." headers

Scenario: A course's criteria are replaced by a loading placeholder while its outline loads
  Given a criteria group contains criteria for subsections in a course
  When I view that group before the course's outline has loaded
  Then that course's criteria are replaced by one ghosted placeholder rule box showing a loading
    spinner, rather than a loading state on each criterion
    And no criterion is labelled "Content unavailable" while the outline is still loading
    And once the outline has loaded, the placeholder is replaced by the criteria, each showing
    its subsection title or the "Content unavailable" chip

Scenario: A failed or refused subsection lookup shows the same "Content unavailable" chip
  Given a criterion points at a subsection in a course whose outline cannot be read, either
    because the request fails or because I do not have access to that course
  When I view its group
  Then the criterion shows the "Content unavailable" chip with the Warning icon, the same as a
    criterion whose subsection was deleted, because the page cannot tell the two cases apart
    And the group's other criteria still render normally

Scenario: A group whose every criterion is unresolvable still renders
  Given every criterion in a criteria group points at a deleted subsection
  When I view that group
  Then the group renders with each of its criteria showing the "Content unavailable" chip
    And the group is not shown as empty

Scenario: The unresolvable state is available to a keyboard and screen reader user
  Given a criterion whose subsection was deleted
  When I move through its group using a keyboard or a screen reader
  Then the "Content unavailable" state is conveyed in text, not by color or the icon alone

Scenario: The unresolvable criterion is usable on a small screen
  Given a criterion whose subsection was deleted
  When I view its group on a small screen
  Then its "Content unavailable" chip and its rule box's "From within ..." header stay readable
    without the layout overflowing

Description

The Competency Management page lists a competency's criteria inside the criteria groups, and
shows each criterion as the gradeable subsection it points at (#672). A criterion identifies that
subsection by a usage key stored on the tagging row, with no database reference to the
content itself, and the page resolves the key to a title by looking it up in the
course's outline.

Deleting a gradeable subsection from a course leaves the criterion, its tagging row,
and every learner's recorded competency status in place. This is done so that mastery a learner has already demonstrated is not taken away because someone edited the course afterwards. What the deletion does remove is the assignment's entry in the course outline, and that entry is where the page gets the assignment's name from. With no name to display, the requirement isn't visable on the page. So a competency can end up carrying a requirement that can't ever meet again, with nothing on the page to indicate that.

This ticket makes those broken criteria visible.

Figma Designs:
U1 · Content unavailable
U3 · Loading

Image Image
Technical Details

This section is background and a suggested approach, not the ticket's source of truth. The User
Story and Acceptance Criteria define what must be true when the work is done.

In short

Nothing on the backend hides these criteria, so no backend change is needed. The tagging row
a criterion points at stores the subsection as a plain opaque string with no foreign key to course
content and no resolver, and criterion creation validates only that the string parses as a usage
key whose course matches the group's scope. Deleting the subsection leaves every one of those rows
untouched, so the endpoint that returns a competency's groups and criteria returns an orphaned
criterion like any other. The one backend requirement is a negative one, that this read path never
acquires a filter on whether the content still exists, which is recorded in Open Questions rather
than built here.

How the page decides a criterion is unresolvable. Subsection titles are resolved on the client
by taking the criterion's usage key, reading the outline of the course that key names, and finding
the matching node. A criterion counts as unresolvable when that outline was read and contains no
node with that usage key, and also when the outline could not be read at all, because the request
failed or the author has no access to the course: from the client the two cases look the same, so
both show the same "Content unavailable" chip. A request still in flight is the one state that is
not a verdict: while a course's outline loads, that course's criteria are replaced by one ghosted
placeholder rule box with a LoadingSpinner, so no criterion is labelled before its outline has
been checked.

Which course outlines the page loads, and through which query. Parse the course key out of
each criterion's usage key, reduce that to the distinct set of courses the competency's criteria
reference, and read one outline per course. The page needs exactly these outlines anyway to title
the criteria that do resolve, so this adds no request that titling does not already require. Read
them through the same cached query the course browse panel uses rather than a second request path,
so a course already expanded in the browse panel costs nothing to resolve here.

What the criterion shows in place of a title. The "Content unavailable" chip, with Paragon's
Warning icon next to the text, so the state is named in words rather than by an icon or a color
alone. The course the subsection belonged to is not repeated on the criterion: the rule box that
holds it already names the course in its "From within ..." header, which is how an author with
several unresolvable criteria tells which course each one came from.

Implementation specifics

  • Extend the Selected Content components [FE] Create & View Competency Criteria #672 adds under
    src/taxonomy/competency-management/criteria-groups/, rather than introducing a parallel
    rendering path for these criteria.
  • Derive the course key with getCourseKey from src/generic/key-utils.ts. It throws on a
    string it cannot parse, so wrap the call: a criterion whose stored key does not parse as a course
    block key is unresolvable with no course to name, and must not break the surrounding list.
  • Read outlines through the query the course browse panel already uses.
    useCourseOutlineIndex (src/course-outline/data/outlineIndexQuery.ts) wraps
    getCourseOutlineIndex and keys on courseOutlineQueryKeys.index(courseId). The number of
    courses is only known at render time, so a hook call per course is not possible: build the batch
    with useQueries over that same key factory and query function, following the batching precedent
    in src/content-tags-drawer/data/apiHooks.ts. Reusing the key factory is what makes these
    entries share the browse panel's cache instead of duplicating its requests. Carry the hook's own
    retry: false and pass refetchOnMount: false, matching how the browse panel reads outlines.
  • Search the returned structure's full section-to-subsection tree for the usage key, starting
    at courseStructure on the response envelope (CourseOutline, src/course-outline/data/types.ts)
    and recursing through each node's childInfo.children; the node fields come from XBlockBase and
    XBlock in src/data/types.ts. Do not assume a position or depth, and do not match on title.
  • Branch on the query state explicitly. A pending outline query renders the ghosted placeholder
    rule box with a LoadingSpinner for that course's criteria. A successful read with no matching
    usage key and a failed read, including a permission failure, both render the "Content
    unavailable" chip.
  • Do not block the criteria list on the outline queries. Render the groups at once, with each
    course's criteria behind its placeholder, then replace each placeholder with the criteria as that
    course's outline settles.
  • Scope a failed outline read to the criteria that depend on it. One unreadable course marks
    only its own criteria "Content unavailable" and must not take down the rest of the competency's
    criteria.
  • Memoize the distinct course-key list on the criteria query's own data reference, not on an
    array recomputed inline in the same render. A dependency rebuilt every render defeats the
    memoization silently, with no type error or failing test to catch it, and here it would refire a
    batch of outline queries.
  • Use Paragon's Warning icon from @openedx/paragon/icons alongside the text label, not in
    place of it, and do not distinguish the state by color alone.
  • Carry the accessible text in a visually hidden sr-only sibling span, not in an aria-label
    on the wrapper.
    A plain span or div takes the implicit generic role, which prohibits
    aria-label and is not reliably announced. CompetencyTreeItem.tsx in the same directory already
    does this for the tree's own accessible labels.
  • Style the state by overriding Paragon's own design tokens, not the resolved CSS properties.
    Trace the token's resolution in the installed theme first: if it already resolves to the intended
    value, the override is dead code and should not be added. Use a relative unit (rem, %) for any
    typography token, as Paragon's own defaults do, even if a design spec names a pixel value.
  • Prefer Paragon's grid for the small-screen behavior over new media queries, as the
    associations panel does with Row/Col. If a component-local SCSS file is added and does use
    Paragon's custom-media breakpoint names, add
    @use "@openedx/paragon/styles/css/core/custom-media-breakpoints.css" in that file: a component
    SCSS imported from its own .tsx compiles as its own root stylesheet and does not inherit the
    @use in src/index.scss, and an unresolved custom-media name is not a syntax error, so the rule
    compiles and then silently never matches. The lint and unit-test commands do not compile Sass, so
    verify any new at-rule by test-compiling the file exactly as the bundler imports it.
  • Key any selector for a criterion inside a nested group off the class marking the group unit
    (for example .criteria-group > ul), not a fixed structural depth (> ul > li > ul), which
    matches one nesting level and stops applying past it.
  • Do not add a backend existence flag to the criteria read endpoint. That endpoint lives in
    openedx-core, which must not import from openedx-platform (enforced by .importlinter), so
    it cannot resolve a usage key against the modulestore or the content search index. Resolution
    stays on the client, which is also where titling already happens.
  • Tests: a criterion whose usage key is absent from a successfully read outline renders the
    "Content unavailable" chip and the warning icon; a criterion present in the outline renders its
    title unchanged; a pending outline query renders the ghosted placeholder rule box with a spinner
    for that course and no "Content unavailable" chip; a failed outline query and a 403 each render
    the "Content unavailable" chip; a group whose every criterion is unresolvable renders all of them;
    an unparseable stored key renders the chip without throwing; several criteria in one course fire
    a single outline query rather than one per criterion, and re-rendering fires no further ones; the
    "Content unavailable" state is present as text in the DOM; the chip and the rule box header stay
    within the layout at a small viewport width.
  • Out of scope: removing an unresolvable criterion, which moved to a follow-up ticket in the
    10/13 rescope; a tooltip or other way to expose the stored usage key, which is its own follow-up;
    the learner-facing progress surface, which faces the same situation and has its own design work
    ahead of it; the warning shown before deleting a subsection on the Course Outline page; any job
    that prunes or reports orphaned criteria in bulk; and any competency-level indication that a
    competency's rules can no longer be satisfied.
Files to create and modify

src/taxonomy/competency-management/ holds the Competency Management page, its competency tree,
and its shared messages.ts (#680), with the associations panel and course browse panel in its
associations/ and course-search/ subdirectories (#670). The Selected Content components this
ticket extends are added by #672 in the same tree; the rows below extend them rather than replacing
them.

New files

File Purpose
src/taxonomy/competency-management/criteria-groups/useSubsectionResolution.ts Batches one course-outline query per distinct course key across a competency's criteria and reports each criterion as resolved, unavailable, or pending. Sits alongside the associations/ and course-search/ directories of the same page
src/taxonomy/competency-management/criteria-groups/useSubsectionResolution.test.ts Covers the three resolution states and the unparseable-key case

Modified files

File Nature of modification
src/taxonomy/competency-management/criteria-groups/SelectedContentItem.tsx Render the "Content unavailable" chip with the warning icon in place of a subsection title, and the ghosted placeholder rule box with a spinner while the course's outline loads
src/taxonomy/competency-management/criteria-groups/SelectedContentList.tsx Pass each criterion's resolution result through to its item
src/taxonomy/competency-management/messages.ts Copy for the "Content unavailable" label
src/taxonomy/competency-management/criteria-groups/*.test.tsx Component coverage for the scenarios above, including the small-viewport and text-in-DOM assertions
Context
  • [FE] Create & View Competency Criteria #672 lists a group's criteria in its Selected Content area and owns the components this ticket
    extends.
  • [FE] Competency Management Course Search/View #670 provides the associations panel and the course browse panel, and establishes reading a
    course outline lazily per course through useCourseOutlineIndex.
  • [FE] Display Competency ID on the Competency Management page #680 provides the Competency Management page, its competency tree, and the page's messages.ts.
  • src/generic/key-utils.ts for getCourseKey; src/course-outline/data/outlineIndexQuery.ts for
    useCourseOutlineIndex and src/course-outline/data/queryKeys.ts for the key factory to batch
    over; src/course-outline/data/types.ts and src/data/types.ts for the outline envelope and its
    node fields; src/content-tags-drawer/data/apiHooks.ts for the useQueries batching precedent.
  • src/taxonomy/competency-management/CompetencyTreeItem.tsx for the sr-only accessible-label
    precedent this page already follows, and src/taxonomy/competency-management/messages.ts for
    where its copy lives.
  • .claude/frontend-app-authoring-architecture-overview.md for the authoring frontend's conventions
    and how it consumes this library's APIs.

Open Questions

  • The endpoint that returns a competency's criteria groups and criteria must return criteria whose
    stored subsection key no longer resolves to live content, rather than filtering them out or
    failing on them. Nothing in the data model filters them today, so this is a requirement to record
    on that endpoint's ticket rather than work for this one. Owner: BA and architect, to fold into
    that ticket and link its issue number here.

Activity

  1. moved this from Needs additional details to Drafting in Competency Criteria and Student Progresson Sep 21, 2026
  2. changed the title [-][Placeholder] Drafting for UC10.21[/-] [+][FE] Show and remove competency criteria whose gradeable subsection has been deleted[/+] on Sep 29, 2026
  3. moved this from Ready for Community Review to Final Axim Review in Competency Criteria and Student Progresson Oct 9, 2026
  4. thelmick-unicon commented on Oct 13, 2026

    @thelmick-unicon
    Author

    The discussion period for this ticket is now complete, and we appreciate all the feedback. We'll be moving forward with development based on the current ticket pending final Axim sign-off. That said, the conversation doesn't have to stop. If you still have feedback, please continue to add to this ticket. We'll review anything new and determine whether it fits within the current timeline or makes more sense as its own ticket for future work. Thanks again for helping make this better!

    @jmakowski1123 This is ready for your final review. Thank you!

  5. illiphilli commented on Oct 13, 2026

    @illiphilli

    @thelmick-unicon @mgwozdz-unicon (cc @AShatsila) following up on our scope conversation: I agree #826 should ship just the simple chip, with the tooltip and removal as follow-up tickets. Here's how I'd split it.

    #826 (MVP): a simple status chip. A criterion whose subsection was deleted shows a chip reading "Subsection not found", with Paragon's Warning icon next to the text (not in place of it). It's in the criteria group (rule box) beside the group's other criteria. Its "From within …" header already names the course. I've updated the frames to match: U1 · Not found and U3 · Checking / couldn't check now show the chip with no tooltip, ✕ or Retry. The follow-up designs sit to their right on the same row, as F1–F3.

    Follow-up tickets:

    1. Tooltip (F1): hovering or focusing the chip shows the course name and usage key, so support can identify the deleted content.
    2. Remove an unresolvable criterion: the chip's ✕ and the standard remove dialog, which repeats the course name and usage key (F2). This would take the "Removing an unresolvable criterion" scenario and the focus scenario after removal out of [FE] Show competency criteria whose gradeable subsection has been deleted #826.

    Questions:

    1. The other lookup states. The AC that a failed or refused lookup isn't reported as unresolvable still seems needed in [FE] Show competency criteria whose gradeable subsection has been deleted #826. Otherwise a network error would show authors a phantom "not found". So I'd keep two more chip states, with no tooltip or Retry:

      • "Checking subsection…" while loading
      • "Couldn't check subsection" if the lookup fails or I have no access to the course (U3)

      That merges the separate no-access state into "Couldn't check". The separate state is now a possible third follow-up (F3). Retry could go with F1 or F3. Does that work?

    2. "Two unresolvable criteria from different courses are told apart by that course name." A rule box belongs to one course, and its header names it, so I'd treat that scenario as covered by the header. OK?

    3. Small screens: chips hug their label, never truncate, and wrap onto their own line in the rule box. Is that written rule enough for the small-screen AC, or do you want a mobile frame?

  6. mgwozdz-unicon commented on Oct 15, 2026

    @mgwozdz-unicon
    Contributor

    @illiphilli thanks for the split, I agree with shipping the simple chip in #826 and moving the tooltip and removal into follow-up tickets. Answers to your three questions:

    1. The other lookup states. I'd rather not add "Checking subsection…" and "Couldn't check subsection" as chip text. I looked at what the criteria list in #672 already does (frontend-app-authoring #3256, still open). Each course group in the list loads its course outline once, and any criterion whose subsection isn't found in that outline gets a chip reading "Content unavailable". That chip shows up when the subsection was deleted, and also while the outline is still loading or when the request fails, because in all three cases the page has no subsection title to show.

    The page can't actually tell a deleted subsection apart from a course outline it couldn't read, so I'd treat those two cases the same and keep "Content unavailable" for both, adding the Warning icon next to the text. The loading case is the only one that's misleading: unless the outline is already cached, every criterion in a course would briefly read "Content unavailable" when the page opens. For that case I'd show the small LoadingSpinner the page already uses (in the course browse panel and while the criteria list loads) in place of the chips, until that course's outline arrives. All of a course's criteria come from one outline request, so the spinner would cover that whole course group rather than each chip.

    This means using "Content unavailable" instead of "Subsection not found", which changes your U1 frame and is wording product hasn't seen yet. I prefer it because it doesn't suggest the content was deleted, which the page can't prove, but I'm open to either. @jmakowski1123, does "Content unavailable" work for you as the label?

    2. Different courses. Agreed. A rule box belongs to one course and its "From within …" header names it, so the header covers that scenario. We can reword that acceptance criterion to say so.

    3. Small screens. The written rule is enough, no mobile frame needed. Mobile layout is a nice-to-have for this work rather than a requirement.

  7. moved this from Final Axim Review to In Progress in Competency Criteria and Student Progresson Oct 21, 2026
  8. jmakowski1123 commented on Oct 21, 2026

    @jmakowski1123

    "Checking subsection" chip seems like overkill, and I agree with the spinner instead. I'm assuming/hoping the call won't take much longer than a second?

    I prefer Content Unavailable because it also is more flexible and can be applied to later designs if we extend down to individual problems or components.

  9. moved this from In Progress to Unicon Code Review in Competency Criteria and Student Progresson Oct 22, 2026
  10. moved this from Unicon Code Review to Final Axim Review in Competency Criteria and Student Progresson Oct 27, 2026
  11. changed the title [-][FE] Show and remove competency criteria whose gradeable subsection has been deleted[/-] [+][FE] Show competency criteria whose gradeable subsection has been deleted[/+] on Oct 28, 2026
  12. mgwozdz-unicon commented on Oct 28, 2026

    @mgwozdz-unicon
    Contributor

    @jmakowski1123 the ticket now reflects both of your points: the label is "Content unavailable" with the Warning icon, and the loading state is a spinner rather than a "Checking subsection…" chip. The acceptance criteria, the technical details, the Figma links (U1 and U3), and the screenshots were all updated, and removal of an unresolvable criterion is now listed as out of scope and moved to a follow-up ticket, per Phil's 10/13 split. Could you take one more look and give your final approval?

    On your timing question: we have not measured it, but we can say what the call is. It is Studio's course outline request, the same one the Course Outline page makes when you open a course, and it returns the course's sections and subsections. So the spinner lasts about as long as opening that course's outline in Studio does: quick for a typical course, longer for a very large one. Two things keep that wait from happening often. The page makes one request per course that has criteria in the competency, not one per criterion, and all of those requests run at the same time. And the result is cached in the browser for an hour, so moving between competencies that share a course, or coming back to the page, does not refetch it.

    @AShatsila the ticket text changed after you opened AShatsila/frontend-app-authoring#8, so please check your PR against the current acceptance criteria. Reading your PR description against the ticket as it stands today, three places look different:

    1. Removal. The ticket now lists removing an unresolvable criterion as out of scope, with its own follow-up ticket. Your PR keeps the "x" on the "Content unavailable" chip and argues for keeping it. We should settle which way this goes before review rather than during it.
    2. A course whose outline cannot be read. The ticket says a failed or refused outline lookup shows the same "Content unavailable" chip as a deleted subsection, because the page cannot tell the two apart. Your PR hides that course's section instead, as the page did before.
    3. The loading state. The ticket describes a ghosted placeholder rule box with a spinner for each course whose outline is still loading. Your PR shows the list's existing spinner in place of the empty state and mounts each course section only after its outline arrives.

    If any of these is a case where the implementation is right and the ticket should change, say so here and we will update the ticket instead.

  13. jmakowski1123 commented on Oct 28, 2026

    @jmakowski1123

    Sounds good, this is approved.

  14. moved this from Final Axim Review to Unicon Code Review in Competency Criteria and Student Progresson Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions