diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e0a12e750777..6a1dfc163936 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -9866,6 +9866,9 @@ const CONST = { FEATURE_LIST: { CTA_BUTTON: 'WorkspaceFeatureList-CtaButton', }, + ROOMS: { + CREATE_ROOM_BUTTON: 'WorkspaceRooms-CreateRoomButton', + }, }, ACCOUNT_SWITCHER: { SHOW_ACCOUNTS: 'AccountSwitcher-ShowAccounts', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ff79ebaa9adf..9b06444c0638 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -743,6 +743,9 @@ const ONYXKEYS = { /** The transaction IDs to be highlighted when opening the Expenses search route page */ TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE: 'transactionIdsHighlightOnSearchRoute', + /** The report ID to be highlighted when returning to the workspace rooms page */ + ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE: 'roomIDHighlightOnRoomsPage', + /** The preferred policy ID to be used when creating a group */ DOMAIN_GROUP_CREATE_PREFERRED_POLICY_ID: 'domainGroupCreatePreferredPolicyID', @@ -1652,6 +1655,7 @@ type OnyxValuesMapping = { [ONYXKEYS.HAS_DENIED_CONTACT_IMPORT_PROMPT]: boolean | undefined; [ONYXKEYS.PERSONAL_POLICY_ID]: string; [ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE]: Record>; + [ONYXKEYS.ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE]: string | null; [ONYXKEYS.DOMAIN_GROUP_CREATE_PREFERRED_POLICY_ID]: string | undefined; }; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2cb9d3cb3d1b..a852b3e6df45 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2388,6 +2388,10 @@ const ROUTES = { return `workspaces/${policyID}/rooms` as const; }, }, + WORKSPACE_ROOM_CREATE: { + route: 'workspaces/:policyID/rooms/new', + getRoute: (policyID: string) => `workspaces/${policyID}/rooms/new` as const, + }, WORKSPACE_MEMBERS_IMPORT: { route: 'workspaces/:policyID/members/import', getRoute: (policyID: string) => `workspaces/${policyID}/members/import` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c541dd306aaa..e25654f2ce16 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -763,6 +763,7 @@ const SCREENS = { INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website', MEMBERS: 'Workspace_Members', ROOMS: 'Workspace_Rooms', + ROOM_CREATE: 'Workspace_Room_Create', MEMBERS_IMPORT: 'Members_Import', MEMBERS_IMPORTED: 'Members_Imported', MEMBERS_IMPORTED_CONFIRMATION: 'Members_Imported_Confirmation', diff --git a/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx b/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx index 8d6a2254d307..60b3b1d01d69 100644 --- a/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx +++ b/src/components/Tables/WorkspaceRoomsTable/WorkspaceRoomsTableRow.tsx @@ -46,9 +46,12 @@ type WorkspaceRoomsTableRowProps = { /** Whether to use narrow table row layout */ shouldUseNarrowTableLayout: boolean; + + /** Whether or not the row should animate in highlighted */ + shouldAnimateInHighlight?: boolean; }; -function WorkspaceRoomsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: WorkspaceRoomsTableRowProps) { +function WorkspaceRoomsTableRow({item, rowIndex, shouldUseNarrowTableLayout, shouldAnimateInHighlight}: WorkspaceRoomsTableRowProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -64,6 +67,7 @@ function WorkspaceRoomsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: Wo accessibilityLabel={item.name} skeletonReasonAttributes={{context: 'WorkspaceRoomsTableRow'}} onPress={item.action} + shouldAnimateInHighlight={shouldAnimateInHighlight} > {({hovered}) => ( <> diff --git a/src/components/Tables/WorkspaceRoomsTable/index.tsx b/src/components/Tables/WorkspaceRoomsTable/index.tsx index e8c3108a53ae..f47a5cf27534 100644 --- a/src/components/Tables/WorkspaceRoomsTable/index.tsx +++ b/src/components/Tables/WorkspaceRoomsTable/index.tsx @@ -1,6 +1,6 @@ import type {ListRenderItemInfo} from '@shopify/flash-list'; -import React from 'react'; -import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table'; +import React, {useEffect, useRef} from 'react'; +import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn, TableHandle} from '@components/Table'; import Table from '@components/Table'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -14,13 +14,28 @@ type WorkspaceRoomsTableColumnKey = 'name' | 'createdBy' | 'members' | 'actions' type WorkspaceRoomsTableProps = { /** Pre-built row data for each room */ rooms: WorkspaceRoomRowData[]; + + /** The reportID of the room that should play the highlight animation (e.g. when it was just created) */ + highlightedReportID?: string; }; -function WorkspaceRoomsTable({rooms}: WorkspaceRoomsTableProps) { +function WorkspaceRoomsTable({rooms, highlightedReportID}: WorkspaceRoomsTableProps) { const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const shouldUseNarrowTableLayout = shouldUseNarrowLayout || isMediumScreenWidth; + const tableRef = useRef>(null); + + useEffect(() => { + if (!highlightedReportID) { + return; + } + const highlightedRoom = rooms.find((room) => room.reportID === highlightedReportID); + if (!highlightedRoom) { + return; + } + tableRef.current?.scrollToItem({item: highlightedRoom, animated: false}); + }, [highlightedReportID, rooms]); const columns: Array> = [ {key: 'name', label: translate('common.name'), sortable: true}, @@ -50,11 +65,13 @@ function WorkspaceRoomsTable({rooms}: WorkspaceRoomsTableProps) { item={item} rowIndex={index} shouldUseNarrowTableLayout={shouldUseNarrowTableLayout} + shouldAnimateInHighlight={!!highlightedReportID && item.reportID === highlightedReportID} /> ); return ( require('../../../../pages/workspace/downgrade/WorkspaceDowngradePage').default, [SCREENS.WORKSPACE.PAY_AND_DOWNGRADE]: () => require('../../../../pages/workspace/downgrade/PayAndDowngradePage').default, [SCREENS.WORKSPACE.MEMBER_DETAILS]: () => require('../../../../pages/workspace/members/WorkspaceMemberDetailsPage').default, + [SCREENS.WORKSPACE.ROOM_CREATE]: () => require('../../../../pages/workspace/rooms/WorkspaceRoomCreatePage').default, [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE]: () => require('../../../../pages/workspace/members/WorkspaceMemberDetailsRolePage').default, [SCREENS.WORKSPACE.MEMBER_CUSTOM_FIELD]: () => require('../../../../pages/workspace/members/WorkspaceMemberCustomFieldPage').default, [SCREENS.WORKSPACE.OWNER_CHANGE_CHECK]: () => require('@pages/workspace/members/WorkspaceOwnerChangeWrapperPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 9592240bad4e..0ad3c0e0b026 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -26,7 +26,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.MEMBER_DETAILS]: { path: ROUTES.WORKSPACE_MEMBER_DETAILS.route, }, + [SCREENS.WORKSPACE.ROOM_CREATE]: { + path: ROUTES.WORKSPACE_ROOM_CREATE.route, + }, [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE]: { path: ROUTES.WORKSPACE_MEMBER_DETAILS_ROLE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index dcd5071a87e0..63ec857f110c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -669,6 +669,9 @@ type SettingsNavigatorParamList = { policyID: string; accountID: string; }; + [SCREENS.WORKSPACE.ROOM_CREATE]: { + policyID: string; + }; [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE]: { policyID: string; accountID: string; diff --git a/src/libs/actions/Policy/Room.ts b/src/libs/actions/Policy/Room.ts index f1d09251713f..f7db5b14f0f5 100644 --- a/src/libs/actions/Policy/Room.ts +++ b/src/libs/actions/Policy/Room.ts @@ -1,6 +1,8 @@ +import Onyx from 'react-native-onyx'; import {read} from '@libs/API'; import type {OpenPolicyRoomsPageParams} from '@libs/API/parameters'; import {READ_COMMANDS} from '@libs/API/types'; +import ONYXKEYS from '@src/ONYXKEYS'; function openPolicyRoomsPage(policyID: string) { const params: OpenPolicyRoomsPageParams = {policyID}; @@ -8,5 +10,12 @@ function openPolicyRoomsPage(policyID: string) { read(READ_COMMANDS.OPEN_POLICY_ROOMS_PAGE, params); } -// eslint-disable-next-line import/prefer-default-export -export {openPolicyRoomsPage}; +function setRoomIDToHighlightOnRoomsPage(reportID: string) { + Onyx.set(ONYXKEYS.ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE, reportID); +} + +function clearRoomIDToHighlightOnRoomsPage() { + Onyx.set(ONYXKEYS.ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE, null); +} + +export {openPolicyRoomsPage, setRoomIDToHighlightOnRoomsPage, clearRoomIDToHighlightOnRoomsPage}; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 297fee12b394..b0a1978f8004 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4329,7 +4329,6 @@ function addPolicyReport(policyReport: OptimisticChatReport) { }; API.write(WRITE_COMMANDS.ADD_WORKSPACE_ROOM, parameters, {optimisticData, successData, failureData}); - Navigation.dismissModalWithReport({reportID: policyReport.reportID}); } /** Deletes a report, along with its reportActions, any linked reports, and any linked IOU report. */ diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 210b8bd7e814..2368089978c0 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -9,6 +9,8 @@ import Button from '@components/Button'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RoomNameInput from '@components/RoomNameInput'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -18,8 +20,10 @@ import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddi import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; +import {clearRoomIDToHighlightOnRoomsPage, setRoomIDToHighlightOnRoomsPage} from '@libs/actions/Policy/Room'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getActivePolicies} from '@libs/PolicyUtils'; @@ -68,9 +72,14 @@ type WorkspaceNewRoomPageRef = { type WorkspaceNewRoomPageProps = { /** Forwarded ref to pass to the room name input */ ref?: Ref; + + /** When provided, the workspace picker is hidden and the room is created under this policy */ + policyID?: string; }; -function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { +function WorkspaceNewRoomPage({ref, policyID: lockedPolicyID}: WorkspaceNewRoomPageProps) { + const isLocked = !!lockedPolicyID; + const lockedPolicy = usePolicy(lockedPolicyID); const styles = useThemeStyles(); const isFocused = useIsFocused(); const {translate, localeCompare} = useLocalize(); @@ -103,6 +112,9 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { [policies, session?.email, localeCompare], ); const [policyID, setPolicyID] = useState(() => { + if (lockedPolicyID) { + return lockedPolicyID; + } if (!!activePolicyID && workspaceOptions.some((option) => option.value === activePolicyID)) { return activePolicyID; } @@ -120,12 +132,10 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { * @param values - form input values passed by the Form component */ const submit = (values: FormOnyxValues) => { - setNewRoomFormLoading(); const currentUserAccountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID; - const participants = [currentUserAccountID]; const parsedDescription = getParsedComment(values.reportDescription ?? '', {policyID}); const policyReport = buildOptimisticChatReport({ - participantList: participants, + participantList: [currentUserAccountID], reportName: values.roomName, chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, policyID, @@ -137,9 +147,22 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { currentUserAccountID, }); + if (isLocked) { + addPolicyReport(policyReport); + // Mark the new room so its row highlights on the rooms page, then clear it once the back transition ends so it doesn't replay later. + setRoomIDToHighlightOnRoomsPage(policyReport.reportID); + Navigation.goBack(ROUTES.WORKSPACE_ROOMS.getRoute(policyID), { + afterTransition: () => clearRoomIDToHighlightOnRoomsPage(), + waitForTransition: true, + }); + return; + } + + setNewRoomFormLoading(); InteractionManager.runAfterInteractions(() => { requestAnimationFrame(() => { addPolicyReport(policyReport); + Navigation.dismissModalWithReport({reportID: policyReport.reportID}); }); }); }; @@ -154,6 +177,9 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { }, [isFocused]); useEffect(() => { + if (isLocked) { + return; + } if (policyID) { if (!workspaceOptions.some((opt) => opt.value === policyID)) { setPolicyID(''); @@ -165,7 +191,7 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { } else { setPolicyID(''); } - }, [activePolicyID, policyID, workspaceOptions]); + }, [activePolicyID, isLocked, policyID, workspaceOptions]); useEffect(() => { if (isAdminPolicy) { @@ -186,6 +212,7 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { } const errors: {policyID?: string; roomName?: string} = {}; + const validationPolicyID = isLocked ? policyID : values.policyID; if (!values.roomName || values.roomName === CONST.POLICY.ROOM_PREFIX) { // We error if the user doesn't enter a room name or left blank @@ -196,7 +223,7 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { } else if (isReservedRoomName(values.roomName)) { // Certain names are reserved for default rooms and should not be used for policy rooms. addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameReservedError', values.roomName)); - } else if (isExistingRoomName(values.roomName, reports, values.policyID)) { + } else if (isExistingRoomName(values.roomName, reports, validationPolicyID)) { // Certain names are reserved for default rooms and should not be used for policy rooms. addErrorMessage(errors, 'roomName', translate('newRoomPage.roomAlreadyExistsError')); } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) { @@ -208,13 +235,13 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { addErrorMessage(errors, 'reportDescription', translate('common.error.characterLimitExceedCounter', descriptionLength, CONST.REPORT_DESCRIPTION.MAX_LENGTH)); } - if (!values.policyID) { + if (!isLocked && !values.policyID) { errors.policyID = translate('newRoomPage.pleaseSelectWorkspace'); } return errors; }, - [reports, policyID, translate, shouldEnableValidation], + [reports, policyID, translate, shouldEnableValidation, isLocked], ); const writeCapabilityOptions = useMemo( @@ -240,17 +267,32 @@ function WorkspaceNewRoomPage({ref}: WorkspaceNewRoomPageProps) { return ( roomPageInputRef.current?.focus(), + shouldEnableMaxHeight: true, + shouldEnableKeyboardAvoidingView: true, + includeSafeAreaPaddingBottom: true, + } + : { + enableEdgeToEdgeBottomSafeAreaPadding: true, + includePaddingTop: false, + shouldShowOfflineIndicator: true, + shouldEnablePickerAvoiding: false, + shouldEnableKeyboardAvoidingView: workspaceOptions.length !== 0, + keyboardVerticalOffset: variables.contentHeaderHeight + variables.tabSelectorButtonHeight + variables.tabSelectorButtonPadding + top, + // Disable the focus trap of this page to activate the parent focus trap in `NewChatSelectorPage`. + focusTrapSettings: {active: false}, + })} testID="WorkspaceNewRoomPage" > - {workspaceOptions.length === 0 ? ( + {isLocked && ( + Navigation.goBack(ROUTES.WORKSPACE_ROOMS.getRoute(policyID))} + /> + )} + {workspaceOptions.length === 0 && !isLocked ? ( ) : ( - - setPolicyID(value as typeof policyID)} - /> - + {isLocked ? ( + + + + ) : ( + + setPolicyID(value as typeof policyID)} + /> + + )} {isAdminPolicy && ( ; + +function WorkspaceRoomCreatePage({route}: WorkspaceRoomCreatePageProps) { + const {isBetaEnabled} = usePermissions(); + return ( + + + + ); +} + +export default WorkspaceRoomCreatePage; diff --git a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx index 45e451170ce6..b4b1b912f814 100644 --- a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx +++ b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx @@ -56,6 +56,11 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { [policyID, archivedReportsIdSet], ); + // The newly created room reportID is stored in Onyx right before navigating back here so its row can play the highlight animation. + // It is cleared by the create page once the navigation transition ends (see WorkspaceNewRoomPage), so the animation doesn't replay on a later visit. + const [roomIDToHighlight] = useOnyx(ONYXKEYS.ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE); + const highlightedReportID = roomIDToHighlight ?? undefined; + const rooms: WorkspaceRoomRowData[] = (policyReports ?? []).map((report) => { const ownerDetails = report.ownerAccountID ? personalDetails?.[report.ownerAccountID] : undefined; return { @@ -96,8 +101,7 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { {!shouldUseNarrowLayout && (