From 4602bba40ead16d75a9864c44fb06a80a04c4127 Mon Sep 17 00:00:00 2001 From: Matt Jenkinson <75292329+mattdjenkinson@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:01:37 +0100 Subject: [PATCH] fix: org type is deprecated --- lib/src/datum_cloud.rs | 10 ++++++++-- lib/src/state.rs | 13 ------------- ui/src/components/invite_user_dialog.rs | 12 ++---------- ui/src/views/navbar.rs | 9 ++------- ui/src/views/select_project.rs | 1 - 5 files changed, 12 insertions(+), 33 deletions(-) diff --git a/lib/src/datum_cloud.rs b/lib/src/datum_cloud.rs index fdebb86..3d2ce6b 100644 --- a/lib/src/datum_cloud.rs +++ b/lib/src/datum_cloud.rs @@ -260,7 +260,9 @@ impl DatumCloudClient { .get("organization")? .as_object()?; let name = org.get("displayName")?.as_str()?; - let r#type = org.get("type")?.as_str()?; + // `type` is deprecated (ignored when the UnifiedOrganizations feature + // gate is enabled) and absent from most memberships; treat as optional. + let r#type = org.get("type").and_then(|v| v.as_str()).unwrap_or_default(); let spec = item.get("spec")?.as_object()?; let resource_id = spec .get("organizationRef")? @@ -282,7 +284,11 @@ impl DatumCloudClient { Api::ResourceManager(ResourceManager::OrganizationMemberships), ) .await?; - parse_orgs(&json).context("Failed to parse reply") + let mut orgs: Vec = parse_orgs(&json).context("Failed to parse reply")?; + // A user can hold multiple memberships in the same org; keep one entry per org. + let mut seen = std::collections::HashSet::new(); + orgs.retain(|org| seen.insert(org.resource_id.clone())); + Ok(orgs) } pub async fn projects(&self, org_id: &str) -> Result> { diff --git a/lib/src/state.rs b/lib/src/state.rs index c428fe8..0d47ca2 100644 --- a/lib/src/state.rs +++ b/lib/src/state.rs @@ -48,25 +48,12 @@ pub struct SelectedContext { pub org_name: String, pub project_id: String, pub project_name: String, - /// Organization type (e.g. "personal", "team"). Invitations are only allowed when not "personal". - #[serde(default)] - pub org_type: String, } impl SelectedContext { pub fn label(&self) -> String { format!("{} / {}", self.org_name, self.project_name) } - - /// True if this org is a personal org (invitations not allowed). - pub fn is_personal_org(&self) -> bool { - self.org_type.eq_ignore_ascii_case("personal") - } - - /// True if the user can send invitations (org is not personal and type is known). - pub fn can_send_invite(&self) -> bool { - !self.org_type.is_empty() && !self.is_personal_org() - } } #[derive(Debug, Clone)] diff --git a/ui/src/components/invite_user_dialog.rs b/ui/src/components/invite_user_dialog.rs index 02bc2a4..bc6aa76 100644 --- a/ui/src/components/invite_user_dialog.rs +++ b/ui/src/components/invite_user_dialog.rs @@ -105,11 +105,7 @@ pub fn InviteUserDialog(open: ReadSignal, on_open_change: EventHandler, on_open_change: EventHandler)| async move { let state = consume_context::(); let ctx = state.selected_context().context("No org selected")?; - if !ctx.can_send_invite() { - n0_error::bail_any!("Invitations are not available for personal organizations"); - } - state .datum() .create_user_invitation_org( @@ -203,7 +195,7 @@ pub fn InviteUserDialog(open: ReadSignal, on_open_change: EventHandler Element { .unwrap_or_default() }; - // Disable Invite when org is Personal (matches web app: org?.type === 'Personal') - let invite_disabled = use_memo(move || { - !selected_context - .read() - .as_ref() - .map_or(false, |c| c.can_send_invite()) - }); + // Invite requires a selected org; all orgs can invite (unified organizations). + let invite_disabled = use_memo(move || selected_context.read().is_none()); rsx! { // App header bar - below titlebar, contains Add tunnel button and user menu diff --git a/ui/src/views/select_project.rs b/ui/src/views/select_project.rs index c36c6ed..3c7de8f 100644 --- a/ui/src/views/select_project.rs +++ b/ui/src/views/select_project.rs @@ -136,7 +136,6 @@ pub fn SelectProject() -> Element { org_name: org.org.display_name.clone(), project_id, project_name: project.display_name.clone(), - org_type: org.org.r#type.clone(), }; spawn({