Files
docmost/apps/client/src/features/space/components/multi-member-select.tsx
Philip Okugbe f4082171ec feat: display user email below name in multi-member-select dropdown (#1355)
- Added email field to user items mapping
- Updated renderMultiSelectOption to show email in smaller, dimmed text
- Email only displays for user type options, not groups
2025-07-14 10:37:13 +01:00

124 lines
3.7 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { useDebouncedValue } from "@mantine/hooks";
import { Group, MultiSelect, MultiSelectProps, Text } from "@mantine/core";
import { IGroup } from "@/features/group/types/group.types.ts";
import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query.ts";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { IUser } from "@/features/user/types/user.types.ts";
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
import { useTranslation } from "react-i18next";
interface MultiMemberSelectProps {
onChange: (value: string[]) => void;
}
const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
option,
}) => (
<Group gap="sm" wrap="nowrap">
{option["type"] === "user" && (
<CustomAvatar
avatarUrl={option["avatarUrl"]}
size={20}
name={option.label}
/>
)}
{option["type"] === "group" && <IconGroupCircle />}
<div>
<Text size="sm" lineClamp={1}>{option.label}</Text>
{option["type"] === "user" && option["email"] && (
<Text size="xs" c="dimmed" lineClamp={1}>{option["email"]}</Text>
)}
</div>
</Group>
);
export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
const { t } = useTranslation();
const [searchValue, setSearchValue] = useState("");
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
const { data: suggestion, isLoading } = useSearchSuggestionsQuery({
query: debouncedQuery,
includeUsers: true,
includeGroups: true,
});
const [data, setData] = useState([]);
useEffect(() => {
if (suggestion) {
// Extract user and group items
const userItems = suggestion?.users.map((user: IUser) => ({
value: `user-${user.id}`,
label: user.name,
email: user.email,
avatarUrl: user.avatarUrl,
type: "user",
}));
const groupItems = suggestion?.groups.map((group: IGroup) => ({
value: `group-${group.id}`,
label: group.name,
type: "group",
}));
// Function to merge items into groups without duplicates
const mergeItemsIntoGroups = (existingGroups, newItems, groupName) => {
const existingValues = new Set(
existingGroups.flatMap((group) =>
group.items.map((item) => item.value),
),
);
const newItemsFiltered = newItems.filter(
(item) => !existingValues.has(item.value),
);
const updatedGroups = existingGroups.map((group) => {
if (group.group === groupName) {
return { ...group, items: [...group.items, ...newItemsFiltered] };
}
return group;
});
// Use spread syntax to avoid mutation
return updatedGroups.some((group) => group.group === groupName)
? updatedGroups
: [...updatedGroups, { group: groupName, items: newItemsFiltered }];
};
// Merge user items into groups
const updatedUserGroups = mergeItemsIntoGroups(
data,
userItems,
t("Select a user"),
);
// Merge group items into groups
const finalData = mergeItemsIntoGroups(
updatedUserGroups,
groupItems,
t("Select a group"),
);
setData(finalData);
}
}, [suggestion, data]);
return (
<MultiSelect
data={data}
renderOption={renderMultiSelectOption}
hidePickedOptions
maxDropdownHeight={300}
label={t("Add members")}
placeholder={t("Search for users and groups")}
searchable
searchValue={searchValue}
onSearchChange={setSearchValue}
clearable
variant="filled"
onChange={onChange}
maxValues={50}
/>
);
}