Skip to content

Conversation

@gumaerc
Copy link
Contributor

@gumaerc gumaerc commented Jan 5, 2026

What are the relevant tickets?

Closes https://github.com/mitodl/hq/issues/9679

Description (What does it do?)

This PR reworks functionality in the dashboard surrounding b2b organizations / contracts:

  • Instead of a tab per organization the user is in, there is now a tab per contract. Organizations can have multiple contracts, but previously we were just selecting the first one.
  • The URL structure for dashboard contract pages is set up as /dashboard/organization/org-slug/contract/contract-slug
  • The OrganizationContent component was refactored / renamed to ContractContent and now requires a contract ID along with the org ID.

Screenshots (if appropriate):

image image

How can this be tested?

  • Follow the instructions in the readme to set up MITx Online locally and connect it to your instance of Learn. They need to be sharing the same instance of Keycloak / APISIX so your user is consistent in both apps.
  • In MITx Online:
    • Create a B2B organization
    • Add your user to the B2B organization
    • Create multiple B2B contracts tied to said org
    • Create multiple programs with courses, assigning some to the first contract and some to the second
  • Back in MIT Learn:
    • Visit the dashboard
    • Verify that you now see two tabs in the dashboard, one for the first contract and one for the second
    • Visit these pages by clicking on the tabs
    • Verify that the URL structure for each contract is correct (/dashboard/organization/org-slug/contract/contract-slug)
    • Verify that each contract displays the programs / courses you would expect based on what you added above

Checklist:

  • Set up redirects for the current org dashboards to go to the first contract available

@gumaerc gumaerc changed the title org dashboard contract refactor [WIP] org dashboard contract refactor Jan 5, 2026
@gumaerc gumaerc force-pushed the cg/org-dashboard-contract-refactor branch from f226648 to 9a2411b Compare January 6, 2026 17:09
@gumaerc gumaerc added Needs Review An open Pull Request that is ready for review and removed Work in Progress labels Jan 6, 2026
@gumaerc gumaerc changed the title [WIP] org dashboard contract refactor org dashboard contract refactor Jan 6, 2026
Copy link
Contributor

@jonkafton jonkafton left a comment

Choose a reason for hiding this comment

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

👍

@ChristopherChudzicki
Copy link
Contributor

ChristopherChudzicki commented Jan 8, 2026

Checklist:

  • Set up redirects for the current org dashboards to go to the first contract available

Another thing we probably want to do is change some contract slugs. Currently it would be, e.g., https://learn.mit.edu/dashboard/organization/mit-universal-ai/contracts/contract-504-universal-ai for MIT UAI. The contract slugs aren't used for anything public right now, so it's a good opportunity to clean them up.

Copy link
Contributor

@ChristopherChudzicki ChristopherChudzicki left a comment

Choose a reason for hiding this comment

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

This is working well for me except the empty contract issue.

Suggestion: In the interest of caution, do you think we should leave /dashboard/organization/[orgSlug] route in place for now, rendering something like

<ContractContent // <-- modify to use first contract if contractSlug missing
  orgSlug={resolved.orgSlug}
  contractSlug={null}
/>

Edit: Alternatively, could add a nextjs redirect for organization/[slug] to /organization, which would then redirect to most recent org contract.

Comment on lines 444 to 447
if (!contract?.programs || contract.programs.length === 0) {
return true
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Blocking Question: What's the point of this filter? I currently have an org with 3 contracts, one of which is empty. The empty contract shows all org content (but in a kinda buggy way).

Separately: I think the contract filtering should ideally be done at the request level, though probably the APIs don't support that filter yet.

<Typography variant="subtitle2" component="span">
{org.name}
</Typography>
{` - ${contract?.name}`}
Copy link
Contributor

Choose a reason for hiding this comment

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

github suggestions seem to have disappeared for me...

But contract.name would be fine, it's not nullable.

const contract = b2bOrganization.contracts[0]
if (contract) {
const contractUrl = contractView(
b2bOrganization.slug.replace("org-", ""),
Copy link
Contributor

Choose a reason for hiding this comment

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

We should remove all the org prefixes in prod, IMO, and drop all the org- stripping shenanigans.

Since wagtail slugs only need to be unique within their parent, and all organization pages are children of the organization index page, it should be fine to drop the prefix. And client side we've always stripped the prefix, so there should be no behavior change for users.

Separate issue, though.

Comment on lines +9 to +10
invariant(resolved.orgSlug, "orgSlug is required")
invariant(resolved.contractSlug, "contractSlug is required")
Copy link
Contributor

Choose a reason for hiding this comment

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

These invariant calls are unnecessary:

Image

The non-nullable string type here makes sense to me because the router will only match this file if orgSlug and contractSlug are not empty.

Copy link
Contributor

Choose a reason for hiding this comment

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

We may have similar invariant calls in place elsewhere. Before NextJS added that PageProps thing, it wouldn't have known that those params were non-nullale.

@gumaerc
Copy link
Contributor Author

gumaerc commented Jan 9, 2026

This is working well for me except the empty contract issue.

Suggestion: In the interest of caution, do you think we should leave /dashboard/organization/[orgSlug] route in place for now, rendering something like

<ContractContent // <-- modify to use first contract if contractSlug missing
  orgSlug={resolved.orgSlug}
  contractSlug={null}
/>

Edit: Alternatively, could add a nextjs redirect for organization/[slug] to /organization, which would then redirect to most recent org contract.

In the last commit (a5149ad) I added this functionality you suggested. It either displays the first available contract or redirects to the dashboard home if none are found. What do you think?

@gumaerc gumaerc force-pushed the cg/org-dashboard-contract-refactor branch from a5149ad to e3d8e54 Compare January 9, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Review An open Pull Request that is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants