feat: Added invitation badge to sidebar (#37635)

This commit is contained in:
Aleksander Nicacio da Silva 2025-12-19 13:42:43 -03:00 committed by GitHub
parent 1c06f6098e
commit f056c451c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 258 additions and 8 deletions

View File

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/i18n": patch
---
Adds invitation badge to sidebar

View File

@ -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(<Story />);
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

View File

@ -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<typeof InvitationBadge>;
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'),
},
};

View File

@ -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<ComponentProps<typeof Icon>, 'name' | 'color' | 'role'> & {
invitationDate: string | Date;
};
const InvitationBadge = ({ invitationDate, ...props }: InvitationBadgeProps) => {
const { t } = useTranslation();
const timeAgo = useTimeAgo();
return (
<Icon
size='x20'
{...props}
role='status'
color='info'
name='mail'
aria-hidden='false'
title={t('Invited__date__', { date: timeAgo(invitationDate) })}
/>
);
};
export default InvitationBadge;

View File

@ -0,0 +1,31 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`renders WithDateObject without crashing 1`] = `
<body>
<div>
<i
aria-hidden="false"
class="rcx-box rcx-box--full rcx-icon--name-mail rcx-icon rcx-css-dpa92h"
role="status"
title="Invited January 1, 2025"
>
</i>
</div>
</body>
`;
exports[`renders WithISOStringDate without crashing 1`] = `
<body>
<div>
<i
aria-hidden="false"
class="rcx-box rcx-box--full rcx-icon--name-mail rcx-icon rcx-css-dpa92h"
role="status"
title="Invited January 1, 2025"
>
</i>
</div>
</body>
`;

View File

@ -0,0 +1 @@
export { default } from './InvitationBadge';

View File

@ -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(
<SidebarItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
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(<SidebarItemBadges room={createFakeSubscription()} />, { wrapper: appRoot });
expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
});
});

View File

@ -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) => {
<Margins inlineStart={8}>
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
{isInviteSubscription(room) && <InvitationBadge mbs={2} invitationDate={room.ts} />}
</Margins>
);
};

View File

@ -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(
<SidebarItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
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(<SidebarItemBadges room={createFakeSubscription()} />, {
wrapper: appRoot,
});
expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
});
});

View File

@ -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 && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
{isInviteSubscription(room) && <InvitationBadge mbs={2} invitationDate={room.ts} />}
</>
);
};

View File

@ -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(
<SidebarItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
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(<SidebarItemBadges room={createFakeSubscription()} />, { wrapper: appRoot });
expect(screen.queryByRole('status', { name: /Invited/ })).not.toBeInTheDocument();
});
});

View File

@ -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 && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}</>;
return (
<>
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isInviteSubscription(room) && <InvitationBadge mbs={2} invitationDate={room.ts} />}
</>
);
};
export default SidebarItemBadges;

View File

@ -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 (
<SidebarV2ItemBadge
variant={variant}
title={title}
role='status'
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle: title, roomTitle })}
>
<span aria-hidden>{total}</span>
</SidebarV2ItemBadge>
);
};
export default UnreadBadge;

View File

@ -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(
<RoomSidePanelItemBadges
room={createFakeSubscription({
status: 'INVITED',
inviter: { name: 'Rocket Cat', username: 'rocket.cat', _id: 'rocket.cat' },
ts: new Date('2025-01-01T00:00:00.000Z'),
})}
/>,
{
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(<RoomSidePanelItemBadges room={createFakeSubscription()} />, { wrapper: appRoot });
expect(screen.queryByRole('status', { name: 'Invited' })).not.toBeInTheDocument();
});
});

View File

@ -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) && <SidePanelOmnichannelBadges room={room} />}
{showUnread && <UnreadBadge title={unreadTitle} roomTitle={roomTitle} variant={unreadVariant} total={unreadCount.total} />}
{isInviteSubscription(room) && <InvitationBadge invitationDate={room.ts} />}
</>
);
};

View File

@ -2756,6 +2756,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.",