fix: resolve organization member visibility issue during owned work sharing (#21657)

The workspace sharing autocomplete was using the site-wide /api/v2/users
endpoint which requires user:read permission. Regular org members don't
have this permission, so they couldn't see other members to share with.

## Sharing Scope
* This iteration of shared workspaces is slated for beta, and the
currently understood use case does not include cross-org workspace
sharing. This can be addressed later if necessary.

## What's Changed
* Changed to use /api/v2/organizations/{org}/members instead, which only
requires organization_member:read permission (already granted to org
members when workspace sharing is enabled).
* Added `OrganizationMemberWithUserData` to the `UserLike` union to
allow for more flexibility in differentiating groups from users
This commit is contained in:
Sushant P
2026-01-26 18:25:12 -08:00
committed by GitHub
parent d2e54819bf
commit f2e998848e
2 changed files with 42 additions and 12 deletions
+6 -1
View File
@@ -1,5 +1,6 @@
import type {
Group,
OrganizationMemberWithUserData,
ReducedUser,
User,
WorkspaceUser,
@@ -8,7 +9,11 @@ import type {
/**
* Union of all user-like types that can be distinguished from Group.
*/
type UserLike = User | ReducedUser | WorkspaceUser;
type UserLike =
| User
| ReducedUser
| WorkspaceUser
| OrganizationMemberWithUserData;
/**
* Type guard to check if the value is a Group.
@@ -1,15 +1,16 @@
import { groupsByOrganization } from "api/queries/groups";
import { users } from "api/queries/users";
import type { Group, User } from "api/typesGenerated";
import { organizationMembers } from "api/queries/organizations";
import type { Group, OrganizationMemberWithUserData } from "api/typesGenerated";
import { Autocomplete } from "components/Autocomplete/Autocomplete";
import { AvatarData } from "components/Avatar/AvatarData";
import { Check } from "lucide-react";
import { getGroupSubtitle, isGroup } from "modules/groups";
import { type FC, useState } from "react";
import { keepPreviousData, useQuery } from "react-query";
import { prepareQuery } from "utils/filters";
type AutocompleteOption = User | Group;
type OrganizationMember = OrganizationMemberWithUserData & { id: string };
type AutocompleteOption = OrganizationMember | Group;
export type UserOrGroupAutocompleteValue = AutocompleteOption | null;
type ExcludableOption = { id?: string | null } | null;
@@ -21,6 +22,13 @@ type UserOrGroupAutocompleteProps = {
exclude: ExcludableOption[];
};
const normalizeMember = (
member: OrganizationMemberWithUserData,
): OrganizationMember => ({
...member,
id: member.user_id,
});
export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
value,
onChange,
@@ -37,11 +45,11 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
}
};
const usersQuery = useQuery({
...users({
q: prepareQuery(encodeURI(inputValue)),
limit: 25,
}),
// Use org members endpoint instead of site-wide /users endpoint.
// This allows regular org members to see other members in their org
// for workspace sharing, without needing site-wide user:read permission.
const membersQuery = useQuery({
...organizationMembers(organizationId),
enabled: open,
placeholderData: keepPreviousData,
});
@@ -53,6 +61,8 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
});
const filterValue = inputValue.trim().toLowerCase();
// Filter groups by search input (client-side filtering).
const groupOptions = groupsQuery.data
? groupsQuery.data.filter((group) => {
if (!filterValue) {
@@ -63,13 +73,28 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
})
: [];
// Filter members by search input (client-side filtering since org members
// endpoint doesn't support search params).
const userOptions = membersQuery.data?.members
? membersQuery.data.members
.filter((member) => {
if (!filterValue) {
return true;
}
const haystack =
`${member.name ?? ""} ${member.username} ${member.email}`.toLowerCase();
return haystack.includes(filterValue);
})
.map(normalizeMember)
: [];
const excludeIds = exclude
.map((optionToExclude) => optionToExclude?.id)
.filter((id): id is string => Boolean(id));
const options: AutocompleteOption[] = [
...groupOptions,
...(usersQuery.data?.users ?? []),
...userOptions,
].filter((result) => !excludeIds.includes(result.id));
return (
@@ -102,7 +127,7 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
onOpenChange={handleOpenChange}
inputValue={inputValue}
onInputChange={setInputValue}
loading={usersQuery.isFetching || groupsQuery.isFetching}
loading={membersQuery.isFetching || groupsQuery.isFetching}
placeholder="Search for user or group"
noOptionsText="No users or groups found"
className="w-80"