# Feature: Email thread members visibility For this feature we implemented a chip and a dropdown menu that allows users to check which workspace members can see an email thread, as depicted on issue (#4199). ## Implementations - create a new database table (messageThreadMember) - relations between `messageThreadMembers` and the relevant existing tables (`MessageThread` and `WorkspaceMembers`) - added a new column to the `MessageThread table`: `everyone` - to indicate that all workspace members can see the email thread - create a new repository for the new table, including new queries - edit the queries so that the new fields could be fetched from the frontend - created a component `MultiChip`, that shows a group of user avatars, instead of just one - created a component, `ShareDropdownMenu`, that shows up once the `EmailThreadMembersChip` is clicked. On this menu you can see which workspace members can view the email thread. ## Screenshots Here are some screenshots of the frontend components that were created: Chip with everyone in the workspace being part of the message thread:  Chip with just one member of the workspace (the owner) being part of the message thread:  Chip with some members of the workspace being part of the message thread:  How the chip looks in a message thread:  Dropdown that opens when you click on the chip:  ## Testing and Mock data We also added mock data (TypeORM seeds), focusing on adding mock data related to message thread members. ## Conclusion As some of the changes that we needed to do, regarding the change of visibility of the message thread, were not covered by the existing documentation, we were told to open a PR and ask for feedback on this part of the implementation. Right now, our implementation is focused on displaying who is part of an email thread. Feel free to let us know which steps we should follow next :) --------- Co-authored-by: Simão Sanguinho <simao.sanguinho@tecnico.ulisboa.pt> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -0,0 +1,16 @@
|
||||
import { MessageThreadSubscribersDropdownButton } from '@/activities/emails/components/MessageThreadSubscribersDropdownButton';
|
||||
import { MessageThread } from '@/activities/emails/types/MessageThread';
|
||||
|
||||
export const EmailThreadMembersChip = ({
|
||||
messageThread,
|
||||
}: {
|
||||
messageThread: MessageThread;
|
||||
}) => {
|
||||
const subscribers = messageThread.subscribers ?? [];
|
||||
|
||||
return (
|
||||
<MessageThreadSubscribersDropdownButton
|
||||
messageThreadSubscribers={subscribers}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { MessageThreadSubscriberDropdownAddSubscriberMenuItem } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem';
|
||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export const MessageThreadSubscriberDropdownAddSubscriber = ({
|
||||
existingSubscribers,
|
||||
}: {
|
||||
existingSubscribers: MessageThreadSubscriber[];
|
||||
}) => {
|
||||
const { records: workspaceMembersLeftToAdd } =
|
||||
useFindManyRecords<WorkspaceMember>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
filter: {
|
||||
not: {
|
||||
id: {
|
||||
in: existingSubscribers.map(
|
||||
({ workspaceMember }) => workspaceMember.id,
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuSearchInput />
|
||||
<DropdownMenuSeparator />
|
||||
{workspaceMembersLeftToAdd.map((workspaceMember) => (
|
||||
<MessageThreadSubscriberDropdownAddSubscriberMenuItem
|
||||
workspaceMember={workspaceMember}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
export const MessageThreadSubscriberDropdownAddSubscriberMenuItem = ({
|
||||
workspaceMember,
|
||||
}: {
|
||||
workspaceMember: WorkspaceMember;
|
||||
}) => {
|
||||
const text = `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`;
|
||||
|
||||
const { createOneRecord } = useCreateOneRecord<MessageThreadSubscriber>({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageThreadSubscriber,
|
||||
});
|
||||
|
||||
const handleAddButtonClick = () => {
|
||||
createOneRecord({
|
||||
workspaceMember,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItemAvatar
|
||||
avatar={{
|
||||
placeholder: workspaceMember.name.firstName,
|
||||
avatarUrl: workspaceMember.avatarUrl,
|
||||
placeholderColorSeed: workspaceMember.id,
|
||||
size: 'md',
|
||||
type: 'rounded',
|
||||
}}
|
||||
text={text}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPlus,
|
||||
onClick: handleAddButtonClick,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,80 @@
|
||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
Chip,
|
||||
ChipVariant,
|
||||
IconChevronDown,
|
||||
ThemeContext,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const MAX_NUMBER_OF_AVATARS = 3;
|
||||
|
||||
export const MessageThreadSubscribersChip = ({
|
||||
messageThreadSubscribers,
|
||||
}: {
|
||||
messageThreadSubscribers: MessageThreadSubscriber[];
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const numberOfMessageThreadSubscribers = messageThreadSubscribers.length;
|
||||
|
||||
const isOnlyOneSubscriber = numberOfMessageThreadSubscribers === 1;
|
||||
|
||||
const isPrivateThread = isOnlyOneSubscriber;
|
||||
|
||||
const privateLabel = 'Private';
|
||||
|
||||
const susbcriberAvatarUrls = messageThreadSubscribers
|
||||
.map((member) => member.workspaceMember.avatarUrl)
|
||||
.filter(isNonEmptyString);
|
||||
|
||||
const firstAvatarUrl = susbcriberAvatarUrls[0];
|
||||
const firstAvatarColorSeed = messageThreadSubscribers?.[0].workspaceMember.id;
|
||||
const firstAvatarPlaceholder =
|
||||
messageThreadSubscribers?.[0].workspaceMember.name.firstName;
|
||||
|
||||
const subscriberNames = messageThreadSubscribers.map(
|
||||
(member) => member.workspaceMember?.name.firstName,
|
||||
);
|
||||
|
||||
const moreAvatarsLabel =
|
||||
numberOfMessageThreadSubscribers > MAX_NUMBER_OF_AVATARS
|
||||
? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}`
|
||||
: null;
|
||||
|
||||
const label = isPrivateThread ? privateLabel : moreAvatarsLabel ?? '';
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={label}
|
||||
variant={ChipVariant.Highlighted}
|
||||
leftComponent={
|
||||
isOnlyOneSubscriber ? (
|
||||
<Avatar
|
||||
avatarUrl={firstAvatarUrl}
|
||||
placeholderColorSeed={firstAvatarColorSeed}
|
||||
placeholder={firstAvatarPlaceholder}
|
||||
size="md"
|
||||
type={'rounded'}
|
||||
/>
|
||||
) : (
|
||||
<AvatarGroup
|
||||
avatars={subscriberNames.map((name, index) => (
|
||||
<Avatar
|
||||
key={name}
|
||||
avatarUrl={susbcriberAvatarUrls[index] ?? ''}
|
||||
placeholder={name}
|
||||
type="rounded"
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
rightComponent={<IconChevronDown size={theme.icon.size.sm} />}
|
||||
clickable
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,110 @@
|
||||
import { offset } from '@floating-ui/react';
|
||||
import { IconMinus, IconPlus } from 'twenty-ui';
|
||||
|
||||
import { MessageThreadSubscriberDropdownAddSubscriber } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber';
|
||||
import { MessageThreadSubscribersChip } from '@/activities/emails/components/MessageThreadSubscribersChip';
|
||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID =
|
||||
'message-thread-subscriber';
|
||||
|
||||
export const MessageThreadSubscribersDropdownButton = ({
|
||||
messageThreadSubscribers,
|
||||
}: {
|
||||
messageThreadSubscribers: MessageThreadSubscriber[];
|
||||
}) => {
|
||||
const [isAddingSubscriber, setIsAddingSubscriber] = useState(false);
|
||||
|
||||
const { closeDropdown } = useDropdown(MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID);
|
||||
|
||||
const mockSubscribers = [
|
||||
...messageThreadSubscribers,
|
||||
...messageThreadSubscribers,
|
||||
...messageThreadSubscribers,
|
||||
...messageThreadSubscribers,
|
||||
];
|
||||
|
||||
// TODO: implement
|
||||
const handleAddSubscriberClick = () => {
|
||||
setIsAddingSubscriber(true);
|
||||
};
|
||||
|
||||
// TODO: implement
|
||||
const handleRemoveSubscriber = (_subscriber: MessageThreadSubscriber) => {
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
useListenRightDrawerClose(() => {
|
||||
closeDropdown();
|
||||
});
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID}
|
||||
clickableComponent={
|
||||
<MessageThreadSubscribersChip
|
||||
messageThreadSubscribers={mockSubscribers}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu width="160px" z-index={offset(1)}>
|
||||
{isAddingSubscriber ? (
|
||||
<MessageThreadSubscriberDropdownAddSubscriber
|
||||
existingSubscribers={messageThreadSubscribers}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuItemsContainer>
|
||||
{messageThreadSubscribers?.map((subscriber) => (
|
||||
<MenuItemAvatar
|
||||
key={subscriber.workspaceMember.id}
|
||||
testId="menu-item"
|
||||
onClick={() => {
|
||||
handleRemoveSubscriber(subscriber);
|
||||
}}
|
||||
text={
|
||||
subscriber.workspaceMember.name.firstName +
|
||||
' ' +
|
||||
subscriber.workspaceMember.name.lastName
|
||||
}
|
||||
avatar={{
|
||||
placeholder: subscriber.workspaceMember.name.firstName,
|
||||
avatarUrl: subscriber.workspaceMember.avatarUrl,
|
||||
placeholderColorSeed: subscriber.workspaceMember.id,
|
||||
size: 'md',
|
||||
type: 'rounded',
|
||||
}}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconMinus,
|
||||
onClick: () => {
|
||||
handleRemoveSubscriber(subscriber);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<MenuItem
|
||||
LeftIcon={IconPlus}
|
||||
onClick={handleAddSubscriberClick}
|
||||
text="Add subscriber"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -2,7 +2,13 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||
|
||||
export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
||||
({ messageThreadId }: { messageThreadId: string }) => ({
|
||||
({
|
||||
messageThreadId,
|
||||
isSubscribersEnabled,
|
||||
}: {
|
||||
messageThreadId: string;
|
||||
isSubscribersEnabled: boolean;
|
||||
}) => ({
|
||||
objectNameSingular: CoreObjectNameSingular.Message,
|
||||
variables: {
|
||||
filter: {
|
||||
@ -25,6 +31,18 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation
|
||||
subject: true,
|
||||
text: true,
|
||||
receivedAt: true,
|
||||
messageThread: {
|
||||
id: true,
|
||||
subscribers: isSubscribersEnabled
|
||||
? {
|
||||
workspaceMember: {
|
||||
id: true,
|
||||
name: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
messageParticipants: {
|
||||
id: true,
|
||||
role: true,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
|
||||
@ -8,8 +9,8 @@ import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMe
|
||||
import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages';
|
||||
import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread';
|
||||
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
|
||||
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
|
||||
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -22,21 +23,31 @@ const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const getVisibleMessages = (messages: EmailThreadMessageType[]) =>
|
||||
messages.filter(({ messageParticipants }) => {
|
||||
const from = messageParticipants.find(
|
||||
(participant) => participant.role === 'from',
|
||||
);
|
||||
const receivers = messageParticipants.filter(
|
||||
(participant) => participant.role !== 'from',
|
||||
);
|
||||
return from && receivers.length > 0;
|
||||
});
|
||||
|
||||
export const RightDrawerEmailThread = () => {
|
||||
const setMessageThread = useSetRecoilState(messageThreadState);
|
||||
|
||||
const { thread, messages, fetchMoreMessages, loading } =
|
||||
useRightDrawerEmailThread();
|
||||
|
||||
const visibleMessages = useMemo(() => {
|
||||
return messages.filter(({ messageParticipants }) => {
|
||||
const from = messageParticipants.find(
|
||||
(participant) => participant.role === 'from',
|
||||
);
|
||||
const receivers = messageParticipants.filter(
|
||||
(participant) => participant.role !== 'from',
|
||||
);
|
||||
return from && receivers.length > 0;
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleMessages[0]?.messageThread) {
|
||||
return;
|
||||
}
|
||||
setMessageThread(visibleMessages[0]?.messageThread);
|
||||
});
|
||||
|
||||
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
|
||||
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
);
|
||||
@ -60,7 +71,6 @@ export const RightDrawerEmailThread = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const visibleMessages = getVisibleMessages(messages);
|
||||
const visibleMessagesCount = visibleMessages.length;
|
||||
const is5OrMoreMessages = visibleMessagesCount >= 5;
|
||||
const firstMessages = visibleMessages.slice(
|
||||
|
||||
@ -3,12 +3,13 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
|
||||
import { EmailThread } from '@/activities/emails/types/EmailThread';
|
||||
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
|
||||
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const useRightDrawerEmailThread = () => {
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
@ -20,19 +21,26 @@ export const useRightDrawerEmailThread = () => {
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
},
|
||||
onCompleted: (record) => upsertRecords([record]),
|
||||
onCompleted: (record) => {
|
||||
upsertRecords([record]);
|
||||
},
|
||||
});
|
||||
|
||||
const isMessageThreadSubscribersEnabled = useIsFeatureEnabled(
|
||||
'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED',
|
||||
);
|
||||
|
||||
const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE =
|
||||
fetchAllThreadMessagesOperationSignatureFactory({
|
||||
messageThreadId: viewableRecordId,
|
||||
isSubscribersEnabled: isMessageThreadSubscribersEnabled,
|
||||
});
|
||||
|
||||
const {
|
||||
records: messages,
|
||||
loading,
|
||||
fetchMoreRecords,
|
||||
} = useFindManyRecords<EmailThreadMessageType>({
|
||||
} = useFindManyRecords<EmailThreadMessage>({
|
||||
limit: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.limit,
|
||||
filter: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.filter,
|
||||
objectNameSingular:
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
|
||||
import { MessageThread } from '@/activities/emails/types/MessageThread';
|
||||
|
||||
export type EmailThreadMessage = {
|
||||
id: string;
|
||||
@ -7,5 +8,6 @@ export type EmailThreadMessage = {
|
||||
subject: string;
|
||||
messageThreadId: string;
|
||||
messageParticipants: EmailThreadMessageParticipant[];
|
||||
messageThread: MessageThread;
|
||||
__typename: 'EmailThreadMessage';
|
||||
};
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||
|
||||
export type MessageThread = {
|
||||
id: string;
|
||||
subscribers?: MessageThreadSubscriber[];
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export type MessageThreadSubscriber = {
|
||||
__typename: 'MessageThreadSubscriber';
|
||||
id: string;
|
||||
workspaceMember: WorkspaceMember;
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { EmailThreadMembersChip } from '@/activities/emails/components/EmailThreadMembersChip';
|
||||
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const MessageThreadSubscribersTopBar = () => {
|
||||
const isMessageThreadSubscriberEnabled = useIsFeatureEnabled(
|
||||
'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED',
|
||||
);
|
||||
|
||||
const messageThread = useRecoilValue(messageThreadState);
|
||||
|
||||
const numberOfSubscribers = messageThread?.subscribers?.length ?? 0;
|
||||
|
||||
const shouldShowMembersChip = numberOfSubscribers > 0;
|
||||
|
||||
if (
|
||||
!isMessageThreadSubscriberEnabled ||
|
||||
!isDefined(messageThread) ||
|
||||
!shouldShowMembersChip
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButtonContainer>
|
||||
<EmailThreadMembersChip messageThread={messageThread} />
|
||||
</StyledButtonContainer>
|
||||
);
|
||||
};
|
||||
@ -27,4 +27,5 @@ export enum CoreObjectNameSingular {
|
||||
ViewSort = 'viewSort',
|
||||
Webhook = 'webhook',
|
||||
WorkspaceMember = 'workspaceMember',
|
||||
MessageThreadSubscriber = 'messageThreadSubscriber',
|
||||
}
|
||||
|
||||
@ -3,13 +3,12 @@ import {
|
||||
SingleEntitySelectMenuItems,
|
||||
SingleEntitySelectMenuItemsProps,
|
||||
} from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems';
|
||||
import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch';
|
||||
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
|
||||
|
||||
export type SingleEntitySelectMenuItemsWithSearchProps = {
|
||||
excludedRelationRecordIds?: string[];
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
|
||||
@ -3,6 +3,7 @@ import { useRecoilState } from 'recoil';
|
||||
import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useDropdown = (dropdownId?: string) => {
|
||||
@ -27,10 +28,10 @@ export const useDropdown = (dropdownId?: string) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] =
|
||||
useRecoilState(isDropdownOpenState);
|
||||
|
||||
const closeDropdown = () => {
|
||||
const closeDropdown = useCallback(() => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
}, [goBackToPreviousHotkeyScope, setIsDropdownOpen]);
|
||||
|
||||
const openDropdown = () => {
|
||||
setIsDropdownOpen(true);
|
||||
|
||||
@ -25,6 +25,7 @@ import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
|
||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
||||
import { RightDrawerHotkeyScope } from '../types/RightDrawerHotkeyScope';
|
||||
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { RightDrawerRouter } from './RightDrawerRouter';
|
||||
|
||||
const StyledContainer = styled(motion.div)`
|
||||
@ -47,6 +48,41 @@ const StyledRightDrawer = styled.div`
|
||||
`;
|
||||
|
||||
export const RightDrawer = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const animationVariants = {
|
||||
fullScreen: {
|
||||
x: '0%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
},
|
||||
normal: {
|
||||
x: '0%',
|
||||
width: theme.rightDrawerWidth,
|
||||
height: '100%',
|
||||
bottom: '0',
|
||||
top: '0',
|
||||
},
|
||||
closed: {
|
||||
x: '100%',
|
||||
width: '0',
|
||||
height: '100%',
|
||||
bottom: '0',
|
||||
top: 'auto',
|
||||
},
|
||||
minimized: {
|
||||
x: '0%',
|
||||
width: 220,
|
||||
height: 41,
|
||||
bottom: '0',
|
||||
top: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
type RightDrawerAnimationVariant = keyof typeof animationVariants;
|
||||
|
||||
const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
|
||||
isRightDrawerOpenState,
|
||||
);
|
||||
@ -82,6 +118,8 @@ export const RightDrawer = () => {
|
||||
if (isRightDrawerOpen && !isRightDrawerMinimized) {
|
||||
set(rightDrawerCloseEventState, event);
|
||||
closeRightDrawer();
|
||||
|
||||
emitRightDrawerCloseEvent();
|
||||
}
|
||||
},
|
||||
[closeRightDrawer],
|
||||
@ -89,11 +127,8 @@ export const RightDrawer = () => {
|
||||
mode: ClickOutsideMode.comparePixels,
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
|
||||
() => {
|
||||
closeRightDrawer();
|
||||
},
|
||||
@ -103,56 +138,27 @@ export const RightDrawer = () => {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const rightDrawerWidth = isRightDrawerOpen
|
||||
? isMobile
|
||||
? '100%'
|
||||
: theme.rightDrawerWidth
|
||||
: '0';
|
||||
const targetVariantForAnimation: RightDrawerAnimationVariant =
|
||||
!isRightDrawerOpen
|
||||
? 'closed'
|
||||
: isRightDrawerMinimized
|
||||
? 'minimized'
|
||||
: isMobile
|
||||
? 'fullScreen'
|
||||
: 'normal';
|
||||
|
||||
const handleAnimationComplete = () => {
|
||||
setIsRightDrawerAnimationCompleted(isRightDrawerOpen);
|
||||
};
|
||||
|
||||
if (!isDefined(rightDrawerPage)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const variants = {
|
||||
fullScreen: {
|
||||
x: '0%',
|
||||
},
|
||||
normal: {
|
||||
x: '0%',
|
||||
width: rightDrawerWidth,
|
||||
},
|
||||
closed: {
|
||||
x: '100%',
|
||||
},
|
||||
minimized: {
|
||||
x: '0%',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
bottom: '0',
|
||||
top: 'auto',
|
||||
},
|
||||
};
|
||||
const handleAnimationComplete = () => {
|
||||
setIsRightDrawerAnimationCompleted(isRightDrawerOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
initial={
|
||||
isRightDrawerOpen
|
||||
? isRightDrawerMinimized
|
||||
? 'minimized'
|
||||
: 'normal'
|
||||
: 'closed'
|
||||
}
|
||||
animate={
|
||||
isRightDrawerOpen
|
||||
? isRightDrawerMinimized
|
||||
? 'minimized'
|
||||
: 'normal'
|
||||
: 'closed'
|
||||
}
|
||||
variants={variants}
|
||||
animate={targetVariantForAnimation}
|
||||
variants={animationVariants}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
|
||||
@ -5,9 +5,11 @@ import { RightDrawerCalendarEvent } from '@/activities/calendar/right-drawer/com
|
||||
import { RightDrawerAIChat } from '@/activities/copilot/right-drawer/components/RightDrawerAIChat';
|
||||
import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread';
|
||||
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
|
||||
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
|
||||
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
|
||||
|
||||
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
|
||||
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
||||
import { RightDrawerPages } from '../types/RightDrawerPages';
|
||||
|
||||
@ -28,39 +30,31 @@ const StyledRightDrawerBody = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const RIGHT_DRAWER_PAGES_CONFIG = {
|
||||
[RightDrawerPages.ViewEmailThread]: {
|
||||
page: <RightDrawerEmailThread />,
|
||||
topBar: <RightDrawerTopBar page={RightDrawerPages.ViewEmailThread} />,
|
||||
},
|
||||
[RightDrawerPages.ViewCalendarEvent]: {
|
||||
page: <RightDrawerCalendarEvent />,
|
||||
topBar: <RightDrawerTopBar page={RightDrawerPages.ViewCalendarEvent} />,
|
||||
},
|
||||
[RightDrawerPages.ViewRecord]: {
|
||||
page: <RightDrawerRecord />,
|
||||
topBar: <RightDrawerTopBar page={RightDrawerPages.ViewRecord} />,
|
||||
},
|
||||
[RightDrawerPages.Copilot]: {
|
||||
page: <RightDrawerAIChat />,
|
||||
topBar: <RightDrawerTopBar page={RightDrawerPages.Copilot} />,
|
||||
},
|
||||
const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = {
|
||||
[RightDrawerPages.ViewEmailThread]: <RightDrawerEmailThread />,
|
||||
[RightDrawerPages.ViewCalendarEvent]: <RightDrawerCalendarEvent />,
|
||||
[RightDrawerPages.ViewRecord]: <RightDrawerRecord />,
|
||||
[RightDrawerPages.Copilot]: <RightDrawerAIChat />,
|
||||
};
|
||||
|
||||
export const RightDrawerRouter = () => {
|
||||
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
|
||||
|
||||
const { topBar = null, page = null } = rightDrawerPage
|
||||
? RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage]
|
||||
: {};
|
||||
const rightDrawerPageComponent = isDefined(rightDrawerPage) ? (
|
||||
RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage]
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
|
||||
|
||||
return (
|
||||
<StyledRightDrawerPage>
|
||||
{topBar}
|
||||
<RightDrawerTopBar />
|
||||
{!isRightDrawerMinimized && (
|
||||
<StyledRightDrawerBody>{page}</StyledRightDrawerBody>
|
||||
<StyledRightDrawerBody>
|
||||
{rightDrawerPageComponent}
|
||||
</StyledRightDrawerBody>
|
||||
)}
|
||||
</StyledRightDrawerPage>
|
||||
);
|
||||
|
||||
@ -8,16 +8,19 @@ import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShow
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
|
||||
import { RightDrawerTopBarDropdownButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton';
|
||||
import { RightDrawerTopBarExpandButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
|
||||
import { RightDrawerTopBarMinimizeButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton';
|
||||
import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar';
|
||||
import { RIGHT_DRAWER_PAGE_ICONS } from '@/ui/layout/right-drawer/constants/RightDrawerPageIcons';
|
||||
import { RIGHT_DRAWER_PAGE_TITLES } from '@/ui/layout/right-drawer/constants/RightDrawerPageTitles';
|
||||
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
|
||||
import { rightDrawerPageState } from '@/ui/layout/right-drawer/states/rightDrawerPageState';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
const StyledTopBarWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
@ -40,9 +43,11 @@ const StyledMinimizeTopBarIcon = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => {
|
||||
export const RightDrawerTopBar = () => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
|
||||
|
||||
const [isRightDrawerMinimized, setIsRightDrawerMinimized] = useRecoilState(
|
||||
isRightDrawerMinimizedState,
|
||||
);
|
||||
@ -57,8 +62,6 @@ export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => {
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const PageIcon = getIcon(RIGHT_DRAWER_PAGE_ICONS[page]);
|
||||
|
||||
const viewableRecordNameSingular = useRecoilValue(
|
||||
viewableRecordNameSingularState,
|
||||
);
|
||||
@ -69,14 +72,21 @@ export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => {
|
||||
objectNameSingular: viewableRecordNameSingular ?? 'company',
|
||||
});
|
||||
|
||||
if (!rightDrawerPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const PageIcon = getIcon(RIGHT_DRAWER_PAGE_ICONS[rightDrawerPage]);
|
||||
|
||||
const ObjectIcon = getIcon(objectMetadataItem.icon);
|
||||
|
||||
const label =
|
||||
page === RightDrawerPages.ViewRecord
|
||||
rightDrawerPage === RightDrawerPages.ViewRecord
|
||||
? objectMetadataItem.labelSingular
|
||||
: RIGHT_DRAWER_PAGE_TITLES[page];
|
||||
: RIGHT_DRAWER_PAGE_TITLES[rightDrawerPage];
|
||||
|
||||
const Icon = page === RightDrawerPages.ViewRecord ? ObjectIcon : PageIcon;
|
||||
const Icon =
|
||||
rightDrawerPage === RightDrawerPages.ViewRecord ? ObjectIcon : PageIcon;
|
||||
|
||||
return (
|
||||
<StyledRightDrawerTopBar
|
||||
@ -101,6 +111,7 @@ export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => {
|
||||
</StyledMinimizeTopBarTitleContainer>
|
||||
)}
|
||||
<StyledTopBarWrapper>
|
||||
<RightDrawerTopBarDropdownButton />
|
||||
{!isMobile && !isRightDrawerMinimized && (
|
||||
<RightDrawerTopBarMinimizeButton />
|
||||
)}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { MessageThreadSubscribersTopBar } from '@/activities/right-drawer/components/MessageThreadSubscribersTopBar';
|
||||
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
|
||||
import { rightDrawerPageState } from '@/ui/layout/right-drawer/states/rightDrawerPageState';
|
||||
import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const RIGHT_DRAWER_TOP_BAR_DROPDOWN_BUTTON_CONFIG: ComponentByRightDrawerPage =
|
||||
{
|
||||
[RightDrawerPages.ViewEmailThread]: <MessageThreadSubscribersTopBar />,
|
||||
};
|
||||
|
||||
export const RightDrawerTopBarDropdownButton = () => {
|
||||
const [isRightDrawerMinimized] = useRecoilState(isRightDrawerMinimizedState);
|
||||
|
||||
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
|
||||
|
||||
if (isRightDrawerMinimized || !isDefined(rightDrawerPage)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dropdownButtonComponent =
|
||||
RIGHT_DRAWER_TOP_BAR_DROPDOWN_BUTTON_CONFIG[rightDrawerPage];
|
||||
|
||||
return dropdownButtonComponent ?? <></>;
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
import { RIGHT_DRAWER_CLOSE_EVENT_NAME } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useListenRightDrawerClose = (callback: () => void) => {
|
||||
useEffect(() => {
|
||||
window.addEventListener(RIGHT_DRAWER_CLOSE_EVENT_NAME, callback);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(RIGHT_DRAWER_CLOSE_EVENT_NAME, callback);
|
||||
};
|
||||
}, [callback]);
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { MessageThread } from '@/activities/emails/types/MessageThread';
|
||||
|
||||
export const messageThreadState = createState<MessageThread | null>({
|
||||
key: 'messageThreadState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { RightDrawerTopBarDropdownButtons } from '@/ui/layout/right-drawer/types/RightDrawerTopBarDropdownButtons';
|
||||
|
||||
export const rightDrawerTopBarDropdownButtonState =
|
||||
createState<RightDrawerTopBarDropdownButtons | null>({
|
||||
key: 'rightDrawerTopBarDropdownButtonState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
|
||||
export type ComponentByRightDrawerPage = {
|
||||
[componentName in RightDrawerPages]?: JSX.Element;
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export enum RightDrawerTopBarDropdownButtons {
|
||||
EmailThreadSubscribers = 'EmailThreadSubscribers',
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export const RIGHT_DRAWER_CLOSE_EVENT_NAME = 'right-drawer-close';
|
||||
|
||||
export const emitRightDrawerCloseEvent = () => {
|
||||
window.dispatchEvent(new CustomEvent(RIGHT_DRAWER_CLOSE_EVENT_NAME));
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react';
|
||||
import { IconChevronRight, IconComponent } from 'twenty-ui';
|
||||
|
||||
import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton';
|
||||
@ -76,7 +76,6 @@ export const MenuItem = ({
|
||||
<LightIconButtonGroup iconButtons={iconButtons} size="small" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasSubMenu && (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.sm}
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { FunctionComponent, MouseEvent, ReactElement } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarProps,
|
||||
IconChevronRight,
|
||||
IconComponent,
|
||||
isDefined,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton';
|
||||
import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup';
|
||||
|
||||
import {
|
||||
StyledHoverableMenuItemBase,
|
||||
StyledMenuItemLeftContent,
|
||||
} from '../internals/components/StyledMenuItemBase';
|
||||
import { MenuItemAccent } from '../types/MenuItemAccent';
|
||||
|
||||
export type MenuItemIconButton = {
|
||||
Wrapper?: FunctionComponent<{ iconButton: ReactElement }>;
|
||||
Icon: IconComponent;
|
||||
accent?: LightIconButtonProps['accent'];
|
||||
onClick?: (event: MouseEvent<any>) => void;
|
||||
};
|
||||
|
||||
export type MenuItemAvatarProps = {
|
||||
accent?: MenuItemAccent;
|
||||
className?: string;
|
||||
iconButtons?: MenuItemIconButton[];
|
||||
isIconDisplayedOnHoverOnly?: boolean;
|
||||
isTooltipOpen?: boolean;
|
||||
avatar?: Pick<
|
||||
AvatarProps,
|
||||
'avatarUrl' | 'placeholderColorSeed' | 'placeholder' | 'size' | 'type'
|
||||
> | null;
|
||||
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
onMouseEnter?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
onMouseLeave?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
testId?: string;
|
||||
text: string;
|
||||
hasSubMenu?: boolean;
|
||||
};
|
||||
|
||||
// TODO: merge with MenuItem
|
||||
export const MenuItemAvatar = ({
|
||||
accent = 'default',
|
||||
className,
|
||||
iconButtons,
|
||||
isIconDisplayedOnHoverOnly = true,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
testId,
|
||||
avatar,
|
||||
hasSubMenu = false,
|
||||
text,
|
||||
}: MenuItemAvatarProps) => {
|
||||
const theme = useTheme();
|
||||
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
|
||||
|
||||
const handleMenuItemClick = (event: MouseEvent<HTMLDivElement>) => {
|
||||
if (!onClick) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClick?.(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHoverableMenuItemBase
|
||||
data-testid={testId ?? undefined}
|
||||
onClick={handleMenuItemClick}
|
||||
className={className}
|
||||
accent={accent}
|
||||
isIconDisplayedOnHoverOnly={isIconDisplayedOnHoverOnly}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<StyledMenuItemLeftContent>
|
||||
{isDefined(avatar) && (
|
||||
<Avatar
|
||||
placeholder={avatar.placeholder}
|
||||
avatarUrl={avatar.avatarUrl}
|
||||
placeholderColorSeed={avatar.placeholderColorSeed}
|
||||
size={avatar.size}
|
||||
type={avatar.type}
|
||||
/>
|
||||
)}
|
||||
<OverflowingTextWithTooltip text={text ?? ''} />
|
||||
</StyledMenuItemLeftContent>
|
||||
<div className="hoverable-buttons">
|
||||
{showIconButtons && (
|
||||
<LightIconButtonGroup iconButtons={iconButtons} size="small" />
|
||||
)}
|
||||
</div>
|
||||
{hasSubMenu && (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
)}
|
||||
</StyledHoverableMenuItemBase>
|
||||
);
|
||||
};
|
||||
@ -7,4 +7,5 @@ export type FeatureFlagKey =
|
||||
| 'IS_STRIPE_INTEGRATION_ENABLED'
|
||||
| 'IS_FUNCTION_SETTINGS_ENABLED'
|
||||
| 'IS_COPILOT_ENABLED'
|
||||
| 'IS_CRM_MIGRATION_ENABLED';
|
||||
| 'IS_CRM_MIGRATION_ENABLED'
|
||||
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED';
|
||||
|
||||
Reference in New Issue
Block a user