diff --git a/.changeset/twelve-forks-destroy.md b/.changeset/twelve-forks-destroy.md new file mode 100644 index 0000000000000..b4cc7321bed9f --- /dev/null +++ b/.changeset/twelve-forks-destroy.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Adds invitation badge to sidebar diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx new file mode 100644 index 0000000000000..b47b32f73497f --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.spec.tsx @@ -0,0 +1,18 @@ +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './InvitationBadge.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(); + + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx new file mode 100644 index 0000000000000..658f206075718 --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.stories.tsx @@ -0,0 +1,32 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { Meta } from '@storybook/react'; + +import InvitationBadge from './InvitationBadge'; + +const meta = { + component: InvitationBadge, + parameters: { + layout: 'centered', + }, + decorators: [ + mockAppRoot() + .withTranslations('en', 'core', { + Invited__date__: 'Invited {{date}}', + }) + .buildStoryDecorator(), + ], +} satisfies Meta; + +export default meta; + +export const WithISOStringDate = { + args: { + invitationDate: '2025-01-01T12:00:00Z', + }, +}; + +export const WithDateObject = { + args: { + invitationDate: new Date('2025-01-01T12:00:00Z'), + }, +}; diff --git a/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx b/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx new file mode 100644 index 0000000000000..5c47878208703 --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/InvitationBadge.tsx @@ -0,0 +1,28 @@ +import { Icon } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useTimeAgo } from '../../hooks/useTimeAgo'; + +type InvitationBadgeProps = Omit, 'name' | 'color' | 'role'> & { + invitationDate: string | Date; +}; + +const InvitationBadge = ({ invitationDate, ...props }: InvitationBadgeProps) => { + const { t } = useTranslation(); + const timeAgo = useTimeAgo(); + + return ( + + ); +}; + +export default InvitationBadge; diff --git a/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap b/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap new file mode 100644 index 0000000000000..260d7e11131a4 --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/__snapshots__/InvitationBadge.spec.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders WithDateObject without crashing 1`] = ` + +
+ +  + +
+ +`; + +exports[`renders WithISOStringDate without crashing 1`] = ` + +
+ +  + +
+ +`; diff --git a/apps/meteor/client/components/InvitationBadge/index.ts b/apps/meteor/client/components/InvitationBadge/index.ts new file mode 100644 index 0000000000000..78b459e5d05da --- /dev/null +++ b/apps/meteor/client/components/InvitationBadge/index.ts @@ -0,0 +1 @@ +export { default } from './InvitationBadge'; diff --git a/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx b/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx index eec3c2820b6b6..285c5ecdbc72d 100644 --- a/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx +++ b/apps/meteor/client/sidebar/badges/SidebarItemBadges.spec.tsx @@ -12,7 +12,7 @@ jest.mock('../../views/omnichannel/components/OmnichannelBadges', () => ({ describe('SidebarItemBadges', () => { const appRoot = mockAppRoot() .withTranslations('en', 'core', { - Message_request: 'Message request', + Invited__date__: 'Invited {{date}}', mentions_counter_one: '{{count}} mention', mentions_counter_other: '{{count}} mentions', __unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}', @@ -50,4 +50,27 @@ describe('SidebarItemBadges', () => { expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument(); }); + + it('should render InvitationBadge when subscription has status INVITED', () => { + render( + , + { + wrapper: appRoot, + }, + ); + + expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument(); + }); + + it('should not render InvitationBadge when subscription does not have status INVITED', () => { + render(, { wrapper: appRoot }); + + expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument(); + }); }); diff --git a/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx b/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx index cec0b35344391..de8264eb4a422 100644 --- a/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx +++ b/apps/meteor/client/sidebar/badges/SidebarItemBadges.tsx @@ -1,8 +1,9 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom, isInviteSubscription } from '@rocket.chat/core-typings'; import { Margins } from '@rocket.chat/fuselage'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import UnreadBadge from './UnreadBadge'; +import InvitationBadge from '../../components/InvitationBadge'; import OmnichannelBadges from '../../views/omnichannel/components/OmnichannelBadges'; import { useUnreadDisplay } from '../hooks/useUnreadDisplay'; @@ -18,6 +19,7 @@ const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => { {showUnread && } {isOmnichannelRoom(room) && } + {isInviteSubscription(room) && } ); }; diff --git a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx index eec3c2820b6b6..9866febc2dcaa 100644 --- a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx +++ b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.spec.tsx @@ -12,7 +12,7 @@ jest.mock('../../views/omnichannel/components/OmnichannelBadges', () => ({ describe('SidebarItemBadges', () => { const appRoot = mockAppRoot() .withTranslations('en', 'core', { - Message_request: 'Message request', + Invited__date__: 'Invited {{date}}', mentions_counter_one: '{{count}} mention', mentions_counter_other: '{{count}} mentions', __unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}', @@ -50,4 +50,29 @@ describe('SidebarItemBadges', () => { expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument(); }); + + it('should render InvitationBadge when subscription has status INVITED', () => { + render( + , + { + wrapper: appRoot, + }, + ); + + expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument(); + }); + + it('should not render InvitationBadge when subscription does not have status INVITED', () => { + render(, { + wrapper: appRoot, + }); + + expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument(); + }); }); diff --git a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx index 762c4bb175f8b..aedfd3610ea9c 100644 --- a/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx +++ b/apps/meteor/client/sidebarv2/badges/SidebarItemBadges.tsx @@ -1,7 +1,8 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { isInviteSubscription, isOmnichannelRoom } from '@rocket.chat/core-typings'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import UnreadBadge from './UnreadBadge'; +import InvitationBadge from '../../components/InvitationBadge'; import OmnichannelBadges from '../../views/omnichannel/components/OmnichannelBadges'; import { useUnreadDisplay } from '../hooks/useUnreadDisplay'; @@ -17,6 +18,7 @@ const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => { <> {showUnread && } {isOmnichannelRoom(room) && } + {isInviteSubscription(room) && } ); }; diff --git a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx index 599b1ae643879..9a2fbf9fc711f 100644 --- a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx +++ b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.spec.tsx @@ -7,7 +7,7 @@ import { createFakeSubscription } from '../../../../../tests/mocks/data'; describe('SidebarItemBadges', () => { const appRoot = mockAppRoot() .withTranslations('en', 'core', { - Message_request: 'Message request', + Invited__date__: 'Invited {{date}}', mentions_counter_one: '{{count}} mention', mentions_counter_other: '{{count}} mentions', __unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}', @@ -33,4 +33,27 @@ describe('SidebarItemBadges', () => { expect(screen.queryByRole('status', { name: 'Test Room' })).not.toBeInTheDocument(); }); + + it('should render InvitationBadge when subscription has status INVITED and has inviter', () => { + render( + , + { + wrapper: appRoot, + }, + ); + + expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument(); + }); + + it('should not render InvitationBadge when subscription does not have status INVITED', () => { + render(, { wrapper: appRoot }); + + expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument(); + }); }); diff --git a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx index 6cc2203a5234c..9b4e6d4539d1d 100644 --- a/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx +++ b/apps/meteor/client/views/navigation/sidebar/badges/SidebarItemBadges.tsx @@ -1,5 +1,7 @@ +import { isInviteSubscription } from '@rocket.chat/core-typings'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import InvitationBadge from '../../../../components/InvitationBadge'; import UnreadBadge from '../../../../sidebarv2/badges/UnreadBadge'; import { useUnreadDisplay } from '../hooks/useUnreadDisplay'; @@ -11,7 +13,12 @@ type SidebarItemBadgesProps = { const SidebarItemBadges = ({ room, roomTitle }: SidebarItemBadgesProps) => { const { unreadCount, unreadTitle, unreadVariant, showUnread } = useUnreadDisplay(room); - return <>{showUnread && }; + return ( + <> + {showUnread && } + {isInviteSubscription(room) && } + + ); }; export default SidebarItemBadges; diff --git a/apps/meteor/client/views/navigation/sidebar/badges/UnreadBadge.tsx b/apps/meteor/client/views/navigation/sidebar/badges/UnreadBadge.tsx new file mode 100644 index 0000000000000..d3abe4f8aaca6 --- /dev/null +++ b/apps/meteor/client/views/navigation/sidebar/badges/UnreadBadge.tsx @@ -0,0 +1,26 @@ +import { SidebarV2ItemBadge } from '@rocket.chat/fuselage'; +import { useTranslation } from 'react-i18next'; + +type UnreadBadgeProps = { + title: string; + roomTitle?: string; + variant: 'primary' | 'warning' | 'danger' | 'secondary'; + total: number; +}; + +const UnreadBadge = ({ title, variant, total, roomTitle }: UnreadBadgeProps) => { + const { t } = useTranslation(); + + return ( + + {total} + + ); +}; + +export default UnreadBadge; diff --git a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx index 49dda4aa6b842..991dd9fa97ab6 100644 --- a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx +++ b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx @@ -12,7 +12,7 @@ jest.mock('../omnichannel/SidePanelOmnichannelBadges', () => ({ describe('RoomSidePanelItemBadges', () => { const appRoot = mockAppRoot() .withTranslations('en', 'core', { - Message_request: 'Message request', + Invited__date__: 'Invited {{date}}', mentions_counter_one: '{{count}} mention', mentions_counter_other: '{{count}} mentions', __unreadTitle__from__roomTitle__: '{{unreadTitle}} from {{roomTitle}}', @@ -52,4 +52,27 @@ describe('RoomSidePanelItemBadges', () => { expect(screen.queryByRole('status', { name: 'OmnichannelBadges' })).not.toBeInTheDocument(); }); + + it('should render InvitationBadge when subscription has status INVITED', () => { + render( + , + { + wrapper: appRoot, + }, + ); + + expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument(); + }); + + it('should not render InvitationBadge when subscription does not have status INVITED', () => { + render(, { wrapper: appRoot }); + + expect(screen.queryByRole('status', { name: 'Invited' })).not.toBeInTheDocument(); + }); }); diff --git a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx index 7519a9338b99f..8a9a759439af6 100644 --- a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx +++ b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.tsx @@ -1,6 +1,7 @@ -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { isInviteSubscription, isOmnichannelRoom } from '@rocket.chat/core-typings'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import InvitationBadge from '../../../../components/InvitationBadge'; import UnreadBadge from '../../../../sidebarv2/badges/UnreadBadge'; import { useUnreadDisplay } from '../../../../sidebarv2/hooks/useUnreadDisplay'; import SidePanelOmnichannelBadges from '../omnichannel/SidePanelOmnichannelBadges'; @@ -17,6 +18,7 @@ const RoomSidePanelItemBadges = ({ room, roomTitle }: RoomSidePanelItemBadgesPro <> {isOmnichannelRoom(room) && } {showUnread && } + {isInviteSubscription(room) && } ); }; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index ce902b70ee484..f99625450eb86 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2679,6 +2679,7 @@ "Invitation_Subject": "Invitation Subject", "Invitation_Subject_Default": "You have been invited to [Site_Name]", "Invite": "Invite", + "Invited__date__": "Invited {{date}}", "Invite_Link": "Invite Link", "Invite_Users": "Invite Members", "Invite_and_add_members_to_this_workspace_to_start_communicating": "Invite and add members to this workspace to start communicating.",