mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
fix: don't show existing members when adding to groups and spaces (WIP)
This commit is contained in:
@@ -533,5 +533,8 @@
|
||||
"Remove image": "Remove image",
|
||||
"Failed to remove image": "Failed to remove image",
|
||||
"Image exceeds 10MB limit.": "Image exceeds 10MB limit.",
|
||||
"Image removed successfully": "Image removed successfully"
|
||||
"Image removed successfully": "Image removed successfully",
|
||||
"Added successfully": "Added successfully",
|
||||
"Removed successfully": "Removed successfully",
|
||||
"Failed to add group members": "Failed to add group members"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function AddGroupMemberModal() {
|
||||
<MultiUserSelect
|
||||
label={t("Add group members")}
|
||||
onChange={handleMultiSelectChange}
|
||||
groupId={groupId}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
interface MultiUserSelectProps {
|
||||
onChange: (value: string[]) => void;
|
||||
label?: string;
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
@@ -21,7 +22,9 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
size={36}
|
||||
/>
|
||||
<div>
|
||||
<Text size="sm" lineClamp={1}>{option.label}</Text>
|
||||
<Text size="sm" lineClamp={1}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text size="xs" opacity={0.5}>
|
||||
{option?.["email"]}
|
||||
</Text>
|
||||
@@ -29,14 +32,20 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
</Group>
|
||||
);
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -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<void, Error, { groupId: string; userIds: string[] }>({
|
||||
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;
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function AddSpaceMembersModal({
|
||||
<Divider size="xs" mb="xs" />
|
||||
|
||||
<Stack>
|
||||
<MultiMemberSelect onChange={handleMultiSelectChange} />
|
||||
<MultiMemberSelect onChange={handleMultiSelectChange} spaceId={spaceId} />
|
||||
<SpaceMemberRole
|
||||
onSelect={handleRoleSelection}
|
||||
defaultRole={role}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
interface MultiMemberSelectProps {
|
||||
onChange: (value: string[]) => void;
|
||||
spaceId?: string;
|
||||
}
|
||||
|
||||
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
@@ -25,23 +26,38 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
)}
|
||||
{option["type"] === "group" && <IconGroupCircle />}
|
||||
<div>
|
||||
<Text size="sm" lineClamp={1}>{option.label}</Text>
|
||||
<Text size="sm" lineClamp={1}>
|
||||
{option.label}
|
||||
</Text>
|
||||
{option["type"] === "user" && option["email"] && (
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>{option["email"]}</Text>
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{option["email"]}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface QueryParams {
|
||||
query?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
export enum UserRole {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -23,4 +23,8 @@ export class PaginationOptions {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
query: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user