Skip to content

Standalone organization/tenant dropdowns#3306

Open
TehShrike wants to merge 24 commits intomainfrom
feat--organization-and-tenant-dropdowns
Open

Standalone organization/tenant dropdowns#3306
TehShrike wants to merge 24 commits intomainfrom
feat--organization-and-tenant-dropdowns

Conversation

@TehShrike
Copy link
Collaborator

@TehShrike TehShrike commented Mar 17, 2026

I'll leave this as a draft PR until my other PRs get merged with their other changes to some of the organization UI, since I don't want to touch the Cypress tests in this branch until that gets merged in, and there may be other conflicts.

Description

Standalone organization/tenant dropdowns, ala Vercel.

CleanShot 2026-03-17 at 14 45 20@2x CleanShot 2026-03-17 at 14 45 34@2x CleanShot 2026-03-17 at 14 46 22@2x

Other changes along the way:

  • TenantMembership's Tenant object is no longer optional – this affects the API types
  • I attempted to regenerate the SDKs. This caused some lines to change in ways I wouldn't have expected, I kept them all relegated to one commit
  • the user universe now exposes constant-time ways to lookup organization or tenant using a tenant id, and use-organizations uses one of those functions rather than looping over all organizations+tenants in its own getOrganizationForTenant
  • the user universe query builds the tenantId->tenant hashmap and does the things that seem like they should be done by default to every place where we display organizations and tenants
    • tenants are sorted by name
    • archived tenants are filtered out

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Documentation change (pure documentation change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking changes to code which doesn't change any behaviour)
  • CI (any automation pipeline changes)
  • Chore (changes which are not directly related to any business logic)
  • Test changes (add, refactor, improve or change a test)
  • This change requires a documentation update

What's Changed

  • Add a list of tasks or features here...

Note

Medium Risk
Medium risk because it changes the API contract/codegen to require TenantMember.tenant (potentially breaking clients that relied on it being optional) and refactors tenant/org selection plus user-universe data shaping, which can affect navigation and tenant scoping across the app.

Overview
Introduces standalone organization and tenant dropdowns in the top nav (Vercel-style): OrganizationSelector now selects an org (and switches to its first tenant) while a new TenantSelector handles tenant switching and “New Tenant”; the old combined tenant-switcher UI is removed.

Updates the user universe provider to precompute constant-time lookups (getOrganizationForTenant, getTenantWithTenantId), sort tenants/org tenants by name, and filter out archived tenants, and adjusts use-organizations to delegate org lookup to this provider.

Makes TenantMember.tenant required across the OpenAPI schema and regenerated clients (Go/TS/Python/Ruby) and tweaks related transformers; also includes regenerated Ruby REST SDK additions for durable-task/task APIs (e.g., v1_durable_task_branch, v1_task_restore) and new task “running/evicted” fields, plus small Cypress selector tweaks and enabling experimentalRunAllSpecs.

Written by Cursor Bugbot for commit 46fd4d1. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Mar 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hatchet-docs Ready Ready Preview, Comment Mar 18, 2026 10:17pm

Request Review

And update the button copy because I do like the more explicit version
@TehShrike TehShrike marked this pull request as ready for review March 19, 2026 14:24
@promptless-for-oss
Copy link

📝 Documentation updates detected!

New suggestion: Update multi-tenancy instructions for standalone organization/tenant dropdowns


Tip: Use Vale? Add your vale.ini when setting up your Docs Collection.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Organization select crashes when org has no tenants
    • Filtered organizations with empty tenants arrays from dropdown to prevent crash when clicking organizations with all archived tenants.
  • ✅ Fixed: Throwing lookup breaks isTenantArchivedInOrg for archived tenants
    • Changed getOrganizationForTenant to return undefined instead of throwing for missing tenants, fixing isTenantArchivedInOrg graceful handling.

Create PR

Or push these changes by commenting:

@cursor push e01cabaaeb
Preview (e01cabaaeb)
diff --git a/frontend/app/src/components/v1/molecules/nav-bar/organization-selector.tsx b/frontend/app/src/components/v1/molecules/nav-bar/organization-selector.tsx
--- a/frontend/app/src/components/v1/molecules/nav-bar/organization-selector.tsx
+++ b/frontend/app/src/components/v1/molecules/nav-bar/organization-selector.tsx
@@ -54,12 +54,14 @@
     if (!organizations) {
       return [];
     }
-    return [...organizations].sort((a, b) => {
-      if (a.isOwner !== b.isOwner) {
-        return a.isOwner ? -1 : 1;
-      }
-      return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
-    });
+    return [...organizations]
+      .filter((org) => org.tenants.length > 0)
+      .sort((a, b) => {
+        if (a.isOwner !== b.isOwner) {
+          return a.isOwner ? -1 : 1;
+        }
+        return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
+      });
   }, [organizations]);
 
   const handleOrgSelect = useCallback(

diff --git a/frontend/app/src/providers/user-universe.tsx b/frontend/app/src/providers/user-universe.tsx
--- a/frontend/app/src/providers/user-universe.tsx
+++ b/frontend/app/src/providers/user-universe.tsx
@@ -30,7 +30,9 @@
           isLoaded: true;
           organizations: OrganizationForUserList['rows'];
           tenantMemberships: TenantMember[];
-          getOrganizationForTenant: (tenantId: string) => OrganizationForUser;
+          getOrganizationForTenant: (
+            tenantId: string,
+          ) => OrganizationForUser | undefined;
           getTenantWithTenantId: (tenantId: string) => Tenant;
         }
       | {
@@ -225,7 +227,9 @@
         );
         const getOrganizationForTenant = (tenantId: string) => {
           const organization = tenantIdToOrganization.get(tenantId);
-          invariant(organization);
+          if (!organization) {
+            return undefined;
+          }
           return organization;
         };

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

setOpen(false);
},
[isUniverseLoaded, getTenantWithTenantId, setTenant],
);
Copy link

Choose a reason for hiding this comment

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

Organization select crashes when org has no tenants

Medium Severity

handleOrgSelect calls org.tenants.at(0) and then invariant(firstTenant), which throws if the array is empty. Since the user universe query filters archived tenants from organization.tenants, an organization whose tenants are all archived will have an empty tenants array. That org still appears in sortedOrgs (no filtering step removes zero-tenant orgs), so clicking it in the dropdown causes a runtime crash.

Additional Locations (1)
Fix in Cursor Fix in Web

isUserUniverseLoaded,
isCloudEnabled,
],
);
Copy link

Choose a reason for hiding this comment

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

Throwing lookup breaks isTenantArchivedInOrg for archived tenants

Medium Severity

getOrganizationForTenant from the user universe now uses invariant and throws when a tenant ID isn't in the map, whereas the old implementation gracefully returned undefined. The isTenantArchivedInOrg function in use-organizations.ts calls getOrganizationForTenant with potentially archived tenant IDs and checks for a falsy return (if (!orgForTenant)). Since archived tenants are now filtered from the map, looking up an archived tenant crashes instead of returning undefined. This breaks tenant-forbidden.tsx line 140, which is the error page shown when access is denied.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants