-
{option.label}
+
+ {option.label}
+
{option?.["email"]}
@@ -29,14 +32,20 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
);
-export function MultiUserSelect({ onChange, label }: MultiUserSelectProps) {
+export function MultiUserSelect({
+ onChange,
+ label,
+ groupId,
+}: MultiUserSelectProps) {
const { t } = useTranslation();
const [searchValue, setSearchValue] = useState("");
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
const { data: users, isLoading } = useWorkspaceMembersQuery({
query: debouncedQuery,
limit: 50,
+ ...(groupId && { groupId }),
});
+
const [data, setData] = useState([]);
useEffect(() => {
diff --git a/apps/client/src/features/group/queries/group-query.ts b/apps/client/src/features/group/queries/group-query.ts
index aeb7787f..150cd6f7 100644
--- a/apps/client/src/features/group/queries/group-query.ts
+++ b/apps/client/src/features/group/queries/group-query.ts
@@ -22,6 +22,7 @@ import { IUser } from "@/features/user/types/user.types.ts";
import { useEffect } from "react";
import { validate as isValidUuid } from "uuid";
import { queryClient } from "@/main.tsx";
+import { useTranslation } from "react-i18next";
export function useGetGroupsQuery(
params?: QueryParams,
@@ -119,18 +120,24 @@ export function useGroupMembersQuery(
export function useAddGroupMemberMutation() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation
({
mutationFn: (data) => addGroupMember(data),
onSuccess: (data, variables) => {
- notifications.show({ message: "Added successfully" });
+ notifications.show({ message: t("Added successfully") });
queryClient.invalidateQueries({
queryKey: ["groupMembers", variables.groupId],
});
+
+ queryClient.invalidateQueries({
+ predicate: (item) =>
+ ["workspaceMembers"].includes(item.queryKey[0] as string),
+ });
},
onError: () => {
notifications.show({
- message: "Failed to add group members",
+ message: t("Failed to add group members"),
color: "red",
});
},
@@ -139,6 +146,7 @@ export function useAddGroupMemberMutation() {
export function useRemoveGroupMemberMutation() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation<
void,
@@ -150,10 +158,15 @@ export function useRemoveGroupMemberMutation() {
>({
mutationFn: (data) => removeGroupMember(data),
onSuccess: (data, variables) => {
- notifications.show({ message: "Removed successfully" });
+ notifications.show({ message: t("Removed successfully") });
queryClient.invalidateQueries({
queryKey: ["groupMembers", variables.groupId],
});
+
+ queryClient.invalidateQueries({
+ predicate: (item) =>
+ ["workspaceMembers"].includes(item.queryKey[0] as string),
+ });
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
diff --git a/apps/client/src/features/space/components/add-space-members-modal.tsx b/apps/client/src/features/space/components/add-space-members-modal.tsx
index 5efd32f6..7df2b762 100644
--- a/apps/client/src/features/space/components/add-space-members-modal.tsx
+++ b/apps/client/src/features/space/components/add-space-members-modal.tsx
@@ -55,7 +55,7 @@ export default function AddSpaceMembersModal({
-
+
void;
+ spaceId?: string;
}
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
@@ -25,23 +26,38 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
)}
{option["type"] === "group" && }
- {option.label}
+
+ {option.label}
+
{option["type"] === "user" && option["email"] && (
- {option["email"]}
+
+ {option["email"]}
+
)}
);
-export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
+export function MultiMemberSelect({
+ onChange,
+ spaceId,
+}: MultiMemberSelectProps) {
const { t } = useTranslation();
const [searchValue, setSearchValue] = useState("");
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
- const { data: suggestion, isLoading } = useSearchSuggestionsQuery({
- query: debouncedQuery,
+
+ console.log("vacant", spaceId);
+
+ // Filter out empty parameters to avoid duplicate cache keys
+ const queryParams = {
+ ...(debouncedQuery && { query: debouncedQuery }),
includeUsers: true,
includeGroups: true,
- });
+ ...(spaceId && { spaceId }),
+ };
+
+ const { data: suggestion, isLoading } =
+ useSearchSuggestionsQuery(queryParams);
const [data, setData] = useState([]);
useEffect(() => {
@@ -63,14 +79,14 @@ export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
// Create fresh data structure based on current search results
const newData = [];
-
+
if (userItems && userItems.length > 0) {
newData.push({
group: t("Select a user"),
items: userItems,
});
}
-
+
if (groupItems && groupItems.length > 0) {
newData.push({
group: t("Select a group"),
diff --git a/apps/client/src/features/space/queries/space-query.ts b/apps/client/src/features/space/queries/space-query.ts
index b51e195e..7ea74923 100644
--- a/apps/client/src/features/space/queries/space-query.ts
+++ b/apps/client/src/features/space/queries/space-query.ts
@@ -190,6 +190,28 @@ export function useAddSpaceMemberMutation() {
queryClient.invalidateQueries({
queryKey: ["spaceMembers", variables.spaceId],
});
+
+ // Optimistically update search suggestions cache by filtering out added users and groups
+ queryClient.setQueriesData(
+ { queryKey: ["search-suggestion"], exact: false },
+ (oldData: any) => {
+ if (!oldData) return oldData;
+
+ const filteredUsers = oldData.users?.filter((user: any) =>
+ !variables.userIds?.includes(user.id)
+ ) || [];
+
+ const filteredGroups = oldData.groups?.filter((group: any) =>
+ !variables.groupIds?.includes(group.id)
+ ) || [];
+
+ return {
+ ...oldData,
+ users: filteredUsers,
+ groups: filteredGroups,
+ };
+ }
+ );
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
@@ -209,6 +231,16 @@ export function useRemoveSpaceMemberMutation() {
queryClient.invalidateQueries({
queryKey: ["spaceMembers", variables.spaceId],
});
+
+ // For remove operations, invalidate to get fresh data
+ // since adding the user/group back requires fetching their current data
+ queryClient.invalidateQueries({
+ queryKey: ["search-suggestion"],
+ predicate: (query) => {
+ const queryKey = query.queryKey as any[];
+ return queryKey[1]?.spaceId === variables.spaceId;
+ },
+ });
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
diff --git a/apps/client/src/lib/types.ts b/apps/client/src/lib/types.ts
index fd0e3212..17a6f7e7 100644
--- a/apps/client/src/lib/types.ts
+++ b/apps/client/src/lib/types.ts
@@ -2,6 +2,7 @@ export interface QueryParams {
query?: string;
page?: number;
limit?: number;
+ groupId?: string;
}
export enum UserRole {
diff --git a/apps/server/src/core/search/search.service.ts b/apps/server/src/core/search/search.service.ts
index 60432ce8..b22e2442 100644
--- a/apps/server/src/core/search/search.service.ts
+++ b/apps/server/src/core/search/search.service.ts
@@ -145,7 +145,7 @@ export class SearchService {
const query = suggestion.query.toLowerCase().trim();
if (suggestion.includeUsers) {
- const userQuery = this.db
+ let userQuery = this.db
.selectFrom('users')
.select(['id', 'name', 'email', 'avatarUrl'])
.where('workspaceId', '=', workspaceId)
@@ -159,14 +159,25 @@ export class SearchService {
),
eb(sql`users.email`, 'ilike', sql`f_unaccent(${`%${query}%`})`),
]),
- )
- .limit(limit);
+ );
+ // Filter out users who are already members of the space
+ if (suggestion.spaceId) {
+ userQuery = userQuery.where('users.id', 'not in', (eb) =>
+ eb
+ .selectFrom('spaceMembers')
+ .select('userId')
+ .where('spaceId', '=', suggestion.spaceId)
+ .where('userId', 'is not', null),
+ );
+ }
+
+ userQuery = userQuery.limit(limit);
users = await userQuery.execute();
}
if (suggestion.includeGroups) {
- groups = await this.db
+ let groupQuery = this.db
.selectFrom('groups')
.select(['id', 'name', 'description'])
.where((eb) =>
@@ -176,9 +187,21 @@ export class SearchService {
sql`LOWER(f_unaccent(${`%${query}%`}))`,
),
)
- .where('workspaceId', '=', workspaceId)
- .limit(limit)
- .execute();
+ .where('workspaceId', '=', workspaceId);
+
+ // Filter out groups that are already members of the space
+ if (suggestion.spaceId) {
+ groupQuery = groupQuery.where('groups.id', 'not in', (eb) =>
+ eb
+ .selectFrom('spaceMembers')
+ .select('groupId')
+ .where('spaceId', '=', suggestion.spaceId)
+ .where('groupId', 'is not', null),
+ );
+ }
+
+ groupQuery = groupQuery.limit(limit);
+ groups = await groupQuery.execute();
}
if (suggestion.includePages) {
diff --git a/apps/server/src/database/pagination/pagination-options.ts b/apps/server/src/database/pagination/pagination-options.ts
index e0481910..457e8bda 100644
--- a/apps/server/src/database/pagination/pagination-options.ts
+++ b/apps/server/src/database/pagination/pagination-options.ts
@@ -23,4 +23,8 @@ export class PaginationOptions {
@IsOptional()
@IsString()
query: string;
+
+ @IsOptional()
+ @IsString()
+ groupId?: string;
}
diff --git a/apps/server/src/database/repos/user/user.repo.ts b/apps/server/src/database/repos/user/user.repo.ts
index c7c7b2b2..850b3c14 100644
--- a/apps/server/src/database/repos/user/user.repo.ts
+++ b/apps/server/src/database/repos/user/user.repo.ts
@@ -162,6 +162,15 @@ export class UserRepo {
);
}
+ if (pagination.groupId) {
+ query = query.where('users.id', 'not in', (eb) =>
+ eb
+ .selectFrom('groupUsers')
+ .select('userId')
+ .where('groupId', '=', pagination.groupId),
+ );
+ }
+
const result = executeWithPagination(query, {
page: pagination.page,
perPage: pagination.limit,