feat(authz): add scope selection step to the Role Assignment Wizard#111
Conversation
|
Thanks for the pull request, @bra-i-am! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
9ff96b8 to
b193f35
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #111 +/- ##
==========================================
+ Coverage 96.70% 97.34% +0.63%
==========================================
Files 81 89 +8
Lines 1880 2106 +226
Branches 423 457 +34
==========================================
+ Hits 1818 2050 +232
+ Misses 59 53 -6
Partials 3 3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
cfad951 to
e1967e1
Compare
e1967e1 to
a9269c9
Compare
1867e05 to
71230b9
Compare
|
@bra-i-am Could you rebase please? |
11d9fa3 to
2a52e40
Compare
|
@jacobo-dominguez-wgu, sorry, I had some issues solving the conflicts, but now this PR is ready for review |
| } | ||
|
|
||
| const ScopeCheckboxItem = ({ scope, checked, onToggle }: ScopeCheckboxItemProps) => ( | ||
| <div className="mb-2"> |
There was a problem hiding this comment.
I just compared with the design and noticed that elements have a bigger spacing between them.
| <div className="mb-2"> | |
| <div className="my-4"> |
| {scope.displayName} | ||
| </Form.Checkbox> | ||
| {scope.description && ( | ||
| <small className="d-block text-muted pl-4">{scope.description}</small> |
There was a problem hiding this comment.
| <small className="d-block text-muted pl-4">{scope.description}</small> | |
| <small className="d-block text-muted font-italic pl-4">{scope.description}</small> |
| import { useMemo } from 'react'; | ||
| import { useValidateUserPermissions } from '@src/data/hooks'; | ||
| import { useOrgs } from '@src/authz-module/data/hooks'; | ||
| import { CONTENT_LIBRARY_PERMISSIONS, CONTENT_COURSE_PERMISSIONS } from '../../constants'; |
There was a problem hiding this comment.
Use absolute path for this import.
| @@ -0,0 +1,79 @@ | |||
| import { useMemo } from 'react'; | |||
There was a problem hiding this comment.
I don't see this hook being used. The intention is define it and integrate it later?
| queryState, | ||
| platformAggregateScopeItem, | ||
| showOrgAggregates, | ||
| } = useScopeListData({ contextType, search: debouncedSearch, org: selectedOrgs[0] || '' }); |
There was a problem hiding this comment.
org: selectedOrgs[0] || '' It is this intentional?
The filter is presented as a multi-select checkbox but the filter is always applied to 1 organization if I choose 2or more the filter is not applied
Screencast.from.2026-04-22.16-43-52.webm
There was a problem hiding this comment.
ay verdad, thanks for the call. Yes, it is intentional... the backend still doesn't receive multiple orgs at that endpoint. At the moment, if we send all the orgs, it treats them as one string and looks for org1,org2 instead of treating them as separate orgs.
| <Form.Control | ||
| type="text" | ||
| value={search} | ||
| onChange={(e: { target: { value: string } }) => onSearchChange(e.target.value)} |
There was a problem hiding this comment.
| onChange={(e: { target: { value: string } }) => onSearchChange(e.target.value)} | |
| onChange={(e: ChangeEvent<HTMLInputElement>) => onSearchChange(e.target.value)} |
@jacobo-dominguez-wgu, it is not related to these changes; I just checked out to |
…ing and UI updates
…multiple preset users
| @@ -109,13 +116,21 @@ | |||
| resize: vertical; | |||
| caret-color: #212529; | |||
There was a problem hiding this comment.
Just a nit for a near future: Lets try to use design tokens on this file.
|
I have tested it and everything works well, nice job! Lets just wait for the resolution on the orgs filter and the All courses/libraries on the platform slack discussion. |
this has been fixed in another PR |
…zations and enhance error handling
arbrandes
left a comment
There was a problem hiding this comment.
This jumps out from the code:
src/authz-module/role-assignation-wizard/hooks/useScopePermissions.ts is not imported by any non-test file. Meanwhile useScopeListData.ts:69-83 gates both the platform-wide aggregate (*) and the per-org aggregates on getAuthenticatedUser()?.administrator === true.
This is the gap called out in rows 6-9 of the test matrix in the PR description. Is it going to be implemented later?
…cope handling and organization filtering
…ndling in role assignment wizard
@arbrandes, thanks for taking the time to review this I made some changes to implement per-org aggregates using The gap in row 8 related to the platform-wide aggregate is explained here by @rodmgwgu: https://openedx.slack.com/archives/C07PRCN6G0N/p1776869300085249 |
|
@rodmgwgu, @jacobo-dominguez-wgu, @jesusbalderramawgu I added per-org aggregates using Screencast.from.23-04-26.11.26.33.webm |
| const hasPlatformPermission = contextType === 'course' | ||
| ? hasPlatformCoursePermission | ||
| : hasPlatformLibraryPermission; | ||
| // TODO: compute hasPlatformPermission from per-org permission requests once the backend |
There was a problem hiding this comment.
This TODO is no longer current, I think
There was a problem hiding this comment.
You're right... I updated the message to keep the TODO. What do you think now?
…ration in role assignment
jacobo-dominguez-wgu
left a comment
There was a problem hiding this comment.
LGTM! Great work @bra-i-am
|
I've tested as well and it works, thank you @bra-i-am |


Implements Step 2 — Define Application Scope of the Assign Role Wizard, allowing users to select which courses and/or libraries a role assignment should apply to.
Additional Information
Resolves #92
Screencast
Screencast.from.21-04-26.14.06.24.webm
What's included
API layer (
data/api.ts)getScopes()— paginated endpoint withsearch,org,context_type, andmanagement_permission_onlyfilters.getOrganizations()— fetches available organizations for the filter dropdown.Hooks (
data/hooks.ts)useScopes— infinite-query hook with page-based pagination (parsespagefrom thenextURL viagetNextPageParam).useOrgs— standard query with 30-min stale time.useAssignTeamMembersRole— invalidatesteamMembersAll,permissionsByRole,userRoles, andallRoleAssignmentscaches on success.data/hooks.ts(global,@src/data/hooks.ts)useValidateUserPermissions—useSuspenseQuerywrapper around the permissions validation endpoint; used byuseScopePermissionsto check per-org access.DefineApplicationScopeStep/ new componentsScopeFilterBar— debounced search input (300 ms) + organization filter dropdown; active org filter shown as a Paragon Badge chip.ScopeList— scopes grouped by organization via collapsibleOrgSectionsections; infinite scroll viaIntersectionObserverwith loading spinners for initial load and paginated fetches.useScopeListData— data-preparation hook that composes scope groups and injects per-org aggregate items (course-v1:{org}+*/lib:{org}:*) filtered by the user's management permissions; platform aggregate item is gated onhasPlatformPermission.useScopePermissions— callsuseValidateUserPermissionswith one request per org (using the appropriateMANAGE_COURSE_TEAMorMANAGE_LIBRARY_TEAMaction and wildcard scope) and maps the results intoorgHasPermission: Record<string, boolean>;hasPlatformPermissionis hardcoded tofalsepending backend support for scope-less permission checks.LibrariesUserManagerAssignNewRoleTriggercomponent with a direct navigation button to/authz/assign-role?scope=<libraryId>&users=<username>.i18n & error handling
messages.tsand wrapped withintl.formatMessage.How to test?
Prerequisites
frontend-app-admin-consolepointed at a backend that supports the/authz/scopes/,/authz/organizations/, and/api/v1/permissions/validate/endpoints.manage_course_teamormanage_library_teamon at least one org), and a second user without those permissions.Manual test steps
Org: <orgName>. Collapse and expand a section to confirm it works.course-v1:<org>+*orlib:<org>:*) is submitted when that option is selected and saved./authz/assign-role?scope=<libraryId>&users=<username>without the oldAssignNewRoleTriggermodal.Test Cases
Note
Test case 8 cannot be fully implemented yet because the backend needs a mechanism to integrate platform-wide permissions.
useScopePermissions.tshardcodeshasPlatformPermission = falseuntil that constraint is lifted.messages['wizard.step.defineScope.title']='Where It Applies'ScopeFilterBar.tsxwith debounceScopeFilterBar.tsxOrgSectionrendersOrg: {orgName}— includes an extra "Org:" prefix not in the specScopeCheckboxItem+Set<string>inAssignRoleWizard.tsxmanage_course_teamat org level see "All courses in this organization"orgAggregateScopeItemsis filtered byorgHasPermissionfromuseScopePermissions, which callsuseValidateUserPermissionswithcourse-v1:{org}+*scopesmanage_library_teamat org level see "All libraries in this organization"lib:{org}:*scopeshasPlatformPermission = false); platform-wide checks are not yet possibleorgHasPermission[org]isfalse; platform aggregate hidden for all users (see item 8)IntersectionObserverinScopeList.tsxtriggersfetchNextPageselectedScopeslives inAssignRoleWizardparent state, untouched by search/filter changesassignRoleMutation.isPendingdrivesStatefulButtonpending state with spinneronClose()is called, delegating navigation to the callershowErrorToastcalled, step state unchanged on error