From cd0d50016e7f6ce65fdf3c92fff6f680764003b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?=
<60678893+juliajforesti@users.noreply.github.com>
Date: Fri, 6 Sep 2024 19:00:04 +0100
Subject: [PATCH 1/7] feat: `Sidebar` new components (#32821)
---
.changeset/many-balloons-scream.md | 13 +
.../client/sidebarv2/Item/Condensed.tsx | 38 +-
.../sidebarv2/Item/Extended.stories.tsx | 9 +-
.../meteor/client/sidebarv2/Item/Extended.tsx | 70 ++--
apps/meteor/client/sidebarv2/Item/Medium.tsx | 41 +-
.../client/sidebarv2/RoomList/RoomList.tsx | 121 ++----
.../client/sidebarv2/RoomList/RoomListRow.tsx | 19 +-
.../sidebarv2/RoomList/RoomListRowWrapper.tsx | 3 +-
...ta.tsx => SidebarItemTemplateWithData.tsx} | 42 +-
.../RoomList/useSidebarListNavigation.ts | 4 +-
apps/meteor/client/sidebarv2/Sidebar.tsx | 26 +-
.../client/sidebarv2/header/SearchList.tsx | 2 +-
.../client/sidebarv2/header/SearchSection.tsx | 15 +-
.../sidebarv2/header/actions/CreateRoom.tsx | 4 +-
.../sidebarv2/header/actions/Search.tsx | 50 ---
.../client/sidebarv2/header/actions/Sort.tsx | 6 +-
.../sidebarv2/hooks/useAvatarTemplate.tsx | 4 +-
apps/meteor/client/sidebarv2/search/Row.tsx | 10 +-
.../client/sidebarv2/search/SearchList.tsx | 382 ------------------
.../client/sidebarv2/search/UserItem.tsx | 14 +-
.../sections/StatusDisabledSection.tsx | 10 +-
apps/meteor/package.json | 2 +-
ee/packages/ui-theming/package.json | 2 +-
packages/fuselage-ui-kit/package.json | 2 +-
packages/gazzodown/package.json | 2 +-
packages/ui-avatar/package.json | 2 +-
packages/ui-client/package.json | 2 +-
packages/ui-composer/package.json | 2 +-
packages/ui-video-conf/package.json | 2 +-
packages/uikit-playground/package.json | 2 +-
yarn.lock | 26 +-
31 files changed, 196 insertions(+), 731 deletions(-)
create mode 100644 .changeset/many-balloons-scream.md
rename apps/meteor/client/sidebarv2/RoomList/{SideBarItemTemplateWithData.tsx => SidebarItemTemplateWithData.tsx} (86%)
delete mode 100644 apps/meteor/client/sidebarv2/header/actions/Search.tsx
delete mode 100644 apps/meteor/client/sidebarv2/search/SearchList.tsx
diff --git a/.changeset/many-balloons-scream.md b/.changeset/many-balloons-scream.md
new file mode 100644
index 000000000000..f017cdb81137
--- /dev/null
+++ b/.changeset/many-balloons-scream.md
@@ -0,0 +1,13 @@
+---
+'@rocket.chat/uikit-playground': minor
+'@rocket.chat/fuselage-ui-kit': minor
+'@rocket.chat/ui-theming': minor
+'@rocket.chat/ui-video-conf': minor
+'@rocket.chat/ui-composer': minor
+'@rocket.chat/gazzodown': minor
+'@rocket.chat/ui-avatar': minor
+'@rocket.chat/ui-client': minor
+'@rocket.chat/meteor': minor
+---
+
+Replaced new `SidebarV2` components under feature preview
diff --git a/apps/meteor/client/sidebarv2/Item/Condensed.tsx b/apps/meteor/client/sidebarv2/Item/Condensed.tsx
index db76935d4c3f..6ca428fc7208 100644
--- a/apps/meteor/client/sidebarv2/Item/Condensed.tsx
+++ b/apps/meteor/client/sidebarv2/Item/Condensed.tsx
@@ -1,11 +1,11 @@
-import { IconButton, Sidebar } from '@rocket.chat/fuselage';
+import { IconButton, SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemMenu, SidebarV2ItemTitle } from '@rocket.chat/fuselage';
import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { ReactElement } from 'react';
import React, { memo, useState } from 'react';
type CondensedProps = {
- title: ReactElement | string;
+ title: string;
titleIcon?: ReactElement;
avatar: ReactElement | boolean;
icon?: IconName;
@@ -19,7 +19,7 @@ type CondensedProps = {
clickable?: boolean;
};
-const Condensed = ({ icon, title = '', avatar, actions, href, unread, menu, badges, ...props }: CondensedProps) => {
+const Condensed = ({ icon, title, avatar, actions, href, unread, menu, badges, selected }: CondensedProps) => {
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
const isReduceMotionEnabled = usePrefersReducedMotion();
@@ -32,28 +32,18 @@ const Condensed = ({ icon, title = '', avatar, actions, href, unread, menu, badg
};
return (
-
- {avatar && {avatar}}
-
-
- {icon}
-
- {title}
-
-
- {badges && {badges}}
- {menu && (
-
- {menuVisibility ? menu() : }
-
- )}
-
- {actions && (
-
- {actions}
-
+
+ {avatar && {avatar}}
+ {icon && icon}
+ {title}
+ {badges && badges}
+ {actions && actions}
+ {menu && (
+
+ {menuVisibility ? menu() : }
+
)}
-
+
);
};
diff --git a/apps/meteor/client/sidebarv2/Item/Extended.stories.tsx b/apps/meteor/client/sidebarv2/Item/Extended.stories.tsx
index a6392eae5d61..7029e9f24dc1 100644
--- a/apps/meteor/client/sidebarv2/Item/Extended.stories.tsx
+++ b/apps/meteor/client/sidebarv2/Item/Extended.stories.tsx
@@ -25,14 +25,7 @@ export default {
const Template: ComponentStory = (args) => (
-
- John Doe
-
- 15:38
-
- }
+ title='John Doe'
subtitle={
diff --git a/apps/meteor/client/sidebarv2/Item/Extended.tsx b/apps/meteor/client/sidebarv2/Item/Extended.tsx
index f288f5fd35c6..13112bc96cec 100644
--- a/apps/meteor/client/sidebarv2/Item/Extended.tsx
+++ b/apps/meteor/client/sidebarv2/Item/Extended.tsx
@@ -1,4 +1,14 @@
-import { Sidebar, IconButton } from '@rocket.chat/fuselage';
+import {
+ SidebarV2Item,
+ SidebarV2ItemAvatarWrapper,
+ SidebarV2ItemCol,
+ SidebarV2ItemRow,
+ SidebarV2ItemTitle,
+ SidebarV2ItemTimestamp,
+ SidebarV2ItemContent,
+ SidebarV2ItemMenu,
+ IconButton,
+} from '@rocket.chat/fuselage';
import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
import type { Keys as IconName } from '@rocket.chat/icons';
import React, { memo, useState } from 'react';
@@ -7,7 +17,7 @@ import { useShortTimeAgo } from '../../hooks/useTimeAgo';
type ExtendedProps = {
icon?: IconName;
- title?: React.ReactNode;
+ title: string;
avatar?: React.ReactNode | boolean;
actions?: React.ReactNode;
href?: string;
@@ -24,7 +34,7 @@ type ExtendedProps = {
const Extended = ({
icon,
- title = '',
+ title,
avatar,
actions,
href,
@@ -37,7 +47,6 @@ const Extended = ({
threadUnread: _threadUnread,
unread,
selected,
- ...props
}: ExtendedProps) => {
const formatDate = useShortTimeAgo();
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
@@ -47,42 +56,33 @@ const Extended = ({
const handleMenu = useEffectEvent((e) => {
setMenuVisibility(e.target.offsetWidth > 0 && Boolean(menu));
});
-
const handleMenuEvent = {
[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: handleMenu,
};
return (
-
- {avatar && {avatar}}
-
-
-
- {icon}
-
- {title}
-
- {time && {formatDate(time)}}
-
-
-
-
- {subtitle}
- {badges}
- {menu && (
-
- {menuVisibility ? menu() : }
-
- )}
-
-
-
- {actions && (
-
- {actions}
-
- )}
-
+
+ {avatar && {avatar}}
+
+
+
+ {icon && icon}
+ {title}
+ {time && {formatDate(time)}}
+
+
+
+ {subtitle}
+ {badges && badges}
+ {actions && actions}
+ {menu && (
+
+ {menuVisibility ? menu() : }
+
+ )}
+
+
+
);
};
diff --git a/apps/meteor/client/sidebarv2/Item/Medium.tsx b/apps/meteor/client/sidebarv2/Item/Medium.tsx
index ffc13047f66d..13d2305ad7d5 100644
--- a/apps/meteor/client/sidebarv2/Item/Medium.tsx
+++ b/apps/meteor/client/sidebarv2/Item/Medium.tsx
@@ -1,12 +1,13 @@
-import { Sidebar, IconButton } from '@rocket.chat/fuselage';
+import { IconButton, SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemMenu, SidebarV2ItemTitle } from '@rocket.chat/fuselage';
import { useEffectEvent, usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
+import type { Keys as IconName } from '@rocket.chat/icons';
import React, { memo, useState } from 'react';
type MediumProps = {
- title: React.ReactNode;
+ title: string;
titleIcon?: React.ReactNode;
avatar: React.ReactNode | boolean;
- icon?: string;
+ icon?: IconName;
actions?: React.ReactNode;
href?: string;
unread?: boolean;
@@ -16,7 +17,7 @@ type MediumProps = {
menuOptions?: any;
};
-const Medium = ({ icon, title = '', avatar, actions, href, badges, unread, menu, ...props }: MediumProps) => {
+const Medium = ({ icon, title, avatar, actions, href, badges, unread, menu, selected }: MediumProps) => {
const [menuVisibility, setMenuVisibility] = useState(!!window.DISABLE_ANIMATION);
const isReduceMotionEnabled = usePrefersReducedMotion();
@@ -29,28 +30,18 @@ const Medium = ({ icon, title = '', avatar, actions, href, badges, unread, menu,
};
return (
-
- {avatar && {avatar}}
-
-
- {icon}
-
- {title}
-
-
- {badges && {badges}}
- {menu && (
-
- {menuVisibility ? menu() : }
-
- )}
-
- {actions && (
-
- {actions}
-
+
+ {avatar}
+ {icon && icon}
+ {title}
+ {badges && badges}
+ {actions && actions}
+ {menu && (
+
+ {menuVisibility ? menu() : }
+
)}
-
+
);
};
diff --git a/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx b/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx
index 3f137d4709c7..5f6592210d65 100644
--- a/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx
+++ b/apps/meteor/client/sidebarv2/RoomList/RoomList.tsx
@@ -1,11 +1,11 @@
-import type { IRoom } from '@rocket.chat/core-typings';
-import { css } from '@rocket.chat/css-in-js';
-import { Box } from '@rocket.chat/fuselage';
+/* eslint-disable react/no-multi-comp */
+import type { ISubscription, IRoom } from '@rocket.chat/core-typings';
+import { Box, SidebarV2GroupTitle } from '@rocket.chat/fuselage';
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
+import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useUserPreference, useUserId, useTranslation } from '@rocket.chat/ui-contexts';
-import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
-import { Virtuoso } from 'react-virtuoso';
+import { GroupedVirtuoso } from 'react-virtuoso';
import { VirtuosoScrollbars } from '../../components/CustomScrollbars';
import { useOpenedRoom } from '../../lib/RoomManager';
@@ -18,7 +18,25 @@ import RoomListRow from './RoomListRow';
import RoomListRowWrapper from './RoomListRowWrapper';
import RoomListWrapper from './RoomListWrapper';
-const computeItemKey = (index: number, room: IRoom): IRoom['_id'] | number => room._id || index;
+const getRoomsByGroup = (rooms: (ISubscription & IRoom)[]) => {
+ const groupCounts = rooms
+ .reduce((acc, item, index) => {
+ if (typeof item === 'string') {
+ acc.push(index);
+ }
+ return acc;
+ }, [] as number[])
+ .map((item, index, arr) => (arr[index + 1] ? arr[index + 1] : rooms.length) - item - 1);
+
+ const groupList = rooms.filter((item) => typeof item === 'string') as unknown as TranslationKey[];
+ const roomList = rooms.filter((item) => typeof item !== 'string');
+
+ return {
+ groupCounts,
+ groupList,
+ roomList,
+ };
+};
const RoomList = () => {
const t = useTranslation();
@@ -26,7 +44,7 @@ const RoomList = () => {
const roomsList = useRoomList();
const avatarTemplate = useAvatarTemplate();
const sideBarItemTemplate = useTemplateByViewMode();
- const { ref } = useResizeObserver({ debounceDelay: 100 });
+ const { ref } = useResizeObserver({ debounceDelay: 100 });
const openedRoom = useOpenedRoom() ?? '';
const sidebarViewMode = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode') || 'extended';
@@ -35,7 +53,7 @@ const RoomList = () => {
() => ({
extended,
t,
- SideBarItemTemplate: sideBarItemTemplate,
+ SidebarItemTemplate: sideBarItemTemplate,
AvatarTemplate: avatarTemplate,
openedRoom,
sidebarViewMode,
@@ -47,87 +65,16 @@ const RoomList = () => {
usePreventDefault(ref);
useShortcutOpenMenu(ref);
- const roomsListStyle = css`
- position: relative;
-
- display: flex;
-
- overflow-x: hidden;
- overflow-y: hidden;
-
- flex: 1 1 auto;
-
- height: 100%;
-
- &--embedded {
- margin-top: 2rem;
- }
-
- &__list:not(:last-child) {
- margin-bottom: 22px;
- }
-
- &__type {
- display: flex;
-
- flex-direction: row;
-
- padding: 0 var(--sidebar-default-padding) 1rem var(--sidebar-default-padding);
-
- color: var(--rooms-list-title-color);
-
- font-size: var(--rooms-list-title-text-size);
- align-items: center;
- justify-content: space-between;
-
- &-text--livechat {
- flex: 1;
- }
- }
-
- &__empty-room {
- padding: 0 var(--sidebar-default-padding);
-
- color: var(--rooms-list-empty-text-color);
-
- font-size: var(--rooms-list-empty-text-size);
- }
-
- &__toolbar-search {
- position: absolute;
- z-index: 10;
- left: 0;
-
- overflow-y: scroll;
-
- height: 100%;
-
- background-color: var(--sidebar-background);
-
- padding-block-start: 12px;
- }
-
- @media (max-width: 400px) {
- padding: 0 calc(var(--sidebar-small-default-padding) - 4px);
-
- &__type,
- &__empty-room {
- padding: 0 calc(var(--sidebar-small-default-padding) - 4px) 0.5rem calc(var(--sidebar-small-default-padding) - 4px);
- }
- }
- `;
+ const { groupCounts, groupList, roomList } = getRoomsByGroup(roomsList);
return (
-
-
- }
- />
-
+
+ }
+ itemContent={(index) => }
+ components={{ Item: RoomListRowWrapper, List: RoomListWrapper, Scroller: VirtuosoScrollbars }}
+ />
);
};
diff --git a/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx b/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx
index 64796d2e12e4..b520033056f3 100644
--- a/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx
+++ b/apps/meteor/client/sidebarv2/RoomList/RoomListRow.tsx
@@ -1,18 +1,17 @@
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
-import { SidebarSection } from '@rocket.chat/fuselage';
import type { useTranslation } from '@rocket.chat/ui-contexts';
import React, { memo, useMemo } from 'react';
import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '../../contexts/VideoConfContext';
import type { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import type { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';
-import SideBarItemTemplateWithData from './SideBarItemTemplateWithData';
+import SidebarItemTemplateWithData from './SidebarItemTemplateWithData';
type RoomListRowProps = {
data: {
extended: boolean;
t: ReturnType;
- SideBarItemTemplate: ReturnType;
+ SidebarItemTemplate: ReturnType;
AvatarTemplate: ReturnType;
openedRoom: string;
sidebarViewMode: 'extended' | 'condensed' | 'medium';
@@ -22,7 +21,7 @@ type RoomListRowProps = {
};
const RoomListRow = ({ data, item }: RoomListRowProps) => {
- const { extended, t, SideBarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data;
+ const { extended, t, SidebarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data;
const acceptCall = useVideoConfAcceptCall();
const rejectCall = useVideoConfRejectIncomingCall();
@@ -38,22 +37,14 @@ const RoomListRow = ({ data, item }: RoomListRowProps) => {
[acceptCall, rejectCall, currentCall],
);
- if (typeof item === 'string') {
- return (
-
- {t(item)}
-
- );
- }
-
return (
-
diff --git a/apps/meteor/client/sidebarv2/RoomList/RoomListRowWrapper.tsx b/apps/meteor/client/sidebarv2/RoomList/RoomListRowWrapper.tsx
index b2cd75193466..a848c74ad1d5 100644
--- a/apps/meteor/client/sidebarv2/RoomList/RoomListRowWrapper.tsx
+++ b/apps/meteor/client/sidebarv2/RoomList/RoomListRowWrapper.tsx
@@ -1,10 +1,11 @@
+import { SidebarV2ListItem } from '@rocket.chat/fuselage';
import type { ForwardedRef, HTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
type RoomListRoomWrapperProps = HTMLAttributes;
const RoomListRoomWrapper = forwardRef(function RoomListRoomWrapper(props: RoomListRoomWrapperProps, ref: ForwardedRef) {
- return ;
+ return ;
});
export default RoomListRoomWrapper;
diff --git a/apps/meteor/client/sidebarv2/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx
similarity index 86%
rename from apps/meteor/client/sidebarv2/RoomList/SideBarItemTemplateWithData.tsx
rename to apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx
index 4eaba8cc37f0..51b8ce495af6 100644
--- a/apps/meteor/client/sidebarv2/RoomList/SideBarItemTemplateWithData.tsx
+++ b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx
@@ -1,6 +1,6 @@
import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings';
import { isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom, isVideoConfMessage } from '@rocket.chat/core-typings';
-import { Badge, Sidebar, SidebarItemAction, SidebarItemActions, Margins } from '@rocket.chat/fuselage';
+import { SidebarV2Action, SidebarV2Actions, SidebarV2ItemBadge, SidebarV2ItemIcon } from '@rocket.chat/fuselage';
import type { useTranslation } from '@rocket.chat/ui-contexts';
import { useLayout } from '@rocket.chat/ui-contexts';
import type { AllHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react';
@@ -15,7 +15,7 @@ import { OmnichannelBadges } from '../badges/OmnichannelBadges';
import type { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import { normalizeSidebarMessage } from './normalizeSidebarMessage';
-const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnType): string | undefined => {
+export const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnType): string | undefined => {
if (!lastMessage) {
return t('No_messages_yet');
}
@@ -34,7 +34,7 @@ const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnTyp
return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`;
};
-const getBadgeTitle = (
+export const getBadgeTitle = (
userMentions: number,
threadUnread: number,
groupMentions: number,
@@ -61,7 +61,7 @@ const getBadgeTitle = (
type RoomListRowProps = {
extended: boolean;
t: ReturnType;
- SideBarItemTemplate: ComponentType<
+ SidebarItemTemplate: ComponentType<
{
icon: ReactNode;
title: ReactNode;
@@ -98,13 +98,13 @@ type RoomListRowProps = {
};
};
-const SideBarItemTemplateWithData = ({
+const SidebarItemTemplateWithData = ({
room,
id,
selected,
style,
extended,
- SideBarItemTemplate,
+ SidebarItemTemplate,
AvatarTemplate,
t,
isAnonymous,
@@ -132,19 +132,19 @@ const SideBarItemTemplateWithData = ({
const highlighted = Boolean(!hideUnreadStatus && (alert || unread));
const icon = (
- // TODO: Remove icon='at'
-
-
-
+ }
+ />
);
const actions = useMemo(
() =>
videoConfActions && (
-
-
-
-
+
+
+
+
),
[videoConfActions],
);
@@ -165,18 +165,18 @@ const SideBarItemTemplateWithData = ({
const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t);
const badges = (
-
+ <>
{showBadge && isUnread && (
-
+
{unread + tunread?.length}
-
+
)}
{isOmnichannelRoom(room) && }
-
+ >
);
return (
- {
+export default memo(SidebarItemTemplateWithData, (prevProps, nextProps) => {
if (keys.some((key) => prevProps[key] !== nextProps[key])) {
return false;
}
diff --git a/apps/meteor/client/sidebarv2/RoomList/useSidebarListNavigation.ts b/apps/meteor/client/sidebarv2/RoomList/useSidebarListNavigation.ts
index f5c2d00d4b2c..343df536c3f4 100644
--- a/apps/meteor/client/sidebarv2/RoomList/useSidebarListNavigation.ts
+++ b/apps/meteor/client/sidebarv2/RoomList/useSidebarListNavigation.ts
@@ -1,8 +1,8 @@
import { useFocusManager } from '@react-aria/focus';
import { useCallback } from 'react';
-const isListItem = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-item');
-const isListItemMenu = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-item__menu');
+const isListItem = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-v2-item');
+const isListItemMenu = (node: EventTarget) => (node as HTMLElement).classList.contains('rcx-sidebar-v2-item__menu');
/**
* Custom hook to provide the sidebar navigation by keyboard.
diff --git a/apps/meteor/client/sidebarv2/Sidebar.tsx b/apps/meteor/client/sidebarv2/Sidebar.tsx
index 573d90dd0d23..7209f51507d9 100644
--- a/apps/meteor/client/sidebarv2/Sidebar.tsx
+++ b/apps/meteor/client/sidebarv2/Sidebar.tsx
@@ -1,5 +1,4 @@
-import { css } from '@rocket.chat/css-in-js';
-import { Box } from '@rocket.chat/fuselage';
+import { SidebarV2 } from '@rocket.chat/fuselage';
import { useSessionStorage } from '@rocket.chat/fuselage-hooks';
import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts';
import React, { memo } from 'react';
@@ -15,31 +14,18 @@ const Sidebar = () => {
const [bannerDismissed, setBannerDismissed] = useSessionStorage('presence_cap_notifier', false);
const presenceDisabled = useSetting('Presence_broadcast_disabled');
- const sidebarLink = css`
- a {
- text-decoration: none;
- }
- `;
-
return (
-
{presenceDisabled && !bannerDismissed && setBannerDismissed(true)} />}
-
+
);
};
diff --git a/apps/meteor/client/sidebarv2/header/SearchList.tsx b/apps/meteor/client/sidebarv2/header/SearchList.tsx
index 70c666fead50..b132af80f674 100644
--- a/apps/meteor/client/sidebarv2/header/SearchList.tsx
+++ b/apps/meteor/client/sidebarv2/header/SearchList.tsx
@@ -34,7 +34,7 @@ const SearchList = ({ filterText, onEscSearch }: SearchListProps) => {
() => ({
items,
t,
- SideBarItemTemplate: sideBarItemTemplate,
+ SidebarItemTemplate: sideBarItemTemplate,
avatarTemplate,
useRealName,
extended,
diff --git a/apps/meteor/client/sidebarv2/header/SearchSection.tsx b/apps/meteor/client/sidebarv2/header/SearchSection.tsx
index c1f3f8cf8c21..660b8ee19cd5 100644
--- a/apps/meteor/client/sidebarv2/header/SearchSection.tsx
+++ b/apps/meteor/client/sidebarv2/header/SearchSection.tsx
@@ -1,5 +1,5 @@
import { css } from '@rocket.chat/css-in-js';
-import { Box, Icon, TextInput, Palette, Sidebar } from '@rocket.chat/fuselage';
+import { Box, Icon, TextInput, Palette, SidebarV2Section } from '@rocket.chat/fuselage';
import { useMergedRefs, useOutsideClick } from '@rocket.chat/fuselage-hooks';
import { useTranslation, useUser } from '@rocket.chat/ui-contexts';
import React, { useCallback, useEffect, useRef } from 'react';
@@ -70,15 +70,7 @@ const SearchSection = () => {
return (
-
+
{
>
)}
-
-
+
{isDirty && }
);
diff --git a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx b/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx
index 478e7cce33e1..a80954c8b297 100644
--- a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx
+++ b/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx
@@ -1,4 +1,4 @@
-import { Sidebar } from '@rocket.chat/fuselage';
+import { SidebarV2Action } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { HTMLAttributes } from 'react';
import React from 'react';
@@ -13,7 +13,7 @@ const CreateRoom = (props: CreateRoomProps) => {
const sections = useCreateRoom();
- return ;
+ return ;
};
export default CreateRoom;
diff --git a/apps/meteor/client/sidebarv2/header/actions/Search.tsx b/apps/meteor/client/sidebarv2/header/actions/Search.tsx
deleted file mode 100644
index 06d42114d76b..000000000000
--- a/apps/meteor/client/sidebarv2/header/actions/Search.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Sidebar } from '@rocket.chat/fuselage';
-import { useEffectEvent, useOutsideClick } from '@rocket.chat/fuselage-hooks';
-import type { HTMLAttributes } from 'react';
-import React, { useState, useEffect, useRef } from 'react';
-import tinykeys from 'tinykeys';
-
-import SearchList from '../../search/SearchList';
-
-type SearchProps = Omit, 'is'>;
-
-const Search = (props: SearchProps) => {
- const [searchOpen, setSearchOpen] = useState(false);
-
- const ref = useRef(null);
- const handleCloseSearch = useEffectEvent(() => {
- setSearchOpen(false);
- });
-
- useOutsideClick([ref], handleCloseSearch);
-
- const openSearch = useEffectEvent(() => {
- setSearchOpen(true);
- });
-
- useEffect(() => {
- const unsubscribe = tinykeys(window, {
- '$mod+K': (event) => {
- event.preventDefault();
- openSearch();
- },
- '$mod+P': (event) => {
- event.preventDefault();
- openSearch();
- },
- });
-
- return (): void => {
- unsubscribe();
- };
- }, [openSearch]);
-
- return (
- <>
-
- {searchOpen && }
- >
- );
-};
-
-export default Search;
diff --git a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx b/apps/meteor/client/sidebarv2/header/actions/Sort.tsx
index e7f3b398e5f6..9956acf266b5 100644
--- a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx
+++ b/apps/meteor/client/sidebarv2/header/actions/Sort.tsx
@@ -1,4 +1,4 @@
-import { Sidebar } from '@rocket.chat/fuselage';
+import { SidebarV2Action } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { HTMLAttributes } from 'react';
import React from 'react';
@@ -13,9 +13,7 @@ const Sort = (props: SortProps) => {
const sections = useSortMenu();
- return (
-
- );
+ return ;
};
export default Sort;
diff --git a/apps/meteor/client/sidebarv2/hooks/useAvatarTemplate.tsx b/apps/meteor/client/sidebarv2/hooks/useAvatarTemplate.tsx
index 9fd1023a32e7..e5bf780ee5b0 100644
--- a/apps/meteor/client/sidebarv2/hooks/useAvatarTemplate.tsx
+++ b/apps/meteor/client/sidebarv2/hooks/useAvatarTemplate.tsx
@@ -18,7 +18,7 @@ export const useAvatarTemplate = (
return null;
}
- const size = ((): 'x36' | 'x28' | 'x16' => {
+ const size = ((): 'x36' | 'x28' | 'x20' => {
switch (viewMode) {
case 'extended':
return 'x36';
@@ -26,7 +26,7 @@ export const useAvatarTemplate = (
return 'x28';
case 'condensed':
default:
- return 'x16';
+ return 'x20';
}
})();
diff --git a/apps/meteor/client/sidebarv2/search/Row.tsx b/apps/meteor/client/sidebarv2/search/Row.tsx
index 68ceecd2ad88..f8541546ec4b 100644
--- a/apps/meteor/client/sidebarv2/search/Row.tsx
+++ b/apps/meteor/client/sidebarv2/search/Row.tsx
@@ -2,7 +2,7 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
import type { ReactElement } from 'react';
import React, { memo } from 'react';
-import SideBarItemTemplateWithData from '../RoomList/SideBarItemTemplateWithData';
+import SidebarItemTemplateWithData from '../RoomList/SidebarItemTemplateWithData';
import UserItem from './UserItem';
type RowProps = {
@@ -11,7 +11,7 @@ type RowProps = {
};
const Row = ({ item, data }: RowProps): ReactElement => {
- const { t, SideBarItemTemplate, avatarTemplate: AvatarTemplate, useRealName, extended } = data;
+ const { t, SidebarItemTemplate, avatarTemplate: AvatarTemplate, useRealName, extended } = data;
if (item.t === 'd' && !item.u) {
return (
@@ -20,18 +20,18 @@ const Row = ({ item, data }: RowProps): ReactElement => {
useRealName={useRealName}
t={t}
item={item}
- SideBarItemTemplate={SideBarItemTemplate}
+ SidebarItemTemplate={SidebarItemTemplate}
AvatarTemplate={AvatarTemplate}
/>
);
}
return (
-
);
diff --git a/apps/meteor/client/sidebarv2/search/SearchList.tsx b/apps/meteor/client/sidebarv2/search/SearchList.tsx
deleted file mode 100644
index c43fe854ac30..000000000000
--- a/apps/meteor/client/sidebarv2/search/SearchList.tsx
+++ /dev/null
@@ -1,382 +0,0 @@
-import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
-import { css } from '@rocket.chat/css-in-js';
-import { Sidebar, TextInput, Box, Icon } from '@rocket.chat/fuselage';
-import { useMutableCallback, useDebouncedValue, useAutoFocus, useUniqueId, useMergedRefs } from '@rocket.chat/fuselage-hooks';
-import { escapeRegExp } from '@rocket.chat/string-helpers';
-import { useUserPreference, useUserSubscriptions, useSetting, useTranslation, useMethod } from '@rocket.chat/ui-contexts';
-import type { UseQueryResult } from '@tanstack/react-query';
-import { useQuery } from '@tanstack/react-query';
-import type {
- ReactElement,
- MutableRefObject,
- SetStateAction,
- Dispatch,
- FormEventHandler,
- Ref,
- MouseEventHandler,
- ForwardedRef,
-} from 'react';
-import React, { forwardRef, useState, useMemo, useEffect, useRef } from 'react';
-import type { VirtuosoHandle } from 'react-virtuoso';
-import { Virtuoso } from 'react-virtuoso';
-import tinykeys from 'tinykeys';
-
-import { VirtuosoScrollbars } from '../../components/CustomScrollbars';
-import { getConfig } from '../../lib/utils/getConfig';
-import { useAvatarTemplate } from '../hooks/useAvatarTemplate';
-import { usePreventDefault } from '../hooks/usePreventDefault';
-import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';
-import Row from './Row';
-
-const mobileCheck = function () {
- let check = false;
- (function (a: string) {
- if (
- /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
- a,
- ) ||
- /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
- a.substr(0, 4),
- )
- )
- check = true;
- })(navigator.userAgent || navigator.vendor || window.opera || '');
- return check;
-};
-
-declare global {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- interface Window {
- opera?: string;
- }
- // eslint-disable-next-line @typescript-eslint/naming-convention
- interface Navigator {
- userAgentData?: {
- mobile: boolean;
- };
- }
-}
-
-const shortcut = ((): string => {
- if (navigator.userAgentData?.mobile || mobileCheck()) {
- return '';
- }
- if (window.navigator.platform.toLowerCase().includes('mac')) {
- return '(\u2318+K)';
- }
- return '(Ctrl+K)';
-})();
-
-const LIMIT = parseInt(String(getConfig('Sidebar_Search_Spotlight_LIMIT', 20)));
-
-const options = {
- sort: {
- lm: -1,
- name: 1,
- },
- limit: LIMIT,
-} as const;
-
-const useSearchItems = (filterText: string): UseQueryResult<(ISubscription & IRoom)[] | undefined, Error> => {
- const [, mention, name] = useMemo(() => filterText.match(/(@|#)?(.*)/i) || [], [filterText]);
- const query = useMemo(() => {
- const filterRegex = new RegExp(escapeRegExp(name), 'i');
-
- return {
- $or: [{ name: filterRegex }, { fname: filterRegex }],
- ...(mention && {
- t: mention === '@' ? 'd' : { $ne: 'd' },
- }),
- };
- }, [name, mention]);
-
- const localRooms = useUserSubscriptions(query, options);
-
- const usernamesFromClient = [...localRooms?.map(({ t, name }) => (t === 'd' ? name : null))].filter(Boolean) as string[];
-
- const searchForChannels = mention === '#';
- const searchForDMs = mention === '@';
-
- const type = useMemo(() => {
- if (searchForChannels) {
- return { users: false, rooms: true, includeFederatedRooms: true };
- }
- if (searchForDMs) {
- return { users: true, rooms: false };
- }
- return { users: true, rooms: true, includeFederatedRooms: true };
- }, [searchForChannels, searchForDMs]);
-
- const getSpotlight = useMethod('spotlight');
-
- return useQuery(
- ['sidebar/search/spotlight', name, usernamesFromClient, type, localRooms.map(({ _id, name }) => _id + name)],
- async () => {
- if (localRooms.length === LIMIT) {
- return localRooms;
- }
-
- const spotlight = await getSpotlight(name, usernamesFromClient, type);
-
- const filterUsersUnique = ({ _id }: { _id: string }, index: number, arr: { _id: string }[]): boolean =>
- index === arr.findIndex((user) => _id === user._id);
-
- const roomFilter = (room: { t: string; uids?: string[]; _id: string; name?: string }): boolean =>
- !localRooms.find(
- (item) =>
- (room.t === 'd' && room.uids && room.uids.length > 1 && room.uids?.includes(item._id)) ||
- [item.rid, item._id].includes(room._id),
- );
- const usersFilter = (user: { _id: string }): boolean =>
- !localRooms.find((room) => room.t === 'd' && room.uids && room.uids?.length === 2 && room.uids.includes(user._id));
-
- const userMap = (user: {
- _id: string;
- name: string;
- username: string;
- avatarETag?: string;
- }): {
- _id: string;
- t: string;
- name: string;
- fname: string;
- avatarETag?: string;
- } => ({
- _id: user._id,
- t: 'd',
- name: user.username,
- fname: user.name,
- avatarETag: user.avatarETag,
- });
-
- type resultsFromServerType = {
- _id: string;
- t: string;
- name: string;
- teamMain?: boolean;
- fname?: string;
- avatarETag?: string | undefined;
- uids?: string[] | undefined;
- }[];
-
- const resultsFromServer: resultsFromServerType = [];
- resultsFromServer.push(...spotlight.users.filter(filterUsersUnique).filter(usersFilter).map(userMap));
- resultsFromServer.push(...spotlight.rooms.filter(roomFilter));
-
- const exact = resultsFromServer?.filter((item) => [item.name, item.fname].includes(name));
- return Array.from(new Set([...exact, ...localRooms, ...resultsFromServer]));
- },
- {
- staleTime: 60_000,
- keepPreviousData: true,
- placeholderData: localRooms,
- },
- );
-};
-
-const useInput = (initial: string): { value: string; onChange: FormEventHandler; setValue: Dispatch> } => {
- const [value, setValue] = useState(initial);
- const onChange = useMutableCallback((e) => {
- setValue(e.currentTarget.value);
- });
- return { value, onChange, setValue };
-};
-
-const toggleSelectionState = (next: HTMLElement, current: HTMLElement | undefined, input: HTMLElement | undefined): void => {
- input?.setAttribute('aria-activedescendant', next.id);
- next.setAttribute('aria-selected', 'true');
- next.classList.add('rcx-sidebar-item--selected');
- if (current) {
- current.removeAttribute('aria-selected');
- current.classList.remove('rcx-sidebar-item--selected');
- }
-};
-
-type SearchListProps = {
- onClose: () => void;
-};
-
-const SearchList = forwardRef(function SearchList({ onClose }: SearchListProps, ref: ForwardedRef) {
- const listId = useUniqueId();
- const t = useTranslation();
- const { setValue: setFilterValue, ...filter } = useInput('');
-
- const cursorRef = useRef(null);
- const autofocus: Ref = useMergedRefs(useAutoFocus(), cursorRef);
-
- const listRef = useRef(null);
- const boxRef = useRef(null);
-
- const selectedElement: MutableRefObject = useRef(null);
- const itemIndexRef = useRef(0);
-
- const sidebarViewMode = useUserPreference('sidebarViewMode');
- const useRealName = useSetting('UI_Use_Real_Name');
-
- const sideBarItemTemplate = useTemplateByViewMode();
- const avatarTemplate = useAvatarTemplate();
-
- const extended = sidebarViewMode === 'extended';
-
- const filterText = useDebouncedValue(filter.value, 100);
-
- const placeholder = [t('Search'), shortcut].filter(Boolean).join(' ');
-
- const { data: items = [], isLoading } = useSearchItems(filterText);
-
- const itemData = useMemo(
- () => ({
- items,
- t,
- SideBarItemTemplate: sideBarItemTemplate,
- avatarTemplate,
- useRealName,
- extended,
- sidebarViewMode,
- }),
- [avatarTemplate, extended, items, useRealName, sideBarItemTemplate, sidebarViewMode, t],
- );
-
- const changeSelection = useMutableCallback((dir) => {
- let nextSelectedElement = null;
-
- if (dir === 'up') {
- const potentialElement = selectedElement.current?.parentElement?.previousSibling as HTMLElement;
- if (potentialElement) {
- nextSelectedElement = potentialElement.querySelector('a');
- }
- } else {
- const potentialElement = selectedElement.current?.parentElement?.nextSibling as HTMLElement;
- if (potentialElement) {
- nextSelectedElement = potentialElement.querySelector('a');
- }
- }
-
- if (nextSelectedElement) {
- toggleSelectionState(nextSelectedElement, selectedElement.current || undefined, cursorRef?.current || undefined);
- return nextSelectedElement;
- }
- return selectedElement.current;
- });
-
- const resetCursor = useMutableCallback(() => {
- setTimeout(() => {
- itemIndexRef.current = 0;
- listRef.current?.scrollToIndex({ index: itemIndexRef.current });
- selectedElement.current = boxRef.current?.querySelector('a.rcx-sidebar-item');
- if (selectedElement.current) {
- toggleSelectionState(selectedElement.current, undefined, cursorRef?.current || undefined);
- }
- }, 0);
- });
-
- usePreventDefault(boxRef);
-
- useEffect(() => {
- resetCursor();
- });
-
- useEffect(() => {
- resetCursor();
- }, [filterText, resetCursor]);
-
- useEffect(() => {
- if (!cursorRef?.current) {
- return;
- }
- return tinykeys(cursorRef?.current, {
- Escape: (event) => {
- event.preventDefault();
- setFilterValue((value) => {
- if (!value) {
- onClose();
- }
- resetCursor();
- return '';
- });
- },
- Tab: onClose,
- ArrowUp: () => {
- const currentElement = changeSelection('up');
- itemIndexRef.current = Math.max(itemIndexRef.current - 1, 0);
- listRef.current?.scrollToIndex({ index: itemIndexRef.current });
- selectedElement.current = currentElement;
- },
- ArrowDown: () => {
- const currentElement = changeSelection('down');
- itemIndexRef.current = Math.min(itemIndexRef.current + 1, items.length + 1);
- listRef.current?.scrollToIndex({ index: itemIndexRef.current });
- selectedElement.current = currentElement;
- },
- Enter: (event) => {
- event.preventDefault();
- if (selectedElement.current && items.length > 0) {
- selectedElement.current.click();
- } else {
- onClose();
- }
- },
- });
- }, [cursorRef, changeSelection, items.length, onClose, resetCursor, setFilterValue]);
-
- const handleClick: MouseEventHandler = (e): void => {
- if (e.target instanceof Element && [e.target.tagName, e.target.parentElement?.tagName].includes('BUTTON')) {
- return;
- }
- return onClose();
- };
-
- return (
-
-
- }
- />
-
-
- room._id}
- itemContent={(_, data): ReactElement =>
}
- ref={listRef}
- />
-
-
- );
-});
-
-export default SearchList;
diff --git a/apps/meteor/client/sidebarv2/search/UserItem.tsx b/apps/meteor/client/sidebarv2/search/UserItem.tsx
index 8b9667913311..9cfa97fd797c 100644
--- a/apps/meteor/client/sidebarv2/search/UserItem.tsx
+++ b/apps/meteor/client/sidebarv2/search/UserItem.tsx
@@ -1,5 +1,5 @@
import type { IUser } from '@rocket.chat/core-typings';
-import { Sidebar } from '@rocket.chat/fuselage';
+import { SidebarV2ItemIcon } from '@rocket.chat/fuselage';
import React, { memo } from 'react';
import { ReactiveUserStatus } from '../../components/UserStatus';
@@ -13,24 +13,20 @@ type UserItemProps = {
t: string;
};
t: (value: string) => string;
- SideBarItemTemplate: any;
+ SidebarItemTemplate: any;
AvatarTemplate: any;
id: string;
style?: CSSStyleRule;
useRealName?: boolean;
};
-const UserItem = ({ item, id, style, t, SideBarItemTemplate, AvatarTemplate, useRealName }: UserItemProps) => {
+const UserItem = ({ item, id, style, t, SidebarItemTemplate, AvatarTemplate, useRealName }: UserItemProps) => {
const title = useRealName ? item.fname || item.name : item.name || item.fname;
- const icon = (
-
-
-
- );
+ const icon = } />;
const href = roomCoordinator.getRouteLink(item.t, { name: item.name });
return (
- {
const handleStatusDisabledModal = useStatusDisabledModal();
return (
-
);
};
diff --git a/apps/meteor/package.json b/apps/meteor/package.json
index e1f2eff94e07..43b62def0819 100644
--- a/apps/meteor/package.json
+++ b/apps/meteor/package.json
@@ -241,7 +241,7 @@
"@rocket.chat/favicon": "workspace:^",
"@rocket.chat/forked-matrix-appservice-bridge": "^4.0.2",
"@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.3",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/fuselage-polyfills": "~0.31.25",
"@rocket.chat/fuselage-toastbar": "^0.33.0",
diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json
index 04b22e72aae3..5f3bb4977d7d 100644
--- a/ee/packages/ui-theming/package.json
+++ b/ee/packages/ui-theming/package.json
@@ -4,7 +4,7 @@
"private": true,
"devDependencies": {
"@rocket.chat/css-in-js": "~0.31.25",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/icons": "~0.38.0",
"@rocket.chat/ui-contexts": "workspace:~",
diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json
index 478502ebf93d..7c644744c3cf 100644
--- a/packages/fuselage-ui-kit/package.json
+++ b/packages/fuselage-ui-kit/package.json
@@ -66,7 +66,7 @@
"@rocket.chat/apps-engine": "1.45.0-alpha.868",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/eslint-config": "workspace:^",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/fuselage-polyfills": "~0.31.25",
"@rocket.chat/icons": "~0.38.0",
diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json
index 029dc0bfc606..8b751c895b0f 100644
--- a/packages/gazzodown/package.json
+++ b/packages/gazzodown/package.json
@@ -6,7 +6,7 @@
"@babel/core": "~7.22.20",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/css-in-js": "~0.31.25",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/fuselage-tokens": "^0.33.1",
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/message-parser": "workspace:^",
diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json
index 84868f8ccb4e..66a50ce8c8e1 100644
--- a/packages/ui-avatar/package.json
+++ b/packages/ui-avatar/package.json
@@ -4,7 +4,7 @@
"private": true,
"devDependencies": {
"@babel/core": "~7.22.20",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/ui-contexts": "workspace:^",
"@types/babel__core": "~7.20.3",
"@types/react": "~17.0.69",
diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json
index 97fd45a4f1b5..d9cdb9f24ade 100644
--- a/packages/ui-client/package.json
+++ b/packages/ui-client/package.json
@@ -6,7 +6,7 @@
"@babel/core": "~7.22.20",
"@react-aria/toolbar": "^3.0.0-beta.1",
"@rocket.chat/css-in-js": "~0.31.25",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/icons": "~0.38.0",
"@rocket.chat/jest-presets": "workspace:~",
diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json
index f25571435fc5..55e22a79e34c 100644
--- a/packages/ui-composer/package.json
+++ b/packages/ui-composer/package.json
@@ -19,7 +19,7 @@
"@babel/core": "~7.22.20",
"@react-aria/toolbar": "^3.0.0-beta.1",
"@rocket.chat/eslint-config": "workspace:^",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/icons": "~0.38.0",
"@storybook/addon-actions": "~6.5.16",
"@storybook/addon-docs": "~6.5.16",
diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json
index bfbb16eb682c..40253050d9bf 100644
--- a/packages/ui-video-conf/package.json
+++ b/packages/ui-video-conf/package.json
@@ -6,7 +6,7 @@
"@babel/core": "~7.22.20",
"@rocket.chat/css-in-js": "~0.31.25",
"@rocket.chat/eslint-config": "workspace:^",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/icons": "~0.38.0",
"@rocket.chat/jest-presets": "workspace:~",
diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json
index 1e842208211b..d309c2d54599 100644
--- a/packages/uikit-playground/package.json
+++ b/packages/uikit-playground/package.json
@@ -15,7 +15,7 @@
"@codemirror/tooltip": "^0.19.16",
"@lezer/highlight": "^1.1.6",
"@rocket.chat/css-in-js": "~0.31.25",
- "@rocket.chat/fuselage": "^0.57.1",
+ "@rocket.chat/fuselage": "^0.59.0",
"@rocket.chat/fuselage-hooks": "^0.33.1",
"@rocket.chat/fuselage-polyfills": "~0.31.25",
"@rocket.chat/fuselage-toastbar": "^0.33.0",
diff --git a/yarn.lock b/yarn.lock
index 71d9ba0760ea..9197fbb0b63a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8883,7 +8883,7 @@ __metadata:
"@rocket.chat/apps-engine": 1.45.0-alpha.868
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/eslint-config": "workspace:^"
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/fuselage-hooks": ^0.33.1
"@rocket.chat/fuselage-polyfills": ~0.31.25
"@rocket.chat/gazzodown": "workspace:^"
@@ -8944,9 +8944,9 @@ __metadata:
languageName: unknown
linkType: soft
-"@rocket.chat/fuselage@npm:^0.57.1":
- version: 0.57.1
- resolution: "@rocket.chat/fuselage@npm:0.57.1"
+"@rocket.chat/fuselage@npm:^0.59.0":
+ version: 0.59.0
+ resolution: "@rocket.chat/fuselage@npm:0.59.0"
dependencies:
"@rocket.chat/css-in-js": ^0.31.25
"@rocket.chat/css-supports": ^0.31.25
@@ -8964,7 +8964,7 @@ __metadata:
react: ^17.0.2
react-dom: ^17.0.2
react-virtuoso: 1.2.4
- checksum: ed40c4e9ec6f6294e0e7c7a3912ae7c9eca026455506f3f1983483010d3d0c41169f9e38d173e5e63ed0e9824979edd607dda3c881202bf797a97b5b76e83a34
+ checksum: 259dce5381a3c3e0d7c7f3dc7ab51346cb65a9f4906a5ca5d6a976627d05e01e7f8a3a940604d0ad1b2b4ed89c250a871ef3fb253f6bbb69d35bc931e193898d
languageName: node
linkType: hard
@@ -8975,7 +8975,7 @@ __metadata:
"@babel/core": ~7.22.20
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/css-in-js": ~0.31.25
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/fuselage-tokens": ^0.33.1
"@rocket.chat/jest-presets": "workspace:~"
"@rocket.chat/message-parser": "workspace:^"
@@ -9342,7 +9342,7 @@ __metadata:
"@rocket.chat/favicon": "workspace:^"
"@rocket.chat/forked-matrix-appservice-bridge": ^4.0.2
"@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.3
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/fuselage-hooks": ^0.33.1
"@rocket.chat/fuselage-polyfills": ~0.31.25
"@rocket.chat/fuselage-toastbar": ^0.33.0
@@ -10214,7 +10214,7 @@ __metadata:
resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar"
dependencies:
"@babel/core": ~7.22.20
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/ui-contexts": "workspace:^"
"@types/babel__core": ~7.20.3
"@types/react": ~17.0.69
@@ -10240,7 +10240,7 @@ __metadata:
"@babel/core": ~7.22.20
"@react-aria/toolbar": ^3.0.0-beta.1
"@rocket.chat/css-in-js": ~0.31.25
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/fuselage-hooks": ^0.33.1
"@rocket.chat/icons": ~0.38.0
"@rocket.chat/jest-presets": "workspace:~"
@@ -10290,7 +10290,7 @@ __metadata:
"@babel/core": ~7.22.20
"@react-aria/toolbar": ^3.0.0-beta.1
"@rocket.chat/eslint-config": "workspace:^"
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/icons": ~0.38.0
"@storybook/addon-actions": ~6.5.16
"@storybook/addon-docs": ~6.5.16
@@ -10385,7 +10385,7 @@ __metadata:
resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming"
dependencies:
"@rocket.chat/css-in-js": ~0.31.25
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/fuselage-hooks": ^0.33.1
"@rocket.chat/icons": ~0.38.0
"@rocket.chat/ui-contexts": "workspace:~"
@@ -10415,7 +10415,7 @@ __metadata:
"@rocket.chat/css-in-js": ~0.31.25
"@rocket.chat/emitter": ~0.31.25
"@rocket.chat/eslint-config": "workspace:^"
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/fuselage-hooks": ^0.33.1
"@rocket.chat/icons": ~0.38.0
"@rocket.chat/jest-presets": "workspace:~"
@@ -10464,7 +10464,7 @@ __metadata:
"@codemirror/tooltip": ^0.19.16
"@lezer/highlight": ^1.1.6
"@rocket.chat/css-in-js": ~0.31.25
- "@rocket.chat/fuselage": ^0.57.1
+ "@rocket.chat/fuselage": ^0.59.0
"@rocket.chat/fuselage-hooks": ^0.33.1
"@rocket.chat/fuselage-polyfills": ~0.31.25
"@rocket.chat/fuselage-toastbar": ^0.33.0
From e0050c363bee1f0e037faefc060c0ff125e96117 Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Fri, 6 Sep 2024 14:25:00 -0600
Subject: [PATCH 2/7] fix: NPS passing `startAt` as the expiration date when
creating a banner (#33155)
---
.changeset/nasty-tools-enjoy.md | 5 +
apps/meteor/server/services/nps/service.ts | 5 +-
.../unit/server/services/nps/spec.tests.ts | 103 ++++++++++++++++++
3 files changed, 112 insertions(+), 1 deletion(-)
create mode 100644 .changeset/nasty-tools-enjoy.md
create mode 100644 apps/meteor/tests/unit/server/services/nps/spec.tests.ts
diff --git a/.changeset/nasty-tools-enjoy.md b/.changeset/nasty-tools-enjoy.md
new file mode 100644
index 000000000000..b6e8dae3785a
--- /dev/null
+++ b/.changeset/nasty-tools-enjoy.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixed a code issue on NPS service. It was passing `startAt` as the expiration date when creating a banner.
diff --git a/apps/meteor/server/services/nps/service.ts b/apps/meteor/server/services/nps/service.ts
index 9554a472fb0a..be5b44582a5a 100644
--- a/apps/meteor/server/services/nps/service.ts
+++ b/apps/meteor/server/services/nps/service.ts
@@ -21,7 +21,10 @@ export class NPSService extends ServiceClassInternal implements INPSService {
const any = await Nps.findOne({}, { projection: { _id: 1 } });
if (!any) {
- await Banner.create(getBannerForAdmins(nps.startAt));
+ if (nps.expireAt < nps.startAt || nps.expireAt < new Date()) {
+ throw new Error('NPS already expired');
+ }
+ await Banner.create(getBannerForAdmins(nps.expireAt));
await notifyAdmins(nps.startAt);
}
diff --git a/apps/meteor/tests/unit/server/services/nps/spec.tests.ts b/apps/meteor/tests/unit/server/services/nps/spec.tests.ts
new file mode 100644
index 000000000000..21c5ecfe7c98
--- /dev/null
+++ b/apps/meteor/tests/unit/server/services/nps/spec.tests.ts
@@ -0,0 +1,103 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+import p from 'proxyquire';
+import sinon from 'sinon';
+
+const modelsMock = {
+ 'NpsVote': {},
+ 'Nps': {
+ findOne: sinon.stub(),
+ save: sinon.stub(),
+ },
+ 'Settings': {
+ getValueById: sinon.stub(),
+ },
+ '@noCallThru': true,
+};
+
+const servicesMock = {
+ Banner: {
+ create: sinon.stub(),
+ },
+};
+
+const getbannerforadminsMock = sinon.stub();
+
+const { NPSService } = p('../../../../../server/services/nps/service.ts', {
+ '@rocket.chat/models': modelsMock,
+ '@rocket.chat/core-services': servicesMock,
+ './sendNpsResults': { 'sendNpsResults': sinon.stub(), '@noCallThru': true },
+ '../../lib/logger/system': { 'SystemLogger': { error: sinon.stub() }, '@noCallThru': true },
+ './notification': { 'notifyAdmins': sinon.stub(), 'getBannerForAdmins': getbannerforadminsMock, '@noCallThru': true },
+});
+
+describe('NPS Service', () => {
+ it('should instantiate properly', () => {
+ expect(new NPSService()).to.be.an('object');
+ });
+
+ describe('@create', () => {
+ beforeEach(() => {
+ modelsMock.Settings.getValueById.reset();
+ modelsMock.Nps.findOne.reset();
+ modelsMock.Nps.save.reset();
+ servicesMock.Banner.create.reset();
+ getbannerforadminsMock.reset();
+ });
+ it('should fail when user opted out of nps', async () => {
+ modelsMock.Settings.getValueById.withArgs('NPS_survey_enabled').resolves(false);
+
+ await expect(new NPSService().create({})).to.be.rejectedWith('Server opted-out for NPS surveys');
+ });
+ it('should fail when nps expireDate is less than nps startAt', async () => {
+ modelsMock.Settings.getValueById.withArgs('NPS_survey_enabled').resolves(true);
+ modelsMock.Nps.findOne.resolves(null);
+
+ await expect(new NPSService().create({ expireAt: new Date('2020-01-01'), startAt: new Date('2020-01-02') })).to.be.rejectedWith(
+ 'NPS already expired',
+ );
+ });
+ it('should fail when expireDate is less than current date', async () => {
+ modelsMock.Settings.getValueById.withArgs('NPS_survey_enabled').resolves(true);
+ modelsMock.Nps.findOne.resolves(null);
+
+ await expect(new NPSService().create({ expireAt: new Date('2020-01-02'), startAt: new Date('2020-01-01') })).to.be.rejectedWith(
+ 'NPS already expired',
+ );
+ });
+ it('should try to create a banner when theres no nps saved', async () => {
+ modelsMock.Settings.getValueById.withArgs('NPS_survey_enabled').resolves(true);
+ modelsMock.Nps.findOne.resolves(null);
+
+ const today = new Date();
+ const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
+
+ await new NPSService().create({
+ expireAt: tomorrow,
+ startAt: today,
+ createdBy: { _id: 'tomorrow', username: 'tomorrow' },
+ npsId: 'test',
+ });
+ expect(getbannerforadminsMock.called).to.be.true;
+ expect(getbannerforadminsMock.calledWith(tomorrow)).to.be.true;
+ expect(modelsMock.Nps.save.called).to.be.true;
+ expect(
+ modelsMock.Nps.save.calledWith(
+ sinon.match({
+ expireAt: tomorrow,
+ startAt: today,
+ status: 'open',
+ _id: 'test',
+ createdBy: { _id: 'tomorrow', username: 'tomorrow' },
+ }),
+ ),
+ ).to.be.true;
+ });
+ it('should fail if theres an error when saving the Nps', async () => {
+ modelsMock.Settings.getValueById.withArgs('NPS_survey_enabled').resolves(true);
+ modelsMock.Nps.findOne.resolves({ _id: 'test' });
+ modelsMock.Nps.save.rejects();
+ await expect(new NPSService().create({})).to.be.rejectedWith('Error creating NPS');
+ });
+ });
+});
From 06dbf725685d52c0514808b4c338e258d0571ddf Mon Sep 17 00:00:00 2001
From: Rafael Tapia
Date: Mon, 9 Sep 2024 12:24:02 -0300
Subject: [PATCH 3/7] feat: unknown contacts (#32865)
---
apps/meteor/app/livechat/server/api/v1/contact.ts | 4 ++--
apps/meteor/app/livechat/server/lib/Contacts.ts | 4 ++--
apps/meteor/app/livechat/server/lib/LivechatTyped.ts | 11 +++++++++++
.../tests/end-to-end/api/livechat/09-visitors.ts | 1 +
packages/core-typings/src/ILivechatContact.ts | 4 ++--
packages/core-typings/src/ILivechatVisitor.ts | 1 +
6 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts
index 94bd5ed3e11c..7e9457d2f185 100644
--- a/apps/meteor/app/livechat/server/api/v1/contact.ts
+++ b/apps/meteor/app/livechat/server/api/v1/contact.ts
@@ -92,7 +92,7 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps },
{
async post() {
- if (!process.env.TEST_MODE) {
+ if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
}
const contactId = await createContact({ ...this.bodyParams, unknown: false });
@@ -106,7 +106,7 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps },
{
async post() {
- if (!process.env.TEST_MODE) {
+ if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
}
diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts
index 58404ce27584..f6f812ce8af8 100644
--- a/apps/meteor/app/livechat/server/lib/Contacts.ts
+++ b/apps/meteor/app/livechat/server/lib/Contacts.ts
@@ -44,8 +44,8 @@ type RegisterContactProps = {
type CreateContactParams = {
name: string;
- emails: string[];
- phones: string[];
+ emails?: string[];
+ phones?: string[];
unknown: boolean;
customFields?: Record;
contactManager?: string;
diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
index be79d565f6de..88f9494159a2 100644
--- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -71,6 +71,7 @@ import * as Mailer from '../../../mailer/server/api';
import { metrics } from '../../../metrics/server';
import { settings } from '../../../settings/server';
import { businessHourManager } from '../business-hour';
+import { createContact } from './Contacts';
import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper';
import { QueueManager } from './QueueManager';
import { RoutingManager } from './RoutingManager';
@@ -664,6 +665,16 @@ class LivechatClass {
}
}
+ if (process.env.TEST_MODE?.toUpperCase() === 'TRUE') {
+ const contactId = await createContact({
+ name: name ?? (visitorDataToUpdate.username as string),
+ emails: email ? [email] : [],
+ phones: phone ? [phone.number] : [],
+ unknown: true,
+ });
+ visitorDataToUpdate.contactId = contactId;
+ }
+
const upsertedLivechatVisitor = await LivechatVisitors.updateOneByIdOrToken(visitorDataToUpdate, {
upsert: true,
returnDocument: 'after',
diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts
index 5bc961087efc..f02d9d1d1e95 100644
--- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts
+++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts
@@ -56,6 +56,7 @@ describe('LIVECHAT - visitors', () => {
expect(body).to.have.property('success', true);
expect(body).to.have.property('visitor');
expect(body.visitor).to.have.property('token', 'test');
+ expect(body.visitor).to.have.property('contactId');
// Ensure all new visitors are created as online :)
expect(body.visitor).to.have.property('status', 'online');
diff --git a/packages/core-typings/src/ILivechatContact.ts b/packages/core-typings/src/ILivechatContact.ts
index 149dab2b88b1..1e7bd4ff5399 100644
--- a/packages/core-typings/src/ILivechatContact.ts
+++ b/packages/core-typings/src/ILivechatContact.ts
@@ -14,8 +14,8 @@ export interface ILivechatContactConflictingField {
export interface ILivechatContact extends IRocketChatRecord {
name: string;
- phones: string[];
- emails: string[];
+ phones?: string[];
+ emails?: string[];
contactManager?: string;
unknown?: boolean;
hasConflict?: boolean;
diff --git a/packages/core-typings/src/ILivechatVisitor.ts b/packages/core-typings/src/ILivechatVisitor.ts
index 21819cc23f24..eefb4ebd720c 100644
--- a/packages/core-typings/src/ILivechatVisitor.ts
+++ b/packages/core-typings/src/ILivechatVisitor.ts
@@ -49,6 +49,7 @@ export interface ILivechatVisitor extends IRocketChatRecord {
};
activity?: string[];
disabled?: boolean;
+ contactId?: string;
}
export interface ILivechatVisitorDTO {
From 7c14fd1a802a2f63f3dc6796e83192b54cbd4ff2 Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Mon, 9 Sep 2024 13:12:34 -0600
Subject: [PATCH 4/7] feat: Allow admins to control if visitors can close
omnichannel conversations (#33139)
---
.changeset/healthy-rivers-nail.md | 8 +++
.../imports/server/rest/appearance.ts | 1 +
.../app/livechat/server/api/lib/appearance.ts | 1 +
.../app/livechat/server/api/lib/livechat.ts | 1 +
.../meteor/app/livechat/server/api/v1/room.ts | 4 ++
.../app/livechat/server/lib/LivechatTyped.ts | 1 +
.../omnichannel/appearance/AppearanceForm.tsx | 15 ++++++
.../omnichannel/appearance/AppearancePage.tsx | 1 +
apps/meteor/server/settings/omnichannel.ts | 7 +++
.../omnichannel/omnichannel-livechat.spec.ts | 52 +++++++++++++++++++
.../tests/end-to-end/api/livechat/00-rooms.ts | 19 +++++++
packages/i18n/src/locales/en.i18n.json | 2 +
.../livechat/src/routes/Chat/connector.tsx | 2 +
.../livechat/src/routes/Chat/container.js | 4 +-
packages/livechat/src/store/index.tsx | 1 +
15 files changed, 117 insertions(+), 2 deletions(-)
create mode 100644 .changeset/healthy-rivers-nail.md
diff --git a/.changeset/healthy-rivers-nail.md b/.changeset/healthy-rivers-nail.md
new file mode 100644
index 000000000000..a8da9bec846e
--- /dev/null
+++ b/.changeset/healthy-rivers-nail.md
@@ -0,0 +1,8 @@
+---
+"@rocket.chat/meteor": minor
+"@rocket.chat/i18n": minor
+"@rocket.chat/livechat": minor
+---
+
+Added new setting `Allow visitors to finish conversations` that allows admins to decide if omnichannel visitors can close a conversation or not. This doesn't affect agent's capabilities of room closing, neither apps using the livechat bridge to close rooms.
+However, if currently your integration relies on `livechat/room.close` endpoint for closing conversations, it's advised to use the authenticated version `livechat/room.closeByUser` of it before turning off this setting.
diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts
index 48863fc9e5d3..7496b6243abe 100644
--- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts
+++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts
@@ -51,6 +51,7 @@ API.v1.addRoute(
'Livechat_background',
'Livechat_widget_position',
'Livechat_hide_system_messages',
+ 'Omnichannel_allow_visitors_to_close_conversation',
];
const valid = settings.every((setting) => validSettingList.includes(setting._id));
diff --git a/apps/meteor/app/livechat/server/api/lib/appearance.ts b/apps/meteor/app/livechat/server/api/lib/appearance.ts
index 785413ead9d1..0fc7d3547b2c 100644
--- a/apps/meteor/app/livechat/server/api/lib/appearance.ts
+++ b/apps/meteor/app/livechat/server/api/lib/appearance.ts
@@ -28,6 +28,7 @@ export async function findAppearance(): Promise<{ appearance: ISetting[] }> {
'Livechat_background',
'Livechat_widget_position',
'Livechat_hide_system_messages',
+ 'Omnichannel_allow_visitors_to_close_conversation',
],
},
};
diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts
index a922edd40899..8041566d796e 100644
--- a/apps/meteor/app/livechat/server/api/lib/livechat.ts
+++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts
@@ -142,6 +142,7 @@ export async function settings({ businessUnit = '' }: { businessUnit?: string }
hiddenSystemMessages: initSettings.Livechat_hide_system_messages,
livechatLogo: initSettings.Assets_livechat_widget_logo,
hideWatermark: initSettings.Livechat_hide_watermark || false,
+ visitorsCanCloseChat: initSettings.Omnichannel_allow_visitors_to_close_conversation,
},
theme: {
title: initSettings.Livechat_title,
diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts
index 565f8e0bb3f4..7aacfacb4476 100644
--- a/apps/meteor/app/livechat/server/api/v1/room.ts
+++ b/apps/meteor/app/livechat/server/api/v1/room.ts
@@ -107,6 +107,10 @@ API.v1.addRoute(
async post() {
const { rid, token } = this.bodyParams;
+ if (!rcSettings.get('Omnichannel_allow_visitors_to_close_conversation')) {
+ throw new Error('error-not-allowed-to-close-conversation');
+ }
+
const visitor = await findGuest(token);
if (!visitor) {
throw new Error('invalid-token');
diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
index 88f9494159a2..89d125033977 100644
--- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -1079,6 +1079,7 @@ class LivechatClass {
'Livechat_background',
'Assets_livechat_widget_logo',
'Livechat_hide_watermark',
+ 'Omnichannel_allow_visitors_to_close_conversation',
] as const;
type SettingTypes = (typeof validSettings)[number] | 'Livechat_Show_Connecting';
diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx
index 4253ff023ee9..a4435398d9a9 100644
--- a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx
+++ b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx
@@ -52,6 +52,7 @@ const AppearanceForm = () => {
const livechatWidgetPositionField = useUniqueId();
const livechatBackgroundField = useUniqueId();
const livechatHideSystemMessagesField = useUniqueId();
+ const omnichannelVisitorsCanCloseConversationField = useUniqueId();
return (
@@ -140,6 +141,20 @@ const AppearanceForm = () => {
/>
+
+
+
+ {t('Omnichannel_allow_visitors_to_close_conversation')}
+
+ (
+
+ )}
+ />
+
+
diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx
index a2cfb7b8103b..b90c32af6a7d 100644
--- a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx
+++ b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx
@@ -28,6 +28,7 @@ type LivechatAppearanceSettings = {
Livechat_conversation_finished_text: string;
Livechat_enable_message_character_limit: boolean;
Livechat_message_character_limit: number;
+ Omnichannel_allow_visitors_to_close_conversation: boolean;
};
type AppearanceSettings = Partial;
diff --git a/apps/meteor/server/settings/omnichannel.ts b/apps/meteor/server/settings/omnichannel.ts
index ed1daa8ce228..c86cd6674d4e 100644
--- a/apps/meteor/server/settings/omnichannel.ts
+++ b/apps/meteor/server/settings/omnichannel.ts
@@ -157,6 +157,13 @@ export const createOmniSettings = () =>
i18nLabel: 'Show_agent_email',
});
+ await this.add('Omnichannel_allow_visitors_to_close_conversation', true, {
+ type: 'boolean',
+ group: 'Omnichannel',
+ public: true,
+ enableQuery: omnichannelEnabledQuery,
+ });
+
await this.add('Livechat_request_comment_when_closing_conversation', true, {
type: 'boolean',
group: 'Omnichannel',
diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts
index bf14584ed89f..405e7f82e3c4 100644
--- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts
+++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts
@@ -2,6 +2,7 @@ import { createFakeVisitor } from '../../mocks/data';
import { createAuxContext } from '../fixtures/createAuxContext';
import { Users } from '../fixtures/userStates';
import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects';
+import { setSettingValueById } from '../utils';
import { createAgent } from '../utils/omnichannel/agents';
import { test, expect } from '../utils/test';
@@ -93,6 +94,57 @@ test.describe.serial('OC - Livechat', () => {
});
});
+test.describe.serial('OC - Livechat - Visitors closing the room is disabled', () => {
+ let poLiveChat: OmnichannelLiveChat;
+ let poHomeOmnichannel: HomeOmnichannel;
+
+ test.beforeAll(async ({ api }) => {
+ await api.post('/livechat/users/agent', { username: 'user1' });
+ });
+
+ test.beforeAll(async ({ browser, api }) => {
+ const { page: livechatPage } = await createAuxContext(browser, Users.user1, '/livechat', false);
+
+ poLiveChat = new OmnichannelLiveChat(livechatPage, api);
+ });
+
+ test.beforeAll(async ({ browser, api }) => {
+ await setSettingValueById(api, 'Livechat_allow_visitor_closing_chat', false);
+ const { page: omniPage } = await createAuxContext(browser, Users.user1, '/', true);
+ poHomeOmnichannel = new HomeOmnichannel(omniPage);
+ });
+
+ test.afterAll(async ({ api }) => {
+ await setSettingValueById(api, 'Livechat_allow_visitor_closing_chat', true);
+ await api.delete('/livechat/users/agent/user1');
+ await poLiveChat.page.close();
+ });
+
+ test('OC - Livechat - Close Chat disabled', async () => {
+ await poLiveChat.page.reload();
+ await poLiveChat.openAnyLiveChat();
+ await poLiveChat.sendMessage(firstVisitor, false);
+ await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_user');
+ await poLiveChat.btnSendMessageToOnlineAgent.click();
+
+ await test.step('expect to close a livechat conversation', async () => {
+ await expect(poLiveChat.btnOptions).not.toBeVisible();
+ await expect(poLiveChat.btnCloseChat).not.toBeVisible();
+ });
+ });
+
+ test('OC - Livechat - Close chat disabled, agents can close', async () => {
+ await poHomeOmnichannel.sidenav.openChat(firstVisitor.name);
+
+ await test.step('expect livechat conversation to be closed by agent', async () => {
+ await poHomeOmnichannel.content.btnCloseChat.click();
+ await poHomeOmnichannel.content.closeChatModal.inputComment.fill('this_is_a_test_comment');
+ await poHomeOmnichannel.content.closeChatModal.btnConfirm.click();
+ await expect(poHomeOmnichannel.toastSuccess).toBeVisible();
+ });
+ });
+});
+
test.describe.serial('OC - Livechat - Resub after close room', () => {
let poLiveChat: OmnichannelLiveChat;
let poHomeOmnichannel: HomeOmnichannel;
diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
index 7142725a1d99..4388a4d24341 100644
--- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
+++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
@@ -689,6 +689,25 @@ describe('LIVECHAT - rooms', () => {
expect(latestRoom).to.not.have.property('pdfTranscriptFileId');
},
);
+
+ describe('Special case: visitors closing is disabled', () => {
+ before(async () => {
+ await updateSetting('Omnichannel_allow_visitors_to_close_conversation', false);
+ });
+ after(async () => {
+ await updateSetting('Omnichannel_allow_visitors_to_close_conversation', true);
+ });
+ it('should not allow visitor to close a conversation', async () => {
+ const { room, visitor } = await startANewLivechatRoomAndTakeIt();
+ await request
+ .post(api('livechat/room.close'))
+ .send({
+ token: visitor.token,
+ rid: room._id,
+ })
+ .expect(400);
+ });
+ });
});
describe('livechat/room.forward', () => {
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index ea7e31422cb1..d3324c6e749a 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -4037,6 +4037,8 @@
"Omnichannel_Reports_Summary": "Gain insights into your operation and export your metrics.",
"Omnichannel_max_fallback_forward_depth": "Maximum fallback forward departments depth",
"Omnichannel_max_fallback_forward_depth_Description": "Maximum number of hops that a room being transfered will do when the target department has a Fallback Forward Department set up. When limit is reached, chat won't be transferred and process will stop. Depending on your configuration, setting a high number may cause performance issues.",
+ "Omnichannel_allow_visitors_to_close_conversation": "Allow visitors to finish conversations",
+ "Omnichannel_allow_visitors_to_close_conversation_Description": "When disabled, visitors won't be able to finish an ongoing conversation either via UI or via API.",
"On": "On",
"on-hold-livechat-room": "On Hold Omnichannel Room",
"on-hold-livechat-room_description": "Permission to on hold omnichannel room",
diff --git a/packages/livechat/src/routes/Chat/connector.tsx b/packages/livechat/src/routes/Chat/connector.tsx
index 36e574b246b1..3c72f9ae88cf 100644
--- a/packages/livechat/src/routes/Chat/connector.tsx
+++ b/packages/livechat/src/routes/Chat/connector.tsx
@@ -22,6 +22,7 @@ export const ChatConnector: FunctionalComponent<{ path: string; default: boolean
nameFieldRegistrationForm,
emailFieldRegistrationForm,
limitTextLength,
+ visitorsCanCloseChat,
},
messages: { conversationFinishedMessage },
theme: { title = '' } = {},
@@ -94,6 +95,7 @@ export const ChatConnector: FunctionalComponent<{ path: string; default: boolean
ongoingCall={ongoingCall}
messageListPosition={messageListPosition}
theme={theme}
+ visitorsCanCloseChat={visitorsCanCloseChat}
/>
);
};
diff --git a/packages/livechat/src/routes/Chat/container.js b/packages/livechat/src/routes/Chat/container.js
index 19172cc7fe5a..43ff281c6472 100644
--- a/packages/livechat/src/routes/Chat/container.js
+++ b/packages/livechat/src/routes/Chat/container.js
@@ -288,8 +288,8 @@ class ChatContainer extends Component {
};
canFinishChat = () => {
- const { room, connecting } = this.props;
- return room !== undefined || connecting;
+ const { room, connecting, visitorsCanCloseChat } = this.props;
+ return visitorsCanCloseChat && (room !== undefined || connecting);
};
canRemoveUserData = () => {
diff --git a/packages/livechat/src/store/index.tsx b/packages/livechat/src/store/index.tsx
index f8629ce693cc..e7d4b8caae17 100644
--- a/packages/livechat/src/store/index.tsx
+++ b/packages/livechat/src/store/index.tsx
@@ -59,6 +59,7 @@ export type StoreState = {
hideWatermark?: boolean;
livechatLogo?: { url: string };
transcript?: boolean;
+ visitorsCanCloseChat?: boolean;
};
online?: boolean;
departments: Department[];
From 4146c3956d6d8337b7a23232506f7796f113df17 Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Mon, 9 Sep 2024 15:21:14 -0600
Subject: [PATCH 5/7] fix: Allow to use the token from `room.v` when requesting
transcript instead of finding visitor (#33211)
---
.changeset/four-cherries-kneel.md | 5 +++
.../app/livechat/server/lib/sendTranscript.ts | 17 +++----
apps/meteor/tests/data/livechat/rooms.ts | 4 +-
.../end-to-end/api/livechat/11-livechat.ts | 21 +++++++++
.../server/lib/sendTranscript.spec.ts | 45 +++++++++++++------
5 files changed, 67 insertions(+), 25 deletions(-)
create mode 100644 .changeset/four-cherries-kneel.md
diff --git a/.changeset/four-cherries-kneel.md b/.changeset/four-cherries-kneel.md
new file mode 100644
index 000000000000..095d5af0aa76
--- /dev/null
+++ b/.changeset/four-cherries-kneel.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Allow to use the token from `room.v` when requesting transcript instead of visitor token. Visitors may change their tokens at any time, rendering old conversations impossible to access for them (or for APIs depending on token) as the visitor token won't match the `room.v` token.
diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts
index 74032121ee50..bc7c06e0eaae 100644
--- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts
+++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts
@@ -3,12 +3,13 @@ import {
type IUser,
type MessageTypesValues,
type IOmnichannelSystemMessage,
+ type ILivechatVisitor,
isFileAttachment,
isFileImageAttachment,
} from '@rocket.chat/core-typings';
import colors from '@rocket.chat/fuselage-tokens/colors';
import { Logger } from '@rocket.chat/logger';
-import { LivechatRooms, LivechatVisitors, Messages, Uploads, Users } from '@rocket.chat/models';
+import { LivechatRooms, Messages, Uploads, Users } from '@rocket.chat/models';
import { check } from 'meteor/check';
import moment from 'moment-timezone';
@@ -41,16 +42,12 @@ export async function sendTranscript({
const room = await LivechatRooms.findOneById(rid);
- const visitor = await LivechatVisitors.getVisitorByToken(token, {
- projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 },
- });
-
- if (!visitor) {
- throw new Error('error-invalid-token');
+ const visitor = room?.v as ILivechatVisitor;
+ if (token !== visitor?.token) {
+ throw new Error('error-invalid-visitor');
}
- // @ts-expect-error - Visitor typings should include language?
- const userLanguage = visitor?.language || settings.get('Language') || 'en';
+ const userLanguage = settings.get('Language') || 'en';
const timezone = getTimezone(user);
logger.debug(`Transcript will be sent using ${timezone} as timezone`);
@@ -59,7 +56,7 @@ export async function sendTranscript({
}
// allow to only user to send transcripts from their own chats
- if (room.t !== 'l' || !room.v || room.v.token !== token) {
+ if (room.t !== 'l') {
throw new Error('error-invalid-room');
}
diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts
index 9532fd4214ab..b5d89762c614 100644
--- a/apps/meteor/tests/data/livechat/rooms.ts
+++ b/apps/meteor/tests/data/livechat/rooms.ts
@@ -33,10 +33,10 @@ export const createLivechatRoom = async (visitorToken: string, extraRoomParams?:
return response.body.room;
};
-export const createVisitor = (department?: string, visitorName?: string): Promise =>
+export const createVisitor = (department?: string, visitorName?: string, customEmail?: string): Promise =>
new Promise((resolve, reject) => {
const token = getRandomVisitorToken();
- const email = `${token}@${token}.com`;
+ const email = customEmail || `${token}@${token}.com`;
const phone = `${Math.floor(Math.random() * 10000000000)}`;
void request.get(api(`livechat/visitor/${token}`)).end((err: Error, res: DummyResponse) => {
if (!err && res && res.body && res.body.visitor) {
diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts
index c07f7bcecc81..7ce582025538 100644
--- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts
+++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts
@@ -283,6 +283,27 @@ describe('LIVECHAT - Utils', () => {
.send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' });
expect(body).to.have.property('success', true);
});
+ it('should allow a visitor to get a transcript even if token changed by using an old token that matches room.v', async () => {
+ const visitor = await createVisitor();
+ const room = await createLivechatRoom(visitor.token);
+ await closeOmnichannelRoom(room._id);
+ const visitor2 = await createVisitor(undefined, undefined, visitor.visitorEmails?.[0].address);
+ const room2 = await createLivechatRoom(visitor2.token);
+ await closeOmnichannelRoom(room2._id);
+
+ expect(visitor.token !== visitor2.token).to.be.true;
+ const { body } = await request
+ .post(api('livechat/transcript'))
+ .set(credentials)
+ .send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' });
+ expect(body).to.have.property('success', true);
+
+ const { body: body2 } = await request
+ .post(api('livechat/transcript'))
+ .set(credentials)
+ .send({ token: visitor2.token, rid: room2._id, email: 'visitor@notadomain.com' });
+ expect(body2).to.have.property('success', true);
+ });
});
describe('livechat/transcript/:rid', () => {
diff --git a/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts b/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts
index 64da050cfd88..ca39a64c21a9 100644
--- a/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts
+++ b/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts
@@ -6,9 +6,6 @@ const modelsMock = {
LivechatRooms: {
findOneById: sinon.stub(),
},
- LivechatVisitors: {
- getVisitorByToken: sinon.stub(),
- },
Messages: {
findLivechatClosingMessage: sinon.stub(),
findVisibleByRoomIdNotContainingTypesBeforeTs: sinon.stub(),
@@ -75,7 +72,6 @@ describe('Send transcript', () => {
beforeEach(() => {
checkMock.reset();
modelsMock.LivechatRooms.findOneById.reset();
- modelsMock.LivechatVisitors.getVisitorByToken.reset();
modelsMock.Messages.findLivechatClosingMessage.reset();
modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.reset();
modelsMock.Users.findOneById.reset();
@@ -87,11 +83,9 @@ describe('Send transcript', () => {
await expect(sendTranscript({})).to.be.rejectedWith(Error);
});
it('should throw error when visitor not found', async () => {
- modelsMock.LivechatVisitors.getVisitorByToken.resolves(null);
await expect(sendTranscript({ rid: 'rid', email: 'email', logger: mockLogger })).to.be.rejectedWith(Error);
});
it('should attempt to send an email when params are valid using default subject', async () => {
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } });
modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]);
tStub.returns('Conversation Transcript');
@@ -117,7 +111,6 @@ describe('Send transcript', () => {
).to.be.true;
});
it('should use provided subject', async () => {
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } });
modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]);
@@ -143,7 +136,6 @@ describe('Send transcript', () => {
).to.be.true;
});
it('should use subject from setting (when configured) when no subject provided', async () => {
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } });
modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]);
mockSettingValues.Livechat_transcript_email_subject = 'A custom subject obtained from setting.get';
@@ -170,36 +162,63 @@ describe('Send transcript', () => {
});
it('should fail if room provided is invalid', async () => {
modelsMock.LivechatRooms.findOneById.resolves(null);
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
await expect(sendTranscript({ rid: 'rid', email: 'email', logger: mockLogger })).to.be.rejectedWith(Error);
});
it('should fail if room provided is of different type', async () => {
modelsMock.LivechatRooms.findOneById.resolves({ t: 'c' });
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error);
});
it('should fail if room is of valid type, but doesnt doesnt have `v` property', async () => {
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
modelsMock.LivechatRooms.findOneById.resolves({ t: 'l' });
await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error);
});
it('should fail if room is of valid type, has `v` prop, but it doesnt contain `token`', async () => {
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { otherProp: 'xxx' } });
await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error);
});
it('should fail if room is of valid type, has `v.token`, but its different from the one on param (room from another visitor)', async () => {
- modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null });
modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'xxx' } });
await expect(sendTranscript({ rid: 'rid', email: 'email', token: 'xveasdf' })).to.be.rejectedWith(Error);
});
+
+ it('should throw an error when token is not the one on room.v', async () => {
+ modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'xxx' } });
+
+ await expect(sendTranscript({ rid: 'rid', email: 'email', token: 'xveasdf' })).to.be.rejectedWith(Error);
+ });
+ it('should work when token matches room.v', async () => {
+ modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token-123' } });
+ modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]);
+ delete mockSettingValues.Livechat_transcript_email_subject;
+ tStub.returns('Conversation Transcript');
+
+ await sendTranscript({
+ rid: 'rid',
+ token: 'token-123',
+ email: 'email',
+ user: { _id: 'x', name: 'x', utcOffset: '-6', username: 'x' },
+ });
+
+ expect(getTimezoneMock.calledWith({ _id: 'x', name: 'x', utcOffset: '-6', username: 'x' })).to.be.true;
+ expect(modelsMock.Messages.findLivechatClosingMessage.calledWith('rid', { projection: { ts: 1 } })).to.be.true;
+ expect(modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.called).to.be.true;
+ expect(
+ mailerMock.calledWith({
+ to: 'email',
+ from: 'test@rocket.chat',
+ subject: 'Conversation Transcript',
+ replyTo: 'test@rocket.chat',
+ html: '
',
+ }),
+ ).to.be.true;
+ });
});
From 0f21fa01a3f1c9fcd533d696d001c2594ca38092 Mon Sep 17 00:00:00 2001
From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com>
Date: Tue, 10 Sep 2024 16:08:43 +0530
Subject: [PATCH 6/7] feat: Option to disable 2FA for OAuth users (#32945)
---
.changeset/tiny-geckos-kiss.md | 6 ++
.../app/2fa/server/code/EmailCheck.spec.ts | 70 +++++++++++++++
apps/meteor/app/2fa/server/code/EmailCheck.ts | 6 +-
apps/meteor/app/2fa/server/code/index.ts | 12 +--
apps/meteor/app/api/server/v1/misc.ts | 14 +--
.../server/functions/getBaseUserFields.ts | 34 ++++++++
.../server/functions/getDefaultUserFields.ts | 35 ++------
.../account/security/AccountSecurityPage.tsx | 12 ++-
.../views/account/security/TwoFactorEmail.tsx | 4 +-
apps/meteor/server/settings/accounts.ts | 12 +++
packages/core-typings/src/IUser.ts | 85 +++++++++++++------
packages/i18n/src/locales/en.i18n.json | 2 +
12 files changed, 217 insertions(+), 75 deletions(-)
create mode 100644 .changeset/tiny-geckos-kiss.md
create mode 100644 apps/meteor/app/2fa/server/code/EmailCheck.spec.ts
create mode 100644 apps/meteor/app/utils/server/functions/getBaseUserFields.ts
diff --git a/.changeset/tiny-geckos-kiss.md b/.changeset/tiny-geckos-kiss.md
new file mode 100644
index 000000000000..d38150970310
--- /dev/null
+++ b/.changeset/tiny-geckos-kiss.md
@@ -0,0 +1,6 @@
+---
+'@rocket.chat/i18n': minor
+'@rocket.chat/meteor': minor
+---
+
+Added a new setting which allows workspace admins to disable email two factor authentication for SSO (OAuth) users. If enabled, SSO users won't be asked for email two factor authentication.
diff --git a/apps/meteor/app/2fa/server/code/EmailCheck.spec.ts b/apps/meteor/app/2fa/server/code/EmailCheck.spec.ts
new file mode 100644
index 000000000000..5c3574f0b395
--- /dev/null
+++ b/apps/meteor/app/2fa/server/code/EmailCheck.spec.ts
@@ -0,0 +1,70 @@
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+import proxyquire from 'proxyquire';
+import sinon from 'sinon';
+
+const settingsMock = sinon.stub();
+
+const { EmailCheck } = proxyquire.noCallThru().load('./EmailCheck', {
+ '@rocket.chat/models': {
+ Users: {},
+ },
+ 'meteor/accounts-base': {
+ Accounts: {
+ _bcryptRounds: () => '123',
+ },
+ },
+ '../../../../server/lib/i18n': {
+ i18n: {
+ t: (key: string) => key,
+ },
+ },
+ '../../../mailer/server/api': {
+ send: () => undefined,
+ },
+ '../../../settings/server': {
+ settings: {
+ get: settingsMock,
+ },
+ },
+});
+
+const normalUserMock = { services: { email2fa: { enabled: true } }, emails: [{ email: 'abc@gmail.com', verified: true }] };
+const normalUserWithUnverifiedEmailMock = {
+ services: { email2fa: { enabled: true } },
+ emails: [{ email: 'abc@gmail.com', verified: false }],
+};
+const OAuthUserMock = { services: { google: {} }, emails: [{ email: 'abc@gmail.com', verified: true }] };
+
+describe('EmailCheck', () => {
+ let emailCheck: typeof EmailCheck;
+ beforeEach(() => {
+ settingsMock.reset();
+
+ emailCheck = new EmailCheck();
+ });
+
+ it('should return EmailCheck is enabled for a normal user', () => {
+ settingsMock.returns(true);
+
+ const isEmail2FAEnabled = emailCheck.isEnabled(normalUserMock);
+
+ expect(isEmail2FAEnabled).to.be.equal(true);
+ });
+
+ it('should return EmailCheck is not enabled for a normal user with unverified email', () => {
+ settingsMock.returns(true);
+
+ const isEmail2FAEnabled = emailCheck.isEnabled(normalUserWithUnverifiedEmailMock);
+
+ expect(isEmail2FAEnabled).to.be.equal(false);
+ });
+
+ it('should return EmailCheck is not enabled for a OAuth user with setting being false', () => {
+ settingsMock.returns(true);
+
+ const isEmail2FAEnabled = emailCheck.isEnabled(OAuthUserMock);
+
+ expect(isEmail2FAEnabled).to.be.equal(false);
+ });
+});
diff --git a/apps/meteor/app/2fa/server/code/EmailCheck.ts b/apps/meteor/app/2fa/server/code/EmailCheck.ts
index 123df96ee264..d947c1b30c2e 100644
--- a/apps/meteor/app/2fa/server/code/EmailCheck.ts
+++ b/apps/meteor/app/2fa/server/code/EmailCheck.ts
@@ -1,4 +1,4 @@
-import type { IUser } from '@rocket.chat/core-typings';
+import { isOAuthUser, type IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import bcrypt from 'bcrypt';
@@ -24,6 +24,10 @@ export class EmailCheck implements ICodeCheck {
return false;
}
+ if (!settings.get('Accounts_twoFactorAuthentication_email_available_for_OAuth_users') && isOAuthUser(user)) {
+ return false;
+ }
+
if (!user.services?.email2fa?.enabled) {
return false;
}
diff --git a/apps/meteor/app/2fa/server/code/index.ts b/apps/meteor/app/2fa/server/code/index.ts
index 1fbe658e5682..b05157416e31 100644
--- a/apps/meteor/app/2fa/server/code/index.ts
+++ b/apps/meteor/app/2fa/server/code/index.ts
@@ -45,14 +45,10 @@ function getAvailableMethodNames(user: IUser): string[] {
export async function getUserForCheck(userId: string): Promise {
return Users.findOneById(userId, {
projection: {
- 'emails': 1,
- 'language': 1,
- 'createdAt': 1,
- 'services.totp': 1,
- 'services.email2fa': 1,
- 'services.emailCode': 1,
- 'services.password': 1,
- 'services.resume.loginTokens': 1,
+ emails: 1,
+ language: 1,
+ createdAt: 1,
+ services: 1,
},
});
}
diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts
index dd4da47bff05..8348b8429e4e 100644
--- a/apps/meteor/app/api/server/v1/misc.ts
+++ b/apps/meteor/app/api/server/v1/misc.ts
@@ -1,6 +1,6 @@
import crypto from 'crypto';
-import type { IUser } from '@rocket.chat/core-typings';
+import { isOAuthUser, type IUser } from '@rocket.chat/core-typings';
import { Settings, Users } from '@rocket.chat/models';
import {
isShieldSvgProps,
@@ -26,7 +26,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP
import { passwordPolicy } from '../../../lib/server';
import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
-import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields';
+import { getBaseUserFields } from '../../../utils/server/functions/getBaseUserFields';
import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured';
import { getURL } from '../../../utils/server/getURL';
import { API } from '../api';
@@ -176,15 +176,19 @@ API.v1.addRoute(
{ authRequired: true },
{
async get() {
- const fields = getDefaultUserFields();
- const { services, ...user } = (await Users.findOneById(this.userId, { projection: fields })) as IUser;
+ const userFields = { ...getBaseUserFields(), services: 1 };
+ const { services, ...user } = (await Users.findOneById(this.userId, { projection: userFields })) as IUser;
return API.v1.success(
await getUserInfo({
...user,
+ isOAuthUser: isOAuthUser({ ...user, services }),
...(services && {
services: {
- ...services,
+ ...(services.github && { github: services.github }),
+ ...(services.gitlab && { gitlab: services.gitlab }),
+ ...(services.email2fa?.enabled && { email2fa: { enabled: services.email2fa.enabled } }),
+ ...(services.totp?.enabled && { totp: { enabled: services.totp.enabled } }),
password: {
// The password hash shouldn't be leaked but the client may need to know if it exists.
exists: Boolean(services?.password?.bcrypt),
diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts
new file mode 100644
index 000000000000..5e2a3bf2b4d7
--- /dev/null
+++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts
@@ -0,0 +1,34 @@
+type UserFields = {
+ [k: string]: number;
+};
+
+export const getBaseUserFields = (): UserFields => ({
+ 'name': 1,
+ 'username': 1,
+ 'nickname': 1,
+ 'emails': 1,
+ 'status': 1,
+ 'statusDefault': 1,
+ 'statusText': 1,
+ 'statusConnection': 1,
+ 'bio': 1,
+ 'avatarOrigin': 1,
+ 'utcOffset': 1,
+ 'language': 1,
+ 'settings': 1,
+ 'enableAutoAway': 1,
+ 'idleTimeLimit': 1,
+ 'roles': 1,
+ 'active': 1,
+ 'defaultRoom': 1,
+ 'customFields': 1,
+ 'requirePasswordChange': 1,
+ 'requirePasswordChangeReason': 1,
+ 'statusLivechat': 1,
+ 'banners': 1,
+ 'oauth.authorizedClients': 1,
+ '_updatedAt': 1,
+ 'avatarETag': 1,
+ 'extension': 1,
+ 'openBusinessHours': 1,
+});
diff --git a/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts b/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts
index 03d0cae77ab9..293eb8607342 100644
--- a/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts
+++ b/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts
@@ -1,39 +1,14 @@
-type DefaultUserFields = {
+import { getBaseUserFields } from './getBaseUserFields';
+
+type UserFields = {
[k: string]: number;
};
-export const getDefaultUserFields = (): DefaultUserFields => ({
- 'name': 1,
- 'username': 1,
- 'nickname': 1,
- 'emails': 1,
- 'status': 1,
- 'statusDefault': 1,
- 'statusText': 1,
- 'statusConnection': 1,
- 'bio': 1,
- 'avatarOrigin': 1,
- 'utcOffset': 1,
- 'language': 1,
- 'settings': 1,
- 'enableAutoAway': 1,
- 'idleTimeLimit': 1,
- 'roles': 1,
- 'active': 1,
- 'defaultRoom': 1,
- 'customFields': 1,
- 'requirePasswordChange': 1,
- 'requirePasswordChangeReason': 1,
+export const getDefaultUserFields = (): UserFields => ({
+ ...getBaseUserFields(),
'services.github': 1,
'services.gitlab': 1,
'services.password.bcrypt': 1,
'services.totp.enabled': 1,
'services.email2fa.enabled': 1,
- 'statusLivechat': 1,
- 'banners': 1,
- 'oauth.authorizedClients': 1,
- '_updatedAt': 1,
- 'avatarETag': 1,
- 'extension': 1,
- 'openBusinessHours': 1,
});
diff --git a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx
index 536ba8a04ef7..06619f0618f5 100644
--- a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx
+++ b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx
@@ -1,6 +1,6 @@
import { Box, Accordion, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
-import { useSetting, useTranslation } from '@rocket.chat/ui-contexts';
+import { useSetting, useTranslation, useUser } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
@@ -15,6 +15,11 @@ const passwordDefaultValues = { password: '', confirmationPassword: '' };
const AccountSecurityPage = (): ReactElement => {
const t = useTranslation();
+ const user = useUser();
+
+ const isEmail2FAAvailableForOAuth = useSetting('Accounts_twoFactorAuthentication_email_available_for_OAuth_users');
+ const isOAuthUser = user?.isOAuthUser;
+ const isEmail2FAAllowed = !isOAuthUser || isEmail2FAAvailableForOAuth;
const methods = useForm({
defaultValues: passwordDefaultValues,
@@ -30,6 +35,7 @@ const AccountSecurityPage = (): ReactElement => {
const twoFactorByEmailEnabled = useSetting('Accounts_TwoFactorAuthentication_By_Email_Enabled');
const e2eEnabled = useSetting('E2E_Enable');
const allowPasswordChange = useSetting('Accounts_AllowPasswordChange');
+ const showEmailTwoFactor = twoFactorByEmailEnabled && isEmail2FAAllowed;
const passwordFormId = useUniqueId();
@@ -48,10 +54,10 @@ const AccountSecurityPage = (): ReactElement => {
)}
- {(twoFactorTOTP || twoFactorByEmailEnabled) && twoFactorEnabled && (
+ {(twoFactorTOTP || showEmailTwoFactor) && twoFactorEnabled && (
{twoFactorTOTP && }
- {twoFactorByEmailEnabled && }
+ {showEmailTwoFactor && }
)}
{e2eEnabled && (
diff --git a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx
index c890dc61e658..3654d17b5dec 100644
--- a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx
+++ b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx
@@ -1,11 +1,11 @@
import { Box, Button, Margins } from '@rocket.chat/fuselage';
import { useUser, useTranslation } from '@rocket.chat/ui-contexts';
-import type { ComponentProps, ReactElement } from 'react';
+import type { ComponentProps } from 'react';
import React, { useCallback } from 'react';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
-const TwoFactorEmail = (props: ComponentProps): ReactElement => {
+const TwoFactorEmail = (props: ComponentProps) => {
const t = useTranslation();
const user = useUser();
diff --git a/apps/meteor/server/settings/accounts.ts b/apps/meteor/server/settings/accounts.ts
index a744c47b2a41..b4da1cd913e9 100644
--- a/apps/meteor/server/settings/accounts.ts
+++ b/apps/meteor/server/settings/accounts.ts
@@ -31,6 +31,18 @@ export const createAccountSettings = () =>
public: true,
});
+ await this.add('Accounts_twoFactorAuthentication_email_available_for_OAuth_users', true, {
+ type: 'boolean',
+ enableQuery: [
+ enable2FA,
+ {
+ _id: 'Accounts_TwoFactorAuthentication_By_Email_Enabled',
+ value: true,
+ },
+ ],
+ public: true,
+ });
+
await this.add('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In', true, {
type: 'boolean',
enableQuery: [
diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts
index fa411c6f7e47..d6854bef7243 100644
--- a/packages/core-typings/src/IUser.ts
+++ b/packages/core-typings/src/IUser.ts
@@ -45,7 +45,35 @@ export type ILoginUsername =
};
export type LoginUsername = string | ILoginUsername;
-export interface IUserServices {
+export interface IOAuthUserServices {
+ google?: any;
+ facebook?: any;
+ github?: any;
+ linkedin?: any;
+ twitter?: any;
+ gitlab?: any;
+ saml?: {
+ inResponseTo?: string;
+ provider?: string;
+ idp?: string;
+ idpSession?: string;
+ nameID?: string;
+ };
+ ldap?: {
+ id: string;
+ idAttribute?: string;
+ };
+ nextcloud?: {
+ accessToken: string;
+ refreshToken: string;
+ serverURL: string;
+ };
+ dolphin?: {
+ NickName?: string;
+ };
+}
+
+export interface IUserServices extends IOAuthUserServices {
password?: {
exists?: boolean;
bcrypt?: string;
@@ -62,12 +90,6 @@ export interface IUserServices {
refreshToken: string;
expiresAt: Date;
};
- google?: any;
- facebook?: any;
- github?: any;
- linkedin?: any;
- twitter?: any;
- gitlab?: any;
totp?: {
enabled: boolean;
hashedBackup: string[];
@@ -79,27 +101,37 @@ export interface IUserServices {
changedAt: Date;
};
emailCode?: IUserEmailCode;
- saml?: {
- inResponseTo?: string;
- provider?: string;
- idp?: string;
- idpSession?: string;
- nameID?: string;
- };
- ldap?: {
- id: string;
- idAttribute?: string;
- };
- nextcloud?: {
- accessToken: string;
- refreshToken: string;
- serverURL: string;
- };
- dolphin?: {
- NickName?: string;
- };
}
+type IUserService = keyof IUserServices;
+type IOAuthService = keyof IOAuthUserServices;
+
+const defaultOAuthKeys = [
+ 'google',
+ 'dolphin',
+ 'facebook',
+ 'github',
+ 'gitlab',
+ 'google',
+ 'ldap',
+ 'linkedin',
+ 'nextcloud',
+ 'saml',
+ 'twitter',
+] as IOAuthService[];
+const userServiceKeys = ['emailCode', 'email2fa', 'totp', 'resume', 'password', 'passwordHistory', 'cloud', 'email'] as IUserService[];
+
+export const isUserServiceKey = (key: string): key is IUserService =>
+ userServiceKeys.includes(key as IUserService) || defaultOAuthKeys.includes(key as IOAuthService);
+
+export const isDefaultOAuthUser = (user: IUser): boolean =>
+ !!user.services && Object.keys(user.services).some((key) => defaultOAuthKeys.includes(key as IOAuthService));
+
+export const isCustomOAuthUser = (user: IUser): boolean =>
+ !!user.services && Object.keys(user.services).some((key) => !isUserServiceKey(key));
+
+export const isOAuthUser = (user: IUser): boolean => isDefaultOAuthUser(user) || isCustomOAuthUser(user);
+
export interface IUserEmail {
address: string;
verified?: boolean;
@@ -183,6 +215,7 @@ export interface IUser extends IRocketChatRecord {
_pendingAvatarUrl?: string;
requirePasswordChange?: boolean;
requirePasswordChangeReason?: string;
+ isOAuthUser?: boolean; // client only field
}
export interface IRegisterUser extends IUser {
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index d3324c6e749a..e6867528ad4c 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -282,6 +282,8 @@
"Accounts_TwoFactorAuthentication_By_Email_Code_Expiration": "Time to expire the code sent via email in seconds",
"Accounts_TwoFactorAuthentication_By_Email_Enabled": "Enable Two Factor Authentication via Email",
"Accounts_TwoFactorAuthentication_By_Email_Enabled_Description": "Users with email verified and the option enabled in their profile page will receive an email with a temporary code to authorize certain actions like login, save the profile, etc.",
+ "Accounts_twoFactorAuthentication_email_available_for_OAuth_users": "Make two factor via email available for oAuth users",
+ "Accounts_twoFactorAuthentication_email_available_for_OAuth_users_Description": "People that use oAuth will receive an email with a temporary code to authorize actions like login, save profile, etc.",
"Accounts_TwoFactorAuthentication_Enabled": "Enable Two Factor Authentication",
"Accounts_TwoFactorAuthentication_Enabled_Description": "If deactivated, this setting will deactivate all Two Factor Authentication. \nTo force users to use Two Factor Authentication, the admin has to configure the 'user' role to enforce it.",
"Accounts_TwoFactorAuthentication_Enforce_Password_Fallback": "Enforce password fallback",
From 6730a3c9117fbb196ba36d5f1e10c752bd882112 Mon Sep 17 00:00:00 2001
From: Guilherme Gazzo
Date: Tue, 10 Sep 2024 21:19:10 -0300
Subject: [PATCH 7/7] chore: fix ui-playground build (#33250)
---
packages/password-policies/tsconfig.json | 3 +--
packages/uikit-playground/vite.config.ts | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/packages/password-policies/tsconfig.json b/packages/password-policies/tsconfig.json
index f0a66c843c50..e2be47cf5499 100644
--- a/packages/password-policies/tsconfig.json
+++ b/packages/password-policies/tsconfig.json
@@ -2,8 +2,7 @@
"extends": "../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "./src",
- "outDir": "./dist",
- "module": "commonjs"
+ "outDir": "./dist"
},
"include": ["./src/**/*"]
}
diff --git a/packages/uikit-playground/vite.config.ts b/packages/uikit-playground/vite.config.ts
index a18e01b590e8..61a5ab30e647 100644
--- a/packages/uikit-playground/vite.config.ts
+++ b/packages/uikit-playground/vite.config.ts
@@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig(() => ({
- logLevel: 'info',
+ base: './',
esbuild: {},
plugins: [react()],
optimizeDeps: {