From 53d9b3beac9b82b6db9587d1d62a89a0082135a5 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Wed, 20 Jul 2022 18:35:49 -0300 Subject: [PATCH 1/9] wip --- .../InfoPanel/Action.tsx | 0 .../InfoPanel/ActionGroup.tsx | 0 .../InfoPanel/Avatar.tsx | 0 .../{views => components}/InfoPanel/Field.tsx | 0 .../InfoPanel/InfoPanel.stories.tsx | 0 .../InfoPanel/InfoPanel.tsx | 0 .../{views => components}/InfoPanel/Label.tsx | 0 .../InfoPanel/RetentionPolicyCallout.tsx | 0 .../InfoPanel/Section.tsx | 0 .../{views => components}/InfoPanel/Text.tsx | 0 .../{views => components}/InfoPanel/Title.tsx | 0 .../{views => components}/InfoPanel/index.ts | 0 .../components/UserCard/UserCardRole.tsx | 2 +- .../components/UserCard/UserCardUsername.tsx | 2 +- .../UserInfo/UserInfo.stories.tsx | 4 +- .../UserInfo/UserInfo.tsx} | 135 +++++++++--------- .../components/UserInfo/UserInfoAction.tsx | 15 ++ .../components/UserInfo/UserInfoAvatar.tsx | 9 ++ .../components/UserInfo/UserInfoUsername.tsx | 17 +++ .../client/components/UserInfo/index.ts | 13 ++ .../UserAvatarEditor/UserAvatarEditor.js | 4 +- .../client/hooks/useUserCustomFields.ts | 42 ++++++ .../client/lib/utils/getUserEmailVerified.ts | 2 +- .../admin/users/AdminUserInfoActions.tsx | 124 ++++++++++++++++ ...oWithData.js => AdminUserInfoWithData.tsx} | 85 ++++++----- .../users/hooks/useChangeAdminStatusAction.ts | 28 ++++ .../users/hooks/useChangeUserStatusAction.ts | 50 +++++++ .../admin/users/hooks/useDeleteUserAction.tsx | 72 ++++++++++ .../users/hooks/useResetE2EKeyAction.tsx | 43 ++++++ .../admin/users/hooks/useResetTOTPAction.tsx | 43 ++++++ .../admin/users/lib/confirmOwnerChanges.tsx | 40 ++++++ .../views/omnichannel/agents/AgentEdit.tsx | 2 +- .../views/omnichannel/agents/AgentInfo.tsx | 2 +- .../calls/contextualBar/InfoField.tsx | 2 +- .../calls/contextualBar/VoipInfo.tsx | 2 +- .../contextualBar/Info/RoomInfo/RoomInfo.js | 4 +- .../room/contextualBar/UserInfo/Action.js | 11 -- .../room/contextualBar/UserInfo/Avatar.js | 7 - .../UserActions.js => UserInfoActions.tsx} | 28 ++-- .../UserInfo/UserInfoWithData.js | 10 +- .../room/contextualBar/UserInfo/Username.js | 7 - .../room/contextualBar/UserInfo/index.js | 15 -- .../room/contextualBar/UserInfo/index.ts | 1 + .../MaxChatsPerAgentDisplay.js | 2 +- packages/core-typings/src/IUser.ts | 2 + 45 files changed, 656 insertions(+), 169 deletions(-) rename apps/meteor/client/{views => components}/InfoPanel/Action.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/ActionGroup.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/Avatar.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/Field.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/InfoPanel.stories.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/InfoPanel.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/Label.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/RetentionPolicyCallout.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/Section.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/Text.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/Title.tsx (100%) rename apps/meteor/client/{views => components}/InfoPanel/index.ts (100%) rename apps/meteor/client/{views/room/contextualBar => components}/UserInfo/UserInfo.stories.tsx (96%) rename apps/meteor/client/{views/room/contextualBar/UserInfo/UserInfo.js => components/UserInfo/UserInfo.tsx} (61%) create mode 100644 apps/meteor/client/components/UserInfo/UserInfoAction.tsx create mode 100644 apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx create mode 100644 apps/meteor/client/components/UserInfo/UserInfoUsername.tsx create mode 100644 apps/meteor/client/components/UserInfo/index.ts create mode 100644 apps/meteor/client/hooks/useUserCustomFields.ts create mode 100644 apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx rename apps/meteor/client/views/admin/users/{AdminUserInfoWithData.js => AdminUserInfoWithData.tsx} (51%) create mode 100644 apps/meteor/client/views/admin/users/hooks/useChangeAdminStatusAction.ts create mode 100644 apps/meteor/client/views/admin/users/hooks/useChangeUserStatusAction.ts create mode 100644 apps/meteor/client/views/admin/users/hooks/useDeleteUserAction.tsx create mode 100644 apps/meteor/client/views/admin/users/hooks/useResetE2EKeyAction.tsx create mode 100644 apps/meteor/client/views/admin/users/hooks/useResetTOTPAction.tsx create mode 100644 apps/meteor/client/views/admin/users/lib/confirmOwnerChanges.tsx delete mode 100644 apps/meteor/client/views/room/contextualBar/UserInfo/Action.js delete mode 100644 apps/meteor/client/views/room/contextualBar/UserInfo/Avatar.js rename apps/meteor/client/views/room/contextualBar/UserInfo/{actions/UserActions.js => UserInfoActions.tsx} (50%) delete mode 100644 apps/meteor/client/views/room/contextualBar/UserInfo/Username.js delete mode 100644 apps/meteor/client/views/room/contextualBar/UserInfo/index.js create mode 100644 apps/meteor/client/views/room/contextualBar/UserInfo/index.ts diff --git a/apps/meteor/client/views/InfoPanel/Action.tsx b/apps/meteor/client/components/InfoPanel/Action.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/Action.tsx rename to apps/meteor/client/components/InfoPanel/Action.tsx diff --git a/apps/meteor/client/views/InfoPanel/ActionGroup.tsx b/apps/meteor/client/components/InfoPanel/ActionGroup.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/ActionGroup.tsx rename to apps/meteor/client/components/InfoPanel/ActionGroup.tsx diff --git a/apps/meteor/client/views/InfoPanel/Avatar.tsx b/apps/meteor/client/components/InfoPanel/Avatar.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/Avatar.tsx rename to apps/meteor/client/components/InfoPanel/Avatar.tsx diff --git a/apps/meteor/client/views/InfoPanel/Field.tsx b/apps/meteor/client/components/InfoPanel/Field.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/Field.tsx rename to apps/meteor/client/components/InfoPanel/Field.tsx diff --git a/apps/meteor/client/views/InfoPanel/InfoPanel.stories.tsx b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/InfoPanel.stories.tsx rename to apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx diff --git a/apps/meteor/client/views/InfoPanel/InfoPanel.tsx b/apps/meteor/client/components/InfoPanel/InfoPanel.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/InfoPanel.tsx rename to apps/meteor/client/components/InfoPanel/InfoPanel.tsx diff --git a/apps/meteor/client/views/InfoPanel/Label.tsx b/apps/meteor/client/components/InfoPanel/Label.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/Label.tsx rename to apps/meteor/client/components/InfoPanel/Label.tsx diff --git a/apps/meteor/client/views/InfoPanel/RetentionPolicyCallout.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/RetentionPolicyCallout.tsx rename to apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx diff --git a/apps/meteor/client/views/InfoPanel/Section.tsx b/apps/meteor/client/components/InfoPanel/Section.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/Section.tsx rename to apps/meteor/client/components/InfoPanel/Section.tsx diff --git a/apps/meteor/client/views/InfoPanel/Text.tsx b/apps/meteor/client/components/InfoPanel/Text.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/Text.tsx rename to apps/meteor/client/components/InfoPanel/Text.tsx diff --git a/apps/meteor/client/views/InfoPanel/Title.tsx b/apps/meteor/client/components/InfoPanel/Title.tsx similarity index 100% rename from apps/meteor/client/views/InfoPanel/Title.tsx rename to apps/meteor/client/components/InfoPanel/Title.tsx diff --git a/apps/meteor/client/views/InfoPanel/index.ts b/apps/meteor/client/components/InfoPanel/index.ts similarity index 100% rename from apps/meteor/client/views/InfoPanel/index.ts rename to apps/meteor/client/components/InfoPanel/index.ts diff --git a/apps/meteor/client/components/UserCard/UserCardRole.tsx b/apps/meteor/client/components/UserCard/UserCardRole.tsx index e699f2e32c9d..719313009126 100644 --- a/apps/meteor/client/components/UserCard/UserCardRole.tsx +++ b/apps/meteor/client/components/UserCard/UserCardRole.tsx @@ -3,7 +3,7 @@ import React, { ReactNode, ReactElement } from 'react'; const UserCardRole = ({ children }: { children: ReactNode }): ReactElement => ( - + ); diff --git a/apps/meteor/client/components/UserCard/UserCardUsername.tsx b/apps/meteor/client/components/UserCard/UserCardUsername.tsx index 674a9ce0139f..494f2f3e1e25 100644 --- a/apps/meteor/client/components/UserCard/UserCardUsername.tsx +++ b/apps/meteor/client/components/UserCard/UserCardUsername.tsx @@ -21,7 +21,7 @@ const UserCardUsername = ({ name, status = , ...props }: U withTruncatedText {...props} > - {status}{' '} + {status} {name} diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfo.stories.tsx b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx similarity index 96% rename from apps/meteor/client/views/room/contextualBar/UserInfo/UserInfo.stories.tsx rename to apps/meteor/client/components/UserInfo/UserInfo.stories.tsx index 9ca356a80919..7c5f0173a1a1 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfo.stories.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx @@ -1,8 +1,8 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import * as Status from '../../../../components/UserStatus'; -import VerticalBar from '../../../../components/VerticalBar'; +import * as Status from '../UserStatus'; +import VerticalBar from '../VerticalBar'; import UserInfo from './UserInfo'; export default { diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfo.js b/apps/meteor/client/components/UserInfo/UserInfo.tsx similarity index 61% rename from apps/meteor/client/views/room/contextualBar/UserInfo/UserInfo.js rename to apps/meteor/client/components/UserInfo/UserInfo.tsx index 7c1e4d66afb0..daefdcd7fbd0 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfo.js +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -1,74 +1,80 @@ +import { IUser } from '@rocket.chat/core-typings'; import { Box, Margins, Tag } from '@rocket.chat/fuselage'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { memo } from 'react'; - -import MarkdownText from '../../../../components/MarkdownText'; -import UTCClock from '../../../../components/UTCClock'; -import UserCard from '../../../../components/UserCard'; -import VerticalBar from '../../../../components/VerticalBar'; -import { useTimeAgo } from '../../../../hooks/useTimeAgo'; -import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; -import InfoPanel from '../../../InfoPanel'; -import Avatar from './Avatar'; - -function UserInfo({ +import { TranslationKey, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement } from 'react'; + +import { useTimeAgo } from '../../hooks/useTimeAgo'; +import { useUserCustomFields } from '../../hooks/useUserCustomFields'; +import { getUserDisplayName } from '../../lib/getUserDisplayName'; +import InfoPanel from '../InfoPanel'; +import MarkdownText from '../MarkdownText'; +import UTCClock from '../UTCClock'; +import UserCard from '../UserCard'; +import VerticalBar from '../VerticalBar'; +import UserInfoAvatar from './UserInfoAvatar'; + +type UserInfoProps = { + username: IUser['username']; + name: IUser['name']; + lastLogin: IUser['lastLogin']; + nickname: IUser['nickname']; + bio: IUser['bio']; + avatarETag: IUser['avatarETag']; + roles: IUser['roles']; + utcOffset: IUser['utcOffset']; + phone: IUser['phone']; + createdAt: IUser['createdAt']; + status: ReactElement; + statusText: IUser['statusText']; + canViewAllInfo: IUser['canViewAllInfo']; + email: string; + verified: boolean; + actions: ReactElement; + showRealNames: unknown; + customFields: IUser['customFields']; +}; + +const UserInfo = ({ username, + name, + lastLogin, + nickname, bio, - canViewAllInfo, + avatarETag, + roles, + utcOffset, + phone, email, verified, - showRealNames, - status, - phone, - customStatus, - roles = [], - lastLogin, createdAt, - utcOffset, - customFields = [], - name, - data, - nickname, + status, + statusText, + customFields, + canViewAllInfo, actions, + showRealNames, ...props -}) { +}: UserInfoProps): ReactElement => { const t = useTranslation(); const timeAgo = useTimeAgo(); - const customFieldsToShowSetting = useSetting('Accounts_CustomFieldsToShowInUserInfo'); - let customFieldsToShowObj; - try { - customFieldsToShowObj = JSON.parse(customFieldsToShowSetting); - } catch (error) { - customFieldsToShowObj = undefined; - } - - const customFieldsToShow = customFieldsToShowObj - ? Object.values(customFieldsToShowObj).map((value) => { - const role = Object.values(value); - const roleNameToShow = Object.keys(value); - const customField = {}; - customField[roleNameToShow] = customFields[role]; - return customField; - }) - : []; + const userCustomFields = useUserCustomFields(customFields); return ( - - - + {username && ( + + + + )} {actions && {actions}} - - {customStatus && ( - - - - )} + + + @@ -79,7 +85,7 @@ function UserInfo({ )} - {Number.isInteger(utcOffset) && ( + {utcOffset && Number.isInteger(utcOffset) && ( {t('Local_Time')} @@ -127,7 +133,6 @@ function UserInfo({ {phone && ( - {' '} {t('Phone')} @@ -139,7 +144,6 @@ function UserInfo({ {email && ( - {' '} {t('Email')} @@ -153,15 +157,16 @@ function UserInfo({ )} - {customFieldsToShow.map((customField) => - Object.values(customField)[0] ? ( - - {t(Object.keys(customField)[0])} - - - - - ) : null, + {userCustomFields?.map( + (customField) => + customField?.value && ( + + {t(customField.label as TranslationKey)} + + + + + ), )} {createdAt && ( @@ -174,6 +179,6 @@ function UserInfo({ ); -} +}; export default memo(UserInfo); diff --git a/apps/meteor/client/components/UserInfo/UserInfoAction.tsx b/apps/meteor/client/components/UserInfo/UserInfoAction.tsx new file mode 100644 index 000000000000..9cd161b0d891 --- /dev/null +++ b/apps/meteor/client/components/UserInfo/UserInfoAction.tsx @@ -0,0 +1,15 @@ +import { Button, Icon } from '@rocket.chat/fuselage'; +import React, { ReactElement, ComponentProps } from 'react'; + +type UserInfoActionProps = { + icon: ComponentProps['name']; +} & ComponentProps; + +const UserInfoAction = ({ icon, label, ...props }: UserInfoActionProps): ReactElement => ( + +); + +export default UserInfoAction; diff --git a/apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx b/apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx new file mode 100644 index 000000000000..4a0340f33817 --- /dev/null +++ b/apps/meteor/client/components/UserInfo/UserInfoAvatar.tsx @@ -0,0 +1,9 @@ +import React, { ComponentProps, ReactElement } from 'react'; + +import UserAvatar from '../avatar/UserAvatar'; + +const UserInfoAvatar = ({ username, ...props }: ComponentProps): ReactElement => ( + +); + +export default UserInfoAvatar; diff --git a/apps/meteor/client/components/UserInfo/UserInfoUsername.tsx b/apps/meteor/client/components/UserInfo/UserInfoUsername.tsx new file mode 100644 index 000000000000..61a64e780a11 --- /dev/null +++ b/apps/meteor/client/components/UserInfo/UserInfoUsername.tsx @@ -0,0 +1,17 @@ +import { IUser } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; +import React, { ReactElement, ComponentProps } from 'react'; + +import UserCard from '../UserCard'; + +type UserInfoUsername = { + username: IUser['username']; + status: ReactElement; +} & ComponentProps; + +// TODO: Remove UserCard.Username +const UserInfoUsername = ({ username, status, ...props }: UserInfoUsername): ReactElement => ( + +); + +export default UserInfoUsername; diff --git a/apps/meteor/client/components/UserInfo/index.ts b/apps/meteor/client/components/UserInfo/index.ts new file mode 100644 index 000000000000..f8fe955dbe49 --- /dev/null +++ b/apps/meteor/client/components/UserInfo/index.ts @@ -0,0 +1,13 @@ +import InfoPanel from '../InfoPanel'; +import UserInfo from './UserInfo'; +import UserInfoAction from './UserInfoAction'; +import UserInfoAvatar from './UserInfoAvatar'; +import UserInfoUsername from './UserInfoUsername'; + +export default Object.assign(UserInfo, { + Action: UserInfoAction, + Avatar: UserInfoAvatar, + Info: InfoPanel.Text, + Label: InfoPanel.Label, + Username: UserInfoUsername, +}); diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js index 23a40edcd0a7..5e2b2418e5c6 100644 --- a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js @@ -75,8 +75,8 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, suggestions - - + + {suggestions && ( { + const customFieldsToShowSetting = useSetting('Accounts_CustomFieldsToShowInUserInfo'); + + let customFieldsToShowObj: CustomField[] | undefined; + try { + customFieldsToShowObj = JSON.parse(customFieldsToShowSetting as string); + } catch (error) { + customFieldsToShowObj = undefined; + } + + if (!customFieldsToShowObj) { + return undefined; + } + + const customFieldsToShow = customFieldsToShowObj.map((value) => { + if (!value) { + return undefined; + } + + const [customFieldLabel] = Object.keys(value); + const [customFieldValue] = Object.values(value); + const fieldValue = customFields?.[customFieldValue]; + return { label: customFieldLabel, value: fieldValue !== '' ? fieldValue : undefined }; + }); + + return customFieldsToShow; +}; diff --git a/apps/meteor/client/lib/utils/getUserEmailVerified.ts b/apps/meteor/client/lib/utils/getUserEmailVerified.ts index fd2cd887a2f3..c47eeba9d0b6 100644 --- a/apps/meteor/client/lib/utils/getUserEmailVerified.ts +++ b/apps/meteor/client/lib/utils/getUserEmailVerified.ts @@ -1,4 +1,4 @@ import type { IUser } from '@rocket.chat/core-typings'; -export const getUserEmailVerified = (user: IUser): boolean | undefined => +export const getUserEmailVerified = (user: Pick): boolean | undefined => Array.isArray(user.emails) ? user.emails.find(({ verified }) => !!verified)?.verified : undefined; diff --git a/apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx b/apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx new file mode 100644 index 000000000000..4dd5638649f6 --- /dev/null +++ b/apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx @@ -0,0 +1,124 @@ +import { IUser } from '@rocket.chat/core-typings'; +import { ButtonGroup, Menu, Option } from '@rocket.chat/fuselage'; +import { useRoute, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback, useMemo, ReactElement } from 'react'; + +import UserInfo from '../../../components/UserInfo'; +import { useActionSpread } from '../../hooks/useActionSpread'; +import { useChangeAdminStatusAction } from './hooks/useChangeAdminStatusAction'; +import { useChangeUserStatusAction } from './hooks/useChangeUserStatusAction'; +import { useDeleteUserAction } from './hooks/useDeleteUserAction'; +import { useResetE2EKeyAction } from './hooks/useResetE2EKeyAction'; +import { useResetTOTPAction } from './hooks/useResetTOTPAction'; + +type AdminUserInfoActionsProps = { + username: IUser['username']; + userId: IUser['_id']; + isActive: boolean; + isAdmin: boolean; + onChange: () => void; + onReload: () => void; +}; + +const AdminUserInfoActions = ({ username, userId, isActive, isAdmin, onChange, onReload }: AdminUserInfoActionsProps): ReactElement => { + const t = useTranslation(); + const directRoute = useRoute('direct'); + const userRoute = useRoute('admin-users'); + const canDirectMessage = usePermission('create-d'); + const canEditOtherUserInfo = usePermission('edit-other-user-info'); + + const changeAdminStatusAction = useChangeAdminStatusAction(userId, isAdmin, username, onChange); + const changeUserStatusAction = useChangeUserStatusAction(userId, isActive, onChange); + const deleteUserAction = useDeleteUserAction(userId, onChange, onReload); + const resetTOTPAction = useResetTOTPAction(userId); + const resetE2EKeyAction = useResetE2EKeyAction(userId); + + const directMessageClick = useCallback( + () => + directRoute.push({ + rid: username, + }), + [directRoute, username], + ); + + const editUserClick = useCallback( + () => + userRoute.push({ + context: 'edit', + id: userId, + }), + [userId, userRoute], + ); + + const options = useMemo( + () => ({ + ...(canDirectMessage && { + directMessage: { + icon: 'balloon', + label: t('Direct_Message'), + action: directMessageClick, + }, + }), + ...(canEditOtherUserInfo && { + editUser: { + icon: 'edit', + label: t('Edit'), + action: editUserClick, + }, + }), + makeAdmin: changeAdminStatusAction, + resetE2EKey: resetE2EKeyAction, + resetTOTP: resetTOTPAction, + delete: deleteUserAction, + changeActiveStatus: changeUserStatusAction, + }), + [ + t, + canDirectMessage, + directMessageClick, + canEditOtherUserInfo, + editUserClick, + changeAdminStatusAction, + changeUserStatusAction, + deleteUserAction, + resetE2EKeyAction, + resetTOTPAction, + ], + ); + + const { actions: actionsDefinition, menu: menuOptions } = useActionSpread(options); + + const menu = useMemo(() => { + if (!menuOptions) { + return null; + } + + return ( +