Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/src/datum_cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?
Expand All @@ -282,7 +284,11 @@ impl DatumCloudClient {
Api::ResourceManager(ResourceManager::OrganizationMemberships),
)
.await?;
parse_orgs(&json).context("Failed to parse reply")
let mut orgs: Vec<Organization> = 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<Vec<Project>> {
Expand Down
13 changes: 0 additions & 13 deletions lib/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
12 changes: 2 additions & 10 deletions ui/src/components/invite_user_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,7 @@ pub fn InviteUserDialog(open: ReadSignal<bool>, on_open_change: EventHandler<boo
None
}

let can_invite = use_memo(move || {
selected_context()
.as_ref()
.map_or(false, |c| c.can_send_invite())
});
let can_invite = use_memo(move || selected_context().is_some());
let email_validation = use_memo(move || validate_email(&email()));
let email_invalid = use_memo(move || email().trim().is_empty() || email_validation().is_some());
let no_role_selected = use_memo(move || selected_role_name().is_none());
Expand All @@ -127,10 +123,6 @@ pub fn InviteUserDialog(open: ReadSignal<bool>, on_open_change: EventHandler<boo
move |(email_value, role_ref): (String, Option<RoleReference>)| async move {
let state = consume_context::<AppState>();
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(
Expand Down Expand Up @@ -203,7 +195,7 @@ pub fn InviteUserDialog(open: ReadSignal<bool>, on_open_change: EventHandler<boo
} else if !can_invite() {
div { class: "space-y-5 mt-5 w-[452px]",
p { class: "text-sm text-foreground",
"Invitations are only available for team organizations, not personal organizations."
"Select an organization and project before inviting users."
}
div { class: "flex justify-end pt-2",
Button {
Expand Down
9 changes: 2 additions & 7 deletions ui/src/views/navbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,8 @@ pub fn AppHeader(props: AppHeaderProps) -> 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
Expand Down
1 change: 0 additions & 1 deletion ui/src/views/select_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading