From 1cc2bc75e9af43bd9f531cd53f8489f64dce268a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:48:34 -0300 Subject: [PATCH] fix: app actions showing in room toolbar without icon (#34839) --- .changeset/kind-ducks-thank.md | 5 + .../room/Header/RoomToolbox/RoomToolbox.tsx | 46 +---- .../hooks/useRoomToolboxActions.spec.ts | 172 ++++++++++++++++++ .../hooks/useRoomToolboxActions.ts | 49 +++++ .../room/HeaderV2/RoomToolbox/RoomToolbox.tsx | 46 +---- .../hooks/useRoomToolboxActions.spec.ts | 172 ++++++++++++++++++ .../hooks/useRoomToolboxActions.ts | 49 +++++ 7 files changed, 455 insertions(+), 84 deletions(-) create mode 100644 .changeset/kind-ducks-thank.md create mode 100644 apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.spec.ts create mode 100644 apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts create mode 100644 apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.spec.ts create mode 100644 apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.ts diff --git a/.changeset/kind-ducks-thank.md b/.changeset/kind-ducks-thank.md new file mode 100644 index 000000000000..67a94a0fe0a3 --- /dev/null +++ b/.changeset/kind-ducks-thank.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes apps actions showing in toolbar without an icon diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx index 08ccddf9ca8c..40c54dfe1499 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx @@ -1,12 +1,11 @@ import type { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericMenu } from '@rocket.chat/ui-client'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useRoomToolboxActions } from './hooks/useRoomToolboxActions'; import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; @@ -15,48 +14,13 @@ type RoomToolboxProps = { className?: ComponentProps['className']; }; -type MenuActionsProps = { - id: string; - items: GenericMenuItemProps[]; -}[]; - const RoomToolbox = ({ className }: RoomToolboxProps) => { const { t } = useTranslation(); - const { roomToolboxExpanded } = useLayout(); const toolbox = useRoomToolbox(); - const { actions, openTab } = toolbox; - - const featuredActions = actions.filter((action) => action.featured); - const normalActions = actions.filter((action) => !action.featured); - const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); - - const hiddenActions = (!roomToolboxExpanded ? actions : normalActions.slice(6)) - .filter((item) => !item.disabled && !item.featured) - .map((item) => ({ - 'key': item.id, - 'content': t(item.title), - 'onClick': - item.action ?? - ((): void => { - openTab(item.id); - }), - 'data-qa-id': `ToolBoxAction-${item.icon}`, - ...item, - })) - .reduce((acc, item) => { - const group = item.type ? item.type : ''; - const section = acc.find((section: { id: string }) => section.id === group); - if (section) { - section.items.push(item); - return acc; - } - - const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; - acc.push(newSection); + const { featuredActions, hiddenActions, visibleActions } = useRoomToolboxActions(toolbox); - return acc; - }, [] as MenuActionsProps); + const showKebabMenu = hiddenActions.length > 0; const renderDefaultToolboxItem: RoomToolboxActionConfig['renderToolboxItem'] = useEffectEvent( ({ id, className, index, icon, title, toolbox: { tab }, action, disabled, tooltip }) => { @@ -92,9 +56,7 @@ const RoomToolbox = ({ className }: RoomToolboxProps) => { {featuredActions.map(mapToToolboxItem)} {featuredActions.length > 0 && } {visibleActions.map(mapToToolboxItem)} - {(normalActions.length > 6 || !roomToolboxExpanded) && !!hiddenActions.length && ( - - )} + {showKebabMenu && } ); }; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.spec.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.spec.ts new file mode 100644 index 000000000000..555cf915aa18 --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.spec.ts @@ -0,0 +1,172 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react'; + +import { useRoomToolboxActions } from './useRoomToolboxActions'; +import type { RoomToolboxActionConfig } from '../../../contexts/RoomToolboxContext'; + +describe('useRoomToolboxActions', () => { + it('should return an empty array if there are no actions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: [], openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toEqual([]); + expect(result.current.hiddenActions).toEqual([]); + expect(result.current.visibleActions).toEqual([]); + }); + + it('should return apps actions only inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: appsActions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + const appsSection = result.current.hiddenActions[0]; + const appsItems = appsSection.items; + + expect(appsSection).toBeDefined(); + expect(appsSection).toHaveProperty('id', 'apps'); + expect(appsItems).toMatchObject(appsActions); + }); + + it('should return max of 6 items on visibleActions and the rest items inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.hiddenActions.length).toBeGreaterThan(0); + expect(result.current.visibleActions.length).toBe(6); + }); + + it('should return featured items inside featuredActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toMatchObject(actions.filter((action) => action.featured)); + }); +}); + +const appsActions: RoomToolboxActionConfig[] = [ + { + id: 'app1', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, + { + id: 'app2', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, +]; + +const actions: RoomToolboxActionConfig[] = [ + { + id: 'team-info', + groups: ['team'], + anonymous: true, + full: true, + title: 'Teams_Info', + icon: 'info-circled', + order: 1, + }, + { + id: 'thread', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + full: true, + title: 'Threads', + icon: 'thread', + order: 2, + }, + { + id: 'team-channels', + groups: ['team'], + anonymous: true, + full: true, + title: 'Team_Channels', + icon: 'hash', + order: 2, + }, + { + id: 'discussions', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Discussions', + icon: 'discussion', + full: true, + order: 3, + }, + { + id: 'start-call', + title: 'Call', + icon: 'phone', + groups: ['direct', 'direct_multiple', 'group', 'team', 'channel', 'direct'], + disabled: false, + full: true, + order: 4, + featured: true, + }, + { + id: 'rocket-search', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Search_Messages', + icon: 'magnifier', + order: 5, + }, + { + id: 'mentions', + groups: ['channel', 'group', 'team'], + title: 'Mentions', + icon: 'at', + order: 6, + type: 'organization', + }, + { + id: 'members-list', + groups: ['channel', 'group', 'team'], + title: 'Teams_members', + icon: 'members', + order: 7, + }, + { + id: 'uploaded-files-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Files', + icon: 'clip', + order: 8, + type: 'organization', + }, + { + id: 'pinned-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Pinned_Messages', + icon: 'pin', + order: 9, + type: 'organization', + }, + { + id: 'starred-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Starred_Messages', + icon: 'star', + order: 10, + type: 'organization', + }, + { + id: 'keyboard-shortcut-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Keyboard_Shortcuts_Title', + icon: 'keyboard', + order: 99, + type: 'customization', + }, + { + id: 'clean-history', + groups: ['channel', 'group', 'team', 'direct_multiple', 'direct'], + full: true, + title: 'Prune_Messages', + icon: 'eraser', + order: 250, + type: 'customization', + }, +]; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts new file mode 100644 index 000000000000..fb328fcf2f14 --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts @@ -0,0 +1,49 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import type { RoomToolboxContextValue } from '../../../contexts/RoomToolboxContext'; + +type MenuActionsProps = { + id: string; + items: GenericMenuItemProps[]; +}[]; + +export const useRoomToolboxActions = ({ actions, openTab }: Pick) => { + const { t } = useTranslation(); + const { roomToolboxExpanded } = useLayout(); + + const normalActions = actions.filter((action) => !action.featured && action.type !== 'apps'); + const featuredActions = actions.filter((action) => action.featured); + const appsActions = actions.filter((action) => action.type === 'apps'); + const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); + + const hiddenActions = (!roomToolboxExpanded ? actions : [...appsActions, ...normalActions.slice(6)]) + .filter((item) => !item.disabled && !item.featured) + .map((item) => ({ + 'key': item.id, + 'content': t(item.title), + 'onClick': + item.action ?? + ((): void => { + openTab(item.id); + }), + 'data-qa-id': `ToolBoxAction-${item.icon}`, + ...item, + })) + .reduce((acc, item) => { + const group = item.type ? item.type : ''; + const section = acc.find((section: { id: string }) => section.id === group); + if (section) { + section.items.push(item); + return acc; + } + + const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; + acc.push(newSection); + + return acc; + }, [] as MenuActionsProps); + + return { hiddenActions, featuredActions, visibleActions }; +}; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx index 08ccddf9ca8c..40c54dfe1499 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx @@ -1,12 +1,11 @@ import type { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericMenu } from '@rocket.chat/ui-client'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useRoomToolboxActions } from './hooks/useRoomToolboxActions'; import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; @@ -15,48 +14,13 @@ type RoomToolboxProps = { className?: ComponentProps['className']; }; -type MenuActionsProps = { - id: string; - items: GenericMenuItemProps[]; -}[]; - const RoomToolbox = ({ className }: RoomToolboxProps) => { const { t } = useTranslation(); - const { roomToolboxExpanded } = useLayout(); const toolbox = useRoomToolbox(); - const { actions, openTab } = toolbox; - - const featuredActions = actions.filter((action) => action.featured); - const normalActions = actions.filter((action) => !action.featured); - const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); - - const hiddenActions = (!roomToolboxExpanded ? actions : normalActions.slice(6)) - .filter((item) => !item.disabled && !item.featured) - .map((item) => ({ - 'key': item.id, - 'content': t(item.title), - 'onClick': - item.action ?? - ((): void => { - openTab(item.id); - }), - 'data-qa-id': `ToolBoxAction-${item.icon}`, - ...item, - })) - .reduce((acc, item) => { - const group = item.type ? item.type : ''; - const section = acc.find((section: { id: string }) => section.id === group); - if (section) { - section.items.push(item); - return acc; - } - - const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; - acc.push(newSection); + const { featuredActions, hiddenActions, visibleActions } = useRoomToolboxActions(toolbox); - return acc; - }, [] as MenuActionsProps); + const showKebabMenu = hiddenActions.length > 0; const renderDefaultToolboxItem: RoomToolboxActionConfig['renderToolboxItem'] = useEffectEvent( ({ id, className, index, icon, title, toolbox: { tab }, action, disabled, tooltip }) => { @@ -92,9 +56,7 @@ const RoomToolbox = ({ className }: RoomToolboxProps) => { {featuredActions.map(mapToToolboxItem)} {featuredActions.length > 0 && } {visibleActions.map(mapToToolboxItem)} - {(normalActions.length > 6 || !roomToolboxExpanded) && !!hiddenActions.length && ( - - )} + {showKebabMenu && } ); }; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.spec.ts b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.spec.ts new file mode 100644 index 000000000000..555cf915aa18 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.spec.ts @@ -0,0 +1,172 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react'; + +import { useRoomToolboxActions } from './useRoomToolboxActions'; +import type { RoomToolboxActionConfig } from '../../../contexts/RoomToolboxContext'; + +describe('useRoomToolboxActions', () => { + it('should return an empty array if there are no actions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: [], openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toEqual([]); + expect(result.current.hiddenActions).toEqual([]); + expect(result.current.visibleActions).toEqual([]); + }); + + it('should return apps actions only inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: appsActions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + const appsSection = result.current.hiddenActions[0]; + const appsItems = appsSection.items; + + expect(appsSection).toBeDefined(); + expect(appsSection).toHaveProperty('id', 'apps'); + expect(appsItems).toMatchObject(appsActions); + }); + + it('should return max of 6 items on visibleActions and the rest items inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.hiddenActions.length).toBeGreaterThan(0); + expect(result.current.visibleActions.length).toBe(6); + }); + + it('should return featured items inside featuredActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toMatchObject(actions.filter((action) => action.featured)); + }); +}); + +const appsActions: RoomToolboxActionConfig[] = [ + { + id: 'app1', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, + { + id: 'app2', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, +]; + +const actions: RoomToolboxActionConfig[] = [ + { + id: 'team-info', + groups: ['team'], + anonymous: true, + full: true, + title: 'Teams_Info', + icon: 'info-circled', + order: 1, + }, + { + id: 'thread', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + full: true, + title: 'Threads', + icon: 'thread', + order: 2, + }, + { + id: 'team-channels', + groups: ['team'], + anonymous: true, + full: true, + title: 'Team_Channels', + icon: 'hash', + order: 2, + }, + { + id: 'discussions', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Discussions', + icon: 'discussion', + full: true, + order: 3, + }, + { + id: 'start-call', + title: 'Call', + icon: 'phone', + groups: ['direct', 'direct_multiple', 'group', 'team', 'channel', 'direct'], + disabled: false, + full: true, + order: 4, + featured: true, + }, + { + id: 'rocket-search', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Search_Messages', + icon: 'magnifier', + order: 5, + }, + { + id: 'mentions', + groups: ['channel', 'group', 'team'], + title: 'Mentions', + icon: 'at', + order: 6, + type: 'organization', + }, + { + id: 'members-list', + groups: ['channel', 'group', 'team'], + title: 'Teams_members', + icon: 'members', + order: 7, + }, + { + id: 'uploaded-files-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Files', + icon: 'clip', + order: 8, + type: 'organization', + }, + { + id: 'pinned-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Pinned_Messages', + icon: 'pin', + order: 9, + type: 'organization', + }, + { + id: 'starred-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Starred_Messages', + icon: 'star', + order: 10, + type: 'organization', + }, + { + id: 'keyboard-shortcut-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Keyboard_Shortcuts_Title', + icon: 'keyboard', + order: 99, + type: 'customization', + }, + { + id: 'clean-history', + groups: ['channel', 'group', 'team', 'direct_multiple', 'direct'], + full: true, + title: 'Prune_Messages', + icon: 'eraser', + order: 250, + type: 'customization', + }, +]; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.ts b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.ts new file mode 100644 index 000000000000..fb328fcf2f14 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.ts @@ -0,0 +1,49 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import type { RoomToolboxContextValue } from '../../../contexts/RoomToolboxContext'; + +type MenuActionsProps = { + id: string; + items: GenericMenuItemProps[]; +}[]; + +export const useRoomToolboxActions = ({ actions, openTab }: Pick) => { + const { t } = useTranslation(); + const { roomToolboxExpanded } = useLayout(); + + const normalActions = actions.filter((action) => !action.featured && action.type !== 'apps'); + const featuredActions = actions.filter((action) => action.featured); + const appsActions = actions.filter((action) => action.type === 'apps'); + const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); + + const hiddenActions = (!roomToolboxExpanded ? actions : [...appsActions, ...normalActions.slice(6)]) + .filter((item) => !item.disabled && !item.featured) + .map((item) => ({ + 'key': item.id, + 'content': t(item.title), + 'onClick': + item.action ?? + ((): void => { + openTab(item.id); + }), + 'data-qa-id': `ToolBoxAction-${item.icon}`, + ...item, + })) + .reduce((acc, item) => { + const group = item.type ? item.type : ''; + const section = acc.find((section: { id: string }) => section.id === group); + if (section) { + section.items.push(item); + return acc; + } + + const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; + acc.push(newSection); + + return acc; + }, [] as MenuActionsProps); + + return { hiddenActions, featuredActions, visibleActions }; +};